1581 lines
48 KiB
HTML
1581 lines
48 KiB
HTML
<!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("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
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> </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>
|