UniApp实战:从零开发跨平台电商购物车模块全解析

2025-11-06 0 511

作者:全栈开发者 | 发布时间: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开发之旅提供有价值的参考,欢迎在评论区交流讨论!

UniApp实战:从零开发跨平台电商购物车模块全解析
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp UniApp实战:从零开发跨平台电商购物车模块全解析 https://www.taomawang.com/web/uniapp/1387.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务