手寫類似於BetterScroll樣式的左右聯動菜單 uni-app+vue3+ts (使用了script setup語法糖)

来源:https://www.cnblogs.com/zengbingqian/archive/2023/12/04/17875201.html
-Advertisement-
Play Games

註意:在模擬器用滑鼠滾動是不會切換游標的,因為使用的是觸摸滑動。【自定義類型貼在最後了】 script 部分如下: import { onMounted } from 'vue' import type { orderDetail } from '@/types/category' import t ...


 註意:在模擬器用滑鼠滾動是不會切換游標的,因為使用的是觸摸滑動。【自定義類型貼在最後了】

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文分享自華為雲社區《GaussDB(DWS)查詢優化技術大揭秘》,作者: 胡辣湯。 大數據時代,數據量呈爆髮式增長,經常面臨百億、千億數據查詢場景,當數據倉庫數據量較大、SQL語句執行效率低時,數據倉庫性能會受到影響。本期《GaussDB(DWS)查詢優化技術大揭秘》的主題直播中,我們邀請到華為雲 ...
  • 現代資料庫系統能夠存儲和處理大量數據。因此,由任何一個用戶單獨負責處理與管理資料庫相關的所有活動的情況相對較少。通常,不同的資料庫用戶需要對資料庫的某些部分具有不同級別的訪問許可權:某些用戶可能只需要讀取特定資料庫中的數據,而其他用戶則必須能夠插入新文檔或修改現有文檔。同樣,應用程式可能需要獨特的許可權 ...
  • C(Chapter) C-01.資料庫概述 1.為什麼要用資料庫 持久化(persistence):把數據保存到可掉電式存儲設備(硬碟)中以供之後使用。大多數情況下,特別是企業應用,數據持久化是將記憶體中的數據保存到硬碟上加以"固化",而持久化的實現過程大多使用各種關係資料庫來完成。 持久化的主要作用 ...
  • 本文分享自華為雲社區《深入理解HarmonyOS UIAbility:生命周期、WindowStage與啟動模式探析》,作者:檸檬味擁抱。 UIAbility組件概述 UIAbility組件是HarmonyOS中一種包含UI界面的應用組件,主要用於與用戶進行交互。每個UIAbility組件實例對應最 ...
  • 只有不斷學習和成長,才能適應這個快速變化的世界。 1. 懶載入 1.1 React 懶載入 React 中懶載入 Lazy 與 Suspense 需要搭配使用。 React.lazy 定義: React.1azy 函數能讓你像渲染常規組件一樣處理動態引入的組件。其實就是懶載入。 為什麼代碼要分割? ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 測試發現了一個問題,簡單描述問題就是通過函數刪除一個數組中多個元素,傳入的參數是一個數組索引。 然後發現實際效果有時刪除的不是想要的內容。 具體 Bug 代碼實現: const arr = [1,2,3,4,5,6,7]; cons ...
  • 本文先介紹了 wasm-pack 官方的教程,還有其他組件測試、發佈等的流程先不在這裡介紹了。以下用一個實際開發中的模塊來說一下開發 wasm 組件過程中遇到的問題和解決方法。 ...
  • 小程式上想要實現成點擊標簽跳轉某標簽,在標簽內滾動時隨著超過滾動內容 tab 選中態變化。 藉助了 @vant/weapp 框架 index.wxml <view class="list-page"> <van-tabs sticky active="{{ active }}" bind:click ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...