定位获取城市功能,需要微信地图的key?

This commit is contained in:
2026-07-06 14:02:07 +08:00
parent 67af6d7d53
commit c87d79109a
18 changed files with 792 additions and 70 deletions
+33 -7
View File
@@ -6,6 +6,11 @@ import TabMainHeader from '../components/TabMainHeader';
import AppToast from '../components/AppToast';
import { getProductImages } from '../lib/product-images';
import { track } from '../lib/analytics';
import {
CITY_STORAGE_KEY,
FALLBACK_CITY_CODE,
resolveUserCity,
} from '../lib/wechat-location';
import CouponBadge from '@dukang/shared-ui/CouponBadge';
type Product = {
@@ -34,18 +39,18 @@ const AROMA_TABS = [
{ key: 'NONGXIANG', label: '浓香型', open: false },
];
const CITY_STORAGE_KEY = 'dukang_selected_city';
export default function HomePage() {
const [tab, setTab] = useState('QINGXIANG');
const [products, setProducts] = useState<Product[]>([]);
const [cities, setCities] = useState<City[]>([]);
const [cityCode, setCityCode] = useState(() => localStorage.getItem(CITY_STORAGE_KEY) || '410100');
const [cityCode, setCityCode] = useState(() => localStorage.getItem(CITY_STORAGE_KEY) || FALLBACK_CITY_CODE);
const [locatedCityLabel, setLocatedCityLabel] = useState('');
const [citySource, setCitySource] = useState<'auto' | 'manual'>('auto');
const [toast, setToast] = useState('');
useEffect(() => {
track('home_view', { pagePath: '/' });
}, []);
track('home_view', { pagePath: '/', cityCode });
}, [cityCode]);
useEffect(() => {
request<City[]>('USER_H5', '/catalog/cities').then((list) => {
@@ -56,6 +61,21 @@ export default function HomePage() {
});
}, []);
useEffect(() => {
if (citySource !== 'auto') return;
resolveUserCity().then((resolved) => {
if (!resolved) return;
setLocatedCityLabel(resolved.displayCity);
if (resolved.openCity && resolved.cityCode) {
setCityCode(resolved.cityCode);
} else {
setCityCode(FALLBACK_CITY_CODE);
setToast('当前城市暂未开城,已展示郑州商品');
window.setTimeout(() => setToast(''), 2200);
}
});
}, [citySource]);
useEffect(() => {
if (!cityCode) return;
localStorage.setItem(CITY_STORAGE_KEY, cityCode);
@@ -76,6 +96,7 @@ export default function HomePage() {
}
const selectedCity = cities.find((c) => c.code === cityCode);
const headerCityLabel = locatedCityLabel || selectedCity?.name || '郑州市';
const filtered = products.filter((p) => p.aromaType === tab);
const onSale = tab === 'QINGXIANG';
@@ -86,10 +107,15 @@ export default function HomePage() {
extra={(
<div className="tab-main-city">
<span className="material-symbols-outlined">location_on</span>
<span className="tab-main-city-label">{headerCityLabel}</span>
<select
value={cityCode}
onChange={(e) => setCityCode(e.target.value)}
style={{ border: 'none', background: 'transparent', font: 'inherit', color: 'inherit' }}
onChange={(e) => {
setCitySource('manual');
setCityCode(e.target.value);
}}
className="tab-main-city-select"
aria-label="选择开城城市"
>
{cities.map((c) => (
<option key={c.code} value={c.code}>{c.name}</option>
+57 -11
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { request } from '../lib/api';
@@ -7,10 +7,12 @@ import TabMainHeader from '../components/TabMainHeader';
import RegionPicker from '../components/RegionPicker';
import {
DEFAULT_REGION,
FALLBACK_CITY_REGION,
formatRegion,
REGION_ALL,
type RegionSelection,
} from '../lib/region-data';
import { FALLBACK_CITY_CODE, resolveUserCity } from '../lib/wechat-location';
type OpenCity = {
code: string;
@@ -62,6 +64,9 @@ export default function StoreListPage() {
const [keyword, setKeyword] = useState('');
const [region, setRegion] = useState<RegionSelection>(DEFAULT_REGION);
const [regionPickerOpen, setRegionPickerOpen] = useState(false);
const [filterMode, setFilterMode] = useState<'auto' | 'manual'>('auto');
const [usedFallback, setUsedFallback] = useState(false);
const [geoReady, setGeoReady] = useState(false);
const cityCode = useMemo(() => resolveCityCode(region, cities), [region, cities]);
@@ -74,15 +79,45 @@ export default function StoreListPage() {
}, []);
useEffect(() => {
if (filterMode !== 'auto' || geoReady) return;
resolveUserCity().then((resolved) => {
if (resolved) {
setRegion(resolved.region);
}
setGeoReady(true);
});
}, [filterMode, geoReady]);
const fetchStores = useCallback(
async (targetRegion: RegionSelection, allowFallback: boolean) => {
const code = resolveCityCode(targetRegion, cities);
const qs = code ? `?cityCode=${encodeURIComponent(code)}` : '';
const data = await request<StoreItem[]>('USER_H5', `/stores${qs}`).catch(() => [] as StoreItem[]);
if (
allowFallback &&
filterMode === 'auto' &&
data.length === 0 &&
!usedFallback &&
code &&
code !== FALLBACK_CITY_CODE
) {
setUsedFallback(true);
setRegion(FALLBACK_CITY_REGION);
return null;
}
return data;
},
[cities, filterMode, usedFallback],
);
useEffect(() => {
if (filterMode === 'auto' && !geoReady) return;
let cancelled = false;
setLoading(true);
const qs = cityCode ? `?cityCode=${encodeURIComponent(cityCode)}` : '';
request<StoreItem[]>('USER_H5', `/stores${qs}`)
fetchStores(region, true)
.then((data) => {
if (!cancelled) setStores(data);
})
.catch(() => {
if (!cancelled) setStores([]);
if (!cancelled && data) setStores(data);
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -90,7 +125,7 @@ export default function StoreListPage() {
return () => {
cancelled = true;
};
}, [cityCode]);
}, [region, cities, filterMode, geoReady, fetchStores, usedFallback]);
const filtered = useMemo(() => {
let list = stores;
@@ -122,6 +157,10 @@ export default function StoreListPage() {
}, [stores, categoryTab, keyword, region, cityCode]);
const regionLabel = formatRegion(region.province, region.city, region.district);
const emptyMessage =
filterMode === 'manual' && filtered.length === 0
? '未找到匹配门店'
: '暂无门店';
return (
<div className="page store-page">
@@ -144,7 +183,10 @@ export default function StoreListPage() {
type="search"
placeholder="搜索门店名称或地址"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onChange={(e) => {
setFilterMode('manual');
setKeyword(e.target.value);
}}
/>
</div>
</div>
@@ -156,7 +198,10 @@ export default function StoreListPage() {
key={tab}
type="button"
className={`store-category-tab${categoryTab === tab ? ' active' : ''}`}
onClick={() => setCategoryTab(tab)}
onClick={() => {
setFilterMode('manual');
setCategoryTab(tab);
}}
>
{tab}
</button>
@@ -215,7 +260,7 @@ export default function StoreListPage() {
))}
{!loading && filtered.length === 0 && (
<div className="store-empty">{stores.length === 0 ? '暂无门店' : '未找到匹配门店'}</div>
<div className="store-empty">{emptyMessage}</div>
)}
{!loading && filtered.length > 0 && (
@@ -228,6 +273,7 @@ export default function StoreListPage() {
value={region}
onClose={() => setRegionPickerOpen(false)}
onConfirm={(next) => {
setFilterMode('manual');
setRegion(next);
setRegionPickerOpen(false);
}}