主要特点是支持手势滑动,支持判断站点是否在可视范围中,文字竖向布局效果

一、HTML

<view class="routevialist">
  <scroll-view class="schedulelist schedulelist-average{{ViewModel.viaList.length}}" scroll-x scroll-with-animation scroll-left="{{ViewModel.scrollLeft}}">
    <block wx:for="{{ViewModel.viaList}}" wx:key="index">
      <view class="viapoint-right {{viatype+'-viapoint-color'}} {{index == 0?'startpoint':''}} {{index ==ViewModel.viaList.length-1?'endpoint':''}}" wx:key="index" bindtap="onClickSelectVia" data-index="{{index}}" id="{{'viapoint-'+index}}">
        <!-- 起点 -->
        <image src="/busubway/resources/image/vialistimg/start-via.png" class="start-via" wx:if="{{index ==0}}" />
        <!-- 终点 -->
        <image src="/busubway/resources/image/vialistimg/end-via.png" class="end-via" wx:elif="{{index==ViewModel.viaList.length-1}}" />
        <!-- 途经点 -->
        <block wx:else>
          <image src="/busubway/resources/image/vialistimg/checked-arrow.png" class="checked-arrow" wx:if="{{index == ViewModel.viaCheckedIndex}}" />
          <image src="/busubway/resources/image/vialistimg/arrow-right.png" class="arrow-right" wx:else />
        </block>
        <!-- 白色箭头 -->
        <image src="/busubway/resources/image/vialistimg/arrow-down.png" class="arrow-down" wx:if="{{index == ViewModel.viaCheckedIndex}}" />
        <!-- 公交车 -->
        <image src="/busubway/resources/image/vialistimg/bus.png" class="bus" wx:if="{{item.bus}}" />
        <!-- 站点名称 -->
        <view class="viapoint {{index == ViewModel.viaCheckedIndex?'viapoint-checked':''}}">{{item.vianame}}</view>
        <!-- 附近地铁 -->
        <block wx:if="{{item.subway&&item.subway.length}}">
          <view class="subway-tag" wx:for="{{item.subway}}" wx:key="index">{{item}}</view>
        </block>
        <!-- 离我最近 -->
        <view class="distance-tag {{index ==ViewModel.viaList.length-1?'distance-tag-last':''}}" wx:if="{{item.nearest}}">离我最近</view>
      </view>
    </block>
  </scroll-view>
  <view class="auto-scroll-view auto-scroll-left" hidden="{{!ViewModel.scrollLeftShow}}" bindtap="onClickMoveTo">
    <image src="/busubway/resources/image/vialistimg/{{viatype}}-left.png" class="auto-img" />
    <view class="auto-left-text {{viatype+'-color'}}">
      {{ViewModel.viaList[ViewModel.viaCheckedIndex].vianame}}
    </view>
  </view>
  <view class="auto-scroll-view auto-scroll-right" hidden="{{!ViewModel.scrollRightShow}}" bindtap="onClickMoveTo">
    <image src="/busubway/resources/image/vialistimg/{{viatype}}-right.png" class="auto-img" />
    <view class="auto-left-text {{viatype+'-color'}}">
      {{ViewModel.viaList[ViewModel.viaCheckedIndex].vianame}}
    </view>
  </view>
</view>

二、CSS

.routevialist {
  position: relative;
  padding: 0 30rpx;
}

.schedulelist {
  white-space: nowrap;
  box-sizing: border-box;
}

.viapoint-right {
  padding-top: 150rpx;
  width: 120rpx;
  display: inline-block;
  position: relative;
}

.schedulelist-average6 .viapoint-right {
  width: 130rpx;
}

.schedulelist-average5 .viapoint-right {
  width: 160rpx;
}

.schedulelist-average4 .viapoint-right {
  width: 215rpx;
}

.schedulelist-average3 .viapoint-right {
  width: 325rpx;
}

.schedulelist-average2 .viapoint-right {
  width: 645rpx;
}

.bus-viapoint-color::before {
  content: "";
  position: absolute;
  width: 100%;
  height: 10rpx;
  background-color: #26925e;
  top: 110rpx;
  z-index: 1;
}
.subway-viapoint-color::before {
  content: "";
  position: absolute;
  width: 100%;
  height: 10rpx;
  background-color: #3e85ee;
  top: 110rpx;
  z-index: 1;
}

.schedulelist .startpoint::before {
  left: 20rpx;
}

.schedulelist .endpoint {
  width: 40rpx;
}

.schedulelist .endpoint::before {
  width: 50%;
}

