vue3搭建实战项目笔记四
vue3搭建实战项目笔记四
·
内容简牍
一.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.网络请求添加请求动画
-
- 搭建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>
- 搭建loading组件,在App.vue中引入
-
- 在公共的mianStore中存储一个loading状态,默认为false
import { defineStore } from "pinia"; const useMainStore = defineStore('main', { state: () => ({ isLoading: true }) }) export default useMainStore
-
- 每次请求都需要显示请求动画,所以在请求拦截器显示请求动画,在响应拦截器中,关闭请求动画设置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事件场景
-
- 场景:在子组件不同类型卡片监听点击事件
- 原因;因为子组件监听点击需要重复执行监听事件,所以需要在父组件绑定事件
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>
-
- 给父组件绑定click事件需要注意场景:
- 子组件一个只有一个根元素时,默认绑定到根元素上
- 子组件多个根元素时,需要使用v-bind=“$attrs”,没有指定绑定元素,会报一个警告
4.3.封装轮播图组件
-
- 观察轮播图组件:发现指示器需要使用插槽自定义,然后需要自己写包裹指示器数据
-
-
- 封装轮播图组件的思路:
-
- 利用vant组件中的swiper组件,然后使用v-slot自定义指示器
-
- 完整代码如下:
<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.封装业务组件的思路
-
- 观察下图已知:头部和查看更多是一样的样式,内容是动态的使用插槽
-
- 封装业务组件的思路:
- 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>
-
- 封装业务组件调用:
- 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.引入百度地图
-
- 打开百度地图开发者平台,认证个人开发者
-
- 在应用管理里,创建应用,填写应用名,选择web应用,允许访问的域名,没有可以写*,点击提交,生成密钥
-
-
-
- 在index.html中引入百度地图API文件
-
- 初始化地图逻辑
<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. 开发出来一个tabControl组件
- 1.2. 监听滚动
- 1.3. 监听tabcontrol点击,点击后滚动到正确的位置
-
- 详细步骤点:
- 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>
- 2.2.1. 这个页面是元素滚动,不是window滚动,所以需要修改useScroll方法,获取滚动的元素,把滚动的元素作为参数传递给useScroll
更多推荐
所有评论(0)