uni-app 购物车系统
核心知识点
购物车系统架构
购物车系统是电商应用中的核心功能之一,它允许用户临时存储想要购买的商品,并在结算前进行管理。在 uni-app 中实现购物车系统需要考虑以下几个方面:
数据结构设计:
- 购物车商品数据模型
- 商品数量、价格、选中状态等属性
- 购物车数据的本地存储和同步
核心功能模块:
- 商品添加与删除
- 商品数量调整
- 商品选中状态管理
- 价格计算(单价、总价、优惠等)
- 跨端数据同步
性能优化:
- 购物车数据缓存策略
- 批量操作处理
- 渲染性能优化
数据存储方案
在 uni-app 中,购物车数据可以通过以下方式存储:
本地存储:
uni.setStorageSync()和uni.getStorageSync()用于持久化存储- 适合存储用户的购物车数据,确保应用重启后数据不丢失
Vuex 状态管理:
- 用于应用运行时的状态管理
- 提供响应式的数据更新和组件间数据共享
- 适合管理购物车的实时状态,如选中状态、数量变化等
服务器存储:
- 对于需要跨设备同步的场景
- 适合存储用户的购物车数据到云端
跨端适配考虑
uni-app 支持多端发布,在实现购物车系统时需要考虑以下跨端适配问题:
样式适配:
- 不同平台的样式差异
- 响应式布局设计
API 差异:
- 不同平台的存储 API 差异
- 支付流程的平台差异
性能差异:
- 不同平台的渲染性能
- 数据处理能力差异
实用案例:实现完整的购物车功能
1. 购物车数据模型设计
首先,我们需要设计购物车的数据模型,包括商品信息、数量、选中状态等。
// 购物车商品数据模型
const cartItemModel = {
id: String, // 商品ID
name: String, // 商品名称
price: Number, // 商品价格
quantity: Number, // 商品数量
selected: Boolean, // 是否选中
image: String, // 商品图片
stock: Number, // 商品库存
skuId: String, // SKU ID(可选)
skuName: String, // SKU 名称(可选)
attrs: Array // 商品属性(可选)
}
// 购物车数据模型
const cartModel = {
items: Array, // 商品列表
totalPrice: Number, // 总价格
selectedCount: Number // 选中商品数量
}2. Vuex 状态管理实现
创建 Vuex 模块来管理购物车状态:
// store/modules/cart.js
const state = {
items: [],
isEditMode: false
}
const mutations = {
// 设置购物车数据
SET_CART_ITEMS(state, items) {
state.items = items
},
// 添加商品到购物车
ADD_TO_CART(state, product) {
const existingItem = state.items.find(item => item.id === product.id)
if (existingItem) {
// 如果商品已存在,增加数量
existingItem.quantity += product.quantity
} else {
// 如果商品不存在,添加新商品
state.items.push({
...product,
selected: true
})
}
// 保存到本地存储
uni.setStorageSync('cartItems', state.items)
},
// 更新商品数量
UPDATE_QUANTITY(state, { id, quantity }) {
const item = state.items.find(item => item.id === id)
if (item) {
item.quantity = quantity
// 保存到本地存储
uni.setStorageSync('cartItems', state.items)
}
},
// 切换商品选中状态
TOGGLE_SELECT(state, id) {
const item = state.items.find(item => item.id === id)
if (item) {
item.selected = !item.selected
// 保存到本地存储
uni.setStorageSync('cartItems', state.items)
}
},
// 切换全选状态
TOGGLE_SELECT_ALL(state, selected) {
state.items.forEach(item => {
item.selected = selected
})
// 保存到本地存储
uni.setStorageSync('cartItems', state.items)
},
// 删除购物车商品
REMOVE_FROM_CART(state, id) {
state.items = state.items.filter(item => item.id !== id)
// 保存到本地存储
uni.setStorageSync('cartItems', state.items)
},
// 清空购物车
CLEAR_CART(state) {
state.items = []
// 清空本地存储
uni.removeStorageSync('cartItems')
},
// 切换编辑模式
TOGGLE_EDIT_MODE(state) {
state.isEditMode = !state.isEditMode
}
}
const actions = {
// 初始化购物车
initCart({ commit }) {
const cartItems = uni.getStorageSync('cartItems') || []
commit('SET_CART_ITEMS', cartItems)
},
// 添加商品到购物车
addToCart({ commit }, product) {
commit('ADD_TO_CART', product)
},
// 更新商品数量
updateQuantity({ commit }, payload) {
commit('UPDATE_QUANTITY', payload)
},
// 切换商品选中状态
toggleSelect({ commit }, id) {
commit('TOGGLE_SELECT', id)
},
// 切换全选状态
toggleSelectAll({ commit }, selected) {
commit('TOGGLE_SELECT_ALL', selected)
},
// 删除购物车商品
removeFromCart({ commit }, id) {
commit('REMOVE_FROM_CART', id)
},
// 清空购物车
clearCart({ commit }) {
commit('CLEAR_CART')
},
// 切换编辑模式
toggleEditMode({ commit }) {
commit('TOGGLE_EDIT_MODE')
}
}
const getters = {
// 获取购物车商品数量
cartCount: state => {
return state.items.reduce((total, item) => total + item.quantity, 0)
},
// 获取选中商品数量
selectedCount: state => {
return state.items
.filter(item => item.selected)
.reduce((total, item) => total + item.quantity, 0)
},
// 获取选中商品总价
selectedTotal: state => {
return state.items
.filter(item => item.selected)
.reduce((total, item) => total + item.price * item.quantity, 0)
},
// 是否全选
isAllSelected: state => {
if (state.items.length === 0) return false
return state.items.every(item => item.selected)
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}3. 购物车组件实现
创建购物车页面组件,实现购物车的 UI 展示和交互功能:
<template>
<view class="cart-container">
<view v-if="items.length === 0" class="empty-cart">
<image src="/static/empty-cart.png" mode="aspectFit"></image>
<text>购物车是空的</text>
<navigator url="/pages/product/list/list" class="go-shopping">去逛逛</navigator>
</view>
<view v-else class="cart-content">
<!-- 购物车商品列表 -->
<view class="cart-list">
<view v-for="item in items" :key="item.id" class="cart-item">
<!-- 商品选中框 -->
<view class="checkbox" @click="toggleSelect(item.id)">
<view class="checkbox-inner" :class="{ 'checked': item.selected }">
<text v-if="item.selected">✓</text>
</view>
</view>
<!-- 商品图片 -->
<view class="item-image">
<image :src="item.image" mode="aspectFill"></image>
</view>
<!-- 商品信息 -->
<view class="item-info">
<text class="item-name">{{ item.name }}</text>
<text v-if="item.skuName" class="item-sku">{{ item.skuName }}</text>
<view class="item-bottom">
<text class="item-price">¥{{ item.price.toFixed(2) }}</text>
<!-- 数量控制 -->
<view class="quantity-control">
<view class="quantity-btn" @click="decreaseQuantity(item)" :class="{ 'disabled': item.quantity <= 1 }">-
</view>
<view class="quantity-input">{{ item.quantity }}</view>
<view class="quantity-btn" @click="increaseQuantity(item)" :class="{ 'disabled': item.quantity >= item.stock }">+
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="cart-footer">
<!-- 全选 -->
<view class="footer-left" @click="toggleSelectAll">
<view class="checkbox" @click.stop="toggleSelectAll">
<view class="checkbox-inner" :class="{ 'checked': isAllSelected }">
<text v-if="isAllSelected">✓</text>
</view>
</view>
<text>全选</text>
</view>
<!-- 价格和操作 -->
<view class="footer-right">
<view class="total-price">
<text>合计:</text>
<text class="price">¥{{ selectedTotal.toFixed(2) }}</text>
</view>
<view class="footer-buttons">
<view v-if="!isEditMode" class="delete-btn" @click="toggleEditMode">编辑</view>
<view v-else class="delete-btn" @click="deleteSelectedItems">删除</view>
<view v-if="!isEditMode" class="checkout-btn" @click="checkout" :class="{ 'disabled': selectedCount === 0 }">
结算({{ selectedCount }})
</view>
<view v-else class="checkout-btn" @click="toggleEditMode">完成</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
// 本地状态
}
},
computed: {
...mapState('cart', ['items', 'isEditMode']),
...mapGetters('cart', ['cartCount', 'selectedCount', 'selectedTotal', 'isAllSelected'])
},
onLoad() {
// 初始化购物车
this.initCart()
},
methods: {
...mapActions('cart', [
'initCart',
'addToCart',
'updateQuantity',
'toggleSelect',
'toggleSelectAll',
'removeFromCart',
'clearCart',
'toggleEditMode'
]),
// 增加商品数量
increaseQuantity(item) {
if (item.quantity < item.stock) {
this.updateQuantity({
id: item.id,
quantity: item.quantity + 1
})
} else {
uni.showToast({
title: '已达到库存上限',
icon: 'none'
})
}
},
// 减少商品数量
decreaseQuantity(item) {
if (item.quantity > 1) {
this.updateQuantity({
id: item.id,
quantity: item.quantity - 1
})
}
},
// 删除选中商品
deleteSelectedItems() {
const selectedItems = this.items.filter(item => item.selected)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的商品',
icon: 'none'
})
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}件商品吗?`,
success: (res) => {
if (res.confirm) {
selectedItems.forEach(item => {
this.removeFromCart(item.id)
})
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
},
// 结算
checkout() {
if (this.selectedCount === 0) {
uni.showToast({
title: '请选择要结算的商品',
icon: 'none'
})
return
}
// 获取选中的商品
const selectedItems = this.items.filter(item => item.selected)
// 跳转到结算页面
uni.navigateTo({
url: `/pages/order/checkout?items=${encodeURIComponent(JSON.stringify(selectedItems))}`
})
}
}
}
</script>
<style scoped>
.cart-container {
flex: 1;
background-color: #f5f5f5;
}
.empty-cart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-cart image {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
}
.empty-cart text {
font-size: 32rpx;
color: #999;
margin-bottom: 40rpx;
}
.go-shopping {
padding: 20rpx 60rpx;
background-color: #007aff;
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
}
.cart-list {
padding-bottom: 120rpx;
}
.cart-item {
display: flex;
padding: 20rpx;
background-color: #fff;
margin-bottom: 10rpx;
}
.checkbox {
margin-right: 20rpx;
display: flex;
align-items: center;
}
.checkbox-inner {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
color: #fff;
}
.checkbox-inner.checked {
background-color: #007aff;
border-color: #007aff;
}
.item-image {
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
}
.item-image image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-name {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
line-height: 44rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-sku {
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}
.item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-price {
font-size: 36rpx;
color: #ff4d4f;
font-weight: bold;
}
.quantity-control {
display: flex;
align-items: center;
}
.quantity-btn {
width: 48rpx;
height: 48rpx;
border: 2rpx solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.quantity-btn.disabled {
color: #ccc;
}
.quantity-input {
width: 80rpx;
height: 48rpx;
border-top: 2rpx solid #ddd;
border-bottom: 2rpx solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.cart-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
border-top: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
}
.footer-left {
display: flex;
align-items: center;
}
.footer-left text {
margin-left: 10rpx;
font-size: 28rpx;
color: #333;
}
.footer-right {
display: flex;
align-items: center;
}
.total-price {
font-size: 28rpx;
margin-right: 30rpx;
}
.total-price .price {
font-size: 36rpx;
color: #ff4d4f;
font-weight: bold;
}
.footer-buttons {
display: flex;
}
.delete-btn {
width: 120rpx;
height: 60rpx;
border: 2rpx solid #ddd;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.checkout-btn {
width: 180rpx;
height: 60rpx;
background-color: #007aff;
color: #fff;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.checkout-btn.disabled {
background-color: #ccc;
}
</style>4. 商品详情页添加购物车功能
在商品详情页中添加加入购物车的功能:
<template>
<view class="product-detail">
<!-- 商品信息 -->
<view class="product-info">
<image :src="product.image" mode="aspectFill"></image>
<text class="product-name">{{ product.name }}</text>
<text class="product-price">¥{{ product.price.toFixed(2) }}</text>
</view>
<!-- 商品属性选择 -->
<view class="product-attrs">
<!-- 属性选择逻辑 -->
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="cart-icon" @click="goToCart">
<image src="/static/cart.png" mode="aspectFit"></image>
<view v-if="cartCount > 0" class="cart-badge">{{ cartCount }}</view>
</view>
<view class="buy-now" @click="buyNow">立即购买</view>
<view class="add-to-cart" @click="addToCart">加入购物车</view>
</view>
</view>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
product: {
id: '1',
name: '示例商品',
price: 99.99,
image: '/static/product.jpg',
stock: 100
},
quantity: 1
}
},
computed: {
...mapGetters('cart', ['cartCount'])
},
methods: {
...mapActions('cart', ['addToCart']),
// 加入购物车
addToCart() {
const productToAdd = {
...this.product,
quantity: this.quantity
}
this.addToCart(productToAdd)
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
},
// 立即购买
buyNow() {
const productToBuy = {
...this.product,
quantity: this.quantity,
selected: true
}
// 跳转到结算页面
uni.navigateTo({
url: `/pages/order/checkout?items=${encodeURIComponent(JSON.stringify([productToBuy]))}`
})
},
// 跳转到购物车
goToCart() {
uni.switchTab({
url: '/pages/cart/cart'
})
}
}
}
</script>
<style scoped>
/* 样式省略 */
</style>5. 购物车数据同步与跨端适配
为了确保购物车数据在不同平台上的一致性,我们需要处理跨端数据同步问题:
// utils/cartSync.js
/**
* 同步购物车数据到服务器
* @param {Array} cartItems 购物车商品列表
* @returns {Promise}
*/
export const syncCartToServer = async (cartItems) => {
try {
// 这里需要根据实际的后端 API 进行调整
const res = await uni.request({
url: 'https://api.example.com/cart/sync',
method: 'POST',
data: {
items: cartItems
},
header: {
'Authorization': 'Bearer ' + uni.getStorageSync('token')
}
})
return res.data
} catch (error) {
console.error('同步购物车数据失败:', error)
return null
}
}
/**
* 从服务器获取购物车数据
* @returns {Promise<Array>}
*/
export const getCartFromServer = async () => {
try {
// 这里需要根据实际的后端 API 进行调整
const res = await uni.request({
url: 'https://api.example.com/cart/get',
method: 'GET',
header: {
'Authorization': 'Bearer ' + uni.getStorageSync('token')
}
})
return res.data.items || []
} catch (error) {
console.error('获取购物车数据失败:', error)
return []
}
}
/**
* 跨端适配的购物车数据存储
* @param {Array} cartItems 购物车商品列表
*/
export const saveCartData = (cartItems) => {
try {
// 针对不同平台进行适配
#ifdef H5
// H5 平台使用 localStorage
localStorage.setItem('cartItems', JSON.stringify(cartItems))
#else
// 其他平台使用 uni.setStorageSync
uni.setStorageSync('cartItems', cartItems)
#endif
} catch (error) {
console.error('保存购物车数据失败:', error)
}
}
/**
* 跨端适配的购物车数据获取
* @returns {Array}
*/
export const getCartData = () => {
try {
// 针对不同平台进行适配
#ifdef H5
// H5 平台使用 localStorage
const cartItems = localStorage.getItem('cartItems')
return cartItems ? JSON.parse(cartItems) : []
#else
// 其他平台使用 uni.getStorageSync
return uni.getStorageSync('cartItems') || []
#endif
} catch (error) {
console.error('获取购物车数据失败:', error)
return []
}
}性能优化策略
1. 购物车数据缓存
- 使用本地存储缓存购物车数据,减少网络请求
- 实现数据过期机制,确保数据新鲜度
- 考虑使用 indexedDB 存储大量购物车数据
2. 批量操作处理
- 实现购物车商品的批量添加、删除和更新
- 减少频繁的存储操作,使用防抖或节流
3. 渲染性能优化
- 使用虚拟列表渲染大量购物车商品
- 避免不必要的计算和渲染
- 合理使用 computed 和 watch
4. 网络优化
- 实现购物车数据的增量同步
- 使用 WebSocket 实现实时数据更新
- 合理设置缓存策略
总结
本教程详细介绍了在 uni-app 中实现购物车系统的方法,包括:
- 核心知识点:购物车系统架构、数据存储方案、跨端适配考虑
- 实现方法:
- Vuex 状态管理
- 购物车组件开发
- 商品添加与管理
- 数量控制与价格计算
- 实用案例:完整的购物车功能实现
- 性能优化:数据缓存、批量操作、渲染性能优化
通过本教程的学习,你应该能够掌握在 uni-app 中实现购物车系统的方法,并能够根据实际项目需求进行定制和扩展。购物车系统是电商应用的核心功能之一,合理的设计和实现可以显著提升用户体验和转化率。
在实际开发中,还需要考虑更多因素,如商品库存检查、价格变动处理、优惠活动集成等,这些都需要根据具体的业务需求进行调整和优化。