Browse Source

✨ feat(cart): 优化购物车页面交互体验

- 添加购物车商品数量角标显示功能
- 实现购物车页面骨架屏加载效果
- 优化空购物车状态显示和引导按钮样式

✨ feat(cart): 增强购物车功能

- 添加商品选择功能和批量删除
- 实现全选/取消全选功能
- 优化结算按钮样式和交互反馈

♻️ refactor(cart): 重构购物车逻辑

- 添加isLoading状态管理加载过程
- 优化购物车商品总价计算逻辑
- 修复底部结算栏被TabBar遮挡问题

💄 style(cart): 优化购物车UI样式

- 改进商品卡片布局和视觉层次
- 优化价格显示样式,增加原价对比
- 美化选择框和按钮样式
yourname 3 months ago
parent
commit
5f2fd56421
3 changed files with 227 additions and 94 deletions
  1. 14 2
      mini/src/layouts/tab-bar-layout.tsx
  2. 206 89
      mini/src/pages/cart/index.tsx
  3. 7 3
      mini/src/utils/cart.ts

+ 14 - 2
mini/src/layouts/tab-bar-layout.tsx

@@ -6,6 +6,7 @@ import Taro from '@tarojs/taro'
 export interface TabBarLayoutProps {
   children: ReactNode
   activeKey: string
+  cartCount?: number
 }
 
 const tabBarItems: TabBarItem[] = [
@@ -35,7 +36,7 @@ const tabBarItems: TabBarItem[] = [
   },
 ]
 
