目录

1.商品分类接口

2.显示一级分类

3.激活选中的分类

4.显示二级分类

5.better-scroll滚动组件

6.单击一级分类切换二级分类

7.滚动二级分类切换一级分类

8.左侧菜单根据激活项自动滚动

1.商品分类跳转商品列表

2.商品列表接口

3.显示商品列表

4.加载更多

1.商品列表跳转商品详情

2.商品详情接口

3.显示商品详情

4.显示商品预览图

5.购买数量组件


一、商品分类

1.商品分类接口

1、确保后台tpadmin项目安装部署正常,API接口可用

测试:

src\pages\Category.vue

<script>

export default {

  data () {

    return {

      menus: []

    }

  },

  created () {

    this.$indicator.open({

      text: '加载中'

    })

    this.$http.get('category').then(res => {

  console.log(res);

      this.$indicator.close();

      this.menus = res.data.data;

    })

  }

}

</script>

浏览器控制台查看API接口返回结果:

2.显示一级分类

src\pages\Category.vue

<template>

  <div>

    <div class="menu">

      <div class="menu-left">

        <ul>

          <li class="menu-item" v-for="(menu,index) in menus" :key="index">

            <p class="text">{{ menu.name }}</p>

          </li>

        </ul>

      </div>

      <div class="menu-right" ref="itemList">

      </div>

    </div>

  </div>

</template>

样式:

<style lang="scss" scoped>

ul {

  margin: 0;

  padding: 0;

}

li {

  list-style: none;

}

.menu {

  display: flex;

  position: absolute;

  text-align: center;

  top: 40px;

  bottom: 50px;

  width: 100%;

  overflow: hidden;

  .menu-left {

    flex: 0 0 80px;

    width: 80px;

    background: #f3f5f7;

    .menu-item {

      height: 54px;

      width: 100%;

      .text {

        width: 100%;

        line-height: 54px;

        margin-bottom: 0;

      }

    }

    .current {

      width: 100%;

      background: #fff;

      .text {

        color: red;

      }

    }

  }

  .menu-right {

    flex: 1;

    background: #fff;

  }

}

</style>

效果:

3.激活选中的分类

单击左边菜单中的某一项,它会变成激活的效果。

给它设置添加一个class,叫做current。

<li class="menu-item" v-for="(menu,index) in menus" :key="index" :class="{current: index === currentIndex}" @click="clickList(index)">

  <p class="text">{{ menu.name }}</p>

</li>

data() {

  return {

    menus: [],

    currentIndex: 0

  }

},

methods: {

  clickList (index) {

    this.currentIndex = index

  }

},

4.显示二级分类

<div class="menu-right">

  <ul>

    <li class="cate" v-for="(menu, index1) in menus" :key="index1">

      <h4 class="cate-title">{{ menu.name }}</h4>

      <ul class="cate-item">

        <li v-for="(item, index2) in menu.sub" :key="index2">

          <a href="#" class="cate-item-wrapper">

            <div class="cate-item-img">

              <img :src="item.image" alt="">

            </div>

            <span>{{ item.name }}</span>

          </a>

        </li>

      </ul>

    </li>

  </ul>

</div>

样式:

  .menu-right {

    flex: 1;

    background: #fff;

    .cate {

      height: 100%;

      .cate-title {

        margin: 0;

        text-align: left;

        font-size: 14px;

        color: #333;

        font-weight: bold;

        padding: 10px;

      }

      .cate-item {

        padding: 7px 10px 10px;

        display: flex;

        overflow: hidden;

        flex-flow: row wrap;

        li {

          width: 33.3%;

          .cate-item-wrapper {

            .cate-item-img {

              width: 100%;

              img {

                width: 70px;

                height: 70px;

              }

            }

            span {

              display: inline-block;

              font-size: 14px;

              color: #333;

            }

          }

        }

      }

    }

  }

运行结果:

5.better-scroll滚动组件

为了在Vue中实现左右菜单联动的滚动效果,

需要借助better-scroll滚动组件来完成。

安装better-scroll

npm install better-scroll@1.15.2 --save

src\pages\Category.vue

导入进来

<script>

  import BScroll from 'better-scroll'

</script>

better-scroll需要进行DOM操作,

需要在menus数据加载完成后,并且已经在页面中显示出来以后,

再来初始化better-scroll。

因此,利用watch监听menu的数据变动,

一旦发生变动,则页面也会发生变化。

为了让页面更新完成后,再初始化better-scroll,

需要使用vue中的this.$nextTick()

watch: {

  menus () {

    // $nextTick用来在下次DOM更新循环结束之后执行延迟回调

    this.$nextTick(() => {

      this._initBScroll()      // 初始化better-scroll

    })

  }

},

