From 0aa721afe8da7453158e02e05788e0bee084696b Mon Sep 17 00:00:00 2001 From: argszero Date: Wed, 19 Aug 2026 15:54:46 +0800 Subject: [PATCH] fix(ui): dashboard/sharings no longer fall back to mock data when logged in (v1.21.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Host feedback (rant 2026-08-19T15:48:17): dashboard '我的共享' showed D.SHARINGS mock rows (GLM Coding Plan / deepseek-v4-flash) for logged-in users on a clean DB because loadDashboard() never fetched /api/sharings. - loadDashboard(): fetch /api/sharings (failure -> Live.sharings = null) - renderDashboard(): logged-in + no data -> empty state ('还没有上架的 key'); logged-in + fetch failure -> loadErrorHtml empty state + retry button; mock (D.SHARINGS) kept for guest mode only - renderSharing(): same degradation principle (logged-in-only view) - bindLiveRetry() wired once at startup for #dash-sharings / #share-body - ui/README.md: v1.21.2 convention section (data source + degradation) --- ui/README.md | 10 ++++++++++ ui/js/app.js | 56 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/ui/README.md b/ui/README.md index 0728428..54e28a4 100644 --- a/ui/README.md +++ b/ui/README.md @@ -301,3 +301,13 @@ ui/ - **后端错误映射**:`api.js` 抛错前过 `I18n.mapErr()`——en 模式下已知中文错误(「该模型暂无可用 key」「点数余额不足」「需要管理员权限」等 15 组)映射为英文,未知原样返回;zh 模式原样透传; - **单语原则(v1.21.1 去混排)**:zh 词典值一律纯中文(仅保留 API/Key/Plan/tokens/CSV 等专有名词、键盘快捷键与占位符),不再内联英文注释;`index.html` 已移除全部 `` 静态小字(55 处);`.en` CSS 样式已删除;en 词典保持纯英文; - 冒烟测试:node 无 DOM 桩跑 i18n.js(t/setLang/mapErr/fmtNum/fmtRelTime 断言,见开发记录);Key 一致性扫描(app.js+index.html 引用的 496 键全部存在于 ZH/EN)。 + +## 仪表盘「我的共享」数据源与降级约定(v1.21.2,rant 2026-08-19T15:48:17 BUG) + +- **数据源**:登录后 `loadDashboard()` 拉取 `/api/wallet` + `/api/dashboard` + **`/api/sharings`**(此前漏拉 sharings → 仪表盘「我的共享」恒显示 `D.SHARINGS` mock,如 GLM/deepseek 假行);`Live.sharings` 拉取成功后 `renderDashboard()` 用真实数据渲染「厂商 · Plan / 已用 / 额度 / 累计收益」; +- **降级原则**:**mock 只用于游客模式**——`Live.x ? view(Live.x) : D.x` 的 fallback 在登录态一律不得暴露 mock: + - 登录态 `Live.sharings` 为空数组(`[]`,上架 0 条)→ 空态「还没有上架的 key」; + - 登录态拉取失败(`null`)→ `loadErrorHtml(空态文案, 重试函数, err.loadFail)`:空态 + 「重试」按钮(`data-live-retry` 委托,`bindLiveRetry` 在 DOMContentLoaded 一次性绑定 `#dash-sharings` / `#share-body`),不白屏、不显示 mock; + - 游客模式(`loggedIn()` 为假)→ 继续用 `D.SHARINGS` mock 浏览(不变); +- **覆盖范围**:仪表盘 `renderDashboard()` 与共享管理 `renderSharing()` 两处 `Live.sharings` 消费点均按此约定(共享页为登录态专属视图,原 mock fallback 只在拉取失败时暴露,同属本 bug 类); +- **冒烟测试注意**:登录态断言「我的共享」不得含 `glm-5.2` / `deepseek-v4-flash` 行;干净库(上架 0 条)应见空态文案;stub `api.get("/api/sharings")` 抛错时断言出现 `[data-live-retry]` 按钮且点击后重新调用 `loadDashboard()`。 diff --git a/ui/js/app.js b/ui/js/app.js index d10ee97..c21e648 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -555,32 +555,43 @@ stat(T("dash.trades"), T("cnt.trades", { n: (Live.transactions ? (Live.transactions.total || 0) : txs.length) }), T("dash.trades.sub")), ].join(""); - const shares = Live.sharings ? sharingsToView(Live.sharings) : D.SHARINGS; - const on = shares.filter((s) => s.status === "on"); - $("#dash-sharings").innerHTML = on.map((s) => - '
' + esc(s.model) + "
" + - '
' + esc(s.plan || "API") + " · " + T("dash.used", { used: D.fmt(s.used), quota: D.fmt(s.quota), price: D.fmt(s.price) }) + "
" + - '
+' + D.fmt(s.earned) + "
" + T("dash.earned") + "
" - ).join("") + (on.length ? "" : '
' + EMPTY_ICON + "

" + T("dash.noSharing") + "

" + T("dash.noSharing.sub") + "

"); - // 共享收益累计趋势 sparkline(rant 18:06:09 A;无上架 key 时保留空状态,不画图) - if (on.length) { - const days = lastDayLabels(7); - const earn = dailySeries(days, (t) => t.type === "earn"); - let cum = 0; - const cumSeries = earn.map((v) => { cum = Math.round((cum + v) * 100) / 100; return cum; }); - $("#dash-sharings").insertAdjacentHTML("afterbegin", - sparkline(cumSeries, { labels: days, fmt: (v) => "+" + D.fmt(v), stroke: "var(--ok)" })); + // 降级原则(rant 2026-08-19T15:48:17):mock 只用于游客模式; + // 登录态加载失败 → 空态 + 重试(loadErrorHtml),不静默 fallback 到 D.SHARINGS + const shares = Live.sharings ? sharingsToView(Live.sharings) : (loggedIn() ? null : D.SHARINGS); + if (shares) { + const on = shares.filter((s) => s.status === "on"); + $("#dash-sharings").innerHTML = on.map((s) => + '
' + esc(s.model) + "
" + + '
' + esc(s.plan || "API") + " · " + T("dash.used", { used: D.fmt(s.used), quota: D.fmt(s.quota), price: D.fmt(s.price) }) + "
" + + '
+' + D.fmt(s.earned) + "
" + T("dash.earned") + "
" + ).join("") + (on.length ? "" : '
' + EMPTY_ICON + "

" + T("dash.noSharing") + "

" + T("dash.noSharing.sub") + "

"); + // 共享收益累计趋势 sparkline(rant 18:06:09 A;无上架 key 时保留空状态,不画图) + if (on.length) { + const days = lastDayLabels(7); + const earn = dailySeries(days, (t) => t.type === "earn"); + let cum = 0; + const cumSeries = earn.map((v) => { cum = Math.round((cum + v) * 100) / 100; return cum; }); + $("#dash-sharings").insertAdjacentHTML("afterbegin", + sparkline(cumSeries, { labels: days, fmt: (v) => "+" + D.fmt(v), stroke: "var(--ok)" })); + } + } else { + $("#dash-sharings").innerHTML = loadErrorHtml(T("dash.noSharing"), () => loadDashboard(), T("err.loadFail")); } renderMonthChanges(); } - // P2-B:拉取仪表盘所需数据(wallet + dashboard + 交易数) + // P2-B:拉取仪表盘所需数据(wallet + dashboard + sharings + 交易数) async function loadDashboard() { if (!loggedIn()) return; try { await refreshWallet(); } catch (e) { /* 降级 */ } try { Live.dashboard = await api.get("/api/dashboard"); } catch (e) { Live.dashboard = null; } + // rant 2026-08-19T15:48:17:仪表盘「我的共享」此前从不拉 sharings → 登录态恒显示 D.SHARINGS mock; + // 现在拉真实数据;失败置 null → renderDashboard 走空态 + 重试(mock 仅游客) + try { + Live.sharings = await api.get("/api/sharings"); + } catch (e) { Live.sharings = null; } renderDashboard(); } @@ -718,8 +729,14 @@ }; function renderSharing() { - // P2-B:登录 → 用后端 /api/sharings;否则 mock - const list = Live.sharings ? sharingsToView(Live.sharings) : D.SHARINGS; + // P2-B:登录 → 用后端 /api/sharings;否则 mock(游客实际被导航拦截,仅作兜底) + const list = Live.sharings ? sharingsToView(Live.sharings) : (loggedIn() ? null : D.SHARINGS); + if (!list) { + // 降级原则(rant 2026-08-19T15:48:17):登录态加载失败 → 空态 + 重试,不暴露 mock + $("#share-stats").innerHTML = ""; + $("#share-body").innerHTML = loadErrorHtml(T("share.empty"), () => loadSharing(), T("err.loadFail")); + return; + } const on = list.filter((s) => s.status === "on"); const totalEarned = list.reduce((a, s) => a + s.earned, 0); const totalUsed = list.reduce((a, s) => a + s.used, 0); @@ -2835,6 +2852,9 @@ }); renderNav(); bindEvents(); + // 登录态加载失败的空态重试按钮(rant 2026-08-19T15:48:17:loadErrorHtml 的 data-live-retry 委托) + bindLiveRetry("dash-sharings", () => loadDashboard()); + bindLiveRetry("share-body", () => loadSharing()); renderView("dashboard"); $("#side-balance").textContent = D.fmt(D.USER.balance);