【微信小程序】 实现购物车
原生实现,不使用任何框架,网上没有啥好看的购物车,而且都是抄来抄去的,我来写一个好点的作为参考吧,拿来就能用。
接口自行添加。
链接:购物车代码片段。图片太大了没办法上传到代码片段,自己找几张图片就行。
购物车功能包括:
- 显示默认地址,或选择地址(需定向到你自己的页面去选择,这里不做演示)
- 显示店铺,点击店铺可全选该店铺的所有商品
- 显示商品,可选择商品,增加或减少数量,可左滑删除
- 左滑删除:如果有一个已经显示出删除按钮,未操作,此时滑动另一个商品,之前的显示将回归原位
- 选择商品下方显示总价并结算订单
- 选择全部购物车商品
- 显示已失效商品
效果图片
cart.wxml
<!-- 选择地址 --><view class="choose-address"> <view class="address">配送至:<text>{{address}}</text></view> <image class="img" src="/images/arrow_right_grey.svg"></image></view><!-- 生效商品区域 --><view class="cart-effect"> <block wx:for="{{cartEffectList}}" wx:key="key" wx:for-item="item"> <checkbox-group class="check-group" wx:if="{{item.merchandises.length > 0}}"> <checkbox class="check-store-all" data-store="{{item.store}}" bind:tap="checkStoreAll" checked="{{item.checked || item.merchandiseChecked}}"> <view class="store">{{item.store}}</view> </checkbox> <block wx:for="{{item.merchandises}}" wx:key="key" wx:for-item="merchandise"> <movable-area class="move-area"> <movable-view class="move-view" x="{{merchandise.x}}" data-store="{{item.store}}" data-id="{{merchandise.id}}" direction="horizontal" out-of-bounds="true" damping="50" inertia="true" bind:touchstart="touchStart" bind:touchmove="touchMove" bind:touchend="touchEnd" bindchange="touchChange"> <view class="info"> <checkbox class="merchandise-check" data-store="{{item.store}}" data-merchandise="{{merchandise}}" bind:tap="checkSingle" checked="{{merchandise.checked}}"></checkbox> <view class="merchandise"> <image class="merchandise-img" src="{{merchandise.imgUrl}}"></image> <view class="merchandise-name">{{merchandise.name}}</view> <view class="merchandise-price">¥{{merchandise.price}}/瓶</view> <view class="merchandise-amount"> <view class="minus" data-store="{{item.store}}" data-id="{{merchandise.id}}" bind:tap="minusAmount">-</view> <view class="amount">{{merchandise.amount}}</view> <view class="plus" data-store="{{item.store}}" data-id="{{merchandise.id}}" bind:tap="plusAmount">+</view> </view> </view> </view> </movable-view> <view class="delete" data-store="{{item.store}}" data-id="{{merchandise.id}}" bind:tap="deleteMerchandise">删除</view> </movable-area> </block> </checkbox-group> </block></view><!-- 失效商品 --><view class="cart-lapse"> <block wx:for="{{cartLapseList}}" wx:key="key"> <checkbox-group class="check-group" bindchange="checkStoreAll"> <view class="store">已失效商品({{cartLapseList.length}})</view> <movable-area class="move-area"> <movable-view class="move-view" x="{{item.x}}" data-id="{{item.id}}" direction="horizontal" out-of-bounds="true" damping="50" inertia="true" bind:touchstart="touchStart" bind:touchmove="touchMove" bind:touchend="touchEnd" bindchange="touchChange"> <view class="info"> <checkbox class="merchandise-check" disabled="true"></checkbox> <view class="merchandise"> <view class="sold-out"> <image class="merchandise-img" src="{{item.imgUrl}}"></image> <view class="status">{{item.info}}</view> </view> <view class="merchandise-name">{{item.name}}</view> <view class="merchandise-price">¥{{item.price}}/瓶</view> <view class="merchandise-amount"> <view class="minus" style="color: gray;">-</view> <view class="amount" style="color: gray;">{{item.amount}}</view> <view class="plus" style="color: gray;">+</view> </view> </view> </view> </movable-view> <view class="delete" data-id="{{item.id}}" bind:tap="deleteMerchandise">删除</view> </movable-area> </checkbox-group> </block></view><!-- 结算 --><view class="count"> <checkbox class="check-all" bind:tap="checkAll" checked="{{checkedAll}}">全选</checkbox> <view class="grand">合计:¥{{total}}</view> <button class="lapse {{totalCount > 0 ? 'settle-btn' : ''}}" bind:tap="settleBill" hover-class="settle-bill" disabled="{{totalCount == 0}}" loading="{{showLoading}}">结算({{totalCount}})</button></view>
cart.wxss
page { padding: 16rpx; padding-bottom: 172rpx; box-sizing: border-box;}/* 选择地址 */.choose-address { display: flex; align-items: center; justify-content: flex-start; text-align: left;}.choose-address .address { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.choose-address .img { width: 32rpx; height: 32rpx;}/* 选择地址 *//* 生效商品 */.cart-effect { display: flex; flex-direction: column;}.cart-effect .check-group { background-color: #fbfbfd; padding: 16rpx; box-sizing: border-box; border-radius: 32rpx; margin-top: 16rpx; z-index: 11;}.cart-effect .check-group .check-store-all { display: flex; justify-content: flex-start; align-items: center;}.cart-effect .check-group .store { font-size: 36rpx; font-weight: bolder; margin-left: 8rpx;}.cart-effect .check-group .move-area{ /* 减去删除部分的宽度 */ width: calc(100% - 128rpx); height: 280rpx; position: relative; display: flex;}.cart-effect .check-group .move-area .move-view { display: flex; justify-content: center; align-items: center; height: 100%; /* 上面减了多少宽度,下面就要加多少,不然无法起到遮挡作用 */ width: calc(100% + 128rpx); background-color: #fbfbfd; box-sizing: border-box; z-index: 10; padding-right: 12rpx;}.cart-effect .check-group .move-area .delete { position: absolute; top: 0; bottom: 0; left: 100%; /* 删除部分的宽度 */ width: 128rpx; text-align: center; z-index: 9; display: flex; justify-content: center; align-items: center; background-color: #f43838; color: #fbfbfd;}.cart-effect .check-group .move-area .move-view .info { display: flex; justify-content: center; align-items: center; width: 100vw; overflow: hidden; box-sizing: border-box;}.cart-effect .check-group .move-area .move-view .info .merchandise { display: grid; grid-template-columns: 1fr 2fr 2fr; grid-template-rows: 110rpx 60rpx; grid-column-gap: 12rpx; grid-row-gap: 12rpx;}.cart-effect .check-group .move-area .move-view .info .merchandise .merchandise-img { width: 180rpx; height: 180rpx; border-radius: 50%; grid-column: 1 / 2; grid-row: 1 / 3;}.cart-effect .check-group .move-area .move-view .info .merchandise .merchandise-name { grid-column: 2 / 4; grid-row: 1 / 2; font-size: 32rpx;}.cart-effect .check-group .move-area .move-view .info .merchandise .merchandise-price { grid-column: 2 / 3; grid-row: 2 / 3; font-size: 32rpx; font-weight: 550;}.cart-effect .check-group .move-area .move-view .merchandise .merchandise-amount { grid-column: 3 / 4; grid-row: 2 / 3; text-align: right; display: grid; grid-template-columns: repeat(3, 1fr); align-items: center; text-align: center; line-height: 50rpx;}.cart-effect .check-group .move-area .move-view .merchandise .merchandise-amount .minus { font-size: 42rpx; background-color: #e5e5e5; border-radius: 16rpx;}.cart-effect .check-group .move-area .move-view .merchandise .merchandise-amount .amount { font-size: 36rpx;}.cart-effect .check-group .move-area .move-view .merchandise .merchandise-amount .plus { background-color: #e5e5e5; border-radius: 16rpx; font-size: 42rpx;}/* 失效商品 */.cart-lapse { display: flex; flex-direction: column;}.cart-lapse .check-group { background-color: #fbfbfd; padding: 16rpx; box-sizing: border-box; border-radius: 32rpx; margin-top: 16rpx; z-index: 11;}.cart-lapse .check-group .check-store-all { display: flex; justify-content: flex-start; align-items: center;}.cart-lapse .check-group .store { font-size: 36rpx; font-weight: bolder;}.cart-lapse .check-group .move-area{ /* 减去删除部分的宽度 */ width: calc(100% - 128rpx); height: 280rpx; position: relative; display: flex;}.cart-lapse .check-group .move-area .move-view { display: flex; justify-content: center; align-items: center; height: 100%; /* 上面减了多少宽度,下面就要加多少,不然无法起到遮挡作用 */ width: calc(100% + 128rpx); background-color: #fbfbfd; box-sizing: border-box; z-index: 10; padding-right: 12rpx;}.cart-lapse .check-group .move-area .delete { position: absolute; top: 0; bottom: 0; left: 100%; /* 删除部分的宽度 */ width: 128rpx; text-align: center; z-index: 9; display: flex; justify-content: center; align-items: center; background-color: #f43838; color: #fbfbfd;}.cart-lapse .check-group .move-area .move-view .info { display: flex; justify-content: center; align-items: center; width: 100vw; overflow: hidden; box-sizing: border-box;}.cart-lapse .check-group .move-area .move-view .info .merchandise { display: grid; grid-template-columns: 1fr 2fr 2fr; grid-template-rows: 110rpx 60rpx; grid-column-gap: 12rpx; grid-row-gap: 12rpx;}.cart-lapse .check-group .move-area .move-view .info .merchandise .sold-out { position: relative; width: 180rpx; height: 180rpx; border-radius: 50%;}.cart-lapse .check-group .move-area .move-view .info .merchandise .sold-out .merchandise-img { width: 100%; height: 100%; border-radius: 50%; grid-column: 1 / 2; grid-row: 1 / 3;}.cart-lapse .check-group .move-area .move-view .info .merchandise .sold-out .status{ width: 100%; height: 100%; background-color: #1a1a1a90; position: absolute; top: 0; left: 0; text-align: center; display: flex; justify-content: center; align-items: center; border-radius: 50%; color: #fbfbfb; font-size: 36rpx; font-weight: 500;}.cart-lapse .check-group .move-area .move-view .info .merchandise .merchandise-name { grid-column: 2 / 4; grid-row: 1 / 2; font-size: 32rpx;}.cart-lapse .check-group .move-area .move-view .info .merchandise .merchandise-price { grid-column: 2 / 3; grid-row: 2 / 3; font-size: 32rpx; font-weight: 550;}.cart-lapse .check-group .move-area .move-view .merchandise .merchandise-amount { grid-column: 3 / 4; grid-row: 2 / 3; text-align: right; display: grid; grid-template-columns: repeat(3, 1fr); align-items: center; text-align: center; line-height: 60rpx;}.cart-lapse .check-group .move-area .move-view .merchandise .merchandise-amount .minus { font-size: 42rpx; background-color: #e5e5e5; border-radius: 16rpx;}.cart-lapse .check-group .move-area .move-view .merchandise .merchandise-amount .amount { font-size: 36rpx;}.cart-lapse .check-group .move-area .move-view .merchandise .merchandise-amount .plus { background-color: #e5e5e5; border-radius: 16rpx; font-size: 42rpx;}.count { width: calc(100% - 32rpx); box-sizing: border-box; margin-top: 16rpx; padding: 16rpx; background-color: #fbfbfd; border-radius: 120rpx; position: fixed; bottom: 42rpx; z-index: 99; display: flex; justify-content: space-between; align-items: center; border: 1rpx solid #e5e5e5;}.count .check-all{ font-size: 34rpx; font-weight: 550;}.count .grand { width: 50%; text-align: right; font-size: 32rpx; font-weight: 550; color: #d1362f;}.count .lapse { width: 200rpx; height: 80rpx; color: #fdfdfd; border-radius: 120rpx; margin: 0; text-align: center; background-color: #e5e5e5; padding: 0; line-height: 80rpx;}.count .settle-btn { background-color: #d1362f;}.count .settle-bill { background-color: #d1362f80}
cart.js
Page({ /** * 页面的初始数据 */ data: { address: "XX省XX市XX区XX街道XX号XX室", cartEffectList: [], // 生效商品 example: [ { store: "龙门客栈", checked: false, merchandiseChecked: false, merchandises: [ { id: "1", imgUrl: "/images/wine1.jpg", name: "宫廷玉液酒", price: 180, amount: 1, status: 0, x: 0, checked: false, }, { id: "2", imgUrl: "/images/wine2.png", name: "群英荟萃", price: 98, amount: 99, status: 1, x: 0, checked: false, }, { id: "3", imgUrl: "/images/wine3.png", name: "二锅头", price: 28, amount: 1, status: 1, x: 0, checked: false, }, ] }, { store: "八马茶叶", checked: false, merchandiseChecked: false, merchandises: [ { id: "1", imgUrl: "/images/wine3.png", name: "黑普洱茶饼", price: 1800, amount: 1, status: 1, x: 0, checked: false, }, ] }, ], // 失效商品 cartLapseList: [ { id: "3", imgUrl: "/images/wine2.png", name: "威士忌", info: "已售罄", price: 148, amount: 1, x: 0, status: -1, checked: false, } ], startX: 0, moveStore: '', delBtnW: 128, isLeft: 0, total: 0, totalCount: 0, checkedAll: false, billLoading: false, }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { this.getCartList(); }, /** * 生命周期函数--监听页面显示 */ onShow() { }, /** * 获取用户购物车 */ getCartList() { let cartEffectList = this.data.example; wx.setStorageSync('cartEffectList', cartEffectList); this.setData({ cartEffectList: cartEffectList, }); }, /** * 店铺全选 * @param {*} e */ checkStoreAll(e) { let storeName = e.currentTarget.dataset.store; let cartEffectList = this.data.cartEffectList; let updatedCart = cartEffectList.map(store => { if (store.store === storeName) { // 切换商店的已勾选标记 store.checked = !store.checked; // 重置商店的商品已勾选标记 store.merchandiseChecked = false; store.merchandises = store.merchandises.map(merch => { // 根据商店的已检查标记更新商品的已选择标记 merch.checked = store.checked; return merch; }); } return store; }); this.setData({ cartEffectList: updatedCart }); this.grand(); }, /** * 单个选择 * @param {*} e */ checkSingle(e) { let storeName = e.currentTarget.dataset.store; let merchandiseData = e.currentTarget.dataset.merchandise; let cartEffectList = this.data.cartEffectList; let updatedCart = cartEffectList.map(store => { if (store.store === storeName) { store.merchandises = store.merchandises.map(merch => { if (merch.id === merchandiseData.id) { // 更改商品的选择状态 merch.checked = !merch.checked; // 更改店铺的选中状态 store.merchandiseChecked = merch.checked; } return merch; }); } return store; }); this.setData({ cartEffectList: updatedCart }); this.grand(); }, /** * 减少数量,下限为1 * @param {*} e */ minusAmount(e) { let storeName = e.currentTarget.dataset.store; let id = e.currentTarget.dataset.id; let cartEffectList = this.data.cartEffectList; let updatedCart = cartEffectList.map(store => { if (store.store === storeName) { // 更新商店的已选择商品标记 store.merchandiseChecked = true; store.merchandises = store.merchandises.map(merch => { if (merch.id === id) { // 更新商品的选择状态 merch.checked = true; if (merch.amount > 1) { merch.amount--; } else { wx.showModal({ content: '宝贝数量不能再减少了', showCancel: false, }); } } return merch; }); } return store; }); this.setData({ cartEffectList: updatedCart }); this.grand(); }, /** * 增加数量,上限为99 * @param {*} e */ plusAmount(e) { let storeName = e.currentTarget.dataset.store; let id = e.currentTarget.dataset.id; let cartEffectList = this.data.cartEffectList; let updatedCart = cartEffectList.map(store => { if (store.store === storeName) { // 更新店铺的选中状态 store.merchandiseChecked = true; store.merchandises = store.merchandises.map(merch => { if (merch.id === id) { // 更新商品的选择状态 merch.checked = true; if (merch.amount < 99) { merch.amount++; } else { wx.showModal({ content: '宝贝数量不能再增加了', showCancel: false, }); } } return merch; }); } return store; }); this.setData({ cartEffectList: updatedCart }); this.grand(); }, /** * 删除商品 * @param {*} e */ deleteMerchandise(e) { let storeName = e.currentTarget.dataset.store; let id = e.currentTarget.dataset.id; let cartEffectList = this.data.cartEffectList; let cartLapseList = this.data.cartLapseList; // 遍历每个商店 let updatedEffectCart = cartEffectList.map(store => { if (store.store === storeName) { // 如果是目标商店,过滤掉指定ID的商品 store.merchandises = store.merchandises.filter(merch => merch.id !== id); } return store; }); cartLapseList = cartLapseList.filter(item => item.id !== id); this.setData({ cartEffectList: updatedEffectCart, cartLapseList: cartLapseList }); }, /** * 全选 */ checkAll() { let cartEffectList = this.data.cartEffectList; let checkedAll = this.data.checkedAll; // 使用map方法更新每个商店和商品的选中状态 let updatedCart = cartEffectList.map(store => { return { ...store, checked: !checkedAll, merchandiseChecked: !checkedAll, merchandises: store.merchandises.map(merch => ({ ...merch, checked: !checkedAll })) }; }); this.setData({ cartEffectList: updatedCart, checkedAll: !checkedAll, }); this.grand(); }, /** * 合计 */ grand() { let cartEffectList = this.data.cartEffectList; // 初始化总价和总数量 let total = 0; let totalCount = 0; cartEffectList.forEach(store => { store.merchandises.forEach(merch => { if (merch.checked) { total += merch.amount * merch.price; totalCount += merch.amount; } }); }); this.setData({ total: total, totalCount: totalCount, }); }, /** * 结算 */ settleBill() { this.setData({ showLoading: true, }); // 进行深拷贝 let cartEffectList = JSON.parse(JSON.stringify(this.data.cartEffectList)); // 筛选出未选中的商店或没有选中商品的商店 let filteredStores = cartEffectList.filter(store => { if (store.checked) { // 如果商店被选中,保留它 return true; } // 从商店中筛选出未选中的商品 store.merchandises = store.merchandises.filter(merch => merch.checked); // 如果筛选后,商店有选中的商品,保留该商店 return store.merchandises.length > 0; }); wx.navigateTo({ url: `/pages/index/settle-bill/settle-bill?chosenList=${JSON.stringify(filteredStores)}`, complete: () => { this.setData({ showLoading: false, }); } }); }, /** * 开始滑动 * @param {*} e */ touchStart (e) { let index = e.currentTarget.dataset.id; let store = e.currentTarget.dataset.store; let cartEffectList = this.data.cartEffectList; let cartLapseList = this.data.cartLapseList; // 复位,这样子就能保证一次显示一个删除按钮 for (let i in cartEffectList) { for (let j in cartEffectList[i].merchandises) { cartEffectList[i].merchandises[j].x = 0 } } for (let i in cartLapseList) { cartLapseList[i].x = 0; } // 判断是否为多触点 if (e.touches.length == 1) { // 记录开始触摸的位置 this.setData({ startX: e.touches[0].clientX, cartEffectList: cartEffectList, cartLapseList: cartLapseList }); } }, /** * 开始移动 * @param {*} e */ touchMove (e) { let id = e.currentTarget.dataset.id; let store = e.currentTarget.dataset.store; if (e.touches.length == 1) { // 记录移动的距离 let disX = e.touches[0].clientX - this.data.startX; // 大于0则时向右滑,复位 if (disX >= 0) { // 向右滑 this.setData({ isLeft: 0 }); } else { // 小于则是向左滑 this.setData({ isLeft: 1 }); } } }, /** * 滑动终点 * @param {*} e */ touchEnd (e) { let id = e.currentTarget.dataset.id; let store = e.currentTarget.dataset.store; let delw = this.data.delBtnW; if (e.touches.length == 1) { let endX = e.touches[0].clientX - this.data.startX; if (endX < 0) { this.setXmove(id, store, - delw); } else { this.setXmove(id, store, 0); } } }, /** * 滑动事件 * @param {*} e */ touchChange (e) { let delw = this.data.delBtnW; let store = e.currentTarget.dataset.store; let id = e.currentTarget.dataset.id; if (this.data.isLeft) { if (e.detail.source == 'friction') { if (e.detail.x < 0) { this.setXmove(id, store, -delw); } else { this.setXmove(id, store, 0); } } } else { if (e.detail.source == 'friction') { this.setXmove(id, store, 0); } } }, /** * 设置起始位置 * @param {*} id * @param {*} store * @param {*} x */ setXmove(id, store, x) { let that = this; if (store) { let cartEffectList = this.data.cartEffectList; for (let i in cartEffectList) { if (cartEffectList[i].store == store) { for (let j in cartEffectList[i].merchandises) { if (cartEffectList[i].merchandises[j].id == id) { cartEffectList[i].merchandises[j].x = x; } } } } that.setData({ cartEffectList: cartEffectList, }); } else { let cartLapseList = this.data.cartLapseList; for (let i in cartLapseList) { if (cartLapseList[i].id == id) { cartLapseList[i].x = x; } } that.setData({ cartLapseList: cartLapseList, }); } }})
checkbox样式更改
checkbox .wx-checkbox-input{ width: 40rpx; height: 40rpx; border-radius: 50%;}/* 选中后的背景样式 */checkbox .wx-checkbox-input.wx-checkbox-input-checked{ background: #b0474c;}/* 选中后的勾子样式 */checkbox .wx-checkbox-input.wx-checkbox-input-checked::before{ width: 40rpx; height: 40rpx; line-height: 40rpx; border-radius: 50%; text-align: center; font-size:32rpx; color:#fbfbfd; background: transparent; transform:translate(-50%, -50%) scale(1); -webkit-transform:translate(-50%, -50%) scale(1);}