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
2 changes: 1 addition & 1 deletion docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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–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. 数字列对齐 ✓(PR #43)**:价格 / 点数 / 已用 / 额度 / 余额等**数字列统一右对齐 + 等宽字体**(`--mono` + `tabular-nums`,新增 `.table .num` 类,覆盖市场输入/输出价与上下文、交易 Token 用量/点数、共享已用/额度·单价·收益、成员配额/剩余、部门成员数·月分配·已用·剩余、运营者余额、加额申请点数),时间列 / 状态列保持左对齐;金额统一 `D.fmt`(整数千分位、小数 2 位)全覆盖检查无遗漏。其余 D–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。
Expand Down
6 changes: 6 additions & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ ui/
- **相对时间**:`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`)写入即可自动相对化。

## 表格数字列约定(v1.17,rant 2026-08-17T16:57:17 C 数字列对齐)

- **数字列**(价格 / 点数 / 已用 / 额度 / 余额 / 用量)统一 `<td class="num">` + 表头 `<th class="num">`:右对齐 + `--mono` 等宽 + `tabular-nums`,便于纵向扫读;时间列 / 状态列 / 文本列保持左对齐;
- 数据表格渲染器 `buildDataTable` 支持列配置 `align: "num"`(交易表 tokens / pts 已启用);
- 金额一律 `D.fmt()`(整数千分位;小数保留 2 位),禁止裸数字拼接。

## 数据说明

