小程序优化,商铺定位等

This commit is contained in:
2026-07-27 22:33:00 +08:00
parent 1725b9e4b4
commit 23c0cc7a5f
15 changed files with 441 additions and 41 deletions
@@ -17,8 +17,12 @@ type Store = {
id: string;
name: string;
address?: string;
province?: string;
cityName?: string;
city?: string;
district?: string;
phone?: string;
intro?: string | null;
coverUrl?: string | null;
carouselUrls?: string[] | null;
openTime?: string | null;
@@ -26,9 +30,16 @@ type Store = {
openTime2?: string | null;
closeTime2?: string | null;
avgPrice?: number | null;
latitude?: number | string | null;
longitude?: number | string | null;
category?: { name: string } | null;
};
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 ?? '';
@@ -57,7 +68,7 @@ export default function StoreDetailPage() {
const sharePayload = useMemo(
() => ({
title: store?.name || DEFAULT_SHARE_TITLE,
desc: store?.address || DEFAULT_SHARE_DESC,
desc: store?.intro?.trim() || store?.address || DEFAULT_SHARE_DESC,
path: `/pages/store-detail/index?id=${storeId}`,
imgUrl: store?.coverUrl || store?.carouselUrls?.[0] || undefined,
}),
@@ -77,6 +88,44 @@ export default function StoreDetailPage() {
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">
@@ -93,6 +142,8 @@ export default function StoreDetailPage() {
? [store.coverUrl]
: []) as string[];
const intro = store.intro?.trim() || '';
return (
<PageShell variant="scroll" className="store-detail-page" hasFixedFooter>
<WechatShareReady payload={sharePayload} />
@@ -110,10 +161,17 @@ export default function StoreDetailPage() {
<View className="store-detail-info-card">
<Text className="store-detail-name">{store.name}</Text>
<Text className="store-detail-meta">
{store.district ? `${store.district} · ` : ''}
{store.address || '地址待完善'}
</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">
:{' '}
{(() => {
@@ -126,7 +184,16 @@ export default function StoreDetailPage() {
{store.avgPrice != null && Number(store.avgPrice) > 0 ? (
<Text className="store-detail-meta"> ¥{Number(store.avgPrice).toFixed(0)}</Text>
) : null}
{store.phone ? <Text className="store-detail-meta">: {store.phone}</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>
@@ -136,8 +203,18 @@ export default function StoreDetailPage() {
</View>
</View>
{intro ? (
<View className="store-detail-section">
<Text className="store-detail-section-title"></Text>
<Text className="store-detail-intro">{intro}</Text>
</View>
) : null}
<View className="store-detail-bar">
<View className="u-btn u-btn--block" onClick={() => toast('核销请前往「好客权益」')}>
<View
className="u-btn u-btn--block"
onClick={() => Taro.navigateTo({ url: '/pages/redeem/index' })}
>
<Text></Text>
</View>
</View>
+38 -11
View File
@@ -17,8 +17,15 @@ import {
type RegionSelection,
} from '../../lib/region-data';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
import {
getCityCodeForCatalog,
readCachedUserCoords,
resolveUserCity,
toCityWideRegion,
type UserCoords,
} from '../../lib/user-location';
import { FALLBACK_CITY_CODE } from '../../lib/product-images';
import { formatDistanceMeters } from '../../lib/geo';
import { request, toast } from '../../lib/api';
type Store = {
@@ -37,10 +44,11 @@ type Store = {
status?: string;
categoryId?: string | null;
category?: { id?: string; name?: string; parentId?: string | null } | null;
latitude?: number | string | null;
longitude?: number | string | null;
distanceMeters?: number | null;
};
const MOCK_DISTANCES = ['800m', '1.2km', '3.5km', '1.5km', '2.0km'];
export default function StoresPage() {
const [stores, setStores] = useState<Store[]>([]);
const [loading, setLoading] = useState(true);
@@ -52,6 +60,7 @@ export default function StoresPage() {
const [categoryOpen, setCategoryOpen] = useState(false);
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
const [cityCode, setCityCode] = useState<string>(FALLBACK_CITY_CODE);
const [userCoords, setUserCoords] = useState<UserCoords | null>(() => readCachedUserCoords());
const regionLabel = formatRegionLabel(region);
const categoryLabel = formatCategoryLabel(category);
@@ -69,8 +78,9 @@ export default function StoresPage() {
useDidShow(() => {
syncTabBarSelected(1);
void resolveUserCity().then((resolved) => {
setRegion(resolved.region);
setRegion(toCityWideRegion(resolved.region));
setCityCode(getCityCodeForCatalog(resolved));
setUserCoords(readCachedUserCoords());
});
});
@@ -82,12 +92,19 @@ export default function StoresPage() {
const loadStores = useCallback(() => {
setLoading(true);
const path = cityCode ? `/stores?cityCode=${encodeURIComponent(cityCode)}` : '/stores';
const qs = new URLSearchParams();
if (cityCode) qs.set('cityCode', cityCode);
const coords = userCoords ?? readCachedUserCoords();
if (coords) {
qs.set('lat', String(coords.latitude));
qs.set('lng', String(coords.longitude));
}
const path = qs.toString() ? `/stores?${qs}` : '/stores';
return request<Store[]>(path)
.then((list) => setStores(Array.isArray(list) ? list : []))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [cityCode]);
}, [cityCode, userCoords]);
useEffect(() => {
void loadStores();
@@ -96,12 +113,20 @@ export default function StoresPage() {
usePullDownRefresh(() => {
void (async () => {
try {
const resolved = await resolveUserCity();
setRegion(resolved.region);
const resolved = await resolveUserCity(true);
setRegion(toCityWideRegion(resolved.region));
const nextCode = getCityCodeForCatalog(resolved);
setCityCode(nextCode);
const coords = readCachedUserCoords();
setUserCoords(coords);
setLoading(true);
const path = nextCode ? `/stores?cityCode=${encodeURIComponent(nextCode)}` : '/stores';
const qs = new URLSearchParams();
if (nextCode) qs.set('cityCode', nextCode);
if (coords) {
qs.set('lat', String(coords.latitude));
qs.set('lng', String(coords.longitude));
}
const path = qs.toString() ? `/stores?${qs}` : '/stores';
const list = await request<Store[]>(path);
setStores(Array.isArray(list) ? list : []);
} catch (e) {
@@ -189,7 +214,7 @@ export default function StoresPage() {
{loading ? <View className="u-empty"></View> : null}
{!loading && filtered.length === 0 ? <View className="u-empty"></View> : null}
{!loading &&
filtered.map((s, index) => (
filtered.map((s) => (
<View
key={s.id}
className="store-card"
@@ -211,7 +236,9 @@ export default function StoresPage() {
<Text className="store-card-meta">¥{Number(s.avgPrice).toFixed(0)}</Text>
) : null}
<View className="store-card-footer">
<Text className="store-card-distance">{MOCK_DISTANCES[index % MOCK_DISTANCES.length]}</Text>
<Text className="store-card-distance">
{formatDistanceMeters(s.distanceMeters)}
</Text>
<Text
className="store-card-cta"
onClick={(e) => {