|
@@ -1,21 +1,30 @@
|
|
|
-import { View, ScrollView, Text, Image, RichText } from '@tarojs/components'
|
|
|
|
|
|
|
+import { View, ScrollView, Text, RichText } from '@tarojs/components'
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
import { useState } from 'react'
|
|
import { useState } from 'react'
|
|
|
import Taro from '@tarojs/taro'
|
|
import Taro from '@tarojs/taro'
|
|
|
import { goodsClient } from '@/api'
|
|
import { goodsClient } from '@/api'
|
|
|
-import { InferResponseType } from 'hono'
|
|
|
|
|
|
|
+// import { InferResponseType } from 'hono'
|
|
|
import { Navbar } from '@/components/ui/navbar'
|
|
import { Navbar } from '@/components/ui/navbar'
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Button } from '@/components/ui/button'
|
|
|
-import { Card } from '@/components/ui/card'
|
|
|
|
|
import { Carousel } from '@/components/ui/carousel'
|
|
import { Carousel } from '@/components/ui/carousel'
|
|
|
|
|
+import { GoodsSpecSelector } from '@/components/goods-spec-selector'
|
|
|
import { useCart } from '@/utils/cart'
|
|
import { useCart } from '@/utils/cart'
|
|
|
|
|
+import './index.css'
|
|
|
|
|
|
|
|
-type GoodsResponse = InferResponseType<typeof goodsClient[':id']['$get'], 200>
|
|
|
|
|
|
|
+// type GoodsResponse = InferResponseType<typeof goodsClient[':id']['$get'], 200>
|
|
|
|
|
+
|
|
|
|
|
+interface SelectedSpec {
|
|
|
|
|
+ name: string
|
|
|
|
|
+ price: number
|
|
|
|
|
+ stock: number
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
export default function GoodsDetailPage() {
|
|
export default function GoodsDetailPage() {
|
|
|
const [quantity, setQuantity] = useState(1)
|
|
const [quantity, setQuantity] = useState(1)
|
|
|
|
|
+ const [selectedSpec, setSelectedSpec] = useState<SelectedSpec | null>(null)
|
|
|
|
|
+ const [showSpecModal, setShowSpecModal] = useState(false)
|
|
|
const { addToCart } = useCart()
|
|
const { addToCart } = useCart()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 获取商品ID
|
|
// 获取商品ID
|
|
|
const params = Taro.getCurrentInstance().router?.params
|
|
const params = Taro.getCurrentInstance().router?.params
|
|
|
const goodsId = params?.id ? parseInt(params.id) : 0
|
|
const goodsId = params?.id ? parseInt(params.id) : 0
|
|
@@ -36,17 +45,31 @@ export default function GoodsDetailPage() {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 商品轮播图
|
|
// 商品轮播图
|
|
|
- const carouselItems = goods?.slideImages?.map(file => ({
|
|
|
|
|
|
|
+ const carouselItems = goods?.slideImages?.map((file: any) => ({
|
|
|
src: file.fullUrl || '',
|
|
src: file.fullUrl || '',
|
|
|
title: goods.name,
|
|
title: goods.name,
|
|
|
description: ''
|
|
description: ''
|
|
|
})) || []
|
|
})) || []
|
|
|
|
|
|
|
|
|
|
+ // 规格选择处理
|
|
|
|
|
+ const handleSpecSelect = (spec: any, selectedQuantity: number) => {
|
|
|
|
|
+ setSelectedSpec({
|
|
|
|
|
+ name: spec.name,
|
|
|
|
|
+ price: spec.price,
|
|
|
|
|
+ stock: spec.stock
|
|
|
|
|
+ })
|
|
|
|
|
+ setQuantity(selectedQuantity)
|
|
|
|
|
+ setShowSpecModal(false)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 添加到购物车
|
|
// 添加到购物车
|
|
|
const handleAddToCart = () => {
|
|
const handleAddToCart = () => {
|
|
|
if (!goods) return
|
|
if (!goods) return
|
|
|
-
|
|
|
|
|
- if (quantity > goods.stock) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const currentPrice = selectedSpec?.price || goods.price
|
|
|
|
|
+ const currentStock = selectedSpec?.stock || goods.stock
|
|
|
|
|
+
|
|
|
|
|
+ if (quantity > currentStock) {
|
|
|
Taro.showToast({
|
|
Taro.showToast({
|
|
|
title: '库存不足',
|
|
title: '库存不足',
|
|
|
icon: 'none'
|
|
icon: 'none'
|
|
@@ -57,12 +80,13 @@ export default function GoodsDetailPage() {
|
|
|
addToCart({
|
|
addToCart({
|
|
|
id: goods.id,
|
|
id: goods.id,
|
|
|
name: goods.name,
|
|
name: goods.name,
|
|
|
- price: goods.price,
|
|
|
|
|
|
|
+ price: currentPrice,
|
|
|
image: goods.imageFile?.fullUrl || '',
|
|
image: goods.imageFile?.fullUrl || '',
|
|
|
- stock: goods.stock,
|
|
|
|
|
- quantity
|
|
|
|
|
|
|
+ stock: currentStock,
|
|
|
|
|
+ quantity,
|
|
|
|
|
+ spec: selectedSpec?.name || ''
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
Taro.showToast({
|
|
Taro.showToast({
|
|
|
title: '已添加到购物车',
|
|
title: '已添加到购物车',
|
|
|
icon: 'success'
|
|
icon: 'success'
|
|
@@ -72,8 +96,11 @@ export default function GoodsDetailPage() {
|
|
|
// 立即购买
|
|
// 立即购买
|
|
|
const handleBuyNow = () => {
|
|
const handleBuyNow = () => {
|
|
|
if (!goods) return
|
|
if (!goods) return
|
|
|
-
|
|
|
|
|
- if (quantity > goods.stock) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const currentPrice = selectedSpec?.price || goods.price
|
|
|
|
|
+ const currentStock = selectedSpec?.stock || goods.stock
|
|
|
|
|
+
|
|
|
|
|
+ if (quantity > currentStock) {
|
|
|
Taro.showToast({
|
|
Taro.showToast({
|
|
|
title: '库存不足',
|
|
title: '库存不足',
|
|
|
icon: 'none'
|
|
icon: 'none'
|
|
@@ -86,13 +113,14 @@ export default function GoodsDetailPage() {
|
|
|
goods: {
|
|
goods: {
|
|
|
id: goods.id,
|
|
id: goods.id,
|
|
|
name: goods.name,
|
|
name: goods.name,
|
|
|
- price: goods.price,
|
|
|
|
|
|
|
+ price: currentPrice,
|
|
|
image: goods.imageFile?.fullUrl || '',
|
|
image: goods.imageFile?.fullUrl || '',
|
|
|
- quantity
|
|
|
|
|
|
|
+ quantity,
|
|
|
|
|
+ spec: selectedSpec?.name || ''
|
|
|
},
|
|
},
|
|
|
- totalAmount: goods.price * quantity
|
|
|
|
|
|
|
+ totalAmount: currentPrice * quantity
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
Taro.navigateTo({
|
|
Taro.navigateTo({
|
|
|
url: '/pages/order-submit/index'
|
|
url: '/pages/order-submit/index'
|
|
|
})
|
|
})
|
|
@@ -115,121 +143,167 @@ export default function GoodsDetailPage() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <View className="min-h-screen bg-gray-50">
|
|
|
|
|
|
|
+ <View className="goods-detail-page">
|
|
|
<Navbar
|
|
<Navbar
|
|
|
title="商品详情"
|
|
title="商品详情"
|
|
|
leftIcon="i-heroicons-chevron-left-20-solid"
|
|
leftIcon="i-heroicons-chevron-left-20-solid"
|
|
|
onClickLeft={() => Taro.navigateBack()}
|
|
onClickLeft={() => Taro.navigateBack()}
|
|
|
/>
|
|
/>
|
|
|
-
|
|
|
|
|
- <ScrollView className="h-screen pt-12 pb-20">
|
|
|
|
|
- {/* 商品轮播图 */}
|
|
|
|
|
- {carouselItems.length > 0 && (
|
|
|
|
|
- <View className="mb-4">
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <ScrollView className="goods-detail-scroll" scrollY>
|
|
|
|
|
+ {/* 商品轮播图区域 */}
|
|
|
|
|
+ <View className="goods-swiper-section">
|
|
|
|
|
+ {carouselItems.length > 0 && (
|
|
|
<Carousel
|
|
<Carousel
|
|
|
items={carouselItems}
|
|
items={carouselItems}
|
|
|
- height={375}
|
|
|
|
|
|
|
+ height={750}
|
|
|
autoplay={true}
|
|
autoplay={true}
|
|
|
interval={4000}
|
|
interval={4000}
|
|
|
circular={true}
|
|
circular={true}
|
|
|
/>
|
|
/>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 商品信息区域 */}
|
|
|
|
|
+ <View className="goods-info-section">
|
|
|
|
|
+ <View className="goods-price-row">
|
|
|
|
|
+ <View className="price-container">
|
|
|
|
|
+ <Text className="current-price">¥{(selectedSpec?.price || goods.price).toFixed(2)}</Text>
|
|
|
|
|
+ <Text className="original-price">¥{goods.costPrice.toFixed(2)}</Text>
|
|
|
|
|
+ {!selectedSpec && <Text className="price-suffix">起</Text>}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="sales-info">
|
|
|
|
|
+ <Text className="sales-text">已售{goods.salesNum}件</Text>
|
|
|
|
|
+ </View>
|
|
|
</View>
|
|
</View>
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- <View className="px-4 space-y-4">
|
|
|
|
|
- {/* 商品信息 */}
|
|
|
|
|
- <Card>
|
|
|
|
|
- <View className="p-4">
|
|
|
|
|
- <Text className="text-xl font-bold text-gray-900 mb-2">
|
|
|
|
|
- {goods.name}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <Text className="goods-title">{goods.name}</Text>
|
|
|
|
|
+ <Text className="goods-description">{goods.instructions || '暂无商品描述'}</Text>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 规格选择区域 */}
|
|
|
|
|
+ <View className="spec-section">
|
|
|
|
|
+ <View className="spec-header">
|
|
|
|
|
+ <Text className="spec-title">规格</Text>
|
|
|
|
|
+ <Text className="spec-selected">
|
|
|
|
|
+ {selectedSpec ? `已选:${selectedSpec.name}` : '请选择规格'}
|
|
|
</Text>
|
|
</Text>
|
|
|
-
|
|
|
|
|
- <Text className="text-sm text-gray-500 mb-4">
|
|
|
|
|
- {goods.instructions || '暂无商品描述'}
|
|
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="spec-selector"
|
|
|
|
|
+ onClick={() => setShowSpecModal(true)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text className="spec-placeholder">
|
|
|
|
|
+ {selectedSpec ? selectedSpec.name : '选择规格'}
|
|
|
</Text>
|
|
</Text>
|
|
|
-
|
|
|
|
|
- <View className="flex items-center justify-between mb-4">
|
|
|
|
|
- <View>
|
|
|
|
|
- <Text className="text-red-500 text-2xl font-bold">
|
|
|
|
|
- ¥{goods.price.toFixed(2)}
|
|
|
|
|
- </Text>
|
|
|
|
|
- <Text className="text-sm text-gray-400 line-through ml-2">
|
|
|
|
|
- ¥{goods.costPrice.toFixed(2)}
|
|
|
|
|
- </Text>
|
|
|
|
|
- </View>
|
|
|
|
|
- <View className="text-sm text-gray-500">
|
|
|
|
|
- 库存: {goods.stock}件
|
|
|
|
|
- </View>
|
|
|
|
|
- </View>
|
|
|
|
|
-
|
|
|
|
|
- <View className="text-sm text-gray-500">
|
|
|
|
|
- 已售: {goods.salesNum}件
|
|
|
|
|
- </View>
|
|
|
|
|
|
|
+ <View className="i-heroicons-chevron-right-20-solid spec-arrow" />
|
|
|
</View>
|
|
</View>
|
|
|
- </Card>
|
|
|
|
|
-
|
|
|
|
|
- {/* 商品详情 */}
|
|
|
|
|
- <Card>
|
|
|
|
|
- <View className="p-4">
|
|
|
|
|
- <Text className="text-lg font-bold mb-4">商品详情</Text>
|
|
|
|
|
- {goods.detail ? (
|
|
|
|
|
- <RichText
|
|
|
|
|
- nodes={goods.detail
|
|
|
|
|
- .replace(/<img/g, '<img style="max-width:100%;height:auto"')
|
|
|
|
|
- .replace(/<p>/g, '<p style="margin:10px 0">')
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <Text className="text-gray-500">暂无商品详情</Text>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 商品评价区域 */}
|
|
|
|
|
+ <View className="review-section">
|
|
|
|
|
+ <View className="review-header">
|
|
|
|
|
+ <Text className="review-title">商品评价</Text>
|
|
|
|
|
+ <View className="review-more" onClick={() => Taro.navigateTo({ url: '/pages/reviews/index' })}>
|
|
|
|
|
+ <Text className="review-more-text">查看全部</Text>
|
|
|
|
|
+ <View className="i-heroicons-chevron-right-20-solid review-arrow" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View className="review-stats">
|
|
|
|
|
+ <View className="rating-overview">
|
|
|
|
|
+ <Text className="rating-score">4.8</Text>
|
|
|
|
|
+ <Text className="rating-text">分</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="rating-details">
|
|
|
|
|
+ <Text className="rating-count">999+ 条评价</Text>
|
|
|
|
|
+ <Text className="rating-percent">99% 好评</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 评价列表(模拟数据) */}
|
|
|
|
|
+ <View className="review-list">
|
|
|
|
|
+ <View className="review-item">
|
|
|
|
|
+ <View className="review-user">
|
|
|
|
|
+ <Text className="user-name">用户****</Text>
|
|
|
|
|
+ <Text className="review-time">2025-11-21</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <Text className="review-content">商品质量很好,物流很快,非常满意!</Text>
|
|
|
</View>
|
|
</View>
|
|
|
- </Card>
|
|
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 商品详情区域 */}
|
|
|
|
|
+ <View className="detail-section">
|
|
|
|
|
+ <Text className="detail-title">商品详情</Text>
|
|
|
|
|
+ {goods.detail ? (
|
|
|
|
|
+ <RichText
|
|
|
|
|
+ className="detail-content"
|
|
|
|
|
+ nodes={goods.detail
|
|
|
|
|
+ .replace(/<img/g, '<img style="max-width:100%;height:auto"')
|
|
|
|
|
+ .replace(/<p>/g, '<p style="margin:10px 0">')
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="no-detail">暂无商品详情</Text>
|
|
|
|
|
+ )}
|
|
|
</View>
|
|
</View>
|
|
|
</ScrollView>
|
|
</ScrollView>
|
|
|
|
|
|
|
|
{/* 底部操作栏 */}
|
|
{/* 底部操作栏 */}
|
|
|
- <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
|
|
|
|
|
- <View className="flex items-center justify-between">
|
|
|
|
|
- <View className="flex items-center">
|
|
|
|
|
- <Text className="text-gray-600 mr-2">数量:</Text>
|
|
|
|
|
- <View className="flex items-center border border-gray-300 rounded">
|
|
|
|
|
|
|
+ <View className="bottom-action-bar">
|
|
|
|
|
+ <View className="action-left">
|
|
|
|
|
+ <View className="quantity-selector">
|
|
|
|
|
+ <Text className="quantity-label">数量:</Text>
|
|
|
|
|
+ <View className="quantity-controls">
|
|
|
<Button
|
|
<Button
|
|
|
size="sm"
|
|
size="sm"
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
- className="px-3"
|
|
|
|
|
|
|
+ className="quantity-btn"
|
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
|
>
|
|
>
|
|
|
-
|
|
-
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Text className="px-4 py-1 border-x border-gray-300">{quantity}</Text>
|
|
|
|
|
|
|
+ <Text className="quantity-value">{quantity}</Text>
|
|
|
<Button
|
|
<Button
|
|
|
size="sm"
|
|
size="sm"
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
- className="px-3"
|
|
|
|
|
- onClick={() => setQuantity(Math.min(goods.stock, quantity + 1))}
|
|
|
|
|
|
|
+ className="quantity-btn"
|
|
|
|
|
+ onClick={() => setQuantity(Math.min(selectedSpec?.stock || goods.stock, quantity + 1))}
|
|
|
>
|
|
>
|
|
|
+
|
|
+
|
|
|
</Button>
|
|
</Button>
|
|
|
</View>
|
|
</View>
|
|
|
</View>
|
|
</View>
|
|
|
-
|
|
|
|
|
- <View className="flex gap-2">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- onClick={handleAddToCart}
|
|
|
|
|
- disabled={goods.stock <= 0}
|
|
|
|
|
- >
|
|
|
|
|
- 加入购物车
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleBuyNow}
|
|
|
|
|
- disabled={goods.stock <= 0}
|
|
|
|
|
- >
|
|
|
|
|
- 立即购买
|
|
|
|
|
- </Button>
|
|
|
|
|
- </View>
|
|
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View className="action-right">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="add-cart-btn"
|
|
|
|
|
+ onClick={handleAddToCart}
|
|
|
|
|
+ disabled={goods.stock <= 0}
|
|
|
|
|
+ >
|
|
|
|
|
+ 加入购物车
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="buy-now-btn"
|
|
|
|
|
+ onClick={handleBuyNow}
|
|
|
|
|
+ disabled={goods.stock <= 0}
|
|
|
|
|
+ >
|
|
|
|
|
+ 立即购买
|
|
|
|
|
+ </Button>
|
|
|
</View>
|
|
</View>
|
|
|
</View>
|
|
</View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 规格选择弹窗 */}
|
|
|
|
|
+ <GoodsSpecSelector
|
|
|
|
|
+ visible={showSpecModal}
|
|
|
|
|
+ onClose={() => setShowSpecModal(false)}
|
|
|
|
|
+ onConfirm={handleSpecSelect}
|
|
|
|
|
+ goodsId={goodsId}
|
|
|
|
|
+ currentSpec={selectedSpec?.name}
|
|
|
|
|
+ currentQuantity={quantity}
|
|
|
|
|
+ />
|
|
|
</View>
|
|
</View>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|