电商产品列表与购物车

在现代前端开发中,构建电商网站的产品列表与购物车功能是提升用户体验的核心环节。本章节将围绕路由设计、商品数据处理、购物车状态管理及异步交互四个维度,详细阐述基于Vue.js 3.5的"迷你电商网站"前端实现方案,通过场景化开发流程展示从页面导航到数据持久化的完整技术链路。

路由设计与参数传递

路由系统是电商网站页面跳转的基础,需通过Vue Router实现页面间的无缝切换与参数传递。首先在router/index.ts中定义路由配置,采用TypeScript确保类型安全,核心路由数组包含商品列表页、详情页及购物车页:

typescript
复制代码
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import ProductList from '@/views/ProductList.vue'
import ProductDetail from '@/views/ProductDetail.vue'
import Cart from '@/views/Cart.vue'

const routes = [
  { path: '/', name: 'home', component: ProductList },
  { path: '/product/:id', name: 'product', component: ProductDetail, props: true },
  { path: '/cart', name: 'cart', component: Cart }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

App.vue中使用<router-link>创建顶部导航栏,通过active-class实现当前页高亮效果:

plaintext
复制代码
<!-- App.vue -->
<template>
  <nav class="navbar">
    <router-link to="/" active-class="active">商品列表</router-link>
    <router-link to="/cart" active-class="active">购物车 ({{ cartStore.items.length }})</router-link>
  </nav>
  <router-view />
</template>

商品列表页跳转到详情页时,通过router.push传递商品ID参数。在ProductList组件中,为每个商品卡片绑定点击事件:

plaintext
复制代码
<!-- ProductList.vue -->
<template>
  <div class="product-card" v-for="product in filteredProducts" @click="goToDetail(product.id)">
    <img :src="product.image" alt="product.name">
    <h3>{{ product.name }}</h3>
    <p>¥{{ product.price.toFixed(2) }}</p>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

const goToDetail = (id) => {
  router.push({ name: 'product', params: { id } })
}
</script>

商品列表与筛选功能

商品列表的实现核心在于数据请求、筛选交互与加载状态管理。采用VueUse提供的useFetch简化异步数据获取,其内置响应式处理与缓存机制可大幅减少模板代码:

plaintext
复制代码
<!-- ProductList.vue -->
<script setup>
import { useFetch } from '@vueuse/core'
import { ref, computed } from 'vue'

// 基础数据与加载状态
const { data: products, isLoading, error } = useFetch('/api/products')
const isError = ref(false)

// 筛选条件
const minPrice = ref(0)
const maxPrice = ref(10000)
const selectedCategories = ref([])
const sortType = ref('default') // 'default' | 'price-asc' | 'price-desc'

// 计算属性:筛选并排序商品
const filteredProducts = computed(() => {
  if (!products.value) return []
  
  return products.value
    .filter(p => 
      p.price >= minPrice.value && 
      p.price <= maxPrice.value &&
      (selectedCategories.value.length === 0 || selectedCategories.value.includes(p.category))
    )
    .sort((a, b) => {
      switch (sortType.value) {
        case 'price-asc': return a.price - b.price
        case 'price-desc': return b.price - a.price
        default: return 0
      }
    })
})
</script>

筛选表单实现价格区间滑块与分类多选框,通过v-model双向绑定筛选条件:

plaintext
复制代码
<!-- 筛选表单 -->
<template>
  <div class="filters">
    <div class="price-filter">
      <label>价格区间: ¥{{ minPrice }} - ¥{{ maxPrice }}</label>
      <input 
        type="range" 
        v-model="minPrice" 
        min="0" 
        max="10000" 
        step="10"
      >
      <input 
        type="range" 
        v-model="maxPrice" 
        min="0" 
        max="10000" 
        step="10"
      >
    </div>

    <div class="category-filter">
      <label v-for="category in allCategories" :key="category">
        <input 
          type="checkbox" 
          :value="category" 
          v-model="selectedCategories"
        >
        {{ category }}
      </label>
    </div>

    <div class="sort-filter">
      <select v-model="sortType">
        <option value="default">默认排序</option>
        <option value="price-asc">价格从低到高</option>
        <option value="price-desc">价格从高到低</option>
      </select>
    </div>
  </div>
</template>

为提升用户体验,在数据加载过程中显示骨架屏组件。创建ProductSkeleton组件作为占位容器:

plaintext
复制代码
<!-- ProductSkeleton.vue -->
<template>
  <div class="skeleton-card">
    <div class="skeleton-image"></div>
    <div class="skeleton-line"></div>
    <div class="skeleton-line short"></div>
  </div>
</template>

<style scoped>
.skeleton-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
}
.skeleton-image {
  width: 100%;
  height: 200px;
  background: #e0e0e0;
  border-radius: 8px;
  animation: pulse 1.5s infinite;
}
.skeleton-line {
  height: 24px;
  background: #e0e0e0;
  border-radius: 4px;
  animation: pulse 1.5s infinite;
}
.short {
  width: 60%;
}
@keyframes pulse {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 0.4; }
}
</style>

