用户小程序手机号

门店闪烁逻辑
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;
};
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() {
const [stores, setStores] = useState<Store[]>([]);
const [loading, setLoading] = useState(true);
const [stores, setStores] = useState<Store[]>(() => storesListCache?.items ?? []);
const [loading, setLoading] = useState(() => !storesListCache);
const [keywordInput, setKeywordInput] = 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 [category, setCategory] = useState<CategorySelection>(EMPTY_CATEGORY);
const [categoryOpen, setCategoryOpen] = useState(false);
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
/** 上次已用于请求的城市编码;仅换城才再拉列表 */
const cityCodeRef = useRef<string | null>(null);
const fetchCityKeyRef = useRef<string | null>(storesListCache?.cityKey ?? null);
const fetchSeqRef = useRef(0);
const regionLabel = formatRegionLabel(region);
const categoryLabel = formatCategoryLabel(category);
/** 有数据时只显示列表,不用「加载中」盖住(避免闪烁) */
const showBootLoading = loading && stores.length === 0;
const childIdsByParent = useMemo(() => {
const map = new Map<string, string[]>();
@@ -85,10 +103,13 @@ export default function StoresPage() {
return map;
}, [categoryTree]);
/** 按城市拉门店;coords 仅用于排序距离,不单独触发二次请求 */
async function fetchStores(nextCode: string, coords: UserCoords | null) {
async function fetchStores(
nextCode: string,
coords: UserCoords | null,
cityKey: string,
nextRegion: RegionSelection,
) {
const seq = ++fetchSeqRef.current;
setLoading(true);
const qs = new URLSearchParams();
if (nextCode) qs.set('cityCode', nextCode);
if (coords) {
@@ -99,7 +120,15 @@ export default function StoresPage() {
try {
const list = await request<Store[]>(path);
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) {
if (seq !== fetchSeqRef.current) return;
toast(e instanceof Error ? e.message : '加载失败');
@@ -109,26 +138,46 @@ export default function StoresPage() {
}
/**
* 流程:先定位 → 再请求一次列表(成功用定位城市,失败用默认城市)
* 再次进入页且城市未变:不重复请求,避免列表闪烁。
* 先稳住缓存画面 → 再定位;同城不请求;换城再拉
*/
useDidShow(() => {
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 () => {
const resolved = await resolveUserCity();
const nextCode = getCityCodeForCatalog(resolved);
const nextCoords = readCachedUserCoords();
const nextRegion = toCityWideRegion(resolved.region);
const cityChanged = cityCodeRef.current !== nextCode;
const nextCityKey = makeCityKey(nextRegion);
setRegion((prev) => (regionEqual(prev, nextRegion) ? prev : nextRegion));
if (cityCodeRef.current == null || cityChanged) {
cityCodeRef.current = nextCode;
await fetchStores(nextCode, nextCoords);
if (
fetchCityKeyRef.current === nextCityKey ||
storesListCache?.cityKey === nextCityKey
) {
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;
}
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 nextCode = getCityCodeForCatalog(resolved);
const nextRegion = toCityWideRegion(resolved.region);
const coords = readCachedUserCoords();
cityCodeRef.current = nextCode;
const nextCityKey = makeCityKey(nextRegion);
setRegion(nextRegion);
await fetchStores(nextCode, coords);
if (fetchCityKeyRef.current !== nextCityKey) {
setStores([]);
setLoading(true);
}
await fetchStores(nextCode, readCachedUserCoords(), nextCityKey, nextRegion);
} catch (e) {
toast(e instanceof Error ? e.message : '加载失败');
setLoading(false);
@@ -246,9 +298,11 @@ export default function StoresPage() {
</View>
<View className="store-list">
{loading ? <View className="u-empty"></View> : null}
{!loading && filtered.length === 0 ? <View className="u-empty"></View> : null}
{!loading &&
{showBootLoading ? <View className="u-empty"></View> : null}
{!showBootLoading && filtered.length === 0 ? (
<View className="u-empty"></View>
) : null}
{!showBootLoading &&
filtered.map((s) => (
<View
key={s.id}