记录 vue + better-scroll 仿美团菜单左右联动 + 小球飞入购物车

公司提出需求要做一个点餐系统

需求:

点击左侧菜单 右侧滑动到相应菜品分类位置, 右侧滑动时滑动到哪个分类左侧相应分类高亮并且向中部滚动

使用 vue + better-scroll

1、项目下载 better-scroll

npm install better-scroll --save

2、在需要页面或组件引入

import BScroll from 'better-scroll'

因嘴笨直接贴代码了... DOM 结构

<template>
  <div class="Dishes" id="Dishes">
    <div class="header">
      <div class="search">
        <div class="left">
          <img class="picSea" src="../assets/search.png" alt="">
          <van-cell-group>
            <van-field v-model="value" type="search" placeholder="搜索菜品名" class="searchFood"/>
          </van-cell-group>
        </div>
      </div>
    </div>
    <div :class="[{afterScroll: afterScroll}, 'bb']">
      <div class="set">
        <div class="place">
          <h3>苏宁广场店</h3>
          <p>距离您约556.8km</p>
        </div>
        <div class="way" @click="changeWay">
          <p :class="way ? 'active' : ''">堂食</p>
          <p :class="!way ? 'active' : ''">外卖</p>
          <div class="blackBoll" :style="{left: blackBoll}"></div>
        </div>
      </div>
    <div class="advertising">
      <img src="../assets/aaa.png" alt="">
    </div>
    </div>
    <div class="shop" id="shop">
      <!-- 左边 -->
      <div class="menu-wrapper" :style="{height: clientHeightY + 'px'}">
        <ul>
          <!-- current -->
          <li 
            class="menu-item"
            v-for="(goods,index) in searchgoods" 
            :key="index"
            :class="{active: index === currentIndex}"
            @click="clickList(index)"
            ref="menuList"
            >
            <span>{{goods}}</span>
          </li>
        </ul>
      </div>
      <!-- 右边 -->
      <div class="shop-wrapper" :style="{height: clientHeightY + 'px'}">
        <ul ref="itemList" class="food-item">
          <li class="shops-li food-list" v-for="(goods, index1) in searchgoods" :key="index1">
            <div class="shops-title">
              <h4>{{goods}}</h4>
            </div>
            <ul>
              <li v-for="(it, ind) in 8" :key="ind">
                <div class="foodImg">
                  <img src="../assets/shopImg.png" alt="">
                </div>
                <div class="foodPrices">
                  <div class="title">店铺招牌百威啤酒</div>
                  <div class="good">好评  99+</div>
                  <div class="info">
                    <div>
                      <p>¥</p>
                      <p>10</p>
                      <p>/份</p>
                      <p>¥15</p>
                    </div>
                    <div>
                      <i @click="additem" class="iconfont icontianjia"></i>
                    </div>
                  </div>
                </div>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
    <!-- 购物车 -->
    <div class="footer">
      <div class="ball-container">
        <!--小球-->
        <div v-for="(ball, index) in balls" :key="index">
          <transition
            name="drop"
            @before-enter="beforeDrop"
            @enter="dropping"
            @after-enter="afterDrop"
          >
            <div class="ball" v-show="ball.show">
              <div class="inner inner-hook"></div>
            </div>
          </transition>
        </div>
      </div>
      <div class="shoppingCar" @click="showShopCar">
        <i  :class="{addBig: addBig}" class="iconfont icongouwuche"></i>
        <div class="total">
          <div>总计</div>
          <span>¥</span>
          <div class="money">200</div>
        </div>
      </div>
      <div class="submit" @click="submit">
        确定下单
      </div>
    </div>
    <!-- 购物车弹框 -->
    <van-action-sheet v-model="show"  style="z-inde: 5!important;"  @close="onClose">
      <div class="buyCar">
        <div class="title">
          清空购物车
        </div>
        <ul>
          <li v-for="(item, index) in 3" :key="index">
            <div class="shopImg">
              <img src="../assets/shopImg.png" alt="">
            </div>
            <div class="shopInfo">
              <h3>店铺招牌百威啤酒</h3>
              <p>(约500毫升)+冰块</p>
            </div>
            <div class="shopNum">
              10
            </div>
          </li>
        </ul>
      </div>
    </van-action-sheet>
  </div>
