用户小程序手机号
门店闪烁逻辑
This commit is contained in:
@@ -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) {
|
||||||
return;
|
fetchCityKeyRef.current = nextCityKey;
|
||||||
|
setStores((prev) => (prev.length > 0 ? prev : storesListCache!.items));
|
||||||
|
// 同城不覆盖用户已选区县
|
||||||
|
setRegion((prev) => (sameFilterCity(prev, nextRegion) ? prev : nextRegion));
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换城:先清空再 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user