uniapp点餐或商城系统菜单左右联动

目前市面上很多小程序或者app都出现点餐或者商城类的,这里使用到了uniapp的内置组件scroll-view

注意:

这个组件用的时候要注意需要给滚动区域设置高度,滚动才能生效

需要给二级分类添加id,并且值不能已数字开头,可以加上类似于 :id="'item-' + index"与js中的api对应

实现的效果,如图所示

因为是从完整的代码中取得,若有样式问题请自行搭建补充

搭建html

左侧菜单品类


      <!-- 左侧一级分类 -->
      <scroll-view
        scroll-y="true"
        :scroll-with-animation="false"
        class="leftColumn"
      >
        <view>
          <view
            class="menu-item"
            v-for="(good, index) in orderlist"
            :key="good.categoryCode"
            :class="{ current: index === leftCilck }"
            :style="{ color: index === leftCilck ? iconColor : '#000' }"
            @click="clickMenuItem(index)"
          >
            <image
              :showLoading="true"
              v-if="good.categoryImage"
              :src="good.categoryImage"
              class="firstLevelImage"
            ></image>
            <span v-if="good.categoryName">{{ good.categoryName }}</span>
          </view>
        </view>
      </scroll-view>
    

右侧菜单二级分类


      <!-- 右侧联动二级分类 -->
      <scroll-view
        ref="rightList"
        scroll-y="true"
        :scroll-with-animation="false"
        @scroll="mainScroll"
        :scroll-into-view="scrollInto"
      >
        <view class="'foods-wrapper'">
          <view
            class="food-list-hook"
            v-for="(item, index) in orderlist"
            :id="'item-' + index"
            :key="index"
          >
            <view class="title">{{ item.categoryName }}</view>
            <view class="box" v-for="v in item.goods" :key="v.goodsCode">
              <view class="box_left">
                <view class="box_left_img">
                  <image :src="v.imageUrl" mode="aspectFill"></image>
                </view>
              </view>
              <view class="box_right">
                <text class="box_right_title">{{ v.goodsName }}</text>
                <span class="describe" v-html="v.goodsDesc"></span>
                <view class="box_right_price">
                  <view class="box_right_price_left">
                    ¥{{ v.amount }}
                    <text class="provinciate_price">¥{{ v.price }}</text>
                  </view>
                  <!-- 多规格产品 -->
                  <view
                    v-if="v.goodsType == '2' || v.goodsType == '3'"
                    class="box_right_price_right"
                    @tap="specifications(v)"
                  >
                    <text>选规格</text>
                  </view>
                  <!-- 单品 -->
                  <u-number-box
                    :asyncChange="true"
                    v-if="v.goodsType == '1'"
                    :min="0"
                    :max="v.maxQuantity"
                    :disablePlus="shopVal == maxQuantity"
                    v-model="v.num"
                    @change="
                      (value) => {
                        menuChange(v, value);
                      }
                    "
                  >
                    <view slot="minus" class="minus">
                      <u-icon name="minus" size="12"></u-icon>
                    </view>
                    <text
                      slot="input"
                      style="text-align: center"
                      class="input"
                      >{{ v.num }}</text
                    >
                    <view slot="plus" class="plus">
                      <u-icon name="plus" color="#FFFFFF" size="12"></u-icon>
                    </view>
                  </u-number-box>
                </view>
              </view>
            </view>
          </view>
        </view>
      </scroll-view>
  

js部分-让滚动区域能够正常滚动跳转

 async onLoad() {
    await this.onRightListFetch();
    await this.initScrollView();
  }, 
methods:{
  clickMenuItem(index) {
      this.leftCilck = index;
      this.scrollInto = "item-" + index;
    },
    /* 主区域滚动监听 */
    mainScroll(e) {
      clearTimeout(this.mainThrottle);
      this.mainThrottle = setTimeout(() => {
        scrollFn();
      }, 10);
      let scrollFn = () => {
        let top = e.detail.scrollTop;
        let index = 0;
        /* 查找当前滚动距离 */
        this.topArr.forEach((item, id) => {
          if (top + 140 >= item) {
            index = id;
            this.leftCilck = index < 0 ? 0 : index;
          }
        });
      };
    },
    /* 初始化滚动区域 */
    initScrollView() {
      return new Promise((resolve, reject) => {
        let view = uni.createSelectorQuery().select(".goods");
        view
          .boundingClientRect((res) => {
            this.scrollTopSize = res.top;
            this.scrollHeight = res.height;
            setTimeout(() => {
              resolve();
            }, 100);
          })
          .exec();
      });
    },
    /* 获取元素顶部信息 */
    getElementTop() {
      new Promise((resolve) => {
        let view = uni.createSelectorQuery().selectAll(".food-list-hook");
        view
          .boundingClientRect((data) => {
            resolve(data);
          })
          .exec();
      }).then((res) => {
        let topArr = res.map((item) => {
          return item.top - this.scrollTopSize; /* 减去滚动容器距离顶部的距离 */
        });
        this.topArr = topArr;
      });
    },
 clickMenuItem(index) {
      this.leftCilck = index;
      this.scrollInto = "item-" + index;
    },
 // 获取右边数据
    async onRightListFetch() {
      this.$nextTick(() => {
        this.initScrollView();
      });
    },
}