</template>

js 代码


<script>
import BScroll from 'better-scroll'
export default {
  data() {
    return {
      addBig: false,
      afterScroll: false,
      oldHeight: 0,
      clientHeightY: 400,
      ortherHeight: 0,
      value: '',
      isScroll: false,
      way: true,
      clientWidth: 0,
      blackBoll: '0px',
      show: false,
      searchgoods: [],
      scrollY: 0, //右侧列表滑动的y轴坐标
      rightLiTops:[], //所有分类头部位置
      balls: [
        //小球 多设置几个 因为可能多次点击
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
      ],
      dropBalls: []
    }
  },
  computed: {
    //动态绑定class类名
    currentIndex(index) {
      const {scrollY,rightLiTops} = this;
      return rightLiTops.findIndex((tops,index )=>{
        this._initLeftScroll(index);
        return scrollY >= tops && scrollY < rightLiTops[index + 1]
      })
    }
  },
  created () {
    this.searchgoods = ['aa', 'bb', 'cc', 'dd', 'ff', 'ee', 'ss', 'vggv', 'asd', 'hgt', 'sad', 'asda', 'iojo', 'asd']
  },
  mounted () {
    // 为了滑动区域高度适应所有机型
    let height = document.body.clientHeight
    let width = document.body.clientWidth
    this.clientWidth = width
    this.clientHeightY = height - width / 750 * 450
    this.oldHeight = this.clientHeightY
    this.ortherHeight = this.clientHeightY + this.clientWidth / 750 * 220
    console.log(this.clientHeightY)
    //监听数据
    this.$nextTick(() =>{
      //左右两边滚动
      this. _initBScroll();
      //右边列表高度
      this._initRightHeight()
    })
  },
  methods: {
    additem(event) {
      this.drop(event.target);
      // this.count++;
      setTimeout(() => {
        this.addBig = true
        setTimeout(() => {
          this.addBig = false
        }, 100)
      }, 400)
    },
    drop(el) {
      //抛物
      for (let i = 0; i < this.balls.length; i++) {
        let ball = this.balls[i];
        if (!ball.show) {
          ball.show = true;
          ball.el = el;
          this.dropBalls.push(ball);
          return;
        }
      }
    },
    beforeDrop(el) {
      /* 购物车小球动画实现 */
      let count = this.balls.length;
      while (count--) {
        let ball = this.balls[count];
        if (ball.show) {
          let rect = ball.el.getBoundingClientRect(); //元素相对于视口的位置
          let x = rect.left - 60;
          let y = -(window.innerHeight - rect.top - 44); //获取y
          el.style.display = "";
          el.style.webkitTransform = "translateY(" + y - 4 + "px)"; //translateY
          el.style.transform = "translateY(" + y + "px)";
          let inner = el.getElementsByClassName("inner-hook")[0];
          inner.style.webkitTransform = "translateX(" + x + "px)";
          inner.style.transform = "translateX(" + x + "px)";
        }
      }
    },
    dropping(el, done) {
      /*重置小球数量  样式重置*/
      let rf = el.offsetHeight;
      el.style.webkitTransform = "translate3d(0,0,0)";
      el.style.transform = "translate3d(0,0,0)";
      let inner = el.getElementsByClassName("inner-hook")[0];
      inner.style.webkitTransform = "translate3d(0,0,0)";
      inner.style.transform = "translate3d(0,0,0)";
      el.addEventListener("transitionend", done);
    },
    afterDrop(el) {
      /*初始化小球*/
      let ball = this.dropBalls.shift();
      if (ball) {
        ball.show = false;
        el.style.display = "none";
      }
    },
    _initBScroll() {
      //左边滚动
      this.leftBscroll = new BScroll('.menu-wrapper',{click: true});
    
      //右边滚动
      this.rightBscroll = new BScroll('.shop-wrapper',{
        probeType:3,
        click: true,
        bounce: false
      });
      //监听右边滚动事件
      this.rightBscroll.on('scroll',(pos) => {
        this.scrollY = Math.abs(pos.y);
        if (this.scrollY >= 30) {
          this.afterScroll = true
          this.clientHeightY = this.ortherHeight
        } else if (this.scrollY >= 3 || this.scrollY === 0) {
          this.afterScroll = false
          this.clientHeightY = this.oldHeight
        }
      })
    },
    
    //求出右边列表的高度
    _initRightHeight () {
      let itemArray = []; //定义一个伪数组
      let top = 0;
      itemArray.push(top)
      //获取右边所有li的礼
      let allList = this.$refs.itemList.getElementsByClassName('shops-li');
      //allList伪数组转化成真数组
      Array.prototype.slice.call(allList).forEach(li => {
        top += li.clientHeight; //获取所有li的每一个高度
        itemArray.push(top)
      });
      this.rightLiTops = itemArray;
      // console.log(this.rightLiTops)
    },
    //点击左边实现滚动
    clickList(index){
      console.log('111')
        this.scrollY = this.rightLiTops[index];
        console.log(this.scrollY)
        this.rightBscroll.scrollTo(0, -this.scrollY, 600,)
    },
    //左右联调 
    _initLeftScroll(index){
      let menu = this.$refs.menuList;
      let el = menu[index];
      this.leftBscroll.scrollToElement(el, 200 , 0, true)
    },
    submit() {
      this.$router.push('/order')
    },
    onClose() {
      this.show = false
    },
    showShopCar() {
      this.show = true
    },
    changeWay() {
      this.way = !this.way
      this.way ? this.blackBoll = '0px' : this.blackBoll = '38px'
    },
    
  }
}
</script>

