vue3搭建实战项目笔记四

发布于:2025-05-13 ⋅ 阅读:(10) ⋅ 点赞:(0)

内容简牍

一.Loading展示

1.1. Loading组件搭建

1.2. Loading状态保存

1.3. Loading状态改变
  - hyRequest中的拦截器中

二.详情页

2.1. 点击item跳转

2.2 详情页导航搭建

2.3. 详情页数据请求和管理
  - 页面管理数据 props传递

2.4. 详情页数据展示
  - 轮播图
  - 自定义指示器

2.5 描述信息的搭建

2.6. detail-section组件搭建

2.7. 搭建内容部分
  - 设施
  - 房东
  - 评论
  - 须知

2.8. 引入百度地图

2.9. tabControl控制
  2.9.1. tabControl的搭建
    - 使用之前封装组件

  2.9.2. 控制tabControl的显示
    - 监听页面滚动
      - 监听元素的滚动
    - >= 300时显示

  2.9.3. tabControl的点击
    - 获取组件的根元素的offsetTop,ref绑定函数的方式
    - 监听点击:找到元素,滚动对应的位置
    - 动态的组件的names,传递给tabControl

详细笔记

4.1.网络请求添加请求动画

    1. 搭建loading组件,在App.vue中引入
      原因:
    • 因为所有的请求都需要使用请求动画,放到App.vue中,那么所有的页面都可以使用,不用每个页面都引入一次
    • 在App.vue中引入loading组件
    • App.vue根组件:
          <template>
          <div class="app">
            <!-- 根据路由元信息是否显示 tabBar -->
            <router-view></router-view>
            <tab-bar v-if="!route.meta.hideTabBar"/>
            <loading />
          </div>
          </template>
      
          <script setup>
          import tabBar from '@/components/tab-bar/tab-bar.vue';
          import { useRoute } from 'vue-router';
          import Loading from '@/components/loading/loading.vue';
          // 当前活跃的路由对象
          const route = useRoute()
      
          </script>
      
      
      
          <style scoped>
      
          </style>
      
    • loading.vue组件:
        <template>
          <div 
            class="loading"
            v-if="mainStore.isLoading"
            @click="loadingClick">
            <div class="bg">
              <img src="@/assets/img/home/full-screen-loading.gif" alt="">
            </div>
          </div>
        </template>
      
        <script setup>
        import useMainStore from '@/stores/modules/main';
      
        const mainStore =  useMainStore()
      
        const loadingClick = () => {
          mainStore.isLoading = false
        }
        </script>
        <style lang="less" scoped>
          .loading {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: rgba(0, 0, 0, 0.5);
            .bg {
              width: 104px;
              height: 104px;
              display: flex;
              justify-content: center;
              align-items: center;
              background: url(@/assets/img/home/loading-bg.png) 0 0 / 100% 100%;
      
              img{
                width: 70px;
                height: 70px;
                margin-bottom: 10px;
              }
            }
          }
        </style>
      
    1. 在公共的mianStore中存储一个loading状态,默认为false
      import { defineStore } from "pinia";
      const useMainStore = defineStore('main', {
        state: () => ({
          isLoading: true
        })
      })
    
      export default useMainStore
    
    1. 每次请求都需要显示请求动画,所以在请求拦截器显示请求动画,在响应拦截器中,关闭请求动画设置loading为false
    • 关键代码如下:
        this.instance.interceptors.request.use((config) => {
          mainStore.isLoading = true
          return config
        }, err => {
          // 发送请求失败的是没有必要把isLoading设置为true,这个请求都发不出去所以不需要显示loading
          return err
        })
        this.instance.interceptors.response.use((res) => {
          // 在返回响应的时候设置isLoading为false,不管是成功还是失败
          mainStore.isLoading = false
          return res
        }, err => {
          mainStore.isLoading = false
          return err
        })
    
    • 完整代码如下:
        import axios from 'axios'
        import { BASE_URL, TIME_OUT } from './config'
        import useMainStore from '@/stores/modules/main'
        const mainStore = useMainStore()
    
        class HyRequest {
          constructor(baseURL, timeout = 10000) {
            this.instance = axios.create({
              baseURL,
              timeout
            })
            this.instance.interceptors.request.use((config) => {
              mainStore.isLoading = true
              return config
            }, err => {
              // 发送请求失败的是没有必要把isLoading设置为true,这个请求都发不出去所以不需要显示loading
              return err
            })
            this.instance.interceptors.response.use((res) => {
              // 在返回响应的时候设置isLoading为false,不管是成功还是失败
              mainStore.isLoading = false
              return res
            }, err => {
              mainStore.isLoading = false
              return err
            })
          }
    
          request(config) {
            return new Promise((resolve, reject) => {
              // mainStore.isLoading = true
              this.instance.request(config).then(res => {
                resolve(res.data)
                // mainStore.isLoading = false
              }).catch(err => {
                reject(err)
                // mainStore.isLoading = false
              })
            })
          }
    
          get (config) {
            return this.request({...config, method: 'get'})
          }
    
          post (config) {
            return this.request({...config, method: 'post'})
          }
        }
        export default new HyRequest(BASE_URL, TIME_OUT)
    