在methods中编写_initBScroll()方法,实现better-scroll的初始化。

methods: {

  ……(原有代码)

  // 初始化better-scroll

  _initBScroll() {

    this.leftBscroll = new BScroll('.menu-left', {

      click: true,

      mouseWheel: true

    })

    this.rightBscroll = new BScroll('.menu-right', {

      click: true,

      mouseWheel: true

    })

  }

}

new BScroll():创建一个实例,第1个参数表示对应的元素,第2个参数表示选项。

选项中,click表示是否允许单击,mouseWheel表示可以用鼠标滚动进行滚动。

访问测试,观察页面是否可以进行上下滚动了。

6.单击一级分类切换二级分类

思路:

先算出来,二级分类中,每个分类的位置,

当用户单击一级分类的时候,拿到当前单击的分类的索引值,

然后根据索引值,找到对应的二级分类的位置,

让右侧菜单滚动到该位置。

先算出来右边菜单中的每个二级分类的高度

menus () {

  this.$nextTick(() => {

    this._initBScroll()      // 初始化better-scroll

    this._initRightHeight() // 初始化右边菜单的高度

  })

}

methods中添加:

// 初始化右边菜单的高度

_initRightHeight () {

}

为了获取每个二级分类的高度,需要先获取DOM元素。

<div class="menu-right" ref="itemList">

  ……(原有代码)

</div>

然后获取并记录下来。

_initRightHeight () {

  let itemArray = []

  let top = 0

  itemArray.push(top)

  let allList = this.$refs.itemList.getElementsByClassName('cate')

  // 将 allList 转换为普通数组进行遍历,记录每个元素的高度

  Array.prototype.slice.call(allList).forEach(li => {

    top += li.clientHeight

    itemArray.push(top)

  })

  this.rightLiTops = itemArray

}

保存给data:

data () {

  return {

    menus: [],

    rightLiTops: [],  // 右菜单每项的高度

  }

},

修改clickList()方法

// 单击左菜单中的某一项后,将右菜单切换到对应项下面

clickList (index) {

  this.currentIndex = index

  // scrollTo(x, y, time, easing)

  this.rightBscroll.scrollTo(0, -this.rightLiTops[index])

},

调用了scrollTo()方法,表示滚动到指定的位置。

它有4个参数,

前两个表示x、y坐标,第3个参数表示动画时间,第4个表示动画效果easing。

注意这里的y坐标需要传负数。

测试程序,观察切换效果是否已经实现。

7.滚动二级分类切换一级分类

开发思路:

当右侧菜单滚动时,记下滚动的位置offsetY,

然后根据offsetY找到对应的二级分类的索引,

将左侧菜单中的对应索引的一级分类设为选中效果。

利用scroll事件来实现:

_initBScroll() {

  ……(原有代码)

  this.rightBscroll.on('scroll', (pos) => {

    window.console.log(pos.y)

  })

}

默认情况下,只有鼠标滚轮滚动的时候,才会触发scroll事件:

如果是用鼠标上下拉动页面,则不会触发scroll事件。

为了让鼠标上下拉动页面的时候也触发scroll事件,需要在创建rightBscroll的时候,

给它加上一个选项,把probeType设为3。

this.rightBscroll = new BScroll('.menu-right', {

  click: true,

  mouseWheel: true,

  probeType: 3  // 实时派发scroll事件

})

然后再试一下,用鼠标上下拉动,scroll事件也可以触发了。

然后再改一下代码,将pox.y取绝对值,然后保存给this.scrollY。

this.rightBscroll.on('scroll', (pos) => {

  this.scrollY = Math.abs(pos.y)

})

保存了scrollY以后,在scrollY发生变化的时候,需要左侧菜单激活对应的分类,

可以将currentIndex改为一个计算属性,根据scrollY的变化来修改currentIndex的值。

在data中增加scrollY,并把currentIndex去掉。

data () {

  return {

    menus: [],

    rightLiTops: [],

    scrollY: 0        // 记住右菜单的滚动距离

  }

},

在methods中的clickList()中,把对currentIndex的操作去掉,并保存this.scrollY。

// 单击左菜单中的某一项后,将右菜单切换到对应项下面

clickList (index) {

  this.scrollY = this.rightLiTops[index]

  this.rightBscroll.scrollTo(0, -this.scrollY)

},

然后编写currentIndex计算属性。

computed: {

  currentIndex () {

    // 当 scrollY 发生改变时,修改左菜单的激活项

    const {scrollY, rightLiTops} = this

    // 从右菜单中查找元素,返回元素索引

    return rightLiTops.findIndex((top, index) => {

      if (scrollY >= top && scrollY < rightLiTops[index + 1]) {

        return true

      }

    })

  }

},

