重构 Admin 控制台信息架构与页面骨架,按业务域重排导航并统一配置页分节布局。

本次在保持现有主题配色不变的前提下,提升了可读性与操作效率,补充了页内分节导航与长页滚动定位体验。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 15:48:49 +08:00
parent 37a2ef35f4
commit 85ce1194c9
3 changed files with 273 additions and 92 deletions

View file

@ -1309,6 +1309,58 @@ function mount(node) {
c.appendChild(node);
}
function renderPageShell(opts = {}, children = []) {
const title = String(opts.title || "").trim();
const subtitle = String(opts.subtitle || "").trim();
const actions = Array.isArray(opts.actions) ? opts.actions.filter(Boolean) : [];
const sections = Array.isArray(opts.sections) ? opts.sections.filter((x) => x && x.id && x.label) : [];
const head = [];
if (title || subtitle || actions.length) {
const left = [];
if (title) left.push(el("h1", { class: "page-shell__title", text: title }));
if (subtitle) left.push(el("div", { class: "page-shell__subtitle muted", text: subtitle }));
head.push(
el("div", { class: "page-shell__header" }, [
el("div", { class: "page-shell__headline" }, left),
el("div", { class: "page-shell__actions row" }, actions),
]),
);
if (sections.length > 1) {
head.push(
el(
"div",
{ class: "page-shell__toc", role: "navigation", "aria-label": "page sections" },
sections.map((s) =>
el("button", {
class: "page-shell__tocItem",
type: "button",
text: String(s.label),
onclick: () => {
const target = document.getElementById(String(s.id));
if (!target) return;
target.scrollIntoView({ behavior: "smooth", block: "start" });
},
}),
),
),
);
}
}
return el("section", { class: "page-shell" }, [...head, ...children.filter(Boolean)]);
}
function renderSectionCard(title, subtitle, bodyNodes = [], opts = {}) {
const nodes = [el("div", { class: "card__title", text: title })];
if (subtitle) nodes.push(el("div", { class: "muted", text: subtitle }));
if (Array.isArray(opts.actions) && opts.actions.length) {
nodes.push(el("div", { class: "row section-card__actions" }, opts.actions));
}
bodyNodes.filter(Boolean).forEach((node) => nodes.push(node));
const attrs = { class: "card section-card" };
if (opts.id) attrs.id = String(opts.id);
return el("div", attrs, nodes);
}
function shortText(v, maxLen = 200) {
const s = String(v ?? "");
if (s.length <= maxLen) return s;
@ -3836,25 +3888,32 @@ async function renderModels() {
// Show secret migration helper when legacy secrets exist.
topBits.push(secretsCard);
return el("div", {}, [
return renderPageShell({
title: t("title.models"),
subtitle: "模型切换、绑定与评估配置",
sections: [
{ id: "models-overview", label: "概览" },
{ id: "models-bindings", label: "绑定" },
{ id: "models-api", label: "API配置" },
{ id: "models-experts", label: "Experts" },
{ id: "models-eval", label: "评估" },
],
}, [
...topBits,
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("models.sectionActive") }),
el("div", { class: "page-grid page-grid--two" }, [
renderSectionCard(t("models.sectionActive"), "", [
el("div", { class: "row" }, [el("label", { text: t("models.pickModel") }), activeSelect]),
], { id: "models-overview" }),
renderSectionCard(t("models.sectionNew"), "", [
el("div", { class: "row" }, [newName, newMode, btnCreate]),
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("models.sectionBindings") }),
el("div", { class: "muted", text: t("models.agentBindingHelp") }),
]),
renderSectionCard(t("models.sectionBindings"), t("models.agentBindingHelp"), [
el("div", { class: "muted", text: t("models.bindingsExtraHint") }),
el("div", { class: "muted", text: t("models.bindingsScopeHint") }),
bindingWrap,
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("models.sectionNew") }),
el("div", { class: "row" }, [newName, newMode, btnCreate]),
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("models.sectionApi") }),
], { id: "models-bindings" }),
renderSectionCard(t("models.sectionApi"), "", [
builtinCap,
readonlyProfileHint,
el("div", { class: "row" }, [el("label", { text: t("models.profileName") }), profName]),
@ -3867,18 +3926,16 @@ async function renderModels() {
el("div", { class: "row" }, [el("label", { text: t("models.apiKey") }), keyInp]),
el("div", { class: "row" }, [el("label", { text: t("models.rememberKey") }), rememberCb]),
el("div", { class: "row" }, [btnSave, btnDelete]),
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: "Experts (runtime/workspaces)" }),
el("div", { class: "muted", text: "Only SOUL.md and ROLE_SYSTEM.md are maintained in Admin. SOUL.md is required." }),
], { id: "models-api" }),
renderSectionCard("Experts (runtime/workspaces)", "Only SOUL.md and ROLE_SYSTEM.md are maintained in Admin. SOUL.md is required.", [
el("div", { class: "row" }, [el("label", { text: "Existing" }), expertsSelect, btnExpertDelete]),
el("div", { class: "row" }, [el("label", { text: "Create" }), expertNewId, btnExpertCreate]),
el("div", { class: "row" }, [el("label", { text: "SOUL.md" }), expertSoul]),
el("div", { class: "row" }, [el("label", { text: "ROLE_SYSTEM.md" }), expertRoleSystem]),
el("div", { class: "row" }, [btnExpertSave]),
expertsStatus,
]),
el("div", { class: "card" }, [evalDetails]),
], { id: "models-experts" }),
el("div", { class: "card section-card", id: "models-eval" }, [evalDetails]),
]);
}
@ -5618,24 +5675,28 @@ async function renderPlugins() {
bindingReverseWrap,
bindingStatus,
]);
const pluginsPageHeader = el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("plugins.title") }),
el("div", {
class: "muted",
text: "以下按功能分为多个区块,默认折叠;表格均支持翻页(每页 " + PLUGINS_PAGE_SIZE + " 条)。",
}),
]);
return el("div", {}, [
pluginsPageHeader,
foldToolPolicy,
foldMcpMarket,
foldMcpInstall,
foldMcpInstalled,
return renderPageShell({
title: t("plugins.title"),
subtitle: "按功能分区展示,默认折叠;表格支持分页(每页 " + PLUGINS_PAGE_SIZE + " 条)。",
sections: [
{ id: "plugins-tool-policy", label: "策略" },
{ id: "plugins-market", label: "市场" },
{ id: "plugins-install", label: "安装" },
{ id: "plugins-instances", label: "实例" },
{ id: "plugins-binding", label: "专家绑定" },
],
}, [
el("div", { class: "page-grid page-grid--single" }, [
el("div", { id: "plugins-tool-policy" }, [foldToolPolicy]),
el("div", { id: "plugins-market" }, [foldMcpMarket]),
el("div", { id: "plugins-install" }, [foldMcpInstall]),
el("div", { id: "plugins-instances" }, [foldMcpInstalled]),
foldMcpUsage,
foldWireGlobal,
foldWireTools,
foldExpertBindingDash,
foldMcpBinding,
el("div", { id: "plugins-binding" }, [foldMcpBinding]),
]),
]);
}
@ -6163,13 +6224,17 @@ async function renderSessionMonitor() {
detailModal.addEventListener("click", (ev) => {
if (ev.target === detailModal) closeDetailModal();
});
return el("div", {}, [
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("sessionMonitor.totals") }),
totalsWrap,
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("sessionMonitor.userStats") }),
return renderPageShell({
title: t("title.sessionMonitor"),
subtitle: "按用户、会话与消息详情进行巡检与导出",
sections: [
{ id: "sm-totals", label: "总览" },
{ id: "sm-users", label: "用户" },
{ id: "sm-sessions", label: "会话" },
],
}, [
renderSectionCard(t("sessionMonitor.totals"), "", [totalsWrap], { id: "sm-totals" }),
renderSectionCard(t("sessionMonitor.userStats"), "", [
el("div", { class: "row" }, [userQ, btnQueryUsers]),
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
@ -6189,9 +6254,8 @@ async function renderSessionMonitor() {
userBody,
]),
]),
]),
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("sessionMonitor.sessions") }),
], { id: "sm-users" }),
renderSectionCard(t("sessionMonitor.sessions"), "", [
el("div", { class: "row" }, [sessQ, activeOnlyLabel, btnQuerySessions]),
selectedHint,
sessionPagerTop,
@ -6212,7 +6276,7 @@ async function renderSessionMonitor() {
]),
]),
sessionPagerBottom,
]),
], { id: "sm-sessions" }),
detailModal,
]);
}
@ -6582,13 +6646,20 @@ async function renderAttachments() {
inputEl,
]);
return el("div", {}, [
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("attachments.title") }),
el("div", { class: "muted", text: t("attachments.excelPolicy") }),
el("div", { style: "height:8px" }),
hint,
el("div", { style: "height:10px" }),
return renderPageShell({
title: t("attachments.title"),
subtitle: "附件处理阈值、预算与回放策略配置",
actions: [loadBtn, resetBtn, saveBtn],
sections: [
{ id: "attach-overview", label: "说明" },
{ id: "attach-table", label: "表格" },
{ id: "attach-media", label: "图像/视频" },
{ id: "attach-archive", label: "压缩包" },
],
}, [
renderSectionCard("策略说明", t("attachments.excelPolicy"), [hint], { id: "attach-overview" }),
el("div", { class: "page-grid page-grid--two" }, [
renderSectionCard("表格与工具模式", "", [
inputRow(t("attachments.maxRowsRead"), rowInput),
inputRow(t("attachments.maxColumns"), colInput),
inputRow(t("attachments.maxCellChars"), cellInput),
@ -6602,6 +6673,8 @@ async function renderAttachments() {
inputRow(t("attachments.toolModeMaxBytes"), toolMaxBytesInput),
inputRow(t("attachments.sqlTimeoutMs"), sqlTimeoutInput),
el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("attachments.sqlTimeoutHint") }),
], { id: "attach-table" }),
renderSectionCard("图像与视频策略", "", [
inputRow(t("attachments.imageReplayCapChars"), imageReplayCapInput),
el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("attachments.imageReplayCapHint") }),
inputRow(t("attachments.videoReplayCapChars"), videoReplayCapInput),
@ -6609,15 +6682,17 @@ async function renderAttachments() {
inputRow(t("attachments.videoTranscriptChunkSize"), videoTranscriptChunkSizeInput),
inputRow(t("attachments.videoTranscriptChunkOverlap"), videoTranscriptChunkOverlapInput),
el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("attachments.videoTranscriptChunkHint") }),
], { id: "attach-media" }),
]),
renderSectionCard("压缩包预算策略", "", [
inputRow(t("attachments.archiveMaxDepth"), archiveMaxDepthInput),
inputRow(t("attachments.archiveMaxFileCount"), archiveMaxFileCountInput),
inputRow(t("attachments.archiveMaxEntryBytes"), archiveMaxEntryBytesInput),
inputRow(t("attachments.archiveMaxTotalBytes"), archiveMaxTotalBytesInput),
el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("attachments.archivePolicyHint") }),
el("div", { class: "row", style: "gap:8px;margin-top:12px;flex-wrap:wrap;" }, [loadBtn, resetBtn, saveBtn]),
el("div", { style: "height:8px" }),
status,
]),
], { id: "attach-archive" }),
]);
}

