From 7f466cd4653f847d11646f81e15784a989fe149a Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 17 Aug 2026 18:19:12 +0800 Subject: [PATCH] feat(ui): mobile card layout for tables at 560px (v1.18 C) --- docs/user-stories.md | 2 +- ui/README.md | 5 +++ ui/css/style.css | 40 +++++++++++++++++++++ ui/js/app.js | 85 ++++++++++++++++++++++---------------------- 4 files changed, 89 insertions(+), 43 deletions(-) diff --git a/docs/user-stories.md b/docs/user-stories.md index abc747e..3d6bb86 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. 亮色主题 ✓(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.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`**;两主题对比度均达标(正文/次要文字/边框层次清晰)。**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)。其余 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 63e32bc..7ca0dd4 100644 --- a/ui/README.md +++ b/ui/README.md @@ -134,6 +134,11 @@ ui/ - **双主题**:`: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`。 +## 移动端表格卡片化约定(v1.18,rant 2026-08-17T18:06:09 C) + +- **@media (max-width: 560px)**:`.table thead` 隐藏,行 → 卡片(border + radius + 间距),`td` 变 `label: value` 两栏(`td::before { content: attr(data-label) }`),操作按钮整行宽换行; +- **所有表格 td 必须带 `data-label`**(市场/共享/交易/设置 API Key/成员/部门/运营者/加额申请);`buildDataTable` 动态列自动用 `col.title` 作 label。 + ## 数据说明 - 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙) diff --git a/ui/css/style.css b/ui/css/style.css index 77b374a..d01dc87 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -641,4 +641,44 @@ select.input option { background: var(--bg-card); } .stat-grid { grid-template-columns: 1fr 1fr; } .card-grid { grid-template-columns: 1fr; } .api-key-card { margin-top: 12px; } + /* 移动端表格卡片化(rant 18:06:09 C:隐藏表头,行 → label:value 卡片,不再横滚) */ + .table { display: block; } + .table thead { display: none; } + .table tbody { display: block; } + .table tbody tr { + display: block; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 4px 12px; + margin-bottom: 10px; + background: var(--bg-card); + } + .table tbody tr:nth-child(even) { background: var(--bg-card); } + .table tbody tr:hover { background: var(--bg-hover); } + .table tbody td { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 7px 0; + border-bottom: 1px solid var(--table-row-border); + text-align: right; + white-space: normal; + } + .table tbody tr > td:last-child { border-bottom: none; } + .table tbody td::before { + content: attr(data-label); + color: var(--text-mute); + font-size: 12px; + font-weight: 500; + flex-shrink: 0; + text-align: left; + } + .table tbody td[data-label=""]::before { display: none; } + /* 操作按钮整行宽 */ + .table tbody td:last-child { flex-wrap: wrap; } + .table tbody td .btn { flex: 1 1 45%; } + .pager-size { display: none; } + .empty-cell { display: block; text-align: center; } + .empty-cell .empty-state { padding: 8px 0; } } diff --git a/ui/js/app.js b/ui/js/app.js index baa7f9b..1fa730a 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -356,12 +356,12 @@ $("#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 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>') + + "<tr><td data-label='厂商'>" + esc(m.provider) + "</td><td data-label='模型'><strong>" + esc(m.model) + "</strong></td>" + + '<td class="num" data-label="输入价 /1M">' + D.fmt(m.in) + " 点</td>" + '<td class="num" data-label="输出价 /1M">' + D.fmt(m.out) + " 点</td>" + + '<td class="num" data-label="上下文">' + D.ctxFmt(m.ctx) + "</td>" + + "<td data-label='可用性'>" + (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>" + + "<td data-label='操作'><button class='btn btn-primary' style='padding:4px 10px;font-size:12px' data-use-model='" + m.id + "'" + (m.avail ? "" : " disabled") + ">使用 / 消费</button>" + "<div class='muted' style='margin-top:4px;font-size:12px'>成功率 " + m.success + "%</div></td></tr>" ).join("") : emptyRow(7, "没有匹配的模型", "试试调整搜索或筛选条件", '<button type="button" class="btn btn-ghost" data-mk-clear-filters>清除筛选</button>'); @@ -444,15 +444,15 @@ } $("#share-body").innerHTML = D.SHARINGS.length ? D.SHARINGS.map((s, i) => - "<tr><td><strong>" + esc(D.PROVIDER_LABELS[s.provider] || s.provider) + " · " + esc(s.plan || "API") + + "<tr><td data-label='厂商 · Plan / 模型'><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 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'>" + + "<td data-label='Key' class='mono'>" + esc(maskKey(s.key)) + "</td>" + + "<td data-label='已用/额度' class='num'>" + D.fmt(s.used) + " / " + D.fmt(s.quota) + "</td>" + + '<td class="num" data-label="单价">' + D.fmt(s.price) + " 点/1M</td>" + + '<td class="num" data-label="收益">+' + D.fmt(s.earned) + " 点</td>" + + "<td data-label='上架时间'>" + timeCell(s.time) + "</td>" + + "<td data-label='状态'>" + badge(s.status, SHARE_STATUS) + "</td>" + + "<td data-label='操作'><button class='btn btn-ghost' data-share-toggle='" + i + "' style='padding:4px 10px;font-size:12px'>" + (s.status === "on" ? "暂停" : s.status === "paused" ? "恢复" : "重新上架") + "</button> " + "<button class='btn btn-danger' data-share-delete='" + i + "' style='padding:4px 10px;font-size:12px'>删除</button></td></tr>" ).join("") : emptyRow(8, "还没有上架的 key", "把闲置 key 放进池子,开始赚点数", @@ -672,11 +672,11 @@ if (!el) return; 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:12px'>" + esc(r.email) + "</div></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" + "<tr><td data-label='成员'><strong>" + esc(r.user) + "</strong><div class='muted' style='font-size:12px'>" + esc(r.email) + "</div></td>" + + '<td class="num" data-label="申请点数">+' + D.fmt(r.amount) + " 点</td>" + + "<td data-label='原因'>" + esc(r.reason) + "</td>" + + "<td data-label='状态'>" + badge(r.status, RAISE_STATUS) + "</td>" + + "<td data-label='操作'>" + (r.status === "pending" ? "<button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-raise-approve='" + i + "'>批准</button> " + "<button class='btn btn-danger' style='padding:4px 10px;font-size:12px' data-raise-reject='" + i + "'>驳回</button>" : '<span class="muted" style="font-size:12px">' + timeCell(r.time) + "</span>") + "</td></tr>" @@ -820,7 +820,8 @@ pageRows.forEach((row) => { html += "<tr>"; columns.forEach((col) => { - html += "<td" + (col.align === "num" ? ' class="num"' : "") + ">" + (col.render ? col.render(row) : esc(row[col.key] == null ? "" : row[col.key])) + "</td>"; + html += "<td" + (col.align === "num" ? ' class="num"' : "") + ' data-label="' + esc(col.title) + '">' + + (col.render ? col.render(row) : esc(row[col.key] == null ? "" : row[col.key])) + "</td>"; }); html += "</tr>"; }); @@ -894,12 +895,12 @@ const q = ($("#ak-search").value || "").toLowerCase(); const list = D.API_KEYS.filter((k) => !q || k.name.toLowerCase().includes(q)); $("#api-keys").innerHTML = list.length ? list.map((k, i) => - "<tr><td><strong>" + esc(k.name) + "</strong></td>" + - "<td><code>" + esc(maskAtk(k.id)) + "</code></td>" + - "<td>" + esc(k.created) + "</td>" + - "<td>" + timeCell(k.last) + "</td>" + - "<td>" + (k.status === "active" ? '<span class="badge ok">启用</span>' : '<span class="badge dim">' + esc(k.status || "—") + "</span>") + "</td>" + - "<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-key-copy='" + i + "'>复制</button> " + + "<tr><td data-label='名字'><strong>" + esc(k.name) + "</strong></td>" + + "<td data-label='Key'><code>" + esc(maskAtk(k.id)) + "</code></td>" + + "<td data-label='创建时间'>" + esc(k.created) + "</td>" + + "<td data-label='最近使用'>" + timeCell(k.last) + "</td>" + + "<td data-label='状态'>" + (k.status === "active" ? '<span class="badge ok">启用</span>' : '<span class="badge dim">' + esc(k.status || "—") + "</span>") + "</td>" + + "<td data-label='操作'><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-key-copy='" + i + "'>复制</button> " + "<button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-key-rename='" + i + "'>改名</button> " + "<button class='btn btn-danger' style='padding:4px 10px;font-size:12px' data-key-del='" + i + "'>删除</button></td></tr>" ).join("") : emptyRow(6, "还没有 API Key", "生成一个 key,用于本地工具 / 代码接入平台", @@ -1004,12 +1005,12 @@ stat("剩余 Remain", D.fmt(total - used) + " 点", "按成员分配"), ].join(""); $("#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 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> " + + "<tr data-emp-row='" + i + "'><td data-label='成员'><strong>" + esc(e.name) + "</strong></td>" + + "<td data-label='部门'>" + (e.dept ? esc(e.dept) : '<span class="muted">未分配</span>') + "</td>" + + '<td class="num" data-label="配额 / 已用">' + D.fmt(e.used) + " / " + D.fmt(e.quota) + "</td>" + + '<td class="num" data-label="剩余">' + D.fmt(e.quota - e.used) + "</td>" + + "<td data-label='状态'>" + (e.used / e.quota > 0.9 ? '<span class="badge warn">接近限额</span>' : '<span class="badge ok">正常</span>') + "</td>" + + "<td data-label='操作'><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>" ).join(""); pulseTbody($("#emp-body")); @@ -1095,13 +1096,13 @@ const used = deptUsed(d); 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 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> " + + return "<tr><td data-label='部门'><strong>" + esc(d.name) + "</strong></td>" + + '<td class="num" data-label="成员数">' + deptMemberCount(d.name) + " 人</td>" + + '<td class="num" data-label="月分配(点数)">' + D.fmt(d.quota) + " 点</td>" + + '<td class="num" data-label="已用">' + D.fmt(used) + " 点</td>" + + '<td class="num" data-label="剩余">' + D.fmt(d.quota - used) + " 点</td>" + + "<td data-label='状态'>" + st + "</td>" + + "<td data-label='操作'><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>"; }).join("") : emptyRow(7, "没有匹配的部门", "试试调整搜索关键词", '<button type="button" class="btn btn-ghost" data-dept-clear-search>清除搜索</button>'); @@ -1188,10 +1189,10 @@ ].join(""); $("#ops-body").innerHTML = list.length ? list.map((u) => - "<tr><td><strong>" + esc(u.name) + "</strong></td>" + - "<td>" + esc(u.email) + "</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>" + "<tr><td data-label='用户'><strong>" + esc(u.name) + "</strong></td>" + + "<td data-label='邮箱'>" + esc(u.email) + "</td>" + + '<td class="num" data-label="余额(点数)">' + D.fmt(u.balance) + " 点</td>" + + "<td data-label='操作'><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-ops-topup='" + u.id + "'>充值点数</button></td></tr>" ).join("") : emptyRow(4, "没有匹配的用户", "试试其他用户名 / 邮箱"); pulseTbody($("#ops-body")); }