Vue3.0 handwritten provinces, cities and regions linkage details

Table of contents

1. Register a global component city 

 2. Use city components

 3. Switch between display and hide in the city component

4. Click outside the city widget to close the city widget

5. The city component obtains data

 6. City components-logic interaction

initial value

  7. City component - interaction - display default address

8. City component click interactive child to parent

 9. City component click interactive parent receive

10. Add cache to the city component

 11. Save the loading effect when requesting city data

 full code


achieve effect 

 Because the data of provinces and cities is relatively large, the data is imported through links

https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json

1. Register a global component city 

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

Register global components slightly 

 2. Use city components

goods-name.vue

     <dl>
      <dt>配送</dt>
      <dd>至 <XtxCity></XtxCity> 城市组件</dd>
    </dl>

current effect 

 

 3. Switch between display and hide in the city component

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

Add a click to select the delivery address to switch off the display city drop-down box. When it is true, it will display open and give the class name a white background.

4. Click outside the city widget to close the city widget

 How to determine whether to click inside the element using the onClickOutside method in the vueuse tool library

npm i @vueuse/core
// 1. 导入方法
import { onClickOutside } from '@vueuse/core'

setup() {
  // 鼠标在目标之外点击,就会执行回调
  onClickOutside(监听的目标, (e) => {
    // 鼠标在目标之外点击,要做什么?
  })  
}

 use

 <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. The city component obtains data

<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 })
    }

Note that the open method is called when it is opened  

 current effect

 6. City components-logic interaction

Display the text of provinces and cities, so that the component can select provinces and cities and feed back to the parent component.

initial value

  • The user is not logged in: in the current product data, the backend will pass userAddresses: null, at this time, we should use the default address: Dongcheng District, Beijing City

  • The user has logged in: in the current product data, the backend will pass userAddresses: an array of addresses, similar to the following:

isDefault: 1 means the default address. At this point, we should display the address provided by the user.  

From father to son: The fullLocation obtained from the product details should be displayed to xtx-city

Passing from child to parent: Click on the three levels of information in xtx-city, complete the setting of the address, and pass the currently selected data to the parent component.

After selecting provinces and cities: 4 data to be passed to the backend:

  1. save code

  2. city ​​code

  3. region code;

  4. the words they combine

  7. City component - interaction - display default address

In the parent component goods-name:

(1) Prepare data according to the requirements of the interface document

(2) Analyze the current address from the parent component and pass it to the child component for display.

The parent component sets the code data of provinces and cities, and the corresponding text data.

 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>

 pass to the city component

<XtxCity :fullLocation="fullLocation" ></XtxCity>

 City component reception

  props: {
    fullLocation: { type: String, default: '' }
  },

show default address

<span class="placeholder" v-if="!fullLocation">请选择配送地址</span>
<span class="value" v-else> {
   
   {fullLocation}} </span>

8. City component click interactive child to parent

Click the province --> display the list of cities

Click on City -->Display area list.

Click the area --> close the pop-up layer and notify the parent component

The displayed content is directly related to the user's selection, so use the calculated property to determine

xtx-city.vue

    <div class="option" v-show="visible">
+      <span @click="changeItem(item)" class="ellipsis"></span>

Define city components

 // 子组件选中的数据
    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()   //选完市以后关闭弹框  代表三个都点击完了
      }
    }

 Define the calculated property to get the current value to be rendered. For example, first render the province, click to render the city, click the city to render the area

 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 }

change traverse data traverse curList

  <span class="ellipsis" @click="changeItem(item)" v-for="item in curList" :key="item.code"> {
   
   {item.name}} </span>

 Clear the last option when opening the component

    // 打开城市组件
    const openCity = () => {
      visible.value = true
      getCityData().then(res => { cityData.value = res.data })
      //   清空上次的结果  例如用户点错了 重新点开应该重新选择省
      for (const key in changeResult) {
        changeResult[key] = ''
      }
    }

 9. City component click interactive parent receive

<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. Add cache to the city component

The current situation is that a new ajax will be sent every time it is opened, sending a lot of the same data wastes memory resources

 Save the solution to the window, you can also save it to 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. Save the loading effect when requesting city data

When the network is slow, the data cannot be obtained, and a loading effect should be displayed

<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;
    }

 The loading effect changes

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 // 加载完成
        })
      }
    }

A small bug set the network to slow 3g, and found that when the page was opened at the beginning, it was empty, and it took a while to display the loading effect 

 The reason is that it takes time to request the image, that is to say, the request time for the loading effect image is 2.32s and the size is 8.2kb

 Solve the loading bug and convert the image to base64 format  

Remember to restart the server after modifying the configuration items in 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
	}
}

 After converting to base64 format, the request will not be sent 

 full code

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>

Guess you like

Origin blog.csdn.net/m0_46846526/article/details/119116407#comments_26423426