vue3.0手写省市区地区联动 详细

目录

1、注册一个全局组件城市 

 2.使用城市组件

 3、城市组件中切换显示隐藏

4. 在城市组件外点击关闭城市组件

5、 城市组件 获取数据

 6、城市组件-逻辑交互

初始值

  7、城市组件-交互-显示默认地址

8. 城市组件 点选交互 子传父

 9. 城市组件 点选交互 父接收

10. 给城市组件加缓存

 11.请求城市数据时保存loading效果

 完整代码


实现效果 

 因为省市区数据较大 所以数据通过链接引入

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个数据给后端:

  1. 省code

  2. 市code

  3. 地区code;

  4. 它们组合在一起的文字

  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 = () => {
      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>

猜你喜欢

转载自blog.csdn.net/m0_46846526/article/details/119116407#comments_26423426