css 样式 预编译语言用的less 适配过rem 基准宽度750

使用的适配插件 postcss-pxtorem


<style scoped  lang="less">

@keyframes changeBig {
  from {
    transform: scale(1.2);
  }
  to {
    transform: scale(1);
  }
}

.addBig {
  animation: changeBig 0.2s;
  // transform: scale(1.2);
  // transition: all 0.1s;
}

.ball{
  position: fixed;
  left: 120px;
  bottom: 60px;
  z-index: 200;
  transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41); /*贝塞尔曲线*/
}
.inner{
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background-color: red;
  transition: all 0.4s linear;
}

.bb {
  height: 220px;
  transition: all 0.5s;
}

.afterScroll {
  opacity: 0;
  transition: all 0.5s;
  height: 0;
}

.Dishes {
  height: 100vh;
  box-sizing: border-box;
  padding-top: 102px;
}
.shop {
  position: relative;
  top: 0;
  padding: 0;
  background: #fff;
  z-index: 10;
}
.menu-wrapper {
  position: absolute;
  left: 0;
  top: 0;
  overflow: hidden;
  height: 800px;
  transition: all 0.5s;
  background: #FFFFFF;
  box-shadow:10px 0 15px -15px rgba(0, 0, 0, 0.5);
  ul {
    padding-bottom: 40px;
    li {
      width: 140px;
      text-align: center;
      line-height: 80px;
    }
    .active {
      background: #FFE103;
    }
  }
}
.shop-wrapper {
  position: absolute;
  left: 140px;
  top: 0;
  overflow: hidden;
  background: #FFFFFF;
  transition: all 0.5s;
  height: 800px;
  .food-item {
    padding-bottom: 80px;
    .shops-li {
      width: 610px;
      // text-align: center;
      // line-height: 800px;
      // height: 800px;
      .shops-title {
        padding: 40px 0 0 40px;
      }
    }
  }
}
.header {
  width: 100%;
  background: #FECF02;
  position: fixed;
  z-index: 5;
  top: 0;
  padding: 0 0 0 60px;
  box-sizing: border-box;
  box-shadow:inset 0px 15px 10px -15px rgba(0, 0, 0, 0.3);
}

