From 8eac0ed35f807a70bd602f04857d29e74aeb629e Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 17 Aug 2026 18:13:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20light=20theme=20=E2=80=94=20CSS=20v?= =?UTF-8?q?ar=20refactor=20+=20sidebar=20toggle=20(v1.18=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/user-stories.md | 2 +- ui/README.md | 5 ++++ ui/css/style.css | 69 +++++++++++++++++++++++++++++++++++++++----- ui/index.html | 4 +++ ui/js/app.js | 13 +++++++++ 5 files changed, 85 insertions(+), 8 deletions(-) diff --git a/docs/user-stories.md b/docs/user-stories.md index c57b053..abc747e 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.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–G(亮色主题、移动端卡片化、搜索增强、动效与系统偏好、动态标题、其他细节)分批落地。 +> **v1.18(2026-08-17)· UI/UE 第三轮多角度深化(rant `2026-08-17T18:06:09`,分批 PR).** **A. 仪表盘数据可视化 ✓(PR #48)**:纯 SVG sparkline(零外部依赖,手写 path,颜色用现有 CSS 变量):「本月点数变化」卡片按天聚合近 7 日净变化画迷你折线图(渐变填充,每点 `<title>` 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`**;两主题对比度均达标(正文/次要文字/边框层次清晰)。其余 B–G(亮色主题、移动端卡片化、搜索增强、动效与系统偏好、动态标题、其他细节)分批落地。 > **v1.12(2026-08-17)· 移除「组织设置 Organization settings」表单.** 组织管理页只保留部门列表(部门 CRUD + 每月点数分配),删除底部「组织设置」表单(组织名称 / 默认成员配额 / 开关)。「关闭外部注册」属**部署配置项,不在 UI 中设置**(US-16 标注);「成员自助申请加额需管理员审批」开关移除后,申请加额流程**默认需审批**(US-20 说明)。 > **v1.11(2026-08-17)· 管理视图移除独立「Key 池」管理,统一走「上架 key」.** 管理员与普通用户一样通过**共享管理页「上架新 key」**(选厂商 → Plan → 模型、填 key、声明额度、可用时间段)配置上游 key——企业场景下管理员上架、员工消费,本质上就是 key 池;不再有独立的管理员 Key 池界面。移除 Admin「Key 池」tab(成员管理 / 用量报表 / 组织管理 / 平台运营保留)。「Key 池」保留为**数据层概念**(平台持有的上游 key 集合,见 docs/architecture.md 4.2)。更新 US-12 / 企业流程 / 对齐表。 > **v1.10(2026-08-17)· 上架表单"可用时间段"结构化.** 上架 key 的可用时间段改为**正式字段**(星期多选 + 起止时间,可多选;留空 = 全天不限),后端可解析生效;**备注只作纯文本**,不承载任何逻辑。共享列表展示可用时间段(如「周一~周五 09:00-18:00」),未设置显示「全天」。更新 US-8 / US-9 AC。 diff --git a/ui/README.md b/ui/README.md index 99a66ff..63e32bc 100644 --- a/ui/README.md +++ b/ui/README.md @@ -129,6 +129,11 @@ ui/ - **使用**:`lastDayLabels(n)` 生成近 n 天 MM-DD 标签;`dailySeries(days, filter)` 按天聚合交易点数;仪表盘「本月点数变化」画近 7 日净变化折线(渐变填充),「我的共享」画收益累计趋势(`--ok` 色,无数据保留空状态); - 样式 `.sparkline`(宽 100% / 高 38px)。 +## 主题约定(v1.18,rant 2026-08-17T18:06:09 B 亮色主题) + +- **双主题**:`:root`(深色默认)与 `:root[data-theme="light"]` 重定义全套颜色变量;**禁止硬编码颜色**——表格行边框/斑马纹、卡片 hover 阴影、确认态、spinner、遮罩均用语义变量(`--table-row-border` / `--table-stripe` / `--card-hover-border` / `--card-shadow` / `--danger-soft` / `--danger-text` / `--spin-track` / `--overlay`); +- **切换**:侧边栏 `#theme-toggle`(日/月 SVG 图标按主题显隐)→ 设 `document.documentElement.dataset.theme`,`localStorage["atp-theme"]` 记忆;首次加载无记忆时尊重 `prefers-color-scheme`。 + ## 数据说明 - 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙) diff --git a/ui/css/style.css b/ui/css/style.css index 77c8a49..77b374a 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -18,6 +18,14 @@ --ok: #3ecf8e; --warn: #e5b45b; --danger: #e5666a; + --danger-soft: rgba(229, 102, 106, 0.15); + --danger-text: #ff9d9f; + --card-hover-border: #3a4a63; + --card-shadow: 0 12px 30px rgba(0,0,0,.38); + --table-row-border: rgba(42,51,66,.5); + --table-stripe: rgba(255,255,255,.018); + --spin-track: rgba(255,255,255,.35); + --overlay: rgba(14,17,22,.72); --radius: 12px; --radius-sm: 8px; --sidebar-w: 240px; @@ -26,6 +34,33 @@ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } +/* 亮色主题(rant 18:06:09 B:CSS 变量重构,侧边栏切换 + localStorage 记忆 + prefers-color-scheme) */ +:root[data-theme="light"] { + --bg: #f4f6fa; + --bg-soft: #e9edf3; + --bg-card: #ffffff; + --bg-hover: #e2e8f0; + --border: #d5dde8; + --text: #1c2330; + --text-dim: #55627a; + --text-mute: #8a94a6; + --accent: #159a92; + --accent-soft: rgba(21, 154, 146, 0.12); + --accent-text: #0f7f78; + --ok: #128a5d; + --warn: #b07d1e; + --danger: #cf4550; + --danger-soft: rgba(207, 69, 80, 0.12); + --danger-text: #cf4550; + --card-hover-border: #c4cdda; + --card-shadow: 0 12px 30px rgba(40,55,80,.14); + --table-row-border: rgba(150,165,185,.4); + --table-stripe: rgba(25,40,60,.025); + --spin-track: rgba(0,0,0,.25); + --overlay: rgba(20,26,36,.5); + --shadow: 0 8px 24px rgba(40,55,80,.12); +} + * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } @@ -87,9 +122,9 @@ body { /* 行内二次确认(rant 16:57:17 A:按钮变「确认删除?」红色态,3 秒无操作还原) */ .btn.confirming { - background: rgba(229, 102, 106, 0.15); + background: var(--danger-soft); border-color: var(--danger); - color: #ff9d9f; + color: var(--danger-text); font-weight: 700; } /* 行内编辑容器(输入框 + 确认/取消,Enter 确认 / Esc 取消) */ @@ -169,7 +204,7 @@ select.input option { background: var(--bg-card); } .card h3 { font-size: 15px; margin-bottom: 14px; color: var(--text); } /* 卡片 hover 微交互(rant 15:50:05 A.3:阴影/边框轻提升) */ -.card:hover { border-color: #3a4a63; box-shadow: 0 12px 30px rgba(0,0,0,.38); } +.card:hover { border-color: var(--card-hover-border); box-shadow: var(--card-shadow); } .card-grid { display: grid; @@ -217,13 +252,13 @@ select.input option { background: var(--bg-card); } border-bottom: 1px solid var(--border); white-space: nowrap; } -.table td { padding: 10px 12px; border-bottom: 1px solid rgba(42,51,66,.5); white-space: nowrap; } +.table td { padding: 10px 12px; border-bottom: 1px solid var(--table-row-border); white-space: nowrap; } /* 数字列(rant 16:57:17 C:价格/点数/已用/额度/余额 右对齐 + 等宽字体,便于扫读) */ .table .num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; } .table th.num { text-align: right; } .table th.num .th-sort { text-align: right; } /* 斑马纹(rant 15:50:05 A.3:行 hover 高亮保留,hover 覆盖斑马纹) */ -.table tbody tr:nth-child(even) { background: rgba(255,255,255,.018); } +.table tbody tr:nth-child(even) { background: var(--table-stripe); } .table tbody tr:hover { background: var(--bg-hover); } .badge { @@ -431,6 +466,26 @@ select.input option { background: var(--bg-card); } padding: 14px 12px; } .sidebar-top { display: flex; align-items: center; gap: 10px; padding: 2px 6px 14px; border-bottom: 1px solid var(--border); } +.brand-text { flex: 1; min-width: 0; } +.brand-text strong { font-size: 15px; display: block; } +.brand-text span { color: var(--text-mute); font-size: 11px; } +/* 主题切换按钮(rant 18:06:09 B:日/月 SVG 图标,按当前主题显隐) */ +.theme-toggle { + flex-shrink: 0; + width: 30px; height: 30px; + display: inline-flex; align-items: center; justify-content: center; + border: 1px solid var(--border); + background: var(--bg-soft); + color: var(--text-dim); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all .15s; +} +.theme-toggle:hover { color: var(--accent-text); border-color: var(--accent); } +.theme-toggle svg { width: 15px; height: 15px; } +.theme-toggle .ico-moon { display: none; } +:root[data-theme="light"] .theme-toggle .ico-sun { display: none; } +:root[data-theme="light"] .theme-toggle .ico-moon { display: block; } .brand-text { display: flex; flex-direction: column; line-height: 1.3; } .brand-text strong { font-size: 14px; } .brand-text span { color: var(--text-mute); font-size: 11px; } @@ -503,7 +558,7 @@ select.input option { background: var(--bg-card); } .modal-overlay { position: fixed; inset: 0; - background: rgba(14, 17, 22, .72); + background: var(--overlay); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 24px; } @@ -562,7 +617,7 @@ select.input option { background: var(--bg-card); } .spin { display: inline-block; width: 12px; height: 12px; - border: 2px solid rgba(255,255,255,.35); + border: 2px solid var(--spin-track); border-top-color: currentColor; border-radius: 50%; vertical-align: -2px; diff --git a/ui/index.html b/ui/index.html index 3138ff7..986be16 100644 --- a/ui/index.html +++ b/ui/index.html @@ -44,6 +44,10 @@ <h1>AITokenPool</h1> <strong>AITokenPool</strong> <span id="mode-label">共享市场 · 角色视图</span> </div> + <button type="button" class="theme-toggle" id="theme-toggle" title="切换亮色 / 深色主题"> + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="ico-sun" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg> + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="ico-moon" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> + </button> </div> <nav class="nav" id="nav"></nav> diff --git a/ui/js/app.js b/ui/js/app.js index 5c864cf..baa7f9b 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1594,6 +1594,19 @@ renderView("dashboard"); $("#side-balance").textContent = D.fmt(D.USER.balance); + // 主题(rant 18:06:09 B):localStorage 记忆,首次加载尊重 prefers-color-scheme + const savedTheme = (() => { try { return localStorage.getItem("atp-theme"); } catch (e) { return null; } })(); + const initialTheme = savedTheme === "light" || savedTheme === "dark" + ? savedTheme + : (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"); + document.documentElement.dataset.theme = initialTheme; + $("#theme-toggle").addEventListener("click", () => { + const next = document.documentElement.dataset.theme === "light" ? "dark" : "light"; + document.documentElement.dataset.theme = next; + try { localStorage.setItem("atp-theme", next); } catch (e) { /* 隐私模式忽略 */ } + toast("已切换为" + (next === "light" ? "亮色" : "深色") + "主题", "info"); + }); + // 全局快捷键(rant 16:57:17 D):/ 聚焦市场搜索;数字 1-7 切换侧边栏视图;Esc 关闭行内新建 key document.addEventListener("keydown", (e) => { const t = e.target;