89fd333702
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload. Co-authored-by: Cursor <cursoragent@cursor.com>
273 lines
9.3 KiB
TypeScript
273 lines
9.3 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { View, Text, Image } from '@tarojs/components';
|
|
import Taro, {
|
|
useDidShow,
|
|
usePageScroll,
|
|
useRouter,
|
|
useShareAppMessage,
|
|
useShareTimeline,
|
|
} from '@tarojs/taro';
|
|
import type { ProductDetailContentDto } from '@dukang/shared-types';
|
|
import PageShell from '../../components/PageShell';
|
|
import PageNavBar from '../../components/PageNavBar';
|
|
import ProductCarousel from '../../components/ProductCarousel';
|
|
import ShareNavButton from '../../components/ShareNavButton';
|
|
import WechatShareReady from '../../components/WechatShareReady';
|
|
import { goLogin } from '../../lib/auth-nav';
|
|
import { ensurePayReady } from '../../lib/pay-ready';
|
|
import { isLoggedIn, request, toast } from '../../lib/api';
|
|
import {
|
|
getProductCarouselImages,
|
|
getProductDetailImages,
|
|
getProductMainImage,
|
|
type ProductImageSource,
|
|
} from '../../lib/product-images';
|
|
import {
|
|
canBuyOnline,
|
|
canPickupOnSite,
|
|
normalizeFulfillmentFlags,
|
|
} from '../../lib/product-fulfillment';
|
|
import {
|
|
DEFAULT_SHARE_DESC,
|
|
DEFAULT_SHARE_TITLE,
|
|
toWeappShareMessage,
|
|
} from '../../lib/wechat-share';
|
|
import iconHome from '../../assets/tabbar/home.png';
|
|
import { usePageView } from '../../lib/usePageView';
|
|
|
|
type Product = ProductImageSource & {
|
|
id: string;
|
|
name: string;
|
|
subtitle?: string | null;
|
|
price: number;
|
|
benefitAmount?: number;
|
|
benefitDisplay?: number;
|
|
detailContent?: ProductDetailContentDto | null;
|
|
allowOnSitePickup?: boolean;
|
|
allowOnlinePurchase?: boolean;
|
|
allowCrossCityDelivery?: boolean;
|
|
};
|
|
|
|
export default function ProductDetailPage() {
|
|
const router = useRouter();
|
|
const productId = router.params.id ?? '';
|
|
usePageView(
|
|
'product_detail_view',
|
|
productId ? { refType: 'PRODUCT', refId: productId, productId } : undefined,
|
|
);
|
|
const [product, setProduct] = useState<Product | null>(null);
|
|
const [headerSolid, setHeaderSolid] = useState(false);
|
|
|
|
usePageScroll(({ scrollTop }) => {
|
|
setHeaderSolid(scrollTop > 100);
|
|
});
|
|
|
|
const loadProduct = useCallback(() => {
|
|
if (!productId) return;
|
|
request<Product | null>(`/catalog/products/${productId}`)
|
|
.then((p) => {
|
|
if (!p) {
|
|
setProduct(null);
|
|
toast('商品不存在或暂未开放');
|
|
return;
|
|
}
|
|
setProduct(normalizeFulfillmentFlags(p));
|
|
})
|
|
.catch((e) => {
|
|
setProduct(null);
|
|
toast(e instanceof Error ? e.message : '加载失败');
|
|
});
|
|
}, [productId]);
|
|
|
|
useEffect(() => {
|
|
loadProduct();
|
|
}, [loadProduct]);
|
|
|
|
// 登录后返回详情须带 token 重拉,否则白名单商品会一直空白
|
|
useDidShow(() => {
|
|
loadProduct();
|
|
});
|
|
|
|
const sharePayload = useMemo(
|
|
() => ({
|
|
title: product?.name || DEFAULT_SHARE_TITLE,
|
|
desc: product?.subtitle || DEFAULT_SHARE_DESC,
|
|
path: `/pages/product-detail/index?id=${productId}`,
|
|
imgUrl: (product ? getProductMainImage(product) : '') || undefined,
|
|
}),
|
|
[product, productId],
|
|
);
|
|
|
|
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
|
useShareTimeline(() => ({
|
|
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
|
query: productId ? `id=${productId}` : '',
|
|
imageUrl: sharePayload.imgUrl,
|
|
}));
|
|
|
|
function goBack() {
|
|
const pages = Taro.getCurrentPages();
|
|
if (pages.length > 1) {
|
|
Taro.navigateBack();
|
|
return;
|
|
}
|
|
Taro.switchTab({ url: '/pages/home/index' });
|
|
}
|
|
|
|
function goHome() {
|
|
Taro.switchTab({ url: '/pages/home/index' });
|
|
}
|
|
|
|
async function goBuy() {
|
|
if (!productId) return;
|
|
const returnPath = `/pages/order-confirm/index?productId=${productId}&qty=2`;
|
|
if (!isLoggedIn()) {
|
|
goLogin(returnPath);
|
|
return;
|
|
}
|
|
const ready = await ensurePayReady(returnPath);
|
|
if (!ready) return;
|
|
Taro.navigateTo({ url: returnPath });
|
|
}
|
|
|
|
async function goOnSitePickup() {
|
|
if (!productId) return;
|
|
const returnPath = `/pages/order-confirm-pickup/index?productId=${productId}&qty=1`;
|
|
if (!isLoggedIn()) {
|
|
goLogin(returnPath);
|
|
return;
|
|
}
|
|
const ready = await ensurePayReady(returnPath);
|
|
if (!ready) return;
|
|
Taro.navigateTo({ url: returnPath });
|
|
}
|
|
|
|
if (!product) {
|
|
return (
|
|
<PageShell variant="scroll" className="product-detail-page">
|
|
<PageNavBar title="商品详情" solid onBack={goBack} />
|
|
<View className="page-with-nav-bar u-empty">加载中…</View>
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
const allowOnline = canBuyOnline(product);
|
|
const allowOnSite = canPickupOnSite(product);
|
|
const benefit = Number(product.benefitDisplay ?? product.benefitAmount ?? product.price);
|
|
const carouselImages = getProductCarouselImages(product);
|
|
const detailImages = getProductDetailImages(product);
|
|
const detail = product.detailContent ?? {};
|
|
const features = detail.features ?? [];
|
|
|
|
return (
|
|
<PageShell variant="scroll" className="product-detail-page" hasFixedFooter>
|
|
<WechatShareReady payload={sharePayload} />
|
|
<PageNavBar
|
|
title={product.name}
|
|
solid={headerSolid}
|
|
titleVisible={headerSolid}
|
|
onBack={goBack}
|
|
right={<ShareNavButton payload={sharePayload} />}
|
|
/>
|
|
|
|
<View className="product-detail-main">
|
|
<View className="product-detail-hero full-bleed">
|
|
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
|
|
</View>
|
|
|
|
<View className="product-detail-info">
|
|
<View className="product-detail-price">
|
|
<Text className="product-detail-price-symbol">¥</Text>
|
|
<Text className="product-detail-price-value">{Number(product.price).toFixed(2)}</Text>
|
|
</View>
|
|
<Text className="product-detail-name">{product.name}</Text>
|
|
{product.subtitle ? (
|
|
<Text className="product-detail-subtitle">{product.subtitle}</Text>
|
|
) : null}
|
|
|
|
<View className="product-detail-promo">
|
|
<View className="product-detail-promo-glow" />
|
|
<View className="product-detail-promo-head">
|
|
<View className="product-detail-promo-icon">
|
|
<Text className="product-detail-promo-icon-text">惠</Text>
|
|
</View>
|
|
<Text className="product-detail-promo-title">
|
|
买杜康美酒 · 享全城好客礼遇
|
|
<Text className="product-detail-promo-amount"> ¥{benefit}</Text>
|
|
</Text>
|
|
</View>
|
|
<Text className="product-detail-promo-desc">
|
|
购酒即享本城专属好客权益,为您安排一场地道饭局。权益金可直接用于本地签约门店消费,到店享用,醇香美酒配佳肴。
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className="product-detail-content">
|
|
<View className="product-detail-section-head">
|
|
<View className="product-detail-section-bar" />
|
|
<Text className="product-detail-section-title">商品详情</Text>
|
|
</View>
|
|
|
|
{detailImages.map((src, index) => (
|
|
<Image
|
|
key={`${src}-${index}`}
|
|
className="product-detail-banner full-bleed"
|
|
src={src}
|
|
mode="widthFix"
|
|
/>
|
|
))}
|
|
|
|
{(detail.storyTitle || detail.storyText || features.length > 0) ? (
|
|
<View className="product-detail-copy">
|
|
{(detail.storyTitle || detail.storyText) ? (
|
|
<View className="product-detail-story">
|
|
{detail.storyTitle ? (
|
|
<Text className="product-detail-story-title">{detail.storyTitle}</Text>
|
|
) : null}
|
|
{detail.storyText ? (
|
|
<Text className="product-detail-story-text">{detail.storyText}</Text>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
|
|
{features.length > 0 ? (
|
|
<View className="product-detail-features">
|
|
{features.map((f) => (
|
|
<View key={`${f.title}-${f.icon}`} className="product-detail-feature">
|
|
<Text className="product-detail-feature-icon">★</Text>
|
|
<Text className="product-detail-feature-title">{f.title}</Text>
|
|
<Text className="product-detail-feature-desc">{f.desc}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
|
|
<View className="product-detail-bar">
|
|
<View className="product-detail-bar-home" onClick={goHome}>
|
|
<Image className="product-detail-bar-home-icon" src={iconHome} mode="aspectFit" />
|
|
<Text className="product-detail-bar-home-label">首页</Text>
|
|
</View>
|
|
{allowOnSite ? (
|
|
<View className="product-detail-pickup-btn" onClick={() => void goOnSitePickup()}>
|
|
<Text className="product-detail-pickup-btn-text">现场取货</Text>
|
|
</View>
|
|
) : null}
|
|
{allowOnline ? (
|
|
<View className="product-detail-buy-btn" onClick={() => void goBuy()}>
|
|
<Text className="product-detail-buy-btn-text">立即购买</Text>
|
|
</View>
|
|
) : null}
|
|
{!allowOnline && !allowOnSite ? (
|
|
<View className="product-detail-buy-btn product-detail-buy-btn--disabled">
|
|
<Text className="product-detail-buy-btn-text">暂不可购</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
</PageShell>
|
|
);
|
|
}
|