zcbot/web/static/admin.html

460 lines
25 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>zcbot 管理后台</title>
<style>
:root {
color-scheme: light;
--bg: #f5f6f7; --panel: #ffffff; --panel-soft: #f7f8f9;
--border: #dfe3e6; --border-soft: #eceff1;
--text: #202428; --text-strong: #121619; --muted: #70777f;
--accent: #b83a31; --accent-hover: #a9322a; --accent-soft: #fbecea;
--accent-ring: rgba(184,58,49,.16);
--ok: #238653; --warn: #b66a16; --danger: #b83a31;
--r-md: 5px; --r-lg: 8px;
--shadow-soft: 0 1px 2px rgba(18,24,30,.035);
--mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text); background: var(--bg); font-size: 13px; line-height: 1.45;
-webkit-font-smoothing: antialiased;
}
header {
position: sticky; top: 0; z-index: 10; min-height: 54px;
background: rgba(255,255,255,.94); backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border); box-shadow: 0 1px 0 rgba(18,24,30,.02);
display: flex; align-items: center; gap: 12px; padding: 8px max(18px, calc((100vw - 1480px) / 2));
}
header .brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
header .logo {
width: 30px; height: 30px; flex: 0 0 auto; border-radius: 7px;
background: linear-gradient(135deg, var(--accent), #8e2a20);
box-shadow: 0 3px 9px rgba(184,58,49,.18);
color: #fff; font-weight: 750; font-size: 14px;
display: flex; align-items: center; justify-content: center;
}
header .brand-copy { min-width: 0; }
header .title { color: var(--text-strong); font-weight: 650; font-size: 14px; line-height: 1.2; }
header .eyebrow { margin-top: 1px; color: var(--muted); font-size: 10px; letter-spacing: .7px; text-transform: uppercase; }
header .spacer { flex: 1; }
header .meta { color: var(--muted); font-size: 12px; font-family: var(--mono); }
header .actions { display: flex; align-items: center; gap: 6px; }
header a, header button {
min-height: 30px; display: inline-flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 550; color: var(--text); text-decoration: none;
padding: 4px 10px; border: 1px solid var(--border); border-radius: var(--r-md);
background: #fff; cursor: pointer; transition: .15s ease;
}
header a:hover, header button:hover { border-color: #c8cdd2; background: var(--panel-soft); color: var(--accent); }
header a { color: var(--accent); border-color: #edcfcc; }
header label.auto { color: var(--muted); display: flex; align-items: center; gap: 6px; padding: 0 4px; cursor: pointer; white-space: nowrap; }
input[type="checkbox"] { accent-color: var(--accent); }
main { padding: 16px 18px 30px; max-width: 1480px; margin: 0 auto; }
.msg { padding: 28px 14px; text-align: center; color: var(--muted); }
.msg a { color: var(--accent); }
/* 目录 + 内容两栏;目录 sticky 跟随滚动 */
#layout { display: grid; grid-template-columns: 172px minmax(0, 1fr); gap: 16px; align-items: start; }
#toc {
position: sticky; top: 70px; display: flex; flex-direction: column; gap: 2px;
padding: 7px; background: rgba(255,255,255,.82); border: 1px solid var(--border);
border-radius: var(--r-lg); box-shadow: var(--shadow-soft);
}
#toc .toc-label { padding: 4px 8px 6px; color: #959ba1; font-size: 10px; font-weight: 700; letter-spacing: .8px; }
#toc a {
display: flex; align-items: center; gap: 7px; min-height: 31px;
color: var(--muted); text-decoration: none; font-size: 12px; padding: 5px 8px;
border-radius: 5px; border: 1px solid transparent; transition: .15s ease;
}
#toc .nav-index { width: 20px; color: #a0a6ac; font: 10px/1 var(--mono); }
#toc a:hover { background: var(--panel-soft); color: var(--text-strong); }
#toc a.active { color: var(--accent); border-color: #f0d5d2; background: var(--accent-soft); font-weight: 650; }
#toc a.active .nav-index { color: var(--accent); }
#content { min-width: 0; }
.page-intro {
position: relative; overflow: hidden; margin-bottom: 10px; padding: 13px 16px;
border: 1px solid #e6d9d7; border-radius: var(--r-lg);
background: linear-gradient(120deg, #fff 0%, #fff 54%, #fdf4f2 100%); box-shadow: var(--shadow-soft);
}
.page-intro::after {
content: ""; position: absolute; right: -55px; top: -75px; width: 170px; height: 170px;
border: 32px solid rgba(184,58,49,.045); border-radius: 50%; pointer-events: none;
}
.page-intro h1 { position: relative; margin: 0; color: var(--text-strong); font-size: 17px; line-height: 1.3; }
.page-intro p { position: relative; margin: 3px 0 0; color: var(--muted); font-size: 11px; }
.anchor { scroll-margin-top: 68px; }
/* 首屏运营摘要:跨模块聚合关键状态,点击卡片跳到对应明细。 */
.ops-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 10px; }
.summary-card {
min-width: 0; min-height: 112px; display: flex; flex-direction: column; padding: 10px 12px 9px;
color: inherit; text-decoration: none; background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r-lg); box-shadow: var(--shadow-soft); transition: .15s ease;
}
.summary-card:hover { border-color: #c9ced2; box-shadow: 0 3px 10px rgba(18,24,30,.06); transform: translateY(-1px); }
.summary-card .label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.summary-card .label { min-width: 0; color: var(--muted); font-size: 11px; font-weight: 600; }
.summary-card .jump { flex: 0 0 auto; color: #a0a6ac; font-size: 12px; }
.summary-card .summary-value { margin-top: 4px; color: var(--text-strong); font: 650 21px/1.2 var(--mono); letter-spacing: -.4px; }
.summary-card .summary-value .unit { color: var(--muted); font-size: 11px; font-weight: 550; letter-spacing: 0; }
.summary-card .summary-sub { margin-top: 3px; overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.summary-card .summary-meta { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 7px; color: var(--muted); font-size: 10px; }
.summary-card .summary-meta span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary-card .summary-meta span:last-child { text-align: right; }
.summary-card .summary-meter { height: 3px; margin-top: 5px; overflow: hidden; border-radius: 999px; background: #e8ebed; }
.summary-card .summary-meter > i { display: block; width: var(--value); height: 100%; border-radius: inherit; background: #89939d; }
.summary-card.warn { border-color: #efd8b7; background: #fffaf2; }
.summary-card.warn .summary-value { color: var(--warn); }
.summary-card.warn .summary-meter > i { background: var(--warn); }
.summary-card.danger { border-color: #edc1bd; background: var(--accent-soft); }
.summary-card.danger .summary-value { color: var(--danger); }
.summary-card.danger .summary-meter > i { background: var(--danger); }
.summary-spark { height: 18px; display: flex; align-items: flex-end; gap: 2px; margin-top: 5px; }
.summary-spark i { flex: 1; min-width: 2px; height: var(--value); border-radius: 2px 2px 0 0; background: rgba(184,58,49,.28); }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
.card-head h2 { margin: 0; }
.ctrl { display: flex; gap: 6px; }
.ctrl select {
min-height: 30px; font-size: 12px; padding: 3px 26px 3px 8px; border: 1px solid var(--border);
border-radius: var(--r-md); background: #fff; color: var(--text);
}
.sublabel { color: var(--muted); font-size: 10px; margin-bottom: 3px; font-weight: 600; letter-spacing: .2px; }
.grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg);
padding: 12px 14px; margin-bottom: 10px; box-shadow: var(--shadow-soft);
}
.card h2 { margin: 0 0 8px; font-size: 13px; color: var(--text-strong); font-weight: 650; letter-spacing: .1px; }
/* 大数字 stat 块 */
.stat { position: relative; overflow: hidden; min-height: 102px; display: flex; flex-direction: column; background: linear-gradient(145deg,#fff,var(--panel-soft)); border: 1px solid var(--border-soft); border-radius: 7px; padding: 10px 12px 9px; }
.stat::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: #d9dde1; }
.stat .k { color: var(--muted); font-size: 11px; }
.stat .v { color: var(--text-strong); font-size: 20px; font-weight: 650; font-family: var(--mono); margin-top: 3px; letter-spacing: -.4px; line-height: 1.25; }
.stat .v .unit { color: var(--muted); font-size: 12px; font-weight: 550; letter-spacing: 0; }
.stat .sub { color: var(--muted); font-size: 11px; margin-top: 2px; }
.stat .foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 7px; color: var(--muted); font-size: 10px; }
.stat .foot span:last-child { text-align: right; }
.stat .meter { height: 3px; margin-top: 5px; overflow: hidden; border-radius: 999px; background: #e8ebed; }
.stat .meter > i { display: block; width: var(--value); height: 100%; border-radius: inherit; background: #89939d; }
.stat.warn { border-color: #efd8b7; background: #fffaf2; }
.stat.warn::before { background: var(--warn); }
.stat.warn .v { color: var(--warn); }
.stat.warn .meter > i { background: var(--warn); }
.stat.danger { border-color: #edc1bd; background: var(--accent-soft); }
.stat.danger::before { background: var(--danger); }
.stat.danger .v { color: var(--danger); }
.stat.danger .meter > i { background: var(--danger); }
/* chips(状态分布) */
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
.chip {
font-size: 11px; padding: 2px 8px; border-radius: 999px;
background: #f1f1f1; color: var(--text); font-family: var(--mono);
}
.chip b { font-weight: 700; }
.chip.err { background: var(--accent-soft); color: var(--danger); }
.chip.run { background: #e7f3ff; color: #1565c0; }
.chip.ok { background: #e8f5e9; color: var(--ok); }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; }
th, td { padding: 7px 9px; text-align: right; border-bottom: 1px solid var(--border-soft); white-space: nowrap; }
th:first-child, td:first-child { text-align: left; }
th { color: #626a72; background: var(--panel-soft); font-size: 11px; font-weight: 650; letter-spacing: .1px; }
thead th:first-child { border-radius: 7px 0 0 7px; }
thead th:last-child { border-radius: 0 7px 7px 0; }
tbody tr { transition: background-color .12s ease; }
tbody tr:hover { background: #fafbfc; }
tbody tr:last-child td { border-bottom-color: transparent; }
td.num { font-family: var(--mono); }
td.num.hot { color: var(--danger); font-weight: 700; }
td.trend { font-family: var(--mono); letter-spacing: 1px; color: var(--accent); }
tr.quiet td { opacity: .45; }
td.email { font-family: var(--mono); max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.bar-cell { position: relative; }
tfoot .total-row td { border-top: 2px solid var(--border); border-bottom: none; font-weight: 700; }
.scroll-x { overflow-x: auto; scrollbar-color: #cbd1d6 transparent; scrollbar-width: thin; }
.empty { color: var(--muted); padding: 8px; text-align: center; }
#ext-admin-form label { display: grid; gap: 3px; color: var(--muted); font-size: 12px; }
#ext-admin-form [hidden] { display: none; }
#ext-admin-form input:not([type="checkbox"]), #ext-admin-form select {
width: 100%; padding: 6px 8px; border: 1px solid var(--border);
border-radius: var(--r-md); color: var(--text); background: #fff;
}
#ext-admin-form button, #s-external button {
font-size: 12px; padding: 4px 9px; border: 1px solid var(--border);
border-radius: var(--r-md); background: #fff; cursor: pointer;
}
#s-external button.primary, #ext-admin-form button.primary {
color: #fff; border-color: var(--accent); background: var(--accent);
}
#external-definition-modal .card {
width: min(820px, calc(100vw - 32px)); max-height: calc(100vh - 32px);
margin: 0; padding: 0; display: flex; flex-direction: column;
}
#ext-admin-form { display: flex; flex: 1; min-height: 0; flex-direction: column; }
.ext-modal-head {
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.ext-modal-head h3 { flex: 1; margin: 0; font-size: 14px; }
.ext-modal-head button { font-size: 18px !important; line-height: 1; border: 0 !important; }
.ext-form-body { min-height: 0; padding: 16px; overflow-y: auto; overscroll-behavior: contain; }
.ext-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; }
.ext-form-section {
grid-column: 1 / -1; margin: 4px 0 0; padding-top: 10px;
border-top: 1px solid var(--border-soft); color: var(--text); font-weight: 600;
}
.ext-form-actions {
display: flex; justify-content: flex-end; gap: 8px; padding: 10px 16px;
border-top: 1px solid var(--border); background: #fff;
}
/* 复用主控制台 dialog.js 的单例页内弹框。 */
.modal {
display: none; position: fixed; inset: 0; z-index: 130;
align-items: center; justify-content: center; padding: 16px;
background: rgba(0,0,0,.32);
}
.modal.show { display: flex; }
html.modal-open, body.modal-open { overflow: hidden; }
#app-dialog .card { width: min(560px, calc(100vw - 32px)); margin: 0; padding: 0; }
#app-dialog h3 { margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--border); font-size: 14px; }
#app-dialog .body { padding: 16px; }
#app-dialog label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 6px; }
#app-dialog input, #app-dialog textarea {
width: 100%; padding: 8px 10px; border: 1px solid var(--border);
border-radius: var(--r-md); color: var(--text); background: #fff; resize: vertical;
}
#app-dialog input:focus, #app-dialog textarea:focus { outline: none; border-color: var(--accent); }
#app-dialog .actions { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--border); }
#app-dialog button { padding: 5px 12px; border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; cursor: pointer; }
#app-dialog button.primary { color: #fff; border-color: var(--accent); background: var(--accent); }
#s-windows-node button, #node-enrollment-modal button {
font-size: 12px; padding: 5px 11px; border: 1px solid var(--border);
border-radius: var(--r-md); background: #fff; color: var(--text); cursor: pointer;
}
#s-windows-node button.primary, #node-enrollment-modal button.primary {
color: #fff; border-color: var(--accent); background: var(--accent);
}
#s-windows-node button.danger {
color: var(--danger); border-color: #edc1bd; background: #fff;
}
#s-windows-node button:disabled { opacity: .55; cursor: default; }
.node-status {
display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px;
border-radius: 999px; background: #f1f1f1; color: var(--muted);
}
.node-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.node-status.online { color: var(--ok); background: #e8f5e9; }
.node-status.offline { color: var(--muted); }
.node-status.disabled { color: var(--danger); background: var(--accent-soft); }
.node-name { font-weight: 600; }
.node-id { color: var(--muted); font: 11px/1.4 var(--mono); }
.node-version { margin-top: 3px; color: var(--muted); font: 11px/1.4 var(--mono); }
.node-table { min-width: 820px; }
.node-table th:nth-child(1), .node-table td:nth-child(1) { width: 260px; }
.node-table th:nth-child(2), .node-table td:nth-child(2) { width: 80px; text-align: left; }
.node-table th:nth-child(3), .node-table td:nth-child(3) { text-align: left; }
.node-table th:nth-child(4), .node-table td:nth-child(4) { width: 92px; text-align: center; }
.node-table th:nth-child(5), .node-table td:nth-child(5) { width: 90px; }
.node-table th:nth-child(6), .node-table td:nth-child(6) { width: 150px; }
.node-capabilities { min-width: 360px; }
.node-capability { display: flex; align-items: center; gap: 8px; min-width: max-content; }
.node-capability + .node-capability { margin-top: 5px; }
.node-capability-version { color: var(--muted); font: 11px/1.4 var(--mono); }
.node-empty-capability { color: var(--muted); }
.node-task-count {
display: inline-flex; align-items: center; justify-content: center; min-width: 28px;
padding: 2px 8px; border-radius: 999px; background: #f1f3f4; color: var(--muted);
font: 600 12px/1.5 var(--mono);
}
.node-task-count.busy { background: #fff0de; color: var(--warn); }
.node-actions { text-align: right; }
#node-enrollment-modal .card { width: min(520px, calc(100vw - 32px)); margin: 0; padding: 0; }
.node-modal-head, .node-modal-actions {
display: flex; align-items: center; gap: 8px; padding: 11px 16px;
border-bottom: 1px solid var(--border);
}
.node-modal-head h3 { flex: 1; margin: 0; font-size: 14px; }
.node-modal-head button { padding: 2px 8px !important; border: 0 !important; font-size: 18px !important; }
.node-modal-body { padding: 16px; }
.node-modal-body label { display: block; margin-bottom: 6px; color: var(--muted); font-size: 12px; }
.node-modal-body input {
width: 100%; padding: 8px 10px; border: 1px solid var(--border);
border-radius: var(--r-md); color: var(--text); background: #fff;
}
.node-modal-actions { justify-content: flex-end; border-top: 1px solid var(--border); border-bottom: 0; }
.node-help { margin: 8px 0 0; color: var(--muted); font-size: 12px; }
.node-code-wrap { display: flex; gap: 8px; align-items: stretch; }
.node-code {
flex: 1; min-width: 0; padding: 10px; border: 1px solid var(--border);
border-radius: var(--r-md); background: #f8f8f8; font: 600 13px/1.5 var(--mono);
overflow-wrap: anywhere; user-select: all;
}
.node-warning {
margin: 0 0 12px; padding: 9px 10px; border-left: 3px solid var(--warn);
background: #fff8ec; color: #7a520d; font-size: 12px;
}
#app-messages {
position: fixed; top: 16px; left: 50%; z-index: 200;
display: flex; flex-direction: column; align-items: center; gap: 8px;
transform: translateX(-50%); pointer-events: none;
}
.app-msg {
padding: 8px 13px; border: 1px solid var(--border); border-radius: var(--r-md);
background: #fff; box-shadow: 0 4px 14px rgba(0,0,0,.12);
opacity: 0; transform: translateY(-6px); transition: opacity .15s, transform .15s;
pointer-events: auto;
}
.app-msg.in { opacity: 1; transform: translateY(0); }
.app-msg.success { border-color: #b8ddb9; background: #f1faf1; color: var(--ok); }
.app-msg.error { border-color: #edc1bd; background: var(--accent-soft); color: var(--danger); }
.pager { display: flex; align-items: center; gap: 8px; justify-content: flex-end; margin-top: 7px; }
.pager button {
min-height: 30px; font-size: 12px; padding: 3px 10px; border: 1px solid var(--border); border-radius: var(--r-md);
background: #fff; cursor: pointer; color: var(--text);
}
.pager button:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.pager button:disabled { opacity: .45; cursor: default; }
.pager .pginfo { color: var(--muted); font-size: 12px; font-family: var(--mono); }
button, select, input, textarea { font: inherit; }
button:focus-visible, a:focus-visible, select:focus-visible, input:focus-visible, textarea:focus-visible {
outline: 2px solid var(--accent); outline-offset: 2px;
}
select:focus, input:focus, textarea:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 3px var(--accent-ring); outline: none; }
@media (min-width: 821px) and (max-width: 1080px) {
#layout { grid-template-columns: 148px minmax(0, 1fr); gap: 12px; }
#toc .nav-index { display: none; }
.ops-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 820px) {
main { padding: 10px; }
.grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.stat .v { font-size: 18px; }
/* header 紧凑化:缩 padding/gap、字号,gen-at 时间戳截断不撑宽 */
header { padding: 8px 10px; gap: 8px; flex-wrap: wrap; }
header .title { font-size: 14px; }
header .eyebrow { display: none; }
header .meta { font-size: 11px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
header a, header button { padding: 4px 8px; }
header .actions { order: 3; width: 100%; overflow-x: auto; padding-bottom: 1px; }
header .actions > * { flex: 0 0 auto; }
/* card-head 允许换行,标题长 + 下拉不再撑出横向溢出 */
.card-head { flex-wrap: wrap; }
.ctrl { flex-wrap: wrap; }
.ctrl select { min-width: 0; }
.ext-form-grid { grid-template-columns: 1fr; }
.ext-form-grid > label, .ext-form-section { grid-column: 1 !important; }
.node-code-wrap { flex-direction: column; }
/* 窄屏:目录变顶部横向 chip 条 */
#layout { grid-template-columns: 1fr; gap: 10px; }
#toc {
flex-direction: row; overflow-x: auto; gap: 6px; top: 109px;
background: rgba(244,246,248,.94); backdrop-filter: blur(8px); padding: 6px 0; z-index: 5;
border: 0; border-radius: 0; box-shadow: none;
}
#toc .toc-label, #toc .nav-index { display: none; }
#toc a { min-height: 32px; white-space: nowrap; border: 1px solid var(--border); background: #fff; }
#toc a.active { border-color: var(--accent); }
.page-intro { padding: 12px 14px; }
.page-intro h1 { font-size: 17px; }
.ops-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.anchor { scroll-margin-top: 150px; }
.card { padding: 12px; }
}
@media (max-width: 480px) {
.ops-summary { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
}
/* 屏幕上隐藏打印报告;打印时只显它 */
#print-report { display: none; }
@media print {
header, #layout, .msg { display: none !important; }
body { background: #fff; }
#print-report { display: block; color: #000; }
#print-report h1 { font-size: 18px; margin: 0 0 4px; }
#print-report h2 { font-size: 13px; margin: 14px 0 4px; border-bottom: 1px solid #999; padding-bottom: 2px; }
#print-report .rpt-meta { font-size: 11px; color: #555; margin-bottom: 8px; }
#print-report .rpt-kv { font-size: 12px; margin: 2px 0 4px; }
table.rpt { width: 100%; border-collapse: collapse; font-size: 11px; margin: 4px 0 6px; }
table.rpt th, table.rpt td { border: 1px solid #bbb; padding: 3px 6px; text-align: right; }
table.rpt th:first-child, table.rpt td:first-child { text-align: left; }
table.rpt th { background: #f0f0f0; }
@page { margin: 14mm; }
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="logo">Z</div>
<div class="brand-copy">
<div class="title">zcbot 管理后台</div>
<div class="eyebrow">Operations Console</div>
</div>
</div>
<span class="meta" id="gen-at"></span>
<div class="spacer"></div>
<div class="actions">
<label class="auto"><input type="checkbox" id="auto-refresh" checked /> 自动刷新</label>
<button id="refresh">刷新数据</button>
<button id="export">导出 PDF</button>
<a href="/static/dev.html">返回控制台</a>
</div>
</header>
<main id="main">
<div class="msg" id="boot">加载中…</div>
</main>
<!-- 导出 PDF:屏幕隐藏,仅 @media print 显示;exportPdf() 现填充后 window.print() -->
<div id="print-report"></div>
<div id="app-dialog" class="modal"></div>
<div id="node-enrollment-modal" class="modal" aria-hidden="true">
<div class="card" role="dialog" aria-modal="true" aria-labelledby="node-enrollment-title">
<div class="node-modal-head">
<h3 id="node-enrollment-title">生成 Windows Node 注册码</h3>
<button id="node-enrollment-close" type="button" aria-label="关闭">×</button>
</div>
<form id="node-enrollment-form">
<div class="node-modal-body" id="node-enrollment-inputs">
<label for="node-expected-name">限定节点名称(可选)</label>
<input id="node-expected-name" maxlength="120" autocomplete="off" placeholder="例如:实验室绘图节点 01" />
<p class="node-help">填写后,客户端注册时必须使用完全相同的节点名称。注册码有效期为 10 分钟;节点能力由客户端自动发现并上报。</p>
</div>
<div class="node-modal-body" id="node-enrollment-result" hidden>
<p class="node-warning">该注册码只能成功使用一次,关闭窗口后无法再次查看。请立即复制并发送到目标电脑。</p>
<label>一次性注册码</label>
<div class="node-code-wrap">
<code class="node-code" id="node-enrollment-code"></code>
<button id="node-enrollment-copy" type="button">复制</button>
</div>
<p class="node-help" id="node-enrollment-expiry"></p>
</div>
<div class="node-modal-actions">
<button id="node-enrollment-cancel" type="button">取消</button>
<button id="node-enrollment-submit" class="primary" type="submit">生成注册码</button>
</div>
</form>
</div>
</div>
<script type="module" src="/static/js/admin.js"></script>
</body>
</html>