.header .search {
  width: 100%;
  height: 102px;
  padding: 16px 0 0 20px;
  margin: 0 auto;
  box-sizing: border-box;
}

.header .search .left {
  width: 580px;
  height: 66px;
  background: #ffffff;
  display: flex;
  justify-content: left;
  border-radius: 33px;
  align-items: center;
}

.header .search .left .searchFood {
  width: 400px;
  line-height: 44px;
}

.header .search .right {
  display: flex;
  width: 180px;
  font-size: 28px;
  justify-content: left;
  align-items: center;
}

.header .search .right i {
  font-size: 10px;
  margin: 8px 0 0 8px;
}

.header .search .picSea {
  width: 40px;
  height: 40px;
  margin-left: 50px;
  /* position: absolute;
  top: 34px;
  left: 130px; */
}

.header .selector ul{
  width: 100%;
  height: 62px;
  display: flex;
  justify-content: space-around;
  font-size: 24px;
}

.header .selector ul li {
  display: flex;
  justify-content: left;
  align-items: center;
}

.header .selector ul li i {
  margin: 6px 0 0 6px;
  font-size: 24px;
}

.set {
  width: 100%;
  height: 110px;
  padding: 0px 84px;
  // padding-top: 110px;
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  font-size: 22px;
  align-items: center;
}

.set .place h3 {
  font-size: 26px;
  font-weight: 600;
  margin-bottom: 10px;
}

.set .place p {
  color: #9FA0A0;
}

.advertising img {
  width: 100%;
  height: 110px;
}

.set .way {
  width: 150px;
  height: 50px;
  display: flex;
  line-height: 50px;
  box-sizing: border-box;
  justify-content: space-between;
  border: 1px solid #000000;
  border-radius: 25px;
  font-weight: 600;
  position: relative;
  z-index: 2;
}

.set .way p {
  width: 50%;
  text-align: center;
  position: relative;
  z-index: 3;
}

.blackBoll {
  position: absolute;
  background: #000;
  border-radius: 25px;
  width: 80px;
  height: 50px;
  z-index: 0;
  top: 0;
  transition: all 0.3s;
}

.set .way .active {
  /* width: 60%; */
  border-radius: 25px;
  color: #ffffff;
  font-weight: 600;
}

/* .left_menu {
} */

// .menu-wrapper {
//   width: 142px;
//   position: absolute;
//   top: 330px;
//   /* background: #FECF02; */
//   box-shadow:10px 0 15px -15px rgba(0, 0, 0, 0.3);
// }

// .menu-item {
//   height: 100px;
//   line-height: 100px;
//   text-align: center;
// }

.foods-wrapper {
  width: 608px;
  height: 860px;
  position: absolute;
  right: 0px;
  top: 330px;
  /* background: hotpink; */
}

.food-list h3{
  // line-height: 30px;
  padding-left: 80px;
  color: #666;
  font-size: 24px;
  /* background: #FECF02; */
}

.food-list h3:nth-child(1) {
  margin-top: 20px;
}

.food-list ul:nth-last-child(1) {
  // padding-bottom: 80x;
}

.food-list ul li {
  width: 540px;
  height: 214px;
  margin: 0 auto;
  margin-top: 30px;
  border-radius: 20px;
  box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.1);
  padding: 24px 40px 22px 28px;
  box-sizing: border-box;
  display: flex;
  justify-content: left;
}

