Uniapp Vue3购物车实战:组合式API与Pinia跨端状态管理全方案

2026-05-31 0 897

在电商类小程序或App中,购物车模块几乎无处不在。它的难点在于多端(微信、支付宝、H5等)表现一致性、复杂的状态联动以及频繁的增删改查操作。本文将基于Uniapp + Vue3组合式API + Pinia这一现代技术栈,从零搭建一个功能完整、代码清晰的跨端购物车,并深入讲解条件编译、持久化存储和性能优化技巧。

一、项目初始化与技术选型

使用HBuilderX创建基于Vue3的Uniapp项目。在manifest.json中勾选Vue3选项,并确保在main.js中正确挂载Pinia。

安装Pinia:在项目根目录执行 npm install pinia(或使用HBuilderX的可视化界面安装)。然后在main.js中引入:

// main.js
import App from './App'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return {
    app,
    pinia
  }
}

此结构兼容Uniapp的SSR编译模式,保证Pinia在服务端和客户端均可使用。

二、商品数据结构设计

购物车中每个商品条目包含基础信息、选中状态、购买数量及规格。我们定义通用的类型接口(使用JSDoc或TypeScript皆可,本文采用JavaScript配合清晰的注释)。

// types/cart.js (仅示意,非必需)
/**
 * @typedef {Object} CartItem
 * @property {string} id - 商品ID
 * @property {string} name - 商品名称
 * @property {number} price - 单价(分)
 * @property {string} image - 商品图片
 * @property {string} spec - 当前选择规格描述
 * @property {number} stock - 库存
 * @property {number} quantity - 购买数量
 * @property {boolean} checked - 是否选中
 */

所有金额使用分为单位,避免浮点数精度问题。前端展示时再格式化为元。

三、Pinia Store:购物车核心逻辑

创建store/cart.js,利用Pinia的defineStore和组合式API风格编写所有状态与方法。

// store/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 状态
  const items = ref([])

  // 计算总价(分)
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => {
      if (item.checked) {
        return sum + item.price * item.quantity
      }
      return sum
    }, 0)
  })

  // 选中商品总数
  const checkedCount = computed(() => {
    return items.value.filter(item => item.checked).length
  })

  // 是否全选
  const isAllChecked = computed(() => {
    return items.value.length > 0 && items.value.every(item => item.checked)
  })

  // 方法:添加商品到购物车
  function addToCart(product, spec = '默认', quantity = 1) {
    const existIndex = items.value.findIndex(
      item => item.id === product.id && item.spec === spec
    )
    if (existIndex > -1) {
      const item = items.value[existIndex]
      const newQty = Math.min(item.quantity + quantity, item.stock)
      item.quantity = newQty
    } else {
      items.value.push({
        id: product.id,
        name: product.name,
        price: product.price,
        image: product.image,
        spec,
        stock: product.stock,
        quantity: Math.min(quantity, product.stock),
        checked: true
      })
    }
    // 可选:持久化到本地存储(见后文)
  }

  // 更新数量
  function updateQuantity(id, spec, quantity) {
    const item = items.value.find(i => i.id === id && i.spec === spec)
    if (item) {
      const qty = parseInt(quantity) || 1
      item.quantity = Math.min(Math.max(qty, 1), item.stock)
    }
  }

  // 切换选中状态
  function toggleChecked(id, spec) {
    const item = items.value.find(i => i.id === id && i.spec === spec)
    if (item) {
      item.checked = !item.checked
    }
  }

  // 全选/全不选
  function toggleAllChecked() {
    const newStatus = !isAllChecked.value
    items.value.forEach(item => {
      item.checked = newStatus
    })
  }

  // 删除商品
  function removeItem(id, spec) {
    items.value = items.value.filter(i => !(i.id === id && i.spec === spec))
  }

  // 清空已选商品
  function removeChecked() {
    items.value = items.value.filter(item => !item.checked)
  }

  return {
    items,
    totalPrice,
    checkedCount,
    isAllChecked,
    addToCart,
    updateQuantity,
    toggleChecked,
    toggleAllChecked,
    removeItem,
    removeChecked
  }
})

所有方法都直接操作响应式数组,Pinia自动追踪变化并通知组件更新。

四、商品列表页:加入购物车交互

模拟一个商品列表,每个商品展示名称、价格、库存,并提供规格选择和数量输入。

<!-- pages/goods/list.vue -->
<template>
  <view class="goods-list">
    <view v-for="product in products" :key="product.id" class="goods-item">
      <image :src="product.image" mode="aspectFill"></image>
      <view class="info">
        <text class="name">{{ product.name }}</text>
        <text class="price">¥{{ (product.price / 100).toFixed(2) }}</text>
        <text class="stock">库存: {{ product.stock }}</text>
      </view>
      <view class="spec">
        <picker :range="product.specs" @change="(e) => selectSpec(product.id, product.specs[e.detail.value])">
          <text>{{ selectedSpecs[product.id] || product.specs[0] }}</text>
        </picker>
      </view>
      <input type="number" v-model.number="quantities[product.id]" placeholder="数量" min="1" />
      <button @click="addToCartHandler(product)">加入购物车</button>
    </view>
  </view>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useCartStore } from '@/store/cart'

const cartStore = useCartStore()

// 模拟商品数据(实际来自API)
const products = ref([
  { id: 'p1', name: '简约T恤', price: 7990, image: '/static/t-shirt.jpg', stock: 20, specs: ['S', 'M', 'L', 'XL'] },
  { id: 'p2', name: '牛仔裤', price: 15990, image: '/static/jeans.jpg', stock: 15, specs: ['28码', '30码', '32码'] },
])

// 记录每个商品当前选中的规格和数量
const selectedSpecs = reactive({})
const quantities = reactive({})

// 初始化默认值
products.value.forEach(p => {
  if (!selectedSpecs[p.id]) selectedSpecs[p.id] = p.specs[0]
  if (!quantities[p.id]) quantities[p.id] = 1
})

function selectSpec(productId, spec) {
  selectedSpecs[productId] = spec
}

function addToCartHandler(product) {
  const spec = selectedSpecs[product.id]
  const qty = quantities[product.id] || 1
  cartStore.addToCart(
    { id: product.id, name: product.name, price: product.price, image: product.image, stock: product.stock },
    spec,
    qty
  )
  uni.showToast({ title: '已加入购物车', icon: 'success' })
  quantities[product.id] = 1 // 重置数量
}
</script>

这里使用了组合式API的reactiveref管理本地状态。picker组件用于规格选择。

五、购物车页面:完整功能实现

购物车页面展示所有已添加商品,支持修改数量、勾选、删除和全选操作。

<!-- pages/cart/index.vue -->
<template>
  <view class="cart-page">
    <block v-if="cartStore.items.length > 0">
      <view class="cart-list">
        <view v-for="item in cartStore.items" :key="item.id + item.spec" class="cart-item">
          <view class="checkbox" @click="cartStore.toggleChecked(item.id, item.spec)">
            <text>{{ item.checked ? '☑' : '☐' }}</text>
          </view>
          <image :src="item.image" mode="aspectFill" />
          <view class="info">
            <text class="name">{{ item.name }}</text>
            <text class="spec">{{ item.spec }}</text>
            <text class="price">¥{{ (item.price / 100).toFixed(2) }}</text>
          </view>
          <view class="quantity-control">
            <button @click="cartStore.updateQuantity(item.id, item.spec, item.quantity - 1)" :disabled="item.quantity <= 1">-</button>
            <input type="number" :value="item.quantity" @input="e => cartStore.updateQuantity(item.id, item.spec, e.detail.value)" />
            <button @click="cartStore.updateQuantity(item.id, item.spec, item.quantity + 1)" :disabled="item.quantity >= item.stock">+</button>
          </view>
          <button class="delete-btn" @click="cartStore.removeItem(item.id, item.spec)">删除</button>
        </view>
      </view>

      <view class="footer-bar">
        <view class="select-all" @click="cartStore.toggleAllChecked()">
          <text>{{ cartStore.isAllChecked ? '☑' : '☐' }}</text>
          <text>全选</text>
        </view>
        <view class="total">
          合计: ¥{{ (cartStore.totalPrice / 100).toFixed(2) }}
        </view>
        <button class="checkout-btn" :disabled="cartStore.checkedCount === 0">
          结算({{ cartStore.checkedCount }})
        </button>
        <button class="delete-checked" @click="cartStore.removeChecked()">删除选中</button>
      </view>
    </block>
    <view v-else class="empty">
      <text>购物车为空</text>
    </view>
  </view>
</template>

<script setup>
import { useCartStore } from '@/store/cart'

const cartStore = useCartStore()
</script>

页面完全由Pinia store驱动,交互简洁明了。输入修改数量时直接调用updateQuantity,库存合法性在store中控制。

六、多端条件编译与样式隔离

Uniapp允许通过#ifdef#ifndef实现不同平台的代码块。例如,微信小程序和App可能需要不同的UI表现,或使用特定的API。

示例:在购物车页面,H5端可使用原生滚动条优化,而小程序使用scroll-view。

<template>
  <!-- #ifdef H5 -->
  <div class="cart-scroll-h5">
    <!-- 购物车列表 -->
  </div>
  <!-- #endif -->

  <!-- #ifdef MP-WEIXIN -->
  <scroll-view scroll-y class="cart-scroll-mp">
    <!-- 购物车列表 -->
  </scroll-view>
  <!-- #endif -->
</template>

对于样式,可以通过/* #ifdef H5 */<style>块中编写平台特定CSS。也可以通过JS动态判断平台:

// 在脚本中根据平台调整行为
const isH5 = uni.getSystemInfoSync().platform === 'web'
if (isH5) {
  // H5专属逻辑
}

七、持久化存储:让购物车数据不丢失

用户关闭应用或页面后,购物车数据应保留。我们可以利用Pinia的插件机制或手动在store中整合uni.setStorageSync

扩展store/cart.js,添加持久化逻辑:

// 在store/cart.js 的defineStore内部添加:
import { onMounted, watch } from 'vue' // 若使用组合式风格,需在setup内

// 在defineStore的setup函数中添加以下状态与逻辑:
const STORAGE_KEY = 'cart_items'

// 从本地存储恢复数据
function loadFromStorage() {
  try {
    const saved = uni.getStorageSync(STORAGE_KEY)
    if (saved) {
      items.value = JSON.parse(saved)
    }
  } catch (e) {
    console.error('读取购物车缓存失败', e)
  }
}

// 监听items变化并保存
watch(
  items,
  (newVal) => {
    uni.setStorageSync(STORAGE_KEY, JSON.stringify(newVal))
  },
  { deep: true }
)

// 在页面加载时调用
loadFromStorage()

注意:使用watch需要从vue导入,并在store的setup函数中直接调用(因为Pinia的setup函数相当于组件的setup,支持生命周期钩子)。如果担心频繁存储,可以添加防抖处理,但购物车操作频率一般不高,直接存即可。

八、性能优化与异常处理

1. 库存实时校验:在结算前,应再次调用后端接口校验库存,因为库存可能已变化。可在结算按钮的点击事件中发起请求,如果库存不足则提示用户并更新购物车数量。

async function checkout() {
  const checkedItems = cartStore.items.filter(i => i.checked)
  const res = await uni.request({
    url: '/api/check-stock',
    method: 'POST',
    data: { items: checkedItems.map(i => ({ id: i.id, spec: i.spec, quantity: i.quantity })) }
  })
  // 根据后端返回更新本地库存和数量(如果后端返回最新库存信息)
}

2. 列表长列表优化:如果购物车条目可能非常多(如超过50条),考虑使用虚拟列表或仅渲染当前可见区域。Uniapp的scroll-view配合recycle-view(或uni-ui的uni-list)可以提升渲染性能。

3. 图片懒加载:为商品图片使用lazy-load属性,尤其在列表中。

<image :src="item.image" mode="aspectFill" lazy-load />

4. 按钮防抖:数量增减按钮或删除按钮可能被快速连续点击,通过简单的节流或防抖避免多次触发store操作。

// 使用节流函数包装方法(可自行实现或引入工具库)
const throttledRemove = useThrottleFn(cartStore.removeItem, 300)

九、完整项目预览与总结

整合以上代码,我们得到了一个功能完备、多端可用的购物车模块。它充分利用了Uniapp的跨端能力、Vue3组合式API的逻辑复用优势以及Pinia的简洁状态管理。核心特点包括:

  • 清晰的数据流:组件通过store方法修改状态,状态变化自动驱动UI更新。
  • 规格与库存联动:加入购物车时严格校验库存,修改数量时不会超出范围。
  • 多平台适配:通过条件编译处理不同端的滚动容器和交互细节。
  • 本地持久化:简单实用的存储方案,保证数据不丢失。

实际开发中,你可以在此基础上扩展优惠券计算、运费逻辑、赠品机制等。更重要的是,这套架构模式可以平滑迁移到任何需要复杂状态管理的跨端业务中。

掌握Uniapp + Vue3 + Pinia的组合,你将能以极低的成本同时输出高质量的小程序、H5和App应用。购物车虽小,却是一块极佳的练兵石。

Uniapp Vue3购物车实战:组合式API与Pinia跨端状态管理全方案
收藏 (0) 打赏

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

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

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp Uniapp Vue3购物车实战:组合式API与Pinia跨端状态管理全方案 https://www.taomawang.com/web/uniapp/2054.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

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