uni-app城市选择器组件实战:从列表到索引,解决跨端滚动难题

2026-08-22 0 883

最近在弄一个同城业务的小程序,最绕不开的就是城市选择器。一开始以为用picker就可以,但客户要那种带字母索引的侧边栏,还要能搜索。uniapp官方没有现成的,很多开源组件一用就各种兼容问题,尤其App端和微信小程序表现不一致。后来干脆自己写了一个,前前后后踩了不少坑。今天把这个组件一步步拆给大家看,顺便聊聊跨端的那点事。

一、数据准备

城市数据是“城市名+拼音首字母”的数组,大概是这样:

[
    { name: '北京', key: 'B' },
    { name: '上海', key: 'S' }
]

实际项目里,我习惯把数据按首字母分组,弄成一个对象:{ A: [{…}], B: [{…}] },这样前端展示效率更高。数据量就几百个城市,后端一次性拉全量就行。

二、页面骨架

按uniapp惯例,页面就是一个 .vue 文件。先写模板结构。我们需要一个搜索框、一个滚动的列表区域、一个侧边索引条。注意这里没有写任何样式,只关注布局和逻辑。

<template>
    <view class="page">
        <view class="search-bar">
            <input v-model="keyword" placeholder="输入城市名或拼音" />
        </view>

        <scroll-view class="city-list" :scroll-y="true" :scroll-into-view="scrollIntoId" scroll-with-animation>
            <view v-for="group in filteredGroups" :id="'city-' + group.letter" :key="group.letter">
                <view class="letter-header">{{ group.letter }}</view>
                <view v-for="city in group.cities" :key="city.name" @tap="selectCity(city)" class="city-item">
                    {{ city.name }}
                </view>
            </view>
        </scroll-view>

        <view class="index-bar">
            <view v-for="letter in letters" :key="letter" @touchstart="touchStart" @touchmove.stop.prevent="touchMove" @touchend="touchEnd">
                {{ letter }}
            </view>
        </view>
    </view>
</template>

这里用 scroll-view 而不是页面自带的滚动,是为了配合 scroll-into-view 实现点击索引跳转。后面会说到。

三、核心逻辑:侧边栏滑动

侧边索引条是整个组件最容易翻车的地方。思路很简单:手指在索引条上滑动时,根据触摸点的 Y 坐标计算出对应的字母,然后修改 scrollIntoId 让列表滚动到对应城市组。

但是要拿到“索引条”在整个页面上的位置,这需要用到 uni.createSelectorQuery()。有个坑是 App 端返回的像素单位和 H5 端不一样,所以我们还需要用 uni.getSystemInfoSync() 里的 windowWidth 去换算。稳妥起见,我在组件挂载时查询一次:

mounted() {
    const query = uni.createSelectorQuery().in(this);
    query.select('.index-bar').boundingClientRect((rect) => {
        this.barTop = rect.top;
        this.barHeight = rect.height;
    }).exec();
}

然后在 touchmovetouchstart 里统一调用一个更新索引的方法:

methods: {
    touchStart(e) {
        this.updateIndex(e);
    },
    touchMove(e) {
        this.updateIndex(e);
    },
    touchEnd() {
        // 不需要额外处理,也可以做一下高亮回位
    },
    updateIndex(e) {
        const touch = e.touches[0] || e.changedTouches[0];
        const clientY = touch.clientY;
        const dy = clientY - this.barTop;
        let ratio = dy / this.barHeight;
        if (ratio < 0) ratio = 0;
        if (ratio > 0.99) ratio = 0.99;
        const index = Math.floor(ratio * this.letters.length);
        this.scrollIntoId = 'city-' + this.letters[index];
    }
}

这样手指滑动时,列表会跟手。但要注意:有些小程序端 e.touches 可能为空,为了稳妥,我使用了 touch[0] || touch.changedTouches[0] 的写法。

四、搜索过滤逻辑

搜索功能不是难点,关键在于过滤后侧边索引字母也要同步变化。所以我没有直接用源数据,而是写了一个计算属性 filteredGroups

computed: {
    filteredGroups() {
        if (!this.keyword.trim()) {
            return this.cityGroups;
        }
        const kw = this.keyword.toLowerCase();
        const result = [];
        this.cityGroups.forEach(group => {
            const cities = group.cities.filter(city =>
                city.name.includes(this.keyword.trim()) || city.pinyin.includes(kw)
            );
            if (cities.length) {
                result.push({ letter: group.letter, cities });
            }
        });
        return result;
    },
    letters() {
        return this.filteredGroups.map(group => group.letter);
    }
}

这样搜索“上海”之后,索引条就只显示 “S”,列表也自动收缩。如果搜索词没有匹配任何城市,侧边索引条会变成空数组,那就不会出现滚动错乱的问题。

