|
|
@@ -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>
|