- 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙)
Expand Down
4 changes: 4 additions & 0 deletions ui/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ select.input option { background: var(--bg-card); }
white-space: nowrap;
}
.table td { padding: 10px 12px; border-bottom: 1px solid rgba(42,51,66,.5); 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:hover { background: var(--bg-hover); }
Expand Down
10 changes: 5 additions & 5 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ <h2>模型市场 <span class="en">Marketplace</span></h2>
<div class="table-wrap">
<table class="table">
<thead>
<tr><th>厂商 Provider</th><th>模型 Model</th><th>输入价 /1M</th><th>输出价 /1M</th><th>上下文</th><th>可用性</th><th>操作 <span class="en">Use</span></th></tr>
<tr><th>厂商 Provider</th><th>模型 Model</th><th class="num">输入价 /1M</th><th class="num">输出价 /1M</th><th class="num">上下文</th><th>可用性</th><th>操作 <span class="en">Use</span></th></tr>
</thead>
<tbody id="mk-body"></tbody>
</table>
Expand Down Expand Up @@ -183,7 +183,7 @@ <h3>我的共享列表 <span class="en">My listings</span></h3>
<p class="muted">收益已到账</p>
<div class="table-wrap compact">
<table class="table">
<thead><tr><th>厂商 · Plan / 模型</th><th>Key</th><th>已用/额度</th><th>单价</th><th>收益</th><th>上架时间</th><th>状态</th><th></th></tr></thead>
<thead><tr><th>厂商 · Plan / 模型</th><th>Key</th><th class="num">已用/额度</th><th class="num">单价</th><th class="num">收益</th><th>上架时间</th><th>状态</th><th></th></tr></thead>
<tbody id="share-body"></tbody>
</table>
</div>
Expand Down Expand Up @@ -347,7 +347,7 @@ <h2>管理视图 <span class="en">Admin</span></h2>
<div class="card">
<div class="table-wrap compact">
<table class="table">
<thead><tr><th>成员</th><th>部门</th><th>配额 / 已用</th><th>剩余</th><th>状态</th><th></th></tr></thead>
<thead><tr><th>成员</th><th>部门</th><th class="num">配额 / 已用</th><th class="num">剩余</th><th>状态</th><th></th></tr></thead>
<tbody id="emp-body"></tbody>
</table>
</div>
Expand Down Expand Up @@ -381,7 +381,7 @@ <h3>按成员用量 <span class="en">By member</span></h3>
<h3>部门列表 <span class="en">Departments</span></h3>
<div class="table-wrap compact">
<table class="table">
<thead><tr><th>部门</th><th>成员数</th><th>月分配(点数)</th><th>已用</th><th>剩余</th><th>状态</th><th></th></tr></thead>
<thead><tr><th>部门</th><th class="num">成员数</th><th class="num">月分配(点数)</th><th class="num">已用</th><th class="num">剩余</th><th>状态</th><th></th></tr></thead>
<tbody id="dept-body"></tbody>
</table>
</div>
Expand Down Expand Up @@ -416,7 +416,7 @@ <h3>用户充值 <span class="en">Top up a user</span></h3>
</div>
<div class="table-wrap compact">
<table class="table">
<thead><tr><th>用户</th><th>邮箱</th><th>余额(点数)</th><th></th></tr></thead>
<thead><tr><th>用户</th><th>邮箱</th><th class="num">余额(点数)</th><th></th></tr></thead>
<tbody id="ops-body"></tbody>
</table>
</div>
Expand Down
36 changes: 18 additions & 18 deletions ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@
$("#mk-count").textContent = list.length + " 个在售 key";
$("#mk-body").innerHTML = list.length ? list.map((m) =>
"<tr><td>" + esc(m.provider) + "</td><td><strong>" + esc(m.model) + "</strong></td>" +
"<td>" + D.fmt(m.in) + " 点</td><td>" + D.fmt(m.out) + " 点</td>" +
"<td>" + D.ctxFmt(m.ctx) + "</td>" +
'<td class="num">' + D.fmt(m.in) + " 点</td>" + '<td class="num">' + D.fmt(m.out) + " 点</td>" +
'<td class="num">' + D.ctxFmt(m.ctx) + "</td>" +
"<td>" + (m.avail ? '<span class="badge ok">可用</span>' : '<span class="badge warn">繁忙</span>') +
(m.multi ? ' <span class="badge ok" title="该模型配置多个上游 key,单个 key 不可用时自动故障转移(架构 v0.2 路由策略)">多 key · 自动故障转移</span>' : "") + "</td>" +
"<td><button class='btn btn-primary' style='padding:4px 10px;font-size:12px' data-use-model='" + m.id + "'" + (m.avail ? "" : " disabled") + ">使用 / 消费</button>" +
Expand Down Expand Up @@ -396,9 +396,9 @@
"<tr><td><strong>" + esc(D.PROVIDER_LABELS[s.provider] || s.provider) + " · " + esc(s.plan || "API") +
"</strong><div class='muted' style='font-size:12px'>" + esc(s.model) + " · " + esc(fmtAvailable(s)) + "</div></td>" +
"<td class='mono'>" + esc(maskKey(s.key)) + "</td>" +
"<td>" + D.fmt(s.used) + " / " + D.fmt(s.quota) + "</td>" +
"<td>" + D.fmt(s.price) + " 点/1M</td>" +
"<td>+" + D.fmt(s.earned) + " 点</td>" +
"<td class='num'>" + D.fmt(s.used) + " / " + D.fmt(s.quota) + "</td>" +
'<td class="num">' + D.fmt(s.price) + " 点/1M</td>" +
'<td class="num">+' + D.fmt(s.earned) + " 点</td>" +
"<td>" + timeCell(s.time) + "</td>" +
"<td>" + badge(s.status, SHARE_STATUS) + "</td>" +
"<td><button class='btn btn-ghost' data-share-toggle='" + i + "' style='padding:4px 10px;font-size:12px'>" +
Expand Down Expand Up @@ -543,10 +543,10 @@
function renderRaiseRequests() {
const el = $("#raise-requests");
if (!el) return;
el.innerHTML = D.RAISE_REQUESTS.length ? '<div class="table-wrap compact"><table class="table"><thead><tr><th>成员</th><th>申请点数</th><th>原因</th><th>状态</th><th></th></tr></thead><tbody>' +
el.innerHTML = D.RAISE_REQUESTS.length ? '<div class="table-wrap compact"><table class="table"><thead><tr><th>成员</th><th class="num">申请点数</th><th>原因</th><th>状态</th><th></th></tr></thead><tbody>' +
D.RAISE_REQUESTS.map((r, i) =>
"<tr><td><strong>" + esc(r.user) + "</strong><div class='muted' style='font-size:11px'>" + esc(r.email) + "</div></td>" +
"<td>+" + D.fmt(r.amount) + " 点</td>" +
"<td class='num'>+" + D.fmt(r.amount) + " 点</td>" +
"<td>" + esc(r.reason) + "</td>" +
"<td>" + badge(r.status, RAISE_STATUS) + "</td>" +
"<td>" + (r.status === "pending"
Expand Down Expand Up @@ -593,8 +593,8 @@
{ key: "type", title: "类型", sort: "string", filter: "select", options: ["消费", "收益", "充值", "提现", "赠送"], filterVal: (t) => TX_TYPE[t.type] || t.type,
render: (t) => t.type === "earn" ? '<span class="badge ok">收益</span>' : t.type === "consume" ? '<span class="badge accent">消费</span>' : t.type === "gift" ? '<span class="badge ok">赠送</span>' : '<span class="badge dim">' + esc(TX_TYPE[t.type] || t.type) + "</span>" },
{ key: "partner", title: "模型 / Key", sort: "string", filter: "text" },
{ key: "tokens", title: "Token 用量", sort: "string", filter: "text" },
{ key: "pts", title: "点数", sort: "number", filter: "number-range",
{ key: "tokens", title: "Token 用量", sort: "string", filter: "text", align: "num" },
{ key: "pts", title: "点数", sort: "number", filter: "number-range", align: "num",
render: (t) => '<span style="color:' + (t.pts > 0 ? "var(--ok)" : "var(--text)") + ';font-weight:600">' + (t.pts > 0 ? "+" : "") + D.fmt(t.pts) + "</span>" },
{ key: "status", title: "状态", sort: "string", filter: "select", options: ["成功", "入账", "处理中"],
render: (t) => t.status === "处理中" ? '<span class="badge warn">' + esc(t.status) + "</span>" : esc(t.status) },
Expand Down Expand Up @@ -668,7 +668,7 @@
columns.forEach((col) => {
const sk = state.sort.find((s) => s.key === col.key);
const arrow = sk ? (sk.dir === "asc" ? " ▲" : " ▼") : "";
html += '<th><button type="button" class="th-sort" data-sort-key="' + esc(col.key) + '" title="点击排序 · Shift+点击叠加多列">' +
html += '<th' + (col.align === "num" ? ' class="num"' : "") + '><button type="button" class="th-sort" data-sort-key="' + esc(col.key) + '" title="点击排序 · Shift+点击叠加多列">' +
esc(col.title) + arrow + "</button></th>";
});
html += "</tr><tr>";
Expand All @@ -693,7 +693,7 @@
pageRows.forEach((row) => {
html += "<tr>";
columns.forEach((col) => {
html += "<td>" + (col.render ? col.render(row) : esc(row[col.key] == null ? "" : row[col.key])) + "</td>";
html += "<td" + (col.align === "num" ? ' class="num"' : "") + ">" + (col.render ? col.render(row) : esc(row[col.key] == null ? "" : row[col.key])) + "</td>";
});
html += "</tr>";
});
Expand Down Expand Up @@ -878,8 +878,8 @@
$("#emp-body").innerHTML = D.EMPLOYEES.map((e, i) =>
"<tr data-emp-row='" + i + "'><td><strong>" + esc(e.name) + "</strong></td>" +
"<td>" + (e.dept ? esc(e.dept) : '<span class="muted">未分配</span>') + "</td>" +
"<td>" + D.fmt(e.used) + " / " + D.fmt(e.quota) + "</td>" +
"<td>" + D.fmt(e.quota - e.used) + "</td>" +
'<td class="num">' + D.fmt(e.used) + " / " + D.fmt(e.quota) + "</td>" +
'<td class="num">' + D.fmt(e.quota - e.used) + "</td>" +
"<td>" + (e.used / e.quota > 0.9 ? '<span class="badge warn">接近限额</span>' : '<span class="badge ok">正常</span>') + "</td>" +
"<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-emp-dept='" + i + "'>改部门</button> " +
"<button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-emp-topup='" + i + "'>充值</button></td></tr>"
Expand Down Expand Up @@ -967,10 +967,10 @@
const pct = d.quota > 0 ? used / d.quota : 0;
const st = pct >= 1 ? '<span class="badge danger">已用尽</span>' : pct > 0.9 ? '<span class="badge warn">接近限额</span>' : '<span class="badge ok">正常</span>';
return "<tr><td><strong>" + esc(d.name) + "</strong></td>" +
"<td>" + deptMemberCount(d.name) + " 人</td>" +
"<td>" + D.fmt(d.quota) + " 点</td>" +
"<td>" + D.fmt(used) + " 点</td>" +
"<td>" + D.fmt(d.quota - used) + " 点</td>" +
'<td class="num">' + deptMemberCount(d.name) + " 人</td>" +
'<td class="num">' + D.fmt(d.quota) + " 点</td>" +
'<td class="num">' + D.fmt(used) + " 点</td>" +
'<td class="num">' + D.fmt(d.quota - used) + " 点</td>" +
"<td>" + st + "</td>" +
"<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-dept-edit='" + i + "'>编辑</button> " +
"<button class='btn btn-danger' style='padding:4px 10px;font-size:12px' data-dept-del='" + i + "'>删除</button></td></tr>";
Expand Down Expand Up @@ -1054,7 +1054,7 @@
$("#ops-body").innerHTML = list.length ? list.map((u) =>
"<tr><td><strong>" + esc(u.name) + "</strong></td>" +
"<td>" + esc(u.email) + "</td>" +
"<td>" + D.fmt(u.balance) + " 点</td>" +
'<td class="num">' + D.fmt(u.balance) + " 点</td>" +
"<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-ops-topup='" + u.id + "'>充值点数</button></td></tr>"
).join("") : emptyRow(4, "没有匹配的用户", "试试其他用户名 / 邮箱");
}
Expand Down
Loading