模仿微信官方乘车码小程序实现公交站点功能

442 阅读5分钟

最近坐公交发现微信已经有很多小程序支持实时公交了,可以随时查看当前站点最近车辆位置,很是方便。出于前端程序员的本能,遇到一些自己没做过的东西就想学学咋做的。

那么,废话不多说,开始搞起。

IMG_2226.PNG

首先,先看一下这个公交站点列表,这个列表支持滚动,支持点击站点切换,站点左右滑动隐藏后还会出现悬浮按钮,点击可以回到当前站点。

那么,就可以一步步得往下走了:

1、支持滚动,这个可以直接使用微信小程序的scroll-view组件,没什么问题;
2、支持站点切换,其实不就是我们平时常用的step步骤条吗,比如element的el-steps组件。那这边前端的UI设计就可以完全参考element-ui的step组件实现:

image.png

于是我就打开了element-ui的官方文档,打开F12,看一下组件设计,外层是一个flex盒子,里面每个子item(每一步)的flex-shrink:1 最后一个item的flex-shrink:0这样就保证了从起始到终点刚好连成一条线。只不过最后的item没有那条线了。

这个设计一般是针对父容器盒子宽度固定,子item平分的设计。
但是在移动端,站点列表那么多,总宽度肯定不是固定的,但是每条item的宽度是固定的,这样的话在移动端就可以让每个子item的flex-shrink:取固定值就可以了。
<scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}">
        <view class="line-list">
            <view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" wx:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
                <view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
                <view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
                <view class="step-icon-container">
                    <view class="icon-circle"></view>
                    <!-- <view class="icon-circle"></view> -->
                </view>
                <view class="step-line"></view>
                <view class="step-title">{{ item.stationName }}</view>
            </view>
        </view>
    </scroll-view>
.line-scroll{
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    overflow-x: auto;
    .line-list{
    position: relative;
    height: 100%;
    display: flex;
    padding: 10rpx 40rpx 0;
    .line-item{
        position: relative;
        flex: 0 0 150rpx;
        .step-line{
            position: absolute;
            top: 70rpx;
            left: 0;
            width: 100%;
            height: 10rpx;
            background-color: #0CC777;
            &::after{
                position: absolute;
                display: block;
                content: '';
                z-index: 1;
                left: 0%;
                top: 50%;
                width: 6rpx;
                height: 10rpx;
                background: url('https://file.40017.cn/groundtraffic/urpt/bus-path.png') center center no-repeat;
                background-size: 100% 100%;
                transform: translateX(-50%) translateY(-50%);
                animation: infinitescroll 1.5s linear infinite;
            }
        }
        .step-icon-container{
            position: absolute;
            left: -15rpx;
            top: 75rpx;
            z-index: 1;
            padding: 0 5rpx;
            transform: translateY(-50%);
            .icon-circle{
                box-sizing: border-box;
                width: 26rpx;
                height: 26rpx;
                border: 4rpx solid #0CC777;
                border-radius: 50%;
                background-color: #fff;
            }
        }
        &.active{
            .step-icon-container{
                position: absolute;
                left: -27rpx;
                top: 75rpx;
                z-index: 1;
                padding: 0 5rpx;
                transform: translateY(-50%);
                .icon-circle{
                    box-sizing: border-box;
                    width: 50rpx;
                    height: 50rpx;
                    border: none;
                    border-radius: 50%;
                    background:url('https://file.40017.cn/groundtraffic/urpt/station-icon.png') center center no-repeat;
                    background-size: 100% 100%;
                }
            }
            .step-title{
                color: #00C777;
                font-weight: bold;
            }
        }
        .step-title{
            position: absolute;
            top: 100rpx;
            left: -20rpx;
            width: 40rpx;
            height: 310rpx;
            font-size: 30rpx;
            writing-mode: vertical-lr;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .station-top-icon{
            position: absolute;
            top: 20rpx;
            left: -20rpx;
            width: 48rpx;
            height: 39rpx;
            background: url('https://file.40017.cn/groundtraffic/urpt/bus-icon.png') center center no-repeat;
            background-size: 100% 100%;
        }
        .station-center-icon{
            position: absolute;
            top: 20rpx;
            left: 50%;
            transform: translateX(-50%);
            width: 48rpx;
            height: 39rpx;
            background: url('https://file.40017.cn/groundtraffic/urpt/bus-icon.png') center center no-repeat;
            background-size: 100% 100%;
        }
    }
    .line-item:last-child{
        // position: absolute;
        flex: 0 0 1rpx;
        right: 1rpx;
        width: auto;
        .step-line{
            width: 0;
        }
    }
}
}
于是基本的ui就有了

image.png

第二步、考虑如何实现点击每个站点,能够自动滚动到相应位置。

这个就需要了解scroll-view这个组件了,它有一个scroll-left属性,绑定的滚动值。我们只需要点击相应站点,计算出相应的滚动的scrollLeft值,然后赋值给它就可以了。这个好办:

<scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}" scroll-left="{{ scrollLeft }}" bindscroll="handleScroll">
        <view class="line-list">
            <view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" w<!--  -->x:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
                <view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
                <view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
                <view class="step-icon-container">
                    <view class="icon-circle"></view>
                    <!-- <view class="icon-circle"></view> -->
                </view>
                <view class="step-line"></view>
                <view class="step-title">{{ item.stationName }}</view>
            </view>
        </view>
    </scroll-view>

我们绑定scroll-left,然后绑定站点点击事件switchTab,还有绑定scroll-view的bindscroll滚动监听事件。