.food-list ul li:nth-last-child(1) {
  // margin-bottom: 40px;
}

.food-list ul li .foodImg img {
  width: 168px;
  height: 168px;
  margin-right: 10px;
}

.food-list ul li .foodPrices {
  flex: 1;
  i {
    font-size: 40px;
  }
}

.food-list ul li .foodPrices .title{
  font-size: 24px;
  font-weight: 600;
  line-height: 60px;
}

.food-list ul li .foodPrices .good {
  font-size: 18px;
  color: #9FA0A0;
  margin-bottom: 25px;
}

.food-list ul li .foodPrices .info {
  display: flex;
  justify-content: space-between;
}

.food-list ul li .foodPrices .info div:nth-child(1) {
  height: 30px;
  width: 140px;
  position: relative;
}

.food-list ul li .foodPrices .info div:nth-child(1) p:nth-child(1) {
  font-size: 18px;
  font-weight: 600;
  position: absolute;
  bottom: 0px;
}

.food-list ul li .foodPrices .info div:nth-child(1) p:nth-child(2) {
  font-size: 36px;
  position: absolute;
  bottom: -5px;
  font-weight: 600;
  left: 20px;
}

.food-list ul li .foodPrices .info div:nth-child(1) p:nth-child(3) {
  font-size: 18px;
  position: absolute;
  bottom: 0;
  font-weight: 600;
  left: 68px;
}

.food-list ul li .foodPrices .info div:nth-child(1) p:nth-child(4) {
  font-size: 18px;
  color: #9FA0A0;
  text-decoration:line-through;
  position: absolute;
  bottom: -2px;
  left: 120px;
}

.current {
  background-color: #FECF02;
}

.footer {
  width: 100%;
  height: 133px;
  position: fixed;
  bottom: 0;
  background: #000000;
  display: flex;
  justify-content: space-between;
  z-index: 201500;
}

.footer .shoppingCar {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
}

.footer .shoppingCar .total {
  color: #ffffff;
  font-size: 24px;
  width: 150px;
  height: 40px;
  display: flex;
  justify-content: left;
  position: relative;
}

.footer .shoppingCar .total div:nth-child(1) {
  margin-right: 10px;
  line-height: 44px;
}

.footer .shoppingCar .total span {
  font-size: 20px;
  position: absolute;
  bottom: 4px;
  left: 60px;
}

.footer .shoppingCar .total .money {
  font-size: 36px;
  position: absolute;
  bottom: 0;
  left: 88px;
}

.footer .shoppingCar i {
  color: #FECF02;
  font-size: 66px;
  margin-right: 28px;
}

.footer .submit{
  width: 290px;
  line-height: 133px;
  background: #FFE103;
  font-size: 36px;
  font-weight: 600;
  text-align: center;
}

.buyCar {
  padding-bottom: 132px;
  box-sizing: border-box;
}

.buyCar .title {
  height: 80px;
  background: #FECF02;
  text-align: right;
  line-height: 80px;
  padding-right: 42px;
  color: #ffffff;
  font-size: 24px;
  font-weight: 600;
}

.buyCar ul {
  padding: 0 42px;
  box-sizing: border-box;
}

.buyCar ul li {
  padding: 20px 0;
  display: flex;
  box-sizing: border-box;
  justify-content: space-between;
  border-bottom: 1px solid #FBF8FB;
}

.buyCar ul li .shopImg img{
  width: 90px;
  height: 90px;
  margin-right: 24px;
}

.buyCar ul li .shopInfo {
  flex: 1;
}

.buyCar ul li .shopInfo h3 {
  font-size: 24px;
  font-weight: 600;
  color: #000000;
  margin-bottom: 16px;
  margin-top: 6px;
}

.buyCar ul li .shopInfo p {
  font-size: 18px;
  color: #9FA0A0;
}

::-webkit-scrollbar {
 
width:0;
 
height:0;
 
color:transparent;
 
}
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容