【Vue3+Vite+TS】12.0 组件九:城市选择

必备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>

运行如下:


image.png

进一步完善
修改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>

效果如下:


image.png

搜索区域

修改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>

效果如下:


image.png

样式元素开发

首先需要准城市。
基本格式如下:
src\components\baseline\chooseCity\lib\city.ts


image.png

修改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>

效果如下:


image.png

事件响应

新增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>

效果如下:


image.png

点击字母跳转到对应得位置

几种方案:
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>
image.png

按省份筛选

这里需要引入另一个配置文件,格式如下:


image.png

主要代码如下:

                <!-- 按省份排序 -->
                <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);
}

效果如下:


image.png

使用 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>

当然,可以用拼音和中文搜索还需要更好地人性化完善,这里只是简单实现,效果如下:


image.png

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):