switchTab(e) {
            let { activeIndex } = this.data
            let { id } = e.currentTarget.dataset
            let { list } = this.properties
            let currentActiveIndex = list.findIndex(item => item.stationId === id)
            this.triggerEvent('inform', {
                type: 'switch',
                id
            })
            this.setData({
                currentActiveIndex
            })
        }

我这里其实是做成了一个组件,这边是向父组件通知切换当前激活的站点下标。其实原理就是算出当前点击的站点的下标。然后改变activeIndex,并且监听每个站点下标的变化,一旦变化,执行setScrollLeft事件:

Component({
    properties: {
        list: {
            type: Array,
            value: []
        },
        activeIndex: {
            type: Number,
            value: 0
        },
        activeStation: {
            type: String,
            value: ''
        },
        rr: {
            type: Number,
            value: 2
        }
    },
    observers: {
        activeIndex(val) {
            this.setScrollLeft(val)
        }
    },
    methods:{
         // 设置滚动值
        setScrollLeft(val){
            let { rr } = this.properties
            this.setData({
                scrollLeft: val < 4 ? 0 : (Math.abs((val - 3)) * 150 / rr)
            })
        },
    }
})

这里要说明下,这个rr是rpx与px直接转换的倍率。这里将rpx转换成px赋值给scroll-left。 这里为什么有val < 4 ? 0 : (Math.abs((val - 3)) * 150 / rr)这个计算公式,是因为我的页面设计站点在可视范围内最多就5个,150是每个站点之间的距离(rpx)。所以下标小于4的站点,它的滚动值其实就是0,没有滚动。

这样就实现了组件列表来回点击切换,自动滚动当相应站点的功能了。
第三步,如何实现当前站点左右滚动隐藏后显示滚动条。

image.png 其实就是监听handscroll事件,计算当前激活的站点的scrollLeft值是否在可视范围内。

wxml
<view class="bus-line">
    <scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}" scroll-left="{{ scrollLeft }}" bindscroll="handleScroll">
        <view class="line-list">
            <view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" w<!--  -->x:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
                <view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
                <view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
                <view class="step-icon-container">
                    <view class="icon-circle"></view>
                    <!-- <view class="icon-circle"></view> -->
                </view>
                <view class="step-line"></view>
                <view class="step-title">{{ item.stationName }}</view>
            </view>
        </view>
    </scroll-view>
    <view class="float-left" wx:if="{{ showLeft }}" catch:tap="goActiveStation">{{ activeStation}}</view>
    <view class="float-right" wx:if="{{ showRight }}"  catch:tap="goActiveStation">{{ activeStation }}</view>
</view>
对应的css
.float-left{
    position: absolute;
    background: #fff;
    top: 105rpx;
    left: 8rpx;
    width: 64rpx;
    line-height: 64rpx;
    padding: 35rpx 0 10rpx;
    border-radius: 8rpx;
    font-size: 30rpx;
    writing-mode: vertical-lr;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 2px 4px 14px 0px rgba(0, 0, 0, 0.2);
    &::after{
        position: absolute;
        content: '';
        width: 16rpx;
        height: 12rpx;
        background: url('https://file.40017.cn/groundtraffic/urpt/to_left.png') center center no-repeat;
        background-size: 100%;
        top: 10rpx;
        left: 24rpx;
    }
}
.float-right{
    position: absolute;
    background: #fff;
    top: 105rpx;
    right: 8rpx;
    width: 64rpx;
    line-height: 64rpx;
    padding: 35rpx 0 10rpx;
    border-radius: 8rpx;
    font-size: 30rpx;
    writing-mode: vertical-lr;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 2px 4px 14px 0px rgba(0, 0, 0, 0.2);
    &::after{
        position: absolute;
        content: '';
        width: 16rpx;
        height: 12rpx;
        background: url('https://file.40017.cn/groundtraffic/urpt/to_right.png') center center no-repeat;
        background-size: 100%;
        top: 10rpx;
        left: 24rpx;
    }
}
对应的js
methods:{
     handleScroll(e) {
            let { scrollLeft } = e.detail
            this.setScrollLeftVal(scrollLeft)
            this.checkVisible(scrollLeft)
     },
  // 保存滚动值
    setScrollLeftVal:debounce(function(val){
        this.setData({
            scrollLeftVal: val
        })
    },500),
    // 检查是否需要隐藏左右浮动站点
    checkVisible(scrollLeft){
        let { rr } = this.properties
        let leftHideDis = (this.data.activeIndex * 150 + 40 + 23) / rr
        let rightHideDis = (this.data.activeIndex * 150 + 40 - 23)
        if (leftHideDis < scrollLeft) {
            this.setData({
                showLeft: true
            })
        } else if (rightHideDis > 750 && rightHideDis - scrollLeft * rr > 750) {
            this.setData({
                showRight: true
            })
        } else {
            this.setData({
                showLeft: false,
                showRight: false
            })
        }
    },
     goActiveStation(){
        let { activeIndex } = this.properties
        this.setScrollLeft(activeIndex)
    },
}

这里前端wxml添加左右浮动的按钮,绝对定位。这里面40是起始站点与终点战距离两边的padding值、23是每个站点的图标的半径。其实就是为了计算刚好隐藏当前站点的scrollLeft边界值。然后切换显示与隐藏。 这样基本上一个站点列表的左右滑动、切换,激活站点的悬浮隐藏功能就都有了。剩下的就是对接接口数据切换相应的map组件轨迹、标记点等等了。

下面是完整的效果图

image.png