手写类似于BetterScroll样式的左右联动菜单 uni-app+vue3+ts (使用了script setup语法糖)
注意:在模拟器用鼠标滚动是不会切换光标的,因为使用的是触摸滑动。【自定义类型贴在最后了】
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[]