diff --git a/docs/user-stories.md b/docs/user-stories.md
index 1ee1f01..237861e 100644
--- a/docs/user-stories.md
+++ b/docs/user-stories.md
@@ -5,6 +5,8 @@
> **v1.13(2026-08-17)· UI 演化约束:尽量少用弹窗 / modal.** 新增「UI 约定」章节(§1.1):优先行内(inline)交互(行内展开表单 / 可折叠面板 / 就地编辑),弹窗仅限真正需要聚焦 / 阻塞的场景(如删除确认),**禁止弹窗嵌套**;存量 modal(充值 topup-modal、申请加额 raise-modal、模型消费 chat-modal)标记改造优先级,后续迭代分批改为行内形态。部门添加/编辑已由弹层改为**行内展开表单**(`#dept-form-card`)。
>
> **v1.14(2026-08-17)· 充值 / 加额改行内(P1 完成).** 按 UI 美化与 UE 优化 rant(`2026-08-17T15:50:05`)「少弹窗改造(优先)」:充值(原 topup-modal)与申请加额(原 raise-modal)改为钱包页**行内展开卡片**(`#topup-card` / `#raise-card`,点按钮原地展开、两卡互斥、取消即收起),替代弹层;P1 存量 modal 清零,仅剩 P2 模型消费聊天(chat-modal)。
+>
+> **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.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 498a987..e115ce5 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -61,12 +61,19 @@ python3 -m http.server 8000 --directory ui
```
ui/
├── index.html # 入口(登录页 + 应用外壳 + 全部视图)
-├── css/style.css # 设计系统(深色主题 · 强调色 #4ecdc4 · 响应式预留)
+├── css/style.css # 设计系统(深色主题 · 强调色 #4ecdc4 · 响应式预留 · v1.15 视觉美化)
├── js/data.js # 内嵌 mock 数据(模型价格、交易、共享、成员等)
-├── js/app.js # 交互逻辑(导航、筛选、表单、分页、Toast)
+├── js/app.js # 交互逻辑(导航、筛选、表单、分页、Toast;v1.15 内联 SVG 图标 + 空状态组件)
└── README.md # 本文件
```
+## 视觉约定(v1.15,rant 2026-08-17T15:50:05 A 视觉美化)
+
+- 侧边栏导航图标用**统一内联 SVG**(线性风格、同尺寸、currentColor,禁用 emoji 散落);
+- 卡片 hover 阴影/边框轻提升;表格**斑马纹 + 行 hover 高亮**;
+- 空状态统一 `empty-state` 组件(图标 + 文案 + 行动按钮),禁用裸文本占位;
+- 状态徽章统一带**语义色点**(ok/warn/danger/dim),配色与 `badge.*` 类一致。
+
## 数据说明
- 点数规则与机制细节见 `docs/user-stories.md`(v1.8:机制说明不再进入面向用户的界面文案);UI 只呈现结果(余额数字、模型价格点数、交易金额/类型/状态、可用/繁忙)
diff --git a/ui/css/style.css b/ui/css/style.css
index feb10bd..e258bf3 100644
--- a/ui/css/style.css
+++ b/ui/css/style.css
@@ -111,9 +111,13 @@ select.input option { background: var(--bg-card); }
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
+ transition: border-color .18s ease, box-shadow .18s ease;
}
.card h3 { font-size: 15px; margin-bottom: 14px; color: var(--text); }
+/* 卡片 hover 微交互(rant 15:50:05 A.3:阴影/边框轻提升) */
+.card:hover { border-color: #3a4a63; box-shadow: 0 12px 30px rgba(0,0,0,.38); }
+
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
@@ -161,16 +165,26 @@ 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 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); }
.badge {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--border);
color: var(--text-dim);
}
+/* 语义色点(rant 15:50:05 A.5:状态徽章配色统一、一眼可辨) */
+.badge::before {
+ content: "";
+ width: 6px; height: 6px; border-radius: 50%;
+ background: currentColor; opacity: .85; flex-shrink: 0;
+}
.badge.ok { color: var(--ok); border-color: rgba(62,207,142,.4); background: rgba(62,207,142,.08); }
.badge.warn { color: var(--warn); border-color: rgba(229,180,91,.4); background: rgba(229,180,91,.08); }
.badge.danger { color: var(--danger); border-color: rgba(229,102,106,.4); background: rgba(229,102,106,.08); }
@@ -291,6 +305,21 @@ select.input option { background: var(--bg-card); }
}
.bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--accent), #7ee8b8); border-radius: 999px; }
+/* 空状态(rant 15:50:05 A.4:图标 + 文案 + 可选行动按钮) */
+.empty-state {
+ display: flex; flex-direction: column; align-items: center;
+ gap: 8px; padding: 40px 16px; text-align: center;
+ color: var(--text-dim);
+}
+.empty-state .es-ico { width: 40px; height: 40px; color: var(--text-mute); opacity: .75; }
+.empty-state p { font-size: 13px; }
+.empty-state p.muted { font-size: 12px; }
+.empty-state .btn { margin-top: 8px; }
+.empty-state.compact { padding: 16px 8px; }
+.empty-state.compact .es-ico { width: 26px; height: 26px; }
+.empty-cell { padding: 0 !important; }
+.empty-cell .empty-state { padding: 36px 16px; }
+
/* ---------- 登录页 ---------- */
.login-view {
@@ -357,7 +386,8 @@ select.input option { background: var(--bg-card); }
color: var(--text-dim); cursor: pointer; font-size: 13.5px;
border: none; background: transparent; text-align: left; width: 100%;
}
-.nav-item .ico { width: 18px; text-align: center; font-size: 15px; }
+.nav-item .ico { width: 18px; text-align: center; font-size: 15px; display: inline-flex; align-items: center; justify-content: center; }
+.nav-item .ico svg { width: 18px; height: 18px; display: block; }
.nav-tag {
margin-left: auto;
font-size: 10px;
diff --git a/ui/js/app.js b/ui/js/app.js
index c67357b..2d7d4e8 100644
--- a/ui/js/app.js
+++ b/ui/js/app.js
@@ -33,6 +33,18 @@
esc(labels[status] ? labels[status].text : status) + "";
}
+ // 空状态组件(rant 15:50:05 A.4:列表/表格为空时给出图标 + 文案 + 可选行动按钮)
+ const EMPTY_ICON = '';
+
+ function emptyState(text, sub, actionHtml) {
+ return '
' + EMPTY_ICON + "
" + esc(text) + "
" +
+ (sub ? '
' + esc(sub) + "
" : "") + (actionHtml || "") + "
";
+ }
+
+ function emptyRow(colspan, text, sub, actionHtml) {
+ return '| ' + emptyState(text, sub, actionHtml) + " |
";
+ }
+
// 自动单价:单价是模型×厂商的客观属性,由平台按模型定价自动计算(输出 1M tokens 折算点数)
function autoPrice(model) {
const m = D.MODELS.find((x) => x.model === model);
@@ -74,17 +86,28 @@
/* ---------------- 导航 ---------------- */
+ // 统一内联 SVG 图标(线性风格、同尺寸、currentColor,替代 emoji;rant 15:50:05 A.2)
+ const ICONS = {
+ dashboard: '',
+ marketplace: '',
+ sharing: '',
+ wallet: '',
+ transactions: '',
+ admin: '',
+ settings: '',
+ };
+
const NAV = [
{ g: "主导航", items: [
- { id: "dashboard", ico: "📊", label: "仪表盘 Dashboard" },
- { id: "marketplace", ico: "🛒", label: "模型市场 Marketplace" },
- { id: "sharing", ico: "🔗", label: "共享管理 Sharing" },
- { id: "wallet", ico: "👛", label: "钱包 Wallet" },
- { id: "transactions", ico: "🧾", label: "交易记录 Transactions" },
+ { id: "dashboard", icon: "dashboard", label: "仪表盘 Dashboard" },
+ { id: "marketplace", icon: "marketplace", label: "模型市场 Marketplace" },
+ { id: "sharing", icon: "sharing", label: "共享管理 Sharing" },
+ { id: "wallet", icon: "wallet", label: "钱包 Wallet" },
+ { id: "transactions", icon: "transactions", label: "交易记录 Transactions" },
]},
{ g: "角色视图", items: [
- { id: "admin", ico: "🛠️", label: "管理视图 Admin", role: "admin" },
- { id: "settings", ico: "⚙️", label: "设置 Settings" },
+ { id: "admin", icon: "admin", label: "管理视图 Admin", role: "admin" },
+ { id: "settings", icon: "settings", label: "设置 Settings" },
]},
];
@@ -102,7 +125,7 @@
nav.innerHTML = "";
const groups = isGuest
? [{ g: "游客浏览", items: [
- { id: "marketplace", ico: "🛒", label: "模型市场 Marketplace" },
+ { id: "marketplace", icon: "marketplace", label: "模型市场 Marketplace" },
]}]
: NAV;
groups.forEach((group) => {
@@ -114,7 +137,7 @@
const b = document.createElement("button");
b.className = "nav-item" + (item.id === activeView ? " active" : "");
b.dataset.view = item.id;
- b.innerHTML = '' + item.ico + '' + esc(item.label) + "";
+ b.innerHTML = '' + (ICONS[item.icon] || "") + '' + esc(item.label) + "";
if (item.role) {
const tag = document.createElement("span");
tag.className = "nav-tag";
@@ -173,7 +196,7 @@
'' + esc(s.model) + "
" +
'
' + esc(s.plan || "API") + " · 已用 " + D.fmt(s.used) + " / " + D.fmt(s.quota) + " 点 · 单价 " + D.fmt(s.price) + " 点/1M
" +
'
+' + D.fmt(s.earned) + "累计收益
"
- ).join("") + (on.length ? "" : '还没有上架的 key
');
+ ).join("") + (on.length ? "" : '' + EMPTY_ICON + '
还没有上架的 key
去「共享管理」把闲置 key 放进池子
');
renderMonthChanges();
}
@@ -206,7 +229,8 @@
(m.multi ? ' 多 key · 自动故障转移' : "") + "" +
"" +
" 成功率 " + m.success + "% | "
- ).join("") : '| 没有匹配的模型 |
';
+ ).join("") : emptyRow(7, "没有匹配的模型", "试试调整搜索或筛选条件",
+ '');
}
/* --- 可用时间段(rant 10:54:48:结构化字段,备注只作纯备注) --- */
@@ -284,7 +308,7 @@
fillPlans();
}
- $("#share-body").innerHTML = D.SHARINGS.map((s, i) =>
+ $("#share-body").innerHTML = D.SHARINGS.length ? D.SHARINGS.map((s, i) =>
"| " + esc(D.PROVIDER_LABELS[s.provider] || s.provider) + " · " + esc(s.plan || "API") +
" " + esc(s.model) + " · " + esc(fmtAvailable(s)) + " | " +
"" + esc(maskKey(s.key)) + " | " +
@@ -295,7 +319,8 @@
" " +
" |
"
- ).join("");
+ ).join("") : emptyRow(7, "还没有上架的 key", "把闲置 key 放进池子,开始赚点数",
+ '');
}
function deleteSharing(i) {
@@ -445,7 +470,7 @@
""
: '' + esc(r.time) + "") + ""
).join("") + ""
- : '暂无加额申请
';
+ : emptyState("暂无加额申请", "成员提交的加额申请会显示在这里,批准后自动加点数");
}
function approveRaise(i) {
@@ -580,7 +605,7 @@
}
});
html += "";
- if (!pageRows.length) html += '| 没有匹配的记录 |
';
+ if (!pageRows.length) html += '| ' + emptyState("没有匹配的记录", "试试调整筛选条件") + " |
";
pageRows.forEach((row) => {
html += "";
columns.forEach((col) => {
@@ -666,7 +691,8 @@
"| " +
" " +
" |
"
- ).join("") : '| 没有匹配的 key |
';
+ ).join("") : emptyRow(6, "还没有 API Key", "生成一个 key,用于本地工具 / 代码接入平台",
+ '');
}
// 一键复制完整 key;file:// 下 clipboard API 受限 → 降级:临时 textarea 选中 + execCommand("copy"),仍失败则提示 Ctrl+C
@@ -693,6 +719,17 @@
}
}
+ // 生成新 API Key(带名字;列表展示脱敏、复制给完整 id)
+ function generateApiKey() {
+ const input = window.prompt("新 key 名字(留空则默认「未命名」):", "未命名");
+ if (input == null) return;
+ const name = String(input).trim() || "未命名";
+ const id = "atk_live_" + Array.from({ length: 12 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
+ D.API_KEYS.unshift({ id, name, created: "2026-08-14", last: "从未", status: "active" });
+ renderSettings();
+ toast("已生成新 API Key「" + name + "」(完整 key 已展示在列表,可复制)");
+ }
+
function renameKey(i) {
const k = D.API_KEYS[i];
if (!k) return;
@@ -829,7 +866,8 @@
"" + st + " | " +
" " +
" | ";
- }).join("") : '| 没有匹配的部门 |
';
+ }).join("") : emptyRow(7, "没有匹配的部门", "试试调整搜索关键词",
+ '');
}
/* --- 部门添加/编辑:行内展开表单(UI 原则:少用弹窗,优先行内交互;继承 rant 10:59:47 的可靠响应) --- */
@@ -915,7 +953,7 @@
"" + esc(u.email) + " | " +
"" + D.fmt(u.balance) + " 点 | " +
" | "
- ).join("") : '| 没有匹配的用户 |
';
+ ).join("") : emptyRow(4, "没有匹配的用户", "试试其他用户名 / 邮箱");
}
/* --- 消费模拟(US-6:市场页「使用 / 消费」→ 聊天 Mock,按模型参考价扣小数点数) --- */
@@ -1029,9 +1067,18 @@
// 市场页:使用 / 消费(G4:聊天 Mock 扣小数点数并产生 consume 交易;游客需先登录 US-1)
$("#mk-body").addEventListener("click", (e) => {
const b = e.target.closest("[data-use-model]");
- if (!b) return;
- if (isGuest) { toast("请先登录后再使用 / 消费模型"); return; }
- openChat(Number(b.dataset.useModel));
+ if (b) {
+ if (isGuest) { toast("请先登录后再使用 / 消费模型"); return; }
+ openChat(Number(b.dataset.useModel));
+ return;
+ }
+ // 空状态:清除筛选
+ if (e.target.closest("[data-mk-clear-filters]")) {
+ $("#mk-search").value = "";
+ $("#mk-provider").value = "";
+ $("#mk-sort").value = "default";
+ renderMarketplace();
+ }
});
$("#chat-send").addEventListener("click", sendChat);
$("#chat-close").addEventListener("click", closeChat);
@@ -1106,12 +1153,13 @@
hideShareForm();
});
- // 共享列表操作(事件委托:暂停/恢复/重新上架 + 删除)
+ // 共享列表操作(事件委托:暂停/恢复/重新上架 + 删除 + 空状态上架)
$("#share-body").addEventListener("click", (e) => {
const b = e.target.closest("[data-share-toggle]");
if (b) { toggleSharing(Number(b.dataset.shareToggle)); return; }
const d = e.target.closest("[data-share-delete]");
- if (d) deleteSharing(Number(d.dataset.shareDelete));
+ if (d) { deleteSharing(Number(d.dataset.shareDelete)); return; }
+ if (e.target.closest("[data-share-add]")) showShareForm();
});
// 钱包按钮(充值:US-4 行内卡片;申请加额:US-20 行内卡片;提现仍 disabled)
@@ -1135,15 +1183,7 @@
$$("#tx-tabs .tab").forEach((b) => b.addEventListener("click", () => { txTab = b.dataset.txTab; txTable.page = 1; renderTransactions(); }));
// API Key 生成(带名字;列表展示脱敏、复制给完整 id)
- $("#new-api-key-btn").addEventListener("click", () => {
- const input = window.prompt("新 key 名字(留空则默认「未命名」):", "未命名");
- if (input == null) return;
- const name = String(input).trim() || "未命名";
- const id = "atk_live_" + Array.from({ length: 12 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
- D.API_KEYS.unshift({ id, name, created: "2026-08-14", last: "从未", status: "active" });
- renderSettings();
- toast("已生成新 API Key「" + name + "」(完整 key 已展示在列表,可复制)");
- });
+ $("#new-api-key-btn").addEventListener("click", generateApiKey);
// API Key 搜索 + 行内操作(复制 / 改名 / 删除)
$("#ak-search").addEventListener("input", renderSettings);
@@ -1154,7 +1194,8 @@
const rn = e.target.closest("[data-key-rename]");
if (rn) { renameKey(Number(rn.dataset.keyRename)); return; }
const dl = e.target.closest("[data-key-del]");
- if (dl) deleteKey(Number(dl.dataset.keyDel));
+ if (dl) { deleteKey(Number(dl.dataset.keyDel)); return; }
+ if (e.target.closest("[data-new-key]")) generateApiKey();
});
// 管理台 Tabs
@@ -1203,7 +1244,11 @@
const ed = e.target.closest("[data-dept-edit]");
if (ed) { openDeptForm(Number(ed.dataset.deptEdit)); return; }
const dl = e.target.closest("[data-dept-del]");
- if (dl) deleteDept(Number(dl.dataset.deptDel));
+ if (dl) { deleteDept(Number(dl.dataset.deptDel)); return; }
+ if (e.target.closest("[data-dept-clear-search]")) {
+ $("#od-search").value = "";
+ renderAdmin();
+ }
});
}