这时候,右边滚动的时候,左边对应的分类就能自动选中了。

8.左侧菜单根据激活项自动滚动

上一个小节完成的功能还存在一个问题,

就是如果激活的项超过了显示区域,就看不到哪一项激活了,

所以这个时候需要让左边的菜单能够随着激活项来自动进行滚动。

if (scrollY >= top && scrollY < rightLiTops[index + 1]) {

  this._initLeftScroll(index)

  return true

}

在methods中编写_initLeftScroll(),

// 右菜单滚动时,左菜单联动

_initLeftScroll (index) {

}

为了让左侧菜单自动滚动到指定的元素,可以利用better-scroll里的scrollToElement()来实现。

为了获取元素,先给左边的菜单添加ref。

<li class="menu-item" v-for="(menu,index) in menus" :key="index" :class="{current: index === currentIndex}" @click="clickList(index)" ref="menuList">

  <p class="text">{{ menu.name }}</p>

</li>

让左菜单也进行滚动。

_initLeftScroll (index) {

  let menu = this.$refs.menuList

  let el = menu[index]

  // scrollToElement(el, time, offsetX, offsetY, easing)

  this.leftBscroll.scrollToElement(el, 300, 0, -100)

}

将offsetY设为-100的目的,是为了避免左边的菜单频繁发生自动滚动,

不然会有一跳一跳的感觉,

设为-100可以让自动滚动的幅度更大一些,体验会更好。

还有一个问题,就是当滚动到最后一项的时候,左边永远无法激活:

在页面中,给底部加上一个li,用来撑大底部。

<div class="menu-right">

  <ul>

    ……(原有代码)

    <li class="cate-bottom"></li>

  </ul>

</div>

这个li的高度是自动算出来的。

_initRightHeight () {

  ……(原有代码)

  let bottom = this.$refs.itemList.getElementsByClassName('cate-bottom')[0]

  bottom.style.height = this.$refs.itemList.clientHeight / 1.2 + 'px'

  this.rightLiTops = itemArray

}

在currentIndex计算属性中做一个判断,如果是最后一项,则左边不进行自动滚动了。

return rightLiTops.findIndex((top, index) => {

  if (index === rightLiTops.length - 2) {

    return true

  }

  ……(原有代码)

})

rightLiTops.length表示有多少个

由于index是从0开始的,所以需要给rightLiTops.length减1,两者才能进行比较。

由于rightLiTops中有一个初始的0,所以要减2。

二、商品列表

1.商品分类跳转商品列表

二级分类

src\pages\Category.vue

<router-link class="cate-item-wrapper" :to="{name: 'goods_list', params: {category_id: item.id}}">

  <div class="cate-item-img">

    <img :src="item.image" alt="">

  </div>

  <span>{{ item.name }}</span>

</router-link>

路由:

src\router.js

import GoodsList from './pages/goods/GoodsList.vue'

routes: [

  ……(原有代码)

  { path: '/goodslist/:category_id', component: GoodsList, props: true, name: 'goods_list', meta: { title: '商品列表' } },

],

src\pages\goods\GoodsList.vue

<template>

  <div>商品列表</div>

</template>

<script>

export default {

  props: ['category_id'],

  created () {

    this.getGoodsList()

  },

  methods: {

    getGoodsList () {

      window.console.log(this.category_id)

    }

  }

}

</script>

随意点击一个二级分类,查看是否接收到了id

2.商品列表接口

查询测试:

src\pages\goods\GoodsList.vue

getGoodsList () {

  this.$indicator.open({

    text: '加载中'

  })

  let params = { category_id: this.category_id }

  this.$http.get('goodslist', { params: params }).then(res => {

    this.$indicator.close()

    window.console.log(res.data)

  })

}

浏览器控制台查看API接口返回结果:

3.显示商品列表

src\pages\goods\GoodsList.vue

data () {

  return {

    goodslist: []

  }

},

this.$http.get('goodslist', { params: params }).then(res => {

  this.$indicator.close()

  if (res.data.code === 1) {

    if (res.data.data.length > 0) {

      this.goodslist = this.goodslist.concat(res.data.data)

    } else {

      this.$toast('列表为空')

    }

  }

})

<template>

  <div class="goods-list">

    <div class="goods-item" v-for="item in goodslist" :key="item.id">

      <a href="#">

        <img :src="item.image">

        <h1 class="title">{{ item.name }}</h1>

        <p class="info">

          <span class="price">¥{{ item.price }}</span>

          <span class="sell">剩余 {{ item.num }} 件</span>

        </p>

      </a>

    </div>

  </div>

