forked from gxwebsoft/mp-10550
- 将getCmsArticleByCode重命名为getByCode - 新增cmsNavigation的getByCode查询方法 - 统一返回数据处理逻辑 style(ui): 调整商品详情页底部按钮样式 - 减少底部按钮区域内边距 - 调整咨询按钮文字大小和内间距 - 缩小耳机图标尺寸 chore(app): 移除多余空行和调试信息 - 删除组件间无用空白行 - 将邀请提示改为控制台输出 - 保持导出语句格式一致
398 lines
13 KiB
TypeScript
398 lines
13 KiB
TypeScript
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;
|