.start-via,
.end-via {
  width: 42rpx;
  height: 42rpx;
  position: absolute;
  top: 96rpx;
  z-index: 10;
}

.arrow-right {
  width: 8rpx;
  height: 10rpx;
  position: absolute;
  z-index: 9;
  top: 110rpx;
  left: 18rpx;
}

.checked-arrow {
  width: 48rpx;
  height: 48rpx;
  position: absolute;
  top: 92rpx;
  z-index: 11;
  left: -4rpx;
}

.bus {
  width: 54rpx;
  height: 32rpx;
  position: absolute;
  top: 60rpx;
  z-index: 11;
  left: -4rpx;
}

.arrow-down {
  width: 32rpx;
  height: 30rpx;
  position: absolute;
  top: 0rpx;
  z-index: 11;
  left: 4rpx;
}

.viapoint {
  vertical-align: top;
  font-size: 28rpx;
  letter-spacing: 4rpx;
  writing-mode: vertical-lr;
  writing-mode: tb-lr;
  color: #666666;
  display: inline-block;
  white-space: normal;
  text-align: center;
  margin-bottom: 10rpx;
  /* 设置数字或者字母字符水平展示 */
  text-orientation: upright;
}

.viapoint-checked {
  font-size: 30rpx;
  font-weight: bold;
  color: #333333;
}

.distance-tag {
  vertical-align: top;
  background-color: #e9f2ff;
  border-radius: 4px;
  font-size: 18rpx;
  letter-spacing: 4rpx;
  writing-mode: vertical-lr;
  writing-mode: tb-lr;
  color: #3e85ee;
  width: 30rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  white-space: normal;
  text-align: center;
  margin-bottom: 10rpx;
  /* 设置数字或者字母字符水平展示 */
  text-orientation: upright;
  padding: 8rpx 0;
  position: absolute;
  top: 150rpx;
  left: 44rpx;
}

.distance-tag-last {
  position: relative;
  left: 4rpx;
  top: 0;
}

.subway-tag {
  vertical-align: top;
  font-size: 18rpx;
  letter-spacing: 4rpx;
  writing-mode: vertical-lr;
  writing-mode: tb-lr;
  color: #fff;
  white-space: normal;
  background-color: #3e85ee;
  border-radius: 13px;
  width: 30rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 8rpx 0;
  text-align: center;
  margin-bottom: 10rpx;
  position: relative;
  left: 4rpx;
  /* 设置数字或者字母字符水平展示 */
  text-orientation: upright;
}

/* 设置数字不被拆开换行 */
/* .subway-tag text {
  text-combine-upright: all;
} */

.auto-scroll-view{
  position: absolute;
  top: 150rpx;
  width: 47rpx;
	background-color: #ffffff;
	box-shadow: 0rpx 0rpx 12rpx 0rpx 
		rgba(0, 0, 0, 0.14);
  border-radius: 10rpx;
  z-index: 20;
  display: flex;
  align-items: center;
  flex-direction: column;
  justify-content: center;
  padding: 12rpx 0;
}
.auto-scroll-left{
  left: 30rpx;
}
.auto-scroll-right{
  right: 30rpx;
}
.auto-img{
  width: 17rpx;
  height: 15rpx;
  margin-bottom: 10rpx;
}
.auto-left-text{
  vertical-align: top;
  font-size: 28rpx;
  letter-spacing: 4rpx;
  writing-mode: vertical-lr;
  writing-mode: tb-lr;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  position: relative;
  /* 设置数字或者字母字符水平展示 */
  text-orientation: upright;
}
.bus-color{
  color: #26925e;
}
.subway-color{
  color: #3e85ee;
}

三、JS

