oh-my-muse/design-docs/prototypes/产品-02B-管理员控制台原型.html
2026-05-22 23:18:03 +08:00

1581 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>产品-02B 管理员控制台原型</title>
<style>
:root {
--bg: #f4f6f5;
--surface: #ffffff;
--surface-soft: #f8faf9;
--surface-strong: #edf2f0;
--ink: #17211f;
--muted: #4f5f5b;
--weak: #6f7f7b;
--line: #d3ddda;
--line-strong: #b9c7c3;
--accent: #0f6f63;
--accent-weak: #e5f5f1;
--blue: #275f8f;
--blue-weak: #e7f0f8;
--warning: #8a5a16;
--warning-weak: #fff4dd;
--danger: #a43b36;
--danger-weak: #ffe8e5;
--ok: #237452;
--ok-weak: #e5f5ec;
--shadow: 0 12px 28px rgba(23, 33, 31, 0.06);
--radius: 8px;
--nav-width: 272px;
--inspector-width: 368px;
color-scheme: light;
}
* {
box-sizing: border-box;
}
html {
background: var(--bg);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.45;
}
body {
margin: 0;
min-width: 320px;
}
button,
input,
select {
font: inherit;
}
button,
select {
cursor: pointer;
}
.app {
min-height: 100vh;
display: grid;
grid-template-columns: var(--nav-width) minmax(0, 1fr);
grid-template-rows: 56px minmax(0, 1fr);
}
.topbar {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 9px 16px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.96);
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.brand-mark {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 7px;
background: var(--accent);
color: #fff;
font-weight: 800;
letter-spacing: 0;
flex: 0 0 auto;
}
.brand-title {
margin: 0;
font-size: 15px;
font-weight: 800;
line-height: 1.15;
letter-spacing: 0;
}
.brand-meta {
margin: 1px 0 0;
color: var(--muted);
font-size: 11px;
line-height: 1.2;
}
.top-actions,
.action-row,
.segmented,
.badge-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.top-user {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.avatar {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: 50%;
background: var(--surface-soft);
color: var(--ink);
font-size: 12px;
font-weight: 800;
}
.sidebar {
grid-column: 1;
grid-row: 2;
min-height: 0;
overflow: auto;
border-right: 1px solid var(--line);
background: var(--surface);
}
.nav-section {
padding: 12px;
border-bottom: 1px solid var(--line);
}
.nav-title {
margin: 0 0 8px;
color: var(--weak);
font-size: 11px;
font-weight: 800;
letter-spacing: 0;
}
.nav-button {
width: 100%;
min-height: 38px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
margin: 0 0 4px;
padding: 7px 8px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--ink);
text-align: left;
}
.nav-button:hover {
border-color: var(--line);
background: var(--surface-soft);
}
.nav-button.active {
border-color: #9ed5cc;
background: var(--accent-weak);
color: #083f3a;
}
.nav-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 700;
}
.nav-dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--line-strong);
}
.nav-button.active .nav-dot {
background: var(--accent);
}
.main {
grid-column: 2;
grid-row: 2;
min-height: 0;
min-width: 0;
overflow: auto;
padding: 16px;
}
.screen {
max-width: 1680px;
margin: 0 auto;
animation: screen-in 150ms ease-out;
}
@keyframes screen-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.page-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: start;
margin-bottom: 10px;
}
.page-kicker {
display: flex;
align-items: center;
gap: 8px;
color: #44534f;
font-size: 12px;
font-weight: 700;
}
.page-title {
margin: 3px 0 6px;
font-size: 24px;
line-height: 1.18;
letter-spacing: 0;
}
.page-role {
color: var(--muted);
font-size: 12px;
}
.button {
min-height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid var(--line-strong);
border-radius: 7px;
background: var(--surface);
color: var(--ink);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.button:hover {
border-color: #98aaa5;
background: #f7faf9;
}
.button.primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
.button.primary:hover {
background: #0b5e54;
}
.button.danger {
border-color: #efbab4;
background: var(--danger-weak);
color: var(--danger);
}
.button.ghost {
border-color: transparent;
background: transparent;
color: var(--muted);
}
.button:focus-visible,
.nav-button:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 3px solid rgba(15, 111, 99, 0.18);
outline-offset: 2px;
}
.badge {
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 22px;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
color: var(--muted);
font-size: 11px;
font-weight: 700;
word-break: break-word;
}
.badge.ok {
border-color: #b8dfcb;
background: var(--ok-weak);
color: var(--ok);
}
.badge.warn {
border-color: #eccd91;
background: var(--warning-weak);
color: var(--warning);
}
.badge.danger {
border-color: #efbab4;
background: var(--danger-weak);
color: var(--danger);
}
.badge.blue {
border-color: #bfd4e5;
background: var(--blue-weak);
color: var(--blue);
}
.filter-bar,
.panel,
.metric,
.inspector {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
}
.filter-bar {
margin-bottom: 12px;
padding: 10px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(6, minmax(130px, 1fr));
gap: 8px;
align-items: end;
}
.control {
min-width: 0;
}
.control label {
display: block;
margin: 0 0 4px;
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.control input,
.control select {
width: 100%;
min-height: 30px;
padding: 5px 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface-soft);
color: var(--ink);
font-size: 12px;
}
.control .range {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.control-hint {
display: block;
margin-top: 3px;
color: var(--weak);
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workbench {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, var(--inspector-width));
gap: 12px;
align-items: start;
}
.primary-stack {
display: grid;
gap: 12px;
min-width: 0;
}
.metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.metric {
min-height: 82px;
padding: 10px;
}
.metric.danger {
border-color: #efbab4;
background: #fff6f5;
}
.metric.warn {
border-color: #ebca8b;
background: #fffaf0;
}
.metric.ok {
border-color: #b8dfcb;
background: #f3fbf6;
}
.metric-label {
margin: 0;
color: var(--muted);
font-size: 11px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.metric-value {
margin: 5px 0 4px;
font-size: 21px;
line-height: 1.1;
font-weight: 850;
letter-spacing: 0;
}
.metric-foot {
color: var(--weak);
font-size: 11px;
}
.panel {
min-width: 0;
overflow: hidden;
}
.panel-head {
min-height: 42px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 11px;
border-bottom: 1px solid var(--line);
background: var(--surface-soft);
}
.panel-title {
margin: 0;
font-size: 13px;
font-weight: 850;
letter-spacing: 0;
}
.panel-body {
padding: 10px;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.status-card {
min-width: 0;
min-height: 82px;
display: grid;
align-content: start;
gap: 6px;
padding: 9px;
border: 1px solid var(--line);
border-radius: 7px;
background: #fbfcfc;
}
.card-label {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.status-card strong {
color: var(--ink);
font-size: 12px;
line-height: 1.35;
word-break: break-word;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.form-field {
min-width: 0;
padding: 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: #fbfcfc;
}
.form-field.wide {
grid-column: 1 / -1;
}
.form-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin: 0 0 5px;
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.fake-input {
min-height: 30px;
display: flex;
align-items: center;
padding: 5px 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface);
color: var(--ink);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.textarea-like {
min-height: 70px;
align-items: flex-start;
white-space: normal;
}
.table-wrap {
width: 100%;
overflow: auto;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
table {
width: 100%;
min-width: 860px;
border-collapse: collapse;
font-size: 12px;
}
th,
td {
padding: 8px 9px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: middle;
}
th {
position: sticky;
top: 0;
z-index: 1;
background: var(--surface-soft);
color: #364542;
font-size: 11px;
font-weight: 850;
white-space: nowrap;
vertical-align: top;
}
td {
padding-top: 7px;
padding-bottom: 7px;
color: #263330;
max-width: 210px;
}
tbody tr:hover td {
background: #fbfdfc;
}
.cell-main {
display: grid;
gap: 2px;
min-width: 0;
}
.cell-primary {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 750;
}
.cell-secondary {
color: var(--weak);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.row-actions {
display: inline-flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
min-width: 132px;
white-space: normal;
}
.small-btn {
min-height: 25px;
max-width: 116px;
padding: 3px 7px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
color: var(--ink);
font-size: 11px;
font-weight: 750;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.small-btn.danger {
border-color: #efbab4;
background: var(--danger-weak);
color: var(--danger);
}
.inspector {
position: sticky;
top: 72px;
min-width: 0;
overflow: hidden;
}
.inspector-head {
padding: 10px 11px;
border-bottom: 1px solid var(--line);
background: #fbfcfc;
}
.inspector-title {
margin: 0;
font-size: 13px;
font-weight: 850;
}
.inspector-meta {
margin: 4px 0 0;
color: var(--ink);
font-size: 12px;
font-weight: 750;
}
.detail-list {
display: grid;
margin: 0;
padding: 0;
list-style: none;
}
.detail-item {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 8px;
padding: 7px 11px;
border-bottom: 1px solid var(--line);
}
.detail-key {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.detail-value {
min-width: 0;
color: var(--ink);
font-size: 12px;
word-break: break-word;
}
.feedback-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.feedback-item {
min-width: 0;
padding: 8px 9px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface);
font-size: 12px;
}
.feedback-label {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.feedback-value {
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 750;
}
code {
padding: 1px 5px;
border-radius: 5px;
background: var(--surface-strong);
color: #263330;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.94em;
}
.error {
padding: 14px;
border: 1px solid #efbab4;
border-radius: var(--radius);
background: var(--danger-weak);
color: var(--danger);
font-size: 13px;
}
@media (max-width: 1280px) {
.app {
grid-template-columns: 238px minmax(0, 1fr);
}
.filter-grid {
grid-template-columns: repeat(4, minmax(128px, 1fr));
}
.metrics,
.scenario-grid,
.feedback-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 980px) {
.app {
display: block;
}
.topbar {
position: static;
}
.sidebar {
max-height: 280px;
border-right: 0;
border-bottom: 1px solid var(--line);
}
.workbench,
.page-head {
grid-template-columns: 1fr;
}
.inspector {
position: static;
}
}
@media (max-width: 720px) {
.main {
padding: 12px;
}
.topbar {
align-items: flex-start;
flex-direction: column;
}
.filter-grid,
.form-grid,
.metrics,
.scenario-grid,
.feedback-strip {
grid-template-columns: 1fr;
}
.page-title {
font-size: 21px;
}
.detail-item {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="brand-mark">M</div>
<div>
<p class="brand-title">Muse 管理员控制台</p>
<p class="brand-meta">系统能力 / 权限 / 市场 / 质量 / 用量治理</p>
</div>
</div>
<div class="top-actions">
<div class="top-user">
<span>权限管理员</span>
<span class="avatar">青</span>
</div>
</div>
</header>
<aside class="sidebar" aria-label="管理员导航" id="pageNav"></aside>
<main class="main">
<div id="screen" class="screen"></div>
</main>
</div>
<script>
const SOURCE_URL = "../产品-02B-管理员控制台功能规格.md";
const MAX_TABLE_COLUMNS = 7;
const ROW_COUNT = 6;
const state = {
doc: null,
activePageId: null
};
const people = ["林澈", "周言", "顾南", "许安", "陈序", "沈宁"];
const accounts = ["lin.che", "zhou.yan", "gu.nan", "xu.an", "chen.xu", "shen.ning"];
const objectNames = ["长篇玄幻模板", "角色关系图谱", "章节节拍策略", "世界规则资料集", "市场智能体包", "导入解析任务"];
const modules = ["平台配置", "元结构定义", "系统智能体", "全局知识库", "质量门控", "市场治理", "用户权限", "日志审计"];
const models = ["模型组 A", "模型组 B", "creative-main", "analysis-safe", "detect-guard"];
const scenarioMap = {
"admin-permission-group": {
title: "权限组发布场景",
steps: ["编辑菜单页面权限树", "预览影响范围", "提交复核", "非申请人审批后生效"],
blocked: "自己所在权限组、自审批、扩大管理域无复核时禁用发布。",
recovery: "保存草稿,交由其他权限管理员复核。"
},
"admin-market-asset-governance": {
title: "市场下架 / 召回场景",
steps: ["查看安装和绑定影响", "选择下架或召回范围", "确认通知范围", "写入治理状态和安全审计"],
blocked: "无原因、无影响预览、无合规权限时禁用召回。",
recovery: "保留申诉入口,不修改用户私有副本和已确认作品事实。"
},
"admin-newapi-usage": {
title: "New-API 调用日志归属场景",
steps: ["查询网关用户", "读取调用日志摘要", "匹配 Muse 用户/作品/任务", "记录归属依据"],
blocked: "不能伪造余额或复制外部网关日志为本地权威数据。",
recovery: "无法匹配时进入人工处理项。"
},
"admin-task-ops": {
title: "任务重验 / 重试场景",
steps: ["查看失败原因", "重验来源和授权快照", "判断可重试性", "重试或标记需用户重新发起"],
blocked: "写作候选不能由管理员重新生成;旧上下文失效时禁用重试。",
recovery: "取消、关闭人工处理项或引导用户侧重新发起。"
},
"admin-metaschema-editor": {
title: "MetaSchema 发布预览场景",
steps: ["保存草稿", "校验字段结构", "预览 Canonical/Shadow/检索/生成影响", "复核后发布"],
blocked: "迁移预览失败、影响范围未知或无复核时禁用发布。",
recovery: "保留草稿,回滚到上一已发布版本。"
},
"admin-audit-log": {
title: "审计导出场景",
steps: ["缩小筛选范围", "填写导出理由", "二次认证或复核", "生成脱敏摘要"],
blocked: "全文上下文、私密正文和系统凭据不可导出。",
recovery: "导出失败或过期仍写安全审计。"
}
};
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function renderInline(value) {
return escapeHtml(value)
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
}
function stripMd(value) {
return String(value ?? "").replaceAll("`", "").trim();
}
function splitItems(value) {
return String(value ?? "")
.split(/、|,|,|;|;/)
.map((item) => item.trim())
.filter(Boolean);
}
function splitRow(row) {
return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
}
function isSeparator(row) {
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(row.trim());
}
function getLinesBetween(lines, startPattern, endPattern) {
const start = lines.findIndex((line) => startPattern.test(line));
if (start < 0) return [];
const rest = lines.slice(start + 1);
const end = rest.findIndex((line) => endPattern.test(line));
return end < 0 ? rest : rest.slice(0, end);
}
function parseTable(lines) {
const tableLines = lines.filter((line) => line.trim().startsWith("|"));
const rows = tableLines.filter((line) => !isSeparator(line)).map(splitRow);
if (rows.length < 2) return { headers: [], rows: [] };
const headers = rows[0];
return {
headers,
rows: rows.slice(1).map((cells) => {
const row = {};
headers.forEach((header, index) => {
row[header] = cells[index] ?? "";
});
return row;
})
};
}
function parseSectionTables(lines, startPattern, endPattern) {
const sectionLines = getLinesBetween(lines, startPattern, endPattern);
const sections = [];
let current = { title: "", lines: [] };
sectionLines.forEach((line) => {
const heading = line.match(/^###\s+\d+(?:\.\d+)?\s+(.+)$/);
if (heading) {
if (current.title || current.lines.length) sections.push(current);
current = { title: heading[1].trim(), lines: [] };
} else {
current.lines.push(line);
}
});
if (current.title || current.lines.length) sections.push(current);
return sections
.map((section) => ({ title: section.title, table: parseTable(section.lines) }))
.filter((section) => section.title || section.table.rows.length);
}
function parseDoc(markdown) {
const lines = markdown.split(/\r?\n/);
const moduleTable = parseTable(getLinesBetween(lines, /^## 1\. 功能模块总览$/, /^## 2\./));
const pageTable = parseTable(getLinesBetween(lines, /^## 2\. 页面清单$/, /^## 3\./));
const pageSections = parseSectionTables(lines, /^## 3\. 单页规格$/, /^## 4\./);
const fieldTable = parseTable(getLinesBetween(lines, /^### 4\.3 页面字段与查询条件规格$/, /^## 5\./));
const pageSpecs = pageSections.map((section) => {
const spec = {};
section.table.rows.forEach((row) => {
spec[row["项目"]] = row["内容"];
});
const listRow = pageTable.rows.find((item) => item["页面名称"] === section.title) || {};
const moduleRow = moduleTable.rows.find((item) => item["主要页面"] && item["主要页面"].includes(section.title)) || {};
const id = stripMd(listRow["页面 ID"]) || section.title;
const fieldRow = fieldTable.rows.find((item) => (item["页面"] || "").includes(id) || (item["页面"] || "").includes(section.title)) || {};
return {
id,
name: section.title,
list: listRow,
module: moduleRow,
spec,
fields: fieldRow
};
});
return { moduleTable, pageTable, fieldTable, pageSpecs };
}
function getActivePage() {
return state.doc.pageSpecs.find((page) => page.id === state.activePageId) || state.doc.pageSpecs[0];
}
function fieldList(page, key, fallbackKey) {
return splitItems(page.fields[key] || page.spec[fallbackKey] || "");
}
function stateList(page) {
return splitItems(page.spec["状态"] || page.list["关键状态"] || "");
}
function actionsFrom(value) {
const raw = String(value ?? "").trim();
if (!raw || /^无|^不/.test(raw)) return [];
return splitItems(raw.split(";")[0].split(";")[0]).filter((item) => !/^无|^不/.test(item));
}
function buttonGroup(actions, kind = "") {
return actions.map((item) => `<button type="button" class="button ${kind}">${renderInline(item)}</button>`).join("");
}
function primaryActionGroup(actions) {
return actions.map((item, index) => `<button type="button" class="button ${index === 0 ? "primary" : ""}">${renderInline(item)}</button>`).join("");
}
function statusClass(value) {
if (/失败|异常|阻断|封禁|下架|拒绝|过期|不足|风险|冲突|不可/.test(value)) return "danger";
if (/待|处理中|运行|重试|申诉|补充|草稿|迁移|部分/.test(value)) return "warn";
if (/已|正常|启用|通过|完成|生效|可/.test(value)) return "ok";
return "blue";
}
function badge(value, extra = "") {
if (!value) return "";
return `<span class="badge ${extra || statusClass(value)}">${renderInline(value)}</span>`;
}
function currentState(page) {
const states = stateList(page);
return states.find((item) => !/失败|异常|无权限|不足|被拒绝|不可/.test(item)) || states[0] || "正常";
}
function sampleDate(rowIndex, withTime = true) {
const day = String(22 - (rowIndex % 5)).padStart(2, "0");
const hour = String(10 + rowIndex).padStart(2, "0");
const minute = String((rowIndex * 7 + 12) % 60).padStart(2, "0");
return withTime ? `2026-05-${day} ${hour}:${minute}` : `2026-05-${day}`;
}
function sampleId(prefix, rowIndex) {
return `${prefix}_${String(1024 + rowIndex * 37)}`;
}
function compactText(label, rowIndex, page) {
const pageWord = page.name.replace(/管理员|控制台|详情|列表|队列|处理/g, "").slice(0, 8) || "治理";
return `${pageWord}${label.replace(/摘要|记录|详情|说明|结果|状态/g, "").slice(0, 8)} ${rowIndex + 1}`;
}
function fakeValue(field, rowIndex, page, mode = "table") {
const label = stripMd(field);
const states = stateList(page);
const stateValue = states[rowIndex % Math.max(states.length, 1)] || "正常";
const person = people[rowIndex % people.length];
const account = accounts[rowIndex % accounts.length];
const objectName = objectNames[rowIndex % objectNames.length];
const todoObjects = ["市场资产上架待审", "质量评估任务失败", "New-API 余额不足", "全局知识处理失败", "权限变更待复核", "接口慢请求异常"];
const todoTypes = ["市场审核", "任务失败", "网关异常", "资料处理", "权限复核", "接口异常"];
const jumpTargets = ["市场审核队列", "质量评估结果", "New-API 与用量", "全局知识库详情", "用户、角色与权限", "接口调用日志"];
if (/待办对象摘要/.test(label)) return todoObjects[rowIndex % todoObjects.length];
if (/异常类型/.test(label)) return todoTypes[rowIndex % todoTypes.length];
if (/严重性/.test(label)) return ["高", "中", "低", "高", "中", "低"][rowIndex % 6];
if (/跳转目标/.test(label)) return jumpTargets[rowIndex % jumpTargets.length];
if (/最近处理记录/.test(label)) return `${person} ${sampleDate(rowIndex)} 更新`;
if (/无权限原因/.test(label)) return "无权限模块已隐藏";
if (/请求 ID|请求ID/.test(label)) return sampleId("req", rowIndex);
if (/审计事件 ID|事件 ID|事件ID/.test(label)) return sampleId("aud", rowIndex);
if (/审批单编号/.test(label)) return sampleId("approval", rowIndex);
if (/New-API 用户 ID/.test(label)) return sampleId("gw", rowIndex);
if (/Muse 用户 ID|用户 ID|用户ID/.test(label)) return sampleId("usr", rowIndex);
if (/资产 ID|资产ID/.test(label)) return sampleId("asset", rowIndex);
if (/对象 ID|来源 ID|任务/.test(label) && /ID|编号/.test(label)) return sampleId("obj", rowIndex);
if (/版本号|版本/.test(label)) return `v${1 + (rowIndex % 3)}.${rowIndex + 2}.${rowIndex}`;
if (/账号名/.test(label)) return account;
if (/调用人|操作者|发布者|申诉人|申请人|审批人|处理人|最近修改人|授权人|owner/.test(label)) return person;
if (/角色|权限组|用户类型|操作者角色/.test(label)) return ["权限管理员", "系统运维", "合规管理员", "市场审核员"][rowIndex % 4];
if (/名称|配置项|智能体名称|链路名称|评估集名称|资产|对象摘要|来源|发布材料/.test(label)) return objectName;
if (/类型|类别|业务模块|适用对象|来源类型|资产类型|动作|方法/.test(label)) {
if (/方法/.test(label)) return ["GET", "POST", "PUT", "DELETE"][rowIndex % 4];
return modules[rowIndex % modules.length];
}
if (/状态|结论|结果|回滚状态|治理状态|归属状态|脱敏状态|处理状态|调用状态|当前版本状态|留存状态/.test(label)) return stateValue;
if (/风险等级|风险|严重性|迁移风险|恶意能力/.test(label)) return ["低", "中", "高", "阻断"][rowIndex % 4];
if (/是否|可检索|可生成|允许|系统默认|需重验|外发|含真实|参与/.test(label)) return rowIndex % 3 === 0 ? "是" : "否";
if (/时间|日期|有效期|发布时间|更新时间|调用时间|授权时间|到期时间/.test(label)) return sampleDate(rowIndex);
if (/数量|次数|资料数量|字段数量|样本数量|安装数|绑定数|重试上限|重试次数|Token|耗时|状态码/.test(label)) {
if (/状态码/.test(label)) return ["200", "200", "403", "500"][rowIndex % 4];
if (/耗时/.test(label)) return `${120 + rowIndex * 37}ms`;
if (/Token/.test(label)) return `${(12 + rowIndex * 4).toLocaleString()}k`;
return String(8 + rowIndex * 13);
}
if (/率|通过率|接受率|撤销率|保留率|触发率/.test(label)) return `${72 + rowIndex * 3}%`;
if (/金额|余额|额度/.test(label)) return `¥${(68.4 + rowIndex * 25.7).toFixed(2)}`;
if (/模型|模型绑定/.test(label)) return models[rowIndex % models.length];
if (/接口名称/.test(label)) return ["查询用户列表", "发布质量策略", "查询审计日志", "配置授权"][rowIndex % 4];
if (/路径/.test(label)) return `/admin/${page.id.replace(/^admin-/, "")}/${rowIndex + 1}`;
if (/IP/.test(label)) return `10.0.${rowIndex + 1}.${42 + rowIndex}`;
if (/错误码/.test(label)) return rowIndex % 3 === 0 ? "PERMISSION_DENIED" : "-";
if (/许可|声明|范围|影响|摘要|原因|理由|策略|规则|记录|历史|材料|说明|工具|对象|缺口|建议|详情|日志|快照|字段|维度|阈值|展示方式|前状态|后状态/.test(label)) {
const text = compactText(label, rowIndex, page);
return mode === "detail" ? `${text},已按当前权限脱敏展示。` : text;
}
return compactText(label, rowIndex, page);
}
function fakeControlValue(label, index, page) {
if (/时间范围|有效期/.test(label)) return "";
if (/状态|风险|角色|权限组|类型|类别|模型|版本|动作|方法/.test(label)) return fakeValue(label, index, page);
if (/是否/.test(label)) return index % 2 === 0 ? "全部" : "是";
if (/ID|编号/.test(label)) return sampleId("query", index);
if (/账号/.test(label)) return accounts[index % accounts.length];
return "";
}
function renderFilterControl(label, method, index, page) {
const controlId = `filter-${index}`;
const hint = method || "";
if (/时间范围|有效期/.test(label)) {
return `
<div class="control">
<label for="${controlId}-start">${renderInline(label)}</label>
<div class="range">
<input id="${controlId}-start" type="date" value="2026-05-01" />
<input id="${controlId}-end" type="date" value="2026-05-22" />
</div>
<span class="control-hint">${renderInline(hint)}</span>
</div>
`;
}
if (/状态|风险|角色|权限组|类型|类别|模型|版本|动作|方法|是否/.test(label)) {
const options = ["全部", fakeControlValue(label, index, page), fakeValue(label, index + 1, page), fakeValue(label, index + 2, page)]
.filter(Boolean)
.filter((item, itemIndex, arr) => arr.indexOf(item) === itemIndex);
return `
<div class="control">
<label for="${controlId}">${renderInline(label)}</label>
<select id="${controlId}">
${options.map((option, optionIndex) => `<option ${optionIndex === 1 ? "selected" : ""}>${renderInline(option)}</option>`).join("")}
</select>
<span class="control-hint">${renderInline(hint)}</span>
</div>
`;
}
if (/耗时|金额|余额|额度/.test(label)) {
return `
<div class="control">
<label for="${controlId}-min">${renderInline(label)}</label>
<div class="range">
<input id="${controlId}-min" type="text" value="0" />
<input id="${controlId}-max" type="text" value="${/耗时/.test(label) ? "500ms" : "500"}" />
</div>
<span class="control-hint">${renderInline(hint)}</span>
</div>
`;
}
return `
<div class="control">
<label for="${controlId}">${renderInline(label)}</label>
<input id="${controlId}" type="text" value="${escapeHtml(fakeControlValue(label, index, page))}" placeholder="${escapeHtml(label)}" />
<span class="control-hint">${renderInline(hint)}</span>
</div>
`;
}
function renderFilters(page) {
const filters = fieldList(page, "查询条件", "");
const methods = fieldList(page, "查询方式", "");
if (!filters.length) return "";
return `
<section class="filter-bar" aria-label="查询条件">
<div class="filter-grid">
${filters.map((field, index) => renderFilterControl(field, methods[index] || methods[0] || "", index, page)).join("")}
<div class="control">
<label>&nbsp;</label>
<div class="action-row">
<button type="button" class="button primary">筛选</button>
<button type="button" class="button">重置</button>
</div>
</div>
</div>
</section>
`;
}
function metricFields(page) {
const listFields = fieldList(page, "列表 / 主区展示字段", "可见数据");
const preferred = listFields.filter((field) => /数量|异常|失败|风险|余额|额度|率|次数|趋势|状态|申诉|待审核|Token|安装数|绑定数/.test(field));
return (preferred.length ? preferred : listFields).slice(0, page.id === "admin-home" ? 7 : 4);
}
function metricValue(field, index, page) {
if (/状态|异常|失败|风险/.test(field) && !/数量|次数|率/.test(field)) return badge(fakeValue(field, index, page), "");
return renderInline(fakeValue(field, index, page));
}
function renderMetrics(page) {
const fields = metricFields(page);
if (!fields.length) return "";
return `
<section class="metrics" aria-label="关键指标">
${fields.map((field, index) => `
<article class="metric ${statusClass(fakeValue(field, index, page))}">
<p class="metric-label">${renderInline(field)}</p>
<div class="metric-value">${metricValue(field, index, page)}</div>
<div class="metric-foot">${renderInline(fakeValue("最近更新时间", index, page))}</div>
</article>
`).join("")}
</section>
`;
}
function tableColumns(page) {
const listFields = fieldList(page, "列表 / 主区展示字段", "可见数据");
if (page.id === "admin-home") return fieldList(page, "详情 / 抽屉展示字段", "可见数据").slice(0, MAX_TABLE_COLUMNS);
return listFields.slice(0, MAX_TABLE_COLUMNS);
}
function tableTitle(page) {
const regions = splitItems(page.spec["页面区域"]);
if (page.id === "admin-home") return "我的待办";
if (/日志/.test(page.name)) return "日志列表";
if (/队列/.test(page.name)) return "处理队列";
if (/列表/.test(page.name)) return "数据列表";
if (/结果/.test(page.name)) return "结果列表";
if (/观察|观测/.test(page.name)) return "观测数据";
return regions[0] || "主区";
}
function rowActionLabels(page) {
const primary = actionsFrom(page.spec["主操作"] || page.list["主要操作"]);
const secondary = actionsFrom(page.spec["次操作"]);
const dangerous = actionsFrom(page.spec["危险操作"]);
const safe = [...primary, ...secondary].filter((item) => !/筛选|搜索|刷新|导出|按|查看趋势/.test(item));
return {
safe: (safe.length ? safe : ["查看详情"]).slice(0, 2),
dangerous: dangerous.slice(0, 1)
};
}
function renderCell(field, rowIndex, page) {
const value = fakeValue(field, rowIndex, page);
if (/状态|结论|结果|风险|是否|可检索|可生成|允许|参与|默认/.test(field)) return badge(value);
if (/ID|编号|版本|路径|接口|方法|状态码|错误码/.test(field)) return `<code>${renderInline(value)}</code>`;
if (/名称|账号名|对象摘要|资产|来源|理由|摘要/.test(field)) {
return `
<div class="cell-main">
<span class="cell-primary">${renderInline(value)}</span>
<span class="cell-secondary">${renderInline(fakeValue("更新时间", rowIndex, page))}</span>
</div>
`;
}
return renderInline(value);
}
function renderTable(page) {
const columns = tableColumns(page);
if (!columns.length) return "";
const rowActions = rowActionLabels(page);
return `
<section class="panel">
<div class="panel-head">
<h2 class="panel-title">${renderInline(tableTitle(page))}</h2>
<div class="badge-row">${badge(`${ROW_COUNT} 条`, "blue")}</div>
</div>
<div class="panel-body">
<div class="table-wrap">
<table>
<thead>
<tr>
${columns.map((field) => `<th>${renderInline(field)}</th>`).join("")}
<th>操作</th>
</tr>
</thead>
<tbody>
${Array.from({ length: ROW_COUNT }).map((_, rowIndex) => `
<tr>
${columns.map((field) => `<td>${renderCell(field, rowIndex, page)}</td>`).join("")}
<td>
<span class="row-actions">
${rowActions.safe.map((action) => `<button type="button" class="small-btn">${renderInline(action)}</button>`).join("")}
${rowActions.dangerous.map((action) => `<button type="button" class="small-btn danger">${renderInline(action)}</button>`).join("")}
</span>
</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</div>
</section>
`;
}
function isFormLike(page) {
return /配置|编辑|策略|授权|详情|编排|权限组/.test(page.name);
}
function renderForm(page) {
if (!isFormLike(page)) return "";
const fields = [...fieldList(page, "列表 / 主区展示字段", "可见数据"), ...fieldList(page, "详情 / 抽屉展示字段", "")]
.filter((item, index, arr) => arr.indexOf(item) === index)
.slice(0, 10);
if (!fields.length) return "";
const primaryActions = actionsFrom(page.spec["主操作"]).slice(0, 3);
return `
<section class="panel">
<div class="panel-head">
<h2 class="panel-title">${renderInline(splitItems(page.spec["页面区域"])[0] || "配置表单")}</h2>
<div class="action-row">${buttonGroup(primaryActions, "primary")}</div>
</div>
<div class="panel-body">
<div class="form-grid">
${fields.map((field, index) => {
const longField = /说明|摘要|原因|理由|策略|影响|历史|材料|记录|日志|正文片段|风险片段|Prompt/.test(field);
return `
<div class="form-field ${longField ? "wide" : ""}">
<p class="form-label">
<span>${renderInline(field)}</span>
${/危险|风险|外发|权限|授权|发布|回滚/.test(field) ? badge("需确认", "warn") : ""}
</p>
<div class="fake-input ${longField ? "textarea-like" : ""}">${renderInline(fakeValue(field, index, page, "detail"))}</div>
</div>
`;
}).join("")}
</div>
</div>
</section>
`;
}
function detailTitle(page) {
if (page.id === "admin-home") return "待办详情";
if (/日志/.test(page.name)) return "日志详情";
if (/申诉/.test(page.name)) return "申诉详情";
if (/市场/.test(page.name)) return "资产详情";
if (/用户|权限/.test(page.name)) return "用户权限详情";
if (/New-API/.test(page.name)) return "网关用量详情";
if (/任务/.test(page.name)) return "任务详情";
return "对象详情";
}
function renderInspector(page) {
const details = fieldList(page, "详情 / 抽屉展示字段", "");
const visibleDetails = details.length ? details : tableColumns(page).slice(0, 6);
const dangerActions = actionsFrom(page.spec["危险操作"]);
const rowActions = rowActionLabels(page).safe.slice(0, 2);
return `
<aside class="inspector" aria-label="对象详情">
<div class="inspector-head">
<p class="inspector-title">${renderInline(detailTitle(page))}</p>
<p class="inspector-meta">${renderInline(fakeValue("对象摘要", 0, page))}</p>
</div>
<ul class="detail-list">
${visibleDetails.slice(0, 11).map((field, index) => `
<li class="detail-item">
<span class="detail-key">${renderInline(field)}</span>
<span class="detail-value">${renderCell(field, index, page)}</span>
</li>
`).join("")}
</ul>
<div class="panel-body">
<div class="action-row">
${buttonGroup(rowActions)}
${buttonGroup(dangerActions.slice(0, 2), "danger")}
</div>
</div>
</aside>
`;
}
function renderFeedback(page) {
const fields = fieldList(page, "操作反馈字段", "").slice(0, 4);
if (!fields.length) return "";
return `
<section class="feedback-strip" aria-label="操作反馈">
${fields.map((field, index) => `
<div class="feedback-item">
<div class="feedback-label">${renderInline(field)}</div>
<div class="feedback-value">${renderInline(fakeValue(field, index, page, "detail"))}</div>
</div>
`).join("")}
</section>
`;
}
function renderScenarioPanel(page) {
const scenario = scenarioMap[page.id];
if (!scenario) return "";
return `
<section class="panel">
<div class="panel-head">
<h2 class="panel-title">${renderInline(scenario.title)}</h2>
<div class="badge-row">${badge("端到端验收", "warn")}</div>
</div>
<div class="panel-body">
<div class="scenario-grid">
${scenario.steps.map((step, index) => `
<article class="status-card">
<span class="card-label">步骤 ${index + 1}</span>
<strong>${renderInline(step)}</strong>
</article>
`).join("")}
</div>
<div class="feedback-strip" style="margin-top:12px">
<div class="feedback-item">
<div class="feedback-label">禁用条件</div>
<div class="feedback-value">${renderInline(scenario.blocked)}</div>
</div>
<div class="feedback-item">
<div class="feedback-label">失败恢复</div>
<div class="feedback-value">${renderInline(scenario.recovery)}</div>
</div>
</div>
</div>
</section>
`;
}
function renderPageHeader(page) {
const mainActions = actionsFrom(page.spec["主操作"] || page.list["主要操作"]).slice(0, 4);
const secondaryActions = actionsFrom(page.spec["次操作"]).slice(0, 3);
const selectedState = currentState(page);
return `
<section class="page-head">
<div>
<div class="page-kicker">
<span>${renderInline(page.module["模块名称"] || "管理员控制台")}</span>
${badge(selectedState)}
</div>
<h1 class="page-title">${renderInline(page.name)}</h1>
<div class="page-role">${renderInline(page.spec["使用角色"] || page.list["使用角色"] || "")}</div>
</div>
<div class="action-row">
${buttonGroup(secondaryActions)}
${primaryActionGroup(mainActions)}
</div>
</section>
`;
}
function renderScreen() {
const page = getActivePage();
const showForm = isFormLike(page);
document.getElementById("screen").innerHTML = `
${renderPageHeader(page)}
${renderFilters(page)}
<div class="workbench">
<div class="primary-stack">
${renderMetrics(page)}
${renderScenarioPanel(page)}
${showForm ? renderForm(page) : ""}
${renderTable(page)}
${renderFeedback(page)}
</div>
${renderInspector(page)}
</div>
`;
renderNav();
}
function navGroups() {
const groups = [];
state.doc.moduleTable.rows.forEach((moduleRow) => {
const pages = state.doc.pageSpecs.filter((page) => page.module["模块名称"] === moduleRow["模块名称"]);
if (pages.length) groups.push({ title: moduleRow["模块名称"], pages });
});
const groupedIds = new Set(groups.flatMap((group) => group.pages.map((page) => page.id)));
const rest = state.doc.pageSpecs.filter((page) => !groupedIds.has(page.id));
if (rest.length) groups.push({ title: "其他页面", pages: rest });
return groups;
}
function renderNav() {
const nav = document.getElementById("pageNav");
nav.innerHTML = navGroups().map((group) => `
<section class="nav-section">
<p class="nav-title">${renderInline(group.title)}</p>
${group.pages.map((page) => `
<button type="button" class="nav-button ${page.id === state.activePageId ? "active" : ""}" data-page="${escapeHtml(page.id)}">
<span class="nav-name">${renderInline(page.name)}</span>
<span class="nav-dot"></span>
</button>
`).join("")}
</section>
`).join("");
nav.querySelectorAll("[data-page]").forEach((button) => {
button.addEventListener("click", () => {
state.activePageId = button.dataset.page;
history.replaceState(null, "", `#${encodeURIComponent(state.activePageId)}`);
renderScreen();
});
});
}
async function init() {
try {
const response = await fetch(SOURCE_URL, { cache: "no-store" });
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
const markdown = await response.text();
state.doc = parseDoc(markdown);
const hashPage = decodeURIComponent(location.hash.replace(/^#/, ""));
state.activePageId = state.doc.pageSpecs.some((page) => page.id === hashPage) ? hashPage : state.doc.pageSpecs[0]?.id || null;
renderNav();
renderScreen();
} catch (error) {
document.getElementById("screen").innerHTML = `<div class="error">无法读取原型来源:${escapeHtml(error.message)}</div>`;
}
}
init();
</script>
</body>
</html>