变量部分

data() {
    return {
      mainThrottle: null,
      topArr: [],
      scrollInto: "", //左侧定位
      scrollTopSize: "",
      scrollHeight: "",
      leftCilck: 0, //左侧点击菜单
      listHeight: [], //右侧每一分类高度集合
      scrollY: 0, // 右侧滑动的Y轴坐标 (滑动过程时实时变化)
      tops: [], // 所有右侧分类li的top组成的数组  (列表第一次显示后就不再变化)
      cityInfo: null,
      menuList: [],
      indexList: [],
      selection_goods: {}, //选择规格的元素
      orderlist: [],
      mobilePhoneModel: "",
     
    };
  }

css部分

.goods {
  display: flex;
  width: 100%;
  background: #fff;
  overflow: hidden;

  font-size: 28rpx;
  .leftColumn {
    width: 160rpx;
    background: #f3f5f7;
  }
  .menu-wrapper {
    width: 160rpx;
    background: #f3f5f7;
    height: 100%;
  }
  .listMenu-wrapper {
    width: 160rpx;
    background: #f3f5f7;
    padding-bottom: 90rpx;
    box-sizing: border-box;
  }
  .menu-item {
    image {
      height: 60rpx;
      width: 60rpx;
    }
    display: flex;
    min-height: 120rpx;
    padding: 20rpx;
    box-sizing: border-box;
    line-height: 28rpx;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    font-size: 24rpx;
    text-align: center;
    &.current {
      display: flex;
      align-items: center;
      background: #fff;

      font-weight: 700;
    }

    .icon {
      display: inline-block;
      vertical-align: top;
      width: 24rpxx;
      height: 24rpx;
      margin-right: 4rpx;
      background-size: 24rpx 24rpx;
      background-repeat: no-repeat;
    }

    .text {
      display: table-cell;
      width: 112rpx;
      vertical-align: middle;
      font-size: 24rpx;
    }
  }
}
.foods-wrapper {
  padding: 20rpx 30rpx;
  box-sizing: border-box;
  width: calc(100vw - 160rpx);
}
.box {
  margin-bottom: 60rpx;
  display: flex;
  position: relative;
  border-radius: 40rpx;
  .box_left {
    .box_left_img {
      width: 180rpx;
      height: 180rpx;
      position: relative;

      image {
        border-radius: 11rpx;
      }

      .box_left_img_text {
        width: 63rpx;
        height: 36rpx;
        background: #05b6f6;
        border-radius: 8rpx;
        font-size: 24rpx;
        font-weight: 400;
        color: #fdfeff;
        line-height: 36rpx;
        text-align: center;
        padding: 3rpx 5rpx;
        position: absolute;
        box-sizing: border-box;
        top: -10rpx;
        right: -5rpx;
      }
    }
  }

  ::v-deep .box_right {
    margin-left: 20rpx;
    display: flex;
    justify-content: space-around;
    flex-direction: column;
    flex: 1;
    .box_right_title {
      font-size: 30rpx;
      font-weight: 700;
      color: #353535;
      display: inline-block;
      margin: 10rpx 0;
      width: 100%;
    }

    .box_right_price {
      display: flex;
      align-items: center;
      justify-content: space-between;
      line-height: 1;
      width: 100%;
      margin-top: 14rpx;

      .box_right_price_left {
        font-size: 30rpx;
        color: #ff2b21;
        .provinciate_price {
          color: #999;
          font-size: 22rpx;
          margin-left: 12rpx;
          text-decoration: line-through;
          line-height: 46rpx;
        }
      }

      .box_right_price_right {
        width: 101rpx;
        height: 47rpx;
        border-radius: 17rpx;
        font-size: 24rpx;
        font-weight: 400;
        color: #fdfeff;
        line-height: 47rpx;
        text-align: center;
        position: relative;
      }
    }
  }
}
.minus {
  width: 22px;
  height: 22px;
  border-width: 1px;
  border-color: #e6e6e6;
  border-style: solid;
  border-top-left-radius: 100px;
  border-top-right-radius: 100px;
  border-bottom-left-radius: 100px;
  border-bottom-right-radius: 100px;
  @include flex;
  justify-content: center;
  align-items: center;
}

.input {
  padding: 0 10px;
}