在列表页中根据isLoading状态切换骨架屏与真实数据:

plaintext
复制代码
<!-- ProductList.vue -->
<template>
  <div class="product-list">
    <div v-if="isLoading">
      <ProductSkeleton v-for="i in 8" :key="i" />
    </div>
    <div v-else-if="isError">
      <div class="error-message">加载失败,请刷新页面重试</div>
    </div>
    <div v-else>
      <div class="product-card" v-for="product in filteredProducts" :key="product.id">
        <!-- 商品内容 -->
      </div>
    </div>
  </div>
</template>

购物车状态管理

购物车功能采用Pinia实现跨组件状态共享,结合pinia-plugin-persistedstate实现数据持久化。首先定义cartStore存储购物车核心逻辑:

typescript
复制代码
// stores/cartStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 购物车数据
  const items = ref([])

  // 核心方法
  const addToCart = (product, quantity = 1) => {
    const existingItem = items.value.find(item => item.id === product.id)
    if (existingItem) {
      existingItem.quantity += quantity
    } else {
      items.value.push({ ...product, quantity })
    }
  }

  const updateQuantity = (id, quantity) => {
    const item = items.value.find(item => item.id === id)
    if (item) {
      item.quantity = Math.max(1, quantity) // 确保数量至少为1
    }
  }

  const removeFromCart = (id) => {
    items.value = items.value.filter(item => item.id !== id)
  }

  const clearCart = () => {
    items.value = []
  }

  // 计算属性
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  })

  return {
    items,
    addToCart,
    updateQuantity,
    removeFromCart,
    clearCart,
    totalPrice
  }
}, {
  persist: {
    storage: localStorage, // 持久化到localStorage
    paths: ['items'] // 仅持久化items字段
  }
})

在商品详情页实现"加入购物车"功能,通过数量选择器控制购买数量:

plaintext
复制代码
<!-- ProductDetail.vue -->
<template>
  <div class="product-detail">
    <img :src="product.image" alt="product.name">
    <div class="product-info">
      <h1>{{ product.name }}</h1>
      <p class="price">¥{{ product.price.toFixed(2) }}</p>
      <p class="description">{{ product.description }}</p>
      
      <div class="quantity-selector">
        <button @click="quantity = Math.max(1, quantity - 1)">-</button>
        <span>{{ quantity }}</span>
        <button @click="quantity += 1">+</button>
      </div>
      
      <button 
        class="add-to-cart-btn" 
        @click="addToCart"
      >
        加入购物车
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useCartStore } from '@/stores/cartStore'
import { useRoute } from 'vue-router'
import { useFetch } from '@vueuse/core'

const route = useRoute()
const cartStore = useCartStore()
const quantity = ref(1)
const { data: product } = useFetch(`/api/products/${route.params.id}`)

const addToCart = () => {
  if (product.value) {
    cartStore.addToCart(product.value, quantity.value)
    // 显示加入成功提示
    alert('商品已加入购物车')
  }
}
</script>

购物车页面需实现商品列表渲染、数量修改、删除功能及总价计算:

plaintext
复制代码
<!-- Cart.vue -->
<template>
  <div class="cart-page">
    <h1>我的购物车</h1>
    
    <div v-if="cartStore.items.length === 0" class="empty-cart">
      <p>购物车为空,快去添加商品吧~</p>
      <router-link to="/">返回商品列表</router-link>
    </div>
    
    <div v-else>
      <div class="cart-list">
        <div class="cart-item" v-for="item in cartStore.items" :key="item.id">
          <img :src="item.image" alt="item.name" class="item-image">
          <div class="item-info">
            <h3>{{ item.name }}</h3>
            <p class="price">¥{{ item.price.toFixed(2) }}</p>
          </div>
          <div class="item-quantity">
            <button @click="cartStore.updateQuantity(item.id, item.quantity - 1)">-</button>
            <span>{{ item.quantity }}</span>
            <button @click="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
          </div>
          <div class="item-subtotal">
            ¥{{ (item.price * item.quantity).toFixed(2) }}
          </div>
          <button class="item-remove" @click="cartStore.removeFromCart(item.id)">
            删除
          </button>
        </div>
      </div>
      
      <div class="cart-summary">
        <div class="total-price">
          总计: <span>¥{{ cartStore.totalPrice.toFixed(2) }}</span>
        </div>
        <button class="clear-cart-btn" @click="cartStore.clearCart">
          清空购物车
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cartStore'
</script>