五、封装成组件

如果只在一个页面用,写到页面里就够了。但城市选择器这种场景,很可能在登录页、首页、发布页都要用,所以我还是包装成了组件。组件考虑两点:

  • 支持父组件传入初始城市,比如 value 或者 defaultCity
  • 选择城市后触发事件,把城市对象交给父组件。

在 Vue 3 组合式 API 下面,组件大概是这样:

<template>
    <view class="city-picker">
        <view class="mask" @tap="close"></view>
        <view class="dialog">
            <view class="search-bar">
                <input v-model="keyword" placeholder="输入城市名" />
            </view>
            <scroll-view class="city-list" :scroll-into-view="scrollIntoId">
                <!-- 同前面的列表结构 -->
            </scroll-view>
            <view class="index-bar">...</view>
        </view>
    </view>
</template>

<script setup>
import { ref, computed, watch, nextTick } from 'vue';
const props = defineProps({
    modelValue: String,
    cities: {
        type: Array,
        default: () => []
    }
});
const emit = defineEmits(['update:modelValue', 'change']);
// ...内部逻辑
</script>

然后在父组件中这样用:

<city-picker v-model="city.name" :cities="cityList" @change="onCityChange" />

这样就把组件和业务解耦了。父组件的 city.name 会显示在输入框上,子组件弹出层选择后通过 emit 更新。

六、跨端必须注意的坑

刚才踩了一个大坑,就是 scroll-into-view 的 id 在微信小程序里不能以数字开头,不能包含特殊符号。我之前写的是 id="B",结果怎么都不生效,改成 id="city-B" 就好了。

还有,在 App 端,@touchmove.stop.prevent 有时候不生效,页面还是会跟着滚动。解决的办法是给索引条加上 catchtouchmove?不过 uni-app 里没法直接用 catchtouchmove,只能用 @touchmove.stop。如果仍是滚动穿透,可以给索引条加上 disable-scroll: true?但那个是 scroll-view 的属性,不适用于普通view。实际测试下来,微信小程序和 H5 用 @touchmove.stop.prevent 都可以,App 端比较顽固,我最后是给页面加了超时处理,或者改用小程序的 cover-view?后来发现是项目里没开“renderjs”,导致 App 端 touch 事件和普通 view 滚动冲突。但如果你只是实现城市选择器,可以在选择器弹出的时候给 scroll-view 设置 :scroll-y="false",这样就不会跟索引条抢滚动了。

另外,iPhoneX 底部安全区的问题,索引条最底下的按钮会被 home indicator 挡住,需要在索引条下边加一个 padding-bottom 为 env(safe-area-inset-bottom),这个可以写内联样式吗?按理说我们不写样式,但这里提一下,开发者可以自己加个 class。

七、性能优化

几百个城市用 v-for 渲染完全没问题,但如果做全国县区级选择器,3000多个城市同时渲染,在低端安卓 App 上会明显卡顿。一次性能优化是把列表变成“可视区域渲染”,但是 uni-app 社区没有特别好的通用方案,而且代码复杂度会翻倍。我在做的时候没有直接用虚拟滚动,而是给索引滑动加了一个节流:

function throttle(fn, delay = 30) {
    let timer = null;
    return function(...args) {
        if (timer) return;
        timer = setTimeout(() => {
            fn.apply(this, args);
            timer = null;
        }, delay);
    }
}

updateIndex 用节流包一下,滑动时会减少很多次 scroll-into-view 的赋值,渲染压力降低不少。在真机上滑动特别快时体验会稍微好一些。

八、最终效果与总结

我在微信开发者工具、H5、以及安卓真机上测试了这个小组件。除了 App 端初次渲染稍慢外,其他表现基本一致。现在同城项目的三个页面都复用了这个城市选择器,代码量并没有增加多少,还顺手删掉之前引入的 ui 组件库里的 picker 插件。

其实这种自定义组件,核心就是两点:触摸事件的跨端处理,以及 scroll-view 的 scroll-into-view 控制。如果这两点梳理清楚,再配合 Vue 的响应式数据,做一个城市选择器并不复杂。以后如果想加上“热门城市”或“定位城市”,只需要在数据层多向 filteredGroups 塞一组数据就行,完全不影响索引逻辑。

遇到问题还是要先查官方文档,多写点真机测试,这个组件我前前后后调了一整天,踩了不少冤枉路。分享出来,希望大家可以少走这些坑。

uni-app城市选择器组件实战:从列表到索引,解决跨端滚动难题
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app城市选择器组件实战:从列表到索引,解决跨端滚动难题 https://www.taomawang.com/web/uniapp/2586.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务