vue3.0手写省市区地区联动 详细
Posted 奥特曼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue3.0手写省市区地区联动 详细相关的知识,希望对你有一定的参考价值。
目录
实现效果
因为省市区数据较大 所以数据通过链接引入
https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json
1、注册一个全局组件城市
<template>
<div class="xtx-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>
<script>
export default {
name: 'XtxCity'
}
</script>
<style scoped lang="less">
.xtx-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>
注册全局组件 略
2.使用城市组件
goods-name.vue
<dl>
<dt>配送</dt>
<dd>至 <XtxCity></XtxCity> 城市组件</dd>
</dl>
目前效果
3、城市组件中切换显示隐藏
<template>
<div class="xtx-city">
<div class="select"
+ @click="toggleCity"
+ :class="{active:visible}">
<span class="placeholder">请选择配送地址</span>
<span class="value"></span>
<i class="iconfont icon-angle-down"></i>
</div>
<div class="option"
+ v-show="visible">
<span class="ellipsis" v-for="i in 24" :key="i">北京市</span>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'XtxCity',
setup () {
// 默认不显示
const visible = ref(false)
// 打开城市组件
const openCity = () => {
visible.value = true
}
// 关闭城市组件
const closeCity = () => {
visible.value = false
}
// 切换城市组件开关
const toggleCity = () => {
visible.value ? closeCity() : openCity()
}
return { visible, toggleCity }
}
}
</script>
新增点击请选择配送地址 可切换关闭显示 城市下拉框,为true时显示打开 给类名有一个白色背景
4. 在城市组件外点击关闭城市组件
如何判断是否在元素内点击内 利用vueuse工具库中的onClickOutside 方法
npm i @vueuse/core
// 1. 导入方法
import { onClickOutside } from '@vueuse/core'
setup() {
// 鼠标在目标之外点击,就会执行回调
onClickOutside(监听的目标, (e) => {
// 鼠标在目标之外点击,要做什么?
})
}
使用
<div class="xtx-city" ref="target">
<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
export default {
name: 'XtxCity',
setup () {
// 省略其他 ...
// 点击其他位置隐藏
const target = ref(null)
// 只要点击外部就触发
onClickOutside(target, () => {
closeCity()
})
return { visible, toggleDialog, target }
}
}
</script>
5、 城市组件 获取数据
<template>
// ...
<span class="ellipsis" v-for="item in cityData" :key="item.code"> {{item.name}} </span>
</template>
import axios from 'axios'
const url = 'https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json'
// 获取省市区数据
const getCityData = () => {
return axios({ url })
}
// 城市数据
const cityData = ref([])
const openCity = () => {
visible.value = true
getCityData().then(res => { cityData.value = res.data })
}
注意实在打开时调用 open方法里
目前效果
6、城市组件-逻辑交互
显示省市区文字,让组件能够选择省市区并且反馈给父组件。
初始值
-
用户没有登录:当前商品数据中,后端会传递 userAddresses: null, 此时,我们应该用默认地址:北京市 市辖区 东城区
-
用户已经登录:当前商品数据中,后端会传递 userAddresses: 地址数组,类似如下:
isDefault:1就表示默认地址。此时,我们应显示用户提供的地址。
父传子:从商品详情中获取到fullLocation要给xtx-city来显示
子传父:在xtx-city点选了三个级别的信息,完成了地址的设置,要当前选择的数据传给父组件。
选中省市区之后:要传递4个数据给后端:
-
省code
-
市code
-
地区code;
-
它们组合在一起的文字
7、城市组件-交互-显示默认地址
在父组件goods-name中:
(1) 按接口文档要求准备数据
(2) 从父组件中,分析出当前的地址,传递给子组件显示。
父组件设置 省市区的code数据,对应的文字数据。
goods-name.vue
name: 'GoodName',
props: {
list: { //当前商品信息
type: Object,
default: () => ({})
}
},
<script>
import { ref } from 'vue'
export default {
name: 'GoodName',
props: {
list: {
type: Object,
default: () => ({})
}
},
setup (props) {
// 默认情况
const provinceCode = ref('110000')
const cityCode = ref('119900')
const countyCode = ref('110101')
const fullLocation = ref('北京市 市辖区 东城区')
// 如果有默认地址
if (props.list.userAddresses) {
//找到默认值为1的那一项
const defaultAddr = props.list.userAddresses.find(addr => addr.isDefault === 1)
// 如果找到了就赋值
if (defaultAddr) {
provinceCode.value = defaultAddr.provinceCode
cityCode.value = defaultAddr.cityCode
countyCode.value = defaultAddr.countyCode
fullLocation.value = defaultAddr.fullLocation
}
}
return { fullLocation }
}
}
</script>
传给城市组件
<XtxCity :fullLocation="fullLocation" ></XtxCity>
城市组件接收
props: {
fullLocation: { type: String, default: '' }
},
显示默认地址
<span class="placeholder" v-if="!fullLocation">请选择配送地址</span>
<span class="value" v-else> {{fullLocation}} </span>
8. 城市组件 点选交互 子传父
点击了省-->展示 市列表
点击了市 -->展示 地区列表。
点击了区 --> 关闭弹层,通知父组件
显示的内容与用户的选择直接相关,所以用计算属性来定
xtx-city.vue
<div class="option" v-show="visible">
+ <span @click="changeItem(item)" class="ellipsis"></span>
定义城市组件
// 子组件选中的数据
const changeResult = reactive({
provinceCode: '', // 省code
provinceName: '', // 省 名字
cityCode: '', // 市code
cityName: '', // 市 名字
countyCode: '', // 区 code
countyName: '', // 去 名字
fullLocation: '' // 省区市连起来的名字
})
const changeItem = (item) => {
// 省
if (item.level === 0) { //如果拿到0说明有选中省了
changeResult.provinceName = item.name
changeResult.provinceCode = item.code
}
// 市
if (item.level === 1) { //如果拿到1说明有选中市了
changeResult.cityName = item.name
changeResult.cityCode = item.code
}
// 地区
if (item.level === 2) { //如果拿到2说明有选中区了
changeResult.countyCode = item.code
changeResult.countyName = item.name
changeResult.fullLocation = `${changeResult.provinceName} ${changeResult.cityName} ${changeResult.countyName}`
emit('change', changeResult) //给父组件发送联动数据
closeCity() //选完市以后关闭弹框 代表三个都点击完了
}
}
定义计算属性 拿到当前要渲染的值 例如先渲染省 点击后 渲染市 点击市 渲染区
const curList = computed(() => {
// 省
let curList = cityData.value
// 市
if (changeResult.provinceCode) { // 找到当前code和省中相等的对象 拿到市
curList = curList.find(it => it.code === changeResult.provinceCode).areaList
}
// 区
if (changeResult.cityCode) { // 找到当前code和市中相等的对象 拿到区
curList = curList.find(it => it.code === changeResult.cityCode).areaList
}
return curList
})
return {...省略,curList }
更改遍历数据 遍历curList
<span class="ellipsis" @click="changeItem(item)" v-for="item in curList" :key="item.code"> {{item.name}} </span>
打开组件时清空上一次的选项
// 打开城市组件
const openCity = () => {
visible.value = true
getCityData().then(res => { cityData.value = res.data })
// 清空上次的结果 例如用户点错了 重新点开应该重新选择省
for (const key in changeResult) {
changeResult[key] = ''
}
}
9. 城市组件 点选交互 父接收
<XtxCity :fullLocation="fullLocation" @change="changeCity"></XtxCity>
const changeCity = (result) => {
provinceCode.value = result.provinceCode
cityCode.value = result.cityCode
countyCode.value = result.countyCode
fullLocation.value = result.fullLocation
}
return { fullLocation, changeCity }
10. 给城市组件加缓存
现在的情况是每次点开都会发一次新的ajax,发很多一样的数据浪费了内存资源
解决方案保存到window下 你也可以保存到vuex里
const openCity = () => {
visible.value = true
//如果window下的cityData中有值就从window下拿
if (window.cityData) {
cityData.value = window.cityData
} else {
// 如果没有就在请求数据的时候也给window一份
getCityData().then(res => { cityData.value = res.data; window.cityData = res.data })
}
// 清空上次的结果 例如用户点错了 重新点开应该重新选择省
for (const key in changeResult) {
changeResult[key] = ''
}
}
11.请求城市数据时保存loading效果
当网络较慢时数据拿不到 应该显示一个loading的效果
<div class="option" v-show="visible">
+ <div v-if="loading" class="loading"></div>
+ <template v-else>
<span class="ellipsis"
v-for="item in curList"
:key="item.code">
{{item.name}}
</span>
</template>
</div>
// 补充对应的样式
.loading {
height: 290px;
width: 100%;
background: url('~@/assets/images/loading.gif') no-repeat center;
}
loading 效果改变
const loading = ref(false)
const open = () => {
visible.value = true
// 检查是否在window中有数据
if (window.cityData) {
console.log('有上传保存的数据')
cityData.value = window.cityData
} else {
console.log('没有上传保存的数据,发ajax')
+ loading.value = true // 正在加载
// ajax加载数据
getCityData().then(res => {
cityData.value = res.data
// 向window这个超级对象中存入属性
window.cityData = res.data
+ loading.value = false // 加载完成
})
}
}
小bug 给网络调成慢3g 发现一开始打开页面时 是空的过了一会才显示loading效果
原因是图片也要花时间去请求 也就是说loading效果图的请求时间为2.32s 大小为8.2kb
解决loading的bug 把图片转成base64格式
在vue.config.js中 修改配置项 修改后记得重启服务器
module.exports = {
// 省略其他...
chainWebpack: config => {
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => Object.assign(options, { limit: 10000 })) //小于10kb都转换为base64
}
}
转成base64格式后就不会发请求了
完整代码
goodsName.vue
<template>
<p class="g-name">{{ list.name }}</p>
<p class="g-desc">{{ list.desc }}</p>
<p class="g-price">
<span> {{ list.price }} </span>
<span> {{ list.oldPrice }} </span>
</p>
<div class="g-service">
<dl>
<dt>促销</dt>
<dd>12月好物放送,App领券购买直降120元</dd>
</dl>
<dl>
<dt>配送</dt>
<dd>
至 <XtxCity :fullLocation="fullLocation" @change="changeCity"></XtxCity>
</dd>
</dl>
<dl>
<dt>服务</dt>
<dd>
<span>无忧退货</span>
<span>快速退款</span>
<span>免费包邮</span>
<a href="javascript:;">了解详情</a>
</dd>
</dl>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'GoodName',
props: {
list: {
type: Object,
default: () => ({})
}
},
setup (props) {
// 默认情况
const provinceCode = ref('110000')
const cityCode = ref('119900')
const countyCode = ref('110101')
const fullLocation = ref('北京市 市辖区 东城区')
// 有默认地址
if (props.list.userAddresses) {
const defaultAddr = props.list.userAddresses.find(addr => addr.isDefault === 1)
if (defaultAddr) {
provinceCode.value = defaultAddr.provinceCode
cityCode.value = defaultAddr.cityCode
countyCode.value = defaultAddr.countyCode
fullLocation.value = defaultAddr.fullLocation
}
}
const changeCity = (result) => {
provinceCode.value = result.provinceCode
cityCode.value = result.cityCode
countyCode.value = result.countyCode
fullLocation.value = result.fullLocation
}
return { fullLocation, changeCity }
}
}
</script>
<style lang="less" scoped>
.g-name {
font-size: 22px;
}
.g-desc {
color: #999;
margin-top: 10px;
}
.g-price {
margin-top: 10px;
span {
&::before {
content: "¥";
font-size: 14px;
}
&:first-child {
color: @priceColor;
margin-right: 10px;
font-size: 22px;
}
&:last-child {
color: #999;
text-decoration: line-through;
font-size: 16px;
}
}
}
.g-service {
background: #f5f5f5;
width: 500px;
padding: 20px 10px 0 10px;
margin-top: 10px;
dl {
padding-bottom: 20px;
display: flex;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
color: #666;
&:last-child {
span {
margin-right: 10px;
&::before {
content: "•";
color: @xtxColor;
margin-right: 2px;
}
}
a {
color: @xtxColor;
}
}
}
}
}
</style>
XtxCity.vue
<template>
<div class="xtx-city" ref="target">
<div class="select" @click="toggleCity" :class="{active:visible}">
<span class="placeholder" v-if="!fullLocation">请选择配送地址</span>
<span class="value" v-else> {{fullLocation}} </span>
<i class="iconfont icon-angle-down"></i>
</div>
<div class="option" v-show="visible">
<div v-if="loading" class="loading"></div>
<template v-else>
<span class="ellipsis" @click="changeItem(item)" v-for="item in curList" :key="item.code"> {{item.name}} </span>
</template>
</div>
</div>
</template>
<script>
import { ref, reactive, computed } from 'vue'
import { onClickOutside } from '@vueuse/core'
import axios from 'axios'
export default {
name: 'XtxCity',
props: {
fullLocation: { type: String, default: '' }
},
setup (props, { emit }) {
// 默认不显示
const visible = ref(false)
// 打开城市组件
// loading效果
const loading = ref(false)
const openCity = () => {
loading.value = true
visible.value = true
if (window.cityData) {
cityData.value = window.cityData
} else {
getCityData().then(res => {
cityData.value = res.data
window.cityData = res.data
loading.value = false
})
}
// 清空上次的结果 例如用户点错了 重新点开应该重新选择省
for (const key in changeResult) {
changeResult[key] = ''
}
}
// 关闭城市组件
const closeCity = () => {
visible.value = false
}
// 切换城市组件开关
const toggleCity = () => {
visible.value ? closeCity() : openCity()
}
// 点击其他位置隐藏
const target = ref(null)
onClickOutside(target, () => closeCity())
// 城市数据
const cityData = ref([])
const url = 'https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json'
// 获取城市数据
const getCityData = () => {
return axios({ url })
}
// 子组件选中的数据
const changeResult = reactive({
provinceCode: '', // 省code
provinceName: '', // 省 名字
cityCode: '', // 市code
cityName: '', // 市 名字
countyCode: '', // 区 code
countyName: '', // 去 名字
fullLocation: '' // 省区市连起来的名字
})
const changeItem = (item) => {
// 省
if (item.level === 0) {
changeResult.provinceName = item.name
changeResult.provinceCode = item.code
}
// 市
if (item.level === 1) {
changeResult.cityName = item.name
changeResult.cityCode = item.code
}
// 地区
if (item.level === 2) {
changeResult.countyCode = item.code
changeResult.countyName = item.name
changeResult.fullLocation = `${changeResult.provinceName} ${changeResult.cityName} ${changeResult.countyName}`
emit('change', changeResult)
closeCity()
}
}
const curList = computed(() => {
// 省
let curList = cityData.value
// 市
if (changeResult.provinceCode) {
curList = curList.find(it => it.code === changeResult.provinceCode).areaList
}
// 区
if (changeResult.cityCode) {
curList = curList.find(it => it.code === changeResult.cityCode).areaList
}
return curList
})
return { visible, toggleCity, target, cityData, changeResult, changeItem, curList, loading }
}
}
</script>
<style scoped lang="less">
.xtx-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;
}
}
}
}
.loading {
height: 290px;
width: 100%;
background: url('~@/assets/images/loading.gif') no-repeat center;
}
</style>
以上是关于vue3.0手写省市区地区联动 详细的主要内容,如果未能解决你的问题,请参考以下文章