@@ -1,6 +1,13 @@
import { useMemo , useState } from 'react' ;
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
import { View , Text , Image } from '@tarojs/components' ;
import Taro , { useDidShow , usePageScroll , useRouter , useShareAppMessage , useShareTimeline } from '@tarojs/taro' ;
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' ;
@@ -45,7 +52,7 @@ type Store = {
type RecentRedeem = {
userLabel : string ;
amount : number ;
amount : number | string ;
createdAt : string ;
} ;
@@ -74,10 +81,21 @@ function fullAddress(store: Store) {
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 ( ) ) ) return '—' ;
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' ,
@@ -93,38 +111,114 @@ function formatRedeemTime(input?: string | null) {
return ` ${ get ( 'year' ) } - ${ get ( 'month' ) } - ${ get ( 'day' ) } ${ get ( 'hour' ) } : ${ get ( 'minute' ) } : ${ get ( 'second' ) } ` ;
}
function formatRedeemAmountYuan ( amount : number ) {
if ( ! Number . isFinite ( amount ) ) return '0' ;
if ( Number . isInteger ( amount ) ) return String ( amount ) ;
return amount . toFixed ( 2 ) . replace ( /\.?0+$/ , '' ) ;
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 ) ) < 1 e - 9 ) return String ( Math . round ( n ) ) ;
return n . toFixed ( 2 ) . replace ( /\.?0+$/ , '' ) ;
}
function formatRecentRedeemLine ( row : RecentRedeem ) {
return ` ${ row . userLabel || '用户***' } ${ formatRedeemTime ( row . createdAt ) } 核销 ${ formatRedeemAmountYuan ( Number ( row . amount ) ) } 元 ` ;
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 = router . params . id ? ? '' ;
const [ storeId , setStoreId ] = useState ( ( ) = > pickStoreId ( router . params . id ) ) ;
const [ store , setStore ] = useState < Store | null > ( null ) ;
const [ recentRedeems , setRecentRedeems ] = useState < RecentRedeem [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ loadError , setLoadError ] = useState ( '' ) ;
const [ headerSolid , setHeaderSolid ] = useState ( false ) ;
const storeRef = useRef < Store | null > ( null ) ;
storeRef . current = store ;
usePageScroll ( ( { scrollTop } ) = > {
setHeaderSolid ( scrollTop > 100 ) ;
} ) ;
useDidShow ( ( ) = > {
if ( ! storeId ) return ;
request < Store > ( ` /stores/ ${ storeId } ` )
. then ( setStore )
. catch ( ( ) = > {
const loadStore = useCallback ( async ( id : string ) = > {
if ( ! id ) {
setLoading ( false ) ;
setLoadError ( '缺少门店参数' ) ;
return ;
}
setLoadError ( '' ) ;
if ( ! storeRef . current ) setLoading ( true ) ;
try {
const data = await request < Store > ( ` /stores/ ${ id } ` ) ;
if ( ! data || ! data . id ) {
setStore ( null ) ;
setLoadError ( '门店不存在或暂不可见' ) ;
toast ( '门店不存在或暂不可见' ) ;
} ) ;
request < RecentRedeem [ ] > ( ` /stores/ ${ storeId } /recent-redeems?limit=20 ` )
. then ( ( list ) = > setRecentRedeems ( Array . isArray ( list ) ? list : [ ] ) )
. catch ( ( ) = > setRecentRedeems ( [ ] ) ) ;
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 < unknown > ( ` /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 (
@@ -200,7 +294,9 @@ export default function StoreDetailPage() {
return (
< PageShell variant = "scroll" className = "store-detail-page" >
< PageNavBar title = "门店详情" solid onBack = { goBack } / >
< View className = "page-with-nav-bar u-empty" > 加 载 中 … < / View >
< View className = "page-with-nav-bar u-empty" >
{ loading ? '加载中…' : loadError || '门店不存在或暂不可见' }
< / View >
< / PageShell >
) ;
}
@@ -225,8 +321,7 @@ export default function StoreDetailPage() {
} ) . catch ( ( ) = > toast ( '无法预览图片' ) ) ;
}
// 文案过短时放慢动画,过长时加快一点(仍循环)
const marqueeDurationSec = Math . max ( 18 , Math . min ( 60 , Math . round ( marqueeText . length / 2.2 ) ) ) ;
const marqueeDurationSec = Math . max ( 18 , Math . min ( 60 , Math . round ( ( marqueeText . length || 24 ) / 2.2 ) ) ) ;
return (
< PageShell variant = "scroll" className = "store-detail-page" hasFixedFooter >
@@ -288,15 +383,15 @@ export default function StoreDetailPage() {
< / View >
{ marqueeText ? (
< View className = "store-detail-marquee" aria - hidden >
< View className = "store-detail-marquee" >
< View
className = "store-detail-marquee-track"
style = { { animationDuration : ` ${ marqueeDurationSec } s ` } }
style = { {
animationDuration : ` ${ marqueeDurationSec } s ` ,
} }
>
< Text className = "store-detail-marquee-text " > { marqueeText } < / Text >
< Text className = "store-detail-marquee-gap " > < / Text >
< Text className = "store-detail-marquee-text" > { marqueeText } < / Text >
< Text className = "store-detail-marquee-gap" > < / Text >
< View className = "store-detail-marquee-i tem " > { marqueeText } < / View >
< View className = "store-detail-marquee-item " > { marqueeText } < / View >
< / View >
< / View >
) : null }