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

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

时间:2023-12-04 16:12:04浏览次数:37  
标签:index const BetterScroll script color app value font top

 注意:在模拟器用鼠标滚动是不会切换光标的,因为使用的是触摸滑动。【自定义类型贴在最后了】

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[]

 

标签:index,const,BetterScroll,script,color,app,value,font,top
From: https://www.cnblogs.com/zengbingqian/p/17875201.html

相关文章

  • Android 9.0 app全屏通过系统属性控制手势上滑是否显示虚拟导航栏和状态栏
    1.前言在9.0的系统rom产品定制化os开发中,在系统设置app的全屏后,默认会隐藏导航栏和状态栏,页面全屏显示的时候,然后底部上滑会显示虚拟状态栏和导航栏显示几秒钟后会自动消失,由于项目开发需要要求通过api来控制全屏时上滑是否显示虚拟导航栏和状态栏,这就要从上滑事件分析看如何显......
  • 使用AutoMapper
    1、在控制台中namespaceStudyAutoMapper{publicclassFoo{publicintID{get;set;}publicstringName{get;set;}}publicclassFooDto{publicintID{get;set;}publicstringName{get;se......
  • [script][fdtd]
    fdtd,对象:【监视器反射率】,操作:【使用script语言更改单位units】,【数据保存至txt/mat文件中】;难点:获取监视器中的数据,【.】独特的点运算,rawdata和getresult的区别,使用【?】获取变量的有无和名称,待解决的小困惑点:作图的反射率T的计算公式,获取教学视频中有详细讲解,有空可查看,并不......
  • emscripten 中c 代码引用外部js 函数
    主要是一个简单的学习,webassebly支持通过import调用环境的函数(比如调用浏览器或者nodejs中的一些方法)简单说明方法很多,包含了emscripten提供的调用js的宏,但是以下使用了一个emscripten提供的--js-library功能--js-library简单说明--js-library主要是实现emcc在编译的时......
  • emscripten cmake 简单尝试
    emscripten提供了比较完整的工具链,包含了对于make以及cmake等工具的支持,以下是一个简单的c代码转换为wasm的demo同时基于cmake进行项目管理参考项目项目结构├──CMakeLists.txt├──README.md├──app.js└──src├──add.c├──add.h└─......
  • dapper实现CURD
    dapper的使用 1、引用包 2、student类[Table("dbo.Student")]publicpartialclassStudent{[Required]publicintId{get;set;}publicstringName{get;set;}publicstringAddress{get;set;}}2、DapperRepository中以下两个......
  • mapper 向后端传集合出错
    批量查询<selectid="selectGoods"parameterType="cn.com.xxx.xx.entity.Goods"resultMap="goodsResultMap">selectg.GOODS_ID,g.GOODS_NAME,g.GOODS_CODE,g.GOODS_DATE,g.GOODS_STATE,g.GOODS_NUM_STOCK,g.GOODS_PRICE,g.GO......
  • uniapp 通过命令行创建,运行,发布
    环境安装npminstall-g@vue/cli创建uni-appVue3js版npxdegitdcloudio/uni-preset-vue#viteuniapp-vue3-project源码git: https://gitee.com/dcloud/uni-preset-vue运行、发布uni-appcduniapp-vue3-projectnpminstallnpmrundev:%PLATFORM%npmrunbuild:%PLATFORM%%P......
  • JavaScript的设计模式—构造器模式
    设计模式介绍设计模式是我们在解决问题的时候针对特定问题给出的简洁而优化的处理方案在JS设计模式,最核心的思想:封装变化将变与不变分离,确保变化的部分灵活,不变的部分稳定构造器模式varemployee1={name:'Kerwin',age:100}varemployee2={name:'xiaoming',......
  • 05.app常见bug解析
    一、功能bug内容显示错误功能错误界面显示错乱界面显示后台信息(sql语句、html)推送信息错误二、性能bug加载速度慢应用程序第一次启动速度慢进入到某一个界面加载速度慢启动某一个有动画效果的界面,动画执行过程加载度慢并且有卡顿响应某一个用户事件时,长时间无响应(ANR)......