-export const TabBarLayout: React.FC<TabBarLayoutProps> = ({ children, activeKey }) => {
+export const TabBarLayout: React.FC<TabBarLayoutProps> = ({ children, activeKey, cartCount }) => {
   const handleTabChange = (key: string) => {
     // 使用 Taro 的导航 API 进行页面跳转
     switch (key) {
@@ -56,13 +57,24 @@ export const TabBarLayout: React.FC<TabBarLayoutProps> = ({ children, activeKey
     }
   }
 
+  // 动态设置购物车标签的角标
+  const tabItemsWithBadge = tabBarItems.map(item => {
+    if (item.key === 'cart' && cartCount && cartCount > 0) {
+      return {
+        ...item,
+        badge: cartCount > 99 ? '99+' : cartCount
+      }
+    }
+    return item
+  })
+
   return (
     <View className="min-h-screen bg-gray-50 flex flex-col">
       <View className="flex-1 flex flex-col">
         {children}
       </View>
       <TabBar
-        items={tabBarItems}
+        items={tabItemsWithBadge}
         activeKey={activeKey}
         onChange={handleTabChange}
         fixed={true}

+ 206 - 89
mini/src/pages/cart/index.tsx

@@ -1,5 +1,5 @@
 import { View, ScrollView, Text } from '@tarojs/components'
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 import Taro from '@tarojs/taro'
 import { Navbar } from '@/components/ui/navbar'
 import { Card } from '@/components/ui/card'
@@ -7,10 +7,12 @@ import { Button } from '@/components/ui/button'
 import { Image } from '@/components/ui/image'
 import { useCart } from '@/utils/cart'
 import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import clsx from 'clsx'
 
 export default function CartPage() {
-  const { cart, updateQuantity, removeFromCart, clearCart } = useCart()
+  const { cart, updateQuantity, removeFromCart, clearCart, isLoading } = useCart()
   const [selectedItems, setSelectedItems] = useState<number[]>([])
+  const [showSkeleton, setShowSkeleton] = useState(true)
 
   // 全选/取消全选
   const toggleSelectAll = () => {
@@ -35,6 +37,14 @@ export default function CartPage() {
     .filter(item => selectedItems.includes(item.id))
     .reduce((sum, item) => sum + (item.price * item.quantity), 0)
 
+  // 添加骨架屏效果
+  useEffect(() => {
+    if (!isLoading) {
+      const timer = setTimeout(() => setShowSkeleton(false), 300)
+      return () => clearTimeout(timer)
+    }
+  }, [isLoading])
+
   // 去结算
   const handleCheckout = () => {
     if (selectedItems.length === 0) {
@@ -58,8 +68,42 @@ export default function CartPage() {
     })
   }
 
+  // 骨架屏组件
+  const CartSkeleton = () => (
+    <View className="px-4 py-4">
+      {[...Array(3)].map((_, index) => (
+        <Card key={index} className="mb-4">
+          <View className="p-4">
+            <View className="flex items-start">
+              <View className="w-5 h-5 bg-gray-200 rounded-full mr-3 mt-8" />
+              <View className="w-20 h-20 bg-gray-200 rounded-lg mr-3" />
+              <View className="flex-1">
+                <View className="h-4 bg-gray-200 rounded mb-2 w-3/4" />
+                <View className="h-4 bg-gray-200 rounded mb-2 w-1/2" />
+                <View className="flex items-center justify-between">
+                  <View className="flex items-center">
+                    <View className="w-6 h-6 bg-gray-200 rounded" />
+                    <View className="w-8 h-6 bg-gray-200 mx-2" />
+                    <View className="w-6 h-6 bg-gray-200 rounded" />
+                  </View>
+                  <View className="w-12 h-6 bg-gray-200 rounded" />
+                </View>
+              </View>
+            </View>
+          </View>
+        </Card>
+      ))}
+    </View>
+  )
+
+  // 计算购物车商品总数
+  const cartItemCount = cart.items.reduce((sum, item) => sum + item.quantity, 0)
+
   return (
-    <TabBarLayout activeKey="cart">
+    <TabBarLayout
+      activeKey="cart"
+      cartCount={cartItemCount}
+    >
       <Navbar
         title="购物车"
         leftIcon="i-heroicons-chevron-left-20-solid"
@@ -79,130 +123,203 @@ export default function CartPage() {
         }}
       />
       
-      <ScrollView className="flex-1">
+      <ScrollView
+        className="flex-1"
+        scrollY
+        scrollWithAnimation
+      >
         <View className="px-4 py-4">
-          {cart.items.length === 0 ? (
-            <View className="flex flex-col items-center justify-center py-20">
-              <View className="i-heroicons-shopping-cart-20-solid w-16 h-16 text-gray-300 mb-4" />
-              <Text className="text-gray-500 mb-4">购物车是空的</Text>
+          {showSkeleton && cart.items.length === 0 ? (
+            <CartSkeleton />
+          ) : cart.items.length === 0 ? (
+            <View className="flex flex-col items-center justify-center py-32">
+              <View className="i-heroicons-shopping-cart-20-solid w-20 h-20 text-gray-300 mb-6" />
+              <Text className="text-gray-500 text-lg mb-2">购物车是空的</Text>
+              <Text className="text-gray-400 text-sm mb-6">快去挑选心仪的商品吧</Text>
               <Button
                 onClick={() => Taro.navigateTo({ url: '/pages/goods-list/index' })}
+                className="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-8 py-3 rounded-full font-medium"
               >
-                去逛逛
+                立即选购
               </Button>
             </View>
           ) : (
-            <>
-              {/* 全选 */}
-              <View className="bg-white rounded-lg p-4 mb-4 flex items-center">
-                <View 
-                  className={`w-5 h-5 border-2 rounded-full flex items-center justify-center mr-3 ${
-                    selectedItems.length === cart.items.length 
-                      ? 'bg-blue-500 border-blue-500' 
-                      : 'border-gray-300'
-                  }`}
-                  onClick={toggleSelectAll}
-                >
-                  {selectedItems.length === cart.items.length && (
-                    <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
+            <View>
+              {/* 全选和批量操作 */}
+              <Card className="mb-4">
+                <View className="p-4 flex items-center justify-between">
+                  <View className="flex items-center">
+                    <View
+                      className={clsx(
+                        'w-5 h-5 rounded-full flex items-center justify-center mr-3',
+                        selectedItems.length === cart.items.length
+                          ? 'bg-blue-500 border-blue-500'
+                          : 'border-2 border-gray-300'
+                      )}
+                      onClick={toggleSelectAll}
+                    >
+                      {selectedItems.length === cart.items.length && (
+                        <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
+                      )}
+                    </View>
+                    <Text className="text-gray-900 font-medium">
+                      全选 ({cart.items.length}件商品)
+                    </Text>
+                  </View>
+                  
+                  {selectedItems.length > 0 && (
+                    <View
+                      className="text-red-500 text-sm"
+                      onClick={() => {
+                        Taro.showModal({
+                          title: '删除选中商品',
+                          content: `确定要删除选中的${selectedItems.length}件商品吗?`,
+                          success: (res) => {
+                            if (res.confirm) {
+                              selectedItems.forEach(id => removeFromCart(id))
+                              setSelectedItems([])
+                            }
+                          }
+                        })
+                      }}
+                    >
+                      删除选中
+                    </View>
                   )}
                 </View>
-                <Text className="text-gray-700">全选 ({cart.items.length}件商品)</Text>
-              </View>
+              </Card>
 
               {/* 商品列表 */}
-              {cart.items.map((item) => (
-                <Card key={item.id} className="mb-4">
-                  <View className="p-4">
-                    <View className="flex items-start">
-                      <View 
-                        className={`w-5 h-5 border-2 rounded-full flex items-center justify-center mr-3 mt-8 ${
-                          selectedItems.includes(item.id)
-                            ? 'bg-blue-500 border-blue-500'
-                            : 'border-gray-300'
-                        }`}
-                        onClick={() => toggleSelectItem(item.id)}
-                      >
-                        {selectedItems.includes(item.id) && (
-                          <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
-                        )}
-                      </View>
-                      
-                      <Image 
-                        src={item.image}
-                        className="w-20 h-20 rounded-lg mr-3"
-                        mode="aspectFill"
-                      />
-                      
-                      <View className="flex-1">
-                        <Text className="text-sm font-medium text-gray-900 mb-2 line-clamp-2">
-                          {item.name}
-                        </Text>
+              <View className="space-y-4">
+                {cart.items.map((item) => (
+                  <Card key={item.id} className="overflow-hidden">
+                    <View className="p-4">
+                      <View className="flex items-start">
+                        {/* 选择框 */}
+                        <View
+                          className={clsx(
+                            'w-5 h-5 rounded-full flex items-center justify-center mr-3 mt-16 flex-shrink-0',
+                            selectedItems.includes(item.id)
+                              ? 'bg-blue-500 border-blue-500'
+                              : 'border-2 border-gray-300'
+                          )}
+                          onClick={() => toggleSelectItem(item.id)}
+                        >
+                          {selectedItems.includes(item.id) && (
+                            <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
+                          )}
+                        </View>
                         
-                        <Text className="text-red-500 font-bold mb-2">
-                          ¥{item.price.toFixed(2)}
-                        </Text>
+                        {/* 商品图片 */}
+                        <Image
+                          src={item.image}
+                          className="w-24 h-24 rounded-lg mr-3 flex-shrink-0"
+                          mode="aspectFill"
+                        />
                         
-                        <View className="flex items-center justify-between">
-                          <View className="flex items-center border border-gray-300 rounded">
-                            <Button
-                              size="sm"
-                              variant="ghost"
-                              className="px-2"
-                              onClick={() => updateQuantity(item.id, item.quantity - 1)}
-                            >
-                              -
-                            </Button>
-                            <Text className="px-3 py-1 border-x border-gray-300 text-sm">
-                              {item.quantity}
+                        {/* 商品信息 */}
+                        <View className="flex-1 min-w-0">
+                          <Text className="text-sm font-medium text-gray-900 mb-2 line-clamp-2">
+                            {item.name}
+                          </Text>
+                          
+                          <View className="mb-3">
+                            <Text className="text-red-500 font-bold text-lg">
+                              ¥{item.price.toFixed(2)}
                             </Text>
+                            {item.originalPrice && item.originalPrice > item.price && (
+                              <Text className="text-gray-400 text-sm line-through ml-2">
+                                ¥{item.originalPrice.toFixed(2)}
+                              </Text>
+                            )}
+                          </View>
+                          
+                          <View className="flex items-center justify-between">
+                            {/* 数量选择器 */}
+                            <View className="flex items-center border border-gray-200 rounded-lg bg-gray-50">
+                              <Button
+                                size="sm"
+                                variant="ghost"
+                                className="px-2 h-8 w-8 flex items-center justify-center text-gray-600"
+                                onClick={() => updateQuantity(item.id, Math.max(1, item.quantity - 1))}
+                                disabled={item.quantity <= 1}
+                              >
+                                <View className="i-heroicons-minus-20-solid w-4 h-4" />
+                              </Button>
+                              <Text className="px-4 py-1 border-x border-gray-200 text-sm font-medium min-w-12 text-center">
+                                {item.quantity}
+                              </Text>
+                              <Button
+                                size="sm"
+                                variant="ghost"
+                                className="px-2 h-8 w-8 flex items-center justify-center text-gray-600"
+                                onClick={() => updateQuantity(item.id, item.quantity + 1)}
+                              >
+                                <View className="i-heroicons-plus-20-solid w-4 h-4" />
+                              </Button>
+                            </View>
+                            
+                            {/* 删除按钮 */}
                             <Button
                               size="sm"
                               variant="ghost"
-                              className="px-2"
-                              onClick={() => updateQuantity(item.id, item.quantity + 1)}
+                              className="text-red-500 p-2"
+                              onClick={() => {
+                                Taro.showModal({
+                                  title: '删除商品',
+                                  content: '确定要删除这个商品吗?',
+                                  success: (res) => {
+                                    if (res.confirm) {
+                                      removeFromCart(item.id)
+                                      setSelectedItems(prev => prev.filter(id => id !== item.id))
+                                    }
+                                  }
+                                })
+                              }}
                             >
-                              +
+                              <View className="i-heroicons-trash-20-solid w-4 h-4" />
                             </Button>
                           </View>
-                          
-                          <Button
-                            size="sm"
-                            variant="ghost"
-                            className="text-red-500"
-                            onClick={() => removeFromCart(item.id)}
-                          >
-                            删除
-                          </Button>
                         </View>
                       </View>
                     </View>
-                  </View>
-                </Card>
-              ))}
-            </>
+                  </Card>
+                ))}
+              </View>
+              
+              {/* 底部留白 */}
+              <View className="h-24" />
+            </View>
           )}
         </View>
       </ScrollView>
 
-      {/* 底部结算栏 */}
+      {/* 底部结算栏 - 修正TabBar遮挡问题 */}
       {cart.items.length > 0 && (
-        <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+        <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 pb-20">
           <View className="flex items-center justify-between">
             <View>
               <Text className="text-sm text-gray-600">
-                已选 {selectedItems.length} 件
-              </Text>
-              <Text className="text-lg font-bold text-red-500">
-                ¥{selectedItemsTotal.toFixed(2)}
+                已选 {selectedItems.length} 件商品
               </Text>
+              <View className="flex items-baseline">
+                <Text className="text-red-500 font-bold text-xl">
+                  ¥{selectedItemsTotal.toFixed(2)}
+                </Text>
+              </View>
             </View>
             
             <Button
               onClick={handleCheckout}
               disabled={selectedItems.length === 0}
+              className={clsx(
+                'px-8 py-3 rounded-full font-medium text-base',
+                selectedItems.length > 0
+                  ? 'bg-gradient-to-r from-red-500 to-red-600 text-white shadow-lg'
+                  : 'bg-gray-300 text-gray-500'
+              )}
             >
-              去结算 ({selectedItems.length})
+              {selectedItems.length > 0 ? `去结算(${selectedItems.length})` : '请选择商品'}
             </Button>
           </View>
         </View>

+ 7 - 3
mini/src/utils/cart.ts

@@ -24,6 +24,7 @@ export const useCart = () => {
     totalAmount: 0,
     totalCount: 0
   })
+  const [isLoading, setIsLoading] = useState(true)
 
   // 从本地存储加载购物车
   useEffect(() => {
@@ -31,9 +32,9 @@ export const useCart = () => {
       try {
         const savedCart = Taro.getStorageSync(CART_STORAGE_KEY)
         if (savedCart && Array.isArray(savedCart.items)) {
-          const totalAmount = savedCart.items.reduce((sum: number, item: CartItem) => 
+          const totalAmount = savedCart.items.reduce((sum: number, item: CartItem) =>
             sum + (item.price * item.quantity), 0)
-          const totalCount = savedCart.items.reduce((sum: number, item: CartItem) => 
+          const totalCount = savedCart.items.reduce((sum: number, item: CartItem) =>
             sum + item.quantity, 0)
           
           setCart({
@@ -44,6 +45,8 @@ export const useCart = () => {
         }
       } catch (error) {
         console.error('加载购物车失败:', error)
+      } finally {
+        setIsLoading(false)
       }
     }
 
@@ -170,6 +173,7 @@ export const useCart = () => {
     updateQuantity,
     clearCart,
     isInCart,
-    getItemQuantity
+    getItemQuantity,
+    isLoading
   }
 }