4.2.父组件添加click事件场景

    1. 场景:在子组件不同类型卡片监听点击事件
    • 原因;因为子组件监听点击需要重复执行监听事件,所以需要在父组件绑定事件
      house-item-v9.vue
      house-item-v3.vue
        <template>
          <house-item-v9 v-if="item.discoveryContentType === 9" :item-data="item.data" @click="itemClick(item.data)"/>
          <house-item-v3 v-else-if="item.discoveryContentType === 3" :item-data="item.data" @click="itemClick(item.data)"/>
        </template>
    
    1. 给父组件绑定click事件需要注意场景:
    2. 子组件一个只有一个根元素时,默认绑定到根元素上
    3. 子组件多个根元素时,需要使用v-bind=“$attrs”,没有指定绑定元素,会报一个警告

4.3.封装轮播图组件

    1. 观察轮播图组件:发现指示器需要使用插槽自定义,然后需要自己写包裹指示器数据
  • 在这里插入图片描述
    在这里插入图片描述

    1. 封装轮播图组件的思路:
      1. 利用vant组件中的swiper组件,然后使用v-slot自定义指示器
      1. 完整代码如下:
    <template>
      <div class="swipe">
        <van-swipe class="swipe-list" :autoplay="3000" indicator-color="white">
          <van-swipe-item
            class="swipe-item"
            v-for="(item, index) in swipeData"
            :key="index"
          >
            <img :src="item.url" alt="" />
          </van-swipe-item>
          <!-- 具名插槽  作用域插槽解构 -->
          <template #indicator="{ active, total }">
            <div class="indicator">
              <template v-for="(value, key, index) in swipeGroup" :key="key">
                <span 
                  class="item" 
                  :class="{ active: swipeData[active]?.enumPictureCategory == key }">
                  <span class="text"> {{ getName(value[0].title) }}</span>
                  <span class="count" v-if="swipeData[active]?.enumPictureCategory == key">
                    {{ getCategoryIndex(swipeData[active]) }} / {{ value.length }}
                  </span>
                </span>
              </template>
            </div>
          </template>
        </van-swipe>
      </div>
    </template>
    
    <script setup>
    const props = defineProps({
      swipeData: {
        type: Array,
        default: () => [],
      },
    });
    
    const swipeGroup = {};
    for (const item of props.swipeData) {
      // 首先拿到这个swipeGroup对象这个属性的值, 判断是否为空,,为空重置为空数组,然后把item添加进去
      let valueArr = swipeGroup[item.enumPictureCategory];
      if (!valueArr) {
        valueArr = [];
        swipeGroup[item.enumPictureCategory] = valueArr;
      }
      valueArr.push(item);
    }
    console.log("swipeGroup===", swipeGroup);
    
    const getName = (title) => {
      const nameRegex = /【(.*?)】/i;
      const result = nameRegex.exec(title);
      return result ? result[1] : title;
    };
    
    const getCategoryIndex = (item) => {
      const valueArr = swipeGroup[item.enumPictureCategory]
      return valueArr.findIndex(data => data === item) + 1
    } 
    </script>
    
    <style lang="less" scoped>
      .swipe {
        .swipe-list {
          .swipe-item {
            img {
              width: 100%;
            }
          }
    
          .indicator {
            position: absolute;
            right: 5px;
            bottom: 5px;
            display: flex;
            padding: 2px 5px;
            font-size: 12px;
            color: #fff;
            background: rgba(0, 0, 0, 0.6);
            .item {
              margin: 0 3px;
              &.active {
                padding: 0 3px;
                border-radius: 5px;
                color: #333;
                background-color: #fff;  
              }
            }
          }
        }
      }
      </style>
    

