前言
vue3项目,在选择UI框架时,我比较倾向于Naive UI,因为它的文档很有意思,且无需导入less、sass等样式插件,即可实现主题切换功能,重点是vue3官方推荐它,若大家感兴趣的话,可以查看ant design vue VS native UI以及它们npm包的下载数量
城市下拉组件最终效果,如下图所示:
选择框
我们先实现下拉选择框,代码如下所示:
<template>
<div class="select-city-container">
<n-popover :show="popoverVisible" class="go-input-select-city" trigger="click" placement="bottom-start">
<template #trigger>
<n-button icon-placement="right" @click="changeVisible">
{{ selectedItem.name }}
<template #icon>
<n-icon>
<arrow-up-icon v-if="popoverVisible"></arrow-up-icon>
<arrow-down-icon v-else></arrow-down-icon>
</n-icon>
</template>
</n-button>
</template>
</n-popover>
</div>
</template>
<script setup lang="ts">
export type CityAreaItem = {
name: string,
adcode: string
}
const popoverVisible = ref(false)
const selectedItem = reactive<CityAreaItem>({
name: '请选择城市',
adcode: ''
})
const changeVisible = () => {
popoverVisible.value = !popoverVisible.value
}
ArrowUpIcon和ArrowDownIcon这俩图标,引自另一组件库@vicons/ionicons5
注意:这里需要注意的是
n-popover必须用div包裹,若它为根节点,则会抛出警告信息Runtime directive used on component with non-element root node
初步效果如下图所示:
初始化省份城市数据
省市数据,我分别存放在provinces.json和city.json内,下拉界面默认显示省份数据,部分代码如下所示:
<template>
<div class="select-city-container">
<n-popover :show="popoverVisible" class="go-input-select-city" trigger="click" placement="bottom-start">
<template #trigger>
<n-button icon-placement="right" @click="changeVisible">
{{ selectedItem.name }}
<template #icon>
<n-icon>
<arrow-up-icon v-if="popoverVisible"></arrow-up-icon>
<arrow-down-icon v-else></arrow-down-icon>
</n-icon>
</template>
</n-button>
</template>
<div class="city-select-popover">
<div class="city-groups">
<div wrap-class="scrollbar-wrapper">
<div class="city-group" v-for="cityGroup in currentCityGroups" :key="cityGroup.letter">
<div :ref="el => (scrollbarWrapperRef[cityGroup.letter] = el)" class="letter">{{ cityGroup.letter }}</div>
<template v-if="searchType === 'province'">
<div class="list-container">
<div class="province-list" v-for="(items, key) in cityGroup.cityLists" :key="key">
<div class="province">
<n-button quaternary type="primary" @click="handleProvinceClick(items.province)">{{
getProvinceName(items.province.name)
}}</n-button>
</div>
<div class="city-list">
<span class="city" v-for="item in items.cityList" :key="item.adcode">
<n-button quaternary type="primary" @click="handleCityClick(item)">{{ item.name }}</n-button>
</span>
</div>
</div>
</div>
</template>
<template v-else>
<div class="list-container city-list">
<span class="city" v-for="item in cityGroup.cityList" :key="item.adcode">
<n-button quaternary type="primary" @click="handleCityClick(item)">{{ item.name }}</n-button>
</span>
</div>
</template>
</div>
</div>
</div>
</div>
</n-popover>
</div>
</template>
<script setup lang="ts">
import { cities } from './config'
import provinces from './json/provinces.json'
const scrollbarWrapperRef = ref<any[]>([])
const currentCityGroups = ref<any[]>([])
const searchType = ref('province')
const provinceGroups: any = []
const cityGroupByProvince = () => {
if (provinceGroups.length) return provinceGroups
if (provinceOptions.value?.length && cityOptions.value?.length) {
let provinceOfLetter: any = {}
provinceOptions.value.forEach(provinceOption => {
let letter = provinceOption.letter
let cityLists: any = []
if (provinceOfLetter[letter]) {
cityLists = provinceOfLetter[letter]
} else {
cityLists = []
provinceOfLetter[letter] = cityLists
provinceGroups.push({
letter,
cityLists
})
}
cityLists.push({
province: provinceOption,
cityList: getCityList(provinceOption.adcode)
})
})
provinceGroups.sort((previous: any, next: any) => {
return previous.letter.charCodeAt(0) - next.letter.charCodeAt(0)
})
}
return provinceGroups
}
const cityGroups: any = []
const cityGroupByCity = () => {
if (cityGroups.length) return cityGroups
if (cityOptions.value?.length) {
let cityOfLetter: any = {}
cityOptions.value.forEach(cityOption => {
let letter = cityOption.letter
let cityList: any = []
if (cityOfLetter[letter]) {
cityList = cityOfLetter[letter]
} else {
cityList = []
cityOfLetter[letter] = cityList
cityGroups.push({
letter,
cityList
})
}
cityList.push(cityOption)
})
cityGroups.sort((previous: any, next: any) => {
return previous.letter.charCodeAt(0) - next.letter.charCodeAt(0)
})
}
return cityGroups
}
const initList = () => {
if (searchType.value === 'province') {
currentCityGroups.value = cityGroupByProvince()
} else {
currentCityGroups.value = cityGroupByCity()
}
}
onMounted(() => {
initList()
})
默认显示省份下面的城市数据,若切换到红框内的城市,则按各个城市的首字母进行排序
由于点击字母,需要滑动到对应的区域,故我在代码内定义const scrollbarWrapperRef = ref<any[]>([]),在大写字母模板内,将各个el按照letter索引存储到scrollbarWrapperRef内,<div :ref="el => (scrollbarWrapperRef[cityGroup.letter] = el)"></div>
滚动到对应区域的代码为:
//点击字母
const handleLetterClick = (letter: any) => {
scrollbarWrapperRef.value[letter].scrollIntoView({ behavior: 'smooth', block: 'start' })
}
热门城市
<template>
<div class="select-city-container">
<n-popover :show="popoverVisible" class="go-input-select-city" trigger="click" placement="bottom-start">
<template #trigger>
<n-button icon-placement="right" @click="changeVisible">
{{ selectedItem.name }}
<template #icon>
<n-icon>
<arrow-up-icon v-if="popoverVisible"></arrow-up-icon>
<arrow-down-icon v-else></arrow-down-icon>
</n-icon>
</template>
</n-button>
</template>
<div class="city-select-popover">
<div class="current-city">
当前城市:
<span>{{ selectedItem.name === NOTICE_MSG ? '' : selectedItem.name }}</span>
</div>
<div class="hot-cities">
<div class="hot-city-title">热门城市:</div>
<div style="flex: 1; margin-top: -5px">
<span class="hot-city" v-for="item in hotCities" :key="item.adcode">
<n-button quaternary type="primary" @click="clickHotCity(item)">{{ item.name }}</n-button>
</span>
</div>
</div>
</div>
</n-popover>
</div>
我在city.json内的各个城市数据内,添加hot字段,若hot非0,则表示其为热门城市,并按照hot值大小对热门城市进行排序,代码如下所示:
const hotCities = computed(() => {
let hotCities: any = []
if (cityOptions.value?.length) {
cityOptions.value.forEach(cityOption => {
cityOption.hot = +cityOption.hot
if (cityOption.hot) {
hotCities.push(cityOption)
}
})
}
hotCities.sort((previous: any, next: any) => {
return previous.hot - next.hot
})
return hotCities
})
尾声
借助于Naive UI、Vue3编写组件固然方便,但一旦变换框架,又需要重新编写组件,这极其不便,后期,我打算用webcomponent对其进行重构。若大家觉得有什么好的意见,欢迎在评论区留言~