前置加入购物车
购物车唤起&加入购物车
通过点击加入购物车
首先, 我们需要在vant中找到对应的组件, 这里是ActionSheet
组件。 通过对ActionSheet
组件的修改, 从而得到我们需要的内容。
这里我将已经修改过的代码展示出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'"> <div class="product"> <div class="product-title"> <div class="left"> <img src="http://cba.itlike.com/public/uploads/10001/20230321/8f505c6c437fc3d4b4310b57b1567544.jpg" alt=""> </div> <div class="right"> <div class="price"> <span>¥</span> <span class="nowprice">9.99</span> </div> <div class="count"> <span>库存</span> <span>55</span> </div> </div> </div> <div class="num-box"> <span>数量</span> 数字框占位 </div> <div class="showbtn" v-if="true"> <div class="btn" v-if="true">加入购物车</div> <div class="btn now" v-else>立刻购买</div> </div> <div class="btn-none" v-else>该商品已抢完</div> </div> </van-action-sheet>
|
通过上述的代码, 然后就可以v-model="showPannel"
来进行控制, 如果为true显示。(当然showPannel
需要我们在data中去定义)
接下来我们就可以通过在页面中点击购买或者添加购物车按钮中通过点击来实现唤起弹层的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <div @click="addFn" class="btn-add">加入购物车</div> <div @click="buyFn" class="btn-buy">立刻购买</div>
data () { return { showPannel: false , node: 'cart' ,
} },
methods: { addFn() { this.showPannel = true this.node = 'cart' }, buyFn() { this.showPannel = true this.node = 'buyNo' }, }
|
同理, 对于不同的点击效果, 我们需要使用不同的内容来显示, 加入购物车和 立即购买 就需要两个不同的弹层来显示效果。所以这里就还需要在data中定义不同的弹层显示状态。通过node
变量来改变。
1
| <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
|
封装弹层数字框组件
组件名 CountBox
- 静态结构,左中右三部分
- 数字框的数字,应该是外部传递进来的 (父传子)
- 点击 + - 号,可以修改数字 (子传父)
- 使用 v-model 实现封装 (:value 和 @input 的简写)
- 数字不能减到小于 1
- 可以直接输入内容,输入完成判断是否合法
在prodetail/index.vue
中调用组件
1 2 3 4
| <div class="num-box"> <span>数量</span> <CountBox v-model="addCount"></CountBox> </div>
|
然后就是创建组件, 按照对应的要求完成。
注意 使用v-model
可以实现双向绑定, 但是如果直接使用v-model
会导致数据流向不清晰,使得后期的开发乃至维护都变的异常煎难, 所以我们在父组件中通过v-model
来进行维护, 在子组件中通过props来接收。
但是在子组件中我们需要解析v-model
从而使用:value
和 @input/change
来将输入框中改变的内容来实时传输通过$emit
显示到父组件中, 然后展示出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <template> <div class="count-box"> <button @click="handleSub" class="minus">-</button> <input :value="value" @change="handleChange" class="inp" type="text"> <button @click="handleAdd" class="add">+</button> </div> </template>
<script> export default { props: { value: { type: Number, default: 1, } }, methods: { handleSub () { if (this.value <= 1) { return } this.$emit('input', this.value - 1) }, handleAdd () { this.$emit('input', this.value + 1) }, handleChange (e) { const num = +e.target.value
if (isNaN(num) || num < 1) { e.target.value = this.value return }
this.$emit('input', num) } } } </script>
|
加入购物车操作
点击加入购物车需要登录, 如果用户未登录需要弹出组件来进行提醒用户登录, 我们这里是用的是vant组件库中的Dialog
组件, 如下:
等用户登录完成还需要跳转至用户浏览的界面或者购物车界面, 这里就需要在dialog
中的then中添加一个参数query
1 2 3 4 5 6 7 8
| .then(() =>{ this.$router.replace({ path: '/login', query: { backUrl: this.$route.fullPath } })
|
如果用户跳转到登录页面是从我们点击加入购物车这里跳转过去的, 那么就需要使用this.$route.fullPath
来携带一个参数, 相当于表示符。 如果用户最后想要返回到对应的商品页面就需要在login/index.vue
页面的点击登录方法中添加判断。
1 2 3 4 5 6 7
| if(this.$route.query.backUrl){ this.$router.replace(this.$route.query.backUrl) }else{ this.$router.push('/') } this.$toast('登录成功')
|
通过上述的操作, 用户在商品页面添加到购物车里的, 那么登录之后还是能够跳转到对应的商品详情页面。
注意: 这里跳转回去我们使用的是replace
而不是push
这其中的好处就是不会增加额外的历史记录。
当然对于立即购买就不是登录之后跳转到对应的商品详情页面,而是跳转到购买的界面。
加入购物车请求接口封装
在api/cart.js
中封装请求对应的接口
1 2 3 4 5 6 7 8 9 10 11
| import request from '@/utils/request'
export const addCart = (goodsId, goodsNum, goodsSkuId) => { return request.post('/cart/add', { goodsId, goodsNum, goodsSkuId }) }
|
接下来就可以在页面中进行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| data () { return { cartTotal: 0, } },
async addCart() { if(!this.$store.getters.token){ this.$dialog.confirm({ title: '温馨提示', message: '此时需要先登录才能继续操作哦', confirmButtonText: '去登录', cancelButtonText: '再逛逛' }).then(() =>{ this.$router.replace({ path: '/login', query: { backUrl: this.$route.fullPath } }) }).catch(() => {}) return } const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id) this.cartTotal = data.cartTotal this.$toast('加入购物车成功') this.showPannel = false console.log('加入购物车成功') },
|
通过这样的方式可以实现加入购物车, 但是请求的时候会报错, 因为我们登录之后没有携带token。 所以需要在配置请求拦截器的时候携带对应user的token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
instance.interceptors.request.use(function (config) { const token = store.getters.token if (token) { config.headers['Access-Token'] = token config.headers.platform = 'H5' } Toast.loading({ message: '加载中...', forbidClick: true, loadingType: 'spinner', duration: 0 }) return config;
}, function (error) { return Promise.reject(error); });
|
通过上述的方式就可以成功完成加入购物车请求
购物车
基本静态结构 (快速实现)
详细看项目代码
https://github.com/Ray2310/MallProject/blob/main/src/views/layout/cart.vue
构建 vuex cart 模块,获取数据存储
所有的购物车数据每个用户登录之后 ,一旦点击加入购物车, 那么数据就是不仅限于模块内部了, 所以需要对数据做公共处理,构建vuex的cart模块, 在模块中, 我们使用的cartList
来接受数据请求获取的数据, 从而实现数据全局化。
- 构建vuex的cart模块, 并实现挂载模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { getCartList } from "@/api/cart" export default { namespaced: true, state(){ return { cartList: [], } }, getters: { }, mutations: { }, actions: { } }
|
- 通过异步请求获取用户的购物车数据, 然后存储到
cartList
中。 同时还需要能够在页面中调用
异步请求需要在actions中完成, 同时需要将获取用户购物车数据的请求封装到api/cart
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| actions: { async getCartAction(context) { const { data } = await getCartList() data.list.forEach(element => { element.isChecked = true }); context.commit('setCartList', data.list) } }
export const getCartList = () =>{ return request.get('/cart/list') }
|
如果想要实现页面调用并显示,需要使用dispatch
, 同时提供一个设置cartList的mutation。
这里我们先在页面加载的created中调用, 同时需要做用户登录的校验处理
1 2 3 4 5 6 7
| created() { if(this.$store.getters.token){ this.$store.dispatch('cart/getCartAction') } }
|
1 2 3 4 5 6
| mutations: { setCartList(state, newList){ state.cartList = newList } },
|
基于 数据 动态渲染(通过mapState) 购物车列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!-- 购物车列表 --> <div class="cart-list"> <div class="cart-item" v-for="item in cartList" :key="item.goods_id"> // ..... 后续省略, 都是基于数据进行渲染即可。
<script> import { mapState } from 'vuex' export default { name: 'CartIndex', // 将数据映射到页面 computed: { ...mapState('cart', ['cartList']) }, }
|
封装 getters 实现动态统计
封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| getters: { cartTotal(state) { return state.cartList.reduce((sum, item, index) => sum + item.goods_num, 0) }, selectCartList(state) { return state.cartList.filter((item)=> item.isChecked === true) }, selectCount (state, getters) { return getters.selectCartList.reduce((sum, item, index) => sum + item.goods_num, 0)
}, selectCartPrice(state, getters) { return getters.selectCartList.reduce((sum, item, index) => sum + item.goods_num * item.goods.goods_price_min, 0) } },
|
在页面中应用, 通过mapgetters来映射getters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <div class="all-total"> <div class="price"> <span>合计:</span> <span>¥ <i class="totalPrice">{{ selectCartPrice }}</i></span> </div> <div v-if="true" class="goPay">结算({{ selectCount }})</div> <div v-else class="delete">删除</div> </div>
<script> import { mapState, mapGetters } from 'vuex' export default { name: 'CartIndex', computed: { ...mapState('cart', ['cartList']), ...mapGetters('cart', ['cartTotal','selectCartList','selectCount','selectCartPrice']) },
|
全选反选功能
点击全选或者全不选
单个复选框操作
1
| <van-checkbox :value="item.isChecked" @click="selectOne(item.goods_id)"></van-checkbox>
|
点击复选框之后, 直接对相应的内容取反即可
对应的方法
1 2 3 4 5 6 7 8 9
| methods: { selectOne(goodsId) { this.$store.commit('cart/changeChecked',goodsId) }, selectAll() { this.$store.commit('cart/changeCheckedAll') },
|
然后需要使用vuex
来对cartList
中的每个isChecked
属性检查然后操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| getters: { isAllChecked(state) { return state.cartList.every(item => item.isChecked === true) } }
mutations: { setCartList(state, newList){ state.cartList = newList }, changeChecked(state, goodsId,getters) { const goods = state.cartList.find((item) => item.goods_id === goodsId ) goods.isChecked = !goods.isChecked }, }
|
全选的复选框 操作
1 2 3 4
| <div class="all-check"> <van-checkbox icon-size="18" :value="isAllChecked" @click="selectAll()"></van-checkbox> 全选 </div>
|
全选复选框有些麻烦, 我们需要通过getter来判断是否cartList中的所有元素都被选中(也就是isChecked === true),如果是, 那么就我们的全选复选框也需要选中, 所以这里用到了:value="isAllChecked"
,但是我们想要点击之后也同样能够全部选中所有的内容, 这就需要使用this.$store.commit('cart/changeCheckedAll')
来操作vuex中的数据了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| mutations: { changeChecked(state, goodsId,getters) { const goods = state.cartList.find((item) => item.goods_id === goodsId ) goods.isChecked = !goods.isChecked }, changeCheckedAll(state) { state.cartList.forEach(element => { element.isChecked = !element.isChecked }); } },
|
数字框修改数量功能
数字框是通过之前封装的子组件(CountBox
), 所以需要使用到父传子,子传父的操作。
首先,操作数量,我们需要对数据库进行操作, 所以点击之后就需要进行对应的请求来修改后台数据库的操作。 但是这里因为hm哪里服务器的问题, 我们暂时修改保存本地的内容。
- 封装 api 接口在
api/cart.js
中定义修改购物车数量的接口
1 2 3 4 5 6 7 8
| export const changeCount = (goodsId, goodsNum, goodsSkuId) => { return request.post('/cart/update', { goodsId, goodsNum, goodsSkuId }) }
|
- 页面中注册点击事件,传递数据 (重点, @input使用箭头函数)
1 2 3 4 5 6 7 8 9 10
| <CountBox :value="item.goods_num" @input="value => changeCount(value, item.goods_id, item.goods_sku_id)"></CountBox>
changeCount (value, goodsId, skuId) { this.$store.dispatch('cart/changeCountAction', { value, goodsId, skuId }) },
|
- 提供 action 发送请求, commit mutation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| mutations: { changeCount (state, { goodsId, value }) { const obj = state.cartList.find(item => item.goods_id === goodsId) obj.goods_num = value } }, actions: { async changeCountAction(context, obj) { const { goodsNum ,goodsId, goodsSkuId } = obj context.commit('changeCount', {goodsId, goodsNum }) } }
|
编辑删除功能
点击编辑之后, 可以对购物车中的内容做删除操作。
- 查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
1 2 3 4 5 6
| export const delSelect = (cartIds) => { return request.post('/cart/clear', { cartIds }) }
|
- 注册删除点击事件
1 2 3 4 5 6 7 8 9
| <div v-else :class="{ disabled: selectCount === 0 }" @click="handleDel" class="delete"> 删除({{ selectCount }}) </div>
async handleDel () { if (this.selCount === 0) return await this.$store.dispatch('cart/delSelect') this.isEdit = false },
|
- 提供 actions
1 2 3 4 5 6 7 8 9 10 11 12
| actions: { async delSelect (context) { const selCartList = context.getters.selectCartList const cartIds = selCartList.map(item => item.id) await delSelectShop(cartIds) Toast("删除成功")
context.dispatch('getCartAction') } },
|
空购物车处理
对于未登录或者已经登录 ,但是没有内容。同样需要处理
修改为这种观感舒服的界面, 只需要一下判断即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!-- 使用一个大盒子来封装处理, 如果未登录则显示不同的页面 --> <div v-if="isLogin && cartList.length > 0"> .... </div> <!-- 如果未登录 或者 购物车为空, 那么就给一个其他的样式 --> <div v-else> <div class="cart-box" v-if="isLogin && cartList.length > 0"> <div class="cart-title"> ... </div> <div class="cart-list"> ... </div> <div class="footer-fixed"> ... </div> </div> <div class="empty-cart" v-else> <img src="@/assets/empty.png" alt=""> <div class="tips"> 您的购物车是空的, 快去逛逛吧 </div> <div class="btn" @click="$router.push('/')">去逛逛</div> </div> </div> </div>
|
订单结算台
点击结算之后, 就会跳转到订单结算台, 并且需要携带对单的相关参数。
注意:从立即购买和订单结算中跳转到订单结算台的参数是不相同的。
获取收货地址列表
1 封装获取地址的接口(在api/address.js
)
1 2 3 4 5 6
| import request from '@/utils/request'
export const getAddressList = () => { return request.get('/address/list') }
|
2 页面中 - 调用获取地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| data () { return { addressList: [] } }, computed: { selectAddress () { return this.addressList[0] } }, async created () { this.getAddressList() }, methods: { async getAddressList () { const { data: { list } } = await getAddressList() this.addressList = list } }
|
3 页面中 - 进行渲染
因为后端对订单的内容并没有做处理, 所以这里我们并没有做渲染内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| computed: { longAddress () { const region = this.selectAddress.region return region.province + region.city + region.region + this.selectAddress.detail } },
<div class="info" v-if="selectAddress?.address_id"> <div class="info-content"> <span class="name">{{ selectAddress.name }}</span> <span class="mobile">{{ selectAddress.phone }}</span> </div> <div class="info-address"> {{ longAddress }} </div> </div>
|
订单结算的跳转传参以及渲染
- 购物车结算,需要两个参数
① mode=”cart”
② cartIds=”cartId, cartId”
- 立即购买结算,需要三个参数
① mode=”buyNow”
② goodsId=”商品id”
③ goodsSkuId=”商品skuId”
购物车订单结算
跳转传参在购物车的订单结算中通过点击事件触发
1 2 3 4 5 6 7 8 9 10 11 12 13
| <div @click="goPay">结算({{ selCount }})</div>
goPay () { if (this.selCount > 0) { this.$router.push({ path: '/pay', query: { mode: 'cart', cartIds: this.selCartList.map(item => item.id).join(',') } }) } }
|
重点: 看传递参数的方式, 通过拼接用户购物车列表中的商品id,作为一个字符串进行传递。
支付界面解析请求内容
页面中接收参数, 调用接口,获取数据
通过使用计算属性的方式来接受参数, 实现动态获取参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| data () { return { order: {}, personal: {} } }, computed: { mode () { return this.$route.query.mode }, cartIds () { return this.$route.query.cartIds } }
async created () { this.getOrderList() },
async getOrderList () { if (this.mode === 'cart') { const { data: { order, personal } } = await checkOrder(this.mode, { cartIds: this.cartIds }) this.order = order this.personal = personal } }
|
最后就是将请求的内容解析, 然后基于res进行渲染。
立即购物的方法结算
和在购物车中的请求结算一样, 只是传递的参数不同而已
1 点击跳转传参
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| prodetail/index.vue <div class="btn" v-if="mode === 'buyNow'" @click="goBuyNow">立刻购买</div>
goBuyNow () { this.$router.push({ path: '/pay', query: { mode: 'buyNow', goodsId: this.goodsId, goodsSkuId: this.detail.skuList[0].goods_sku_id, goodsNum: this.addCount } }) }
|
2 计算属性处理参数
1 2 3 4 5 6 7 8 9 10 11 12
| computed: { ... goodsId () { return this.$route.query.goodsId }, goodsSkuId () { return this.$route.query.goodsSkuId }, goodsNum () { return this.$route.query.goodsNum } }
|
3 基于请求时携带参数发请求渲染
1 2 3 4 5 6 7 8 9 10 11 12 13
| async getOrderList () { ... if (this.mode === 'buyNow') { const { data: { order, personal } } = await checkOrder(this.mode, { goodsId: this.goodsId, goodsSkuId: this.goodsSkuId, goodsNum: this.goodsNum }) this.order = order this.personal = personal } }
|
提交订单并支付
1 封装 API 通用方法(统一余额支付)
1 2 3 4 5 6 7 8 9 10 11
| export const submitOrder = (mode, params) => { return request.post('/checkout/submit', { mode, delivery: 10, couponId: 0, payType: 10, isUsePoints: 0, ...params }) }
|
2 买家留言绑定
1 2 3 4 5 6 7 8 9
| data () { return { remark: '' } }, <div class="buytips"> <textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"> </textarea> </div>
|
3 注册点击事件,提交订单并支付
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <div class="tipsbtn" @click="submitOrder">提交订单</div>
async submitOrder () { if (this.mode === 'cart') { await submitOrder(this.mode, { remark: this.remark, cartIds: this.cartIds }) } if (this.mode === 'buyNow') { await submitOrder(this.mode, { remark: this.remark, goodsId: this.goodsId, goodsSkuId: this.goodsSkuId, goodsNum: this.goodsNum }) } this.$toast.success('支付成功') this.$router.replace('/myorder') }
|
后续的订单管理界面, 通过使用组件的方式实现。 具体实现其实和前面的都一样