首先, 讲述一下这个组件需要实现的需求:
1. 在页面显示完整用户选择的省市区信息
2. 此组件是作用在别的组件里面, 接收父组件传入完整的省市区信息; 提供模板中使用
3. 根据后端规定的字段名定义一个对象; 将用户修改后的省市区数据放入这个对象中, 供需要的父组件使用
有三种情况, 父组件会向此组件传入完整的省市区信息
我所讲述的这一个组件是应用在一个电商平台所需要的省市区联动组件
1. 用户没有登录时, 父组件传入一个定死的收货地址信息
2. 用户登录后, 父组件传入从数据库中获取当前登录账号的默认收货地址信息
3. 用户修改后, 因为所有的省市区数据都是在此组件里面; 所以记录用户修改后的省市区数据会 emit 给父组件; 由父组件传给此组件, 然后此组件通过 props 接收提供给模板使用
为什么第三种情况这样做, 是因为第一种和第二种情况都是父组件传给此组件一个完整的省市区数据; 这就说明, 此组件如何显示省市区都是有父组件来决定的
所以, 此组件里面不能直接进行修改, 而是由父组件传入
好了, 说了些基本逻辑; 大家可能还是一头雾水, 不知道我在说啥, 现在就上代码显示
首先, 完成基本布局
布局分析:
1. 大盒子里面包含两个子盒子, 一是显示配送地址信息盒子; 二是所有省市区信息盒子
2. 配送地址信息盒子里面就是两个 span , 一个是显示信息盒子; 另外一个是 icon
<template>
<div class="xtx-city">
<!-- 默认显示或选择完毕的元素 -->
<div class="select">
<!-- 默认显示的span信息 -->
<span class="placeholder">请选择配送地址</span>
<!-- 用户选择完毕之后替换掉默认显示的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>
现在来完成基本的交互
思路分析:
1. 定义控制省市区盒子(option), 显示隐藏的变量(active); 默认值为 false
2. 定义 option 元素显示的方法(open), 将 active 变量值变成 true
3. 定义 option 元素隐藏的方法(close), 将 active 变量值变成 false
4. 定义一个调用 open 和 close 的方法(toggleOption), 通过判断 active 的值; 动态的调用 open 和close方法
5. 当用户点击页面其他地方的时候, 应当将 option 元素隐藏
<template>
<div class="xtx-city" ref="target">
<div class="select" @click="toggleOption" :class="{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>
<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
export default {
name: 'XtxCity',
setup () {
// 控制option选项显示隐藏变量, 默认隐藏
const active = ref(false)
// 展开option元素方法
const open = () => {
active.value = true
}
// 关闭option元素方法
const close = () => {
active.value = false
}
// 根据active状态来改变options元素的显示隐藏
const toggleOption = () => {
if (active.value) close()
else open()
}
// 点击其他地方, 关闭option元素方法
const target = ref(null)
onClickOutside(target, () => {
close()
})
return { active, toggleOption, target }
}
}
</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;
}
}
......
}
</style>
最后完成省市区联动的逻辑交互
思路分析:
1. 首先封装调用接口函数, 获取所有的省市区数据(使用的是阿里的省市区json数据)
2. 用户可以频繁的进行点击, 所以; 需要做缓存
3. 定义变量(cityList), 接收返回回来的数据(在数据还没有返回时, 显示loading效果)
4. 在 open 方法被调用的时候, 调用接口函数; 向 cityList 赋值(cityList的值是全部的初始的数据, 页面需要的数据是计算属性计算得到的数据)
5. 定义后端需要的字段对象(changeResult), 其中有省市区的地域编号和名称
6. 定义一个方法(changeOption), 用户点击进行选择时; 将用户选择的当前数据传给此方法
7. changeOption 方法内判断, 用户点击的是省或市或区; 对 changeResult 对象中的字段进行赋值
8. 使用计算属性(currList), 内部再返回一个变量(currList), 通过 changeResult 对象中的省市区的地域编号动态的改变 currList 的值; 最终 currList 提给给模板渲染数据
9. 用户选择到区一级时, 将完整的省市区数据 emit 给父组件, 调用 close 方法
10. 用户再次进行修改省市区数据时, 情况 changeResult 对象中先前的数据
11. 当用户选择错误时, 点击页面其他地方; 需要对 changeResult 对象中的数据进行重置
<template>
<div class="xtx-city" ref="target">
<div class="select" @click="toggleOption" :class="{active}">
<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-if="active">
<!-- loading效果 -->
<div v-if="loading" class="loading"></div>
<template v-else>
<span class="ellipsis" @click="changeOption(item)" v-for="item in currList" :key="item.code">{{ item.name }}</span>
</template>
</div>
</div>
</template>
<script>
import { ref, computed, reactive } from 'vue'
import { onClickOutside } from '@vueuse/core'
import axios from 'axios'
export default {
name: 'XtxCity',
props: {
fullLocation: {
type: String,
default: ''
}
},
setup (props, { emit }) {
// 1. 获取省市区数据方法
const getCityData = () => {
const url = 'https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json'
return new Promise((resolve, reject) => {
// 2. 做缓存
// 有缓存
if (window.cityData) {
resolve(window.cityData)
} else {
// 没有缓存
axios.get(url).then(({ data }) => {
window.cityData = data
resolve(window.cityData)
})
}
})
}
// 3. 存储获取的省市区数据
const cityList = ref([])
// 当数据还在加载时, 显示loading效果
const loading = ref(false)
// 4. 展开option元素方法
const open = () => {
active.value = true
loading.value = true
getCityData().then(res => {
cityList.value = res
loading.value = false
}).catch(err => err)
// 10. 再次打开的时候, 清空先前的数据
for (const key in changeResult) {
changeResult[key] = ''
}
}
// 5. 定义依据后端字段需要的数据和父组件需要的数据
const changeResult = reactive({
// 省地域编号和名称
provinceCode: '',
provinceName: '',
// 市地域编号和名称
cityCode: '',
cityName: '',
// 区地域编号和名称
countyCode: '',
countyName: '',
// 完整的省市区数据
fullLocation: ''
})
// 6. 修改省市区数据的方法
const changeOption = (item) => {
// 7. 用户点击当前的省数据
if (item.level === 0) {
changeResult.provinceCode = item.code
changeResult.provinceName = item.name
} else if (item.level === 1) {
changeResult.cityCode = item.code
changeResult.cityName = item.name
} else {
changeResult.countyCode = item.code
changeResult.countyName = item.name
// 拼接好完整的数据
changeResult.fullLocation = `${changeResult.provinceName} ${changeResult.cityName} ${changeResult.countyName}`
// 9. 提供给父组件使用
emit('change', changeResult)
close()
}
}
// 8. 根据当前的省市区数据, 获取对应的下一级数据列表
const currList = computed(() => {
// 全部数据(不能直接的去影响cityList的数据, 重新定义变量; 对此变量进行修改即可)
let currList = cityList.value
// 用户选择省数据
// 主要是判断changeResult中省市区的地域编码是否存在; 从而动态的改变的currList值
// 且 currList 是模板渲染的主要数据, 所以用户选择的数据会随着选择变化而变化
if (changeResult.provinceCode) {
currList = currList.find(item => item.code === changeResult.provinceCode).areaList
}
// 用户选择当前省的市数据
if (changeResult.cityCode) {
currList = currList.find(item => item.code === changeResult.cityCode).areaList
}
// 用户选择当前市的区数据
if (changeResult.countyCode) {
currList = currList.find(item => item.code === changeResult.countyCode).areaList
}
return currList
})
// 11. 点击其他地方, 关闭option元素方法
const target = ref(null)
onClickOutside(target, () => {
close()
// 重置数据
for (const key in changeResult) {
changeResult[key] = ''
}
})
// 控制option选项显示隐藏变量, 默认隐藏
const active = ref(false)
// 根据active状态来改变options元素的显示隐藏
const toggleOption = () => {
if (active.value) close()
else open()
}
// 关闭option元素方法
const close = () => {
active.value = false
}
return { active, toggleOption, target, loading, cityList, currList, changeOption, changeResult }
}
}
</script>
<style scoped lang="less">
.xtx-city {
......
.option {
......
.loading {
height: 290px;
width: 100%;
background: url(../../assets/images/loading.gif) no-repeat center;
}
}
}
</style>
父组件代码
<template>
<p class="g-name">{{ goods.name }}</p>
<p class="g-desc">{{ goods.desc }}</p>
<p class="g-price">
<span>{{ goods.price }}</span>
<span>{{ goods.oldPrice }}</span>
</p>
<div class="g-service">
<dl>
<dt>促销</dt>
<dd>12月好物放送,App领券购买直降120元</dd>
</dl>
<dl>
<dt>配送</dt>
<dd>至 <XtxCity @change="changeCity " :fullLocation="fullLocation" /></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: {
goods: {
type: Object,
default: () => {}
}
},
setup (props) {
// 当用户没有登录的时候, 需要显示默认的数据
const provinceCode = ref('110000')
const cityCode = ref('119900')
const countyCode = ref('110101')
const fullLocation = ref('北京市 市辖区 东城区')
// 判断用户时候有登录
if (props.goods.userAddresses) {
// 如果存在, city组件就需要渲染用户的默认收货地址信息
const defaultAddr = props.goods.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 = (data) => {
provinceCode.value = data.provinceCode
cityCode.value = data.cityCode
countyCode.value = data.countyCode
fullLocation.value = data.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>