From 0d5e1633399cacb33ccd4ff1152302be63235798 Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 17 Aug 2026 17:10:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20relative=20time=20display=20?= =?UTF-8?q?=E2=80=94=20timeAgo=20helper=20across=20lists=20(v1.17=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 | 3 +++ ui/index.html | 2 +- ui/js/app.js | 42 ++++++++++++++++++++++++++++++++++++++---- ui/js/data.js | 8 ++++---- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/user-stories.md b/docs/user-stories.md index 08d2f90..3a342e5 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–G(相对时间、数字列对齐、`:focus-visible` 与快捷键、行内校验错误、视图过渡、细节一致性)分批落地。 +> **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–G(数字列对齐、`:focus-visible` 与快捷键、行内校验错误、视图过渡、细节一致性)分批落地。 > **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 ef6aa4a..9783dd0 100644 --- a/ui/README.md +++ b/ui/README.md @@ -86,6 +86,11 @@ ui/ - **禁用原生弹窗**:`grep ui/` 不得出现原生 confirm/prompt 调用;确认走 `confirmInline(btn, onConfirm, text)`(按钮变「确认删除?」红色态 `.confirming`,3 秒无操作或 Esc 还原,再次点击执行),输入走 `inlineForm(cell, opts)`(行内展开 input + 确认/取消,Enter 确认 / Esc 取消,`opts.validate` 返回错误文案时 toast + 重新聚焦); - 已覆盖:key 删除、共享下架、部门删除(confirmInline);API Key 新建/改名、运营者充值、成员充值(inlineForm);新建 key 行内输入框在 `#ak-new-inline`(Enter/Esc 绑定)。 +## 时间显示约定(v1.17,rant 2026-08-17T16:57:17 B 相对时间) + +- **相对时间**:`timeAgo(s)` 支持 `MM-DD HH:mm`(默认今年)与 `YYYY-MM-DD[ HH:mm]`,输出 `刚刚 / N 分钟前 / N 小时前 / 昨天 / MM-DD`,非标准格式原样返回;`timeCell(s)` 输出带 `title`(完整绝对时间)的 `.timeago` 单元格,hover 显示; +- **统一使用**:交易列表(时间列)、加额申请列表、共享列表(上架时间列)、API Key 最近使用时间;数据新增 / 生成时间用 `nowTime()`(`MM-DD HH:mm`)写入即可自动相对化。 + ## 数据说明 - 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙) diff --git a/ui/css/style.css b/ui/css/style.css index 888a090..3f2118e 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -83,6 +83,9 @@ body { .inline-edit { display: inline-flex; gap: 6px; align-items: center; vertical-align: middle; } .inline-edit .input { width: auto; } +/* 相对时间(rant 16:57:17 B):hover 提示完整绝对时间 */ +.timeago { white-space: nowrap; cursor: help; } + .input, select.input { width: 100%; background: var(--bg-soft); diff --git a/ui/index.html b/ui/index.html index 84f7bb2..c07fc0a 100644 --- a/ui/index.html +++ b/ui/index.html @@ -183,7 +183,7 @@

我的共享列表 My listings

收益已到账

- +
厂商 · Plan / 模型Key已用/额度单价收益状态
厂商 · Plan / 模型Key已用/额度单价收益上架时间状态
diff --git a/ui/js/app.js b/ui/js/app.js index 270417b..43ba154 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -399,11 +399,12 @@ "" + D.fmt(s.used) + " / " + D.fmt(s.quota) + "" + "" + D.fmt(s.price) + " 点/1M" + "+" + D.fmt(s.earned) + " 点" + + "" + timeCell(s.time) + "" + "" + badge(s.status, SHARE_STATUS) + "" + " " + "" - ).join("") : emptyRow(7, "还没有上架的 key", "把闲置 key 放进池子,开始赚点数", + ).join("") : emptyRow(8, "还没有上架的 key", "把闲置 key 放进池子,开始赚点数", ''); } @@ -551,7 +552,7 @@ "" + (r.status === "pending" ? " " + "" - : '' + esc(r.time) + "") + "" + : '' + timeCell(r.time) + "") + "" ).join("") + "" : emptyState("暂无加额申请", "成员提交的加额申请会显示在这里,批准后自动加点数"); } @@ -588,7 +589,7 @@ }; const TX_COLUMNS = [ - { key: "time", title: "时间", sort: "string", filter: "text" }, + { key: "time", title: "时间", sort: "string", filter: "text", render: (t) => timeCell(t.time) }, { key: "type", title: "类型", sort: "string", filter: "select", options: ["消费", "收益", "充值", "提现", "赠送"], filterVal: (t) => TX_TYPE[t.type] || t.type, render: (t) => t.type === "earn" ? '收益' : t.type === "consume" ? '消费' : t.type === "gift" ? '赠送' : '' + esc(TX_TYPE[t.type] || t.type) + "" }, { key: "partner", title: "模型 / Key", sort: "string", filter: "text" }, @@ -769,7 +770,7 @@ "" + esc(k.name) + "" + "" + esc(maskAtk(k.id)) + "" + "" + esc(k.created) + "" + - "" + esc(k.last) + "" + + "" + timeCell(k.last) + "" + "" + (k.status === "active" ? '启用' : '' + esc(k.status || "—") + "") + "" + " " + " " + @@ -1098,6 +1099,39 @@ return p(n.getMonth() + 1) + "-" + p(n.getDate()) + " " + p(n.getHours()) + ":" + p(n.getMinutes()); } + // 相对时间(rant 16:57:17 B):刚刚 / N 分钟前 / N 小时前 / 昨天 / MM-DD + // 支持 "MM-DD HH:mm"(默认今年)与 "YYYY-MM-DD[ HH:mm]" 两种格式;非标准格式原样返回 + function timeAgo(s) { + if (!s) return ""; + const p2 = (x) => String(x).padStart(2, "0"); + const full = String(s); + const m = full.match(/^(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/) || full.match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}))?$/); + if (!m) return full; + const isFull = m[1].length === 4; // "YYYY-MM-DD" vs "MM-DD HH:mm"(分组语义不同) + const MM = isFull ? +m[2] : +m[1]; + const DD = isFull ? +m[3] : +m[2]; + const HH = isFull ? +(m[4] || 0) : +m[3]; + const mm = isFull ? +(m[5] || 0) : +m[4]; + const now = new Date(); + const y = isFull ? +m[1] : now.getFullYear(); + const d = new Date(y, MM - 1, DD, HH, mm); + if (isNaN(d.getTime())) return full; + const min = Math.floor((now - d) / 60000); + if (min < 1) return "刚刚"; + if (min < 60) return min + " 分钟前"; + if (min < 60 * 24) return Math.floor(min / 60) + " 小时前"; + const dayDiff = Math.floor( + (new Date(now.getFullYear(), now.getMonth(), now.getDate()) - new Date(y, MM - 1, DD)) / 86400000); + if (dayDiff === 1) return "昨天"; + return p2(MM) + "-" + p2(DD); // MM-DD + } + + // 时间单元格:相对时间展示 + title 悬停显示完整绝对时间 + function timeCell(s) { + if (!s) return ""; + return '' + esc(timeAgo(s)) + ""; + } + function openChat(id) { const m = D.MARKET.find((x) => x.id === id); if (!m) return; diff --git a/ui/js/data.js b/ui/js/data.js index 071824f..559badb 100644 --- a/ui/js/data.js +++ b/ui/js/data.js @@ -100,10 +100,10 @@ // provider + plan:上架时选择「厂商 → Plan → 模型」,列表展示「厂商 · Plan」 // available:结构化可用时间段(days 星期 1-7 + start/end 起止;null = 全天不限) SHARINGS: [ - { id: 1, provider: "zhipu", plan: "GLM Coding Plan", model: "glm-5.2", quota: 100000, used: 32800, price: 28, earned: 1611, status: "on", key: "sk-zhipu-9f2c41ab7d22", available: { days: [1, 2, 3, 4, 5], start: "09:00", end: "18:00" } }, - { id: 2, provider: "deepseek", plan: "API(按量)", model: "deepseek-v4-flash", quota: 80000, used: 51200, price: 2, earned: 1116, status: "on", key: "sk-ds-3b88d077aa91", available: null }, - { id: 3, provider: "moonshot", plan: "Kimi Code 会员", model: "kimi-k3", quota: 50000, used: 800, price: 100, earned: 54, status: "paused", key: "sk-ms-c31f2e8b4405", available: { days: [6, 7], start: "10:00", end: "22:00" } }, - { id: 4, provider: "minimax", plan: "Coding Plan", model: "minimax-m3", quota: 20000, used: 20000, price: 9, earned: 432, status: "off", key: "sk-mx-77d2a1c9e356", available: null }, + { id: 1, provider: "zhipu", plan: "GLM Coding Plan", model: "glm-5.2", quota: 100000, used: 32800, price: 28, earned: 1611, status: "on", time: "08-17 09:30", key: "sk-zhipu-9f2c41ab7d22", available: { days: [1, 2, 3, 4, 5], start: "09:00", end: "18:00" } }, + { id: 2, provider: "deepseek", plan: "API(按量)", model: "deepseek-v4-flash", quota: 80000, used: 51200, price: 2, earned: 1116, status: "on", time: "08-16 20:15", key: "sk-ds-3b88d077aa91", available: null }, + { id: 3, provider: "moonshot", plan: "Kimi Code 会员", model: "kimi-k3", quota: 50000, used: 800, price: 100, earned: 54, status: "paused", time: "08-15 14:00", key: "sk-ms-c31f2e8b4405", available: { days: [6, 7], start: "10:00", end: "22:00" } }, + { id: 4, provider: "minimax", plan: "Coding Plan", model: "minimax-m3", quota: 20000, used: 20000, price: 9, earned: 432, status: "off", time: "08-10 11:20", key: "sk-mx-77d2a1c9e356", available: null }, ], // 市场在售 key(公共版浏览;multi=true 表示该模型配置多个上游 key → 自动故障转移,架构 v0.2 路由策略 G5)