4.4.封装业务组件的思路

    1. 观察下图已知:头部和查看更多是一样的样式,内容是动态的使用插槽
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 在这里插入图片描述
    1. 封装业务组件的思路:
    • 2.1 在components中创建一个组件,组件名:detail-section
    • 2.2 在组件中写头部和查看更多的样式,内容写上一个默认插槽。
    • 2.3 在组件中写一个props,用来接收title和moreText,数据类型为String,默认为空字符串。
    • 2.4 详细代码如下:
      <template>
        <div class="section">
          <div class="header">
            <h2 class="title">{{title}}</h2>
          </div>
          <div class="content">
            <slot>
              <h3>我是默认内容</h3>
            </slot>
          </div>
          <div class="footer" v-if="moreText.length">
            <span class="more">{{moreText}}</span>
            <van-icon name="arrow" />
          </div>
      </div>
    </template>
    
    <script setup>
      defineProps({
        title: {
          type: String,
          default: '默认标题'
        },
        moreText: {
          type: String,
          default: ''
        }
      })
    </script>
    
    <style lang="less" scoped>
      .section {
        padding: 0 15px;
        margin-top: 12px;
        border-top: 5px solid #f2f3f4;
        background-color: #fff;
        .header {
          height: 50px;
          line-height: 50px;
          border-bottom: 1px solid #eee;
    
          .title {
            font-size: 20px;
            color: #333;
          }
        }
    
        .content {
          padding: 8px 0;
        }
    
        .footer {
          display: flex;
          justify-content: flex-end;
          align-items: center;
          height: 44px;
          line-height: 44px;
          color: #ff9645;
          font-size: 14px;
          font-weight: 600;
        }
      }
    </style>
    
    1. 封装业务组件调用:
    • 3.1 引入组件
        import detailSection from '@/components/detail-section/detail-section.vue';
      
    • 3.2 页面中使用组件
      <div class="facility">
        <detail-section title="房屋设施" more-text="查看全部设施">
          <div class="facility-inner"></div>
      
        </detail-section>
      </div>
      

4.5.引入百度地图

    1. 打开百度地图开发者平台,认证个人开发者
    1. 在应用管理里,创建应用,填写应用名,选择web应用,允许访问的域名,没有可以写*,点击提交,生成密钥
  • 在这里插入图片描述

  • 在这里插入图片描述

    1. 在index.html中引入百度地图API文件
    1. 初始化地图逻辑
    <script setup>
      import detailSection from '@/components/detail-section/detail-section.vue';
      import { onMounted, ref } from 'vue';

      const mapRef = ref();
      const props = defineProps({
        position: {
          type: Object,
          default: () => ({})
        }
      })

      onMounted(() => {
        // 首先不能再setup里面写,因为setup不保证是当前元素是否挂载的
        const map = new BMapGL.Map(mapRef.value); // 创建地图实例 
        const point = new BMapGL.Point(props.position.longitude, props.position.latitude);  // 创建点坐标 
        map.centerAndZoom(point, 15); // 初始化地图,设置中心点坐标和地图级别

        const marker = new BMapGL.Marker(point); // 创建标注   
        map.addOverlay(marker); // 将标注添加到地图中 
      })
    </script>

