zcbot/web/static/admin.html

406 lines
21 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: #f4f6f8; --panel: #ffffff; --panel-soft: #f8fafb;
--border: #dfe3e7; --border-soft: #edf0f2;
--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: 6px; --r-lg: 12px;
--shadow-soft: 0 1px 2px rgba(18,24,30,.04), 0 8px 24px 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.55;
-webkit-font-smoothing: antialiased;
}
header {
position: sticky; top: 0; z-index: 10; min-height: 64px;
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: 16px; padding: 10px max(20px, calc((100vw - 1480px) / 2));
}
header .brand { display: flex; align-items: center; gap: 11px; min-width: 0; }
header .logo {
width: 34px; height: 34px; flex: 0 0 auto; border-radius: 9px;
background: linear-gradient(135deg, var(--accent), #8e2a20);
box-shadow: 0 5px 14px rgba(184,58,49,.22);
color: #fff; font-weight: 750; font-size: 15px;
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: 15px; line-height: 1.25; }
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: 7px; }
header a, header button {
min-height: 32px; display: inline-flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 550; color: var(--text); text-decoration: none;
padding: 5px 11px; 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: 28px 24px 48px; max-width: 1480px; margin: 0 auto; }
.msg { padding: 40px 16px; text-align: center; color: var(--muted); }
.msg a { color: var(--accent); }
/* 目录 + 内容两栏;目录 sticky 跟随滚动 */
#layout { display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: 24px; align-items: start; }
#toc {
position: sticky; top: 88px; display: flex; flex-direction: column; gap: 3px;
padding: 10px; background: rgba(255,255,255,.76); border: 1px solid var(--border);
border-radius: var(--r-lg); box-shadow: var(--shadow-soft);
}
#toc .toc-label { padding: 5px 10px 8px; color: #959ba1; font-size: 10px; font-weight: 700; letter-spacing: 1px; }
#toc a {
display: flex; align-items: center; gap: 9px; min-height: 36px;
color: var(--muted); text-decoration: none; font-size: 12px; padding: 7px 10px;
border-radius: 7px; 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: 18px; padding: 22px 24px;
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: -70px; top: -90px; width: 220px; height: 220px;
border: 42px solid rgba(184,58,49,.055); border-radius: 50%; pointer-events: none;
}
.page-intro h1 { position: relative; margin: 0; color: var(--text-strong); font-size: 20px; line-height: 1.35; }
.page-intro p { position: relative; margin: 6px 0 0; color: var(--muted); font-size: 12px; }
.anchor { scroll-margin-top: 82px; }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.card-head h2 { margin: 0; }
.ctrl { display: flex; gap: 6px; }
.ctrl select {
min-height: 32px; font-size: 12px; padding: 4px 28px 4px 9px; border: 1px solid var(--border);
border-radius: var(--r-md); background: #fff; color: var(--text);
}
.sublabel { color: var(--muted); font-size: 11px; margin-bottom: 4px; }
.grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg);
padding: 18px 20px; margin-bottom: 16px; box-shadow: var(--shadow-soft);
}
.card h2 { margin: 0 0 13px; font-size: 13px; color: var(--text-strong); font-weight: 650; letter-spacing: .15px; }
/* 大数字 stat 块 */
.stat { position: relative; overflow: hidden; min-height: 102px; background: linear-gradient(145deg,#fff,var(--panel-soft)); border: 1px solid var(--border-soft); border-radius: 10px; padding: 14px 16px; }
.stat::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: #d9dde1; }
.stat .k { color: var(--muted); font-size: 12px; }
.stat .v { color: var(--text-strong); font-size: 23px; font-weight: 650; font-family: var(--mono); margin-top: 7px; letter-spacing: -.5px; }
.stat .sub { color: var(--muted); font-size: 11px; margin-top: 2px; }
.stat.warn { border-color: #efd8b7; background: #fffaf2; }
.stat.warn::before { background: var(--warn); }
.stat.warn .v { color: var(--warn); }
.stat.danger { border-color: #edc1bd; background: var(--accent-soft); }
.stat.danger::before { background: var(--danger); }
.stat.danger .v { color: var(--danger); }
/* chips(状态分布) */
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
font-size: 12px; padding: 3px 9px; 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: 9px 10px; text-align: right; border-bottom: 1px solid var(--border-soft); white-space: nowrap; }
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); background: var(--panel-soft); font-weight: 600; }
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; }
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: 90px; }
.node-table th:nth-child(5), .node-table td:nth-child(5) { 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-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: 12px; justify-content: flex-end; margin-top: 10px; }
.pager button {
min-height: 32px; font-size: 12px; padding: 4px 12px; 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 (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: 18px; }
.page-intro h1 { font-size: 18px; }
.anchor { scroll-margin-top: 150px; }
.card { padding: 15px; }
}
@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>