From 872c034a80cc4f70902922da9e5ef9a5d8a07f2a Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 17 Aug 2026 17:32:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20detail=20consistency=20=E2=80=94=20?= =?UTF-8?q?inline=20errors=20in=20editors,=20unified=20phrasing=20(v1.17?= =?UTF-8?q?=20G)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/user-stories.md | 2 +- ui/README.md | 7 +++++++ ui/js/app.js | 13 +++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/user-stories.md b/docs/user-stories.md index 15dd0ac..bfca3d6 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -9,7 +9,7 @@ > **v1.15(2026-08-17)· A 视觉美化(rant `2026-08-17T15:50:05`).** 侧边栏导航 emoji → **统一内联 SVG 线性图标**(同尺寸、currentColor、hover/active 态);卡片 hover 微交互(阴影/边框轻提升);表格斑马纹 + 行 hover 高亮;**空状态组件**(图标 + 文案 + 行动按钮:市场清除筛选 / 共享上架新 key / 设置生成 key / 部门清除搜索);状态徽章加**语义色点**(ok/warn/danger/dim 一眼可辨);响应式加固(窄屏侧边栏折叠配合 SVG 图标、表格横向滚动)。 > > **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 重渲染后重启动画,动态表格整表重建自动生效)。 +> **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.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 d3d775d..a99dd69 100644 --- a/ui/README.md +++ b/ui/README.md @@ -116,6 +116,13 @@ ui/ - **视图切换**:`.view:not(.hidden)` 播放 `viewIn`(opacity 0→1 + translateY(6px)→0,150ms ease-out);每次从隐藏变为可见自动重放; - **表格更新**:`.table tbody` 播放 `tbodyIn`(opacity 0→1,150ms);整表重建(buildDataTable / 加额申请列表)新 tbody 节点自动播放;静态 tbody(market/sharing/api-keys/emp/dept/ops)在 innerHTML 更新后用 `pulseTbody(el)` 重启动画(style.animation=none → 强制 reflow → 还原)。 +## 一致性约定(v1.17,rant 2026-08-17T16:57:17 G 细节一致性) + +- **金额**:所有数字/金额一律 `D.fmt()`(整数千分位;小数 2 位),侧边栏余额 / 统计卡 / 表格 / 钱包格式一致;已用/额度统一「已用 X / 额度 Y」; +- **按钮**:表格操作按钮统一 `padding:4px 10px;font-size:12px`,主操作用默认 `.btn` 尺寸; +- **行内编辑校验**:`inlineForm` 的 `opts.validate` 失败走 `setFieldError`(红边框+行内文案),与 E 项表单校验一致,不使用 toast 承载错误; +- 子文本(邮箱/模型等)统一 `font-size:12px`;单价措辞统一「点/1M」。 + ## 数据说明 - 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙) diff --git a/ui/js/app.js b/ui/js/app.js index 900d437..c2f404b 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -135,7 +135,8 @@ const finish = () => { const val = String(input.value).trim(); const err = opts.validate ? opts.validate(val) : null; - if (err) { toast(err, "error"); input.focus(); return; } + if (err) { setFieldError(input, err); return; } + clearFieldError(input); opts.onSubmit(val); }; ok.addEventListener("click", finish); @@ -598,7 +599,7 @@ if (!el) return; el.innerHTML = D.RAISE_REQUESTS.length ? '
' + D.RAISE_REQUESTS.map((r, i) => - "" + + "" + "" + "" + "" + @@ -899,7 +900,7 @@ value: k.name, placeholder: "key 名字", width: "160px", - validate: (v) => v ? null : "名字不能为空(未生效)", + validate: (v) => v ? null : "名字不能为空", onSubmit: (name) => { k.name = name; renderSettings(); toast("已改名为「" + name + "」", "success"); }, onCancel: () => renderSettings(), }); @@ -1134,7 +1135,7 @@ width: "120px", validate: (raw) => { const amt = Math.round(Number(raw) * 100) / 100; - return (!raw || isNaN(amt) || amt <= 0) ? "请输入大于 0 的点数金额(未生效)" : null; + return (!raw || isNaN(amt) || amt <= 0) ? "请输入大于 0 的点数金额" : null; }, onSubmit: (raw) => { const amt = Math.round(Number(raw) * 100) / 100; @@ -1375,7 +1376,7 @@ renderSharing(); if (activeView === "dashboard") renderDashboard(); const label = (D.PROVIDER_LABELS[plan.provider] || plan.provider) + " · " + plan.name; - toast("已上架「" + label + " · " + model + "」(key 已加密托管,单价 " + D.fmt(price) + " 点/1M 自动)", "success"); + toast("已上架「" + label + " · " + model + "」(key 已加密托管,单价 " + D.fmt(price) + " 点/1M(自动))", "success"); e.target.reset(); const p = $("#sf-provider"); p.value = ""; p.dispatchEvent(new Event("change")); $("#sf-quota").value = 5000; @@ -1469,7 +1470,7 @@ width: "120px", validate: (raw) => { const amt = Number(raw); - return (!raw || !Number.isInteger(amt) || amt <= 0) ? "请输入正整数点数金额(未生效)" : null; + return (!raw || !Number.isInteger(amt) || amt <= 0) ? "请输入正整数点数金额" : null; }, onSubmit: (raw) => { const amt = Number(raw);
成员申请点数原因状态
" + esc(r.user) + "
" + esc(r.email) + "
" + esc(r.user) + "
" + esc(r.email) + "
+" + D.fmt(r.amount) + " 点" + esc(r.reason) + "" + badge(r.status, RAISE_STATUS) + "