import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { View, Text, Image } from '@tarojs/components'; 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 WechatShareReady from '../../components/WechatShareReady'; import { request, toast } from '../../lib/api'; import { DEFAULT_SHARE_DESC, DEFAULT_SHARE_TITLE, toWeappShareMessage, } from '../../lib/wechat-share'; type StoreMedia = { url?: string | null; bizType?: string | null; mediaType?: string | null; }; 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; 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; }; 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) { const d = input ? new Date(input) : new Date(); if (Number.isNaN(d.getTime())) { // 后端若已是 "2026-08-03 15:14:30" 直接展示 const s = String(input || '').trim(); if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 19).replace('T', ' '); return '—'; } const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }).formatToParts(d); const get = (type: Intl.DateTimeFormatPartTypes) => parts.find((p) => p.type === type)?.value ?? ''; return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}:${get('second')}`; } function formatRedeemAmountYuan(amount: number | string) { const n = typeof amount === 'number' ? amount : Number(amount); 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 formatRecentRedeemLine(row: RecentRedeem) { return `${row.userLabel || '用户***'} ${formatRedeemTime(row.createdAt)} 核销${formatRedeemAmountYuan(row.amount)}元`; } function normalizeRecentRedeems(payload: unknown): RecentRedeem[] { if (Array.isArray(payload)) return payload as RecentRedeem[]; if (payload && typeof payload === 'object') { const list = (payload as { list?: unknown; items?: unknown }).list ?? (payload as { items?: unknown }).items; if (Array.isArray(list)) return list as RecentRedeem[]; } 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 { title: store?.name || DEFAULT_SHARE_TITLE, desc: store?.intro?.trim() || store?.address || DEFAULT_SHARE_DESC, path: `/pages/store-detail/index?id=${storeId}`, imgUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0] || undefined, }; }, [store, storeId], ); const marqueeText = useMemo(() => { if (!recentRedeems.length) return ''; return recentRedeems.map(formatRecentRedeemLine).join('  '); }, [recentRedeems]); useShareAppMessage(() => toWeappShareMessage(sharePayload)); useShareTimeline(() => ({ title: sharePayload.title || DEFAULT_SHARE_TITLE, query: storeId ? `id=${storeId}` : '', imageUrl: sharePayload.imgUrl, })); 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; } Taro.makePhoneCall({ phoneNumber: 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 images = uniqueUrls([ store.coverUrl, ...(store.carouselUrls || []), ...envPhotos, ]); const intro = store.intro?.trim() || ''; const benefitRuleRaw = store.benefitUsageRule?.trim() || ''; const benefitRule = benefitRuleRaw && !/^null$/i.test(benefitRuleRaw) ? benefitRuleRaw : ''; function previewEnv(index: number) { if (!envPhotos.length) return; Taro.previewImage({ current: envPhotos[index], urls: envPhotos, }).catch(() => toast('无法预览图片')); } const marqueeDurationSec = Math.max(18, Math.min(60, Math.round((marqueeText.length || 24) / 2.2))); 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 ? ( 电话: {store.phone} 拨打 ) : null} {store.category?.name ? ( {store.category.name} ) : null} 可核销 好客门店 {marqueeText ? ( {marqueeText} {marqueeText} ) : null} {intro ? ( 门店详情 {intro} ) : null} {benefitRule ? ( 好客权益券使用规则 {benefitRule} ) : null} {envPhotos.length > 0 ? ( 店内环境 {envPhotos.map((url, index) => ( previewEnv(index)} > ))} ) : null} Taro.navigateTo({ url: '/pages/redeem/index' })} > 到店核销 ); }