View file

@ -55,19 +55,31 @@
</div>
</div>
<nav class="nav">
<div class="nav__group">
<div class="nav__groupTitle">系统与运行</div>
<a class="nav__item" data-page="stack" href="#/stack" data-i18n="nav.stack">Runtime</a>
<a class="nav__item" data-page="session-monitor" href="#/session-monitor" data-i18n="nav.sessionMonitor">会话监控</a>
<a class="nav__item" data-page="workspace-paths" href="#/workspace-paths" data-i18n="nav.workspacePaths">工作区路径</a>
<a class="nav__item" data-page="attachments" href="#/attachments" data-i18n="nav.attachments">附件</a>
</div>
<div class="nav__group">
<div class="nav__groupTitle">模型与插件</div>
<a class="nav__item" data-page="models" href="#/models" data-i18n="nav.models">模型管理</a>
<a class="nav__item" data-page="api-grants" href="#/api-grants" data-i18n="nav.apiGrants">API 使用授权</a>
<a class="nav__item" data-page="stack" href="#/stack" data-i18n="nav.stack">Runtime</a>
<a class="nav__item" data-page="users" href="#/users" data-i18n="nav.users">用户管理</a>
<a class="nav__item" data-page="workspace-paths" href="#/workspace-paths" data-i18n="nav.workspacePaths">工作区路径</a>
<a class="nav__item" data-page="memory" href="#/memory" data-i18n="nav.memory">Memory</a>
<a class="nav__item" data-page="audit" href="#/audit" data-i18n="nav.audit">Audit & Trace</a>
<a class="nav__item" data-page="session-monitor" href="#/session-monitor" data-i18n="nav.sessionMonitor">会话监控</a>
<a class="nav__item" data-page="admin-audit" href="#/admin-audit" data-i18n="nav.adminAudit">Admin Audit</a>
<a class="nav__item" data-page="plugins" href="#/plugins" data-i18n="nav.plugins">Plugins</a>
<a class="nav__item" data-page="skills" href="#/skills" data-i18n="nav.skills">Skills</a>
<a class="nav__item" data-page="attachments" href="#/attachments" data-i18n="nav.attachments">附件</a>
</div>
<div class="nav__group">
<div class="nav__groupTitle">治理与审计</div>
<a class="nav__item" data-page="memory" href="#/memory" data-i18n="nav.memory">Memory</a>
<a class="nav__item" data-page="audit" href="#/audit" data-i18n="nav.audit">Audit & Trace</a>
<a class="nav__item" data-page="admin-audit" href="#/admin-audit" data-i18n="nav.adminAudit">Admin Audit</a>
</div>
<div class="nav__group">
<div class="nav__groupTitle">用户与配置</div>
<a class="nav__item" data-page="users" href="#/users" data-i18n="nav.users">用户管理</a>
<a class="nav__item" data-page="profile" href="#/profile" data-i18n="nav.profile">设置</a>
</div>
</nav>
<div class="sidebar__footer">
<div class="muted" data-i18n="notice.noLogin">v1 无登录:请仅在内网访问</div>

