import React, { useState, useEffect } from 'react' import { View, Text, Image, ScrollView, Swiper, SwiperItem, RichText } from '@tarojs/components' import { Tag } from '@nutui/nutui-react-taro' import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro' import { getShopGoods } from '@/api/shop/shopGoods' import { addShopGoodsFavorite, removeShopGoodsFavorite, getShopGoodsFavoriteStatus } from '@/api/shop/shopGoodsFavorite' import type { ShopGoods, ShopGoodsSku } from '@/api/shop/shopGoods/model' import Price from '@/components/common/Price' import SkuSelector from '@/components/business/SkuSelector' import { useCartContext } from '@/contexts/CartContext' import { useUserContext } from '@/contexts/UserContext' import { useScrollHeight } from '@/hooks/useScrollHeight' import { isGuest } from '@/utils/auth' import { requireLogin } from '@/utils/login-guard' definePageConfig({ navigationBarTitleText: '商品详情', }) const ProductDetailPage: React.FC = () => { const router = useRouter() const id = Number(router.params.id || router.params.goodsId) const [product, setProduct] = useState(null) const [swiperHeight, setSwiperHeight] = useState(375) const [skuVisible, setSkuVisible] = useState(false) const [skuMode, setSkuMode] = useState<'cart' | 'buy'>('cart') const [isFavorite, setIsFavorite] = useState(false) const { addItem } = useCartContext() const { isLoggedIn } = useUserContext() // 底部操作栏高度约 90px(含按钮 + 安全区),动态计算 ScrollView 可用高度 const scrollHeight = useScrollHeight(44) // 微信分享配置 useShareAppMessage(() => { return { title: product?.name || product?.goodsName || '优质商品推荐', path: `/pages/shop/product-detail?id=${id}`, imageUrl: product?.image || '' } }) useEffect(() => { if (id) { loadProduct() if (isLoggedIn) { checkFavoriteStatus() } } }, [id, isLoggedIn]) // 监听 product 变化后再添加到历史记录 useEffect(() => { if (product) { addToHistory() } }, [product]) const loadProduct = async () => { try { const data = await getShopGoods(id) setProduct(data) } catch { /* ignore */ } } const checkFavoriteStatus = async () => { try { const status = await getShopGoodsFavoriteStatus({ goodsId: id }) setIsFavorite(status) } catch { /* ignore */ } } const toggleFavorite = async () => { if (!requireLogin({ action: 'favorite', redirect: `/pages/shop/product-detail?id=${id}` })) return try { if (isFavorite) { await removeShopGoodsFavorite({ goodsId: id }) setIsFavorite(false) Taro.showToast({ title: '已取消收藏', icon: 'success' }) } else { await addShopGoodsFavorite({ goodsId: id }) setIsFavorite(true) Taro.showToast({ title: '收藏成功', icon: 'success' }) } } catch (err: any) { // 接口报错时仍更新UI状态,避免状态不一致 console.error('[ProductDetail] 收藏操作失败:', err?.message || err) if (isFavorite) { setIsFavorite(false) } else { setIsFavorite(true) } // 不再额外弹toast(handleError已经弹了后端返回的错误信息) } } const addToHistory = () => { try { const history = Taro.getStorageSync('browse_history') || [] const newItem = { goodsId: id, name: product?.name || product?.goodsName, image: product?.image, price: product?.price, timestamp: Date.now() } // 去重 + LRU 淘汰(最多50条,单条约200B,总计 < 10MB 安全范围) const MAX_HISTORY = 50 const filtered = history.filter((item: { goodsId: number }) => item.goodsId !== id) filtered.unshift(newItem) const limited = filtered.slice(0, MAX_HISTORY) // 安全写入(捕获存储溢出) try { Taro.setStorageSync('browse_history', limited) } catch (storageErr) { // 存储空间不足时,缩减到一半再试 const reduced = limited.slice(0, Math.floor(MAX_HISTORY / 2)) Taro.setStorageSync('browse_history', reduced) } } catch { /* ignore */ } } const handleAddCart = () => { if (!requireLogin({ action: 'addToCart', redirect: `/pages/shop/product-detail?id=${id}` })) return setSkuMode('cart') setSkuVisible(true) } const handleBuyNow = () => { if (!requireLogin({ action: 'buyNow', redirect: `/pages/shop/product-detail?id=${id}` })) return setSkuMode('buy') setSkuVisible(true) } const handleSkuConfirm = async (sku: ShopGoodsSku | null, quantity: number) => { if (!product) { Taro.showToast({ title: '商品信息异常', icon: 'none' }) return } if (skuMode === 'cart') { try { await addItem(product, sku, quantity) } catch (err) { console.error('[ProductDetail] 加入购物车失败:', err) } } else { const buyNowData = [{ goodsId: product.goodsId!, skuId: sku?.id || 0, quantity: quantity, num: quantity, product: product, sku: sku, checked: true, }] Taro.setStorageSync('buy_now', JSON.stringify(buyNowData)) Taro.navigateTo({ url: '/pages/shop/checkout?from=buyNow' }) } } const handleGoCart = () => { // 购物车不是 tabBar 页面,使用 navigateTo;路径与 app.config.ts 中注册的 'pages/shop/cart' 对应 Taro.navigateTo({ url: '/pages/shop/cart' }) } const handleContactService = () => { // 直接跳转到在线客服页面(已实现完整功能:微信客服按钮 + 历史消息 + 热线 + 微信留言) Taro.navigateTo({ url: '/pages/user/customer-service/index' }) } if (!product) { return ( 加载中... ) } let images: string[] = [] try { if (product.files) { const parsed = JSON.parse(product.files) if (Array.isArray(parsed)) { images = parsed.map((f: any) => f.url || f).filter(Boolean) } } } catch { /* ignore */ } if (images.length === 0 && product.image) { images = [product.image] } // 解析服务保障标签 const ensureTags = product.ensureTag ? product.ensureTag.split(/[,,、]/).filter(Boolean) : [] // 配送方式文案 const deliveryText = product.deliveryMode === 1 ? '限自提' : '送上门' /** * 将商品详情内容中的 Markdown 图片语法转换为 RichText 可识别的 HTML * 支持格式:![alt](url) 和 ![alt](url?params) */ const parseContent = (content: string): string => { if (!content) return '' let html = content // 将 ![...](url) 转换为 html = html.replace( /!\[([^\]]*)\]\(([^)]+)\)/g, '' ) // 兜底:如果内容里直接包含 http(s) 图片链接(非 img 标签包裹的),也尝试转成图片 // 匹配独立的 https://xxx.jpg/png/gif 链接 if (html.includes('http') && !html.includes('' ) } return html } return ( {/* 图片轮播 */} {images.map((img, idx) => ( { // 根据第一张图片的实际宽高比动态设置 Swiper 高度 if (idx === 0) { const { width, height } = e.detail if (width > 0) { const screenWidth = Taro.getSystemInfoSync().windowWidth setSwiperHeight(Math.round((height / width) * screenWidth)) } } }} onClick={() => Taro.previewImage({ current: img, urls: images })} /> ))} {images.length === 0 && ( 暂无图片 )} {/* 价格区域 */} 到手价 {!isGuest() && product.salePrice && product.salePrice !== product.price && ( ¥{product.salePrice} )} {/* 会员价 */} {!isGuest() && product.memberStorePrice && product.memberStorePrice !== product.price && ( 会员价: ¥{product.memberStorePrice} )} {/* 赚取积分 */} {product.gainIntegral && Number(product.gainIntegral) > 0 && ( 购买可得 {product.gainIntegral} 积分 )} 销量: {product.sales || 0} 库存: {product.stock || 0} {product.unitName && ( 单位: {product.unitName} )} {/* 商品名称 */} {product.name || product.goodsName} {/* 分类面包屑 */} {(product.categoryParent || product.categoryName) && ( {product.categoryParent ? `${product.categoryParent}` : ''} {product.categoryParent && product.categoryName ? ' > ' : ''} {product.categoryName ? `${product.categoryName}` : ''} )} {/* SKU 已选提示(多规格商品) */} {product.specs === 1 && ( { setSkuMode('cart') setSkuVisible(true) }} > 已选 {product.specName || '请选择规格'} )} {/* 配送信息 */} 配送 {deliveryText} {product.goodsWeight && Number(product.goodsWeight) > 0 && ( 重量: {product.goodsWeight}kg )} {/* 服务保障 */} {ensureTags.length > 0 && ( 服务 {ensureTags.map((tag, idx) => ( {tag} {idx < ensureTags.length - 1 && ( | )} ))} )} {/* 商品详情 */} 商品详情 {product.content ? ( ) : ( 暂无详情 )} {/* 底部留白(与固定栏同高 + 安全区,确保内容不被遮挡) */} {/* 底部操作栏 - 固定吸底 */} {/* 图标入口 */} 🛒 购物车 💬 客服 {isFavorite ? '❤️' : '🤍'} 收藏 {/* 操作按钮 */} 加入购物车 立即购买 {/* SKU 选择器 */} setSkuVisible(false)} onConfirm={handleSkuConfirm} /> ) } export default ProductDetailPage