</template>

样式:

<style lang="scss" scoped>

.goods-list {

  display: flex;

  flex-wrap: wrap;

  padding-left: 10px;

  .goods-item {

    width: calc(calc(100% / 2) - 10px);

    margin: 10px 10px 0 0;

    background: #fff;

    display: flex;

    flex-direction: column;

    justify-content: space-between;

    border-radius: 10px;

    padding: 10px;

    img {

      width: 100%;

    }

    .title {

      font-size: 14px;

      color: #333;

      margin: 10px 0;

    }

    .info {

      display: flex;

      justify-content: space-between;

      margin-bottom: 0;

      .price {

        color: red;

        font-weight: bold;

        font-size: 16px;

      }

      .sell {

        font-size: 13px;

      }

    }

  }

}

</style>

页面效果:

4.加载更多

src\pages\goods\GoodsList.vue

  data () {

    return {

      goodslist: [],

      last_id: 0

    }

  },

  let params = { last_id: this.last_id, category_id: this.category_id }

  if (res.data.code === 1) {

    if (res.data.data.length > 0) {

      this.goodslist = this.goodslist.concat(res.data.data)

      this.last_id = res.data.data[res.data.data.length - 1].id

    } else if (this.goodslist.length > 0) {

      this.$toast('已经到达底部')

    } else {

      this.$toast('列表为空')

    }

  }

<div class="goods-list">

  ……(原有代码)

  <mt-button class="more" v-if="goodslist.length !== 0" size="large" @click="getMore">加载更多</mt-button>

</div>

.goods-list {

  ……(原有代码)

  .more {

    margin: 15px 0;

    font-size: 14px;

  }

}

methods: {

  ……(原有代码)

  getMore () {

    this.getGoodsList()

  }

}

按钮效果:

三、商品详情

1.商品列表跳转商品详情

src\pages\goods\GoodsList.vue

<router-link :to="{name: 'goods_info', params: {id: item.id}}">

  <img :src="item.image">

  <h1 class="title">{{ item.name }}</h1>

  <p class="info">

    <span class="price">¥{{ item.price }}</span>

     <span class="sell">剩余 {{ item.num }} 件</span>

  </p>

</router-link>

src\router.js

import GoodsInfo from './pages/goods/GoodsInfo.vue'

routes: [

  ……(原有代码)

  { path: '/goodsinfo/:id', component: GoodsInfo, props: true, name: 'goods_info', meta: { title: '商品信息' } }

],

src\pages\goods\GoodsInfo.vue

<template>

  <div>商品详情</div>

</template>

<script>

export default {

  props: ['id'],

  created () {

    this.getGoodsInfo()

  },

  methods: {

    getGoodsInfo () {

      window.console.log(this.id)

    }

  }

}

</script>

查看是否接收到了id

2.商品详情接口

src\pages\goods\GoodsInfo.vue

getGoodsInfo () {

  this.$indicator.open({

    text: '加载中'

  })

  let params = { id: this.id }

  this.$http.get('goodsinfo', { params: params }).then(res => {

    this.$indicator.close()

    window.console.log(res.data)

  })

}

浏览器控制台查看API接口返回结果:

3.显示商品详情

src\pages\goods\GoodsInfo.vue

data () {

  return {

    goodsinfo: {},    // 获取到的商品信息

  }

},

this.$http.get('goodsinfo', { params: params }).then(res => {

  this.$indicator.close()

  if (res.data.code === 0) {

    this.$toast(res.data.msg)

  } else if (res.data.code === 1) {

    if (res.data.data) {

      this.goodsinfo = res.data.data

    } else {

      this.$messagebox.alert('商品不存在').then(() => {

        this.$router.go(-1)

      })

    }

  } else {

    this.$toast('加载失败,服务器异常')

  }

})

页面:

<template>

  <div class="goods-info">

    <!-- 商品购买区域 -->

    <div class="mui-card">

      <div class="mui-card-header">{{ goodsinfo.name }}</div>

      <div class="mui-card-content">

        <div class="mui-card-content-inner">

          <p class="price">

           定价:<span>¥{{ goodsinfo.price }}</span>

          </p>

          <div v-if="goodsinfo.num">

            <p class="go-buy">

              <mt-button type="primary" size="small">立即购买</mt-button>

              <mt-button type="danger" size="small">加入购物车</mt-button>

            </p>

          </div>

          <div v-else>该商品暂时无货</div>

        </div>

      </div>

    </div>

    <!-- 商品参数区域 -->

    <div class="mui-card">

      <div class="mui-card-header">商品参数</div>

      <div class="mui-card-content">

        <div class="mui-card-content-inner">

          <p>商品卖点:{{ goodsinfo.sell_point }}</p>

          <p>库存情况:{{ goodsinfo.num }}件</p>

          <p>上架时间:{{ goodsinfo.create_time }}</p>

        </div>

      </div>

      <div class="mui-card-header">商品详情</div>

      <div class="good-desc">

        <div class="content" v-html="goodsinfo.content"></div>

      </div>

    </div>

  </div>

