(五)Vue项目——微商城:实现商品分类、购物车等功能
通过window+Apache+php+MySQL 服务器配置提升对项目开发认识。
目录
一、商品分类
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
}
},
关注我,嘿嘿
更多推荐
所有评论(0)