跳转至

[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 compile 0 错
  • pnpm build 14.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 后没正确传播错误状态给用户。