</template>

样式:

<style lang="scss" scoped>

.goods-info {

  background: #f1f1ff;

  overflow: hidden;

  .price {

    span {

      color:red;

      font-size: 14px;

      font-weight: bold;

    }

  }

  .go-buy {

    display: flex;

    margin: 10px 0 0px;

    justify-content: center;

    button {

      margin: 0 5px;

    }

  }

  .good-desc {

    background: #fff;

    padding: 5px;

    h3 {

      font-size: 16px;

      color: #226aff;

      text-align: center;

      margin: 15px 0;

    }

    .content {

      font-size: 14px;

      line-height: 28px;

      img {

        width: 100%;

      }

    }

  }

}

</style>

页面效果:

在后台设置一下库存,有货的情况下显示:

后台网址:http://tpadmin.test/admin,账号:admin,密码123456

4.显示商品预览图

src\pages\goods\GoodsInfo.vue

import swiper from '../../components/swiper.vue'

export default {

  ……(原有代码)

  components: {

    swiper

  }

}

<template>

  <div class="goods-info">

    <!-- 商品展示图 -->

    <div class="mui-card">

      <div class="mui-card-content">

        <div class="mui-card-content-inner">

          <swiper :imgList="goodsinfo.album"></swiper>

        </div>

      </div>

    </div>

    ……(原有代码)

  </div>

</template>

需要在后台上传一下图片(预览图)。

然后看一下效果。

5.购买数量组件

商品详情页和购物车页面,都可以更改要购买的数量。

期待的效果:

src\pages\goods\GoodsInfo.vue

<div v-if="goodsinfo.num">

  <p class="go-num">

    购买数量

    <numbox @count="countChange" :max="goodsinfo.num" initcount="1" :goodsid="goodsinfo.id"></numbox>

  </p>

  ……(原有代码)

</div>

封装一个numbox组件,

@count表示绑定count事件,

这个事件会在numbox里的值发生改变的时候,将 值 传过来。

:max是传给组件的一个属性,这里表示用户所能购买的最大值。

initcount也是传给组件的一个属性,表示numbox的初始值。

goodsid表示商品的id。

写一下这个组件:

src\components\numbox.vue

<template>

  <div class="mui-numbox" data-numbox-min="1" :data-numbox-max="max" >

    <button class="mui-btn mui-btn-numbox-minus" type="button">-</button>

    <input class="mui-input-numbox" type="number" :value="initcount" @change="countChanged" ref="num" />

    <button class="mui-btn mui-btn-numbox-plus" type="button">+</button>

  </div>

</template>

<script>

import mui from '../lib/mui/js/mui.min.js'

export default {

  mounted () {

    mui('.mui-numbox').numbox()

  },

  methods: {

    countChanged () {

      var count = parseInt(this.$refs.num.value)

      this.$emit('count', { id: this.goodsid, count: count })

    }

  },

  props: ['initcount', 'max', 'goodsid'],

  watch: {

    'max' (newVal) {

      mui('.mui-numbox').numbox().setOption('max', newVal)

    }

  }

}

</script>

修改根目录的babel.config.js,忽略对mui.min.js的eslint校验:

module.exports = {

  ……(原有代码)

  ignore: [

      "./src/lib/mui/js/mui.min.js"

    ]

}

引入组件:

src\pages\goods\GoodsInfo.vue

import numbox from '../../components/numbox.vue'

components: {

  swiper,

  numbox

}

接收组件传过来的值

methods: {

  ……(原有代码)

  countChange (goodsinfo) {

    window.console.log(goodsinfo)

  }

},

样式:

.goods-info {

  ……(原有代码)

 .go-num {

    div {

      margin-left: 10px;

    }

  }

}

页面效果:

按下“+”或“-”,观察控制台中是否可以看到结果。

保存购买数量

countChange (goodsinfo) {

  this.selectedCount = goodsinfo.count

}

data () {

  return {

    goodsinfo: {},

    selectedCount: 1,  // 保存商品数量,默认是1

  }

},

关注我,嘿嘿

Logo

前往低代码交流专区

更多推荐