diff --git a/docs/user-stories.md b/docs/user-stories.md
index 5c68721..ac85a8f 100644
--- a/docs/user-stories.md
+++ b/docs/user-stories.md
@@ -10,7 +10,7 @@
>
> **v1.16(2026-08-17)· B 交互(UE)优化(rant `2026-08-17T15:50:05`).** **toast 分级**(成功 success / 错误 error / 信息 info,39 个调用点全部分级着色);**按钮 loading 态**(充值 / 加额 / 部门确认 / 上架提交 → 转圈 + 禁用,模拟反馈后恢复);**API Key 复制反馈**(复制后按钮短暂变「已复制 ✓」,1.2s 恢复,降级路径「请 Ctrl+C」);**键盘可达**(充值金额 / 加额金额·原因 / 部门名称·配额 Enter 提交;topup/raise/dept 行内卡片 Esc 关闭;打开时自动聚焦)。**rant 15:50:05 全部验收项满足,UI 美化与 UE 优化波收官(PR #38/#39/#40,docs v1.14→v1.16)。**
> **v1.17(2026-08-17)· 多角度 UI/UE 深化(rant `2026-08-17T16:57:17`,分批 PR).** **A. 清除原生弹窗 ✓(PR #41)**:约 5 处原生确认/输入弹窗(key 删除、共享下架、部门删除、API Key 新建/改名、运营者/成员充值)全部改为**行内二次确认**(按钮变「确认删除?」红色态,3 秒无操作或 Esc 还原,再次点击执行)与**行内编辑**(行内展开输入框,Enter 确认 / Esc 取消);抽象可复用 `confirmInline` / `inlineForm` 组件全站统一;`grep ui/` 无原生弹窗残留。**B. 相对时间显示 ✓(PR #42)**:新增 `timeAgo` 格式化(`刚刚 / N 分钟前 / N 小时前 / 昨天 / MM-DD`,hover `title` 显示完整绝对时间),交易列表、加额申请列表、共享列表(新增「上架时间」列)与 API Key 最近使用时间统一使用。**C. 数字列对齐 ✓(PR #43)**:价格 / 点数 / 已用 / 额度 / 余额等**数字列统一右对齐 + 等宽字体**(`--mono` + `tabular-nums`,新增 `.table .num` 类,覆盖市场输入/输出价与上下文、交易 Token 用量/点数、共享已用/额度·单价·收益、成员配额/剩余、部门成员数·月分配·已用·剩余、运营者余额、加额申请点数),时间列 / 状态列保持左对齐;金额统一 `D.fmt`(整数千分位、小数 2 位)全覆盖检查无遗漏。其余 D–G(`:focus-visible` 与快捷键、行内校验错误、视图过渡、细节一致性)分批落地。**D. 键盘可达性 / 无障碍 ✓(PR #44)**:全局 `:focus-visible` 焦点环(accent 色 outline,Tab 导航清晰可见;输入类控件沿用边框高亮不叠加);**全局快捷键**:`/` 聚焦市场搜索框(输入框内不触发)、数字 **1-7** 切换侧边栏视图(仪表盘/市场/共享/钱包/交易/管理/设置,带 title 与右侧键位角标提示,Cmd/Ctrl+数字 不劫持)、Esc 关闭行内新建 Key;侧边栏导航按钮补 `title`(快捷键提示)。**E. 表单行内校验错误 ✓(PR #45)**:新增 `setFieldError(input, msg)` / `clearFieldError(input)`(输入框**红边框** + 字段下方**红字小号行内文案**,输入修正自动清除,打开表单时重置);覆盖**充值金额**(自定义金额非法)、**申请加额**(点数/原因)、**部门表单**(名称/配额/重名)、**共享上架表单**(API Key/厂商·Plan·模型·额度),提交失败不再仅依赖 toast,聚焦首个错误字段。**F. 视图切换过渡 ✓(PR #46)**:页面切换加轻量动画(`.view:not(.hidden)` **fade + 轻微上移 6px,150ms** `viewIn`,替代生硬切换);数据表格更新无闪烁(`.table tbody` **fade-in** `tbodyIn`,新增 `pulseTbody` 在静态 tbody 重渲染后重启动画,动态表格整表重建自动生效)。**G. 细节一致性 ✓(PR #47)**:一致性审计确认——**所有数字/金额统一 `D.fmt`(千分位/2 位小数)**(侧边栏余额、统计卡、表格、钱包点数格式一致)、已用/额度展示统一;表格操作按钮统一小号样式(`padding:4px 10px;font-size:12px`);**行内编辑校验统一走行内错误**(inlineForm 校验失败由 toast 改为 `setFieldError` 红边框+行内文案,API Key 改名/运营者充值/成员充值 3 处对齐 item E);子文本字号统一(11px→12px);上架 toast 单价措辞统一「点/1M(自动)」;移除 3 处「(未生效)」残留文案。**rant 16:57:17 全部验收项满足(A–G 七项,PR #41–#47),UI/UE 深化波收官。**
-> **v1.20(2026-08-17)· UI/UE 第五轮多角度深化(rant `2026-08-17T20:46:57`,分批 PR,进行中).** **A. 新用户首次引导 ✓(PR #63)**:首次登录(localStorage `atp-tour-done` 未标记)触发**轻量引导 tour**——4 步依次高亮:① 仪表盘(余额/趋势)→ ② 模型市场(浏览/使用)→ ③ 共享管理(上架赚点数)→ ④ 钱包/设置(充值、API Key、接入端点);实现为**非 modal 浮层**(`#tour-overlay` 半透明遮罩 + `#tour-ring` 目标 **accent 描边高亮环**(`tour-pulse` 呼吸动画,`prefers-reduced-motion` 下静止)+ `#tour-pop` 气泡卡片:右上「跳过」、底部「上一步/下一步/完成」(末步变完成)、步骤计数 `n / 4`);**步骤自动切视图**(`switchView(view, {sync:false})` 不入历史)并按目标 `getBoundingClientRect` 定位高亮环与气泡(视口内钳制);**Esc / 点浮层外 / 跳过 / 完成 均可关闭**,关闭即写 `atp-tour-done=1`(之后登录不再出现);设置页加**「重新查看引导」**按钮(`#tour-replay-btn`,`startTour()` 重放)。**B. 复制 Key 后下一步引导 ✓(待合入)**:复制 API Key **成功后**的 toast 变为**可交互**——文案 + 内嵌**「配置接入端点 →」按钮**(`toast()` 扩展 `opts.action = { label, onClick }`:innerHTML 渲染按钮 + 展示时长从 2.6s 延长至 **6s**,给用户留点击时间;普通 toast 零改动);点击按钮 → **`gotoEndpointCard()`**:`switchView("settings")`(已在设置页则幂等保持)+ `#endpoint-card` **高亮闪烁**(`.ep-flash`,accent 光圈 ×2 次 0.8s)+ `scrollIntoView` 滚动到卡片;配合 20:44:18 的端点卡片形成「复制 → 配置端点」完整链路。**C. 表格密度切换 ✓(PR #66)**:设置页「偏好」区新增**「表格密度」**两档单选(`input[name="density"]`,`#density-comfortable` **舒适(默认)** / `#density-compact` **紧凑**,`.density-options` 纵向小字布局,accent 圆点);**localStorage `atp-density` 记忆**(默认 `comfortable`),启动时还原并勾选对应 radio;切换即时生效——`#app` 加/移 **`.density-compact` 修饰类**,全站 `.table` 统一收紧:`th/td` padding 10px/8px→**4px 8px**、`td` 字号 12px、表头 11px、行内按钮 11px(舒适档维持原样,零改动);适合 Key 列表/交易明细等长表格提升信息密度。**D. 市场「最近使用」✓(PR #67)**:市场工具栏下新增**「最近使用」行**(`#mk-recent`,工具栏计数下方、表格上方):**localStorage `atp-recent-models`** 记录最近 **5 个去重**模型 id(复用模型即**置顶去重**,最旧溢出),**复用 `.chip` 药丸**(hover accent 高亮、`--mono` 模型名 + title 显示厂商);**点击 chip 直接打开该模型聊天**(复用 openChat,同「使用 / 消费」主操作;游客点击提示先登录);「清空」小按钮(`data-mk-recent-clear`)一键清空记录并隐藏该行;`openChat` 内即时 `renderRecent()`(使用后 chips 立即更新),进入市场页(`renderMarketplace`)时从 storage 还原;行内删除/重置不破坏数据。**E. 交易记录导出 CSV ✓(PR #68)**:交易页 tab 下方新增右对齐 **「导出 CSV」按钮**(`#tx-export-btn`,ghost 小按钮,title 说明「导出当前筛选的交易记录」);`exportTxCsv()` 把**当前筛选可见的交易**(tab 过滤 + 列筛选 `filterRows`,与表格/汇总条同一数据源)导出为 CSV:**UTF-8 BOM**(`\uFEFF`,Excel 中文不乱码)+ `\r\n` 换行 + 表头「时间,类型,模型 / Key,Token 用量,点数,状态」(类型映射 `TX_TYPE` 中文,点数带正负号原值,字段含 `,`/`"`/换行时按 RFC4180 双引号转义);**Blob + `a[download]`**(`URL.createObjectURL` → 临时 `` click → 移除 → 1s 后 revoke);文件名 **`aitokenpool-transactions-YYYYMMDD.csv`**;无数据时 toast「没有可导出的交易记录」不导出;导出成功 toast「已导出 N 条交易记录」。其余 F–G(键盘导航 / 品牌)分批落地。
+> **v1.20(2026-08-17)· UI/UE 第五轮多角度深化(rant `2026-08-17T20:46:57`,分批 PR,进行中).** **A. 新用户首次引导 ✓(PR #63)**:首次登录(localStorage `atp-tour-done` 未标记)触发**轻量引导 tour**——4 步依次高亮:① 仪表盘(余额/趋势)→ ② 模型市场(浏览/使用)→ ③ 共享管理(上架赚点数)→ ④ 钱包/设置(充值、API Key、接入端点);实现为**非 modal 浮层**(`#tour-overlay` 半透明遮罩 + `#tour-ring` 目标 **accent 描边高亮环**(`tour-pulse` 呼吸动画,`prefers-reduced-motion` 下静止)+ `#tour-pop` 气泡卡片:右上「跳过」、底部「上一步/下一步/完成」(末步变完成)、步骤计数 `n / 4`);**步骤自动切视图**(`switchView(view, {sync:false})` 不入历史)并按目标 `getBoundingClientRect` 定位高亮环与气泡(视口内钳制);**Esc / 点浮层外 / 跳过 / 完成 均可关闭**,关闭即写 `atp-tour-done=1`(之后登录不再出现);设置页加**「重新查看引导」**按钮(`#tour-replay-btn`,`startTour()` 重放)。**B. 复制 Key 后下一步引导 ✓(待合入)**:复制 API Key **成功后**的 toast 变为**可交互**——文案 + 内嵌**「配置接入端点 →」按钮**(`toast()` 扩展 `opts.action = { label, onClick }`:innerHTML 渲染按钮 + 展示时长从 2.6s 延长至 **6s**,给用户留点击时间;普通 toast 零改动);点击按钮 → **`gotoEndpointCard()`**:`switchView("settings")`(已在设置页则幂等保持)+ `#endpoint-card` **高亮闪烁**(`.ep-flash`,accent 光圈 ×2 次 0.8s)+ `scrollIntoView` 滚动到卡片;配合 20:44:18 的端点卡片形成「复制 → 配置端点」完整链路。**C. 表格密度切换 ✓(PR #66)**:设置页「偏好」区新增**「表格密度」**两档单选(`input[name="density"]`,`#density-comfortable` **舒适(默认)** / `#density-compact` **紧凑**,`.density-options` 纵向小字布局,accent 圆点);**localStorage `atp-density` 记忆**(默认 `comfortable`),启动时还原并勾选对应 radio;切换即时生效——`#app` 加/移 **`.density-compact` 修饰类**,全站 `.table` 统一收紧:`th/td` padding 10px/8px→**4px 8px**、`td` 字号 12px、表头 11px、行内按钮 11px(舒适档维持原样,零改动);适合 Key 列表/交易明细等长表格提升信息密度。**D. 市场「最近使用」✓(PR #67)**:市场工具栏下新增**「最近使用」行**(`#mk-recent`,工具栏计数下方、表格上方):**localStorage `atp-recent-models`** 记录最近 **5 个去重**模型 id(复用模型即**置顶去重**,最旧溢出),**复用 `.chip` 药丸**(hover accent 高亮、`--mono` 模型名 + title 显示厂商);**点击 chip 直接打开该模型聊天**(复用 openChat,同「使用 / 消费」主操作;游客点击提示先登录);「清空」小按钮(`data-mk-recent-clear`)一键清空记录并隐藏该行;`openChat` 内即时 `renderRecent()`(使用后 chips 立即更新),进入市场页(`renderMarketplace`)时从 storage 还原;行内删除/重置不破坏数据。**E. 交易记录导出 CSV ✓(PR #68)**:交易页 tab 下方新增右对齐 **「导出 CSV」按钮**(`#tx-export-btn`,ghost 小按钮,title 说明「导出当前筛选的交易记录」);`exportTxCsv()` 把**当前筛选可见的交易**(tab 过滤 + 列筛选 `filterRows`,与表格/汇总条同一数据源)导出为 CSV:**UTF-8 BOM**(`\uFEFF`,Excel 中文不乱码)+ `\r\n` 换行 + 表头「时间,类型,模型 / Key,Token 用量,点数,状态」(类型映射 `TX_TYPE` 中文,点数带正负号原值,字段含 `,`/`"`/换行时按 RFC4180 双引号转义);**Blob + `a[download]`**(`URL.createObjectURL` → 临时 `` click → 移除 → 1s 后 revoke);文件名 **`aitokenpool-transactions-YYYYMMDD.csv`**;无数据时 toast「没有可导出的交易记录」不导出;导出成功 toast「已导出 N 条交易记录」。**F. 数据表格键盘导航 ✓(PR #69)**:所有数据表格(市场 / 共享 / API Key / 员工 / 部门 / 运营者 / 交易记录,`KBD_TABLE_IDS`)支持**纯键盘操作**——点击任意行激活(或直接 Tab 进表格后按方向键):**↑/↓ 行高亮 `.row-active`**(accent 左侧竖条 + 柔光底,随行 `scrollIntoView`,自动跳过展开详情行 `.mk-detail`,未激活时 ↓ 从首行 / ↑ 从末行开始),**Enter 触发该行主操作按钮**(行内第一个可用 `button.btn`,排除行展开 `+/-`;市场=使用/消费、API Key=复制、部门=编辑、运营者=充值…disabled 按钮不触发),**Esc 清除高亮**(无高亮时 Esc 落到原有逻辑:关帮助/关行内表单等);表格间切换自动跟随点击/焦点所在表格(`kbdContainerFrom` 沿 closest tbody/table 解析,`#tx-table` 这类包 table 的容器也识别);点击行内任何按钮同时激活该行,快捷键 `?` 帮助面板与 1-7 视图切换不受影响(先过 typing/meta 守卫)。**rant 20:46:57 已落地 A–F(PR #63/#65/#66/#67/#68/#69),G 品牌登录页 + favicon 待收尾。**
> **v1.19(2026-08-17)· 设置页「API 接入端点」卡片(rant `2026-08-17T20:44:18`).** 设置页新增**「接入方式 / API 端点」卡片**(`#endpoint-card`,放在 API Key 卡片**上方**——先看端点再生成 key):**OpenAI 兼容** `https://gateway.aitokenpool.local/v1`(Chat Completions · Cursor / Cline / Roo Code / OpenCode / OpenAI SDK)与 **Anthropic 兼容** `https://gateway.aitokenpool.local/anthropic`(Messages API · Claude Code / Goose / OpenClaw)各一行——协议标签(accent 药丸)+ **等宽可整段选中** URL(`user-select:all`)+ **复制按钮**(复用 copyKey 降级链:clipboard API → `execCommand("copy")` → 提示 Ctrl+C,带「已复制 ✓」flash)+ 说明小字;URL 旁小注「部署后替换为你的网关域名」(原型占位);卡片内**使用步骤** ① 生成 API Key → ② 工具 Base URL 填端点 → ③ 填 key;端点为 **UI 静态常量** `API_ENDPOINTS`(注释标明真实值来自部署配置,后端实现时接入 `config.server.base_url`);窄屏端点行纵向堆叠 + URL 自动换行。
> **v1.19(2026-08-17)· UI/UE 第四轮多角度深化(rant `2026-08-17T20:39:30`,分批 PR,进行中).****A. URL hash 路由 ✓(PR #55)**:视图与地址栏 hash 联动——`#/marketplace`、`#/wallet` 等视图 hash 可**直接收藏 / 分享 / 刷新恢复**;规则:① 视图切换(导航 / 快捷键 1-7 / 登录恢复)同步 `history.pushState` 更新 hash(已相同不重复入栈,不触发 hashchange 回环);② **前进 / 后退**(hashchange)视图跟随切换;③ **非法 hash** 回退仪表盘视图但**不重写 URL**(避免 pushState 污染历史、后退需两次);④ 空 hash 不动作(无 hash 时保持默认行为);⑤ 刷新后登录自动恢复 hash 对应视图(游客浏览 / 未登录态无冲突);⑥ 游客受限视图 hash 被拦截时保持原视图、不入栈。**B. 交易记录汇总条 ✓(PR #56)**:交易页卡片顶部加**紧凑汇总条**(`.tx-summary`,三列 inline:总收入 / 总支出 / 净变化,+绿 −红,零值中性显示 `0`);**随 tab 过滤联动**——「全部 / 消费 / 收益」tab 切换即时重算,且**与表格列筛选一致**(抽公共 `filterRows(rows, columns, filters)` 供 `buildDataTable` 与汇总共用,汇总反映与表格可见行相同的过滤集,不受分页影响);窄屏自动换行收窄。**C. 原生 select 美化 ✓(待合入)**:全站 `select` 去掉浏览器原生箭头,改**自定义 SVG 下拉箭头**(`--select-arrow` CSS 变量,内联 data-URI,深/亮主题各一色),`appearance:none` + `padding-right` 预留箭头空间;**hover / focus 边框同 input**(`var(--accent)`,focus 加 1px 光圈);`disabled` 态降透明度;覆盖全部 select 来源——静态 `.input`(市场筛选 `#mk-provider`/`#mk-sort`、共享表单 `#sf-provider/plan/model`、设置语言/默认模型)、动态 `select.th-filter`(表格列筛选)、`select[data-page-size]`(分页每页条数);`option` 深色背景与 `input-error` 错误态保留。**D. toast 队列堆叠 ✓(待合入)**:单例 toast 改为**队列容器** `#toast-wrap`(固定底部居中,纵向堆叠,`gap:10px`,`pointer-events:none` 不拦截点击);`toast(msg, type)` 每次**创建独立元素**(不再覆盖旧消息),**最多同时 3 条**(超限立即移除最旧一条腾位),每条**独立生命周期**——到时(2.6s)加 `.out` 淡出(0.2s `toast-out` 动画)后移除,互不影响;**分级样式保留**(success/error/info 边框色与文字色不变,39 个调用点零改动);toast 自身 `pointer-events:auto` 为后续可交互 toast 预留。**E. 快捷键帮助面板 ✓(待合入)**:按 **`?`**(或 `Shift+/`)在右上角弹出**行内卡片**(`#help-panel`,非 modal、无遮罩、不阻塞页面)展示快捷键速查:`/` 聚焦市场搜索、`1–7` 切换视图、`Esc` 关闭/取消、`?` 开合面板;**Esc 或再按 `?` 关闭**(帮助打开时 Esc 优先于行内表单,不会误关「新建 Key」);面板底部显示**当前上下文**(当前视图 + 亮/深色主题);输入框内按 `?` 不劫持(typing 守卫,正常输入字符);关闭按钮 ×;窄屏(≤560px)全宽贴顶。**F. 市场模型行展开详情 ✓(待合入)**:市场表格每行首列加 **`+`/`−` 展开按钮**(`.row-expand`,hover accent);点击在 **tr 下追加详情行**(`.mk-detail`,`colspan=7` 浅底网格,行内展开不弹窗)展示:**Max tokens**(单次输出上限,模型未公布显示「未公布」)、**价格换算**(`1M tokens ≈ N 点` 按输出价 + 输入价/1M)、**上下文长度**、**可用性**(可用/繁忙 + 成功率)、**多 key 自动故障转移说明**(仅 `multi` 模型显示,架构 v0.2 路由策略);**仅展开当前行**(点其它行自动收起,再点收起),展开态存 `mkExpanded` 数据(搜索/筛选重渲染后保留)。**G. 登录页 polish ✓(待合入)**:登录卡片视觉微调——**logo 加大**(52px + 渐变微光 `0 0 18px rgba(78,205,196,.35)`)、**品牌标题层次**(h1 22px + 副文案行高)、**输入框聚焦光晕**(`0 0 0 3px var(--accent-soft)`,双主题适配);**空邮箱/密码提交行内错误**(复用 `field-error` 组件:红边框 + 行内文案,聚焦首个错误字段,输入修正自动清除,`novalidate` 自管校验);**「记住我」checkbox**(`#login-remember`,localStorage `atp-remember` 记忆,加载时还原)+ **演示模式说明小字**(`demo-hint`:demo@aitokenpool.local / demo1234)。**rant 20:39:30 全部验收项满足(A–G 七项,PR #55–#61),UI/UE 第四轮深化波收官,docs v1.19。**
> **v1.18(2026-08-17)· UI/UE 第三轮多角度深化(rant `2026-08-17T18:06:09`,分批 PR).** **A. 仪表盘数据可视化 ✓(PR #48)**:纯 SVG sparkline(零外部依赖,手写 path,颜色用现有 CSS 变量):「本月点数变化」卡片按天聚合近 7 日净变化画迷你折线图(渐变填充,每点 `` hover 显示当天数值);「我的共享」卡显示共享收益累计趋势 sparkline(`--ok` 色),无上架 key 时保留空状态不画图。**B. 亮色主题 ✓(PR #49)**:CSS 变量重构——新增 `:root[data-theme="light"]` 全套变量重定义(--bg/--bg-card/--border/--text 等),并把原先硬编码颜色(表格行边框/斑马纹、卡片 hover 阴影、确认态、spinner、遮罩)抽为语义变量(--table-row-border/--table-stripe/--card-hover-border/--card-shadow/--danger-soft/--danger-text/--spin-track/--overlay)双主题共用;侧边栏加**日/月 SVG 切换按钮**(按当前主题显隐图标),**localStorage 记忆**(atp-theme),首次加载尊重 **`prefers-color-scheme`**;两主题对比度均达标(正文/次要文字/边框层次清晰)。**C. 移动端表格卡片化 ✓(PR #50)**:`@media (max-width: 560px)` 下关键表格(市场/共享/交易/管理/设置 API Key)**隐藏表头**,每行渲染为**卡片**(`label: value` 布局,`td::before` 取 `data-label`),不再横向滚动;卡片内主字段加粗、状态徽章保留、操作按钮整行宽(`flex: 1 1 45%` 换行);全部表格 td 补齐 `data-label`(含 buildDataTable 动态列用 col.title)。**D. 搜索增强 ✓(PR #51)**:全部搜索框(市场 `#mk-search` / API Key `#ak-search` / 部门 `#od-search` / 运营者用户 `#ops-search`)统一接入 **~150ms 输入防抖**(避免每次按键整表重绘闪烁,连续输入只渲染一次);渲染结果对匹配关键词**大小写不敏感 `` 高亮**(先 HTML 转义再包裹,`--accent-soft` 底 + `--accent-text` 字,双主题对比度达标;清空后自动清除);搜索框加**「清空 ×」小按钮**(有内容时显示,点击清空立即重绘并聚焦输入框;空状态「清除搜索/清除筛选」按钮同样同步 × 态)。**E. 动效与系统偏好 ✓(PR #52)**:按钮 **`:active` 轻微下压**(`scale(0.98)`,disabled 不触发);统计卡 **hover 微抬升**(`translateY(-2px)` + 边框/阴影提升,与卡片 hover 语言一致);**数字/点数变化轻微跳动动画**(新增 `bump(el)` 助手——remove→reflow→add `.bump` 类重放 `numJump`(`translateY(-3px) scale(1.02)`,0.35s);接入 4 处余额变化点:钱包充值(侧边栏+钱包余额)、聊天消费扣款、加额批准、运营者给自己充值);**尊重 `prefers-reduced-motion: reduce`**——全局 `animation-duration/transition-duration` 压到 0.01ms,禁用过渡/动画但保留全部功能。**F. 动态文档标题 ✓(PR #53)**:`document.title` **跟随视图切换**(`switchView` 内统一设置「视图标题 · AITokenPool」,如「模型市场 Marketplace · AITokenPool」;`VIEW_TITLE` 7 个视图全覆盖;未知视图回退「AITokenPool」);**默认「AITokenPool」**——首次加载(DOMContentLoaded)与登录页/无视图态回默认标题,HTML `` 同步改为「AITokenPool」;游客受限视图被拦截时标题保持不变。**G. 其他细节 ✓(验证确认,PR #54 收尾)**:① **视图切换滚动复位**——`switchView` 内 `$("#main").scrollTop = 0`(`.main` 即 `overflow-y:auto` 滚动容器,渲染后复位,多次切换均生效,DOM 冒烟验证);② **复制按钮「已复制 ✓」态**——`copyKey` 的 flash 反馈(复制成功按钮短暂变「已复制 ✓」并禁用,1.2s 恢复;降级路径「请 Ctrl+C」;v1.16 已实现,DOM 冒烟验证)。**rant 18:06:09 全部验收项满足(A–G 七项,PR #48–#54),UI/UE 第三轮深化波收官,docs v1.18。**
diff --git a/ui/README.md b/ui/README.md
index 5398fe8..dce226f 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -265,6 +265,14 @@ ui/
- 下载:`Blob(type="text/csv;charset=utf-8")` → `URL.createObjectURL` → 临时 `` click → `remove()` → `setTimeout 1s` revoke;文件名 **`aitokenpool-transactions-YYYYMMDD.csv`**(`new Date()` 本地日期);
- 冒烟测试注意:stub 需给 `document.createElement("a")` 返回带 `click()`/`remove()` 的元素并捕获 `href`/`download`,`URL.createObjectURL` 捕获 Blob(`arrayBuffer()` 首 3 字节 EF BB BF 验证 BOM——`blob.text()` 会按规范剥掉 BOM);列筛选联动可注入 `#tx-table` 的 `querySelectorAll(".th-filter")`/`querySelector('[data-filter-key=…]')` 假输入并 fire `input`。
+## 数据表格键盘导航约定(v1.20,rant 2026-08-17T20:46:57 F)
+
+- **作用表**(`KBD_TABLE_IDS`):`mk-body` / `share-body` / `api-keys` / `emp-body` / `dept-body` / `ops-body`(tbody)+ `tx-table`(div 包 table);
+- **激活**:① 点击表格行(容器 click 委托,`e.target.closest("tr")`,跳过 `.mk-detail`);② 直接按 ↑/↓——`kbdContainerFrom(t)` 沿 `closest("tbody")`(id 匹配)或 `closest("table").parentNode`(#tx-table)解析当前表格,找不到沿用 `kbd.c`;
+- **键位**:`ArrowDown/Up` → `kbdMove(dir, c)` 行高亮 `.row-active`(accent 左侧竖条 `inset 3px 0 0` + `--accent-soft` 底),未激活时 ↓ 首行 / ↑ 末行,`scrollIntoView({block:"nearest"})`;`Enter` → `kbdEnter()` 点击行内首个可用 `button.btn:not(.row-expand)`(disabled 不触发);`Esc` → `kbdClear()`(无高亮时落到原逻辑:关帮助/行内表单/引导);
+- **守卫**:typing(INPUT/TEXTAREA/SELECT/contentEditable)与 meta/ctrl/alt 组合键不拦截;`?`、1-7 视图切换、Esc 原有优先级(引导 > 帮助 > 行内新建 Key > 表格高亮)均不受影响;
+- 冒烟测试注意:`qs(sel)` 的 stub id 会带 `#` 前缀——`kbdContainerFrom` 依赖真实无 `#` 的 id(如 `tx-table`),须手动修正;Esc 分支链依赖 `#ak-new-inline`、`#help-panel` hidden 预置 + `atp-tour-done=1`(防 tour 拦截)。
+
## 数据说明
- 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙)
diff --git a/ui/css/style.css b/ui/css/style.css
index 4f61e57..47af9a2 100644
--- a/ui/css/style.css
+++ b/ui/css/style.css
@@ -337,6 +337,10 @@ select.input option { background: var(--bg-card); }
.table tbody tr:nth-child(even) { background: var(--table-stripe); }
.table tbody tr:hover { background: var(--bg-hover); }
+/* 表格键盘导航(rant 20:46:57 F:↑/↓ 行高亮 .row-active,accent 左侧竖条 + 柔光底) */
+.table tbody tr.row-active { background: var(--accent-soft); }
+.table tbody tr.row-active td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
+
/* 表格密度切换(rant 20:46:57 C:紧凑档行 padding 减小、字号 13px,全站 .table 统一生效) */
.density-compact .table th { padding: 4px 8px; font-size: 11px; }
.density-compact .table td { padding: 4px 8px; font-size: 12px; }
diff --git a/ui/js/app.js b/ui/js/app.js
index f3a1c86..a78068a 100644
--- a/ui/js/app.js
+++ b/ui/js/app.js
@@ -1042,6 +1042,57 @@
toast("已导出 " + list.length + " 条交易记录", "success");
}
+ /* --- 数据表格键盘导航(rant 20:46:57 F:↑/↓ 行高亮 .row-active,Enter 触发主操作,Esc 清除) --- */
+ const KBD_TABLE_IDS = ["mk-body", "share-body", "api-keys", "emp-body", "dept-body", "ops-body", "tx-table"];
+ let kbd = { c: null, i: -1 }; // 当前激活的表格容器 + 高亮行下标
+
+ function kbdRows(c) {
+ if (!c) return [];
+ const trs = c.tagName === "TBODY" ? c.querySelectorAll("tr") : c.querySelectorAll("tbody tr");
+ return [].filter.call(trs, (tr) => tr && !(tr.classList && tr.classList.contains("mk-detail")));
+ }
+ function kbdClear() {
+ if (kbd.c) kbdRows(kbd.c).forEach((tr) => tr.classList.remove("row-active"));
+ kbd = { c: null, i: -1 };
+ }
+ function kbdSet(c, idx) {
+ if (kbd.c && kbd.c !== c) kbdRows(kbd.c).forEach((tr) => tr.classList.remove("row-active"));
+ const rows = kbdRows(c);
+ if (!rows.length) { kbd = { c: null, i: -1 }; return; }
+ idx = Math.max(0, Math.min(idx, rows.length - 1));
+ if (kbd.c === c && kbd.i >= 0 && kbd.i < rows.length && kbd.i !== idx) rows[kbd.i].classList.remove("row-active");
+ rows[idx].classList.add("row-active");
+ kbd = { c: c, i: idx };
+ if (rows[idx].scrollIntoView) rows[idx].scrollIntoView({ block: "nearest" });
+ }
+ function kbdMove(dir, c) {
+ const cont = c || kbd.c;
+ const rows = kbdRows(cont);
+ if (!rows.length) return;
+ let idx = kbd.c === cont ? kbd.i : -1;
+ if (idx < 0) idx = dir > 0 ? -1 : rows.length; // 未激活时 ↓ 从首行、↑ 从末行开始
+ kbdSet(cont, Math.max(0, Math.min(idx + dir, rows.length - 1)));
+ }
+ function kbdEnter() {
+ const rows = kbdRows(kbd.c);
+ if (!rows.length || kbd.i < 0) return;
+ const tr = rows[kbd.i];
+ if (!tr) return;
+ // 主操作 = 行内第一个可用的操作按钮(排除行展开 +/-,含 .btn 但非 row-expand)
+ const btn = tr.querySelector ? tr.querySelector("button.btn:not(.row-expand)") : null;
+ if (btn && !btn.disabled) btn.click();
+ }
+ function kbdContainerFrom(t) {
+ // 从事件目标向上找表格容器(tbody 本身或包 table 的 #tx-table),找不到沿用上次激活的表格
+ if (t && t.closest) {
+ const tb = t.closest("tbody");
+ if (tb && KBD_TABLE_IDS.indexOf(tb.id) >= 0) return tb;
+ const tbl = t.closest("table");
+ if (tbl && tbl.parentNode && tbl.parentNode.id === "tx-table") return tbl.parentNode;
+ }
+ return kbd.c;
+ }
+
/* --- 通用 MRT 风格数据表格渲染器 ---
cfg: { container, columns, rows, state, onState }
columns: [{ key, title, sort?: "string"|"number", filter?: "text"|"select"|"number-range", options?, render? }]
@@ -1887,6 +1938,17 @@
$$("#tx-tabs .tab").forEach((b) => b.addEventListener("click", () => { txTab = b.dataset.txTab; txTable.page = 1; renderTransactions(); }));
$("#tx-export-btn").addEventListener("click", exportTxCsv); // 导出 CSV(rant 20:46:57 E)
+ // 表格键盘导航(rant 20:46:57 F):点击行 → 激活高亮,之后 ↑/↓/Enter/Esc 可用
+ KBD_TABLE_IDS.forEach((id) => {
+ const c = document.getElementById(id);
+ if (!c) return;
+ c.addEventListener("click", (e) => {
+ const tr = e.target.closest ? e.target.closest("tr") : null;
+ if (!tr || (tr.classList && tr.classList.contains("mk-detail"))) return;
+ kbdSet(c, kbdRows(c).indexOf(tr));
+ });
+ });
+
// API Key 生成(行内编辑;列表展示脱敏、复制给完整 id)
$("#new-api-key-btn").addEventListener("click", openNewKeyInline);
$("#ak-new-ok").addEventListener("click", commitNewKey);
@@ -2030,6 +2092,16 @@
if (e.key === "Escape" && helpOpen) { toggleHelp(false); return; }
if (e.key === "Escape" && !$("#ak-new-inline").hidden) { closeNewKeyInline(); return; }
if (typing || e.metaKey || e.ctrlKey || e.altKey) return;
+ // 表格键盘导航(rant 20:46:57 F):↑/↓ 行高亮,Enter 主操作,Esc 清除(无高亮时 Esc 落到后续逻辑)
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
+ const c = kbdContainerFrom(t);
+ if (!c) return;
+ e.preventDefault();
+ kbdMove(e.key === "ArrowDown" ? 1 : -1, c);
+ return;
+ }
+ if (e.key === "Enter") { kbdEnter(); return; }
+ if (e.key === "Escape" && kbd.c) { kbdClear(); return; }
if (e.key === "?") { toggleHelp(); return; }
if (e.key === "/") {
e.preventDefault();