异步数据处理与加载状态

异步数据处理是电商应用的关键环节,需对比不同方案的实现差异并优化用户体验。useFetch与原生fetch的核心区别在于响应式处理与缓存策略:

useFetch vs 原生fetch对比

  • 响应式:useFetch返回的data自动为ref类型,原生fetch需手动用ref包装
  • 缓存:useFetch默认缓存相同请求,原生fetch需自行实现缓存逻辑
  • 状态管理:useFetch内置isLoading、error状态,原生fetch需手动维护

商品详情页采用useFetch预获取数据,结合路由参数实现动态请求:

plaintext
复制代码
<!-- ProductDetail.vue -->
<script setup>
import { useRoute } from 'vue-router'
import { useFetch } from '@vueuse/core'
import { ref } from 'vue'

const route = useRoute()
const id = route.params.id
const { data: product, isLoading, error } = useFetch(`/api/products/${id}`, {
  immediate: true // 路由加载时立即请求
})

// 错误处理
const isError = ref(false)
error.value?.then(err => {
  isError.value = true
  console.error('商品加载失败:', err)
})
</script>

为优化首屏加载体验,使用vue-skeleton-webpack-plugin配置全局骨架屏。在vue.config.js中添加插件配置:

javascript
复制代码
// vue.config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')

module.exports = {
  configureWebpack: {
    plugins: [
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, './src/skeleton.js')
          }
        },
        minimize: true,
        quiet: true,
        router: {
          mode: 'history',
          routes: [
            { path: '/', skeletonId: 'product-list-skeleton' },
            { path: '/product/:id', skeletonId: 'product-detail-skeleton' }
          ]
        }
      })
    ]
  }
}

错误处理采用onErrorCaptured钩子捕获组件树中的错误,统一显示错误提示:

plaintext
复制代码
<!-- ProductDetail.vue -->
<script setup>
import { onErrorCaptured } from 'vue'

onErrorCaptured((err) => {
  console.error('捕获到错误:', err)
  // 显示错误提示
  alert('加载失败,请重试')
  return true // 阻止错误继续传播
})
</script>