View file

@ -67,7 +67,19 @@ body {
letter-spacing: 0.02em;
}
.brand__sub { font-size: 12px; color: rgba(226,232,240,0.7); margin-top: 4px; }
.nav { padding: 8px; display: flex; flex-direction: column; gap: 4px; }
.nav { padding: 8px; display: flex; flex-direction: column; gap: 12px; overflow: auto; }
.nav__group {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav__groupTitle {
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(226,232,240,0.55);
padding: 6px 12px 2px;
}
.nav__item { text-decoration: none; color: rgba(226,232,240,0.85); padding: 10px 12px; border-radius: 10px; }
.nav__item:hover { background: rgba(148,163,184,0.12); }
.nav__item--active { background: rgba(59,130,246,0.22); color: #e2e8f0; }
@ -83,6 +95,82 @@ body {
.btn--danger { background: rgba(239,68,68,0.18); border-color: rgba(239,68,68,0.35); }
.content { padding: 18px; overflow: auto; color: #e2e8f0; }
.page-shell {
display: flex;
flex-direction: column;
gap: 12px;
}
.page-shell__header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
padding: 2px 2px 4px;
}
.page-shell__headline {
display: flex;
flex-direction: column;
gap: 4px;
}
.page-shell__title {
margin: 0;
font-size: 20px;
line-height: 1.2;
font-weight: 650;
}
.page-shell__subtitle {
max-width: 920px;
line-height: 1.45;
}
.page-shell__actions {
justify-content: flex-end;
}
.page-shell__toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.page-shell__tocItem {
text-decoration: none;
color: rgba(226,232,240,0.82);
border: 1px solid rgba(148,163,184,0.22);
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
background: transparent;
cursor: pointer;
}
.page-shell__tocItem:hover {
background: rgba(148,163,184,0.16);
}
.page-grid {
display: grid;
gap: 12px;
}
.page-grid--single {
grid-template-columns: minmax(0, 1fr);
}
.page-grid--two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.section-card {
margin-bottom: 0;
scroll-margin-top: 76px;
}
.section-card__actions {
justify-content: flex-end;
}
.page-grid > [id] {
scroll-margin-top: 76px;
}
.page-shell__header .page-shell__actions {
position: sticky;
top: 0;
z-index: 3;
padding: 2px 0;
}
.card { background: rgba(15,23,42,0.9); border: 1px solid rgba(148,163,184,0.18); border-radius: 14px; padding: 14px; margin-bottom: 12px; }
.card__title { font-weight: 650; margin-bottom: 10px; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
@ -157,6 +245,12 @@ body {
align-items: center;
justify-content: flex-end;
}
@media (max-width: 1200px) {
.page-grid--two {
grid-template-columns: minmax(0, 1fr);
}
}
.session-monitor-row--active {
background: rgba(94, 179, 255, 0.12);
}