用户小程序手机号

门店闪烁逻辑
This commit is contained in:
2026-07-29 12:30:03 +08:00
parent e742f1e88b
commit 2b85f63e2b
+81 -27
View File
@@ -54,25 +54,43 @@ type Store = {
distanceMeters?: number | null; distanceMeters?: number | null;
}; };
function regionEqual(a: RegionSelection, b: RegionSelection): boolean { /** 筛选城市键(省+市);同城不重复请求 */
return a.province === b.province && a.city === b.city && a.district === b.district; function makeCityKey(region: Pick<RegionSelection, 'province' | 'city'>): string {
return `${region.province}|${region.city}`;
} }
function sameFilterCity(a: RegionSelection, b: RegionSelection): boolean {
return a.province === b.province && a.city === b.city;
}
/** 跨 tab 切换 / 页面重建仍复用,避免同城反复打 /stores */
type StoresListCache = {
cityKey: string;
cityCode: string;
region: RegionSelection;
items: Store[];
};
let storesListCache: StoresListCache | null = null;
export default function StoresPage() { export default function StoresPage() {
const [stores, setStores] = useState<Store[]>([]); const [stores, setStores] = useState<Store[]>(() => storesListCache?.items ?? []);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(() => !storesListCache);
const [keywordInput, setKeywordInput] = useState(''); const [keywordInput, setKeywordInput] = useState('');
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const [region, setRegion] = useState<RegionSelection>(DEFAULT_REGION); // 必须与缓存城市对齐,否则 remount 时用默认「郑州」筛掉缓存列表会闪「暂无」
const [region, setRegion] = useState<RegionSelection>(
() => storesListCache?.region ?? DEFAULT_REGION,
);
const [regionOpen, setRegionOpen] = useState(false); const [regionOpen, setRegionOpen] = useState(false);
const [category, setCategory] = useState<CategorySelection>(EMPTY_CATEGORY); const [category, setCategory] = useState<CategorySelection>(EMPTY_CATEGORY);
const [categoryOpen, setCategoryOpen] = useState(false); const [categoryOpen, setCategoryOpen] = useState(false);
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]); const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
/** 上次已用于请求的城市编码;仅换城才再拉列表 */ const fetchCityKeyRef = useRef<string | null>(storesListCache?.cityKey ?? null);
const cityCodeRef = useRef<string | null>(null);
const fetchSeqRef = useRef(0); const fetchSeqRef = useRef(0);
const regionLabel = formatRegionLabel(region); const regionLabel = formatRegionLabel(region);
const categoryLabel = formatCategoryLabel(category); const categoryLabel = formatCategoryLabel(category);
/** 有数据时只显示列表,不用「加载中」盖住(避免闪烁) */
const showBootLoading = loading && stores.length === 0;
const childIdsByParent = useMemo(() => { const childIdsByParent = useMemo(() => {
const map = new Map<string, string[]>(); const map = new Map<string, string[]>();
@@ -85,10 +103,13 @@ export default function StoresPage() {
return map; return map;
}, [categoryTree]); }, [categoryTree]);
/** 按城市拉门店;coords 仅用于排序距离,不单独触发二次请求 */ async function fetchStores(
async function fetchStores(nextCode: string, coords: UserCoords | null) { nextCode: string,
coords: UserCoords | null,
cityKey: string,
nextRegion: RegionSelection,
) {
const seq = ++fetchSeqRef.current; const seq = ++fetchSeqRef.current;
setLoading(true);
const qs = new URLSearchParams(); const qs = new URLSearchParams();
if (nextCode) qs.set('cityCode', nextCode); if (nextCode) qs.set('cityCode', nextCode);
if (coords) { if (coords) {
@@ -99,7 +120,15 @@ export default function StoresPage() {
try { try {
const list = await request<Store[]>(path); const list = await request<Store[]>(path);
if (seq !== fetchSeqRef.current) return; if (seq !== fetchSeqRef.current) return;
setStores(Array.isArray(list) ? list : []); const items = Array.isArray(list) ? list : [];
setStores(items);
fetchCityKeyRef.current = cityKey;
storesListCache = {
cityKey,
cityCode: nextCode,
region: toCityWideRegion(nextRegion),
items,
};
} catch (e) { } catch (e) {
if (seq !== fetchSeqRef.current) return; if (seq !== fetchSeqRef.current) return;
toast(e instanceof Error ? e.message : '加载失败'); toast(e instanceof Error ? e.message : '加载失败');
@@ -109,26 +138,46 @@ export default function StoresPage() {
} }
/** /**
* 流程:先定位 → 再请求一次列表(成功用定位城市,失败用默认城市) * 先稳住缓存画面 → 再定位;同城不请求;换城再拉
* 再次进入页且城市未变:不重复请求,避免列表闪烁。
*/ */
useDidShow(() => { useDidShow(() => {
syncTabBarSelected(1); syncTabBarSelected(1);
// 同步恢复缓存,避免 await 定位期间 region 不对导致列表被滤空
if (storesListCache) {
fetchCityKeyRef.current = storesListCache.cityKey;
setStores((prev) => (prev.length > 0 ? prev : storesListCache!.items));
setRegion((prev) =>
sameFilterCity(prev, storesListCache!.region) ? prev : storesListCache!.region,
);
setLoading(false);
}
void (async () => { void (async () => {
const resolved = await resolveUserCity(); const resolved = await resolveUserCity();
const nextCode = getCityCodeForCatalog(resolved); const nextCode = getCityCodeForCatalog(resolved);
const nextCoords = readCachedUserCoords();
const nextRegion = toCityWideRegion(resolved.region); const nextRegion = toCityWideRegion(resolved.region);
const cityChanged = cityCodeRef.current !== nextCode; const nextCityKey = makeCityKey(nextRegion);
setRegion((prev) => (regionEqual(prev, nextRegion) ? prev : nextRegion)); if (
fetchCityKeyRef.current === nextCityKey ||
if (cityCodeRef.current == null || cityChanged) { storesListCache?.cityKey === nextCityKey
cityCodeRef.current = nextCode; ) {
await fetchStores(nextCode, nextCoords); if (storesListCache?.cityKey === nextCityKey) {
fetchCityKeyRef.current = nextCityKey;
setStores((prev) => (prev.length > 0 ? prev : storesListCache!.items));
// 同城不覆盖用户已选区县
setRegion((prev) => (sameFilterCity(prev, nextRegion) ? prev : nextRegion));
}
setLoading(false);
return; return;
} }
setLoading(false);
// 换城:先清空再 loading,避免旧城数据 + 新城筛选交叉闪一下
setStores([]);
setRegion(nextRegion);
setLoading(true);
await fetchStores(nextCode, readCachedUserCoords(), nextCityKey, nextRegion);
})(); })();
}); });
@@ -144,10 +193,13 @@ export default function StoresPage() {
const resolved = await resolveUserCity(true); const resolved = await resolveUserCity(true);
const nextCode = getCityCodeForCatalog(resolved); const nextCode = getCityCodeForCatalog(resolved);
const nextRegion = toCityWideRegion(resolved.region); const nextRegion = toCityWideRegion(resolved.region);
const coords = readCachedUserCoords(); const nextCityKey = makeCityKey(nextRegion);
cityCodeRef.current = nextCode;
setRegion(nextRegion); setRegion(nextRegion);
await fetchStores(nextCode, coords); if (fetchCityKeyRef.current !== nextCityKey) {
setStores([]);
setLoading(true);
}
await fetchStores(nextCode, readCachedUserCoords(), nextCityKey, nextRegion);
} catch (e) { } catch (e) {
toast(e instanceof Error ? e.message : '加载失败'); toast(e instanceof Error ? e.message : '加载失败');
setLoading(false); setLoading(false);
@@ -246,9 +298,11 @@ export default function StoresPage() {
</View> </View>
<View className="store-list"> <View className="store-list">
{loading ? <View className="u-empty"></View> : null} {showBootLoading ? <View className="u-empty"></View> : null}
{!loading && filtered.length === 0 ? <View className="u-empty"></View> : null} {!showBootLoading && filtered.length === 0 ? (
{!loading && <View className="u-empty"></View>
) : null}
{!showBootLoading &&
filtered.map((s) => ( filtered.map((s) => (
<View <View
key={s.id} key={s.id}