手写类似于BetterScroll样式的左右联动菜单 uni-app+vue3+ts (使用了script setup语法糖)

  • 手写类似于BetterScroll样式的左右联动菜单 uni-app+vue3+ts (使用了script setup语法糖)已关闭评论
  • 57 次浏览
  • 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[]