import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { View, Text, Image, ScrollView } from '@tarojs/components'; import '../../styles/store-detail.css'; import Taro, { useDidShow, useLoad, usePageScroll, useRouter, useShareAppMessage, useShareTimeline, } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import PageNavBar from '../../components/PageNavBar'; import ProductCarousel from '../../components/ProductCarousel'; import ShareNavButton from '../../components/ShareNavButton'; import StoreRedeemMarquee from '../../components/StoreRedeemMarquee'; import WechatShareReady from '../../components/WechatShareReady'; import { request, toast } from '../../lib/api'; import { toMoneyNumber } from '../../lib/money'; import { maskPhone, toDialablePhone } from '../../lib/phone'; import { track } from '../../lib/analytics'; import { formatShanghaiDateTime } from '../../lib/datetime'; import { buildSceneSharePayload, toWeappShareMessage, toWeappShareTimeline, } from '../../lib/wechat-share'; type StoreMedia = { url?: string | null; bizType?: string | null; mediaType?: string | null; }; type StorePackage = { name: string; price: string | number; dishes: string; usableTime?: string | null; otherNotes?: string | null; imageUrl?: string | null; sortOrder?: number; }; type Store = { id: string; name: string; address?: string; province?: string; cityName?: string; city?: string; district?: string; phone?: string; intro?: string | null; benefitUsageRule?: string | null; coverUrl?: string | null; carouselUrls?: string[] | null; media?: StoreMedia[] | null; packages?: StorePackage[] | null; openTime?: string | null; closeTime?: string | null; openTime2?: string | null; closeTime2?: string | null; avgPrice?: number | null; latitude?: number | string | null; longitude?: number | string | null; category?: { name: string } | null; }; type RecentRedeem = { userLabel: string; amount: number | string; createdAt: string; text?: string; }; function uniqueUrls(urls: Array) { const seen = new Set(); const out: string[] = []; for (const raw of urls) { const url = String(raw || '').trim(); if (!url || seen.has(url)) continue; seen.add(url); out.push(url); } return out; } function envPhotoUrls(store: Store) { return uniqueUrls( (store.media || []) .filter((m) => !m.bizType || m.bizType === 'ENV') .map((m) => m.url), ); } function fullAddress(store: Store) { const city = store.cityName || store.city || ''; return `${store.province || ''}${city}${store.district || ''}${store.address || ''}`.trim(); } function pickStoreId(raw?: string | null) { return String(raw || '') .trim() .replace(/[^\d]/g, ''); } /** 与历史记录一致:2026-08-03 15:14:30(Asia/Shanghai) */ function formatRedeemTime(input?: string | null) { return formatShanghaiDateTime(input); } function formatPackagePriceYuan(price: string | number) { const n = typeof price === 'number' ? price : Number(price); if (!Number.isFinite(n)) return '0'; if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n)); return n.toFixed(2).replace(/\.?0+$/, ''); } function formatRedeemAmountYuan(amount: unknown) { const n = toMoneyNumber(amount); if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n)); return n.toFixed(2).replace(/\.?0+$/, ''); } function formatRecentRedeemLine(row: RecentRedeem) { try { // 优先用服务端拼好的 text,避免客户端时区 / Intl 差异 if (row.text?.trim()) return row.text.trim(); const label = String(row.userLabel || '用户***').trim() || '用户***'; const rawTime = String(row.createdAt || '').trim(); const time = /^\d{4}-\d{2}-\d{2}/.test(rawTime) ? rawTime.slice(0, 19).replace('T', ' ') : formatRedeemTime(row.createdAt); const amount = formatRedeemAmountYuan(row.amount); return `${label} ${time} 核销${amount}元`; } catch { return ''; } } function normalizeRecentRedeems(payload: unknown): RecentRedeem[] { try { if (Array.isArray(payload)) return payload as RecentRedeem[]; if (payload && typeof payload === 'object') { const list = (payload as { list?: unknown; items?: unknown; data?: unknown }).list ?? (payload as { items?: unknown }).items ?? (payload as { data?: unknown }).data; if (Array.isArray(list)) return list as RecentRedeem[]; } } catch { /* ignore */ } return []; } export default function StoreDetailPage() { const router = useRouter(); const [storeId, setStoreId] = useState(() => pickStoreId(router.params.id)); const [store, setStore] = useState(null); const [recentRedeems, setRecentRedeems] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [headerSolid, setHeaderSolid] = useState(false); const storeRef = useRef(null); storeRef.current = store; usePageScroll(({ scrollTop }) => { setHeaderSolid(scrollTop > 100); }); const loadStore = useCallback(async (id: string) => { if (!id) { setLoading(false); setLoadError('缺少门店参数'); return; } setLoadError(''); if (!storeRef.current) setLoading(true); try { const data = await request(`/stores/${id}`); if (!data || !data.id) { setStore(null); setLoadError('门店不存在或暂不可见'); toast('门店不存在或暂不可见'); return; } setStore(data); } catch (e) { const msg = e instanceof Error ? e.message : '加载失败'; setLoadError(msg); if (!storeRef.current) toast(msg); } finally { setLoading(false); } }, []); const loadRecentRedeems = useCallback(async (id: string) => { if (!id) { setRecentRedeems([]); return; } try { const list = await request(`/stores/${id}/recent-redeems?limit=20`); setRecentRedeems(normalizeRecentRedeems(list)); } catch { setRecentRedeems([]); } }, []); const bootstrap = useCallback( (id: string) => { const nextId = pickStoreId(id); if (!nextId) { setLoading(false); setLoadError('缺少门店参数'); return; } setStoreId(nextId); void loadStore(nextId); void loadRecentRedeems(nextId); }, [loadStore, loadRecentRedeems], ); // 首屏:useLoad 带 options.id,比仅用 useDidShow 更稳(H5/小程序都覆盖) useLoad((options) => { bootstrap(options?.id || router.params.id || ''); }); useEffect(() => { const fromRouter = pickStoreId(router.params.id); if (fromRouter && fromRouter !== storeId) { bootstrap(fromRouter); } }, [router.params.id, storeId, bootstrap]); // 登录态变化后回到本页:重拉详情与走马灯 useDidShow(() => { const id = pickStoreId(storeId || router.params.id); if (!id) return; void loadStore(id); void loadRecentRedeems(id); }); const sharePayload = useMemo(() => { const envFirst = store ? envPhotoUrls(store)[0] : undefined; return buildSceneSharePayload('storeDetail', { path: `/pages/store-detail/index?id=${storeId}`, dynamicTitle: store?.name, dynamicDesc: store?.intro?.trim() || store?.address, dynamicImageUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0], }); }, [store, storeId]); const marqueeLines = useMemo( () => recentRedeems.map(formatRecentRedeemLine).filter(Boolean), [recentRedeems], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); useShareTimeline(() => toWeappShareTimeline(sharePayload, storeId ? `id=${storeId}` : ''), ); function goBack() { const pages = Taro.getCurrentPages(); if (pages.length > 1) Taro.navigateBack(); else Taro.switchTab({ url: '/pages/stores/index' }); } function callStore() { if (!store?.phone) { toast('暂无门店电话'); return; } track('store_phone_call', { storeId: store.id }); Taro.makePhoneCall({ phoneNumber: toDialablePhone(store.phone) }).catch(() => toast('无法拨打电话')); } function openMap() { if (!store) return; const lat = store.latitude != null ? Number(store.latitude) : NaN; const lng = store.longitude != null ? Number(store.longitude) : NaN; const address = fullAddress(store) || store.address || store.name; if (Number.isFinite(lat) && Number.isFinite(lng)) { Taro.openLocation({ latitude: lat, longitude: lng, name: store.name, address, scale: 16, }).catch(() => { if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') { window.location.href = `https://uri.amap.com/marker?position=${lng},${lat}&name=${encodeURIComponent(store.name)}&address=${encodeURIComponent(address)}`; return; } toast('无法打开地图导航'); }); return; } if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined' && address) { window.location.href = `https://uri.amap.com/search?keyword=${encodeURIComponent(address)}&src=dukang`; return; } toast('门店位置待完善,暂无法导航'); } if (!store) { return ( {loading ? '加载中…' : loadError || '门店不存在或暂不可见'} ); } const envPhotos = envPhotoUrls(store); const heroImages = uniqueUrls([store.coverUrl, ...(store.carouselUrls || [])]); /** 预览相册:封面 + 环境图(去重),页面展示仍分开 */ const previewAlbum = uniqueUrls([store.coverUrl, ...envPhotos]); const packages = store.packages ?? []; const intro = store.intro?.trim() || ''; const benefitRuleRaw = store.benefitUsageRule?.trim() || ''; const benefitRule = benefitRuleRaw && !/^null$/i.test(benefitRuleRaw) ? benefitRuleRaw : ''; function openPackageDetail(index: number) { Taro.navigateTo({ url: `/pages/store-package-detail/index?storeId=${storeId}&index=${index}`, }); } function previewEnv(index: number) { const current = envPhotos[index]; if (!current) return; const urls = previewAlbum.length ? previewAlbum : envPhotos; Taro.previewImage({ current, urls, }).catch(() => toast('无法预览图片')); } return ( } /> {store.name} {store.district ? `${store.district} · ` : ''} {store.address || '地址待完善'} 导航 营业时间:{' '} {(() => { const parts: string[] = []; if (store.openTime && store.closeTime) parts.push(`${store.openTime}-${store.closeTime}`); if (store.openTime2 && store.closeTime2) parts.push(`${store.openTime2}-${store.closeTime2}`); return parts.length ? parts.join(',') : '10:00-22:00'; })()} {store.avgPrice != null && Number(store.avgPrice) > 0 ? ( 人均约 ¥{Number(store.avgPrice).toFixed(0)} ) : null} {store.phone ? ( 电话: {maskPhone(store.phone)} 拨打 ) : null} {store.category?.name ? ( {store.category.name} ) : null} 可核销 好客门店 {marqueeLines.length > 0 ? ( ) : null} {packages.length > 0 ? ( 门店套餐 {packages.map((pkg, index) => ( openPackageDetail(index)} > {pkg.name}     ¥{formatPackagePriceYuan(pkg.price)} ))} ) : null} {benefitRule ? ( 使用规则 {benefitRule} ) : null} {intro ? ( 门店详情 {intro} ) : null} {envPhotos.length > 0 ? ( 店内环境 {envPhotos.map((url, index) => ( previewEnv(index)} > ))} ) : null} Taro.navigateTo({ url: '/pages/redeem/index' })} > 到店核销 ); }