经常需要再页面里面实现 很多个tabs组成一条选择按钮,点击其中一个按钮,滚动到屏幕中间位置,
然后对应的content 内容也滚动到对应的 位置;
如图的结构;
页面结构,最左侧是一级分类:
右侧,顶部的tabs 是二级分类;
右侧:content部分是 分类标题和 推荐的产品列表; 这个页面结构在电商小程序和h5页面都很常见;
下面是实现这个页面的 方法:
红色方框采用两个组件来实现,组件直接通过 事件传值;
第一个组件tabs
<template> <scroll-view :scroll-x="true" :scroll-left="scrollLeft" scroll-with-animation class="u-tabs__wrapper__scroll-view" :show-scrollbar="false" ref="u-tabs__wrapper__scroll-view" > <view ref="u-tabs__wrapper__nav"> <view :class="[ index==innerCurrent && 'active',`u-tabs__wrapper__nav__item-${index}`]" v-for="(item,index) in list" :key="index" @tap="clickHandler(item, index)" :ref="`u-tabs__wrapper__nav__item-${index}`" > <text > {{item.name}}</text> </view> </view> </scroll-view> </template> <script> export default{ name : 'CateList', emits: ['click', 'change'], props:{ list:{ type:Array, default:[] }, current:{ type: Number, default: 0 } }, data(){ return { scrollLeft : 0, innerCurrent : 0, } }, watch: { current: { immediate: true, handler (newValue, oldValue) { // 内外部值不相等时,才尝试移动滑块 if (newValue !== this.innerCurrent) { this.innerCurrent = newValue this.$nextTick(() => { this.resize() }) } } } }, async mounted() { this.init() }, methods:{ sleep(value = 30) { return new Promise((resolve) => { setTimeout(() => { resolve() }, value) }) } , clickHandler(item, index) { // 因为标签可能为disabled状态,所以click是一定会发出的,但是change事件是需要可用的状态才发出 this.$emit('click', { ...item, index }) this.innerCurrent = index; this.resize() this.$emit('change', { ...item, index }) }, init() { this.sleep().then(() => { this.resize() }) }, setScrollLeft() { // 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息 const tabRect = this.list[this.innerCurrent] // 累加得到当前item到左边的距离 const offsetLeft = this.list .slice(0, this.innerCurrent) .reduce((total, curr) => { return total + curr.rect.width }, 0) // 此处为屏幕宽度 const windowWidth = uni.getSystemInfoSync().windowWidth // 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动 let scrollLeft = offsetLeft - (this.tabsRect.width - tabRect.rect.width) / 2 - (windowWidth - this.tabsRect .right) / 2 + this.tabsRect.left / 2 // 这里做一个限制,限制scrollLeft的最大值为整个scroll-view宽度减去tabs组件的宽度 scrollLeft = Math.min(scrollLeft, this.scrollViewWidth - this.tabsRect.width) this.scrollLeft = Math.max(0, scrollLeft) }, // 获取所有标签的尺寸 resize() { // 如果不存在list,则不处理 if(this.list.length === 0) { return } Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(([tabsRect, itemRect = []]) => { this.tabsRect = tabsRect this.scrollViewWidth = 0 itemRect.map((item, index) => { // 计算scroll-view的宽度,这里 this.scrollViewWidth += item.width // 另外计算每一个item的中心点X轴坐标 this.list[index].rect = item }) // 获取了tabs的尺寸之后,设置滑块的位置 this.setScrollLeft() }) }, // 获取导航菜单的尺寸 getTabsRect() { return new Promise(resolve => { this.queryRect('u-tabs__wrapper__scroll-view').then(size => resolve(size) ) }) }, // 获取所有标签的尺寸 getAllItemRect() { return new Promise(resolve => { const promiseAllArr = this.list.map((item, index) => this.queryRect( `u-tabs__wrapper__nav__item-${index}`, true)) Promise.all(promiseAllArr).then(sizes => resolve(sizes) ) }) }, // 获取各个标签的尺寸 queryRect(el, item) { // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同 return new Promise(resolve => { this.uGetRect(`.${el}`).then(size => resolve(size) ) }) }, uGetRect(selector, all) { return new Promise((resolve) => { uni.createSelectorQuery() .in(this)[all ? 'selectAll' : 'select'](selector) .boundingClientRect((rect) => { if (all && Array.isArray(rect) && rect.length) { resolve(rect) } if (!all && rect) { resolve(rect) } }) .exec() }) }, } } </script> <style> .cate-tabs{ display: flex; padding-bottom: 10px; flex-wrap: nowrap; justify-content: flex-start; .sub-cate{ background-color: #F2F2F2; border-radius: 30px; padding: 7px 20px ; margin-right: 10px; flex-shrink: 0; } .active{ background-color: #C8E1FF; color: #4A9BF7; } } </style>
第二个组件:
<template> <scroll-view :scroll-y="true" :scroll-x="false" :scroll-top="scrollTop" scroll-with-animation class="cate-goods cate_scroll_view" :show-scrollbar="false" > <view class="" v-for="(cate,index) in cateGoods"> <view class="cate-title u-margin-bottom-20" :class="[`cate_sub_title-${index}`]" > <text class="font-size18 font-weight">{{cate.title}}</text> </view> <view class="good-items u-margin-bottom-40 " v-for="(good, k) in cate.goods" :key="k" > <GoodItem :item="good"></GoodItem> </view> </view> </scroll-view> </template> <script setup> import GoodItem from "./good-item.vue"; import { ref, onMounted, onUpdated, watchEffect, watch, provide, nextTick } from 'vue'; //定义reactive变量类似 data const scrollTop = ref(0); const isFirst = ref(true); const scrollViewTop = ref(0); //定义属性 const props = defineProps({ currentCate:{ type: Object, }, // 导航列表 cateGoods: { type: Array, default: [ { title: "门票", goods:[ {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""} ] }, { title: "夜宿体验", goods:[ {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""} ] }, { title: "成人票", goods:[ {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""}, {id:1, title: "研学蛮龙片", logo:"", sub_title:""} ] } ] } }); //监听属性变化 watch( ()=> props.currentCate , (newValue, oldValue)=>{ //计算scroll_view 滚动到top的值 resize().then( (cate) => { if(cate){ const item = cate[newValue.index] ; //当前第几个cate if(item && item.rect){ scrollTop.value = (item.rect?.top || 0 ) - scrollViewTop.value -20 ; } } }) }); // 获取所有标签的尺寸 function resize() { // 如果不存在list,则不处理 if(props.cateGoods.length === 0) { return } return new Promise((resolve)=>{ //第一次记录下每个分类的初始位置; if(isFirst.value){ isFirst.value = false; Promise.all([getScrollViewRect(), getAllItemRect()]) .then(([viewRect, itemRect = []]) => { //scroll_view的top位置 scrollViewTop.value = viewRect.top; //每个分类的top位置 itemRect.map((item, index) => { props.cateGoods[index].rect = item ; // 另外计算每一个item的中心点X轴坐标 }) resolve(props.cateGoods) }) }else{ resolve(props.cateGoods) } }) } function getScrollViewRect(){ return new Promise(resolve => { queryRect("cate_scroll_view").then(size=> resolve(size) ) ; }) } function getAllItemRect(){ return new Promise(resolve => { const promiseAllArr = props.cateGoods.map((item, index) => queryRect( `cate_sub_title-${index}`, true)); Promise.all(promiseAllArr).then(sizes => resolve(sizes)) }) } // 获取各个标签的尺寸 function queryRect(el, item) { return new Promise(resolve => { uGetRect(`.${el}`).then(size => resolve(size) ) }) } function uGetRect(selector, all) { return new Promise((resolve) => { uni.createSelectorQuery() .in(this)[all ? 'selectAll' : 'select'](selector) .boundingClientRect((rect) => { if (all && Array.isArray(rect) && rect.length) { resolve(rect) } if (!all && rect) { resolve(rect) } }) .exec() }) } </script> <style> </style>
调用组件,组合成这个商品分类展示商品的页面结构:
<template> <kb-layout :title="title"> <view class="navs u-flex"> <view class="search u-padding-20"> <up-search v-model="keyword" :clearabled="true" actionText="搜索" :showAction="true" height="30" searchIconSize="22" @custom="onKeyword" @search="onKeyword" :animation="true"></up-search> </view> <view> <view> <view class="cate "> <view> <text></text></view> <view class="name u-line-1"> <text>融创乐园</text> <image src="../../static/henxian.png"></image> </view> <view> <text></text></view> </view> <view class="cate active"> <view> <text></text></view> <view> <text class=" u-line-1">海世界 </text> <image src="../../static/henxian.png"></image> </view> <view> <text></text></view> </view> <view class="cate "> <view> <text></text></view> <view class="name u-line-1"> <text>水乐园</text> <image src="../../static/henxian.png"></image> </view> <view> <text></text></view> </view> </view> <view class="navs-right u-padding-30"> <CateList :list="subcate" @change="changeCate"></CateList> <!-- <view> --> <CateGood :currentCate="currentCate"></CateGood> </view> </view> </view> </kb-layout> </template> <script> import GoodItem from "./componets/good-item.vue"; import CateList from "./componets/cate-list.vue"; import CateGood from "./componets/cate-good.vue"; import { getBiwanPage, getSuggestGoodsList, } from "@/common/api.js"; export default { components:{ GoodItem, CateList, CateGood, }, name:"pageMall", props:{ title: { Type: String, default: '' } }, data() { return { keyword:"", goods: [], subcate:[ {name: '门票',id: 1 }, {name: '夜宿体验',id: 2 }, {name: '成人票',id: 3 }, {name: '研学大本营',id: 4 }, ], scrollTop : 0, currentTop: 0, currentCate: undefined, } }, methods: { onKeyword () {}, fetchData() { getSuggestGoodsList().then( ret => this.goods = ret.data) }, /*滚动商品到对应分类*/ changeCate(item ){ this.currentCate = item; }, }, mounted() { this.fetchData(); } } </script> <style> .navs{ display: flex; flex-direction: column; .search{ height: 40px; background-color: #fff; border-bottom: #f2f2f2 1px solid; } .search-list{ display: flex; height: calc(100vh - 160px); } .navs-left{ width: 6rem; background-color: #F2F2F2; overflow-x: hidden; overflow-y: auto; color: #B3B3B3; } .navs-right{ flex:1; background-color: #fff; display: flex; flex-direction: column; .cate-goods{ overflow-y: auto; flex: 1; padding-top: 20px; } } .cate{ text-align: center; height: 3.5rem; display: flex; align-items: center; justify-content: center; font-size: 14px; .name{ position: relative; .henxian{ position: absolute; left: 0; bottom: -5px; height: 15px; width: 130px; object-fit: cover; display: none; } } } .active{ background-color: #fff; position: relative; font-weight: 900; z-index: 1; font-size: 16px; color: #2F2F2F; .name .henxian{ display: block; } .tra{ position: absolute; top: -40rpx; right: 0rpx; width: 40rpx; height: 40rpx; background-color: #f5f5f5; border-bottom-right-radius: 30rpx; text{ position: absolute; top: 0; right: 0; width: 100%; height: 100%; display: inline-block; background-color: #fff; z-index: -1; } } .bra{ position: absolute; bottom: -40rpx; right: 0; width: 40rpx; height: 40rpx; background-color: #f5f5f5; border-top-right-radius: 30rpx; text{ position: absolute; top: 0; right: 0; width: 100%; height: 100%; display: inline-block; background-color: #fff; z-index: -1; } } } } </style>
好了,本文内容全部结束:
下一篇:没有了
实用工具: JSON字符串格式化 | js压缩代码格式化工具 | 异步XMLHttpRequests库axios.js文档 | vue-axios文档 | Go语言文档