// busubway/components/via-list/via-list.js
var app = getApp()
import {
  commonApi
} from "../../../utils/api.js";
var util = require('../../../utils/util.js');
//150为每个站点宽度(120)+外层容器padding(30)
const setingLeft = -app.globalData.screenWidth / 750 * 120;
const setingRight = -app.globalData.screenWidth / 750 * 30;
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    /**
     * 站点类型:bus或者subway
     */
    viatype: {
      type: String,
      // value: "subway",
      value: "bus",
    }
  },
  lifetimes: {
    ready() {
      this.createObserver()
    },
    detached() {
      this.observerDisconnect()
    },
  },
  /**
   * 组件的初始数据
   */
  data: {
    ViewModel: {
      HOST_URL: commonApi.HOST_URL,
      viaCheckedIndex: 8,
      viaList: [{
        vianame: "城子",
        subway: [],
        nearest: "",
        bus: false
      },
                {
                  vianame: "城子北站",
                  subway: ["7号线"],
                  nearest: "离我最近",
                  bus: true
                },
                {
                  vianame: "首钢小区",
                  subway: ['2号线', "7号线"],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "金顶南路",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站",
                  subway: [],
                  nearest: "",
                  bus: true
                },
                {
                  vianame: "地铁苹果园站14",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站15",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站16",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站17",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站18",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站19",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁苹果园站20",
                  subway: [],
                  nearest: "",
                  bus: false
                },
                {
                  vianame: "地铁西二旗站",
                  subway: ['13号线', "昌平线"],
                  nearest: "",
                  bus: false
                }, {
                  vianame: "上地五街",
                  subway: [],
                  nearest: "",
                  bus: true
                }, {
                  vianame: "唐家岭东",
                  subway: [],
                  nearest: "",
                  bus: false
                }, {
                  vianame: "史各庄公交场站",
                  subway: ["7号线"],
                  nearest: "离我最近",
                  bus: false
                }
               ],
      scrollLeft: 0,
      scrollLeftShow: false,
      scrollRightShow: false,
      intersectionObserver: null
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {
    observerDisconnect() {
      var that = this
      that.data.ViewModel.intersectionObserver.disconnect()
      that.setData({
        "ViewModel.intersectionObserver": null
      })
    },
    createObserver() {
      var that = this
      var viaCheckedIndex = that.data.ViewModel.viaCheckedIndex
      var intersectionObserver = that.data.ViewModel.intersectionObserver
      if (intersectionObserver) {
        that.observerDisconnect()
      }
      var callback = (res) => {
        // console.log(res)
        //目标边界
        var boundingClientRect = res.boundingClientRect
        //相交比例
        var intersectionRatio = res.intersectionRatio
        //参照区域的边界
        var relativeRect = res.relativeRect
        if (intersectionRatio == 0) {
          if (boundingClientRect.left + boundingClientRect.width <= relativeRect.left) {
            that.setData({
              "ViewModel.scrollLeftShow": true,
              "ViewModel.scrollRightShow": false,
            })
          } else if (boundingClientRect.left + boundingClientRect.width >= relativeRect.right) {
            that.setData({
              "ViewModel.scrollLeftShow": false,
              "ViewModel.scrollRightShow": true,
            })
          }
        } else {
          that.setData({
            "ViewModel.scrollLeftShow": false,
            "ViewModel.scrollRightShow": false,
          })
        }
      }
      that.moveTo(viaCheckedIndex)
      var observer = that.createIntersectionObserver()
      observer.relativeToViewport({
        left: setingLeft,
        right: setingRight,
        bottom: 0
      }).observe('#viapoint-' + viaCheckedIndex, callback)
      that.setData({
        "ViewModel.intersectionObserver": observer
      })
    },
    onClickMoveTo() {
      this.moveTo(this.data.ViewModel.viaCheckedIndex)
    },
    onClickSelectVia(e) {
      var that = this;
      var index = e.currentTarget.dataset.index
      if (index == that.data.ViewModel.viaCheckedIndex) {
        return
      }
      that.setData({
        "ViewModel.viaCheckedIndex": index
      })
      that.createObserver()
      that.triggerEvent("onViaChange", that.data.ViewModel.viaList[index])
    },
    moveTo: function (index) {
      var that = this;
      const query = wx.createSelectorQuery().in(this);
      query.selectAll('.viapoint-right').boundingClientRect();
      query.exec((res) => {
        var rect = res[0];
        let width = 0;
        // 循环获取计算当前点击的标签项距离左侧的距离
        for (let i = 0; i < index; i++) {
          width += rect[i].width
        }
        // 当大于屏幕一半的宽度则滚动,否则就设置位置为0
        let clientWidth = wx.getSystemInfoSync().windowWidth / 2;
        if (width > clientWidth) {
          that.setData({
            "ViewModel.scrollLeft": width + rect[index].width / 2 - clientWidth
          })
        } else {
          that.setData({
            "ViewModel.scrollLeft": 0
          })
        }
      })
    },
  }
})

四、参考资料

冷知识之CSS篇【文字竖向排列】

IntersectionObserver.observe

完爆scroll事件,交叉观察器 IntersectionObserver 在千万级PV页面中的应用实践

五、效果图

微信小程序实时公交横向站点布局_微信小程序

微信小程序实时公交横向站点布局_微信小程序_02

微信小程序实时公交横向站点布局_微信小程序_03