城市选择-基本功能
能够封装城市选择组件,并且完成基础的显示隐藏的交互功能
(1)封装通用组件src/components/city/index.vue
<script lang="ts" setup name="City"></script> <template> <div class="city"> <div class="select"> <span class="placeholder">请选择配送地址</span> <span class="value"></span> <i class="iconfont icon-angle-down"></i> </div> <div class="option"> <span class="ellipsis" v-for="i in 24" :key="i">北京市</span> </div> </div> </template> <style scoped lang="less"> .city { display: inline-block; position: relative; z-index: 400; .select { border: 1px solid #e4e4e4; height: 30px; padding: 0 5px; line-height: 28px; cursor: pointer; &.active { background: #fff; } .placeholder { color: #999; } .value { color: #666; font-size: 12px; } i { font-size: 12px; margin-left: 5px; } } .option { width: 542px; border: 1px solid #e4e4e4; position: absolute; left: 0; top: 29px; background: #fff; min-height: 30px; line-height: 30px; display: flex; flex-wrap: wrap; padding: 10px; > span { width: 130px; text-align: center; cursor: pointer; border-radius: 4px; padding: 0 3px; &:hover { background: #f5f5f5; } } } } </style>
全局注册
import XtxCity from '@/components/city/index.vue' export default { install(app: App) { app.component('City', City) }, }
提供类型 src/global.d.ts
import XtxCity from '@/components/city/index.vue' declare module 'vue' { City: typeof City } }
(2)在商品详情组件中渲染city组件 src/views/goods/components/goods-name.vue
<dl> <dt>配送</dt> <dd>至 <City></City></dd> </dl>
(3)控制城市的显示和隐藏
- 点击的时候切换省市区的样式
- 切换城市弹层的显示或隐藏
<script lang="ts" setup name="City"> + import { ref } from 'vue' +const active = ref(false) +const toggle = () => { + active.value = !active.value +} </script> <template> <div class="city"> + <div class="select" @click="toggle" :class="{ active: active }"> <span class="placeholder">请选择配送地址</span> <span class="value"></span> <i class="iconfont icon-angle-down"></i> </div> + <div class="option" v-show="active"> <span class="ellipsis" v-for="i in 24" :key="i">北京市</span> </div> </div> </template>
(4)点击弹层外部,关闭弹层
<script lang="ts" setup> import { ref } from 'vue' +import { onClickOutside } from '@vueuse/core' const active = ref(false) const toggle = () => { active.value = !active.value } // 点击外面,关闭弹窗 const target = ref(null) onClickOutside(target, () => { // 参数1:监听哪个元素,target是通过ref绑定给我们监听盒子的 // 参数2:点击了该元素外其他地方触发的函数 active.value = false }) </script> <template> <div class="city" ref="target"> <div class="select" @click="toggle" :class="{ active: active }"> <span class="placeholder">请选择配送地址</span> <span class="value"></span> <i class="iconfont icon-angle-down"></i> </div> <div class="option" v-if="active"> <span class="ellipsis" v-for="i in 24" :key="i">北京市</span> </div> </div> </template>
城市选择-动态渲染
需求:完成城市数据的获取以及渲染
注意
城市数据并不是直接从接口服务器中获取的,而是从阿里云服务器上获取的数据,所以不能使用封装好的request发送请求,直接使用 原生的axios 发送请求即可。https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.jsonopen in new window
(1)定义数据类型
// 城市列表类型 export type AreaList = { code: string level: number name: string areaList: AreaList[] }
(2)获取数据src/components/city/index.vue
需求: 从接口获取城市数据,赋值给本地数据
- 定义列表数据变量
const cityList = ref<AreaList[]>([])
- 封装调接口的方法
- 方法内部调用接口,把获取到的结果赋值给cityList
- 调用方法
<script lang="ts" setup name="City"> import { ref } from 'vue' import { onClickOutside } from '@vueuse/core' import type { AreaList } from '@/types/goods' import axios from 'axios' // 控制弹层的显示隐藏 const active = ref(false) const toggle = () => { active.value = !active.value } const target = ref(null) onClickOutside(target, (e) => { // console.log(e) // 当点击target元素的外面的时候,就会触发 active.value = false }) const cityList = ref<AreaList[]>([]) const getCityList = async () => { const res = await axios.get<AreaList[]>( 'https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json' ) cityList.value = res.data } getCityList() </script>
(3)渲染数据src/components/City/index.vue
<template> <div class="city" ref="target"> <div class="select" :class="{ active: active }" @click="toggle"> <span class="placeholder">请选择配送地址</span> <span class="value"></span> <i class="iconfont icon-angle-down"></i> </div> <div class="option" v-show="active"> <span class="ellipsis" v-for="item in cityList" :key="item.code"> {{ item.name }} </span> </div> </div> </template>
(4)点击弹层外部,关闭弹层
<script lang="ts" setup> import { ref } from 'vue' +import { onClickOutside } from '@vueuse/core' const active = ref(false) const toggle = () => { active.value = !active.value } // 点击外面,关闭弹窗 const target = ref(null) onClickOutside(target, () => { // 参数1:监听哪个元素,target是通过ref绑定给我们监听盒子的 // 参数2:点击了该元素外其他地方触发的函数 active.value = false }) </script> <template> <div class="city" ref="target"> <div class="select" @click="toggle" :class="{ active: active }"> <span class="placeholder">请选择配送地址</span> <span class="value"></span> <i class="iconfont icon-angle-down"></i> </div> <div class="option" v-if="active"> <span class="ellipsis" v-for="i in 24" :key="i">北京市</span> </div> </div> </template>
城市选择-交互逻辑
需求:
- 点击选择省市区的时候能够把省市区数据存储起来
- 根据点击的每一个节点的level决定他应该存储到省/城/区的哪个下面
- 点击的时候,需要把当前弹层城市信息替换为当前点击节点的子节点的城市信息(点击省显示市,点击市显示县)
- 如果点击的是区的节点,则需要关弹层
- 如果中途点击了关闭弹层,则需要重置城市数据
点击某个省,显示省下面的市。点击市,显示市下面的县。
根据level判断级别。
(1)给城市注册点击事件
<div class="option" v-if="active"> <span class="ellipsis" v-for="item in cityList" :key="item.code" + @click="selectCity(item)" > {{ item.name }} </span> </div>
(2)城市切换逻辑
// 选择城市 const changeResult = ref({ provinceCode: '', provinceName: '', cityCode: '', cityName: '', countyCode: '', countyName: '' }) const selectCity = (city: AreaList) => { if (city.level === 0) { // 省 changeResult.value.provinceName = city.name changeResult.value.provinceCode = city.code cityList.value = city.areaList } if (city.level === 1) { // 市 changeResult.value.cityName = city.name changeResult.value.cityCode = city.code cityList.value = city.areaList } if (city.level === 2) { // 县(区) changeResult.value.countyName = city.name changeResult.value.countyCode = city.code // 关闭弹窗 active.value = false } }
(3)关闭时恢复城市数据
const cityList = ref<AreaList[]>([]) const cacheList = ref<AreaList[]>([]) const getCityData = async () => { const {data:res} = await axios.get<AreaListObj[]>('https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json') cityList.value = res cacheList.value = res } getCityData() // 监听关闭弹窗的处理,恢复数据 watch(active, (value) => { // 当关闭active的时候,需要回复数据 if (!value) { cityList.value = cacheList.value } })
城市选择-完整地址处理
需求描述
- 默认展示的配送地址需要从父组件传递过来(登录的用户可以拿到当前用户的默认配送地址)
- 子组件选择完省市区需要传递给父组件,由父组件组织数据传给子组件进行展示(默认展示配送地址的数据源在父组件)
注意:完整地址需要父组件传递给子组件,将来如果登录的用户,父组件可以获取到完整的地址。
(1)父组件将城市数据传递给子组件
<script lang="ts" setup> const userAddress = ref('江西省 九江市 不知道县') </script> <City :userAddress="userAddress"></City></dd>
(2)子组件接收,并且进行展示
注意点:具体的地址和请选择配送同时只展示一个
defineProps<{ userAddress?: string }>() <div class="select" @click="toggle" :class="{ active }"> <span class="value" v-if="userAddress">{{ userAddress }}</span> <span class="placeholder" v-else>请选择配送地址</span> <i class="iconfont icon-angle-down"></i> </div>
(3)子组件选择完城市,需要将数据传递给父组件
// 选择的城市结果类型 export type CityResult = { provinceCode: string provinceName: string cityCode: string cityName: string countyCode: string countyName: string } const changeResult = ref<Partial<CityResult>>({}) const emit = defineEmits<{ (e: 'changeCity', value: CityResult): void }>() const selectCity = (city: AreaList) => { if (city.level === 0) { // 省 changeResult.value.provinceName = city.name changeResult.value.provinceCode = city.code cityList.value = city.areaList } if (city.level === 1) { // 市 changeResult.value.cityName = city.name changeResult.value.cityCode = city.code cityList.value = city.areaList } if (city.level === 2) { // 县(区) changeResult.value.countyName = city.name changeResult.value.countyCode = city.code // 关闭弹窗 active.value = false // 子传父 emit('changeCity', changeResult.value) } }
优化代码(可选)
const changeResult = ref<Partial<CityResult>>({}) // record接受两个泛型参数,第一个为对象key的类型,第二个为对象值的类型 const cityMap: Record<number, 'province' | 'city' | 'county'> = { 0: 'province', 1: 'city', 2: 'county' } const selectedCity = (city: AreaList) => { changeResult.value[`${cityMap[city["level"]]}Name`] = city.name changeResult.value[`${cityMap[city["level"]]}Code`] = city.code if (city.level === 2) { setIsShowCity(false) emits('changeCity', changeResult.value) } else { cityList.value = city.areaList } }
(4)父组件接受数据并且处理
<dl> <dt>配送</dt> <dd> 至 <XtxCity @changeCity="changeCity" :fullPath="fullPath" ></XtxCity> </dd> </dl> const userAddress = ref('江西省 九江市 不知道县') const changeCity = (changeResult: CityResult) => { userAddress.value = changeResult.provinceName + ' ' + changeResult.cityName + ' ' + changeResult.countyName } ', changeResult.value) } else { cityList.value = city.areaList } }
(4)父组件接受数据并且处理
<dl> <dt>配送</dt> <dd> 至 <XtxCity @changeCity="changeCity" :fullPath="fullPath" ></XtxCity> </dd> </dl> const userAddress = ref('江西省 九江市 不知道县') const changeCity = (changeResult: CityResult) => { userAddress.value = changeResult.provinceName + ' ' + changeResult.cityName + ' ' + changeResult.countyName }