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
8 changes: 8 additions & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,11 @@ ui/
- **运营视图**(2 tab 不变):运行概览新增四张卡片——**服务版本**(`/api/ops/runtime` 的 `version`,取自 `env!("CARGO_PKG_VERSION")`,与 `/healthz` 同源;前端**不得**写死版本号)与**运行时长**(`uptime_secs` / `uptime_days` / `uptime_hours` / `uptime_minutes` / `uptime_secs_rest`,进位在后端 `split_uptime` 完成,前端 `fmtUptime` 只挑「最高两个非零位」并取 `ops.uptime.{days,hours,minutes,seconds}` 单位)、**今日调用量(按小时)** `.bar-list`(`today_hours`,服务端 0-23 全量补零;GROUP BY 会省略无调用的小时,不补零会让柱子整体左移,与交易页 `txTrendDays` 同款坑)与**上游 key 健康** `.mini-list`(`key_health` 按厂商聚合 total/on/off,三态 pill:健康 / N 个异常 / 全部失败);成员充值的搜索框移到卡片标题行右侧(原型 `.spread`);
- **交易量卡(第 9 张,回归修复)**:`total_txs`(`ops.stats.trades` + `cnt.trades` + `ops.stats.trades.sub`)自 `85982e8`(PR #80)起就在 `/api/ops/runtime` 返回(全库 `COUNT(*)`,累计**全部类型**),但 v1.22 零 mock 重构 `89963f3` 删掉 mock 分支那张卡时漏了重接。**这不是原型对齐**——原型没有这张卡(原型 4 张,实现 9 张,多出的卡是刻意的)。值一律取自响应,**不取** `D.TRANSACTIONS`;
- **零 mock 不破**:以上数据全部来自真实端点,加载失败仍走空态 + 重试(`.mini-item` / `.bar-row` 只在有真实数据时才渲染)。

## 交易类型筛选:一份状态,两套控件(C2112)

- **只有一份状态**:交易页的「类型」筛选器有两个控件 —— 顶部 **`#tx-tabs`**(全部 / 消费 / 收益)与「类型」列表头下的 **`select.th-filter[data-filter-key="type"]`**(6 个库内值)。**状态只存在 `txTable.filters.type`**(空串 = 不限;取值恒为库内值 ⇒ 语言无关,见 C2031:「选项 value 与文案分离」)。
- **两者都读写它**:`txTypeFilter()` 是唯一取值定义(`loadTransactions` 的 `type` 参数、`renderTransactions` 的 tab 高亮都调它);`setTxTypeFilter(v)` 是唯一写入口,并**同步已渲染的 select**(#148 之后表头不重建 → 控件值不会自己跟上状态)。
- **删掉的第二份状态**:`txTab`。此前顶部 tab 另存一份 `txTab`,而请求按 `filters.type || txTab` 取值 ⇒ 列筛选一旦出值,tab 的写入就被永久盖住:点 tab 只是挪高亮、列表不变(**死控件**),高亮却仍按 `txTab` 画(**说谎的指示器**)。jsdom 实测(改前 5/11):列筛选选「赠送」后,请求 `type=gift` 而「全部」仍高亮;点「消费」后请求仍是 `gift`、`select` 仍显示 `gift` 而高亮已跳到「消费」——三者各说各话。
- **高亮口径**:生效值不属于 all/consume/earn 时(topup / withdraw / gift / expire)**没有任何 tab 自称生效**(都不高亮),因为把「全部」点亮而列表只有赠送行同样是说谎。改版后不变量:**请求参数 / tab 高亮 / 列筛选 select 三者恒为同一份状态的投影**。
- **冒烟测试注意**:点 tab 后状态已变,重拉由 `renderTransactions` 的筛选签名比对触发 —— **不要再显式 `loadTransactions()`**(会与它并发两次请求,后到者可能把先到者的行覆盖回去)。断言请比对「最近一次 `/api/transactions` 请求的 `type`」与「高亮的 tab」与「`select` 的 value」,三者用**同一个派生**(`wantTabs(type)`),不要写死字面量 —— 字面量会在应用的高亮恰好拼对时误绿。
2 changes: 1 addition & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,6 @@ <h3 id="chat-title">使用模型</h3>
<script src="js/api.js?v=20260912-5"></script>
<script src="js/data.js?v=20260912-5"></script>
<script src="js/i18n.js?v=20260914-4"></script>
<script src="js/app.js?v=20260914-6"></script>
<script src="js/app.js?v=20260914-7"></script>
</body>
</html>
40 changes: 33 additions & 7 deletions ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
const T = window.t; // i18n(rant 2026-08-18T20:49:22)

let activeView = "dashboard";
let txTab = "all";
// 交易类型筛选**只有一份状态**:`txTable.filters.type`(见 txTypeFilter(),原 `txTab` 已删)
let txRange = "24h"; // 交易时间段快捷范围:24h / 7d / 30d / all / custom(默认最近 24 小时,rant 2026-08-22T10:50:00)
let txCustomStart = ""; // 自定义开始(datetime-local 值,本地时区)
let txCustomEnd = ""; // 自定义结束
Expand Down Expand Up @@ -1521,7 +1521,11 @@
}

function renderTransactions() {
$$("#tx-tabs .tab").forEach((b) => b.classList.toggle("active", b.dataset.txTab === txTab));
// tab 高亮 = **生效的类型筛选**(txTypeFilter()),不是另存的一份 tab 状态 —— 两处必须同源,
// 否则列筛选一出值,高亮就与列表说的不是同一件事。生效值不属于 all/consume/earn 时
// (topup/withdraw/gift/expire),没有任何 tab 可以自称生效 ⇒ 都不高亮。
const effTab = txTypeFilter() || "all";
$$("#tx-tabs .tab").forEach((b) => b.classList.toggle("active", b.dataset.txTab === effTab));
// 零 mock(rant 2026-08-19T15:54:06):登录态绝不 fallback D.TRANSACTIONS;
// 加载失败 → 空态 + 重试;游客不可达(导航拦截)
if (loggedIn() && !Live.transactions) {
Expand Down Expand Up @@ -1601,6 +1605,23 @@
if (f.status) add("status", f.status);
return p.join("&");
}
// 交易类型筛选:**一份状态,两套控件**。顶部 tab(全部/消费/收益)与「类型」列筛选 select
// (6 个库内值)是同一个筛选器的两种控件,状态只存在 `txTable.filters.type`(空串 = 不限,
// 取值恒为库内值 ⇒ 语言无关,见 C2031)。请求参数、tab 高亮、列筛选控件三者都必须是**它**的
// 投影;此前顶部 tab 另存一份 `txTab`,而请求按 `filters.type || txTab` 取值 ⇒ 列筛选一出值,
// tab 的写入就被永久盖住(点 tab 只是挪高亮、列表不变),高亮也仍按 `txTab` 画
// ⇒ 指示器与列表说的不是同一件事。删掉第二份状态,该类不再存在。
function txTypeFilter() {
return (txTable.filters && txTable.filters.type) || "";
}
// 写类型筛选,并同步**已渲染**的列筛选 select:#148 之后表头不重建(保住筛选框焦点),
// 控件值不会自己跟上状态;表头若尚未渲染/已被清空则无需同步,下次重建会读 state 得到正确值。
function setTxTypeFilter(v) {
txTable.filters = txTable.filters || {};
txTable.filters.type = v || "";
const sel = document.querySelector('#tx-table select[data-filter-key="type"]');
if (sel) sel.value = txTable.filters.type;
}
// 列筛选签名:筛选条件变化 → renderTransactions 触发重拉(rant 2026-08-25T10:33:26)
function txFilterSig() {
const f = txTable.filters || {};
Expand All @@ -1611,10 +1632,9 @@
// loadedPage/loadedPageSize 记录已加载页,renderTransactions 发现页码不一致时自动重拉)
async function loadTransactions() {
if (!loggedIn()) return;
// 类型:列筛选 type(select)优先于顶部 tab(tab=all 时即列筛选值);列筛选后端化后同走 type 参数。
// 类型:单一状态(txTypeFilter()),tab 与列筛选都读写它;后端化后同走 type 参数。
// C2031:筛选状态即库内值(consume/earn/…),直接发送,无需反查。
const colType = (txTable.filters && txTable.filters.type) ? txTable.filters.type : "";
const type = colType || (txTab === "all" ? "" : txTab);
const type = txTypeFilter();
const range = txRangeParams();
const cols = txFilterParams(); // rant 2026-08-25T10:33:26:列筛选随请求发出,后端全量过滤
const page = Math.max(1, txTable.page || 1);
Expand Down Expand Up @@ -3669,8 +3689,14 @@
// 容器级委托,避免为每个视图各绑一次;与原型 data-goto 语义一致
$$("[data-goto]").forEach((b) => b.addEventListener("click", () => switchView(b.dataset.goto)));

// 交易 Tab(P2-B:切 tab 重新拉后端过滤数据)
$$("#tx-tabs .tab").forEach((b) => b.addEventListener("click", () => { txTab = b.dataset.txTab; txTable.page = 1; renderTransactions(); if (loggedIn()) loadTransactions(); }));
// 交易 Tab(P2-B:切 tab 重新拉后端过滤数据):tab 写的就是类型筛选本身(setTxTypeFilter)
// —— 它也负责把「类型」列筛选控件同步成同一个值,否则两份控件又会各说各话;控件状态一变,
// renderTransactions 里的签名比对自会重拉(勿再显式调 loadTransactions,会与它并发两次请求)。
$$("#tx-tabs .tab").forEach((b) => b.addEventListener("click", () => {
setTxTypeFilter(b.dataset.txTab === "all" ? "" : b.dataset.txTab);
txTable.page = 1;
renderTransactions();
}));
$("#tx-export-btn").addEventListener("click", exportTxCsv); // 导出 CSV(rant 20:46:57 E)

// 交易时间段(rant 2026-08-22T10:50:00:快捷范围 + 自定义起止,切换后重载列表与汇总)
Expand Down
Loading