Files
dukang/apps/mini-user/src/pages/product-detail/index.tsx
T
jacy 89fd333702 feat(analytics): persona logging upgrade and Sentry system config
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>
2026-08-03 22:30:02 +08:00

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>
);
}