diff --git a/docs/user-stories.md b/docs/user-stories.md index 3d6bb86..f515d29 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`**;两主题对比度均达标(正文/次要文字/边框层次清晰)。**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.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)。**D. 搜索增强 ✓(PR #51)**:全部搜索框(市场 `#mk-search` / API Key `#ak-search` / 部门 `#od-search` / 运营者用户 `#ops-search`)统一接入 **~150ms 输入防抖**(避免每次按键整表重绘闪烁,连续输入只渲染一次);渲染结果对匹配关键词**大小写不敏感 `<mark>` 高亮**(先 HTML 转义再包裹,`--accent-soft` 底 + `--accent-text` 字,双主题对比度达标;清空后自动清除);搜索框加**「清空 ×」小按钮**(有内容时显示,点击清空立即重绘并聚焦输入框;空状态「清除搜索/清除筛选」按钮同样同步 × 态)。其余 E–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 7ca0dd4..eb96ce9 100644 --- a/ui/README.md +++ b/ui/README.md @@ -49,12 +49,12 @@ python3 -m http.server 8000 --directory ui 同一套界面(角色视图): 1. 仪表盘 Dashboard — 点数余额、本月用量、共享收益、**本月点数变化(近 1 月按类型汇总收支:赠送/过期/收益/消费/充值/提现 + 净变化,取代静态"点数来源"分组)** -2. 模型市场 Marketplace — 模型浏览、搜索、厂商筛选、排序(按价格/上下文);可用性标注「多 key · 自动故障转移」(该模型配置多个上游 key,架构 v0.2 路由策略);每行「使用 / 消费」入口 → 聊天 Mock 模拟调用(按模型输出参考价扣小数点数,如 -0.38 点,产生消费交易;余额不足时阻断并提示) +2. 模型市场 Marketplace — 模型浏览、**搜索(输入防抖 ~150ms + 关键词 `<mark>` 高亮 + 清空 × 按钮,v1.18 D)**、厂商筛选、排序(按价格/上下文);可用性标注「多 key · 自动故障转移」(该模型配置多个上游 key,架构 v0.2 路由策略);每行「使用 / 消费」入口 → 聊天 Mock 模拟调用(按模型输出参考价扣小数点数,如 -0.38 点,产生消费交易;余额不足时阻断并提示) 3. 共享管理 Sharing — 默认只显示统计 + 我的共享列表(key 脱敏展示,可暂停/恢复/重新上架/彻底删除;列表展示「厂商 · Plan / 模型 · 可用时间段」,如「智谱 · GLM Coding Plan / glm-5.2 · 周一~周五 09:00-18:00」,未设置显示「全天」);点击"+ 添加 / 上架新 key"展开上架表单(**三级联动:厂商 → Plan → 模型**——内置国内已知 Plan 清单(阿里云百炼 / 智谱 / 火山方舟 / Kimi / MiniMax / DeepSeek,每家含「API(按量)」= 按量计价的 key),选 Plan 后显示 key 前缀 / 专属端点提示;**可用时间段为结构化字段**:星期多选 chips + 起止时间(留空 = 全天不限),备注仅纯文本;须填 API Key,平台加密托管;分享者只填声明额度,单价由平台按模型定价自动计算并展示参考价),提交成功或取消后自动收起 4. 钱包 Wallet — 点数余额、**本月点数变化(近 1 月按类型汇总收支 + 净变化,与仪表盘一致)**、**充值入口(US-4:模拟流程——输入点数 → 余额增加 → topup 交易记录;文案注明"演示,真实支付后续接入";提现仍 disabled)**、**申请加额(US-20:企业成员余额低时申请更多点数 → 提交后等待管理员审批,默认需审批)**;收支明细已去重,统一到【交易记录】(页内提供跳转提示) 5. 交易记录 Transactions — 消费/收益/充值/提现/赠送(gift)唯一明细入口,Tab 筛选 + MRT 风格表格(列排序/列筛选/分页,与 Tab 叠加生效) -6. 设置 Settings — 账户、**API Key 管理(独占整行全宽,表格字段不挤压;完整增删改查:生成带名字、改名、删除需确认「删除后该 key 立即失效」、按名字搜索、一键复制完整 key——列表脱敏展示 atk_live_****xxxx,复制为完整值;file:// 下 clipboard API 受限时降级为选中+Ctrl/Cmd+C)**、通知、偏好 -7. 管理视图 Admin(管理员 / 运营者角色专属)— 成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门;**加额申请审批(US-20:待审批列表 → 批准=成员余额+申请点数 / 驳回;申请默认需审批)**)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;**添加/编辑部门用行内展开表单(`#dept-form-card`,名称+月分配,替代 window.prompt)**,删除有确认;**无「组织设置」表单**——组织名称 / 默认成员配额 / 开关均移除,"关闭外部注册"为部署配置项不在 UI 中)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱搜索定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)。**无独立 Key 池管理**——管理员与普通用户一样通过共享管理页「上架 key」配置上游 key(共享列表即 key 池视图,可暂停/删除) +6. 设置 Settings — 账户、**API Key 管理(独占整行全宽,表格字段不挤压;完整增删改查:生成带名字、改名、删除需确认「删除后该 key 立即失效」、按名字搜索(防抖+高亮+清空,v1.18 D)、一键复制完整 key——列表脱敏展示 atk_live_****xxxx,复制为完整值;file:// 下 clipboard API 受限时降级为选中+Ctrl/Cmd+C)**、通知、偏好 +7. 管理视图 Admin(管理员 / 运营者角色专属)— 成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门;**加额申请审批(US-20:待审批列表 → 批准=成员余额+申请点数 / 驳回;申请默认需审批)**)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;**部门搜索(防抖+高亮+清空,v1.18 D)**;**添加/编辑部门用行内展开表单(`#dept-form-card`,名称+月分配,替代 window.prompt)**,删除有确认;**无「组织设置」表单**——组织名称 / 默认成员配额 / 开关均移除,"关闭外部注册"为部署配置项不在 UI 中)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱**搜索(防抖+高亮+清空,v1.18 D)**定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)。**无独立 Key 池管理**——管理员与普通用户一样通过共享管理页「上架 key」配置上游 key(共享列表即 key 池视图,可暂停/删除) ## 文件结构 @@ -139,6 +139,13 @@ ui/ - **@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。 +## 搜索增强约定(v1.18,rant 2026-08-17T18:06:09 D) + +- **统一接线 `wireSearch(input, render)`**:所有搜索框(`#mk-search` / `#ak-search` / `#od-search` / `#ops-search`)走**~150ms 输入防抖**渲染(连续输入只渲染一次,避免整表重绘闪烁)+ **「清空 ×」按钮**(有内容时显示;点击清空立即重绘并聚焦,不走防抖);HTML 结构为 `.search-box` 包裹 `<input>` + `<button class="search-clear">`; +- **关键词高亮用 `hl(text, rawQ)`**:先 `esc()` 转义再对查询词**大小写不敏感**包 `<mark>`(正则转义用户输入,`& < >` 等字符与转义正文同构不错位);无关键词返回转义原样(重置后自动清除); +- **程序化清空用 `resetSearch(input)`**(值 + × 按钮态同步,不触发渲染;调用方随后自行重绘)——空状态「清除搜索 / 清除筛选」按钮已统一走此路径; +- `mark` 样式:`--accent-soft` 底 + `--accent-text` 字,双主题对比度达标。 + ## 数据说明 - 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙) diff --git a/ui/css/style.css b/ui/css/style.css index d01dc87..a3eb5f8 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -287,6 +287,37 @@ select.input option { background: var(--bg-card); } .toolbar .input { max-width: 240px; } .toolbar-count { align-self: center; color: var(--text-mute); font-size: 12px; margin-left: auto; } +/* 搜索增强(rant 18:06:09 D:清空 × 按钮 + <mark> 关键词高亮) */ +.search-box { position: relative; display: block; } +.search-box .input { padding-right: 30px; } +.search-clear { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + padding: 0; + border: none; + border-radius: 50%; + background: var(--bg-hover); + color: var(--text-mute); + font-size: 13px; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.search-clear:hover { background: var(--border); color: var(--text); } +/* 关键词高亮:accent 柔和底色 + 主色文字,双主题对比度达标;无关键词时重置为普通文本 */ +mark { + background: var(--accent-soft); + color: var(--accent-text); + border-radius: 3px; + padding: 0 2px; +} + .tabs { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; } .tab { border: 1px solid var(--border); diff --git a/ui/index.html b/ui/index.html index 986be16..e9da293 100644 --- a/ui/index.html +++ b/ui/index.html @@ -92,7 +92,10 @@ <h2>模型市场 <span class="en">Marketplace</span></h2> <p>浏览 / 搜索各家模型,按点数计价</p> </header> <div class="toolbar"> - <input id="mk-search" class="input" placeholder="搜索模型名 / 厂商…"> + <span class="search-box"> + <input id="mk-search" class="input" placeholder="搜索模型名 / 厂商…"> + <button type="button" class="search-clear" hidden aria-label="清空搜索" title="清空搜索">×</button> + </span> <select id="mk-provider" class="input"> <option value="">全部厂商</option> </select> @@ -287,7 +290,10 @@ <h2>设置 <span class="en">Settings</span></h2> <div class="card api-key-card"> <h3>API Key <span class="en">API keys</span></h3> <p class="muted">用于本地工具 / 代码接入平台的密钥。独占整行,避免表格字段挤压。</p> - <input id="ak-search" class="input" placeholder="搜索 key 名字…" style="margin-bottom:10px"> + <span class="search-box" style="display:block;margin-bottom:10px"> + <input id="ak-search" class="input" placeholder="搜索 key 名字…"> + <button type="button" class="search-clear" hidden aria-label="清空搜索" title="清空搜索">×</button> + </span> <div class="table-wrap compact"> <table class="table"> <thead><tr><th>名字</th><th>Key</th><th>创建时间</th><th>最近使用</th><th>状态</th><th></th></tr></thead> @@ -378,7 +384,10 @@ <h3>按成员用量 <span class="en">By member</span></h3> <div class="admin-pane hidden" data-admin-pane="org"> <div class="stat-grid" id="dept-stats"></div> <div class="toolbar"> - <input id="od-search" class="input" placeholder="搜索部门…"> + <span class="search-box"> + <input id="od-search" class="input" placeholder="搜索部门…"> + <button type="button" class="search-clear" hidden aria-label="清空搜索" title="清空搜索">×</button> + </span> <button class="btn btn-primary" id="add-dept-btn">添加部门 <span class="en">(+ Add dept)</span></button> </div> <div class="card"> @@ -416,7 +425,10 @@ <h3 id="dept-form-title">添加部门 <span class="en">Add department</span></h3 <h3>用户充值 <span class="en">Top up a user</span></h3> <p class="muted">运营者 = 宿主本人(职责最小化:仅运行概览 + 充值)。按用户名 / 邮箱定位用户 → 输入点数金额 → 确认后余额增加<strong>永久有效点数</strong>并产生一条交易记录。</p> <div class="toolbar"> - <input id="ops-search" class="input" placeholder="搜索用户名 / 邮箱…"> + <span class="search-box"> + <input id="ops-search" class="input" placeholder="搜索用户名 / 邮箱…"> + <button type="button" class="search-clear" hidden aria-label="清空搜索" title="清空搜索">×</button> + </span> </div> <div class="table-wrap compact"> <table class="table"> diff --git a/ui/js/app.js b/ui/js/app.js index 1fa730a..f4006b5 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -81,6 +81,56 @@ el.style.animation = ""; } + /* --- 搜索增强(rant 18:06:09 D:防抖 + <mark> 关键词高亮 + 清空 × 按钮) --- */ + + // 用户输入作为正则关键词时先转义,避免误当正则语法(如 "C++"、"(") + function escapeRegExp(s) { + return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + // 关键词高亮:先 HTML 转义,再对查询词大小写不敏感包 <mark>;无关键词返回转义原样(重置后自动清除) + function hl(s, q) { + const t = esc(s); + if (!q) return t; + const eq = esc(q); // 与转义后的正文同构,避免 & < > 等字符错位 + if (!eq) return t; + const re = new RegExp(escapeRegExp(eq), "gi"); + return t.replace(re, (m) => "<mark>" + m + "</mark>"); + } + + // 搜索框接线:输入防抖渲染(默认 ~150ms,避免每次按键整表重绘闪烁)+ 清空 × 按钮(有内容时显示,点击清空立即重绘) + function wireSearch(input, render, ms) { + if (!input) return; + const delay = ms || 150; + const box = input.closest(".search-box"); + const clear = box ? box.querySelector(".search-clear") : null; + const syncClear = () => { if (clear) clear.hidden = !input.value; }; + let t = null; + input.addEventListener("input", () => { + clearTimeout(t); + t = setTimeout(() => render(), delay); + syncClear(); + }); + if (clear) { + clear.addEventListener("click", () => { + input.value = ""; + syncClear(); + render(); // 清空立即重绘,不走防抖 + input.focus(); + }); + } + syncClear(); + } + + // 程序化清空搜索框(值 + × 按钮态同步,不触发渲染;调用方随后自行重绘) + function resetSearch(input) { + if (!input) return; + input.value = ""; + const box = input.closest(".search-box"); + const clear = box ? box.querySelector(".search-clear") : null; + if (clear) clear.hidden = true; + } + /* --- 行内二次确认 / 行内编辑(rant 16:57:17 A:清除原生确认/输入弹窗) --- */ // 行内二次确认:首次点击按钮变「确认删除?」红色态,3 秒无操作或 Esc 还原,再次点击执行 @@ -342,7 +392,8 @@ /* --- 模型市场 --- */ function renderMarketplace() { - const q = ($("#mk-search").value || "").toLowerCase(); + const rawQ = $("#mk-search").value || ""; + const q = rawQ.toLowerCase(); const prov = $("#mk-provider").value; const sort = $("#mk-sort").value; @@ -356,7 +407,7 @@ $("#mk-count").textContent = list.length + " 个在售 key"; $("#mk-body").innerHTML = list.length ? list.map((m) => - "<tr><td data-label='厂商'>" + esc(m.provider) + "</td><td data-label='模型'><strong>" + esc(m.model) + "</strong></td>" + + "<tr><td data-label='厂商'>" + hl(m.provider, rawQ) + "</td><td data-label='模型'><strong>" + hl(m.model, rawQ) + "</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>') + @@ -892,10 +943,11 @@ } function renderSettings() { - const q = ($("#ak-search").value || "").toLowerCase(); + const rawQ = $("#ak-search").value || ""; + const q = rawQ.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 data-label='名字'><strong>" + esc(k.name) + "</strong></td>" + + "<tr><td data-label='名字'><strong>" + hl(k.name, rawQ) + "</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>" + @@ -1079,7 +1131,8 @@ } function renderOrg() { - const q = ($("#od-search").value || "").toLowerCase(); + const rawQ = $("#od-search").value || ""; + const q = rawQ.toLowerCase(); const list = D.DEPARTMENTS.filter((d) => !q || d.name.toLowerCase().includes(q)); const totalQuota = D.DEPARTMENTS.reduce((a, d) => a + d.quota, 0); @@ -1096,7 +1149,7 @@ 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 data-label='部门'><strong>" + esc(d.name) + "</strong></td>" + + return "<tr><td data-label='部门'><strong>" + hl(d.name, rawQ) + "</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>" + @@ -1172,7 +1225,8 @@ /* --- 平台运营者视图(US-运营1 / US-运营2:运营者 = 宿主本人,职责仅两项) --- */ function renderOperator() { - const q = ($("#ops-search").value || "").toLowerCase(); + const rawQ = $("#ops-search").value || ""; + const q = rawQ.toLowerCase(); const list = D.OPERATOR_USERS.filter((u) => !q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)); const txs = D.TRANSACTIONS; const flowIn = txs.filter((t) => t.pts > 0).reduce((a, t) => a + t.pts, 0); @@ -1189,8 +1243,8 @@ ].join(""); $("#ops-body").innerHTML = list.length ? list.map((u) => - "<tr><td data-label='用户'><strong>" + esc(u.name) + "</strong></td>" + - "<td data-label='邮箱'>" + esc(u.email) + "</td>" + + "<tr><td data-label='用户'><strong>" + hl(u.name, rawQ) + "</strong></td>" + + "<td data-label='邮箱'>" + hl(u.email, rawQ) + "</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, "没有匹配的用户", "试试其他用户名 / 邮箱"); @@ -1359,8 +1413,8 @@ toast("已退出(静态演示)", "info"); }); - // 市场筛选 - $("#mk-search").addEventListener("input", renderMarketplace); + // 市场筛选(搜索防抖 ~150ms + 高亮 + 清空按钮,rant 18:06:09 D) + wireSearch($("#mk-search"), renderMarketplace); $("#mk-provider").addEventListener("change", renderMarketplace); $("#mk-sort").addEventListener("change", renderMarketplace); if (!$("#mk-provider").dataset.init) { @@ -1378,7 +1432,7 @@ } // 空状态:清除筛选 if (e.target.closest("[data-mk-clear-filters]")) { - $("#mk-search").value = ""; + resetSearch($("#mk-search")); $("#mk-provider").value = ""; $("#mk-sort").value = "default"; renderMarketplace(); @@ -1390,7 +1444,7 @@ $("#chat-modal").addEventListener("click", (e) => { if (e.target === $("#chat-modal")) closeChat(); }); // 平台运营者(G1 / US-运营2):搜索定位用户 + 充值(行内编辑,永久有效点数,产生交易记录) - $("#ops-search").addEventListener("input", renderAdmin); + wireSearch($("#ops-search"), renderAdmin); $("#ops-body").addEventListener("click", (e) => { const b = e.target.closest("[data-ops-topup]"); if (!b) return; @@ -1507,7 +1561,7 @@ }); // API Key 搜索 + 行内操作(复制 / 改名 / 删除[行内二次确认]) - $("#ak-search").addEventListener("input", renderSettings); + wireSearch($("#ak-search"), renderSettings); $("#api-keys").addEventListener("click", (e) => { const cp = e.target.closest("[data-key-copy]"); @@ -1565,7 +1619,7 @@ }); // 组织管理:部门搜索 / 添加 / 编辑 / 删除(事件委托;添加/编辑用行内展开表单) - $("#od-search").addEventListener("input", renderAdmin); + wireSearch($("#od-search"), renderAdmin); $("#add-dept-btn").addEventListener("click", () => openDeptForm(null)); $("#dept-confirm").addEventListener("click", (e) => withLoading(e.currentTarget, confirmDept)); @@ -1581,7 +1635,7 @@ const dl = e.target.closest("[data-dept-del]"); if (dl) { confirmInline(dl, () => deleteDept(Number(dl.dataset.deptDel)), "确认删除部门?"); return; } if (e.target.closest("[data-dept-clear-search]")) { - $("#od-search").value = ""; + resetSearch($("#od-search")); renderAdmin(); } });