Refine admin UI themes
This commit is contained in:
parent
12e58cee2e
commit
8666589e79
191
docs/theme-preview-dark.html
Normal file
191
docs/theme-preview-dark.html
Normal file
@ -0,0 +1,191 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>3588 管理后台深色主题预览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #090909;
|
||||
--panel: #121212;
|
||||
--panel-2: #1a1a1a;
|
||||
--panel-3: #242424;
|
||||
--line: #303030;
|
||||
--line-strong: #565656;
|
||||
--text: #f4f4f4;
|
||||
--muted: #bcbcbc;
|
||||
--subtle: #8d8d8d;
|
||||
--accent: #dddddd;
|
||||
--accent-2: #66c98f;
|
||||
--warn: #d8a657;
|
||||
--bad: #e46f72;
|
||||
--blue: #dddddd;
|
||||
--sidebar: #070707;
|
||||
--sidebar-active: #171717;
|
||||
--selected: #303030;
|
||||
--radius: 4px;
|
||||
--shadow: 0 18px 44px rgba(0, 0, 0, .32);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.45 "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
.shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
||||
aside {
|
||||
background: var(--sidebar);
|
||||
border-right: 1px solid #303030;
|
||||
padding: 18px 14px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }
|
||||
.mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 4px; background: #070707; border: 1px solid #3a3a3a; font-weight: 700; }
|
||||
.brand b { display: block; font-size: 14px; font-weight: 600; }
|
||||
.brand span { color: var(--muted); font-size: 11px; }
|
||||
.section { color: var(--subtle); font-size: 11px; margin: 18px 8px 8px; letter-spacing: .06em; }
|
||||
nav a { display: flex; align-items: center; gap: 10px; color: #eeeeee; text-decoration: none; padding: 9px 10px; border-radius: var(--radius); margin-bottom: 3px; font-weight: 500; }
|
||||
nav a.active, nav a:hover { background: var(--sidebar-active); color: #fff; }
|
||||
.nav-icon { width: 26px; height: 24px; display: grid; place-items: center; border-radius: 3px; border: 1px solid #3a3a3a; color: var(--accent); font-size: 10px; }
|
||||
.workspace { min-width: 0; }
|
||||
header {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28px;
|
||||
background: #0e0e0e;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.crumb { color: var(--subtle); font-size: 11px; letter-spacing: .08em; }
|
||||
h1 { margin: 2px 0 0; font-size: 18px; font-weight: 600; }
|
||||
main { padding: 22px 28px 36px; max-width: 1460px; }
|
||||
.toolbar, .grid, .row { display: grid; gap: 12px; }
|
||||
.toolbar { grid-template-columns: 1fr auto; align-items: end; margin-bottom: 14px; }
|
||||
.summary { color: var(--muted); margin-top: 4px; }
|
||||
.btn { border: 1px solid var(--line-strong); background: var(--panel-3); color: var(--text); padding: 6px 9px; border-radius: var(--radius); text-decoration: none; font-weight: 500; font-size: 12px; line-height: 1.15; }
|
||||
.btn.primary { background: #2b2b2b; border-color: #565656; color: #f4f4f4; }
|
||||
.grid.kpi { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 14px; }
|
||||
.panel, .stat {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.stat { padding: 14px; border-left: 3px solid #3a3a3a; }
|
||||
.stat.hot { border-left-color: var(--accent); }
|
||||
.label { color: var(--muted); font-size: 12px; }
|
||||
.value { margin-top: 7px; font-size: 24px; line-height: 1; font-weight: 400; }
|
||||
.delta { margin-top: 7px; color: var(--subtle); font-size: 12px; }
|
||||
.row.two { grid-template-columns: 1.45fr .8fr; align-items: start; }
|
||||
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
|
||||
h2 { font-size: 15px; margin: 0; font-weight: 600; }
|
||||
.panel-body { padding: 12px 14px; }
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); padding: 0 12px; background: var(--panel-2); }
|
||||
.tabs span { padding: 8px 11px; color: var(--muted); border-bottom: 2px solid transparent; line-height: 1.2; }
|
||||
.tabs span.active { color: var(--text); border-color: var(--accent); background: #101010; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 760px; }
|
||||
.table-wrap { overflow: auto; }
|
||||
th, td { padding: 8px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; line-height: 1.25; }
|
||||
th { color: #bcbcbc; background: #1a1a1a; font-size: 12px; font-weight: 600; }
|
||||
tbody tr.selected { background: var(--selected); outline: 1px solid #dddddd; outline-offset: -1px; }
|
||||
tbody tr:hover { background: #1a1a1a; }
|
||||
.mono { font-family: Consolas, ui-monospace, monospace; }
|
||||
.pill { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); background: var(--panel-3); border-radius: 3px; padding: 1px 6px; min-height: 20px; font-size: 11px; font-weight: 500; line-height: 1.1; }
|
||||
.ok { color: var(--accent-2); border-color: #565656; background: #1a1a1a; }
|
||||
.run { color: var(--blue); border-color: #565656; background: #1a1a1a; }
|
||||
.warn { color: var(--warn); border-color: #565656; background: #1a1a1a; }
|
||||
.bad { color: var(--bad); border-color: #565656; background: #1a1a1a; }
|
||||
.dense-list { display: grid; gap: 8px; }
|
||||
.event { display: grid; grid-template-columns: 72px 1fr auto; gap: 10px; align-items: center; padding: 7px 9px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
td b, .event b { font-weight: 400; }
|
||||
.preview-map { height: 160px; background: linear-gradient(135deg, #242424, #101010); border: 1px solid var(--line); position: relative; overflow: hidden; border-radius: var(--radius); }
|
||||
.preview-map::before, .preview-map::after { content: ""; position: absolute; inset: 18px; border: 1px solid #3a3a3a; }
|
||||
.preview-map::after { inset: 44px 70px 36px 34px; border-color: #666666; background: rgba(255,255,255,.06); }
|
||||
@media (max-width: 960px) {
|
||||
.shell, .row.two, .grid.kpi, .toolbar { grid-template-columns: 1fr; }
|
||||
aside { position: relative; height: auto; }
|
||||
main { padding: 18px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<aside>
|
||||
<div class="brand"><div class="mark">AI</div><div><b>视觉识别运维平台</b><span>Fleet Operations Console</span></div></div>
|
||||
<div class="section">主模块</div>
|
||||
<nav>
|
||||
<a class="active" href="#"><span class="nav-icon">OV</span>总览</a>
|
||||
<a href="#"><span class="nav-icon">DV</span>设备</a>
|
||||
<a href="#"><span class="nav-icon">CFG</span>识别配置</a>
|
||||
<a href="#"><span class="nav-icon">JOB</span>任务</a>
|
||||
<a href="#"><span class="nav-icon">LOG</span>诊断</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="workspace">
|
||||
<header>
|
||||
<div><div class="crumb">多设备视觉识别运维平台</div><h1>总览</h1></div>
|
||||
<div><a class="btn" href="theme-preview-gold.html">石墨金色</a> <a class="btn primary" href="theme-preview-light.html">切换浅色主题</a></div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="toolbar">
|
||||
<div><h2>Fleet 运行态势</h2><div class="summary">只保留需要运维立即判断的信息:在线率、异常节点、执行中任务、最近告警。</div></div>
|
||||
<div><a class="btn" href="#">批量操作</a> <a class="btn primary" href="#">进入设备列表</a></div>
|
||||
</div>
|
||||
<section class="grid kpi">
|
||||
<div class="stat hot"><div class="label">设备总数</div><div class="value">12</div><div class="delta">3 个分组 / 2 个车间</div></div>
|
||||
<div class="stat hot"><div class="label">在线率</div><div class="value">10 / 12</div><div class="delta">83.3%,较昨日 -1</div></div>
|
||||
<div class="stat"><div class="label">离线节点</div><div class="value">2</div><div class="delta">需要检查网络或 agent</div></div>
|
||||
<div class="stat"><div class="label">执行中任务</div><div class="value">3</div><div class="delta">配置下发 2,重启 1</div></div>
|
||||
</section>
|
||||
<section class="row two">
|
||||
<div class="panel">
|
||||
<div class="tabs"><span class="active">异常设备</span><span>全部设备</span><span>任务结果</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>节点</th><th>状态</th><th>识别配置</th><th>管理地址</th><th>最后心跳</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="selected"><td><b>OPI-3588-A03</b><div class="mono label">edge-a03</div></td><td><span class="pill bad">离线</span></td><td>车间全功能识别</td><td class="mono">10.0.0.81:18081</td><td>18 分钟前</td></tr>
|
||||
<tr><td><b>OPI-3588-B02</b><div class="mono label">edge-b02</div></td><td><span class="pill warn">告警</span></td><td>劳保鞋检测</td><td class="mono">10.0.0.92:18081</td><td>37 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C01</b><div class="mono label">edge-c01</div></td><td><span class="pill ok">在线</span></td><td>人脸识别视频流</td><td class="mono">10.0.0.77:18081</td><td>9 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C04</b><div class="mono label">edge-c04</div></td><td><span class="pill run">任务中</span></td><td>服务测试流</td><td class="mono">10.0.0.79:18081</td><td>12 秒前</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-head"><h2>识别配置资产</h2><span class="pill run">模板 5</span></div>
|
||||
<div class="panel-body">
|
||||
<div class="preview-map"></div>
|
||||
<div class="dense-list" style="margin-top:12px">
|
||||
<div class="event"><span class="mono">13/12</span><b>车间全功能识别</b><span class="pill ok">标准</span></div>
|
||||
<div class="event"><span class="mono">7/6</span><b>人脸识别视频流</b><span class="pill">用户</span></div>
|
||||
<div class="event"><span class="mono">10/9</span><b>劳保鞋检测</b><span class="pill ok">标准</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" style="margin-top:12px">
|
||||
<div class="panel-head"><h2>最近任务</h2><span class="label">选中行用高亮背景和描边表示,不再依赖低对比浅灰</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>任务标识</th><th>操作</th><th>状态</th><th>节点数</th><th>耗时</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">task-0428-0018</td><td>下发识别配置</td><td><span class="pill run">执行中</span></td><td>4</td><td>01:42</td></tr>
|
||||
<tr class="selected"><td class="mono">task-0428-0017</td><td>重启视频分析服务</td><td><span class="pill bad">失败</span></td><td>1</td><td>00:19</td></tr>
|
||||
<tr><td class="mono">task-0428-0016</td><td>导出诊断日志</td><td><span class="pill ok">成功</span></td><td>2</td><td>00:43</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
190
docs/theme-preview-gold.html
Normal file
190
docs/theme-preview-gold.html
Normal file
@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>3588 管理后台石墨金色主题预览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #171717;
|
||||
--panel: #202020;
|
||||
--panel-2: #262626;
|
||||
--panel-3: #303030;
|
||||
--line: #3c3c3c;
|
||||
--line-strong: #5a5447;
|
||||
--text: #ece7da;
|
||||
--muted: #aaa296;
|
||||
--subtle: #7f796e;
|
||||
--accent: #d3a84f;
|
||||
--accent-2: #f0cf7a;
|
||||
--warn: #d79a3b;
|
||||
--bad: #d66a63;
|
||||
--blue: #8db3d9;
|
||||
--sidebar: #121212;
|
||||
--sidebar-active: #2a271f;
|
||||
--selected: #332d20;
|
||||
--radius: 4px;
|
||||
--shadow: 0 18px 44px rgba(0, 0, 0, .3);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.45 "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
.shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
||||
aside {
|
||||
background: var(--sidebar);
|
||||
border-right: 1px solid #2f2f2f;
|
||||
padding: 18px 14px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }
|
||||
.mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 4px; background: #090909; border: 1px solid #5a4a2d; color: var(--accent-2); font-weight: 800; }
|
||||
.brand b { display: block; font-size: 14px; color: var(--accent-2); }
|
||||
.brand span { color: var(--muted); font-size: 11px; }
|
||||
.section { color: var(--subtle); font-size: 11px; margin: 18px 8px 8px; letter-spacing: .06em; }
|
||||
nav a { display: flex; align-items: center; gap: 10px; color: #d1cbc1; text-decoration: none; padding: 9px 10px; border-radius: var(--radius); margin-bottom: 3px; }
|
||||
nav a.active, nav a:hover { background: var(--sidebar-active); color: #fff5dc; }
|
||||
.nav-icon { width: 26px; height: 24px; display: grid; place-items: center; border-radius: 3px; border: 1px solid #4f4430; color: var(--accent); font-size: 10px; }
|
||||
.workspace { min-width: 0; }
|
||||
header {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28px;
|
||||
background: #1b1b1b;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.crumb { color: var(--subtle); font-size: 11px; letter-spacing: .08em; }
|
||||
h1 { margin: 2px 0 0; font-size: 18px; color: var(--accent-2); }
|
||||
main { padding: 22px 28px 36px; max-width: 1460px; }
|
||||
.toolbar, .grid, .row { display: grid; gap: 12px; }
|
||||
.toolbar { grid-template-columns: 1fr auto; align-items: end; margin-bottom: 14px; }
|
||||
.summary { color: var(--muted); margin-top: 4px; }
|
||||
.btn { border: 1px solid var(--line-strong); background: var(--panel-3); color: var(--text); padding: 6px 9px; border-radius: var(--radius); text-decoration: none; font-weight: 700; font-size: 12px; line-height: 1.15; }
|
||||
.btn.primary { background: #5d431e; border-color: #9a7438; color: #fff3d2; }
|
||||
.grid.kpi { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 14px; }
|
||||
.panel, .stat {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.stat { padding: 14px; border-left: 3px solid #4a4a4a; }
|
||||
.stat.hot { border-left-color: var(--accent); }
|
||||
.label { color: var(--muted); font-size: 12px; }
|
||||
.value { margin-top: 7px; font-size: 24px; line-height: 1; font-weight: 800; color: var(--accent-2); }
|
||||
.delta { margin-top: 7px; color: var(--subtle); font-size: 12px; }
|
||||
.row.two { grid-template-columns: 1.45fr .8fr; align-items: start; }
|
||||
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
|
||||
h2 { font-size: 15px; margin: 0; color: var(--accent-2); }
|
||||
.panel-body { padding: 12px 14px; }
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); padding: 0 12px; background: var(--panel-2); }
|
||||
.tabs span { padding: 8px 11px; color: var(--muted); border-bottom: 2px solid transparent; line-height: 1.2; }
|
||||
.tabs span.active { color: var(--accent-2); border-color: var(--accent); background: #1d1d1d; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 760px; }
|
||||
.table-wrap { overflow: auto; }
|
||||
th, td { padding: 8px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; line-height: 1.25; }
|
||||
th { color: #d8caa8; background: #191919; font-size: 12px; font-weight: 800; }
|
||||
tbody tr.selected { background: var(--selected); outline: 1px solid #806331; outline-offset: -1px; }
|
||||
tbody tr:hover { background: #292724; }
|
||||
.mono { font-family: Consolas, ui-monospace, monospace; }
|
||||
.pill { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); background: var(--panel-3); border-radius: 3px; padding: 1px 6px; min-height: 20px; font-size: 11px; font-weight: 800; line-height: 1.1; }
|
||||
.ok { color: var(--accent-2); border-color: #806331; background: #2d2617; }
|
||||
.run { color: var(--blue); border-color: #536b82; background: #1d2730; }
|
||||
.warn { color: var(--warn); border-color: #775a2e; background: #302514; }
|
||||
.bad { color: var(--bad); border-color: #744348; background: #301d1c; }
|
||||
.dense-list { display: grid; gap: 8px; }
|
||||
.event { display: grid; grid-template-columns: 72px 1fr auto; gap: 10px; align-items: center; padding: 7px 9px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.preview-map { height: 160px; background: linear-gradient(135deg, #242424, #161616); border: 1px solid var(--line); position: relative; overflow: hidden; border-radius: var(--radius); }
|
||||
.preview-map::before, .preview-map::after { content: ""; position: absolute; inset: 18px; border: 1px solid #444; }
|
||||
.preview-map::after { inset: 44px 70px 36px 34px; border-color: #8b6b35; background: rgba(211,168,79,.08); }
|
||||
@media (max-width: 960px) {
|
||||
.shell, .row.two, .grid.kpi, .toolbar { grid-template-columns: 1fr; }
|
||||
aside { position: relative; height: auto; }
|
||||
main { padding: 18px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<aside>
|
||||
<div class="brand"><div class="mark">AI</div><div><b>视觉识别运维平台</b><span>Fleet Operations Console</span></div></div>
|
||||
<div class="section">主模块</div>
|
||||
<nav>
|
||||
<a class="active" href="#"><span class="nav-icon">OV</span>总览</a>
|
||||
<a href="#"><span class="nav-icon">DV</span>设备</a>
|
||||
<a href="#"><span class="nav-icon">CFG</span>识别配置</a>
|
||||
<a href="#"><span class="nav-icon">JOB</span>任务</a>
|
||||
<a href="#"><span class="nav-icon">LOG</span>诊断</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="workspace">
|
||||
<header>
|
||||
<div><div class="crumb">多设备视觉识别运维平台</div><h1>总览</h1></div>
|
||||
<div><a class="btn" href="theme-preview-dark.html">蓝黑主题</a> <a class="btn primary" href="theme-preview-light.html">浅色主题</a></div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="toolbar">
|
||||
<div><h2>Fleet 运行态势</h2><div class="summary">石墨灰作为主体,金色只用于标题、关键数字和选中态,保持工业控制台的克制感。</div></div>
|
||||
<div><a class="btn" href="#">批量操作</a> <a class="btn primary" href="#">进入设备列表</a></div>
|
||||
</div>
|
||||
<section class="grid kpi">
|
||||
<div class="stat hot"><div class="label">设备总数</div><div class="value">12</div><div class="delta">3 个分组 / 2 个车间</div></div>
|
||||
<div class="stat hot"><div class="label">在线率</div><div class="value">10 / 12</div><div class="delta">83.3%,较昨日 -1</div></div>
|
||||
<div class="stat"><div class="label">离线节点</div><div class="value">2</div><div class="delta">需要检查网络或 agent</div></div>
|
||||
<div class="stat"><div class="label">执行中任务</div><div class="value">3</div><div class="delta">配置下发 2,重启 1</div></div>
|
||||
</section>
|
||||
<section class="row two">
|
||||
<div class="panel">
|
||||
<div class="tabs"><span class="active">异常设备</span><span>全部设备</span><span>任务结果</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>节点</th><th>状态</th><th>识别配置</th><th>管理地址</th><th>最后心跳</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="selected"><td><b>OPI-3588-A03</b><div class="mono label">edge-a03</div></td><td><span class="pill bad">离线</span></td><td>车间全功能识别</td><td class="mono">10.0.0.81:18081</td><td>18 分钟前</td></tr>
|
||||
<tr><td><b>OPI-3588-B02</b><div class="mono label">edge-b02</div></td><td><span class="pill warn">告警</span></td><td>劳保鞋检测</td><td class="mono">10.0.0.92:18081</td><td>37 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C01</b><div class="mono label">edge-c01</div></td><td><span class="pill ok">在线</span></td><td>人脸识别视频流</td><td class="mono">10.0.0.77:18081</td><td>9 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C04</b><div class="mono label">edge-c04</div></td><td><span class="pill run">任务中</span></td><td>服务测试流</td><td class="mono">10.0.0.79:18081</td><td>12 秒前</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-head"><h2>识别配置资产</h2><span class="pill run">模板 5</span></div>
|
||||
<div class="panel-body">
|
||||
<div class="preview-map"></div>
|
||||
<div class="dense-list" style="margin-top:12px">
|
||||
<div class="event"><span class="mono">13/12</span><b>车间全功能识别</b><span class="pill ok">标准</span></div>
|
||||
<div class="event"><span class="mono">7/6</span><b>人脸识别视频流</b><span class="pill">用户</span></div>
|
||||
<div class="event"><span class="mono">10/9</span><b>劳保鞋检测</b><span class="pill ok">标准</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" style="margin-top:12px">
|
||||
<div class="panel-head"><h2>最近任务</h2><span class="label">金色强调更有质感,但大面积使用会降低告警色的辨识度</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>任务标识</th><th>操作</th><th>状态</th><th>节点数</th><th>耗时</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">task-0428-0018</td><td>下发识别配置</td><td><span class="pill run">执行中</span></td><td>4</td><td>01:42</td></tr>
|
||||
<tr class="selected"><td class="mono">task-0428-0017</td><td>重启视频分析服务</td><td><span class="pill bad">失败</span></td><td>1</td><td>00:19</td></tr>
|
||||
<tr><td class="mono">task-0428-0016</td><td>导出诊断日志</td><td><span class="pill ok">成功</span></td><td>2</td><td>00:43</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
190
docs/theme-preview-light.html
Normal file
190
docs/theme-preview-light.html
Normal file
@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>3588 管理后台浅色主题预览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #dfe7f0;
|
||||
--panel: #eef4fa;
|
||||
--panel-2: #e5edf6;
|
||||
--panel-3: #d5e1ef;
|
||||
--line: #b5c4d6;
|
||||
--line-strong: #8499b3;
|
||||
--text: #172333;
|
||||
--muted: #4f6177;
|
||||
--subtle: #6a7b8e;
|
||||
--accent: #2d6fb5;
|
||||
--accent-2: #245f9f;
|
||||
--warn: #9b650e;
|
||||
--bad: #a43f3f;
|
||||
--blue: #2b6fac;
|
||||
--sidebar: #111923;
|
||||
--sidebar-active: #21303a;
|
||||
--selected: #c7d9ee;
|
||||
--radius: 4px;
|
||||
--shadow: 0 14px 34px rgba(35, 50, 60, .12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.45 "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
.shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
||||
aside {
|
||||
background: var(--sidebar);
|
||||
border-right: 1px solid #23303b;
|
||||
padding: 18px 14px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; color: #e7edf2; }
|
||||
.mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 4px; background: #090f15; border: 1px solid #364653; font-weight: 800; }
|
||||
.brand b { display: block; font-size: 14px; }
|
||||
.brand span { color: #9aa7b2; font-size: 11px; }
|
||||
.section { color: #6f7d88; font-size: 11px; margin: 18px 8px 8px; letter-spacing: .06em; }
|
||||
nav a { display: flex; align-items: center; gap: 10px; color: #c7d2dc; text-decoration: none; padding: 9px 10px; border-radius: var(--radius); margin-bottom: 3px; }
|
||||
nav a.active, nav a:hover { background: var(--sidebar-active); color: #fff; }
|
||||
.nav-icon { width: 26px; height: 24px; display: grid; place-items: center; border-radius: 3px; border: 1px solid #33455f; color: #5aa7ff; font-size: 10px; }
|
||||
.workspace { min-width: 0; }
|
||||
header {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28px;
|
||||
background: #e8eef6;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.crumb { color: var(--subtle); font-size: 11px; letter-spacing: .08em; }
|
||||
h1 { margin: 2px 0 0; font-size: 18px; }
|
||||
main { padding: 22px 28px 36px; max-width: 1460px; }
|
||||
.toolbar, .grid, .row { display: grid; gap: 12px; }
|
||||
.toolbar { grid-template-columns: 1fr auto; align-items: end; margin-bottom: 14px; }
|
||||
.summary { color: var(--muted); margin-top: 4px; }
|
||||
.btn { border: 1px solid var(--line-strong); background: var(--panel-3); color: var(--text); padding: 6px 9px; border-radius: var(--radius); text-decoration: none; font-weight: 700; font-size: 12px; line-height: 1.15; }
|
||||
.btn.primary { background: #245f9f; border-color: #245f9f; color: #f3f8ff; }
|
||||
.grid.kpi { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 14px; }
|
||||
.panel, .stat {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.stat { padding: 14px; border-left: 3px solid #96a8bd; }
|
||||
.stat.hot { border-left-color: var(--accent); }
|
||||
.label { color: var(--muted); font-size: 12px; }
|
||||
.value { margin-top: 7px; font-size: 24px; line-height: 1; font-weight: 800; }
|
||||
.delta { margin-top: 7px; color: var(--subtle); font-size: 12px; }
|
||||
.row.two { grid-template-columns: 1.45fr .8fr; align-items: start; }
|
||||
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
|
||||
h2 { font-size: 15px; margin: 0; }
|
||||
.panel-body { padding: 12px 14px; }
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); padding: 0 12px; background: var(--panel-2); }
|
||||
.tabs span { padding: 8px 11px; color: var(--muted); border-bottom: 2px solid transparent; line-height: 1.2; }
|
||||
.tabs span.active { color: var(--text); border-color: var(--accent); background: var(--panel); }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 760px; }
|
||||
.table-wrap { overflow: auto; }
|
||||
th, td { padding: 8px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; line-height: 1.25; }
|
||||
th { color: #293b51; background: #d9e5f2; font-size: 12px; font-weight: 800; }
|
||||
tbody tr.selected { background: var(--selected); outline: 1px solid #6e98ca; outline-offset: -1px; }
|
||||
tbody tr:hover { background: #e2ebf5; }
|
||||
.mono { font-family: Consolas, ui-monospace, monospace; }
|
||||
.pill { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); background: var(--panel-3); border-radius: 3px; padding: 1px 6px; min-height: 20px; font-size: 11px; font-weight: 800; line-height: 1.1; }
|
||||
.ok { color: var(--accent-2); border-color: #83a9d0; background: #dbeafa; }
|
||||
.run { color: var(--blue); border-color: #84a9c7; background: #ddeaf5; }
|
||||
.warn { color: var(--warn); border-color: #d0a561; background: #f0e2c8; }
|
||||
.bad { color: var(--bad); border-color: #ca8888; background: #f0dada; }
|
||||
.dense-list { display: grid; gap: 8px; }
|
||||
.event { display: grid; grid-template-columns: 72px 1fr auto; gap: 10px; align-items: center; padding: 7px 9px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.preview-map { height: 160px; background: linear-gradient(135deg, #e4edf7, #cfdae8); border: 1px solid var(--line); position: relative; overflow: hidden; border-radius: var(--radius); }
|
||||
.preview-map::before, .preview-map::after { content: ""; position: absolute; inset: 18px; border: 1px solid #a8b9ca; }
|
||||
.preview-map::after { inset: 44px 70px 36px 34px; border-color: #6e98ca; background: rgba(45,111,181,.1); }
|
||||
@media (max-width: 960px) {
|
||||
.shell, .row.two, .grid.kpi, .toolbar { grid-template-columns: 1fr; }
|
||||
aside { position: relative; height: auto; }
|
||||
main { padding: 18px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<aside>
|
||||
<div class="brand"><div class="mark">AI</div><div><b>视觉识别运维平台</b><span>Fleet Operations Console</span></div></div>
|
||||
<div class="section">主模块</div>
|
||||
<nav>
|
||||
<a class="active" href="#"><span class="nav-icon">OV</span>总览</a>
|
||||
<a href="#"><span class="nav-icon">DV</span>设备</a>
|
||||
<a href="#"><span class="nav-icon">CFG</span>识别配置</a>
|
||||
<a href="#"><span class="nav-icon">JOB</span>任务</a>
|
||||
<a href="#"><span class="nav-icon">LOG</span>诊断</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="workspace">
|
||||
<header>
|
||||
<div><div class="crumb">多设备视觉识别运维平台</div><h1>总览</h1></div>
|
||||
<a class="btn primary" href="theme-preview-dark.html">切换深色主题</a>
|
||||
</header>
|
||||
<main>
|
||||
<div class="toolbar">
|
||||
<div><h2>Fleet 运行态势</h2><div class="summary">浅色主题统一为蓝灰工业面板,与蓝黑深色主题保持同一套色相。</div></div>
|
||||
<div><a class="btn" href="#">批量操作</a> <a class="btn primary" href="#">进入设备列表</a></div>
|
||||
</div>
|
||||
<section class="grid kpi">
|
||||
<div class="stat hot"><div class="label">设备总数</div><div class="value">12</div><div class="delta">3 个分组 / 2 个车间</div></div>
|
||||
<div class="stat hot"><div class="label">在线率</div><div class="value">10 / 12</div><div class="delta">83.3%,较昨日 -1</div></div>
|
||||
<div class="stat"><div class="label">离线节点</div><div class="value">2</div><div class="delta">需要检查网络或 agent</div></div>
|
||||
<div class="stat"><div class="label">执行中任务</div><div class="value">3</div><div class="delta">配置下发 2,重启 1</div></div>
|
||||
</section>
|
||||
<section class="row two">
|
||||
<div class="panel">
|
||||
<div class="tabs"><span class="active">异常设备</span><span>全部设备</span><span>任务结果</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>节点</th><th>状态</th><th>识别配置</th><th>管理地址</th><th>最后心跳</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="selected"><td><b>OPI-3588-A03</b><div class="mono label">edge-a03</div></td><td><span class="pill bad">离线</span></td><td>车间全功能识别</td><td class="mono">10.0.0.81:18081</td><td>18 分钟前</td></tr>
|
||||
<tr><td><b>OPI-3588-B02</b><div class="mono label">edge-b02</div></td><td><span class="pill warn">告警</span></td><td>劳保鞋检测</td><td class="mono">10.0.0.92:18081</td><td>37 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C01</b><div class="mono label">edge-c01</div></td><td><span class="pill ok">在线</span></td><td>人脸识别视频流</td><td class="mono">10.0.0.77:18081</td><td>9 秒前</td></tr>
|
||||
<tr><td><b>OPI-3588-C04</b><div class="mono label">edge-c04</div></td><td><span class="pill run">任务中</span></td><td>服务测试流</td><td class="mono">10.0.0.79:18081</td><td>12 秒前</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-head"><h2>识别配置资产</h2><span class="pill run">模板 5</span></div>
|
||||
<div class="panel-body">
|
||||
<div class="preview-map"></div>
|
||||
<div class="dense-list" style="margin-top:12px">
|
||||
<div class="event"><span class="mono">13/12</span><b>车间全功能识别</b><span class="pill ok">标准</span></div>
|
||||
<div class="event"><span class="mono">7/6</span><b>人脸识别视频流</b><span class="pill">用户</span></div>
|
||||
<div class="event"><span class="mono">10/9</span><b>劳保鞋检测</b><span class="pill ok">标准</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" style="margin-top:12px">
|
||||
<div class="panel-head"><h2>最近任务</h2><span class="label">浅色主题仍保留明显选中行,不回到低对比白灰表格</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>任务标识</th><th>操作</th><th>状态</th><th>节点数</th><th>耗时</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">task-0428-0018</td><td>下发识别配置</td><td><span class="pill run">执行中</span></td><td>4</td><td>01:42</td></tr>
|
||||
<tr class="selected"><td class="mono">task-0428-0017</td><td>重启视频分析服务</td><td><span class="pill bad">失败</span></td><td>1</td><td>00:19</td></tr>
|
||||
<tr><td class="mono">task-0428-0016</td><td>导出诊断日志</td><td><span class="pill ok">成功</span></td><td>2</td><td>00:43</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -373,6 +373,8 @@ func tablerIconSVG(name string) string {
|
||||
"assets": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 19l4 -14"/><path d="M16 5l4 14"/><path d="M12 5v14"/><path d="M6 15h12"/></svg>`,
|
||||
"audit": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 8l0 4l2 2"/><path d="M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"/></svg>`,
|
||||
"system": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c.996 .608 2.296 .07 2.572 -1.065z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
||||
"theme": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3a9 9 0 1 0 9 9a4.5 4.5 0 0 1 -9 -9"/></svg>`,
|
||||
"bell": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3l2 3h-16l2 -3v-3a7 7 0 0 1 4 -6"/><path d="M9 17v1a3 3 0 0 0 6 0v-1"/></svg>`,
|
||||
"online": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="5"/></svg>`,
|
||||
"detail": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 12h.01"/><path d="M12 12h.01"/><path d="M9 12h.01"/><path d="M5 12a7 7 0 1 0 14 0a7 7 0 0 0 -14 0"/></svg>`,
|
||||
"control": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 4v16l13 -8z"/></svg>`,
|
||||
@ -432,7 +434,6 @@ func (u *UI) Routes() (chi.Router, error) {
|
||||
r.Get("/devices", u.pageDevices)
|
||||
r.Get("/devices/{id}/control", u.pageDeviceControl)
|
||||
r.Get("/assets", u.pageAssets)
|
||||
r.Post("/assets/import", u.actionAssetsImport)
|
||||
r.Get("/assets/templates", u.pageAssetTemplates)
|
||||
r.Post("/assets/templates/create", u.actionAssetTemplateCreate)
|
||||
r.Get("/assets/templates/{name}", u.pageAssetTemplate)
|
||||
@ -506,6 +507,8 @@ func (u *UI) render(w http.ResponseWriter, r *http.Request, content string, data
|
||||
data.ContentHTML = template.HTML(buf.String())
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
if err := u.tpl.ExecuteTemplate(w, "layout", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -1179,24 +1182,6 @@ func (u *UI) pageAssets(w http.ResponseWriter, r *http.Request) {
|
||||
u.render(w, r, "assets", data)
|
||||
}
|
||||
|
||||
func (u *UI) actionAssetsImport(w http.ResponseWriter, r *http.Request) {
|
||||
data := u.assetPageData("overview")
|
||||
if u.preview == nil {
|
||||
data.Error = "配置资产服务未初始化"
|
||||
u.render(w, r, "assets", data)
|
||||
return
|
||||
}
|
||||
result, err := u.preview.ImportAssetsFromMediaRepo()
|
||||
if err != nil {
|
||||
data.Error = err.Error()
|
||||
u.render(w, r, "assets", data)
|
||||
return
|
||||
}
|
||||
data = u.assetPageData("overview")
|
||||
data.Message = fmt.Sprintf("已导入 %d 个业务配置、%d 个叠加项。标准模板保持目录只读,不写入数据库。", result.Profiles, result.Overlays)
|
||||
u.render(w, r, "assets", data)
|
||||
}
|
||||
|
||||
func (u *UI) pageAssetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
data := u.assetPageData("templates")
|
||||
data.Message = strings.TrimSpace(r.URL.Query().Get("msg"))
|
||||
|
||||
@ -1,38 +1,38 @@
|
||||
.graph-editor{display:grid;grid-template-columns:210px minmax(0,1fr) 280px;gap:12px;min-height:760px}
|
||||
.graph-sidebar,.graph-inspector{border:1px solid var(--border);border-radius:8px;background:var(--surface-soft);padding:12px}
|
||||
.graph-sidebar,.graph-inspector{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft);padding:12px}
|
||||
.graph-sidebar h3,.graph-inspector h3{font-size:13px;margin:0 0 10px;font-weight:600}
|
||||
.graph-node-palette-list{display:flex;flex-direction:column;gap:10px;max-height:720px;overflow:auto;padding-right:2px}
|
||||
.graph-node-palette-category{font-size:11px;color:var(--muted);margin:0 0 6px}
|
||||
.graph-node-palette{width:100%;padding:7px 8px;border:1px solid var(--border);border-radius:7px;background:#fff;margin:0 0 6px;cursor:pointer;font-size:12px;text-align:left;display:flex;align-items:center;justify-content:flex-start;gap:8px}
|
||||
.graph-node-palette:hover{border-color:var(--border-strong);background:#f8fafc}
|
||||
.graph-node-palette-icon{display:inline-flex;align-items:center;justify-content:center;width:25px;height:25px;border-radius:7px;background:#e0f2fe;color:#075985;font-size:10px;letter-spacing:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;flex:0 0 auto}
|
||||
.graph-node-palette{width:100%;padding:7px 8px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text);margin:0 0 6px;cursor:pointer;font-size:12px;text-align:left;display:flex;align-items:center;justify-content:flex-start;gap:8px}
|
||||
.graph-node-palette:hover{border-color:var(--border-strong);background:var(--surface-strong)}
|
||||
.graph-node-palette-icon{display:inline-flex;align-items:center;justify-content:center;width:25px;height:25px;border-radius:var(--radius);background:var(--surface-strong);color:var(--primary);font-size:10px;letter-spacing:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;flex:0 0 auto}
|
||||
.graph-node-palette-text{display:flex;flex-direction:column;align-items:flex-start;min-width:0;gap:1px}
|
||||
.graph-node-palette-text span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.graph-node-palette-text small{font-size:10px;color:var(--muted);font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.graph-canvas-wrap{position:relative;border:1px solid var(--border);border-radius:8px;background:#fff;overflow:auto;height:min(80vh,980px);min-height:760px}
|
||||
.graph-canvas-wrap{position:relative;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);overflow:auto;height:min(80vh,980px);min-height:760px}
|
||||
.graph-canvas-toolbar{position:absolute;top:10px;right:10px;z-index:2;display:flex;gap:8px}
|
||||
.graph-canvas-toolbar .btn{background:rgba(255,255,255,.92);font-size:12px;padding:6px 10px}
|
||||
.graph-canvas-toolbar .btn{background:var(--button-soft);font-size:12px;padding:6px 10px}
|
||||
.graph-canvas{display:block;min-width:100%;min-height:760px}
|
||||
.graph-node rect{fill:#fff;stroke:#cbd5e1;stroke-width:1.4}
|
||||
.graph-node rect{fill:var(--surface);stroke:var(--border-strong);stroke-width:1.4}
|
||||
.graph-node{cursor:grab}
|
||||
.graph-node text{font-size:12px;fill:#111827;pointer-events:none}
|
||||
.graph-node .graph-node-icon{font-size:10px;fill:#0284c7;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||
.graph-node .graph-node-type{font-size:11px;fill:#6b7280}
|
||||
.graph-node.selected rect{stroke:#2563eb;stroke-width:2}
|
||||
.graph-edge{stroke:#94a3b8;stroke-width:1.8;fill:none;cursor:pointer}
|
||||
.graph-node text{font-size:12px;fill:var(--text);pointer-events:none}
|
||||
.graph-node .graph-node-icon{font-size:10px;fill:var(--primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||
.graph-node .graph-node-type{font-size:11px;fill:var(--muted)}
|
||||
.graph-node.selected rect{stroke:var(--primary);stroke-width:2}
|
||||
.graph-edge{stroke:var(--border-strong);stroke-width:1.8;fill:none;cursor:pointer}
|
||||
.graph-edge-hit{stroke:transparent;stroke-width:16;fill:none;cursor:pointer}
|
||||
.graph-edge.selected{stroke:#2563eb;stroke-width:2.2}
|
||||
.graph-edge.selected{stroke:var(--primary);stroke-width:2.2}
|
||||
.graph-empty-inspector{font-size:12px;color:var(--muted)}
|
||||
.graph-node-form,.graph-edge-form{display:flex;flex-direction:column;gap:10px}
|
||||
.graph-node-form label,.graph-edge-form label{display:block;width:100%}
|
||||
.graph-node-form label span,.graph-edge-form label span{display:block;font-size:11px;color:var(--muted);margin-bottom:4px}
|
||||
.graph-node-form textarea,.graph-edge-form textarea{display:block;width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.45;resize:vertical;min-height:96px}
|
||||
.graph-inspector-title{font-size:12px;color:#0f172a;margin-top:2px}
|
||||
.graph-inspector-title{font-size:12px;color:var(--text);margin-top:2px}
|
||||
.graph-form-hint{font-size:11px;color:var(--muted);line-height:1.45}
|
||||
.graph-typed-param-fields{display:flex;flex-direction:column;gap:10px}
|
||||
.graph-typed-param-fields:empty::before{content:"此节点暂无常用参数";display:block;font-size:11px;color:var(--muted);padding:8px 0}
|
||||
.graph-advanced-json{border-top:1px solid var(--border);padding-top:8px}
|
||||
.graph-advanced-json label{display:block;width:100%}
|
||||
.graph-advanced-json summary{cursor:pointer;font-size:12px;color:#0f172a;margin-bottom:8px}
|
||||
.graph-advanced-json summary{cursor:pointer;font-size:12px;color:var(--text);margin-bottom:8px}
|
||||
.graph-save-form{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.graph-save-form input[type="text"]{min-width:180px;max-width:240px}
|
||||
|
||||
@ -1,28 +1,105 @@
|
||||
*{box-sizing:border-box}
|
||||
:root{
|
||||
--bg:#f5f7fa;
|
||||
--surface:#ffffff;
|
||||
--surface-soft:#fafbfc;
|
||||
--sidebar:#1f2937;
|
||||
--sidebar-hover:#2b3646;
|
||||
--border:#e5e7eb;
|
||||
--border-strong:#d1d5db;
|
||||
--text:#111827;
|
||||
--muted:#6b7280;
|
||||
--primary:#475569;
|
||||
--primary-strong:#334155;
|
||||
--primary-strong-hover:#273444;
|
||||
--button-soft:#e5e7eb;
|
||||
--button-soft-hover:#dbe1e8;
|
||||
--button-soft-text:#374151;
|
||||
--danger-soft:#f3e8e8;
|
||||
--danger-soft-hover:#eadbdb;
|
||||
--danger-soft-text:#7c4a4a;
|
||||
--teal:#0f766e;
|
||||
--green:#15803d;
|
||||
--amber:#b45309;
|
||||
--red:#b91c1c;
|
||||
--shadow:0 10px 30px rgba(15,23,42,.06);
|
||||
--radius:4px;
|
||||
--bg:#090909;
|
||||
--surface:#121212;
|
||||
--surface-soft:#1a1a1a;
|
||||
--surface-strong:#242424;
|
||||
--topbar:#0e0e0e;
|
||||
--sidebar:#070707;
|
||||
--sidebar-hover:#171717;
|
||||
--sidebar-text:#eeeeee;
|
||||
--sidebar-muted:#8d8d8d;
|
||||
--border:#303030;
|
||||
--border-strong:#565656;
|
||||
--text:#f4f4f4;
|
||||
--muted:#bcbcbc;
|
||||
--table-text:#c8c8c8;
|
||||
--table-link:#f4f4f4;
|
||||
--primary:#dddddd;
|
||||
--primary-strong:#2b2b2b;
|
||||
--primary-strong-hover:#363636;
|
||||
--button-soft:#1d1d1d;
|
||||
--button-soft-hover:#292929;
|
||||
--button-soft-text:#f4f4f4;
|
||||
--input-bg:#101010;
|
||||
--selected-row:#303030;
|
||||
--danger-soft:#242424;
|
||||
--danger-soft-hover:#303030;
|
||||
--danger-soft-text:#dddddd;
|
||||
--teal:#dddddd;
|
||||
--green:#66c98f;
|
||||
--amber:#d8a657;
|
||||
--red:#e46f72;
|
||||
--shadow:0 18px 44px rgba(0,0,0,.32);
|
||||
}
|
||||
|
||||
body[data-theme="blue-light"]{
|
||||
--bg:#dfe7f0;
|
||||
--surface:#eef4fa;
|
||||
--surface-soft:#e5edf6;
|
||||
--surface-strong:#d5e1ef;
|
||||
--topbar:#e8eef6;
|
||||
--sidebar:#0d1520;
|
||||
--sidebar-hover:#1a2a43;
|
||||
--sidebar-text:#d7e2ee;
|
||||
--sidebar-muted:#728299;
|
||||
--border:#b5c4d6;
|
||||
--border-strong:#8499b3;
|
||||
--text:#172333;
|
||||
--muted:#4f6177;
|
||||
--table-text:#172333;
|
||||
--table-link:#2d6fb5;
|
||||
--primary:#2d6fb5;
|
||||
--primary-strong:#245f9f;
|
||||
--primary-strong-hover:#1f528a;
|
||||
--button-soft:#d5e1ef;
|
||||
--button-soft-hover:#c8d8eb;
|
||||
--button-soft-text:#172333;
|
||||
--input-bg:#eef4fa;
|
||||
--selected-row:#c7d9ee;
|
||||
--danger-soft:#f0dada;
|
||||
--danger-soft-hover:#e8caca;
|
||||
--danger-soft-text:#a43f3f;
|
||||
--teal:#2d6fb5;
|
||||
--green:#245f9f;
|
||||
--amber:#9b650e;
|
||||
--red:#a43f3f;
|
||||
--shadow:0 14px 34px rgba(35,50,60,.12);
|
||||
}
|
||||
|
||||
body[data-theme="graphite-gold"]{
|
||||
--bg:#171717;
|
||||
--surface:#202020;
|
||||
--surface-soft:#262626;
|
||||
--surface-strong:#303030;
|
||||
--topbar:#1b1b1b;
|
||||
--sidebar:#121212;
|
||||
--sidebar-hover:#2a271f;
|
||||
--sidebar-text:#d1cbc1;
|
||||
--sidebar-muted:#7f796e;
|
||||
--border:#3c3c3c;
|
||||
--border-strong:#5a5447;
|
||||
--text:#ece7da;
|
||||
--muted:#c9c0b3;
|
||||
--table-text:#eee5d6;
|
||||
--table-link:#d3a84f;
|
||||
--primary:#d3a84f;
|
||||
--primary-strong:#5d431e;
|
||||
--primary-strong-hover:#745426;
|
||||
--button-soft:#303030;
|
||||
--button-soft-hover:#39352d;
|
||||
--button-soft-text:#ece7da;
|
||||
--input-bg:#1b1b1b;
|
||||
--selected-row:#332d20;
|
||||
--danger-soft:#301d1c;
|
||||
--danger-soft-hover:#3b2423;
|
||||
--danger-soft-text:#d66a63;
|
||||
--teal:#d3a84f;
|
||||
--green:#f0cf7a;
|
||||
--amber:#d79a3b;
|
||||
--red:#d66a63;
|
||||
--shadow:0 18px 44px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
body{margin:0;background:var(--bg);color:var(--text);font:13px/1.5 var(--tblr-font-sans-serif,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif)}
|
||||
@ -31,41 +108,47 @@ a:hover{text-decoration:none}
|
||||
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||
|
||||
.app-shell{display:grid;grid-template-columns:240px minmax(0,1fr);min-height:100vh}
|
||||
.sidebar{background:var(--sidebar);color:#f3f4f6;display:flex;flex-direction:column;padding:18px 14px;position:sticky;top:0;height:100vh}
|
||||
.sidebar{background:var(--sidebar);color:var(--sidebar-text);display:flex;flex-direction:column;padding:18px 14px;position:sticky;top:0;height:100vh}
|
||||
.brand-block{display:flex;gap:12px;align-items:center;padding:4px 8px 20px}
|
||||
.brand-mark{width:40px;height:40px;border-radius:8px;background:#111827;border:1px solid rgba(255,255,255,.08);display:grid;place-items:center;font-size:13px;font-weight:700}
|
||||
.brand-mark{width:38px;height:38px;border-radius:var(--radius);background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;font-size:13px;font-weight:600;color:var(--primary)}
|
||||
.brand-title{font-size:14px;font-weight:600}
|
||||
.brand-subtitle{font-size:11px;color:#9ca3af;margin-top:2px}
|
||||
.side-nav{display:flex;flex-direction:column;gap:4px}
|
||||
.nav-section{padding:14px 10px 6px;font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em}
|
||||
.side-nav a{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;color:#e5e7eb;font-size:12px;font-weight:500}
|
||||
.nav-section{padding:14px 10px 6px;font-size:11px;color:var(--sidebar-muted);text-transform:uppercase;letter-spacing:.06em}
|
||||
.side-nav a{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius);color:var(--sidebar-text);font-size:13px;font-weight:500}
|
||||
.side-nav a:hover{background:var(--sidebar-hover)}
|
||||
.nav-icon{width:28px;height:24px;border-radius:6px;border:1px solid rgba(255,255,255,.1);display:grid;place-items:center;font-size:10px;color:#d1d5db}
|
||||
.nav-icon{width:28px;height:24px;border-radius:3px;border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;font-size:10px;color:var(--primary)}
|
||||
.nav-icon .ui-icon{width:14px;height:14px;stroke-width:1.75}
|
||||
.sidebar-footer{margin-top:auto;padding:14px 8px 0;border-top:1px solid rgba(255,255,255,.08)}
|
||||
.sidebar-note{font-size:12px;color:#9ca3af;line-height:1.5}
|
||||
|
||||
.workspace{min-width:0}
|
||||
.topbar{height:72px;background:rgba(255,255,255,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 28px;position:sticky;top:0;z-index:10}
|
||||
.topbar h1{margin:4px 0 0;font-size:18px;font-weight:600}
|
||||
.topbar{height:60px;background:var(--topbar);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 28px;position:sticky;top:0;z-index:100}
|
||||
.topbar h1{margin:0;font-size:18px;font-weight:600}
|
||||
.crumb,.eyebrow{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
|
||||
.topbar-actions{display:flex;gap:8px}
|
||||
.topbar-actions{display:flex;align-items:center;gap:8px}
|
||||
.topbar-icon-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;min-height:32px;padding:0;border-radius:var(--radius);border:1px solid var(--border-strong);background:var(--button-soft);color:var(--button-soft-text)}
|
||||
.topbar-icon-btn:hover{background:var(--button-soft-hover);border-color:var(--primary);color:var(--text)}
|
||||
.topbar-icon-btn .ui-icon{width:16px;height:16px}
|
||||
.topbar-dot{position:absolute;right:6px;top:6px;width:6px;height:6px;border-radius:999px;background:var(--red)}
|
||||
.theme-menu{position:relative}
|
||||
.theme-menu-panel{position:fixed;right:28px;top:54px;min-width:132px;padding:6px;border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow);z-index:9999}
|
||||
.theme-menu-panel button{display:flex;width:100%;justify-content:flex-start;min-height:28px;border-color:transparent;background:transparent;color:var(--text)}
|
||||
.theme-menu-panel button:hover,.theme-menu-panel button[aria-pressed="true"]{background:var(--surface-strong);border-color:var(--border)}
|
||||
main{padding:24px 28px 36px;max-width:1440px}
|
||||
|
||||
.hero-band{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:18px;padding:8px 2px}
|
||||
.hero-band h2{margin:4px 0 6px;font-size:21px;font-weight:600;line-height:1.25}
|
||||
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:18px 20px;margin:16px 0;box-shadow:var(--shadow)}
|
||||
.card h2{margin:0 0 8px;font-size:16px;font-weight:600}
|
||||
.card h3{margin:0 0 6px;font-size:14px;font-weight:600}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin:14px 0;box-shadow:var(--shadow);color:var(--text)}
|
||||
.card h2{margin:0 0 8px;font-size:15px;font-weight:600;color:var(--text)}
|
||||
.card h3{margin:0 0 6px;font-size:13px;font-weight:600;color:var(--text)}
|
||||
.section-title{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}
|
||||
.title-with-icon{display:flex;align-items:center;gap:8px}
|
||||
.title-with-icon .ui-icon{width:16px;height:16px;color:#475569}
|
||||
.title-with-icon .ui-icon{width:16px;height:16px;color:var(--primary)}
|
||||
.muted{color:var(--muted)}
|
||||
.small{font-size:12px}
|
||||
.form-hint{color:var(--muted);font-size:12px;line-height:1.35}
|
||||
|
||||
.btn,button,input,select,textarea{font:inherit}
|
||||
.btn,button{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:5px 10px;border-radius:7px;border:1px solid var(--border-strong);background:var(--button-soft);color:var(--button-soft-text);cursor:pointer;font-size:11px;font-weight:500;line-height:1.15;min-height:32px;transition:background-color .16s ease,border-color .16s ease,color .16s ease,box-shadow .16s ease}
|
||||
.btn,button{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:5px 10px;border-radius:var(--radius);border:1px solid var(--border-strong);background:var(--button-soft);color:var(--button-soft-text);cursor:pointer;font-size:11px;font-weight:500;line-height:1.15;min-height:30px;transition:background-color .16s ease,border-color .16s ease,color .16s ease,box-shadow .16s ease}
|
||||
.btn:hover,button:hover{background:var(--button-soft-hover);border-color:#c3ccd6}
|
||||
.btn.primary,button.primary{background:var(--primary-strong);border-color:var(--primary-strong);color:#f8fafc}
|
||||
.btn.primary:hover,button.primary:hover{background:var(--primary-strong-hover);border-color:var(--primary-strong-hover)}
|
||||
@ -74,36 +157,39 @@ main{padding:24px 28px 36px;max-width:1440px}
|
||||
.btn.danger,button.danger{background:var(--danger-soft);border-color:#e7d5d5;color:var(--danger-soft-text)}
|
||||
.btn.danger:hover,button.danger:hover{background:var(--danger-soft-hover);border-color:#dbc4c4}
|
||||
button:disabled,.btn:disabled{opacity:.55;cursor:not-allowed}
|
||||
input,select,textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--border-strong);background:#fff;color:var(--text);font-size:12px}
|
||||
input,select,textarea{width:100%;padding:8px 10px;border-radius:var(--radius);border:1px solid var(--border-strong);background:var(--input-bg);color:var(--text);font-size:12px}
|
||||
textarea{min-height:140px;line-height:1.55}
|
||||
|
||||
.msg,.error{padding:12px 14px;border-radius:8px;margin:12px 0;white-space:pre-wrap}
|
||||
.msg{background:#ecfdf5;border:1px solid #bbf7d0;color:#166534}
|
||||
.error{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}
|
||||
.msg,.error{padding:12px 14px;border-radius:var(--radius);margin:12px 0;white-space:pre-wrap}
|
||||
.msg{background:var(--surface-soft);border:1px solid var(--border-strong);color:var(--green)}
|
||||
.error{background:var(--danger-soft);border:1px solid var(--danger-soft-hover);color:var(--danger-soft-text)}
|
||||
|
||||
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
|
||||
.stat{padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--surface);box-shadow:var(--shadow)}
|
||||
.stat{padding:14px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow)}
|
||||
.stat .k{font-size:12px;color:var(--muted)}
|
||||
.metric-label{display:flex;align-items:center;gap:6px}
|
||||
.metric-label .ui-icon{width:14px;height:14px}
|
||||
.stat .v{margin-top:8px;font-size:22px;font-weight:700;line-height:1}
|
||||
.stat .v{margin-top:8px;font-size:22px;font-weight:400;line-height:1;color:var(--text)}
|
||||
.stat .hint{margin-top:6px;font-size:12px;color:var(--muted)}
|
||||
.accent-teal .v{color:var(--teal)}
|
||||
.accent-green .v{color:var(--green)}
|
||||
.accent-slate .v{color:#475569}
|
||||
.accent-slate .v{color:var(--muted)}
|
||||
.accent-amber .v{color:var(--amber)}
|
||||
|
||||
.table-tools{display:flex;gap:8px;align-items:center}
|
||||
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:8px;background:#fff}
|
||||
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}
|
||||
table{width:100%;border-collapse:collapse;min-width:900px}
|
||||
th,td{padding:13px 12px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}
|
||||
th{background:var(--surface-soft);font-size:12px;font-weight:600;color:#4b5563}
|
||||
tbody tr:hover{background:#f9fafb}
|
||||
th,td{padding:8px 12px;border-bottom:1px solid var(--border);text-align:left;vertical-align:middle;line-height:1.25;color:var(--table-text)}
|
||||
th{background:var(--surface-soft);font-size:12px;font-weight:600;color:var(--muted)}
|
||||
.table-wrap td a{color:var(--table-link)}
|
||||
.table-wrap td a:hover{color:var(--text)}
|
||||
tbody tr:hover{background:var(--surface-soft)}
|
||||
tbody tr.selected{background:var(--selected-row);outline:1px solid var(--primary);outline-offset:-1px}
|
||||
|
||||
.device-cell{display:flex;align-items:center;gap:12px;min-width:220px}
|
||||
.device-avatar{width:34px;height:34px;border-radius:8px;background:#e5eefc;color:#1d4ed8;display:grid;place-items:center}
|
||||
.device-avatar{width:32px;height:32px;border-radius:var(--radius);background:var(--surface-strong);color:var(--primary);display:grid;place-items:center}
|
||||
.device-avatar .ui-icon{width:18px;height:18px}
|
||||
.device-name{font-size:13px;font-weight:600}
|
||||
.device-name{font-size:13px;font-weight:400;color:var(--table-link)}
|
||||
.device-meta-line{display:flex;flex-wrap:wrap;gap:8px;margin-top:3px;font-size:11px;color:var(--muted)}
|
||||
.status-dot{display:inline-block;width:8px;height:8px;border-radius:999px;margin-right:6px;vertical-align:middle}
|
||||
.status-dot.ok{background:var(--green)}
|
||||
@ -114,11 +200,11 @@ tbody tr:hover{background:#f9fafb}
|
||||
.state-stack{display:flex;flex-direction:column;gap:6px}
|
||||
.state-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
|
||||
|
||||
.pill{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;border:1px solid var(--border);background:#f3f4f6;color:#374151;font-size:11px;font-weight:600}
|
||||
.pill.ok{background:#ecfdf5;border-color:#bbf7d0;color:#166534}
|
||||
.pill.bad{background:#fef2f2;border-color:#fecaca;color:#991b1b}
|
||||
.pill.run{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}
|
||||
.pill.warn{background:#fffbeb;border-color:#fde68a;color:#92400e}
|
||||
.pill{display:inline-flex;align-items:center;min-height:20px;padding:1px 6px;border-radius:3px;border:1px solid var(--border);background:var(--surface-strong);color:var(--button-soft-text);font-size:11px;font-weight:500;line-height:1.1}
|
||||
.pill.ok{background:var(--surface-soft);border-color:var(--border-strong);color:var(--green)}
|
||||
.pill.bad{background:var(--danger-soft);border-color:var(--danger-soft-hover);color:var(--danger-soft-text)}
|
||||
.pill.run{background:var(--surface-soft);border-color:var(--border-strong);color:var(--primary)}
|
||||
.pill.warn{background:var(--surface-soft);border-color:var(--border-strong);color:var(--amber)}
|
||||
|
||||
.actions{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.actions.compact{gap:6px}
|
||||
@ -126,7 +212,7 @@ tbody tr:hover{background:#f9fafb}
|
||||
.btn .ui-icon{width:14px;height:14px}
|
||||
.stack{flex-direction:column;align-items:flex-start}
|
||||
.device-context-head{display:flex;align-items:center;gap:12px}
|
||||
.device-context-icon{width:34px;height:34px;border-radius:8px;background:#e5eefc;color:#1d4ed8;display:grid;place-items:center}
|
||||
.device-context-icon{width:32px;height:32px;border-radius:var(--radius);background:var(--surface-strong);color:var(--primary);display:grid;place-items:center}
|
||||
.device-context-icon .ui-icon{width:18px;height:18px}
|
||||
.device-tab-wrap{margin-top:16px}
|
||||
.asset-tab-wrap{margin-top:0}
|
||||
@ -144,71 +230,76 @@ tbody tr:hover{background:#f9fafb}
|
||||
.quad-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
|
||||
.control-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
|
||||
.selector-card .actions{margin-top:auto}
|
||||
.panel-block{border:1px solid var(--border);border-radius:8px;background:var(--surface-soft);padding:16px}
|
||||
.panel-block{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft);padding:14px}
|
||||
.panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}
|
||||
.field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:14px}
|
||||
.field-grid label span{display:block;margin-bottom:6px;font-size:12px;color:var(--muted)}
|
||||
.field-grid .full{grid-column:1/-1}
|
||||
.field-grid input,.field-grid select,.field-grid textarea{width:100%;padding:9px 10px;border:1px solid var(--border);border-radius:8px;background:#fff;color:var(--text);font:inherit}
|
||||
.field-grid input,.field-grid select,.field-grid textarea{width:100%;padding:9px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--input-bg);color:var(--text);font:inherit}
|
||||
.field-grid textarea{resize:vertical;min-height:120px}
|
||||
.code-input{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5}
|
||||
|
||||
.info-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
||||
.info-list>div{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--surface-soft)}
|
||||
.info-list>div{padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft)}
|
||||
.info-list span{display:block;margin-bottom:5px;font-size:12px;color:var(--muted)}
|
||||
.info-list strong{display:block;font-size:13px;font-weight:600;line-height:1.45}
|
||||
.info-list strong{display:block;font-size:13px;font-weight:400;line-height:1.45}
|
||||
.editable-line{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||||
.editable-line strong{margin:0;flex:1 1 auto}
|
||||
.icon-only{display:inline-flex;align-items:center;justify-content:center;padding:0;min-width:28px;width:28px;height:28px}
|
||||
.icon-only .ui-icon{width:14px;height:14px;margin:0 auto}
|
||||
.btn.ghost.icon-only{background:transparent;border-color:transparent;color:#64748b}
|
||||
.btn.ghost.icon-only:hover{background:#eef2f7;border-color:#dbe1e8;color:#334155}
|
||||
.btn.ghost.icon-only{background:transparent;border-color:transparent;color:var(--muted)}
|
||||
.btn.ghost.icon-only:hover{background:var(--surface-strong);border-color:var(--border);color:var(--text)}
|
||||
.inline-edit-form{display:flex;align-items:center;gap:8px}
|
||||
.inline-edit-form input{flex:1 1 auto;min-width:0}
|
||||
.compact-list{grid-template-columns:1fr}
|
||||
|
||||
.summary-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
|
||||
.control-summary{margin-bottom:16px}
|
||||
.summary-chip{padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--surface-soft)}
|
||||
.summary-chip{padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft)}
|
||||
.summary-chip-label{font-size:12px;color:var(--muted)}
|
||||
.summary-chip-value{margin-top:6px;font-size:13px;font-weight:600}
|
||||
|
||||
.asset-stat{margin-top:12px;font-size:20px;font-weight:700;color:#111827}
|
||||
.asset-stat{margin-top:12px;font-size:20px;font-weight:400;color:var(--text)}
|
||||
.asset-list{margin-top:14px;border-top:1px solid var(--border)}
|
||||
.asset-row{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}
|
||||
.asset-link{color:inherit}
|
||||
.asset-link:hover{background:#f8fafc}
|
||||
.asset-link>span:first-child{color:var(--table-link)}
|
||||
.asset-link:hover{background:var(--surface-soft)}
|
||||
.truncate-cell{max-width:260px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.muted-row{opacity:.62}
|
||||
.asset-tabs .nav-link{font-size:12px;font-weight:500;color:#64748b}
|
||||
.asset-tabs .nav-link:hover{color:#334155}
|
||||
.asset-tabs .nav-link.active{color:#111827}
|
||||
.asset-tabs .nav-link{font-size:12px;font-weight:500;color:var(--muted)}
|
||||
.asset-tabs .nav-link:hover{color:var(--text)}
|
||||
.asset-tabs .nav-link.active{color:var(--text)}
|
||||
.nav-tabs{border-color:var(--border)}
|
||||
.nav-tabs .nav-link{border-color:var(--border)!important;border-radius:var(--radius) var(--radius) 0 0;background:var(--surface-soft)!important;color:var(--muted)!important}
|
||||
.nav-tabs .nav-link:hover{border-color:var(--border-strong)!important;background:var(--surface-strong)!important;color:var(--text)!important}
|
||||
.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{border-color:var(--border)!important;background:var(--surface)!important;color:var(--text)!important}
|
||||
.profile-editor-tabs .tab-content>.card{margin-top:-1px}
|
||||
.nested-section{margin-top:14px;padding:14px}
|
||||
.nested-section pre{margin-top:10px}
|
||||
.profile-instance-editor .field-grid{margin-top:14px}
|
||||
|
||||
.empty-state{padding:18px;border:1px dashed var(--border-strong);border-radius:8px;background:var(--surface-soft)}
|
||||
.empty-state{padding:18px;border:1px dashed var(--border-strong);border-radius:var(--radius);background:var(--surface-soft)}
|
||||
.empty-state.compact{padding:14px}
|
||||
.empty-title{font-size:13px;font-weight:600;margin-bottom:6px}
|
||||
|
||||
.collapsible summary,.collapsible-inline summary{cursor:pointer;font-weight:600}
|
||||
pre{margin-top:12px;padding:12px;border-radius:8px;border:1px solid #1f2937;background:#111827;color:#e5e7eb;overflow:auto;white-space:pre-wrap}
|
||||
pre{margin-top:12px;padding:12px;border-radius:var(--radius);border:1px solid var(--border);background:var(--sidebar);color:var(--sidebar-text);overflow:auto;white-space:pre-wrap}
|
||||
|
||||
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.row>*{flex:1 1 220px}
|
||||
.subnav{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.device-tab-wrap{margin-top:16px}
|
||||
.device-tabs .nav-link{font-size:12px;font-weight:500;color:#64748b}
|
||||
.device-tabs .nav-link:hover{color:#334155}
|
||||
.device-tabs .nav-link.active{color:#111827}
|
||||
.device-tabs .nav-link{font-size:12px;font-weight:500;color:var(--muted)}
|
||||
.device-tabs .nav-link:hover{color:var(--text)}
|
||||
.device-tabs .nav-link.active{color:var(--text)}
|
||||
.device-tab-card{margin-top:0}
|
||||
.device-panel-body{padding:16px}
|
||||
.device-panel-body>.card,.device-panel-body>details.card{margin:0 0 16px}
|
||||
.device-panel-body>.card:last-child,.device-panel-body>details.card:last-child{margin-bottom:0}
|
||||
.ui-icon{display:block;flex:0 0 auto}
|
||||
|
||||
.batch-toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:14px 16px;border:1px solid var(--border);border-radius:8px;background:var(--surface-soft);margin:0 0 12px}
|
||||
.batch-toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:14px 16px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft);margin:0 0 12px}
|
||||
.batch-toolbar-count{font-size:13px;font-weight:600}
|
||||
.batch-toolbar .actions{justify-content:flex-end}
|
||||
.batch-toolbar .actions .btn,.batch-toolbar .actions button{white-space:nowrap}
|
||||
@ -219,6 +310,7 @@ pre{margin-top:12px;padding:12px;border-radius:8px;border:1px solid #1f2937;back
|
||||
.app-shell{grid-template-columns:1fr}
|
||||
.sidebar{position:relative;height:auto}
|
||||
.topbar{position:relative;height:auto;padding:18px;flex-direction:column;align-items:flex-start;gap:12px}
|
||||
.theme-menu{display:flex;flex-direction:column;align-items:flex-start}
|
||||
main{padding:18px}
|
||||
.stats,.detail-grid,.device-selector-grid,.quad-grid,.control-grid,.summary-strip,.info-list,.field-grid{grid-template-columns:1fr}
|
||||
.hero-band{flex-direction:column;align-items:flex-start}
|
||||
|
||||
@ -51,17 +51,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "assets"}}<span>资产操作</span></h2>
|
||||
</div>
|
||||
<form method="post" action="/ui/assets/import">
|
||||
<button type="submit" class="btn secondary">{{icon "apply"}}<span>导入现有 JSON</span></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quad-grid">
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
|
||||
@ -3,11 +3,6 @@
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>全局 KPI</h2>
|
||||
<div class="muted small">只看 fleet 级运行态势、最近任务和需要关注的异常设备。</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn ghost" href="/ui/devices">设备列表</a>
|
||||
<a class="btn ghost" href="/ui/tasks">任务</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
@ -18,46 +13,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<h2>视频分析概览</h2>
|
||||
<div class="muted small">当前版本基于节点在线状态和视频分析服务查询入口汇总。</div>
|
||||
<div class="actions" style="margin-top:12px">
|
||||
<a class="btn ghost" href="/ui/devices">查看视频分析服务</a>
|
||||
<a class="btn ghost" href="/ui/recognition">识别配置</a>
|
||||
<a class="btn ghost" href="/ui/models">模型管理</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>运维工作流</h2>
|
||||
<div class="muted small">按常见现场操作顺序进入对应页面。</div>
|
||||
<div class="actions" style="margin-top:8px">
|
||||
<a class="btn ghost" href="/ui/devices">查看设备列表</a>
|
||||
<a class="btn ghost" href="/ui/recognition">配置识别方案</a>
|
||||
<a class="btn ghost" href="/ui/tasks">批量任务</a>
|
||||
<a class="btn ghost" href="/ui/diagnostics">诊断</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<h2>快速入口</h2>
|
||||
<div class="actions" style="margin-top:8px">
|
||||
<a class="btn ghost" href="/ui/devices-add">新增设备</a>
|
||||
<a class="btn ghost" href="/ui/api">高级调试</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>任务健康</h2>
|
||||
<div class="stats" style="grid-template-columns:repeat(3,minmax(0,1fr))">
|
||||
<div class="stat"><div class="k">执行中任务</div><div class="v">{{.RunningTaskCount}}</div></div>
|
||||
<div class="stat"><div class="k">失败任务</div><div class="v">{{.FailedTaskCount}}</div></div>
|
||||
<div class="stat"><div class="k">成功任务</div><div class="v">{{.SuccessTaskCount}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>异常设备</h2>
|
||||
<div class="table-wrap" style="margin-top:10px">
|
||||
|
||||
@ -6,17 +6,16 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="stylesheet" href="/ui/assets/vendor/tabler.min.css" />
|
||||
<link rel="stylesheet" href="/ui/assets/style.css" />
|
||||
<link rel="stylesheet" href="/ui/assets/graph_editor.css" />
|
||||
<link rel="stylesheet" href="/ui/assets/style.css?v=20260429-theme16" />
|
||||
<link rel="stylesheet" href="/ui/assets/graph_editor.css?v=20260429-theme16" />
|
||||
</head>
|
||||
<body class="theme-light">
|
||||
<body data-theme="blue-dark">
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">AI</div>
|
||||
<div>
|
||||
<div class="brand-title">视觉识别运维平台</div>
|
||||
<div class="brand-subtitle">Fleet Operations Console</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="side-nav" aria-label="主导航">
|
||||
@ -27,16 +26,31 @@
|
||||
<a href="/ui/tasks"><span class="nav-icon">{{icon "task"}}</span><span>任务</span></a>
|
||||
<a href="/ui/diagnostics"><span class="nav-icon">{{icon "logs"}}</span><span>诊断</span></a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-note">总览 fleet 状态,进入设备工作台,统一查看任务与诊断</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="crumb">多设备视觉识别运维平台</div>
|
||||
<div class="topbar-title">
|
||||
<h1>{{.Title}}</h1>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<div class="theme-menu">
|
||||
<button type="button" class="topbar-icon-btn js-theme-menu-toggle" aria-label="主题" title="主题">
|
||||
{{icon "theme"}}
|
||||
</button>
|
||||
<div class="theme-menu-panel" hidden>
|
||||
<button type="button" data-theme-option="blue-dark">黑白暗色</button>
|
||||
<button type="button" data-theme-option="blue-light">蓝灰浅色</button>
|
||||
<button type="button" data-theme-option="graphite-gold">石墨金色</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="topbar-icon-btn" href="/ui/diagnostics" aria-label="告警" title="告警">
|
||||
{{icon "bell"}}
|
||||
<span class="topbar-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a class="topbar-icon-btn" href="/ui/system" aria-label="系统" title="系统">
|
||||
{{icon "system"}}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||
@ -48,6 +62,62 @@
|
||||
<script src="/ui/assets/vendor/tabler.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const themeKey = "3588-admin-theme";
|
||||
const allowedThemes = new Set(["blue-dark", "blue-light", "graphite-gold"]);
|
||||
const themeLabels = {
|
||||
"blue-dark": "黑白暗色",
|
||||
"blue-light": "蓝灰浅色",
|
||||
"graphite-gold": "石墨金色"
|
||||
};
|
||||
|
||||
function applyTheme(theme, persist) {
|
||||
const nextTheme = allowedThemes.has(theme) ? theme : "blue-dark";
|
||||
document.body.setAttribute("data-theme", nextTheme);
|
||||
const themeToggle = document.querySelector(".js-theme-menu-toggle");
|
||||
if (themeToggle) {
|
||||
const label = "当前主题:" + themeLabels[nextTheme];
|
||||
themeToggle.setAttribute("aria-label", label);
|
||||
themeToggle.setAttribute("title", label);
|
||||
}
|
||||
if (persist) {
|
||||
localStorage.setItem(themeKey, nextTheme);
|
||||
}
|
||||
document.querySelectorAll("[data-theme-option]").forEach(function (button) {
|
||||
const active = button.getAttribute("data-theme-option") === nextTheme;
|
||||
button.setAttribute("aria-pressed", active ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
applyTheme(localStorage.getItem("3588-admin-theme") || document.body.getAttribute("data-theme"), false);
|
||||
|
||||
document.querySelectorAll("[data-theme-option]").forEach(function (themeOption) {
|
||||
themeOption.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
applyTheme(themeOption.getAttribute("data-theme-option"), true);
|
||||
const panel = themeOption.closest(".theme-menu-panel");
|
||||
if (panel) {
|
||||
panel.hidden = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openThemePanel(themeToggle, panel) {
|
||||
if (panel.parentElement !== document.body) {
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
const rect = themeToggle.getBoundingClientRect();
|
||||
panel.hidden = false;
|
||||
const panelWidth = panel.offsetWidth || 132;
|
||||
const viewportPadding = 12;
|
||||
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding);
|
||||
const preferredLeft = rect.right - panelWidth;
|
||||
const left = Math.min(Math.max(viewportPadding, preferredLeft), maxLeft);
|
||||
panel.style.top = Math.round(rect.bottom + 6) + "px";
|
||||
panel.style.left = Math.round(left) + "px";
|
||||
panel.style.right = "auto";
|
||||
}
|
||||
|
||||
function suggestedFilenameFromHeader(headerValue, fallback) {
|
||||
if (!headerValue) return fallback;
|
||||
const match = /filename="?([^"]+)"?/i.exec(headerValue);
|
||||
@ -100,6 +170,20 @@
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
const themeToggle = event.target.closest(".js-theme-menu-toggle");
|
||||
if (themeToggle) {
|
||||
event.preventDefault();
|
||||
const panel = document.querySelector(".theme-menu-panel");
|
||||
if (panel) {
|
||||
if (panel.hidden) {
|
||||
openThemePanel(themeToggle, panel);
|
||||
} else {
|
||||
panel.hidden = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = event.target.closest(".js-export-json");
|
||||
if (btn) {
|
||||
event.preventDefault();
|
||||
|
||||
@ -143,6 +143,40 @@ func TestUI_DevicePageUsesEdgeVisionConsoleShell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_LayoutProvidesThemeSwitcherAndCompactTopbar(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
|
||||
body := renderPage(t, ui, "/ui/devices")
|
||||
|
||||
for _, want := range []string{
|
||||
`data-theme="blue-dark"`,
|
||||
`class="topbar-actions"`,
|
||||
`data-theme-option="blue-dark"`,
|
||||
`data-theme-option="blue-light"`,
|
||||
`data-theme-option="graphite-gold"`,
|
||||
`const themeLabels = {`,
|
||||
`"blue-light": "蓝灰浅色"`,
|
||||
`"当前主题:" + themeLabels[nextTheme]`,
|
||||
`aria-label="主题"`,
|
||||
`aria-label="告警"`,
|
||||
`aria-label="系统"`,
|
||||
`localStorage.getItem("3588-admin-theme")`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("expected layout to contain %q, got:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
`class="crumb"`,
|
||||
"多设备视觉识别运维平台",
|
||||
"Fleet Operations Console",
|
||||
} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("expected compact topbar to omit %q, got:\n%s", forbidden, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_ConsoleTypographyStaysModerate(t *testing.T) {
|
||||
css, err := os.ReadFile("ui/assets/style.css")
|
||||
if err != nil {
|
||||
@ -150,13 +184,22 @@ func TestUI_ConsoleTypographyStaysModerate(t *testing.T) {
|
||||
}
|
||||
text := string(css)
|
||||
for _, want := range []string{
|
||||
".brand-title{font-size:14px;font-weight:600",
|
||||
`body[data-theme="blue-light"]`,
|
||||
`body[data-theme="graphite-gold"]`,
|
||||
"--radius:4px",
|
||||
".nav-section{padding:14px 10px 6px;font-size:11px",
|
||||
".topbar h1{margin:4px 0 0;font-size:18px;font-weight:600",
|
||||
".crumb,.eyebrow{font-size:11px",
|
||||
".side-nav a{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius);color:var(--sidebar-text);font-size:13px;font-weight:500}",
|
||||
".topbar h1{margin:0;font-size:18px;font-weight:600",
|
||||
".card h2{margin:0 0 8px;font-size:15px;font-weight:600;color:var(--text)}",
|
||||
".card h3{margin:0 0 6px;font-size:13px;font-weight:600;color:var(--text)}",
|
||||
".btn.ghost.icon-only{background:transparent;border-color:transparent;color:var(--muted)}",
|
||||
".topbar{height:60px;background:var(--topbar);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 28px;position:sticky;top:0;z-index:100}",
|
||||
".topbar-icon-btn{position:relative",
|
||||
".theme-menu-panel{position:fixed;right:28px;top:54px;",
|
||||
"z-index:9999",
|
||||
".btn,button{display:inline-flex",
|
||||
".stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}",
|
||||
".card{background:var(--surface);border:1px solid var(--border);border-radius:8px",
|
||||
".card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected stylesheet to contain %q", want)
|
||||
@ -164,6 +207,41 @@ func TestUI_ConsoleTypographyStaysModerate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_BlueDarkThemeKeepsWorkspaceDarkAndTablesReadable(t *testing.T) {
|
||||
css, err := os.ReadFile("ui/assets/style.css")
|
||||
if err != nil {
|
||||
t.Fatalf("read stylesheet: %v", err)
|
||||
}
|
||||
text := string(css)
|
||||
for _, want := range []string{
|
||||
"--bg:#090909",
|
||||
"--surface:#121212",
|
||||
"--surface-soft:#1a1a1a",
|
||||
"--table-text:#c8c8c8",
|
||||
"--table-link:#f4f4f4",
|
||||
"--primary:#dddddd",
|
||||
"--selected-row:#303030",
|
||||
"--border:#303030",
|
||||
"--border-strong:#565656",
|
||||
"--button-soft:#1d1d1d",
|
||||
"--button-soft-hover:#292929",
|
||||
"--input-bg:#101010",
|
||||
"--green:#66c98f",
|
||||
"--amber:#d8a657",
|
||||
"--red:#e46f72",
|
||||
".card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin:14px 0;box-shadow:var(--shadow);color:var(--text)}",
|
||||
".form-hint{color:var(--muted);font-size:12px;line-height:1.35}",
|
||||
"th,td{padding:8px 12px;border-bottom:1px solid var(--border);text-align:left;vertical-align:middle;line-height:1.25;color:var(--table-text)}",
|
||||
".table-wrap td a{color:var(--table-link)}",
|
||||
".device-name{font-size:13px;font-weight:400;color:var(--table-link)}",
|
||||
".asset-link>span:first-child{color:var(--table-link)}",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected blue dark theme stylesheet to contain %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestUI(t *testing.T) *UI {
|
||||
t.Helper()
|
||||
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
||||
@ -2128,6 +2206,29 @@ func TestUI_DashboardShowsGlobalOperationsSummary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_DashboardOmitsRedundantExplanatoryChrome(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
html := renderPage(t, ui, "/ui/dashboard")
|
||||
|
||||
for _, text := range []string{
|
||||
"只看 fleet 级运行态势、最近任务和需要关注的异常设备。",
|
||||
"当前版本基于节点在线状态和视频分析服务查询入口汇总。",
|
||||
"按常见现场操作顺序进入对应页面。",
|
||||
"视频分析概览",
|
||||
"运维工作流",
|
||||
"快速入口",
|
||||
"任务健康",
|
||||
"失败任务",
|
||||
"成功任务",
|
||||
`<a class="btn ghost" href="/ui/devices">设备列表</a>`,
|
||||
`<a class="btn ghost" href="/ui/tasks">任务</a>`,
|
||||
} {
|
||||
if strings.Contains(html, text) {
|
||||
t.Fatalf("dashboard should omit redundant chrome %q in html: %s", text, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_TasksPageOwnsBatchExecutionDomain(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
html := renderPage(t, ui, "/ui/tasks")
|
||||
@ -2307,11 +2408,13 @@ func TestUI_TasksPageShowsPersistedHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_AssetsOverviewShowsImportAction(t *testing.T) {
|
||||
func TestUI_AssetsOverviewOmitsLegacyImportAction(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
html := renderPage(t, ui, "/ui/assets")
|
||||
if !strings.Contains(html, "导入现有 JSON") {
|
||||
t.Fatalf("expected assets overview to contain import action, got: %s", html)
|
||||
for _, forbidden := range []string{"资产操作", "导入现有 JSON", `action="/ui/assets/import"`} {
|
||||
if strings.Contains(html, forbidden) {
|
||||
t.Fatalf("expected assets overview to omit legacy import action %q, got: %s", forbidden, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2458,7 +2561,7 @@ func TestUI_AssetTemplatePageShowsRenameAndDeleteForUserTemplate(t *testing.T) {
|
||||
ui.preview = service.NewConfigPreviewService(&config.Config{}, repo)
|
||||
|
||||
html := renderPage(t, ui, "/ui/assets/templates?name=helmet")
|
||||
for _, want := range []string{`action="/ui/assets/templates/helmet/rename"`, `action="/ui/assets/templates/helmet/delete"`, "js-inline-edit-toggle", "template-name-edit", "template-description-edit"} {
|
||||
for _, want := range []string{`action="/ui/assets/templates/helmet/rename"`, `action="/ui/assets/templates/helmet/delete"`, "js-inline-edit-toggle", "icon-only", "template-name-edit", "template-description-edit"} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("expected template detail to contain %q, got: %s", want, html)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user