fc03905777
CI / verify (pull_request) Has been cancelled
Add benefitUsageRule on store create/edit (HQ + partner) and C-end detail between intro and env gallery. Co-authored-by: Cursor <cursoragent@cursor.com>
288 lines
9.1 KiB
TypeScript
288 lines
9.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { View, Text, Image } from '@tarojs/components';
|
||
import Taro, { 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;
|
||
};
|
||
|
||
function uniqueUrls(urls: Array<string | null | undefined>) {
|
||
const seen = new Set<string>();
|
||
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();
|
||
}
|
||
|
||
export default function StoreDetailPage() {
|
||
const router = useRouter();
|
||
const storeId = router.params.id ?? '';
|
||
const [store, setStore] = useState<Store | null>(null);
|
||
const [headerSolid, setHeaderSolid] = useState(false);
|
||
|
||
usePageScroll(({ scrollTop }) => {
|
||
setHeaderSolid(scrollTop > 100);
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!storeId) return;
|
||
request<Store>(`/stores/${storeId}`)
|
||
.then(setStore)
|
||
.catch(() => {
|
||
request<Store[]>('/stores')
|
||
.then((list) => {
|
||
const found = (Array.isArray(list) ? list : []).find((s) => s.id === storeId);
|
||
if (found) setStore(found);
|
||
else toast('门店不存在');
|
||
})
|
||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||
});
|
||
}, [storeId]);
|
||
|
||
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],
|
||
);
|
||
|
||
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 (
|
||
<PageShell variant="scroll" className="store-detail-page">
|
||
<PageNavBar title="门店详情" solid onBack={goBack} />
|
||
<View className="page-with-nav-bar u-empty">加载中…</View>
|
||
</PageShell>
|
||
);
|
||
}
|
||
|
||
const envPhotos = envPhotoUrls(store);
|
||
const images = uniqueUrls([
|
||
store.coverUrl,
|
||
...(store.carouselUrls || []),
|
||
...envPhotos,
|
||
]);
|
||
|
||
const intro = store.intro?.trim() || '';
|
||
const benefitRule = store.benefitUsageRule?.trim() || '';
|
||
|
||
function previewEnv(index: number) {
|
||
if (!envPhotos.length) return;
|
||
Taro.previewImage({
|
||
current: envPhotos[index],
|
||
urls: envPhotos,
|
||
}).catch(() => toast('无法预览图片'));
|
||
}
|
||
|
||
return (
|
||
<PageShell variant="scroll" className="store-detail-page" hasFixedFooter>
|
||
<WechatShareReady payload={sharePayload} />
|
||
<PageNavBar
|
||
title={store.name}
|
||
solid={headerSolid}
|
||
titleVisible={headerSolid}
|
||
onBack={goBack}
|
||
right={<ShareNavButton payload={sharePayload} />}
|
||
/>
|
||
|
||
<View className="store-detail-hero full-bleed">
|
||
<ProductCarousel images={images} alt={store.name} variant="store" />
|
||
</View>
|
||
|
||
<View className="store-detail-info-card">
|
||
<Text className="store-detail-name">{store.name}</Text>
|
||
|
||
<View className="store-detail-row">
|
||
<Text className="store-detail-meta store-detail-meta--flex">
|
||
{store.district ? `${store.district} · ` : ''}
|
||
{store.address || '地址待完善'}
|
||
</Text>
|
||
<Text className="store-detail-action" onClick={openMap}>
|
||
导航
|
||
</Text>
|
||
</View>
|
||
|
||
<Text className="store-detail-meta">
|
||
营业时间:{' '}
|
||
{(() => {
|
||
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';
|
||
})()}
|
||
</Text>
|
||
{store.avgPrice != null && Number(store.avgPrice) > 0 ? (
|
||
<Text className="store-detail-meta">人均约 ¥{Number(store.avgPrice).toFixed(0)}</Text>
|
||
) : null}
|
||
|
||
{store.phone ? (
|
||
<View className="store-detail-row">
|
||
<Text className="store-detail-meta store-detail-meta--flex">电话: {store.phone}</Text>
|
||
<Text className="store-detail-action" onClick={callStore}>
|
||
拨打
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
<View className="store-detail-tags">
|
||
{store.category?.name ? (
|
||
<Text className="store-detail-tag">{store.category.name}</Text>
|
||
) : null}
|
||
<Text className="store-detail-tag">可核销</Text>
|
||
<Text className="store-detail-tag">好客门店</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{intro ? (
|
||
<View className="store-detail-section">
|
||
<Text className="store-detail-section-title">门店详情</Text>
|
||
<Text className="store-detail-intro">{intro}</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
{benefitRule ? (
|
||
<View className="store-detail-section">
|
||
<Text className="store-detail-section-title">好客权益券使用规则</Text>
|
||
<Text className="store-detail-intro">{benefitRule}</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
{envPhotos.length > 0 ? (
|
||
<View className="store-detail-section">
|
||
<Text className="store-detail-section-title">店内环境</Text>
|
||
<View className="store-detail-env-grid">
|
||
{envPhotos.map((url, index) => (
|
||
<View
|
||
key={`${url}-${index}`}
|
||
className="store-detail-env-item"
|
||
onClick={() => previewEnv(index)}
|
||
>
|
||
<Image className="store-detail-env-img" src={url} mode="aspectFill" />
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
<View className="store-detail-bar">
|
||
<View
|
||
className="u-btn u-btn--block"
|
||
onClick={() => Taro.navigateTo({ url: '/pages/redeem/index' })}
|
||
>
|
||
<Text>到店核销</Text>
|
||
</View>
|
||
</View>
|
||
</PageShell>
|
||
);
|
||
}
|