IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> vue3.0手写省市区地区联动 详细 -> 正文阅读

[JavaScript知识库]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 = () => {
      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>

?

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-27 16:07:20  更:2021-07-27 16:07:34 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/5 3:18:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码