[ISSUE-0037] 州/省加载失败误显示为「无数据」¶
用户反馈:选择 Australia → 国家列表清楚显示「8 州 · 16023 城市」→ 点
>进详情 → 州列表完全空白,文字说「该国家无州/省效数据」。
用户感知¶
[国家列表]
Australia 澳大利亚 · AU · 8 州 · 16023 城市 >
[点击 > 进入 Australia 详情]
全选整国 (0 个州) ← 矛盾:刚才说 8 州,现在 0?
[搜索州/省]
(空白)
该国家无州/省效数据
矛盾:缓存 stats 说有 8 州,详情却说没数据 → 用户困惑。
根因¶
location-picker-dialog.tsx:303 v0.10.55 之前:
apiCountryLocations({ code: country.iso2 })
.then((r: any) => {
const items = (r?.items as RawNode[]) || [];
stateCache.set(country.iso2, items);
setStatesData(items);
})
.catch(() => setStatesData([])) // ❌ 把"加载失败"也设成空数组
.finally(() => setStatesLoading(false));
链条:
1. 国家列表的「8 州 · 16023 城市」来自 country-stats.ts 提前 preload 的缓存(成功状态)
2. 用户点 > 进详情 → apiCountryLocations({ code: 'AU' }) 本次失败(网络瞬断/VPN 切换/限频)
3. .catch(() => setStatesData([])) 静默吞错 → statesData = []
4. UI 判 list.length === 0 → 显示「该国家无州/省数据」
5. 用户看到的现象:国家列表说有数据,详情却说没数据,且没有重试入口
修复(3 态区分)¶
const [statesLoading, setStatesLoading] = useState(false);
const [statesData, setStatesData] = useState<RawNode[]>([]);
const [statesError, setStatesError] = useState<string | null>(null); // 🆕
const loadStates = (iso2: string, force = false) => {
if (!force && stateCache.has(iso2)) { ... return; }
setStatesLoading(true);
setStatesError(null); // 🆕 重试时清错误
apiCountryLocations({ code: iso2 })
.then((r: any) => {
// 🆕 防御:API 可能返回 array 或 { items: [...] }
const items: RawNode[] = Array.isArray(r) ? r : ((r?.items as RawNode[]) || []);
stateCache.set(iso2, items);
setStatesData(items);
})
.catch((e) => {
setStatesData([]);
setStatesError(e?.message || String(e) || '加载失败'); // 🆕 记录错误
})
.finally(() => setStatesLoading(false));
};
UI 渲染 3 态:
{statesLoading ? <Loading /> :
statesError ? <ErrorWithRetry error={...} onRetry={() => loadStates(iso2, true)} /> : // 🆕
list.length === 0 ? <EmptyMessage /> :
<StatesList />}
修复后用户体验¶
[场景 A: 加载失败]
⚠️ 加载州/省数据失败:Failed to fetch
可能是网络/VPN/防火墙问题。国家列表的「N 州」是缓存值。
[🔄 重试]
[场景 B: 该国真的无州](如 Anguilla)
该国家无州/省数据
[场景 C: 加载中]
⏳ 加载中...
[场景 D: 正常] → 显示州列表
顺手修:API 响应格式防御¶
// 兼容 API 直接返 array 或 { items: [...] }
const items: RawNode[] = Array.isArray(r) ? r : ((r?.items as RawNode[]) || []);
未来如果服务端改格式(少见但有可能),不会直接 crash。
改动文件¶
| 文件 | 改了什么 |
|---|---|
src/components/locations-select/location-picker-dialog.tsx |
+ statesError state + loadStates helper + 错误态 UI + 重试按钮 |
package.json |
0.10.54 → 0.10.55 |
验证¶
- ✅
pnpm compile0 错 - ✅
pnpm build14.3s - 📋 实测路径:
- 触发 API 失败(如开 dialog 时关闭网络/VPN)
- 点 Australia
>→ 应显示「⚠️ 加载州/省数据失败 [重试]」 - 恢复网络后点重试 → 应正常加载
- 真的无州的国家(如 Anguilla)→ 仍显示「该国家无州/省数据」(区分清晰)
audit_grep 防同款¶
audit_grep:
- pattern: "\\.catch\\(\\(\\)\\s*=>\\s*set\\w+\\(\\[\\]\\)\\)"
description: ".catch(() => setData([])) — 加载失败和真空混为一谈,用户失去重试机会"
pnpm scan:issue-coverage 会全仓扫此反模式。
如何避免再犯¶
- fetch.catch 不要静默 setData([]) — 必须区分 loading / error / empty
- error 态必须有重试入口 — 用户不应只能关 dialog 重开
- API 响应格式做防御解析 —
Array.isArray(r) ? r : r.items - 缓存值与实时拉取值不一致 必须明确说明(缓存 vs error)
元-观察:第 4 个用户使用反馈¶
v0.10.52 ISSUE-0034: chrome 错误页 Failed to fetch
v0.10.53 ISSUE-0035: 按钮无反应(storage API 混用)
v0.10.54 ISSUE-0036: toast 成功但任务未入列表
v0.10.55 ISSUE-0037: 加载失败被误报"无数据"(本 issue)
第 4 个连击。共同模式:「业务代码静默吞错」
- ISSUE-0034: unhandledrejection 没 preventDefault → 错误冒到 chrome 错误页
- ISSUE-0035: storage 写不同 key → 监听者收不到(API 错位)
- ISSUE-0036: writeChain catch 吞错 → caller 拿不到失败信号
- ISSUE-0037: API catch 吞错 → 加载失败被误显示为真空数据
4 个 bug 同源:catch 后没正确传播错误状态给用户。