4.6.点击tabBar组件滚动相应位置

    1. 开发思路:
    • 1.1. 开发出来一个tabControl组件
    • 1.2. 监听滚动
    • 1.3. 监听tabcontrol点击,点击后滚动到正确的位置
    1. 详细步骤点:
    • 2.1. 创建一个tabControl组件,组件名:tabControl,引入tabControl,点击时将数据的index传递给父组件
    • 2.2. 监听滚动。
      • 2.2.1. 这个页面是元素滚动,不是window滚动,所以需要修改useScroll方法,获取滚动的元素,把滚动的元素作为参数传递给useScroll
           // 更新useScroll方法为通用方法
           // 1.设置el的默认值为window, 函数接收传入滚动的元素
           // 2.挂载时,如果传入的元素存在,就赋值给el
           // 3.如果是el为window,则返回document.documentElement的clientHeight/scrollTop/scrollHeight,
           //   否则返回el的clientHeight/scrollTop/scrollHeight
            import { ref, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
            import { throttle } from 'underscore'
            export default function useScroll(elRef) {
              let el = window
              const isReachBottom = ref(false)
              const clientHeight = ref(0) // 可见的高度
              const scrollTop = ref(0) // 滚动的距离
              const scrollHeight = ref(0) // 整个文档的可滚动的高度
              
        
              const scrollListenerHandler = throttle((reachBottomCB) => {
                // 2.2 拿到客户端的高度,客户端的高度 + scrollTop >= scrollHeight 说明滚动到底部
                if(el === window) {
                  clientHeight.value = document.documentElement.clientHeight
                  scrollTop.value = document.documentElement.scrollTop
                  scrollHeight.value = document.documentElement.scrollHeight
                } else {
                  clientHeight.value = el.clientHeight
                  scrollTop.value = el.scrollTop
                  scrollHeight.value = el.scrollHeight
                }
                if (clientHeight.value + scrollTop.value >= scrollHeight.value) {
                  isReachBottom.value = true
                }
              }, 100)
        
              onMounted(() => {
                if(elRef) el = elRef.value
                el.addEventListener('scroll', scrollListenerHandler)
              })
        
              onUnmounted(() => {
                el.removeEventListener('scroll', scrollListenerHandler)
              }) 
        
              onActivated(() => {
                el.addEventListener('scroll', scrollListenerHandler)
              })
        
              onDeactivated(() => {
                el.removeEventListener('scroll', scrollListenerHandler)
              })
        
              return {
                isReachBottom,
                clientHeight,
                scrollTop,
                scrollHeight,
              }
            }
        
      • 2.2.2. 控制tabControl的显示,监听滚动相应位置显示
          const detailRef = ref(null)
          const { scrollTop } = useScroll(detailRef)
          const showTabControl = computed(() => {
            return scrollTop.value >= 300
          })
        
      • 2.2.3. 需要动态绑定ref,然后获取每个组件根元素,然后获取到offsetTop,然后滚动到相应位置
          // 1. 给每个组件动态添加ref, :ref="getSectionRef"
          // 2. 滚动过程中,会实时刷新,导致refs的值会变,使用v-memo缓存模块的子树解决
          // 3. 获取每个组件的根元素$el,并保存在数组中,方便后续使用
            <template>
              <div class="detail top-page" ref="detailRef">
                <van-nav-bar
                  title="房屋详情"
                  left-text="返回"
                  left-arrow
                  @click-left="onClickLeft"
                />
                <tab-control
                  class="tabs"
                  v-if="showTabControl"
                  :titles="names"
                  @tabItemClick="tabClick"
                />
                <!-- 内容部分 -->
                <div class="main" v-if="mainPart" v-memo="[mainPart]">
                  <!-- 轮播组件 -->
                  <detail-swipe :swipe-data="mainPart.topModule.housePicture.housePics" />
                  <!-- 动态绑定Ref:在处理复杂组件结构和动态数据时通过动态绑定Ref,我们可以更灵活地访问和操作DOM元素或组件实例,实现更高效的交互和状态管理 -->
                  <detail-infos name="描述" :ref="getSectionRef" :topInfos="mainPart.topModule"/>
                  <detail-facility name="设施" :house-facility="mainPart.dynamicModule.facilityModule.houseFacility"/>
                  <!-- :landload="mainPart.dynamicModule.landloadModule.houseLandload" -->
                  <detail-landlord name="房东" :ref="getSectionRef" :landlord="mainPart.dynamicModule.landlordModule"/>
                  <detail-comment name="评论" :ref="getSectionRef" :comment="mainPart.dynamicModule.commentModule"/>
                  <detail-notice name="须知" :ref="getSectionRef" :order-rules="mainPart.dynamicModule.rulesModule.orderRules"/>
                  <detail-map name="周边" :ref="getSectionRef" :position="mainPart.dynamicModule.positionModule" />
                  <detail-intro :priceIntro="mainPart.introductionModule"/>
                </div>
                <div class="footer">
                  <img src="@/assets/img/detail/icon_ensure.png" alt="">
                  <div class="text">弘源旅途, 永无止境!</div>
                </div>
              </div>
            </template>
        
           <script setup>
            import { computed, ref } from 'vue';
            const sectionEls = {}
            const names = []
            // 找到对应元素的根元素
            const getSectionRef = (value) => {
              // 在滚动时,会引起dom的刷新,导致sectionEls会被重新执行,会有多个sectionEls
              // 使用v-memo,可以解决这个问题,缓存一个模板的子树,当数据变化时才会刷新
              console.log('value===', value);
              // value拿到的是组件实例对象,想要拿到组件对象的根元素,怎么拿到组件对象的根元素.$el
              const name = value.$el.getAttribute('name')
              names.push(name)
              sectionEls[name] = value.$el
            }
        
            // 当点击时滚动到对应位置
            const tabClick = (index) => {
              const key = Object.keys(sectionEls)[index]
              const el = sectionEls[key]
              let instance = el.offsetTop
              if(index !== 0) {
                // 滚动距离减去 44,因为tabControl组件的高度为44,会遮挡模块的标题
                instance = instance - 44
              }
              // scrollTo 使界面滚动到给定元素的指定位置
              detailRef.value.scrollTo({
                top: instance,
                behavior: 'smooth' // 滚动行为 smooth: 平滑滚动
              })
            }
          </script>