通过以上实现,电商网站的产品列表与购物车功能实现了从路由管理、数据交互到状态持久化的完整闭环,同时通过骨架屏、错误处理等细节优化,确保了良好的用户体验。核心技术选型上,VueUse的useFetch简化了异步处理,Pinia提供了高效的状态管理,而持久化插件则保障了用户数据的连贯性,这些工具的组合使用大幅提升了开发效率与代码可维护性。<|FCResponseEnd|>```markdown

在现代电商前端开发中,产品列表与购物车功能是用户体验的核心载体。本章节将基于Vue.js 3.5构建完整的"迷你电商网站"前端系统,涵盖路由设计、数据交互、状态管理及性能优化等关键技术点,通过场景化开发流程展示从页面导航到数据持久化的全链路实现方案。

路由设计与参数传递

路由系统是页面跳转与参数传递的基础,采用Vue Router实现电商网站的核心页面导航。首先创建router/index.ts定义路由配置,通过TypeScript类型约束确保参数传递的安全性:

typescript
复制代码
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import ProductList from '@/views/ProductList.vue'
import ProductDetail from '@/views/ProductDetail.vue'
import Cart from '@/views/Cart.vue'

const routes = [
  { path: '/', name: 'home', component: ProductList },
  { 
    path: '/product/:id', 
    name: 'product', 
    component: ProductDetail,
    props: true // 将路由参数映射为组件props
  },
  { path: '/cart', name: 'cart', component: Cart }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

App.vue中实现顶部导航栏,使用<router-link>组件创建声明式导航,并通过动态绑定购物车数量实现实时更新:

plaintext
复制代码
<!-- App.vue -->
<template>
  <header class="app-header">
    <div class="logo">MiniShop</div>
    <nav>
      <router-link to="/" active-class="active">商品列表</router-link>
      <router-link to="/cart" active-class="active">
        购物车 
        <span class="cart-badge" v-if="cartStore.items.length > 0">
          {{ cartStore.items.length }}
        </span>
      </router-link>
    </nav>
  </header>
  <router-view />
</template>

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

商品列表页跳转到详情页时,通过编程式导航传递商品ID。在ProductList组件中为商品卡片绑定点击事件:

plaintext
复制代码
<!-- ProductList.vue -->
<template>
  <div class="product-grid">
    <div 
      class="product-card" 
      v-for="product in filteredProducts" 
      :key="product.id"
      @click="navigateToDetail(product.id)"
    >
      <img :src="product.imageUrl" :alt="product.name" class="product-image">
      <h3 class="product-name">{{ product.name }}</h3>
      <div class="product-price">¥{{ product.price.toFixed(2) }}</div>
    </div>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

const navigateToDetail = (id) => {
  router.push({ name: 'product', params: { id } })
}
</script>

商品列表与筛选功能

商品列表实现包含数据请求、筛选交互、排序逻辑与加载状态管理四大核心模块。采用VueUse提供的useFetch简化异步数据处理,其内置的响应式转换与请求缓存机制可显著提升开发效率:

plaintext
复制代码
<!-- ProductList.vue -->
<script setup>
import { useFetch } from '@vueuse/core'
import { ref, computed } from 'vue'

// 基础数据获取
const { data: products, isLoading, error } = useFetch('/api/products', {
  refetch: true // 支持手动触发重请求
})

// 筛选条件状态
const priceRange = ref([0, 10000]) // [min, max]
const selectedCategories = ref([])
const sortOption = ref('recommended') // 'recommended' | 'price-asc' | 'price-desc'

// 计算属性:筛选并排序商品
const filteredProducts = computed(() => {
  if (!products.value) return []
  
  return products.value
    // 价格筛选
    .filter(p => p.price >= priceRange.value[0] && p.price <= priceRange.value[1])
    // 分类筛选
    .filter(p => selectedCategories.value.length === 0 || selectedCategories.value.includes(p.category))
    // 排序处理
    .sort((a, b) => {
      switch (sortOption.value) {
        case 'price-asc': return a.price - b.price
        case 'price-desc': return b.price - a.price
        default: return 0 // 推荐排序(原顺序)
      }
    })
})
</script>

筛选表单实现价格区间滑块与分类多选框,通过双向绑定关联筛选条件:

plaintext
复制代码
<!-- ProductList.vue -->
<template>
  <div class="filter-panel">
    <!-- 价格区间滑块 -->
    <div class="price-filter">
      <label>价格区间: ¥{{ priceRange[0] }} - ¥{{ priceRange[1] }}</label>
      <input 
        type="range" 
        v-model="priceRange[0]" 
        min="0" 
        max="10000" 
        step="100"
      >
      <input 
        type="range" 
        v-model="priceRange[1]" 
        min="0" 
        max="10000" 
        step="100"
      >
    </div>

    <!-- 分类多选框 -->
    <div class="category-filter">
      <label v-for="category in allCategories" :key="category">
        <input 
          type="checkbox" 
          :value="category" 
          v-model="selectedCategories"
        >
        {{ category }}
      </label>
    </div>

    <!-- 排序下拉框 -->
    <div class="sort-filter">
      <select v-model="sortOption">
        <option value="recommended">推荐排序</option>
        <option value="price-asc">价格从低到高</option>
        <option value="price-desc">价格从高到低</option>
      </select>
    </div>
  </div>
</template>

加载状态通过骨架屏组件实现,在数据未就绪时显示占位内容:

plaintext
复制代码
<!-- ProductList.vue -->
<template>
  <div class="product-list-container">
    <!-- 骨架屏 -->
    <div v-if="isLoading" class="skeleton-grid">
      <ProductSkeleton v-for="i in 8" :key="i" />
    </div>

    <!-- 错误状态 -->
    <div v-else-if="error" class="error-state">
      <div class="error-icon">⚠️</div>
      <div class="error-message">商品加载失败</div>
      <button class="retry-btn" @click="refetch">重试</button>
    </div>

    <!-- 空状态 -->
    <div v-else-if="filteredProducts.length === 0" class="empty-state">
      <div class="empty-icon">🔍</div>
      <div class="empty-message">未找到符合条件的商品</div>
    </div>

    <!-- 商品列表 -->
    <div v-else class="product-grid">
      <!-- 商品卡片循环 -->
    </div>
  </div>
</template>

骨架屏组件实现如下,通过CSS动画模拟加载状态:

plaintext
复制代码
<!-- ProductSkeleton.vue -->
<template>
  <div class="skeleton-card">
    <div class="skeleton-img"></div>
    <div class="skeleton-line"></div>
    <div class="skeleton-line short"></div>
  </div>
</template>

<style scoped>
.skeleton-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}

.skeleton-img {
  width: 100%;
  height: 200px;
  background: #f0f0f0;
  border-radius: 4px;
  animation: shimmer 1.5s infinite linear;
}

.skeleton-line {
  height: 24px;
  background: #f0f0f0;
  border-radius: 4px;
  animation: shimmer 1.5s infinite linear;
}

.short {
  width: 60%;
}

@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
</style>

购物车状态管理

购物车功能采用Pinia实现跨组件状态共享,结合pinia-plugin-persistedstate实现数据持久化。首先定义cartStore存储核心业务逻辑:

typescript
复制代码
// stores/cartStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 购物车数据
  const items = ref([])

  // 核心方法
  const addToCart = (product, quantity = 1) => {
    const existingItemIndex = items.value.findIndex(item => item.id === product.id)
    
    if (existingItemIndex > -1) {
      // 商品已存在,更新数量
      items.value[existingItemIndex].quantity += quantity
    } else {
      // 新增商品
      items.value.push({
        ...product,
        quantity,
        // 确保必要字段存在
        id: product.id,
        name: product.name,
        price: product.price,
        imageUrl: product.imageUrl || ''
      })
    }
  }

  const updateQuantity = (productId, newQuantity) => {
    const item = items.value.find(item => item.id === productId)
    if (item) {
      // 确保数量为正整数
      item.quantity = Math.max(1, Math.floor(newQuantity))
    }
  }

  const removeFromCart = (productId) => {
    items.value = items.value.filter(item => item.id !== productId)
  }

  const clearCart = () => {
    items.value = []
  }

  // 计算属性
  const totalItems = computed(() => {
    return items.value.reduce((sum, item) => sum + item.quantity, 0)
  })

  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  })

  return {
    items,
    addToCart,
    updateQuantity,
    removeFromCart,
    clearCart,
    totalItems,
    totalPrice
  }
}, {
  // 持久化配置
  persist: {
    key: 'mini-shop-cart',
    storage: localStorage,
    paths: ['items'] // 仅持久化items字段
  }
})

在商品详情页实现"加入购物车"功能,包含数量选择器与交互反馈:

plaintext
复制代码
<!-- ProductDetail.vue -->
<template>
  <div class="product-detail">
    <div class="product-gallery">
      <img :src="product.imageUrl" :alt="product.name">
    </div>
    
    <div class="product-info">
      <h1 class="product-title">{{ product.name }}</h1>
      <div class="product-price">¥{{ product.price.toFixed(2) }}</div>
      <div class="product-desc">{{ product.description }}</div>
      
      <div class="quantity-selector">
        <label>数量:</label>
        <button @click="decrementQuantity">-</button>
        <input type="number" v-model="quantity" min="1">
        <button @click="incrementQuantity">+</button>
      </div>
      
      <button 
        class="add-to-cart-btn" 
        @click="handleAddToCart"
      >
        加入购物车
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useCartStore } from '@/stores/cartStore'
import { useRoute } from 'vue-router'
import { useFetch } from '@vueuse/core'

const route = useRoute()
const cartStore = useCartStore()
const quantity = ref(1)
const { data: product } = useFetch(`/api/products/${route.params.id}`)

const incrementQuantity = () => {
  quantity.value++
}

const decrementQuantity = () => {
  if (quantity.value > 1) {
    quantity.value--
  }
}

const handleAddToCart = () => {
  if (product.value) {
    cartStore.addToCart(product.value, quantity.value)
    // 重置数量
    quantity.value = 1
    // 显示成功提示
    alert('商品已加入购物车')
  }
}
</script>

购物车页面实现商品列表展示、数量修改、删除功能及结算信息:

plaintext
复制代码
<!-- Cart.vue -->
<template>
  <div class="cart-container">
    <h1 class="cart-title">我的购物车</h1>
    
    <!-- 空购物车状态 -->
    <div v-if="cartStore.items.length === 0" class="empty-cart">
      <div class="empty-icon">🛒</div>
      <p>购物车还是空的</p>
      <router-link to="/" class="continue-shopping">继续购物</router-link>
    </div>
    
    <!-- 购物车列表 -->
    <div v-else>
      <div class="cart-items">
        <div class="cart-item" v-for="item in cartStore.items" :key="item.id">
          <img :src="item.imageUrl" :alt="item.name" class="item-img">
          
          <div class="item-info">
            <h3 class="item-name">{{ item.name }}</h3>
            <div class="item-price">¥{{ item.price.toFixed(2) }}</div>
          </div>
          
          <div class="item-quantity">
            <button @click="cartStore.updateQuantity(item.id, item.quantity - 1)">-</button>
            <span>{{ item.quantity }}</span>
            <button @click="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
          </div>
          
          <div class="item-subtotal">
            ¥{{ (item.price * item.quantity).toFixed(2) }}
          </div>
          
          <button 
            class="item-remove" 
            @click="cartStore.removeFromCart(item.id)"
          >
            删除
          </button>
        </div>
      </div>
      
      <div class="cart-summary">
        <div class="summary-row">
          <span>商品总数:</span>
          <span>{{ cartStore.totalItems }}</span>
        </div>
        <div class="summary-row total-price">
          <span>总计:</span>
          <span>¥{{ cartStore.totalPrice.toFixed(2) }}</span>
        </div>
        
        <div class="cart-actions">
          <button class="clear-cart-btn" @click="cartStore.clearCart">
            清空购物车
          </button>
          <button class="checkout-btn">去结算</button>
        </div>
      </div>
    </div>
  </div>
</template>

异步数据处理与加载状态

异步数据处理是电商应用的关键环节,需实现高效的数据请求、状态管理与错误处理机制。对比useFetch与原生fetch的实现差异:

数据请求方案对比

特性 useFetch (VueUse) 原生fetch
响应式 自动转换为ref类型 需手动用ref包装
状态管理 内置isLoading/error 需手动维护状态
缓存机制 支持请求缓存 需自行实现
重请求 提供refetch方法 需重新创建请求
类型支持 泛型类型定义 需手动指定类型

商品详情页采用数据预获取策略,结合路由参数动态加载商品数据:

plaintext
复制代码
<!-- ProductDetail.vue -->
<script setup>
import { useRoute } from 'vue-router'
import { useFetch } from '@vueuse/core'
import { ref } from 'vue'

const route = useRoute()
const id = route.params.id
const isError = ref(false)

// 商品数据请求
const { data: product, isLoading, error } = useFetch(`/api/products/${id}`, {
  beforeFetch({ url, options }) {
    // 请求前处理
    return { url, options }
  },
  afterFetch(ctx) {
    // 响应后处理
    if (ctx.response.ok) {
      return { data: ctx.data.data } // 提取响应体data字段
    }
    return ctx
  }
})

// 错误处理
error.value?.then(err => {
  isError.value = true
  console.error('商品加载失败:', err)
})
</script>

加载状态通过骨架屏组件实现,在详情页中根据isLoading状态切换显示:

plaintext
复制代码
<!-- ProductDetail.vue -->
<template>
  <div v-if="isLoading" class="detail-skeleton">
    <div class="skeleton-row">
      <div class="skeleton-img"></div>
      <div class="skeleton-info">
        <div class="skeleton-line long"></div>
        <div class="skeleton-line medium"></div>
        <div class="skeleton-line full"></div>
        <div class="skeleton-line short"></div>
        <div class="skeleton-btn"></div>
      </div>
    </div>
  </div>
  
  <div v-else-if="isError" class="error-state">
    <div class="error-icon">❌</div>
    <div class="error-message">商品加载失败</div>
    <button class="retry-btn" @click="refetch">重试</button>
  </div>
  
  <div v-else>
    <!-- 商品详情内容 -->
  </div>
</template>

全局错误处理通过onErrorCaptured钩子实现,捕获组件树中的错误并统一处理:

plaintext
复制代码
<!-- App.vue -->
<script setup>
import { onErrorCaptured } from 'vue'

onErrorCaptured((err, instance, info) => {
  console.error('全局错误捕获:', err, instance, info)
  // 显示错误提示
  alert('系统异常,请刷新页面重试')
  return true // 阻止错误继续传播
})
</script>

通过以上实现,电商网站的产品列表与购物车功能实现了从路由管理、数据交互到状态持久化的完整闭环。技术选型上,VueUse的useFetch简化了异步处理,Pinia提供了高效的状态管理,而持久化插件则保障了用户数据的连贯性,这些工具的组合使用大幅提升了开发效率和代码可维护性。在用户体验层面,通过骨架屏、空状态、错误处理等细节优化,确保了应用在各种场景下的稳定性和友好性。