Files
template-10584/src/shop/goodsDetail/index.tsx
赵忠林 f808c22a22 refactor(api): 统一文章和导航查询接口命名
- 将getCmsArticleByCode重命名为getByCode
- 新增cmsNavigation的getByCode查询方法
- 统一返回数据处理逻辑

style(ui): 调整商品详情页底部按钮样式

- 减少底部按钮区域内边距
- 调整咨询按钮文字大小和内间距
- 缩小耳机图标尺寸

chore(app): 移除多余空行和调试信息

- 删除组件间无用空白行
- 将邀请提示改为控制台输出
- 保持导出语句格式一致
2025-10-11 11:52:24 +08:00

398 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {useEffect, useState} from "react";
import {Image, Badge, Popup, CellGroup, Cell} from "@nutui/nutui-react-taro";
import {ArrowLeft, Headphones, Share, Cart, ArrowRight} from "@nutui/icons-react-taro";
import Taro, {useShareAppMessage} from "@tarojs/taro";
import {RichText, View, Text} from '@tarojs/components'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec";
import {ShopGoodsSpec} from "@/api/shop/shopGoodsSpec/model";
import {listShopGoodsSku} from "@/api/shop/shopGoodsSku";
import {ShopGoodsSku} from "@/api/shop/shopGoodsSku/model";
import {Swiper} from '@nutui/nutui-react-taro'
import navTo, {wxParse} from "@/utils/common";
import SpecSelector from "@/components/SpecSelector";
import "./index.scss";
import {useCart} from "@/hooks/useCart";
import {useConfig} from "@/hooks/useConfig";
const GoodsDetail = () => {
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
const [windowWidth, setWindowWidth] = useState<number>(390)
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [files, setFiles] = useState<any[]>([]);
const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]);
const [skus, setSkus] = useState<ShopGoodsSku[]>([]);
const [showSpecSelector, setShowSpecSelector] = useState(false);
const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart');
const [showBottom, setShowBottom] = useState(false)
const [bottomItem, setBottomItem] = useState<any>({
title: '',
content: ''
})
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id;
// 使用购物车Hook
const {cartCount, addToCart} = useCart()
const {config} = useConfig()
// 处理加入购物车
const handleAddToCart = () => {
if (!goods) return;
if (!Taro.getStorageSync('UserId')) {
return Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
}
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('cart');
setShowSpecSelector(true);
return;
}
// 没有规格,直接加入购物车
addToCart({
goodsId: goods.goodsId!,
name: goods.name || '',
price: goods.price || '0',
image: goods.image || ''
});
};
// 处理立即购买
const handleBuyNow = () => {
if (!goods) return;
if (!Taro.getStorageSync('UserId')) {
return Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
}
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('buy');
setShowSpecSelector(true);
return;
}
// 没有规格,直接购买
navTo(`/shop/orderConfirm/index?goodsId=${goods?.goodsId}`, true);
};
// 规格选择确认回调
const handleSpecConfirm = (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
// setSelectedSku(sku);
setShowSpecSelector(false);
if (action === 'cart') {
// 加入购物车
addToCart({
goodsId: goods!.goodsId!,
skuId: sku.id,
name: goods!.name || '',
price: sku.price || goods!.price || '0',
image: goods!.image || '',
specInfo: sku.sku, // sku字段包含规格信息
}, quantity);
} else if (action === 'buy') {
// 立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
} else {
// 默认情况如果action未定义默认为立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
}
};
const openBottom = (title: string, content: string) => {
setBottomItem({
title,
content
})
setShowBottom(true)
}
useEffect(() => {
Taro.getSystemInfo({
success: (res) => {
setWindowWidth(res.windowWidth)
setStatusBarHeight(Number(res.statusBarHeight) + 5)
},
});
if (goodsId) {
setLoading(true);
// 加载商品详情
getShopGoods(Number(goodsId))
.then((res) => {
// 处理富文本内容,去掉图片间距
if (res.content) {
res.content = wxParse(res.content);
}
setGoods(res);
if (res.files) {
const arr = JSON.parse(res.files);
arr.length > 0 && setFiles(arr);
}
})
.catch((error) => {
console.error("Failed to fetch goods detail:", error);
})
.finally(() => {
setLoading(false);
});
// 加载商品规格
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
.then((data) => {
setSpecs(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods specs:", error);
});
// 加载商品SKU
listShopGoodsSku({goodsId: Number(goodsId)} as any)
.then((data) => {
setSkus(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods skus:", error);
});
}
}, [goodsId]);
// 分享给好友
useShareAppMessage(() => {
return {
title: goods?.name || '精选商品',
path: `/shop/goodsDetail/index?id=${goodsId}`,
imageUrl: goods?.image, // 分享图片
success: function (res: any) {
console.log('分享成功', res);
Taro.showToast({
title: '分享成功',
icon: 'success',
duration: 2000
});
},
fail: function (res: any) {
console.log('分享失败', res);
Taro.showToast({
title: '分享失败',
icon: 'none',
duration: 2000
});
}
};
});
if (!goods || loading) {
return <View>...</View>;
}
return (
<View className={"py-0"}>
<View
className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: statusBarHeight + 'px',
left: "10px",
}}
onClick={() => Taro.navigateBack()}
>
<ArrowLeft size={14}/>
</View>
<View className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: statusBarHeight + 'px',
right: "110px",
}}
onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}>
<Badge value={cartCount} top="-2" right="2">
<View style={{display: 'flex', alignItems: 'center'}}>
<Cart size={16}/>
</View>
</Badge>
</View>
{
files.length > 0 && (
<Swiper defaultValue={0} indicator height={windowWidth}>
{files.map((item) => (
<Swiper.Item key={item}>
<Image width={windowWidth} height={windowWidth} src={item.url} mode={'scaleToFill'} lazyLoad={false}/>
</Swiper.Item>
))}
</Swiper>
)
}
{
files.length == 0 && (
<Image
src={goods.image}
mode={"scaleToFill"}
radius="10px 10px 0 0"
height="300"
/>
)
}
<View
className={"flex flex-col justify-between items-center"}
>
<View
className={
"flex flex-col bg-white w-full"
}
>
<View className={"flex flex-col p-3 rounded-lg"}>
<>
<View className={'flex justify-between'}>
<View className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{goods.price}</span>
</View>
<span className={"text-gray-400 text-xs"}> {goods.sales}</span>
</View>
<View className={'flex justify-between items-center'}>
<View className={'goods-info'}>
<View className={"car-no text-lg"}>
{goods.name}
</View>
<View className={"flex justify-between text-xs py-1"}>
<span className={"text-orange-500"}>
{goods.comments}
</span>
</View>
</View>
<View>
<button
className={'flex flex-col justify-center items-center bg-white text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'}
open-type="share">
<Share size={20}/>
<span className={'text-xs'}></span>
</button>
</View>
</View>
</>
</View>
</View>
<View className={'w-full'}>
{
config?.deliveryText && (
<CellGroup>
<Cell title={'配送'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.deliveryText || '14:30下单明天配送'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('配送', `${config?.deliveryText}`)}/>
<Cell title={'保障'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.guaranteeText || '支持7天无理由退货'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('保障', `${config?.guaranteeText}`)}/>
</CellGroup>
)
}
{config?.openComments == '1' && (
<CellGroup>
<Cell title={'用户评价 (0)'} extra={
<>
<Text></Text>
<ArrowRight color="#cccccc" size={15}/>
</>
} onClick={() => navTo(`/shop/comments/index`)}/>
<Cell className={'flex h-32 bg-white p-4'}>
</Cell>
</CellGroup>
)}
</View>
<View className={'w-full'}>
<RichText nodes={goods.content || '内容详情'}/>
<View className={'h-24'}></View>
</View>
</View>
{/*底部弹窗*/}
<Popup
visible={showBottom}
position="bottom"
onClose={() => {
setShowBottom(false)
}}
lockScroll
>
<View className={'flex flex-col p-4'}>
<Text className={'font-bold text-sm'}>{bottomItem.title}</Text>
<Text className={'text-gray-500 my-2'}>{bottomItem.content}</Text>
</View>
</Popup>
{/*底部购买按钮*/}
<View className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-6'}>
<View className={'btn-bar flex justify-between items-center'}>
<View className={'flex justify-center items-center mx-4'}>
<button open-type="contact" className={'flex items-center text-sm py-2'}>
<Headphones size={16} style={{marginRight: '4px'}}/>
</button>
</View>
<View className={'buy-btn mx-4'}>
<View className={'cart-add px-4 text-sm'}
onClick={() => handleAddToCart()}>
</View>
<View className={'cart-buy pl-4 pr-5 text-sm'}
onClick={() => handleBuyNow()}>
</View>
</View>
</View>
</View>
{/* 规格选择器 */}
{showSpecSelector && (
<SpecSelector
goods={goods!}
specs={specs}
skus={skus}
action={specAction}
onConfirm={handleSpecConfirm}
onClose={() => setShowSpecSelector(false)}
/>
)}
</View>
);
};
export default GoodsDetail;