.plus {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.soldOut {
  position: absolute;
  width: 100%;
  height: 100%;

  span {
    position: absolute;
    top: -20rpx;
    left: -20rpx;
    color: #fff;
    background-color: #d62f35;
    padding: 2rpx 6rpx;
    border-radius: 8rpx;
    box-sizing: border-box;
  }
}

<think>嗯,用户想要在uniapp里实现一个页面,顶部和左侧分类固定,还有联动效果。首先得考虑页面结构。可能需要分成几个部分:顶部导航栏、左侧分类菜单和右侧菜品列表。 顶部导航栏需要固定,当页面滚动时保持不动。可能需要用position: fixed,但要注意不要被其他元素覆盖。然后左侧的分类菜单,可能也需要固定,这样用户滚动菜品时还能看到分类。右侧则是滚动的菜品列表。 联动效果的话,当滚动右侧列表到某个分类时,左侧对应的分类项要高亮,同时击左侧分类,右侧要自动滚动到对应位置。这可能需要监听右侧的滚动事件,获取当前显示的分类,然后更新左侧的选中状态。反过来,击左侧分类时,要获取右侧对应分类的位置,然后滚动过去。 uniapp里如何实现滚动监听呢?可能需要使用scroll-view组件,者页面的onPageScroll事件。不过scroll-view可能有更好的控制,因为它有@scroll事件,可以实时获取滚动位置。然后需要计算每个分类区块的位置,比如用节的边界信息,保存到一个数组里。当滚动时,判断当前滚动位置落在哪个区间,更新左侧的activeIndex。 左侧分类击时,要滚动到对应的位置。这时候需要获取对应分类的scrollTop值,然后调用scroll-view的scrollTo方法。这里要注意uniapp中操作DOM可能需要使用uni.createSelectorQuery()来获取节信息。 另外,布局方面,可能需要使用flex布局,左侧固定宽度,右侧自适应。顶部导航栏用fixed定位,可能会影响布局,所以右侧和左侧可能需要设置margin-top来避开顶部栏。 样式方面,左侧的选中状态可以通过动态class来控制,比如activeIndex等于索引时添加高亮样式。 测试过程中可能会遇到的问题:滚动事件触发频繁导致性能问题,可能需要做节流处理。节信息获取不到,可能是因为组件还没渲染,需要在onReady生命周期里执行。滚动位置计算不准确,需要确认offsetTop是否正确。 还有,滑动右侧时左侧分类高亮,可能需要将分类的id索引与右侧的区块对应起来,确保正确匹配。数据结构方面,可能需要一个分类数组,每个分类包含id、name和对应的菜品列表。 最后,考虑到uniapp的跨平台特性,不同端可能有不同的表现,尤其是滚动相关的行为,需要真机测试确认效果。</think>我为您梳理一个基于uniapp实现分类联动页面的开发思路,按步骤分解实现原理: 一、页面结构设计(使用flex布局) ```html <template> <view class="container"> <!-- 顶部固定导航 --> <view class="header"></view> <scroll-view class="content" :scroll-y="true" :scroll-with-animation="true" :scroll-top="scrollTop" @scroll="handleScroll"> <!-- 左侧分类导航 --> <view class="left-nav"> <view v-for="(item,index) in categories" :key="item.id" :class="['nav-item', activeIndex === index ? 'active' : '']" @click="selectCategory(index)"> {{item.name}} </view> </view> <!-- 右侧商品列表 --> <view class="right-list"> <block v-for="item in categories" :key="item.id"> <view class="category-title" :id="'cate'+item.id">{{item.name}}</view> <view class="goods-item" v-for="goods in item.list" :key="goods.id"> <!-- 商品内容 --> </view> </block> </view> </scroll-view> </view> </template> ``` 二、核心样式实现(含固定定位) ```css .header { position: fixed; top: 0; height: 60px; z-index: 10; } .left-nav { position: fixed; left: 0; width: 25%; height: calc(100vh - 60px); overflow-y: auto; } .right-list { margin-left: 25%; padding-top: 60px; /* 补偿顶部固定导航 */ } ``` 三、联动逻辑实现 ```javascript export default { data() { return { activeIndex: 0, scrollTop: 0, categoryPos: [], categories: [...] // 分类数据 } }, mounted() { this.calcCategoryPos() }, methods: { // 计算各分类位置 calcCategoryPos() { const query = uni.createSelectorQuery().in(this) this.categories.forEach(item => { query.select('#cate'+item.id).boundingClientRect() }) query.exec(res => { this.categoryPos = res.map(item => item.top) }) }, // 滚动监听 handleScroll(e) { const scrollTop = e.detail.scrollTop this.categoryPos.forEach((pos, index) => { if (scrollTop >= pos - 40) { // 40px偏移量 this.activeIndex = index } }) }, // 击分类导航 selectCategory(index) { this.activeIndex = index this.scrollTop = this.categoryPos[index] } } } ``` 四、性能优化技巧 1. 滚动事件节流处理(使用uni.$emit代替频繁setData) 2. 虚拟列表技术(超长列表时使用) 3. 图片懒加载(使用uni-lazy-load组件) 4. 数据缓存(分类位置信息本地存储) 五、扩展功能建议 1. 加入购物车浮动按钮 2. 分类导航字母索引 3. 商品搜索功能 4. 分类吸顶效果(二次加固定位) 实现要说明: 1. 使用`position: fixed`实现双栏固定 2. 通过`scroll-view`组件实现联动滚动 3. `boundingClientRect`获取元素位置信息 4. 滚动事件与击事件双向绑定 5. CSS布局使用flex实现响应式 开发注意事项: 1. iOS平台滚动惯性问题需特殊处理 2. Android平台可能出现滚动卡顿 3. 确保`scroll-view`高度正确计算 4. 分类数据建议分页加载 5. 真机测试不同分辨率适配
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值