作者:全栈开发者 | 发布时间:2023年11月
前言:为什么选择UniApp开发购物车模块?
在移动应用开发领域,购物车功能是电商类应用的核心模块之一。UniApp作为基于Vue.js的跨平台开发框架,能够帮助开发者使用一套代码同时发布到iOS、Android、Web以及各种小程序平台。本文将通过一个完整的电商购物车案例,深入讲解UniApp在实际项目中的应用技巧。
一、项目环境搭建与基础配置
1.1 创建UniApp项目
# 使用HBuilderX创建项目
文件 -> 新建 -> 项目 -> UniApp -> 默认模板
# 或使用CLI方式
vue create -p dcloudio/uni-preset-vue my-cart-project
1.2 项目目录结构规划
my-cart-project/
├── pages/ # 页面文件
│ ├── cart/ # 购物车页面
│ ├── product/ # 商品页面
│ └── order/ # 订单页面
├── components/ # 自定义组件
│ ├── cart-item/ # 购物车商品项
│ └── number-box/ # 数量选择器
├── store/ # 状态管理
├── utils/ # 工具函数
└── static/ # 静态资源
二、购物车数据结构设计与状态管理
2.1 Vuex Store设计
// store/cart.js
export default {
state: {
cartItems: [], // 购物车商品列表
selectedItems: [], // 已选中的商品
totalPrice: 0, // 总价格
totalCount: 0 // 总数量
},
mutations: {
// 添加商品到购物车
ADD_TO_CART(state, product) {
const existingItem = state.cartItems.find(item =>
item.id === product.id && item.skuId === product.skuId
);
if (existingItem) {
existingItem.quantity += product.quantity;
} else {
state.cartItems.push({
...product,
selected: true,
quantity: product.quantity || 1
});
}
this.commit('UPDATE_CART_TOTALS');
},
// 更新购物车统计
UPDATE_CART_TOTALS(state) {
state.selectedItems = state.cartItems.filter(item => item.selected);
state.totalPrice = state.selectedItems.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
state.totalCount = state.selectedItems.reduce((total, item) => {
return total + item.quantity;
}, 0);
}
},
actions: {
async addToCart({ commit }, product) {
// 这里可以添加API调用
commit('ADD_TO_CART', product);
}
}
}
三、购物车页面组件开发
3.1 购物车主页面实现
<template>
<view class="cart-page">
<view class="cart-header">
<text class="title">购物车({{cartTotalCount}})</text>
<text class="edit-btn" @tap="toggleEdit">
{{isEditing ? '完成' : '编辑'}}
</text>
</view>
<view class="cart-content" v-if="cartItems.length > 0">
<cart-item
v-for="item in cartItems"
:key="item.id + '-' + item.skuId"
:item="item"
:editable="isEditing"
@quantity-change="onQuantityChange"
@selection-change="onSelectionChange"
@remove="onRemoveItem"
/>
</view>
<view class="empty-cart" v-else>
<image src="/static/images/empty-cart.png" mode="aspectFit"></image>
<text class="empty-text">购物车还是空的~</text>
<button class="go-shopping" @tap="goToProductList">去逛逛</button>
</view>
<view class="cart-footer" v-if="cartItems.length > 0">
<view class="footer-left">
<label class="checkbox-all" @tap="toggleSelectAll">
<view :class="['checkbox', {checked: isAllSelected}]"></view>
<text>全选</text>
</label>
</view>
<view class="footer-right">
<view class="total-price">
<text class="label">合计:</text>
<text class="price">¥{{totalPrice.toFixed(2)}}</text>
</view>
<button
class="checkout-btn"
:class="{disabled: selectedItems.length === 0}"
@tap="handleCheckout"
>
{{isEditing ? '删除' : `去结算(${selectedItems.length})`}}
</button>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
import CartItem from '@/components/cart-item/cart-item.vue';
export default {
components: { CartItem },
data() {
return {
isEditing: false
};
},
computed: {
...mapState('cart', ['cartItems', 'totalPrice', 'selectedItems']),
...mapGetters('cart', ['cartTotalCount', 'isAllSelected'])
},
methods: {
...mapMutations('cart', ['TOGGLE_SELECT_ALL', 'UPDATE_ITEM_QUANTITY', 'REMOVE_ITEM']),
toggleEdit() {
this.isEditing = !this.isEditing;
},
toggleSelectAll() {
this.TOGGLE_SELECT_ALL();
},
onQuantityChange({ item, newQuantity }) {
this.UPDATE_ITEM_QUANTITY({
itemId: item.id,
skuId: item.skuId,
quantity: newQuantity
});
},
onSelectionChange({ item, selected }) {
this.UPDATE_ITEM_SELECTION({
itemId: item.id,
skuId: item.skuId,
selected
});
},
onRemoveItem(item) {
uni.showModal({
title: '提示',
content: '确定要删除这个商品吗?',
success: (res) => {
if (res.confirm) {
this.REMOVE_ITEM({
itemId: item.id,
skuId: item.skuId
});
}
}
});
},
handleCheckout() {
if (this.isEditing) {
// 批量删除选中的商品
this.batchRemoveItems();
} else {
// 跳转到结算页面
if (this.selectedItems.length === 0) {
uni.showToast({
title: '请选择要结算的商品',
icon: 'none'
});
return;
}
uni.navigateTo({
url: '/pages/order/checkout'
});
}
},
goToProductList() {
uni.switchTab({
url: '/pages/product/list'
});
}
}
}
</script>
四、购物车商品项组件开发
4.1 商品项组件实现
<template>
<view class="cart-item">
<view class="item-left">
<label class="checkbox" @tap="toggleSelection">
<view :class="['checkbox-icon', {checked: item.selected}]"></view>
</label>
</view>
<view class="item-content">
<image class="product-image" :src="item.image" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{item.name}}</text>
<text class="product-spec">{{item.spec}}</text>
<view class="price-section">
<text class="current-price">¥{{item.price}}</text>
<text class="original-price" v-if="item.originalPrice">
¥{{item.originalPrice}}
</text>
</view>
</view>
</view>
<view class="item-right">
<view class="quantity-control" v-if="!editable">
<text
class="btn decrease"
:class="{disabled: item.quantity <= 1}"
@tap="decreaseQuantity"
>-</text>
<text class="quantity">{{item.quantity}}</text>
<text class="btn increase" @tap="increaseQuantity">+</text>
</view>
<view class="delete-btn" v-else @tap="handleRemove">
<text class="delete-text">删除</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
},
editable: {
type: Boolean,
default: false
}
},
methods: {
toggleSelection() {
this.$emit('selection-change', {
item: this.item,
selected: !this.item.selected
});
},
decreaseQuantity() {
if (this.item.quantity <= 1) return;
const newQuantity = this.item.quantity - 1;
this.$emit('quantity-change', {
item: this.item,
newQuantity
});
},
increaseQuantity() {
const newQuantity = this.item.quantity + 1;
this.$emit('quantity-change', {
item: this.item,
newQuantity
});
},
handleRemove() {
this.$emit('remove', this.item);
}
}
}
</script>
五、性能优化与最佳实践
5.1 数据持久化方案
// utils/storage.js
export const storage = {
// 保存购物车数据
setCartData(cartData) {
try {
uni.setStorageSync('cart_data', JSON.stringify(cartData));
} catch (e) {
console.error('保存购物车数据失败:', e);
}
},
// 读取购物车数据
getCartData() {
try {
const data = uni.getStorageSync('cart_data');
return data ? JSON.parse(data) : null;
} catch (e) {
console.error('读取购物车数据失败:', e);
return null;
}
},
// 清空购物车数据
clearCartData() {
try {
uni.removeStorageSync('cart_data');
} catch (e) {
console.error('清空购物车数据失败:', e);
}
}
};
// 在App.vue中初始化购物车数据
export default {
onLaunch() {
// 从本地存储恢复购物车数据
const savedCartData = storage.getCartData();
if (savedCartData) {
this.$store.commit('cart/RESTORE_CART', savedCartData);
}
},
// 监听购物车变化并保存
watch: {
'$store.state.cart': {
handler(newVal) {
storage.setCartData(newVal);
},
deep: true
}
}
}
5.2 防抖优化与计算属性缓存
// utils/debounce.js
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 在组件中使用
import { debounce } from '@/utils/debounce';
export default {
methods: {
// 防抖处理的数量变化
onQuantityChange: debounce(function({ item, newQuantity }) {
this.UPDATE_ITEM_QUANTITY({
itemId: item.id,
skuId: item.skuId,
quantity: newQuantity
});
}, 300)
}
}
六、多平台适配技巧
6.1 条件编译处理平台差异
// 处理不同平台的样式和交互差异
<template>
<view class="checkout-btn-wrapper">
<!-- #ifdef MP-WEIXIN -->
<button
class="checkout-btn weixin-btn"
@tap="handleWeixinCheckout"
>
微信支付
</button>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<button
class="checkout-btn app-btn"
@tap="handleAppCheckout"
>
APP支付
</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button
class="checkout-btn h5-btn"
@tap="handleH5Checkout"
>
网页支付
</button>
<!-- #endif -->
</view>
</template>
<script>
export default {
methods: {
// 平台特定的支付处理
handleWeixinCheckout() {
// 微信小程序支付逻辑
uni.requestPayment({
provider: 'wxpay',
// ... 微信支付参数
});
},
handleAppCheckout() {
// APP支付逻辑
uni.requestPayment({
provider: 'alipay',
// ... 支付宝支付参数
});
},
handleH5Checkout() {
// H5支付逻辑
window.location.href = this.paymentUrl;
}
}
}
</script>
七、测试与调试技巧
7.1 购物车功能测试用例
// tests/cart.spec.js
describe('购物车功能测试', () => {
let store;
beforeEach(() => {
store = new Vuex.Store({
modules: {
cart: cartModule
}
});
});
test('添加商品到购物车', () => {
const product = {
id: 1,
skuId: '1-red-xl',
name: '测试商品',
price: 99.9,
image: '/static/test.jpg',
spec: '红色 XL'
};
store.commit('cart/ADD_TO_CART', product);
expect(store.state.cart.cartItems).toHaveLength(1);
expect(store.state.cart.cartItems[0].name).toBe('测试商品');
});
test('更新商品数量', () => {
// 先添加商品
const product = { id: 1, skuId: '1', price: 50, quantity: 1 };
store.commit('cart/ADD_TO_CART', product);
// 更新数量
store.commit('cart/UPDATE_ITEM_QUANTITY', {
itemId: 1,
skuId: '1',
quantity: 3
});
expect(store.state.cart.cartItems[0].quantity).toBe(3);
expect(store.state.cart.totalPrice).toBe(150);
});
test('删除商品', () => {
const product = { id: 1, skuId: '1', price: 50 };
store.commit('cart/ADD_TO_CART', product);
expect(store.state.cart.cartItems).toHaveLength(1);
store.commit('cart/REMOVE_ITEM', { itemId: 1, skuId: '1' });
expect(store.state.cart.cartItems).toHaveLength(0);
});
});
结语
通过本文的完整案例讲解,我们实现了一个功能完善的UniApp购物车模块,涵盖了从项目搭建、状态管理、组件开发到性能优化的全流程。UniApp的强大之处在于其跨平台能力,开发者可以基于这套代码快速适配到不同平台。在实际项目中,还可以根据具体需求扩展更多功能,如优惠券计算、会员折扣、库存验证等。
希望本文能为您的UniApp开发之旅提供有价值的参考,欢迎在评论区交流讨论!

