Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,13 @@ ui/
- **后端错误映射**:`api.js` 抛错前过 `I18n.mapErr()`——en 模式下已知中文错误(「该模型暂无可用 key」「点数余额不足」「需要管理员权限」等 15 组)映射为英文,未知原样返回;zh 模式原样透传;
- **单语原则(v1.21.1 去混排)**:zh 词典值一律纯中文(仅保留 API/Key/Plan/tokens/CSV 等专有名词、键盘快捷键与占位符),不再内联英文注释;`index.html` 已移除全部 `<span class="en">` 静态小字(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()`。
56 changes: 38 additions & 18 deletions ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
'<div class="mini-item"><div><div class="t">' + esc(s.model) + "</div>" +
'<div class="d">' + esc(s.plan || "API") + " · " + T("dash.used", { used: D.fmt(s.used), quota: D.fmt(s.quota), price: D.fmt(s.price) }) + "</div></div>" +
'<div class="r"><span class="pts">+' + D.fmt(s.earned) + "</span><div class='d'>" + T("dash.earned") + "</div></div></div>"
).join("") + (on.length ? "" : '<div class="empty-state compact">' + EMPTY_ICON + "<p>" + T("dash.noSharing") + "</p><p class='muted'>" + T("dash.noSharing.sub") + "</p></div>");
// 共享收益累计趋势 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) =>
'<div class="mini-item"><div><div class="t">' + esc(s.model) + "</div>" +
'<div class="d">' + esc(s.plan || "API") + " · " + T("dash.used", { used: D.fmt(s.used), quota: D.fmt(s.quota), price: D.fmt(s.price) }) + "</div></div>" +
'<div class="r"><span class="pts">+' + D.fmt(s.earned) + "</span><div class='d'>" + T("dash.earned") + "</div></div></div>"
).join("") + (on.length ? "" : '<div class="empty-state compact">' + EMPTY_ICON + "<p>" + T("dash.noSharing") + "</p><p class='muted'>" + T("dash.noSharing.sub") + "</p></div>");
// 共享收益累计趋势 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();
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down
Loading