必备UI组件
将用到的组件:Popover 气泡卡片Radio 单选框Select 选择器Layout 布局Scrollbar 滚动条
组件设计
首先初始化项目文件结构。
新建src\components\baseline\chooseCity\src\index.vue
<template>
<div>ChooseCity</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>
新建src\components\baseline\chooseCity\index.ts
import { App } from 'vue'
import ChooseCity from './src/index.vue'
export { ChooseCity }
//组件可通过use的形式使用
export default {
ChooseCity,
install(app: App) {
app.component('bs-choose-city', ChooseCity)
},
}
修改src\components\baseline\index.ts
import { App } from 'vue'
import ChooseArea from './chooseArea'
import ChooseIcon from './chooseIcon'
import Container from './container'
import Trend from './trend'
import Notification from './notification'
import List from './list'
import Menu from './menu'
import Progress from './progress'
import ChooseTime from './chooseTime'
import ChooseDate from './chooseDate'
import ChooseCity from './chooseCity'
const components = [
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
]
export {
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
}
//组件可通过use的形式使用
export default {
install(app: App) {
components.map(item => {
app.use(item)
})
},
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
}
增加路由
{
path: '/chooseCity',
component: () =>
import('../views/baseline/chooseCity/index.vue'),
}
新建src\views\baseline\chooseCity\index.vue
<template>
<div>
<bs-choose-city></bs-choose-city>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>
运行如下:
进一步完善
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover placement="bottom-start" :width="200" trigger="click">
<template #reference>
<div class="result">
<div>{{ result }}</div>
<div>
<el-icon-arrow-up />
<el-icon-arrow-down />
</div>
</div>
</template>
<div>内容</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const result = ref<string>('请选择')
</script>
<style lang="scss" scoped>
.bs-wrapper {
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
}
}
</style>
效果如下:
搜索区域
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div>
<el-icon-arrow-up v-if="visible" />
<el-icon-arrow-down v-else />
</div>
</div>
</template>
<div class="container">
<el-row>
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-col>
</el-row>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
const result = ref<string>('请选择')
const visible = ref<boolean>(false) //默认不显示
const radioValue = ref<string>('按城市')
const selectValue = ref<string>('')
const options = reactive([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
{
value: 'Option3',
label: 'Option3',
},
{
value: 'Option4',
label: 'Option4',
},
{
value: 'Option5',
label: 'Option5',
},
])
</script>
<style lang="scss" scoped>
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__text {
margin-right: 0.04rem;
}
}
.container {
padding: 0.08rem;
}
</style>
效果如下:
样式元素开发
首先需要准城市。
基本格式如下:
src\components\baseline\chooseCity\lib\city.ts
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div class="result__icon">
<el-icon-arrow-down :class="{ rolate: visible }" />
</div>
</div>
</template>
<div class="container">
<el-row>
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-col>
</el-row>
<!-- <div v-for="(value, key) in cities"> -->
<div class="container__city">
<div
class="container__city__item"
v-for="(item, index) in Object.keys(cities)"
:key="index"
>
{{ item }}
</div>
</div>
<div class="container__list">
<el-scrollbar max-height="3rem">
<template v-for="(value, key) in cities" :key="key">
<el-row class="container__list__row">
<el-col :span="2">{{ key }}:</el-col>
<el-col :span="22" class="container__list__col">
<div
class="container__list__col__item"
v-for="(item, index) in value"
:key="index"
>
<div>
{{ item.name }}
</div>
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</div>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import city from '../lib/city'
const result = ref<string>('请选择')
const visible = ref<boolean>(false) //默认不显示
const radioValue = ref<string>('按城市')
const selectValue = ref<string>('')
const cities = ref(city.cities)
const options = reactive([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
{
value: 'Option3',
label: 'Option3',
},
{
value: 'Option4',
label: 'Option4',
},
{
value: 'Option5',
label: 'Option5',
},
])
</script>
<style lang="scss" scoped>
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__icon {
.rolate {
transform: rotate(180deg);
-ms-transform: rotate(180deg); /* IE 9 */
-moz-transform: rotate(180deg); /* Firefox 4 */
-webkit-transform: rotate(180deg); /* Safari and Chrome */
-o-transform: rotate(180deg); /* Opera */
}
svg {
position: relative;
top: 2px;
margin-left: 0.04rem;
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
transition: all 0.25s linear;
-moz-transition: all 0.25s linear; /* Firefox 4 */
-webkit-transition: all 0.25s linear; /* Safari 和 Chrome */
-o-transition: all 0.25s linear; /* Opera */
}
}
}
.container {
padding: 0.08rem;
&__city {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
margin-top: 0.1rem;
margin-bottom: 0.1rem;
&__item {
padding: 0.03rem 0.06rem;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
border: 1px solid #eee;
cursor: pointer;
}
}
&__list {
&__row {
margin-bottom: 0.1rem;
}
&__col {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
&__item {
cursor: pointer;
margin-right: 0.06rem;
margin-bottom: 0.06rem;
}
}
}
}
</style>
效果如下:
事件响应
新增src\components\baseline\chooseCity\src\types.ts
/**
* 城市类型
*/
export interface TypeCity {
//id
id: number
//拼音
spell: string
//名称
name: string
}
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div class="result__icon">
<el-icon-arrow-down :class="{ rolate: visible }" />
</div>
</div>
</template>
<div class="container">
<el-row>
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-col>
</el-row>
<!-- <div v-for="(value, key) in cities"> -->
<div class="container__city">
<div
class="container__city__item"
v-for="(item, index) in Object.keys(cities)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__list">
<el-scrollbar max-height="3rem">
<template v-for="(value, key) in cities" :key="key">
<el-row class="container__list__row">
<el-col :span="2">{{ key }}:</el-col>
<el-col :span="22" class="container__list__col">
<div
@click="handleClickItem(item)"
class="container__list__col__item"
v-for="(item, index) in value"
:key="index"
>
<div>
{{ item.name }}
</div>
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</div>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import city from '../lib/city'
import { TypeCity } from './types'
//分发事件
let emits = defineEmits(['change'])
//最终选择的结果
const result = ref<string>('请选择')
//控制弹出层的显示
const visible = ref<boolean>(false) //默认不显示
//单选框的值
const radioValue = ref<string>('按城市')
//下拉框的值
const selectValue = ref<string>('')
//城市信息集
const cities = ref(city.cities)
const options = reactive([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
{
value: 'Option3',
label: 'Option3',
},
{
value: 'Option4',
label: 'Option4',
},
{
value: 'Option5',
label: 'Option5',
},
])
//点击给个城市
const handleClickItem = (item: TypeCity) => {
//给结果赋值
result.value = item.name
//关闭弹出层
visible.value = false
emits('change', item)
}
</script>
<style lang="scss" scoped>
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__icon {
.rolate {
transform: rotate(180deg);
-ms-transform: rotate(180deg); /* IE 9 */
-moz-transform: rotate(180deg); /* Firefox 4 */
-webkit-transform: rotate(180deg); /* Safari and Chrome */
-o-transform: rotate(180deg); /* Opera */
}
svg {
position: relative;
top: 2px;
margin-left: 0.04rem;
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
transition: all 0.25s linear;
-moz-transition: all 0.25s linear; /* Firefox 4 */
-webkit-transition: all 0.25s linear; /* Safari 和 Chrome */
-o-transition: all 0.25s linear; /* Opera */
}
}
}
.container {
padding: 0.08rem;
&__city {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
margin-top: 0.1rem;
margin-bottom: 0.1rem;
&__item {
padding: 0.03rem 0.06rem;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
border: 1px solid #eee;
cursor: pointer;
}
}
&__list {
&__row {
margin-bottom: 0.1rem;
}
&__col {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
&__item {
cursor: pointer;
margin-right: 0.06rem;
margin-bottom: 0.06rem;
}
}
}
}
</style>
效果如下:
点击字母跳转到对应得位置
几种方案:
1、A标签的描点链接
2、ref获取、操作DOM
第一种方案,a标签会改变我们地址栏的地址,地址栏变化,在vue中意味着路由也发生变化。路由变化,如果没有对应名称的组件,就会显示空白页面。
第二种方案,vue3中如果使用ref,就会多定义变量,会增加性能开销。如果dom元素少可以,但此组件dom元素比较多 。
所以以上两种都不可行。
解决方案如下:
const clickChat = (item: string) => {
//el-row赋值自定义属性id
//操作原生dom
let el = document.getElementById(item)
//跳转到当前的dom的位置
el && el.scrollIntoView()
}
完整代码,修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div class="result__icon">
<el-icon-arrow-down :class="{ rolate: visible }" />
</div>
</div>
</template>
<div class="container">
<el-row>
<!-- 单选框 -->
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<!-- 可搜索下拉框 -->
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-col>
</el-row>
<!-- 字母 -->
<div class="container__city">
<div
@click="clickChat(item)"
class="container__city__item"
v-for="(item, index) in Object.keys(cities)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__list">
<el-scrollbar max-height="3rem">
<template v-for="(value, key) in cities" :key="key">
<el-row class="container__list__row" :id="key">
<el-col :span="2">{{ key }}:</el-col>
<el-col :span="22" class="container__list__col">
<div
@click="handleClickItem(item)"
class="container__list__col__item"
:class="{
'container__list__light-blue':
result === item.name,
}"
v-for="(item, index) in value"
:key="index"
>
<div>
{{ item.name }}
</div>
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</div>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import city from '../lib/city'
import { TypeCity } from './types'
//分发事件
let emits = defineEmits(['change'])
//最终选择的结果
const result = ref<string>('请选择')
//控制弹出层的显示
const visible = ref<boolean>(false) //默认不显示
//单选框的值
const radioValue = ref<string>('按城市')
//下拉框的值
const selectValue = ref<string>('')
//城市信息集
const cities = ref(city.cities)
const options = reactive([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
{
value: 'Option3',
label: 'Option3',
},
{
value: 'Option4',
label: 'Option4',
},
{
value: 'Option5',
label: 'Option5',
},
])
//点击给个城市
const handleClickItem = (item: TypeCity) => {
//给结果赋值
result.value = item.name
//关闭弹出层
visible.value = false
emits('change', item)
}
const clickChat = (item: string) => {
//el-row赋值自定义属性id
//操作原生dom
let el = document.getElementById(item)
//跳转到当前的dom的位置
el && el.scrollIntoView()
}
</script>
<style lang="scss" scoped>
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__icon {
.rolate {
transform: rotate(180deg);
-ms-transform: rotate(180deg); /* IE 9 */
-moz-transform: rotate(180deg); /* Firefox 4 */
-webkit-transform: rotate(180deg); /* Safari and Chrome */
-o-transform: rotate(180deg); /* Opera */
}
svg {
position: relative;
top: 2px;
margin-left: 0.04rem;
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
transition: all 0.25s linear;
-moz-transition: all 0.25s linear; /* Firefox 4 */
-webkit-transition: all 0.25s linear; /* Safari 和 Chrome */
-o-transition: all 0.25s linear; /* Opera */
}
}
}
.container {
padding: 0.08rem;
&__city {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
margin-top: 0.1rem;
margin-bottom: 0.1rem;
&__item {
padding: 0.03rem 0.06rem;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
border: 1px solid #eee;
cursor: pointer;
}
}
&__list {
&__row {
margin-bottom: 0.1rem;
}
&__col {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
&__item {
cursor: pointer;
margin-right: 0.06rem;
margin-bottom: 0.06rem;
}
}
&__light-blue {
color: #409eff;
}
}
}
</style>
按省份筛选
这里需要引入另一个配置文件,格式如下:
主要代码如下:
<!-- 按省份排序 -->
<template v-else>
<!-- 字母 -->
<div class="container__province__char">
<div
@click="clickChat(item)"
class="container__province__char__item"
v-for="(item, index) in Object.keys(provinces)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__province__list">
<el-scrollbar max-height="3rem">
<!-- Object.values 会查找其中可遍历属性 -->
<template
v-for="(item, index) in Object.values(
provinces
)"
:key="index"
>
<template
v-for="(item1, index1) in item"
:key="index"
>
<el-row
class="container__province__list__row"
:id="item1?.id"
>
<el-col
:span="3"
class="
container__city__list__col_index
"
>
{{ item1.name }}:
</el-col>
<el-col
:span="21"
class="
container__province__list__col
"
>
<div
@click="handleClickItem(item2)"
class="
container__province__list__col__item
"
:class="{
'container__province__list__light-blue':
result === item2.name,
}"
v-for="(
item2, index2
) in item1.data"
:key="index2"
>
{{ item2.name }}
</div>
</el-col>
</el-row>
</template>
</template>
</el-scrollbar>
</div>
</template>
完整代码如下:
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div class="result__icon">
<el-icon-arrow-down
:class="{ transform_180: visible }"
/>
</div>
</div>
</template>
<div class="container">
<el-row>
<!-- 单选框 -->
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<!-- 可搜索下拉框 -->
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-col>
</el-row>
<template v-if="radioValue === '按城市'">
<!-- 字母 -->
<div class="container__city__char">
<div
@click="clickChat(item)"
class="container__city__char__item"
v-for="(item, index) in Object.keys(cities)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__city__list">
<el-scrollbar max-height="3rem">
<template v-for="(value, key) in cities" :key="key">
<el-row
class="container__city__list__row"
:id="key"
>
<el-col
class="container__city__list__col_index"
:span="2"
>
{{ key }}:
</el-col>
<el-col
:span="22"
class="container__city__list__col"
>
<div
@click="handleClickItem(item)"
class="
container__city__list__col__item
"
:class="{
'container__city__list__light-blue':
result === item.name,
}"
v-for="(item, index) in value"
:key="index"
>
{{ item.name }}
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</div>
</template>
<!-- 按省份排序 -->
<template v-else>
<!-- 字母 -->
<div class="container__province__char">
<div
@click="clickChat(item)"
class="container__province__char__item"
v-for="(item, index) in Object.keys(provinces)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__province__list">
<el-scrollbar max-height="3rem">
<!-- Object.values 会查找其中可遍历属性 -->
<template
v-for="(item, index) in Object.values(
provinces
)"
:key="index"
>
<template
v-for="(item1, index1) in item"
:key="index"
>
<el-row
class="container__province__list__row"
:id="item1?.id"
>
<el-col
:span="3"
class="
container__city__list__col_index
"
>
{{ item1.name }}:
</el-col>
<el-col
:span="21"
class="
container__province__list__col
"
>
<div
@click="handleClickItem(item2)"
class="
container__province__list__col__item
"
:class="{
'container__province__list__light-blue':
result === item2.name,
}"
v-for="(
item2, index2
) in item1.data"
:key="index2"
>
{{ item2.name }}
</div>
</el-col>
</el-row>
</template>
</template>
</el-scrollbar>
</div>
</template>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import city from '../lib/city'
import province from '../lib/province.json'
import { TypeCity } from './types'
//分发事件
let emits = defineEmits(['change'])
//最终选择的结果
const result = ref<string>('请选择')
//控制弹出层的显示
const visible = ref<boolean>(false) //默认不显示
//单选框的值
const radioValue = ref<string>('按城市')
//下拉框的值
const selectValue = ref<string>('')
//字母相关的城市信息集
const cities = ref(city.cities)
//省份相关的城市信息集
const provinces = ref(province)
const options = reactive([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
{
value: 'Option3',
label: 'Option3',
},
{
value: 'Option4',
label: 'Option4',
},
{
value: 'Option5',
label: 'Option5',
},
])
//点击单个城市
const handleClickItem = (item: TypeCity) => {
//给结果赋值
result.value = item.name
//关闭弹出层
visible.value = false
emits('change', item)
}
const clickChat = (item: string) => {
//el-row赋值自定义属性id
//操作原生dom
let el = document.getElementById(item)
//跳转到当前的dom的位置
el && el.scrollIntoView()
}
</script>
<style lang="scss" scoped>
@import '@/style/mixins.scss';
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__icon {
svg {
position: relative;
top: 2px;
margin-left: 0.04rem;
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
transition: all 0.25s linear;
-moz-transition: all 0.25s linear; /* Firefox 4 */
-webkit-transition: all 0.25s linear; /* Safari 和 Chrome */
-o-transition: (all 0.25s linear); /* Opera */
}
}
}
.container {
padding: 0.08rem;
&__province,
&__city {
&__char {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
margin-top: 0.1rem;
margin-bottom: 0.1rem;
&__item {
padding: 0.03rem 0.06rem;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
border: 1px solid #eee;
cursor: pointer;
}
}
&__list {
&__row {
margin-bottom: 0.1rem;
}
&__col_index {
cursor: default;
}
&__col {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
&__item {
cursor: pointer;
margin-right: 0.06rem;
margin-bottom: 0.06rem;
}
}
&__light-blue {
color: #409eff;
}
}
}
}
</style>
这里抽取了一些公共css:
修改src\style\mixins.scss
// 超出长度以省略号显示而不会出现换行
//文字的自动展示
@mixin ellipsis {
//文字显示省略号配置
overflow: hidden; //没有滚动条,超出给定的宽度和高度属性,超出的部分会被隐藏,不占位
white-space: nowrap; //不允许换行
text-overflow: ellipsis; //当文本溢出包含元素时:ellipsis-显示省略符号来代表被修剪的文本。
}
//元素旋转
@mixin transform($iconDeg) {
transform: rotate(#{$iconDeg});
-ms-transform: rotate(#{$iconDeg}); /* IE 9 */
-moz-transform: rotate(#{$iconDeg}); /* Firefox */
-webkit-transform: rotate(#{$iconDeg}); /* Safari 和 Chrome */
-o-transform: rotate(#{$iconDeg}); /* Opera */
}
//镜面对称翻转——横向
@mixin flip-horizontal {
-moz-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
-o-transform: scaleX(-1);
transform: scaleX(-1);
filter: fliph; /*IE*/
}
//镜面对称翻转——纵向
@mixin flip-vertical {
-moz-transform: scaleY(-1);
-webkit-transform: scaleY(-1);
-o-transform: scaleY(-1);
transform: scaleY(-1);
filter: flipv; /*IE*/
}
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
@mixin transition($rotate...) {
transition: rotate(#{rotate});
-ms-transition: rotate(#{rotate}); /* IE 9 */
-moz-transition: rotate(#{rotate}); /* Firefox */
-webkit-transition: rotate(#{rotate}); /* Safari 和 Chrome */
-o-transition: rotate(#{rotate}); /* Opera */
}
// 不允许复制
@mixin user-select {
-moz-user-select: none; /* Firefox私有属性 */
-webkit-user-select: none; /* WebKit内核私有属性 */
-ms-user-select: none; /* IE私有属性(IE10及以后) */
-khtml-user-select: none; /* KHTML内核私有属性 */
-o-user-select: none; /* Opera私有属性 */
user-select: none; /* CSS3属性 */
}
修改src\style\ui.scss
@import './mixins.scss';
//修改组件库内部的样式
//1.需要自定义一个类名空间
//2.浏览器中调试样式
//3.调试好的类名放在这个类名中
//4.在App.vue里面引入这个文件
//5.在组件内需要改样式的元素的父元素加上这个类名
.bk--choose-icon-dialog-body-height {
.el-dialog__body {
height: 5rem;
overflow-y: scroll;
overflow-x: auto;
}
}
.transform_90 {
@include transform(90deg);
}
.transform_180 {
@include transform(180deg);
}
.transform_270 {
@include transform(270deg);
}
效果如下:
使用 filter-method 实现搜索过滤
关键代码如下:
......
<!-- 可搜索下拉框 -->
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
@change="selectChange"
:filter-method="filterMethod"
placeholder="请输入关键字搜索城市"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-col>
......
import { ref, onMounted, watch } from 'vue'
......
//下拉框的数据数组
let options = ref<TypeCity[]>([])
//所有的城市数据
let allCityList = ref<TypeCity[]>([])
onMounted(() => {
//获取下拉框的数据
let values = Object.values(cities.value).flat(2)
options.value = values
allCityList.value = values
})
......
const selectChange = (val: number) => {
let city = allCityList.value.find(item => {
return item.id == val
})!
handleClickItem(city)
}
//自定义下拉框的过滤方法
const filterMethod = (val: string) => {
if (!val || val == '') {
options.value = allCityList.value
} else {
//中文和拼音一起过滤
options.value = allCityList.value.filter(item => {
return item.name.includes(val) || item.spell.includes(val)
})
}
}
......
完整代码如下:
修改src\components\baseline\chooseCity\src\index.vue
<template>
<div class="bs-wrapper">
<el-popover
v-model:visible="visible"
placement="bottom-start"
:width="430"
trigger="click"
>
<template #reference>
<div class="result">
<div class="result__text">{{ result }}</div>
<div class="result__icon">
<el-icon-arrow-down
:class="{ transform_180: visible }"
/>
</div>
</div>
</template>
<div class="container">
<el-row>
<!-- 单选框 -->
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<!-- 可搜索下拉框 -->
<el-col :offset="1" :span="15">
<el-select
v-model="selectValue"
size="small"
filterable
@change="selectChange"
:filter-method="filterMethod"
placeholder="请输入关键字搜索城市"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-col>
</el-row>
<template v-if="radioValue === '按城市'">
<!-- 字母 -->
<div class="container__city__char">
<div
@click="clickChat(item)"
class="container__city__char__item"
v-for="(item, index) in Object.keys(cities)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__city__list">
<el-scrollbar max-height="3rem">
<template v-for="(value, key) in cities" :key="key">
<el-row
class="container__city__list__row"
:id="key"
>
<el-col
class="container__city__list__col_index"
:span="2"
>
{{ key }}:
</el-col>
<el-col
:span="22"
class="container__city__list__col"
>
<div
@click="handleClickItem(item)"
class="
container__city__list__col__item
"
:class="{
'container__city__list__light-blue':
result === item.name,
}"
v-for="(item, index) in value"
:key="index"
>
{{ item.name }}
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</div>
</template>
<!-- 按省份排序 -->
<template v-else>
<!-- 字母 -->
<div class="container__province__char">
<div
@click="clickChat(item)"
class="container__province__char__item"
v-for="(item, index) in Object.keys(provinces)"
:key="index"
>
{{ item }}
</div>
</div>
<!-- 具体的城市 -->
<div class="container__province__list">
<el-scrollbar max-height="3rem">
<!-- Object.values 会查找其中可遍历属性 -->
<template
v-for="(item, index) in Object.values(
provinces
)"
:key="index"
>
<template
v-for="(item1, index1) in item"
:key="index"
>
<el-row
class="container__province__list__row"
:id="item1?.id"
>
<el-col
:span="3"
class="
container__city__list__col_index
"
>
{{ item1.name }}:
</el-col>
<el-col
:span="21"
class="
container__province__list__col
"
>
<div
@click="handleClickItem(item2)"
class="
container__province__list__col__item
"
:class="{
'container__province__list__light-blue':
result === item2.name,
}"
v-for="(
item2, index2
) in item1.data"
:key="index2"
>
{{ item2.name }}
</div>
</el-col>
</el-row>
</template>
</template>
</el-scrollbar>
</div>
</template>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue'
import city from '../lib/city'
import province from '../lib/province.json'
import { TypeCity } from './types'
//分发事件
let emits = defineEmits(['change'])
//最终选择的结果
const result = ref<string>('请选择')
//控制弹出层的显示
const visible = ref<boolean>(false) //默认不显示
//单选框的值
const radioValue = ref<string>('按城市')
//下拉框的值
const selectValue = ref<string>('')
//字母相关的城市信息集
const cities = ref(city.cities)
//省份相关的城市信息集
const provinces = ref(province)
//下拉框的数据数组
let options = ref<TypeCity[]>([])
//所有的城市数据
let allCityList = ref<TypeCity[]>([])
onMounted(() => {
//获取下拉框的数据
let values = Object.values(cities.value).flat(2)
options.value = values
allCityList.value = values
})
//点击单个城市
const handleClickItem = (item: TypeCity) => {
//给结果赋值
result.value = item.name
//关闭弹出层
visible.value = false
emits('change', item)
}
const selectChange = (val: number) => {
let city = allCityList.value.find(item => {
return item.id == val
})!
handleClickItem(city)
}
//自定义下拉框的过滤方法
const filterMethod = (val: string) => {
if (!val || val == '') {
options.value = allCityList.value
} else {
//中文和拼音一起过滤
options.value = allCityList.value.filter(item => {
return item.name.includes(val) || item.spell.includes(val)
})
}
}
const clickChat = (item: string) => {
//el-row赋值自定义属性id
//操作原生dom
let el = document.getElementById(item)
//跳转到当前的dom的位置
el && el.scrollIntoView()
}
</script>
<style lang="scss" scoped>
@import '@/style/mixins.scss';
.result {
display: flex;
cursor: pointer;
align-items: center;
width: fit-content; //向内自适应
&__icon {
svg {
position: relative;
top: 2px;
margin-left: 0.04rem;
// transition 可设置4个参数
// transition: property duration timing-function delay;
// 规定设置过渡效果的 CSS 属性的名称
// 规定完成过渡效果需要多少秒或毫秒
// 规定速度效果的速度曲线
// 定义过渡效果何时开始
transition: all 0.25s linear;
-moz-transition: all 0.25s linear; /* Firefox 4 */
-webkit-transition: all 0.25s linear; /* Safari 和 Chrome */
-o-transition: (all 0.25s linear); /* Opera */
}
}
}
.container {
padding: 0.08rem;
&__province,
&__city {
&__char {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
margin-top: 0.1rem;
margin-bottom: 0.1rem;
&__item {
padding: 0.03rem 0.06rem;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
border: 1px solid #eee;
cursor: pointer;
}
}
&__list {
&__row {
margin-bottom: 0.1rem;
}
&__col_index {
cursor: default;
}
&__col {
display: flex;
align-items: center;
flex-wrap: wrap; // 自动换行
&__item {
cursor: pointer;
margin-right: 0.06rem;
margin-bottom: 0.06rem;
}
}
&__light-blue {
color: #409eff;
}
}
}
}
</style>
当然,可以用拼音和中文搜索还需要更好地人性化完善,这里只是简单实现,效果如下: