人人都会AI编程

29.2 电商前台:商品列表、购物车、路由守卫、下单流程

更新时间:2026-07-11

电商前台是 Vue 最典型的应用场景之一。本节以一个简化但完整的前端实现为主线,串联商品列表、购物车、路由守卫和下单流程这四个核心模块,演示如何用 Vue 3 组合式 API + Pinia + Vue Router 搭建一个可用的电商系统。

商品列表:数据获取、展示与状态管理

商品列表通常需要处理分页加载、搜索筛选、排序等功能,并保持组件状态可预测。

数据获取与状态定义
建议将商品列表的状态封装为自定义 Hook,这样列表逻辑可在不同页面复用,且页面代码更清爽:

// composables/useProductList.js
import { ref, reactive, watch } from 'vue'
import { getProducts } from '@/api/product'

export function useProductList() {
  const list = ref([])
  const loading = ref(false)
  const query = reactive({ page: 1, size: 20, keyword: '', sort: '' })

  async function fetchList() {
    loading.value = true
    try {
      const res = await getProducts(query)
      list.value = res.data.records
      // 处理分页信息...
    } finally {
      loading.value = false
    }
  }

  // 当查询条件变化时重新拉取
  watch(query, fetchList, { immediate: true })
  // 或手动触发: function search(kw) { query.keyword = kw; query.page = 1; }

  return { list, loading, query, fetchList }
}

页面组件使用这个 Hook,关注点集中在模板渲染:

<template>
  <div>
    <SearchBar v-model="query.keyword" @search="query.page = 1" />
    <ProductCard v-for="item in list" :key="item.id" :product="item" />
    <Pagination v-model:current="query.page" :total="total" />
  </div>
</template>

列表渲染性能
若商品数量很大,应考虑 虚拟列表(如 vue-virtual-scroller)或采用 v-memo 标记列表项:

<ProductCard v-for="item in list" :key="item.id" v-memo="[item.id, item.price]" :product="item" />

这样只有价格或 id 变化时该行才会重新渲染,避免无谓开销。

购物车:全局状态与本地持久化

购物车是一个典型的跨页面共享状态,适合用 Pinia 管理。同时,购物车数据一般需要本地持久化(刷新不丢失)。

Pinia Store 设计

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

export const useCartStore = defineStore('cart', () => {
  const items = ref(JSON.parse(localStorage.getItem('cart') || '[]'))

  // 计算总价、总数量
  const totalCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0))
  const totalPrice = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0))

  function addToCart(product, quantity = 1) {
    const exist = items.value.find(i => i.id === product.id)
    if (exist) {
      exist.quantity += quantity
    } else {
      items.value.push({ ...product, quantity })
    }
    saveToLocal()
  }

  function removeItem(id) {
    items.value = items.value.filter(i => i.id !== id)
    saveToLocal()
  }

  function clearCart() {
    items.value = []
    saveToLocal()
  }

  function saveToLocal() {
    localStorage.setItem('cart', JSON.stringify(items.value))
  }

  return { items, totalCount, totalPrice, addToCart, removeItem, clearCart }
})

组件中的使用
在商品详情页点击“加入购物车”,或在购物车页面展示列表:

<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>

<template>
  <button @click="cart.addToCart(product)">加入购物车</button>
  <!-- 购物车页面直接遍历 cart.items -->
</template>

关键细节:购物车的持久化应放在 Store 内部的 $subscribe 或自定义 saveToLocal 中,避免每个组件都写持久化逻辑。

路由守卫:登录鉴权与权限控制

电商前台的核心保护路径:购物车、个人中心、下单等页面必须登录后访问。同时,某些页面(如登录、注册)登录后不应再进入。

全局前置守卫
在路由配置文件(router/index.js)中设置 meta.requiresAuth 属性,并统一拦截:

// 路由定义示例
const routes = [
  { path: '/cart', component: () => import('@/views/Cart.vue'), meta: { requiresAuth: true } },
  { path: '/checkout', component: () => import('@/views/Checkout.vue'), meta: { requiresAuth: true } },
  { path: '/login', component: () => import('@/views/Login.vue') }
]

// 守卫逻辑
router.beforeEach((to, from, next) ={
  const userStore = useUserStore()
  const isLoggedIn = !!userStore.token

  if (to.meta.requiresAuth && !isLoggedIn) {
    // 跳转登录,并记录来源路径以便登录后回跳
    next({ path: '/login', query: { redirect: to.fullPath } })
  } else if (to.path === '/login' && isLoggedIn) {
    next({ path: '/' })  // 已登录用户直接跳首页
  } else {
    next()
  }
})

状态持久化与 token 存储
通常将登录后获得的 token 存入 localStorage 并在 Pinia user store 中维护,路由守卫同步检查,避免每次跳转闪白屏。

下单流程:从购物车到订单提交

下单流程一般分为 填写/选择地址 → 确认订单 → 提交 三个视觉步骤,但前端通常将状态集中在一个 Pinia order store 或组件内部,以确保各步骤状态一致。

步骤一:地址管理
地址列表通过接口获取,选中一个默认地址。可以用 provide/inject 在子流程组件间共享选中的地址对象,或直接放 Pinia:

// stores/order.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useOrderStore = defineStore('order', () => {
  const selectedAddress = ref(null)
  const cartItems = ref([]) // 下单前可从购物车 Store 复制出来

  function selectAddress(address) {
    selectedAddress.value = address
  }

  function initOrder(items) {
    cartItems.value = items
  }

  // 提交订单逻辑
  async function submitOrder() {
    // 调用API,携带 selectedAddress, cartItems
  }

  return { selectedAddress, cartItems, selectAddress, initOrder, submitOrder }
})

步骤二:确认订单
页面展示快照:商品列表(不可再修改,只读)、收货地址、优惠信息、应付总额。这些数据均从 order store 读取。

步骤三:提交订单
点击提交时做最后校验(地址不为空、库存充足等),调用下单接口,成功后清空购物车并跳转到支付或订单详情。

async function handleSubmit() {
  if (!orderStore.selectedAddress) return alert('请选择收货地址')
  try {
    await orderStore.submitOrder()
    // 清空购物车
    const cart = useCartStore()
    cart.clearCart()
    router.push('/order/success')
  } catch (e) {
    // 错误处理(如库存不足提示)
  }
}

重要实践

  • 下单前应从购物车 Store 深拷贝一份商品快照到 Order Store,避免用户在下单过程中修改购物车导致价格不一致。
  • 库存校验应在提交时由后端完成,前端仅做友好提示;乐观更新(如扣减库存)在电商中风险较高,谨慎使用。

通过以上四个模块的串联,一个基本的电商前台骨架便搭建完成。Vue 的组合式 API 让每个功能的逻辑保持内聚,Pinia 让跨页面的数据流转清晰可控,而路由守卫则保证了整个流程的安全性。