- A+
所属分类:Web前端
注意:在模拟器用鼠标滚动是不会切换光标的,因为使用的是触摸滑动。【自定义类型贴在最后了】
script 部分如下:
import { onMounted } from 'vue' import type { orderDetail } from '@/types/category' import type { mainArr } from '@/types/main-arr' import { nextTick, ref } from 'vue' import { getCurrentInstance } from 'vue' //页面加载 onMounted(async () => { await getListData() }) //#region 左右联动菜单 const instance = getCurrentInstance() //分类列表数据--可以多写几个 const categoryList = [ { id: '1', name: '即食', picture: 'el-icon-chicken', children: [ { deveicId: 1, memo: '泸州老窖特曲浓香型白酒', discount: 100, id: 2, inventory: 3, goodsName: '草莓', orderNum: 1, goodsPicPath: '/static/images/locate.png', price: 8.0, orderMoney: 0, oldPrice: 0, isLimitPromotion: false, }, ], }, ] const mainArray = ref<mainArr>([]) //右侧显示内容(标题+文本) const topArr = ref<any[]>([]) //每个锚点与到顶部距离 const leftIndex = ref(0) //左边光标index const isMainScroll = ref<boolean>(false) // 是否touch到右侧 const scrollInto = ref('') //锚点 /* 获取列表数据 */ const getListData = async () => { const left = ref<string[]>([]) const main = ref<mainArr>([]) categoryList.forEach((item) => { left.value.push(`${item.id + 1}类商品`) let list: orderDetail[] = [] // for (let i = 0; i < 10; i++) item.children.forEach((itm) => { list.push(itm) }) main.value.push({ title: item.name, list, }) }) mainArray.value = main.value await nextTick(() => { setTimeout(() => { getElementTop() }, 10) }) } //获取距离顶部的高度 const getScrollTop = (selector: string) => { const top = new Promise((resolve, reject) => { let query = uni.createSelectorQuery().in(instance) query .select(selector) .boundingClientRect((data: any) => { resolve(data.top) }) .exec() }) return top } /* 获取元素顶部信息 */ const getElementTop = async () => { /* Promise 对象数组 */ let p_arr: number[] = [] /* 遍历数据,创建相应的 Promise 数组数据 */ for (let i = 0; i < mainArray.value.length; i++) { const resu = await getScrollTop(`#item-${i}`) p_arr.push(Number(resu) - 200) } /* 主区域滚动容器的顶部距离 */ getScrollTop('#scroll-el').then((res: any) => { let top = res // #ifdef H5 top += 43 //因固定提示块的需求,H5的默认标题栏是44px // #endif /* 所有节点信息返回后调用该方法 */ Promise.all(p_arr).then((data) => { topArr.value = data }) }) } /* 主区域滚动监听 */ const mainScroll = (e: { detail: { scrollTop: any } }) => { if (!isMainScroll.value) { return } let top = e.detail.scrollTop let index = -1 if (top >= topArr.value[topArr.value.length - 1]) { index = topArr.value.length - 1 } else { index = topArr.value.findIndex((item: any, index: number) => { return topArr.value[index + 1] >= top }) } leftIndex.value = index < 0 ? 0 : index } /* 主区域触摸 */ const mainTouch = () => { isMainScroll.value = true } /* 左侧导航点击 */ const leftTap = (e: any) => { let index = e.currentTarget.dataset.index isMainScroll.value = false leftIndex.value = Number(index) scrollInto.value = `item-${index}` } //#endregion
template部分如下:
<view class="content" > <view class="list_box"> <!-- 菜单左边 --> <view class="left"> <scroll-view scroll-y class="scroll"> <view class="item" v-for="(item, index) in categoryList" :key="index" :class="{ active: index == leftIndex }" :data-index="index" @tap="leftTap($event)" > {{ item.name }} </view> </scroll-view> </view> <view class="main"> <scroll-view scroll-y @scroll="mainScroll" class="scroll" :scroll-into-view="scrollInto" :scroll-with-animation="true" @touchstart="mainTouch" id="scroll-el" enhanced :show-scrollbar="false" > <view v-for="(item, index) in mainArray" class="item-first-box" :key="index"> <view :id="'item-' + index"> <text class="item-first-title">{{ item.title }}</text> <view class="item-first-content" v-for="(goods, index2) in item.list" :key="index2"> <view class="goods-image-box"> <image :src="goods.goodsPicPath" mode="aspectFill" class="goods-image" /> </view> <view class="meta"> <view> <view class="name ellipsis">{{ goods.goodsName }}</view> <view class="memo">{{ goods.memo }}</view> <view class="activity-tips" v-if="goods.isLimitPromotion">限时优惠</view> </view> <view class="price"> <view> <view class="actual"> <text class="symbol">¥</text> <text>{{ goods.price.toFixed(2) }}</text> </view> <view class="oldprice" v-if="goods.oldPrice != 0 && goods.price < goods.oldPrice" > <text class="symbol">¥</text> <text>{{ goods.oldPrice!.toFixed(2) }}</text> </view> </view> </view> </view> </view> </view> </view> <view style="height: 80%"></view> </scroll-view> </view> </view> </view>
scss样式:
page { height: 100%; overflow: hidden; background: #f6f6f6; } .content { .list_box { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; font-size: 28rpx; height: calc(100vh - 380rpx); .left { width: 200rpx; text-align: center; background-color: #f6f6f6; line-height: 100rpx; box-sizing: border-box; font-size: 32rpx; color: #666; height: 100%; .item { position: relative; &:not(:first-child) { margin-top: 1px; &::after { content: ''; display: block; height: 0; border-top: #d6d6d6 solid 1px; width: 620upx; position: absolute; top: -1px; right: 0; transform: scaleY(0.5); } } &.active, &:active { color: #000000; background-color: #fff; } } } .main { height: 100%; background-color: #fff; padding: 0 20rpx; flex-grow: 1; box-sizing: border-box; .item-first-box { position: relative; padding-top: 20rpx; width: 100%; } .item-first-title { position: relative; margin-top: 20rpx; } .item-first-content { position: relative; padding-top: 20rpx; margin-bottom: 20rpx; height: 180rpx; .goods-image-box { width: 200rpx; position: relative; float: left; z-index: 999; } .goods-image { position: relative; width: 170rpx; height: 170rpx; border-radius: 10rpx; } .goods-inventory { width: 170rpx; height: 36rpx; border-radius: 0 0 10rpx 10rpx; margin-right: 20rpx; opacity: 60%; background-color: #5c9888; position: absolute; bottom: 0rpx; left: 0; font-size: 24rpx; color: white; text-align: center; } .goods-inventory-notenough { position: absolute; width: 170rpx; text-align: center; font-size: 22rpx; bottom: 4rpx; left: 0; color: white; } .goods-inventory-zero { position: absolute; width: 170rpx; text-align: center; font-size: 22rpx; bottom: 4rpx; left: 0; color: white; } } .meta { position: relative; display: inline; } .name { height: 40rpx; font-size: 26rpx; color: #444; font-weight: bold; } .memo { display: flex; margin-top: 6rpx; font-size: 22rpx; color: #888; } .activity-tips { display: flex; margin-top: 15rpx; font-size: 22rpx; background-color: #ffd8cb; color: #fc6d3f; border-radius: 10rpx; padding-left: 10rpx; padding-right: 10rpx; width: 110rpx; } .type { line-height: 1.8; padding: 0 15rpx; font-size: 24rpx; align-self: flex-start; border-radius: 4rpx; color: #888; background-color: #f7f7f8; } .price { display: flex; position: relative; margin-top: 16rpx; font-size: 24rpx; .actual { color: #444; margin-top: 2rpx; margin-left: 0rpx; float: left; } .oldprice { display: inline-block; font-size: 24rpx; margin-top: 2rpx; color: #999; margin-left: 10rpx; text-decoration: line-through; } .symbol { font-size: 24rpx; } .quantity { position: absolute; top: 0; right: 0; font-size: 24rpx; color: #444; z-index: 999999999; } } .right-scroll:last-child { border-bottom: 0; } } .scroll { height: 100%; } } }
category.d.ts
/** 通用商品类型 */ export type GoodsItem = { deveicId?: number /** 商品描述 */ memo: string /** 商品折扣 */ discount: number /** id */ id: number /**库存 */ inventory: number /** 商品名称 */ goodsName: string /** 商品已下单数量 */ orderNum: number /** 商品图片 */ goodsPicPath: string /** 商品价格 */ price: number /** 商品原价格 */ oldPrice?: number /**促销id */ promotionDetialId?: number /**是否是限时优惠 */ isLimitPromotion: boolean orderMoney:number oldPrice:number }
main-arr.d.ts
export type main = { title: string list: orderDetail[] } export type mainArr = main[]