init: DCU ESP32 调试控制台前端与本地页面服务

纯前端上位机界面 + esp32_server.py 静态服务/桌面窗口外壳, 不含固件、
不直接访问硬件; 业务 API 由 ESP32_COMM 固件 80 端口跨域提供。

- tabs.html 单窗口多标签外壳, iframe 承载各功能页
- index.html OTA 中继(ESP32 WiFi / GD32 relay / BLE), js/ota + js/ble
- can.html / can_monitor.html CAN 通信与接收监控, js/can
- motor.html 电机 CAN FD 控制台, js/motor
- continuity_test.html LAN9252 通断测试, spi_test.html SPI 测速(临时, 自包含)
- 三入口: esp32_app.vbs 桌面窗口 / esp32_web.vbs 浏览器 / serve.bat 控制台调试
- 补 README(页面与目录约定、?v= 缓存版本等改动须知)、.gitignore、
  .gitattributes(* -text 保持 Windows 换行原样)
This commit is contained in:
cbb
2026-09-03 21:30:32 +08:00
commit d2c86c425a
54 changed files with 20643 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
/* ============================================================
base.css — DCU Mission Control · 设计令牌与全局基底
风格:深色测控台(deep blue-slate),多色子系统语义配色
子系统色:ESP32=青绿 · GD32=琥珀 · BLE=蓝 · CAN=绿 · 内存/调试=紫
============================================================ */
:root {
/* ---- 底色层次:深蓝灰,层层抬升,拒绝纯黑 ---- */
--bg-root: #0a0f1a;
--bg-panel: #0e1624;
--bg-card: #131d2e;
--bg-elevated: #1a2740;
--bg-input: #0b1220;
--bg-hover: #20304c;
/* ---- 边框 ---- */
--border: #21304a;
--border-strong: #2e4262;
--border-subtle: #1a2740;
/* ---- 文字 ---- */
--text: #e9f0fa;
--text-dim: #93a8c4;
--text-label: #617a99;
/* ---- 子系统语义色 ---- */
--esp32: #2fd8c1; /* 青绿:ESP32 / 主操作 / WiFi OTA */
--esp32-hi: #6ff2e0;
--esp32-dim: rgba(47,216,193,.14);
--gd32: #fcbf3f; /* 琥珀:GD32 / 中继 OTA */
--gd32-hi: #ffd97a;
--gd32-dim: rgba(252,191,63,.14);
--ble: #5fa8ff; /* 蓝:BLE 蓝牙通道 */
--ble-hi: #93c6ff;
--ble-dim: rgba(95,168,255,.14);
--can: #43d97b; /* 绿:CAN 总线 / 健康 */
--can-hi: #7deda6;
--can-dim: rgba(67,217,123,.14);
--motor: #22d3ee; /* 青:电机控制台 */
--motor-hi: #67e8f9;
--motor-dim: rgba(34,211,238,.14);
--mem: #a88bfa; /* 紫:内存 / 调试 / 日志 */
--mem-hi: #c7b3ff;
--mem-dim: rgba(168,139,250,.14);
/* ---- 状态语义色 ---- */
--green: #3ddc8e;
--amber: #fcbf3f;
--red: #ff6b6b;
--red-dim: rgba(255,107,107,.14);
/* ---- 兼容旧令牌(大量旧样式引用,勿删)---- */
--accent: var(--esp32);
--accent-dim: var(--esp32-dim);
--border-focus: var(--esp32);
/* ---- 圆角 ---- */
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
/* ---- 字体 ----
沿用原始系统字体方案:中文优先苹方/雅黑,西文 Segoe UI;
mono 用于 SHA / HEX / 日志 / 数值(SF Mono → Fira Code → Cascadia → Consolas)。
--font-display 保留变量名以兼容既有引用,指向同一系统栈。 */
--font-display: "PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif;
--font: "PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif;
--font-mono: "SF Mono","Fira Code","Cascadia Code","Consolas",monospace;
/* ---- 间距 / 阴影 ---- */
--gap: 18px;
--shadow-card: 0 10px 30px rgba(3,7,16,.45), 0 1px 0 rgba(255,255,255,.03) inset;
--shadow-pop: 0 18px 48px rgba(3,7,16,.6);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg-root);
color: var(--text);
font-family: var(--font);
font-size: 14px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* ---- 环境光背景:细网格 + 双色氛围辉光 + 顶部扫光 ----
网格提供"工程图纸"质感;左上青绿、右下琥珀的两团极淡辉光
暗示 ESP32 / GD32 双核子系统,随视口固定,营造纵深。 */
body::before {
content: "";
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image:
linear-gradient(rgba(148,180,226,.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(148,180,226,.035) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: radial-gradient(ellipse 120% 90% at 50% -8%, black 30%, transparent 78%);
}
body::after {
content: "";
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(900px 520px at 8% -10%, rgba(47,216,193,.10), transparent 60%),
radial-gradient(820px 560px at 100% 112%, rgba(252,191,63,.07), transparent 62%),
radial-gradient(700px 480px at 88% -6%, rgba(95,168,255,.06), transparent 60%);
}
.container {
position: relative; z-index: 1;
max-width: 1480px; margin: 0 auto; padding: 26px 32px 44px;
}
/* ---- 全局滚动条 ---- */
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--border-strong); border-radius: 99px;
border: 2px solid var(--bg-root);
}
::-webkit-scrollbar-thumb:hover { background: #3a5378; }
/* ---- 选区 ---- */
::selection { background: rgba(47,216,193,.28); color: #eafffb; }
/* ---- 通用等宽数字(让指标/百分比对齐、不跳动)---- */
.tabular { font-variant-numeric: tabular-nums; }
/* ---- focus 可见性(键盘可达性)---- */
:focus-visible {
outline: 2px solid var(--esp32);
outline-offset: 2px;
border-radius: 4px;
}
+1712
View File
File diff suppressed because it is too large Load Diff
+944
View File
@@ -0,0 +1,944 @@
/* ============================================================
components.css — 上传 / 文件 / 进度 / 指标 / 徽章 / 阶段流 /
堆概况 / 任务栈 / 日志 / Toast / Tooltip 等组件
============================================================ */
/* ================= 上传区 ================= */
.upload-zone {
border: 2px dashed var(--border-strong); border-radius: var(--radius);
padding: 30px 16px; text-align: center; cursor: pointer;
background: rgba(255,255,255,.015);
transition: border-color .22s, background .22s, transform .22s, box-shadow .22s;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--esp32);
background: var(--esp32-dim);
box-shadow: 0 0 0 4px rgba(47,216,193,.07);
}
.upload-zone .upload-ico { font-size: 32px; margin-bottom: 10px; }
.upload-zone h4 {
font-family: var(--font-display);
font-size: 14px; font-weight: 600; margin-bottom: 4px; letter-spacing: .02em;
}
.upload-zone p { font-size: 12px; color: var(--text-dim); }
.upload-zone-sm { padding: 18px 12px; }
.upload-zone-sm .upload-ico { font-size: 24px; margin-bottom: 6px; }
.upload-zone-sm h4 { font-size: 13px; }
/* ================= 文件信息 ================= */
.file-info {
display: none; margin-top: 12px;
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 13px 15px;
}
.file-info.show { display: block; }
.file-info-row { display: flex; justify-content: space-between; align-items: center; }
.file-info-row + .file-info-row { margin-top: 10px; }
.file-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
.file-meta-item {
background: var(--bg-card); border: 1px solid var(--border-subtle);
padding: 8px 11px; border-radius: 7px; font-size: 12px;
}
.file-meta-item span {
color: var(--text-label); display: block;
font-family: var(--font-display);
font-size: 10px; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 3px;
}
.file-meta-item b { font-weight: 600; font-family: var(--font-mono); }
/* ================= 阶段切换胶囊(ESP32 OTA / GD32 OTA)================= */
.progress-phase-row {
display: flex; align-items: center; gap: 4px; margin-bottom: 15px;
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 999px; padding: 4px;
}
.pp-step {
flex: 1;
display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 10px; border-radius: 999px;
font-family: var(--font-display);
font-size: 12px; font-weight: 600; letter-spacing: .03em;
color: var(--text-dim); background: transparent;
transition: all .25s ease; white-space: nowrap; cursor: default;
}
.pp-ico { font-size: 13px; opacity: .45; transition: opacity .25s ease; }
.pp-step.active {
color: #04231e; background: linear-gradient(135deg, var(--esp32), #14b8a6);
box-shadow: 0 2px 12px rgba(47,216,193,.35);
}
.pp-step.active .pp-ico { opacity: 1; }
.pp-step.done { color: var(--can-hi); background: rgba(61,220,142,.13); }
.pp-step.done .pp-ico { opacity: 1; }
.pp-step.done .pp-txt::after { content: ' ✓'; font-size: 10px; }
/* ================= 进度条 ================= */
.progress-wrap { margin-top: 0; display: none; }
.progress-wrap.show { display: block; }
.progress-track {
width: 100%; height: 11px; border-radius: 999px;
background: var(--bg-input); overflow: hidden;
border: 1px solid var(--border-subtle);
box-shadow: inset 0 1px 3px rgba(0,0,0,.4);
}
.progress-fill {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, var(--esp32), #14b8a6);
width: 0%; transition: width .3s ease;
position: relative;
box-shadow: 0 0 10px rgba(47,216,193,.4);
}
.progress-fill::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.35) 50%, transparent 100%);
animation: sweep 1.6s linear infinite;
}
.progress-fill.done {
background: linear-gradient(90deg, var(--green), #16a374);
box-shadow: 0 0 10px rgba(61,220,142,.4);
}
@keyframes sweep { 0% { transform: translateX(-100%);} 100% { transform: translateX(100%);} }
.progress-text {
display: flex; justify-content: space-between; margin-top: 8px;
font-size: 12px; color: var(--text-dim);
}
.progress-text #progressPct {
font-family: var(--font-display); font-weight: 700;
color: var(--esp32-hi); font-variant-numeric: tabular-nums;
}
/* ================= 按钮行 ================= */
.btn-row { display: flex; gap: 10px; margin-top: 16px; }
.btn-row .btn { flex: 1; }
/* ================= 运行指标 ================= */
.metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.metric {
position: relative;
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 13px 15px;
overflow: hidden;
transition: border-color .22s, transform .22s, box-shadow .22s;
}
.metric::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: var(--metric-accent, var(--esp32));
opacity: .55;
}
.metric:nth-child(1) { --metric-accent: var(--esp32); } /* ESP32 温度 */
.metric:nth-child(2) { --metric-accent: var(--gd32); } /* GD32 温度 */
.metric:nth-child(3) { --metric-accent: var(--ble); } /* 输入 VCC */
.metric:nth-child(4) { --metric-accent: var(--mem); } /* VDDA */
.metric:nth-child(5) { --metric-accent: var(--gd32); } /* GD32 APP */
.metric:nth-child(6) { --metric-accent: var(--can); } /* GD32 BOOT */
.metric:hover {
border-color: color-mix(in srgb, var(--metric-accent, var(--esp32)) 45%, var(--border));
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(3,7,16,.4);
}
.metric .k {
font-family: var(--font);
font-size: 11px; font-weight: 500; color: var(--text-label);
letter-spacing: .04em; margin-bottom: 6px;
}
.metric .v {
font-family: var(--font);
font-size: 20px; font-weight: 700; line-height: 1.2;
word-break: break-word;
font-variant-numeric: tabular-nums;
}
.metric .v.highlight, .metric .v.accent { color: var(--esp32-hi); }
.metric .v.ok { color: var(--green); }
.metric .v.warn { color: var(--amber); }
.metric .v.err { color: var(--red); }
.status-kv-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.status-kv {
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 10px 13px;
}
.status-kv .k {
display: block; font-family: var(--font-display);
font-size: 10px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .1em; margin-bottom: 4px;
}
.status-kv b {
display: block; color: var(--text); font-size: 13px; font-weight: 700;
word-break: break-word; font-family: var(--font-mono);
}
/* ================= 主状态徽章 ================= */
.badge-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.badge-stat {
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: var(--radius); padding: 16px 16px 13px; text-align: center;
transition: border-color .22s, transform .22s, box-shadow .22s;
}
.badge-stat:hover {
border-color: var(--border-strong); transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(3,7,16,.4);
}
.badge-stat .v {
font-family: var(--font-display);
font-size: 23px; font-weight: 700; letter-spacing: .01em;
font-variant-numeric: tabular-nums;
}
.badge-stat .v.accent { color: var(--esp32-hi); }
.badge-stat .v.ok { color: var(--green); text-shadow: 0 0 14px rgba(61,220,142,.35); }
.badge-stat .v.warn { color: var(--amber); }
.badge-stat .v.err { color: var(--red); }
.badge-stat .v.bad { color: var(--red); }
.badge-stat .k {
font-family: var(--font-display);
font-size: 10px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .13em; margin-top: 4px;
}
/* ---- 健康状态 → 启动自检 ---- */
.badge-stat-click { cursor: pointer; user-select: none; }
.badge-stat-click:hover, .badge-stat-click:focus-visible {
outline: none;
border-color: color-mix(in srgb, var(--esp32) 45%, var(--border));
background: color-mix(in srgb, var(--esp32) 7%, var(--bg-elevated));
}
.badge-stat-click.is-open {
border-color: color-mix(in srgb, var(--esp32) 50%, var(--border));
}
.health-boot-hint {
font-size: 9px; letter-spacing: 0; text-transform: none;
color: var(--text-dim); font-weight: 500; margin-left: 3px;
}
.badge-stat-click.is-open .health-boot-hint { color: var(--esp32-hi); }
.health-boot-panel {
margin-top: 12px; padding: 13px 15px;
border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--bg-elevated);
}
.health-boot-panel.collapsed, .health-boot-panel[hidden] { display: none; }
.health-boot-head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 10px; margin-bottom: 7px;
}
.health-boot-title {
font-family: var(--font-display);
font-size: 11px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .1em; font-weight: 600;
}
.health-boot-summary { font-family: var(--font-display); font-size: 14px; font-weight: 700; color: var(--text); text-align: right; }
.health-boot-summary.ok { color: var(--green); }
.health-boot-summary.warn{ color: var(--amber); }
.health-boot-summary.err { color: var(--red); }
.health-boot-failed { font-size: 12px; color: var(--text-dim); margin-bottom: 9px; }
.health-boot-failed b { color: var(--text); font-weight: 600; }
.health-boot-failed b.ok { color: var(--green); }
.health-boot-failed b.warn{ color: var(--amber); }
.health-boot-failed b.err { color: var(--red); }
/* ---- 启动模块芯片 ---- */
.boot-module-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; min-height: 8px; }
.boot-mod {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 9px; border-radius: 999px;
border: 1px solid var(--border); background: var(--bg-root);
font-family: var(--font-mono);
font-size: 11px; font-weight: 600; color: var(--text-dim); line-height: 1.2;
}
.boot-mod .boot-mod-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); flex-shrink: 0; }
.boot-mod.ok { color: var(--can-hi); border-color: rgba(61,220,142,.4); background: rgba(61,220,142,.08); }
.boot-mod.ok .boot-mod-dot { background: var(--green); }
.boot-mod.fail { color: #ff9d9d; border-color: rgba(255,107,107,.4); background: rgba(255,107,107,.08); }
.boot-mod.fail .boot-mod-dot { background: var(--red); }
.boot-mod.fail.optional { color: var(--gd32-hi); border-color: rgba(252,191,63,.4); background: rgba(252,191,63,.08); }
.boot-mod.fail.optional .boot-mod-dot { background: var(--amber); }
.status-section-title {
margin-top: 15px; margin-bottom: 9px;
font-family: var(--font-display);
font-size: 11px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .12em; font-weight: 600;
}
/* ================= 堆概况 / CPU / 任务栈 ================= */
.heap-group {
background: rgba(255,255,255,.02);
border: 1px solid var(--border); border-radius: 9px;
padding: 13px 15px; margin-bottom: 10px;
}
.heap-group:last-child { margin-bottom: 0; }
.cpu-group { margin-bottom: 8px; }
.cpu-task-list { margin-bottom: 12px; }
.cpu-task-list .status-task { background: rgba(255,255,255,.025); }
.cpu-fill { background: linear-gradient(90deg, var(--ble), var(--esp32)); }
.heap-group-title {
font-family: var(--font-display);
font-size: 11px; font-weight: 600; color: var(--text-label);
text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px;
}
.heap-bar-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.heap-bar-text {
order: 1; flex: 1 1 auto; min-width: 0;
font-family: var(--font-display);
font-size: 13px; font-weight: 600; white-space: nowrap; color: var(--text);
}
.heap-bar-pct {
order: 2; flex: 0 0 auto;
font-family: var(--font-mono);
font-size: 12px; font-weight: 600; min-width: 44px; text-align: right;
font-variant-numeric: tabular-nums; color: var(--text-dim);
}
.heap-bar-track {
order: 3; flex: 1 1 100%; width: 100%; height: 14px;
background: rgba(255,255,255,.06); border-radius: 7px;
overflow: hidden; position: relative;
}
.heap-bar-fill {
display: block; height: 100%; border-radius: 7px;
background: linear-gradient(90deg, var(--esp32), #5eead4);
transition: width .5s cubic-bezier(.4,0,.2,1);
}
.heap-bar-fill.warn { background: linear-gradient(90deg, var(--amber), #d97706); }
.heap-bar-fill.danger { background: linear-gradient(90deg, var(--red), #dc2626); }
.heap-detail-row {
display: flex; gap: 16px; flex-wrap: wrap;
font-size: 11px; color: var(--text-dim);
margin-top: 7px; padding-top: 7px;
border-top: 1px solid rgba(255,255,255,.05);
}
.heap-detail-row span { display: inline-flex; align-items: center; gap: 4px; }
.heap-detail-row b { color: var(--text); font-weight: 500; font-family: var(--font-mono); font-size: 11px; }
/* ---- SRAM 分段条 ---- */
.mem-sram-track { display: flex; overflow: hidden; gap: 0; background: rgba(255,255,255,.04); }
.mem-seg { display: block; height: 100%; min-width: 0; transition: width .5s ease; }
.mem-seg-iram { background: linear-gradient(90deg, var(--mem), #7c5ce0); }
.mem-seg-data { background: linear-gradient(90deg, #fb923c, #ea580c); }
.mem-seg-bss { background: linear-gradient(90deg, #94a3b8, #64748b); }
.mem-seg-static { background: linear-gradient(90deg, #64748b, #475569); }
.mem-seg-used { background: linear-gradient(90deg, var(--esp32), #5eead4); }
.mem-seg-free { background: rgba(255,255,255,.08); }
.heap-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 9px; font-size: 11px; color: var(--text-dim); }
.heap-legend-item { display: inline-flex; align-items: center; gap: 5px; }
.heap-legend-item b { color: var(--text); font-weight: 500; font-family: var(--font-mono); font-size: 11px; }
.mem-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: 0 0 auto; }
.mem-dot-iram { background: var(--mem); }
.mem-dot-data { background: #f97316; }
.mem-dot-bss { background: #64748b; }
.mem-dot-static { background: #64748b; }
.mem-dot-used { background: var(--esp32); }
.mem-dot-free { background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.22); box-sizing: border-box; }
.mem-static-list { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: var(--text-dim); }
.mem-static-row {
display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center;
padding: 3px 0; border-bottom: 1px solid rgba(255,255,255,.04);
}
.mem-static-row:last-child { border-bottom: 0; }
.mem-static-name { color: var(--text-label); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mem-static-size { font-family: var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; }
.mem-static-pct { font-family: var(--font-mono); min-width: 42px; text-align: right; color: var(--text-dim); }
.heap-hint {
margin-top: 8px; padding-top: 6px;
border-top: 1px solid rgba(255,255,255,.05);
font-size: 10px; line-height: 1.5; color: var(--text-dim); opacity: .9;
}
/* ---- 任务列表 ---- */
.status-task-list { display: grid; gap: 8px; }
.status-task-list.list-scroll-top3 {
--status-task-row-est: 6.9rem;
max-height: calc(3 * var(--status-task-row-est) + 2 * 8px);
overflow-y: auto; overflow-x: hidden;
padding-right: 4px; scrollbar-gutter: stable; overscroll-behavior: contain;
}
.status-task-list.list-scroll-top3::-webkit-scrollbar { width: 6px; }
.status-task-list.list-scroll-top3::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
.status-task-list.list-scroll-top3::-webkit-scrollbar-track { background: transparent; }
.status-task {
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 11px 13px;
}
.status-task-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.status-task-name { font-size: 12px; font-weight: 700; color: var(--text); font-family: var(--font-mono); }
.status-task-ratio { font-size: 11px; color: var(--text-dim); font-family: var(--font-mono); }
.status-task-bar { width: 100%; height: 8px; border-radius: 999px; background: var(--bg-input); overflow: hidden; margin-bottom: 8px; }
.status-task-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--esp32), #14b8a6); }
.status-task-bar-fill.warn { background: linear-gradient(90deg, var(--amber), #d97706); }
.status-task-bar-fill.err { background: linear-gradient(90deg, var(--red), #dc2626); }
.status-task-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.status-task-meta span {
display: block; font-family: var(--font-display);
font-size: 10px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .07em;
}
.status-task-meta b { display: block; margin-top: 2px; color: var(--text); font-size: 11px; font-family: var(--font-mono); }
/* ---- 二级折叠 + 原始 JSON ---- */
.status-subfold { margin-top: 15px; }
.status-subfold-head {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
cursor: pointer; user-select: none; padding: 6px 0; border-radius: var(--radius-sm);
}
.status-subfold-head:hover { color: var(--text); }
.status-subfold-title { margin: 0 !important; flex: 1; }
.status-subfold-head .collapse-arrow { flex: none; font-size: 10px; color: var(--text-dim); transition: transform .15s ease; }
.status-subfold.collapsed .collapse-arrow { transform: rotate(-90deg); }
.status-subfold.collapsed .status-subfold-body { display: none; }
.status-subfold-body { margin-top: 6px; }
.status-json {
margin-top: 0; padding: 12px;
border-radius: var(--radius-sm); border: 1px solid var(--border);
background: var(--bg-input); color: var(--text-dim);
font-size: 11px; line-height: 1.65;
max-height: 280px; overflow: auto;
white-space: pre-wrap; word-break: break-word;
font-family: var(--font-mono); resize: vertical; min-height: 80px;
}
.status-empty {
padding: 13px; border: 1px dashed var(--border-strong);
border-radius: var(--radius-sm); color: var(--text-dim); font-size: 12px;
}
/* 抽屉内强制等宽 */
.sys-drawer-body .heap-bar-pct,
.sys-drawer-body .heap-detail-row b,
.sys-drawer-body .status-task-name,
.sys-drawer-body .status-task-ratio,
.sys-drawer-body .status-task-meta b,
.sys-drawer-body .status-json { font-family: var(--font-mono); }
.sys-drawer-body .status-section-title,
.sys-drawer-body .heap-group-title,
.sys-drawer-body .heap-bar-text { font-family: var(--font-display); color: inherit; }
.sys-drawer-body .status-section-title { color: var(--text-label); font-size: 11px; font-weight: 600; }
.sys-drawer-body .heap-bar-text { color: var(--text); font-size: 13px; font-weight: 600; }
.sys-drawer-body .status-json { color: var(--text-dim); font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
/* ================= OTA 阶段流 ================= */
.stage-flow { display: flex; align-items: flex-start; gap: 0; padding: 10px 0 6px; }
.stage-node { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 7px; position: relative; }
.stage-node:not(:last-child)::after {
content: ""; position: absolute; top: 19px; left: calc(50% + 20px);
width: calc(100% - 40px); height: 2px;
background: var(--border); transition: background .3s;
}
.stage-node.done:not(:last-child)::after,
.stage-node.live:not(:last-child)::after { background: var(--esp32); }
.stage-dot {
width: 38px; height: 38px; border-radius: 50%;
background: var(--bg-input); border: 2px solid var(--border-strong);
display: flex; align-items: center; justify-content: center;
font-family: var(--font-display); font-size: 16px; font-weight: 700;
color: var(--text-label);
z-index: 1; transition: all .3s;
}
.stage-node.live .stage-dot {
background: linear-gradient(135deg, var(--esp32), #14b8a6);
border-color: var(--esp32); color: #04231e;
box-shadow: 0 0 16px rgba(47,216,193,.45);
}
.stage-node.done .stage-dot {
background: linear-gradient(135deg, var(--green), #16a374);
border-color: var(--green); color: #03231a;
box-shadow: 0 0 12px rgba(61,220,142,.35);
}
.stage-node.err .stage-dot {
background: linear-gradient(135deg, var(--red), #dc4c4c);
border-color: var(--red); color: #fff;
box-shadow: 0 0 12px rgba(255,107,107,.4);
}
.stage-label {
font-family: var(--font-display);
font-size: 11px; color: var(--text-label); text-align: center; line-height: 1.3;
letter-spacing: .02em;
}
.stage-detail {
font-size: 10px; color: var(--esp32-hi); text-align: center; line-height: 1.2;
margin-top: 2px; min-height: 14px;
font-family: var(--font-mono); transition: color .3s;
}
.stage-node.live .stage-label,
.stage-node.done .stage-label,
.stage-node.err .stage-label { color: var(--text); }
.stage-node.live .stage-detail { color: var(--esp32-hi); }
.stage-node.done .stage-detail { color: var(--green); }
.stage-node.err .stage-detail { color: var(--red); }
/* ---- 阶段状态条 ---- */
.stage-status-bar {
display: flex; align-items: center; gap: 10px;
margin-top: 15px; padding: 11px 17px;
border-radius: 999px;
background: var(--bg-elevated); border: 1px solid var(--border);
transition: all .3s;
}
.stage-status-bar .ssb-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--text-dim); animation: dotPulse 2.5s ease-in-out infinite;
}
.stage-status-bar .ssb-text {
font-family: var(--font-display);
color: var(--text-dim); font-size: 13px; font-weight: 600; letter-spacing: .02em;
}
@keyframes dotPulse { 0%,100% { opacity: .45;} 50% { opacity: .85;} }
.stage-status-bar.active {
border-color: rgba(47,216,193,.35);
background: linear-gradient(135deg, rgba(47,216,193,.09), rgba(47,216,193,.02));
}
.stage-status-bar.active .ssb-dot {
background: var(--esp32); box-shadow: 0 0 8px var(--esp32-dim);
animation: dotPulseLive 1.2s ease-in-out infinite;
}
.stage-status-bar.active .ssb-text { color: var(--esp32-hi); }
@keyframes dotPulseLive { 0%,100% { opacity: .7; transform: scale(1);} 50% { opacity: 1; transform: scale(1.3);} }
.stage-status-bar.ok {
border-color: rgba(61,220,142,.35);
background: linear-gradient(135deg, rgba(61,220,142,.09), rgba(61,220,142,.02));
}
.stage-status-bar.ok .ssb-dot { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.4); animation: none; }
.stage-status-bar.ok .ssb-text { color: var(--green); }
.stage-status-bar.err {
border-color: rgba(255,107,107,.35);
background: linear-gradient(135deg, rgba(255,107,107,.09), rgba(255,107,107,.02));
}
.stage-status-bar.err .ssb-dot { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.4); animation: none; }
.stage-status-bar.err .ssb-text { color: var(--red); }
/* ---- 校验信息面板 ---- */
.stage-verify-info {
margin-top: 12px; padding: 13px 15px;
border-radius: var(--radius-sm);
background: var(--bg-elevated); border: 1px solid var(--border);
}
.svi-row {
display: flex; align-items: flex-start; gap: 12px;
padding: 7px 0; font-size: 12px;
border-bottom: 1px solid rgba(255,255,255,.04);
}
.svi-row:last-child { border-bottom: none; }
.svi-key {
color: var(--text-label); font-weight: 600;
font-family: var(--font-display);
text-transform: uppercase; letter-spacing: .07em; font-size: 10px;
min-width: 104px; flex-shrink: 0; padding-top: 2px;
}
.svi-val { color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; word-break: break-all; }
.svi-val.ok { color: var(--green); font-weight: 600; }
.svi-val.err { color: var(--red); font-weight: 600; }
/* ---- 阶段结果面板 ---- */
.stage-result {
margin-top: 12px; padding: 15px 17px;
border-radius: var(--radius-sm);
background: var(--bg-elevated); border: 1px solid var(--border);
display: flex; flex-direction: column; gap: 10px;
}
.stage-result.ok { border-color: rgba(61,220,142,.5); background: rgba(61,220,142,.08); }
.stage-result.err { border-color: rgba(255,107,107,.5); background: rgba(255,107,107,.08); }
.stage-result .sr-head { display: flex; align-items: center; gap: 10px; }
.stage-result .sr-ico {
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 700; color: #04231e;
}
.stage-result.ok .sr-ico { background: var(--green); }
.stage-result.err .sr-ico { background: var(--red); color: #fff; }
.stage-result .sr-title { font-family: var(--font-display); font-size: 16px; font-weight: 700; }
.stage-result.ok .sr-title { color: var(--green); }
.stage-result.err .sr-title { color: var(--red); }
.stage-result .sr-detail { font-size: 13px; color: var(--text); line-height: 1.55; }
.stage-result .sr-actions { display: flex; justify-content: flex-end; gap: 8px; }
/* ================= 日志面板 ================= */
.log-panel { display: flex; flex-direction: column; min-height: 380px; }
.log-panel .card-body { flex: 1; display: flex; flex-direction: column; padding: 0; }
.log-toolbar {
display: flex; align-items: center; justify-content: space-between;
padding: 11px 16px; border-bottom: 1px solid var(--border-subtle);
gap: 10px; flex-wrap: wrap;
}
.log-filters { display: flex; gap: 6px; }
.log-filter {
padding: 5px 12px; border-radius: 999px;
font-family: var(--font-display);
font-size: 11px; font-weight: 600; letter-spacing: .03em;
border: 1px solid var(--border); background: transparent; color: var(--text-dim);
cursor: pointer; transition: all .18s;
}
.log-filter.active {
background: linear-gradient(135deg, var(--esp32), #14b8a6);
color: #04231e; border-color: var(--esp32);
box-shadow: 0 2px 10px rgba(47,216,193,.3);
}
.log-filter:hover:not(.active) { border-color: var(--border-strong); color: var(--text); }
.log-scroll {
flex: 1; overflow-y: auto; padding: 12px 16px;
font-family: var(--font-mono); font-size: 12px; line-height: 1.7;
max-height: 420px; background: rgba(4,8,16,.35);
}
.log-scroll::-webkit-scrollbar { width: 5px; }
.log-scroll::-webkit-scrollbar-track { background: transparent; }
.log-scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
.log-line {
display: flex; gap: 9px; padding: 3px 0;
border-bottom: 1px solid rgba(255,255,255,.025);
}
.log-line .lt { color: var(--text-label); flex-shrink: 0; }
.log-line .lv { font-weight: 700; min-width: 20px; flex-shrink: 0; }
.log-line .lv.E { color: var(--red); }
.log-line .lv.W { color: var(--amber); }
.log-line .lv.I { color: var(--esp32); }
.log-line .lv.D { color: var(--text-label); }
.log-line .lg, .log-line .tag { color: var(--mem-hi); flex-shrink: 0; }
.log-line .lm { color: var(--text); word-break: break-all; }
.log-line .t { color: var(--text-label); flex-shrink: 0; }
.log-foot {
display: flex; justify-content: space-between; align-items: center;
padding: 9px 16px; border-top: 1px solid var(--border-subtle);
font-size: 11px; color: var(--text-label);
}
/* ================= Toast ================= */
.toast {
position: fixed; top: 45%; left: 50%;
transform: translateX(-50%) translateY(30px);
z-index: 9999;
display: flex; align-items: center; gap: 11px;
padding: 14px 26px; border-radius: 12px;
font-family: var(--font-display);
font-size: 14px; font-weight: 600; letter-spacing: .02em;
color: #fff; pointer-events: none; opacity: 0;
transition: all .35s cubic-bezier(.4,0,.2,1);
box-shadow: 0 12px 40px rgba(2,5,12,.55);
border: 1px solid rgba(255,255,255,.12);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
.toast.ok { background: linear-gradient(135deg, #16a374, #0e8a5f); }
.toast.err { background: linear-gradient(135deg, #dc4c4c, #c03535); }
.toast.warn { background: linear-gradient(135deg, #d98a06, #b87405); }
.toast .toast-ico { font-size: 18px; }
/* ================= 杂项 ================= */
.row-2, .row-3, .row-4 { display: grid; gap: 10px; }
.row-2 { grid-template-columns: 1fr 1fr; }
.row-3 { grid-template-columns: repeat(3, 1fr); }
.row-4 { grid-template-columns: repeat(4, 1fr); }
.sub-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.footer-note { margin-top: 12px; font-size: 12px; line-height: 1.7; color: var(--text-dim); }
code { color: var(--esp32-hi); font-family: var(--font-mono); }
/* ================= 自定义 Tooltip(pill)================= */
.pill[data-tooltip] { position: relative; cursor: help; }
.pill[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute; top: calc(100% + 10px); left: 50%;
transform: translateX(-50%);
background: var(--bg-elevated); color: var(--text);
font-size: 11px; font-weight: 400; font-family: var(--font-mono);
padding: 6px 12px; border-radius: 7px;
border: 1px solid var(--border-strong);
box-shadow: 0 6px 20px rgba(2,5,12,.5);
white-space: nowrap; z-index: 1000; pointer-events: none;
animation: tooltipFadeIn .15s ease-out;
}
.pill[data-tooltip]:hover::before {
content: ''; position: absolute; top: calc(100% + 4px); left: 50%;
transform: translateX(-50%);
border: 6px solid transparent; border-bottom-color: var(--border-strong);
z-index: 1001; pointer-events: none;
animation: tooltipFadeIn .15s ease-out;
}
@keyframes tooltipFadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(3px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* ================= 堆低水位 Tooltip ================= */
.heap-low-tooltip {
position: fixed; z-index: 9999;
min-width: 480px; max-width: 640px;
padding: 11px 13px 9px;
background: var(--bg-elevated);
border: 1px solid var(--border-strong);
border-radius: 9px;
box-shadow: 0 8px 28px rgba(2,5,12,.6);
font-size: 11px; color: var(--text);
pointer-events: auto; visibility: hidden;
}
.heap-low-title { font-weight: 600; margin-bottom: 6px; color: var(--text); display: flex; align-items: center; }
.heap-low-sub { font-weight: 400; color: var(--text-dim); margin-left: 6px; }
.heap-low-clear {
margin-left: auto; margin-right: 6px; height: 22px; padding: 0 9px;
border: 1px solid rgba(255,107,107,.45); border-radius: 5px;
background: rgba(255,107,107,.12); color: #ff9d9d;
font-size: 11px; line-height: 20px; cursor: pointer;
transition: background .12s, color .12s, border-color .12s;
}
.heap-low-clear:hover:not(:disabled) { background: rgba(255,107,107,.22); border-color: rgba(255,107,107,.7); color: #ffcdd2; }
.heap-low-clear:disabled { opacity: .55; cursor: wait; }
.heap-low-close {
width: 22px; height: 22px; padding: 0; border: none; border-radius: 5px;
background: transparent; color: var(--text-dim);
font-size: 18px; line-height: 1; cursor: pointer;
transition: background .12s, color .12s;
}
.heap-low-close:hover { background: rgba(255,255,255,.08); color: var(--text); }
.heap-low-table-wrap { max-height: 360px; overflow-y: auto; }
.heap-low-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 10.5px; }
.heap-low-section { margin-bottom: 6px; }
.heap-low-section + .heap-low-section { margin-top: 8px; padding-top: 6px; border-top: 1px dashed rgba(255,255,255,.15); }
.heap-low-section-title {
font-size: 11px; font-weight: 600; color: var(--text-dim);
padding: 4px 6px 2px; background: rgba(255,255,255,.03); border-radius: 3px 3px 0 0;
}
.heap-low-section-hist .heap-low-section-title { color: var(--gd32-hi); }
.heap-low-section-hist .heap-low-table { opacity: .92; }
.heap-low-table th, .heap-low-table td {
padding: 3px 6px; text-align: right; white-space: nowrap;
border-bottom: 1px solid rgba(255,255,255,.04);
}
.heap-low-table th { color: var(--text-dim); font-weight: 500; position: sticky; top: 0; background: var(--bg-elevated); text-align: right; }
.ble-low-table td:nth-child(5), .ble-low-table td:nth-child(6),
.ble-low-table td:nth-child(7), .ble-low-table td:nth-child(9) { text-align: left; }
.ble-low-table td:first-child { color: var(--text-dim); }
/* ================= BLE 连接按钮(launch 风格) =================
圆形物理按键:未连接=暗淡待机;勾选=已连接,蓝光环绕 + 文字波浪动画。
checkbox 隐藏,由 JS setConnectionState 驱动 checked,避免异步连接失败导致状态错位。 */
.ble-launch-wrap {
display: flex; flex-direction: column; align-items: center;
gap: 12px; padding: 6px 0 2px;
}
.ble-launch-btn {
cursor: pointer; position: relative;
display: flex; flex-direction: column; align-items: center;
margin: 0;
}
.ble-launch-btn input {
opacity: 0; width: 0; height: 0; display: none;
}
/* —— 三态:idle 待机 / connecting 连接中转圈 / connected 已连接变亮 —— */
/* 默认 text3 隐藏 */
.ble-launch-btn .back .mid .ble-dial .center .start .text3 {
opacity: 0;
}
/* connecting:仅光晕转圈(loading),按钮整体保持暗淡 */
.ble-launch-btn[data-ble-state="connecting"] .back .spot-shadow,
.ble-launch-btn[data-ble-state="connecting"] .back .mid .spot-main,
.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .spot-light {
opacity: 0.7;
animation: ble-spot-rot 1.4s linear infinite;
}
.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .center .start .text1 {
opacity: 0;
}
.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .center .start .text3 {
opacity: 1;
color: #9bb8d8;
animation: ble-text-pulse 1.2s ease-in-out infinite;
}
/* connected:整个按钮变亮 + 静态整圈蓝光晕(不转圈)+ 文字波浪 */
/* spot 元素默认是横向条状(为转圈动画设计),connected 时改成整圈圆环 */
.ble-launch-btn[data-ble-state="connected"] .back .spot-shadow {
transition: opacity 1.2s ease;
opacity: 0.9;
width: 106px; height: 106px;
border-radius: 50%;
filter: blur(12px) brightness(1.2);
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .spot-main {
transition: opacity 1.2s ease;
opacity: 0.95;
width: 100px; height: 100px;
border-radius: 50%;
filter: blur(9px) brightness(1.6);
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .spot-light {
transition: opacity 1.2s ease;
opacity: 0.9;
width: 92px; height: 92px;
border-radius: 50%;
filter: blur(8px) brightness(1.2);
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .icon svg {
filter: brightness(1.15);
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .launch {
color: #e9f0fa;
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .launch > div {
animation: ble-wave-text 1.8s calc(var(--i) * -0.13s) infinite;
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .start .text1 {
transition: transform 0.5s ease, opacity 0.5s ease;
transform: translateX(10px);
opacity: 0;
}
.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .start .text2 {
opacity: 1;
animation: ble-text2 0.5s forwards;
}
.ble-launch-btn:active .back .mid .ble-dial {
transform: scale(0.94);
box-shadow:
0 -1px 2px rgba(255,255,255,0.3),
inset 0 0 10px rgba(0,0,0,0.24),
inset 0 -13px 26px rgba(0,0,0,0.8);
}
/* —— 外圈金属环 —— */
.ble-launch-btn .back {
position: relative; z-index: 200;
width: 128px; height: 128px;
display: flex; justify-content: center; align-items: center;
border-radius: 50%;
background-image: linear-gradient(0, #5e5e61 20%, #212123);
box-shadow:
inset 0 1px 1px rgba(255,255,255,0.3),
inset 0 3px 5px rgba(255,255,255,0.6),
inset 0 -4px 2px rgba(255,255,255,0.05),
inset 0 -2px 0 rgba(0,0,0,0.15),
inset -2px 4px 0 rgba(0,0,0,0.3),
inset 2px 4px 0 rgba(0,0,0,0.3),
0 -7px 16px 2px rgba(255,255,255,0.18),
0 4px 16px 2px rgba(0,0,0,0.5),
0 2px 5px #7c7c7c;
cursor: pointer;
}
.ble-launch-btn .back .spot-shadow {
position: absolute;
width: 96px; height: 27px;
background: rgba(95,168,255,0.9);
opacity: 0;
filter: blur(14px) brightness(1.1);
}
.ble-launch-btn .back .mid {
overflow: hidden; position: relative;
width: 106px; height: 106px;
display: flex; justify-content: center; align-items: center;
border-radius: 50%;
background: #000;
box-shadow: 0 0 4px #000, 0 1px 2px rgba(255,255,255,0.8);
}
.ble-launch-btn .back .mid::before {
content: ''; position: absolute; z-index: 500;
width: 100%; height: 100%; border-radius: 50%;
background: transparent;
box-shadow: inset 0 -12px 18px rgba(0,0,0,0.8);
pointer-events: none;
}
.ble-launch-btn .back .mid .spot-main {
position: absolute;
width: 110px; height: 20px;
background: #5fa8ff;
opacity: 0;
filter: blur(11px) brightness(1.5);
}
.ble-launch-btn .back .mid .ble-dial {
position: relative; z-index: 90;
display: flex; justify-content: center; align-items: center;
width: 100px; height: 100px; border-radius: 50%;
background: #424242;
box-shadow:
0 -1px 2px rgba(255,255,255,0.5),
inset 0 -9px 18px rgba(0,0,0,0.8);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.ble-launch-btn .back .mid .ble-dial .spot-light {
position: absolute;
width: 110px; height: 20px;
opacity: 0;
background: rgba(95,168,255,0.6);
filter: blur(14px) brightness(1.1);
}
.ble-launch-btn .back .mid .ble-dial .center {
position: relative; z-index: 100;
width: 86px; height: 86px;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 2px;
border-radius: 50%;
background: #232323;
cursor: pointer;
}
.ble-launch-btn .back .mid .ble-dial .center .icon svg {
margin-top: -3px;
width: 18px;
filter: brightness(1) grayscale(1);
transition: filter 1.5s ease;
}
.ble-launch-btn .back .mid .ble-dial .center .launch {
display: flex; margin-top: -3px;
font-size: 13px; color: #595959;
font-family: var(--font-display);
letter-spacing: 0.04em;
}
.ble-launch-btn .back .mid .ble-dial .center .start {
position: relative;
width: 100%; height: 11px;
font-size: 9px; color: #737373;
text-align: center;
font-family: var(--font-display);
}
.ble-launch-btn .back .mid .ble-dial .center .start > div {
position: absolute; left: 50%; top: 0;
transform: translateX(-50%);
white-space: nowrap;
}
.ble-launch-btn .back .mid .ble-dial .center .start .text2 {
opacity: 0;
display: inline-flex;
}
.ble-launch-btn .back .mid .ble-dial .center .start .text2 > span {
animation: ble-wave-text 1.8s calc(var(--n) * -0.13s) infinite;
}
.ble-launch-hint {
font-size: 11px; color: var(--text-dim);
letter-spacing: 0.04em;
}
@keyframes ble-wave-text {
0% { opacity: 0.7; }
50% { color: #c6e0ff; opacity: 1; }
100% { opacity: 0.7; }
}
@keyframes ble-spot-rot {
0% { transform: rotate(0); }
100% { transform: rotate(360deg); }
}
@keyframes ble-text2 {
0% { transform: translateX(-80%); }
100% { transform: translateX(-50%); }
}
@keyframes ble-text-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.heap-low-table tbody tr:hover { background: rgba(255,255,255,.03); }
.heap-low-table td.has-suspect { color: var(--red); font-weight: 500; }
.heap-low-table td.idle-suspect { color: var(--gd32-hi); }
.heap-low-detail-meta { display: flex; gap: 18px; font-size: 10px; color: var(--text-dim); margin-bottom: 6px; }
.heap-low-detail-meta b { color: var(--text); font-family: var(--font-mono); }
.heap-low-hook-meta {
margin: 8px 0 6px; padding: 6px 9px;
background: rgba(255,107,107,.08); border-left: 2px solid var(--red);
display: flex; flex-wrap: wrap; gap: 12px;
font-size: 10px; color: var(--text-dim);
}
.heap-low-hook-title { width: 100%; color: var(--red); font-weight: 600; margin-bottom: 2px; }
.heap-low-hook-meta b { color: var(--text); font-family: var(--font-mono); }
.heap-low-caller { color: var(--red); }
.heap-low-caller-hint { color: var(--text-dim); font-size: 9px; }
.heap-low-main-row { cursor: pointer; }
.heap-low-expand-hint { font-size: 9px; color: var(--text-dim); margin-left: 4px; }
.heap-low-stack-detail > td { padding: 6px 8px 8px; background: rgba(0,0,0,.2); }
.heap-low-stack-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 10px; }
.heap-low-stack-table th, .heap-low-stack-table td { padding: 2px 6px; text-align: left; border-bottom: 1px solid rgba(255,255,255,.03); }
.heap-low-stack-table th { color: var(--text-dim); font-weight: 500; }
.heap-low-stack-table td:last-child { text-align: right; }
.heap-low-stack-table tr.crit td { color: var(--red); }
.heap-low-stack-table tr.warn td { color: var(--gd32-hi); }
.heap-low-empty { padding: 14px 6px; color: var(--text-dim); text-align: center; }
.heap-low-tip { margin-top: 6px; font-size: 10px; color: var(--text-dim); }
+337
View File
@@ -0,0 +1,337 @@
/* ============================================================
layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉
============================================================ */
/* ================= HEADER — 命令栏 ================= */
.header-bar {
position: relative;
z-index: 20; /* 高于下方卡片,保证 pill tooltip 不被遮挡 */
display: flex; align-items: center; gap: 16px;
padding: 16px 22px; margin-bottom: 26px;
background:
radial-gradient(620px 240px at 88% -40%, rgba(47,216,193,.12), transparent 60%),
linear-gradient(160deg, var(--bg-panel) 0%, var(--bg-card) 70%);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
flex-wrap: wrap;
}
/* 顶部三色霓虹描边:ESP32 青绿 → BLE 蓝 → GD32 琥珀,一条细线点出子系统谱系 */
.header-bar::before {
content: "";
position: absolute; top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg,
var(--esp32) 0%, var(--ble) 38%, var(--gd32) 78%, transparent 100%);
opacity: .85;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.header-brand {
display: flex; align-items: center; gap: 12px;
padding-right: 20px; border-right: 1px solid var(--border);
white-space: nowrap;
}
.brand-logo {
width: 40px; height: 40px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
border-radius: 11px;
background: linear-gradient(145deg, rgba(47,216,193,.16), rgba(95,168,255,.10));
border: 1px solid rgba(47,216,193,.3);
box-shadow: 0 0 18px rgba(47,216,193,.18), inset 0 0 12px rgba(47,216,193,.08);
color: var(--esp32);
}
.brand-logo svg { width: 24px; height: 24px; display: block; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong {
font-family: var(--font-display);
font-size: 17px; font-weight: 700; letter-spacing: .02em;
background: linear-gradient(92deg, var(--text) 30%, var(--esp32-hi) 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent;
}
.brand-sub {
font-family: var(--font-mono);
font-size: 10px; letter-spacing: .22em; text-transform: uppercase;
color: var(--text-label); margin-top: 2px;
}
/* 兼容旧结构:独立 dot-mark(无 brand-logo 时)仍显示为一颗呼吸 LED */
.header-brand .dot-mark {
width: 10px; height: 10px; border-radius: 50%;
background: var(--esp32);
box-shadow: 0 0 10px var(--esp32-dim);
flex-shrink: 0;
}
.header-brand strong { font-size: 16px; font-weight: 700; }
.header-status {
display: flex; align-items: center; gap: 8px;
flex-wrap: wrap;
}
.header-sub {
display: flex; gap: 10px; margin-left: auto;
flex-wrap: wrap;
}
/* ---- 状态胶囊 ---- */
.pill {
display: inline-flex; align-items: center; gap: 7px;
padding: 6px 13px; border-radius: 999px;
font-size: 12px; font-weight: 500;
font-family: var(--font-mono);
background: var(--bg-elevated); border: 1px solid var(--border);
color: var(--text-dim);
white-space: nowrap;
transition: border-color .2s, background .2s, transform .2s;
}
.pill .indicator {
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
background: var(--text-label);
}
.pill.online .indicator { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); }
.pill.offline .indicator { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); }
.pill.processing .indicator,
.pill.live .indicator { background: var(--amber); animation: blink 1s infinite; }
.pill.idle .indicator { background: var(--text-dim); }
.pill.online { color: var(--can-hi); border-color: rgba(61,220,142,.35); }
.pill.offline { color: #ffb3b3; border-color: rgba(255,107,107,.3); }
.pill.processing,
.pill.live { color: var(--gd32-hi); border-color: rgba(252,191,63,.35); }
@keyframes blink { 50% { opacity: .3; } }
.pill-val {
font-weight: 700; color: var(--text);
font-family: var(--font-mono);
}
/* ================= 主布局 ================= */
.main-layout {
display: grid;
grid-template-columns: 400px 1fr;
gap: var(--gap);
align-items: start;
}
/* ================= 卡片 ================= */
.card {
position: relative;
background: linear-gradient(170deg, var(--bg-card) 0%, rgba(15,23,38,.92) 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-card);
}
.card + .card { margin-top: var(--gap); }
/* 子系统色带:卡片顶部 2px 渐变,由修饰类决定颜色 */
.card::before {
content: "";
position: absolute; top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, var(--card-accent, var(--esp32)), transparent 85%);
opacity: .7; pointer-events: none;
}
.card--esp32 { --card-accent: var(--esp32); --card-accent-dim: var(--esp32-dim); }
.card--gd32 { --card-accent: var(--gd32); --card-accent-dim: var(--gd32-dim); }
.card--ble { --card-accent: var(--ble); --card-accent-dim: var(--ble-dim); }
.card--can { --card-accent: var(--can); --card-accent-dim: var(--can-dim); }
.card--motor { --card-accent: var(--motor); --card-accent-dim: var(--motor-dim); }
.card--mem { --card-accent: var(--mem); --card-accent-dim: var(--mem-dim); }
.card--status { --card-accent: var(--green); --card-accent-dim: var(--can-dim); }
.card-head {
display: flex; align-items: center; gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-size: 13px; font-weight: 600;
color: var(--text);
letter-spacing: .05em;
cursor: pointer;
user-select: none;
}
/* 图标芯片:圆角方块底色取自卡片子系统色 */
.card-head .ico {
width: 28px; height: 28px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
font-size: 15px;
border-radius: 8px;
background: var(--card-accent-dim, var(--esp32-dim));
border: 1px solid color-mix(in srgb, var(--card-accent, var(--esp32)) 30%, transparent);
}
.card-head-btn { margin-left: auto; padding: 4px 10px; position: relative; z-index: 2; }
.card-body { padding: 17px 18px; }
.card.collapsed .card-body { display: none; }
.card-head .collapse-arrow {
margin-left: auto; font-size: 10px; color: var(--text-label);
transition: transform .2s;
}
.card.collapsed .collapse-arrow { transform: rotate(-90deg); }
/* ================= 表单 ================= */
.form-row { margin-bottom: 14px; }
.form-row:last-child { margin-bottom: 0; }
.form-label {
display: block;
font-family: var(--font-display);
font-size: 11px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .12em;
margin-bottom: 7px;
}
.input, select, textarea {
width: 100%; padding: 10px 13px;
font-size: 13px; font-family: var(--font-mono);
background: var(--bg-input); color: var(--text);
border: 1px solid var(--border); border-radius: var(--radius-sm);
outline: none;
transition: border-color .18s, box-shadow .18s, background .18s;
}
.input::placeholder { color: var(--text-label); }
.input:focus, select:focus, textarea:focus {
border-color: var(--esp32);
background: color-mix(in srgb, var(--esp32) 5%, var(--bg-input));
box-shadow: 0 0 0 3px var(--esp32-dim);
}
select { cursor: pointer; }
select option { background: var(--bg-panel); color: var(--text); }
.input-group { display: flex; gap: 8px; }
.input-group .input { flex: 1; }
/* ================= 按钮 ================= */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
padding: 9px 17px; border: none; border-radius: var(--radius-sm);
font-family: var(--font-display);
font-size: 13px; font-weight: 600; letter-spacing: .03em;
cursor: pointer; white-space: nowrap;
transition: transform .16s, box-shadow .2s, background .2s, border-color .2s, color .2s, opacity .2s;
}
.btn:hover:not(:disabled) { transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0) scale(.97); }
.btn:disabled { opacity: .38; cursor: not-allowed; transform: none; }
/* 主操作 = ESP32 青绿 */
.btn-primary {
background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%);
color: #04231e;
box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 6px 22px rgba(47,216,193,.4), inset 0 1px 0 rgba(255,255,255,.3);
}
.btn-success {
background: linear-gradient(135deg, var(--green) 0%, #16a374 100%);
color: #03231a;
box-shadow: 0 4px 16px rgba(61,220,142,.25), inset 0 1px 0 rgba(255,255,255,.22);
}
.btn-danger {
background: linear-gradient(135deg, var(--red) 0%, #dc4c4c 100%);
color: #fff;
box-shadow: 0 4px 16px rgba(255,107,107,.25), inset 0 1px 0 rgba(255,255,255,.2);
}
.btn-outline {
background: rgba(255,255,255,.02); color: var(--text-dim);
border: 1px solid var(--border-strong);
}
.btn-outline:hover:not(:disabled) {
background: var(--bg-hover); color: var(--text);
border-color: #3d5a84;
}
.btn-ghost { background: transparent; color: var(--text-dim); }
.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); }
.btn-block { width: 100%; justify-content: center; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
/* ---- 多色子系统页签(关键)----
console-mode.js 用 btn-primary / btn-outline 表达"选中/未选中",
这里按按钮 id 把"选中态"染成各自子系统色:
WiFi=青绿 · BLE=蓝 · ESP32=青绿 · GD32=琥珀。 */
#modeBleBtn.btn-primary {
background: linear-gradient(135deg, var(--ble) 0%, #3b82f6 100%);
color: #04142b;
box-shadow: 0 4px 16px rgba(95,168,255,.3), inset 0 1px 0 rgba(255,255,255,.25);
}
#targetGd32Tab.btn-primary {
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
color: #2b1d02;
box-shadow: 0 4px 16px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
}
#targetEsp32Tab.btn-primary {
background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%);
color: #04231e;
}
/* GD32 OTA 大按钮 = 琥珀(区别于 ESP32 青绿) */
#uploadGd32Btn {
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
color: #2b1d02;
box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
}
#uploadEsp32Btn {
box-shadow: 0 6px 20px rgba(47,216,193,.32), inset 0 1px 0 rgba(255,255,255,.28);
}
/* ================= 连接状态条 / 提示 ================= */
.conn-indicator {
display: flex; align-items: center; gap: 10px;
padding: 12px 16px; border-radius: var(--radius-sm);
background: var(--bg-elevated); border: 1px solid var(--border);
font-size: 13px; font-weight: 500;
}
.conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.conn-indicator.ok .light { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); }
.conn-indicator.err .light { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); }
.conn-indicator.warn .light { background: var(--amber); }
.helper-note {
margin-top: 10px; padding: 10px 13px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-elevated);
font-size: 12px; line-height: 1.65; color: var(--text-dim);
}
.helper-note.ok { border-color: rgba(61,220,142,.3); background: rgba(61,220,142,.07); color: #a9f0c6; }
.helper-note.warn { border-color: rgba(252,191,63,.3); background: rgba(252,191,63,.07); color: #fbe3ae; }
.helper-note.err { border-color: rgba(255,107,107,.3); background: rgba(255,107,107,.07); color: #ffc4c4; }
.helper-note code { color: var(--esp32-hi); font-family: var(--font-mono); }
/* ================= 右侧滑出抽屉:堆与任务栈 ================= */
.sys-drawer {
--sys-drawer-panel-w: min(390px, calc(100vw - 48px));
--sys-drawer-peek: 38px;
position: fixed; top: 76px; right: 0; z-index: 80;
width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek));
height: calc(100vh - 92px); max-height: calc(100vh - 92px);
display: flex; flex-direction: row; align-items: stretch;
transform: translateX(var(--sys-drawer-panel-w));
transition: transform .3s cubic-bezier(.22, 1, .36, 1);
filter: drop-shadow(-8px 0 24px rgba(2,5,12,.5));
}
.sys-drawer:hover, .sys-drawer:focus-within { transform: translateX(0); }
.sys-drawer-rail {
flex: 0 0 var(--sys-drawer-peek); width: var(--sys-drawer-peek);
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 12px; padding: 14px 4px;
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-card) 100%);
border: 1px solid var(--border); border-right: none;
border-radius: var(--radius) 0 0 var(--radius);
cursor: default;
}
.sys-drawer-rail-ico { font-size: 16px; line-height: 1; }
.sys-drawer-rail-text {
writing-mode: vertical-rl; text-orientation: mixed;
font-family: var(--font-display);
font-size: 11px; font-weight: 600; letter-spacing: .18em;
color: var(--text-dim);
}
.sys-drawer:hover .sys-drawer-rail-text,
.sys-drawer:focus-within .sys-drawer-rail-text { color: var(--mem-hi); }
.sys-drawer-panel {
flex: 1 1 var(--sys-drawer-panel-w);
width: var(--sys-drawer-panel-w); min-width: 0; height: 100%;
margin: 0 !important;
display: flex; flex-direction: column;
border-radius: 0; overflow: hidden;
background: var(--bg-card);
border: 1px solid var(--border); border-left: none; border-right: none;
}
.sys-drawer-head { cursor: default; flex-shrink: 0; }
.sys-drawer-head .card-head-btn { margin-left: auto; }
.sys-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
+797
View File
@@ -0,0 +1,797 @@
/* motor.css - 电机控制台页面样式
* 由 motor.html 内联 <style>(原 L7-L675)机械外提,内容未改。
* 独立主题(seed tokens),不依赖 base.css/components.css。 */
/* ═══════════════════════ SEED TOKENS ═══════════════════════ */
:root {
--seed-bg: #090e15;
--seed-surface: #0f161f;
--seed-fg: #e2e9f2;
--seed-primary: #22d3ee;
--seed-accent: #f59e0b;
--seed-radius: 10px;
/* derived tokens */
--bg-root: var(--seed-bg);
--bg-panel: var(--seed-surface);
--bg-raised: color-mix(in srgb, var(--seed-surface) 82%, #ffffff 4%);
--fg: var(--seed-fg);
--fg-muted: color-mix(in srgb, var(--seed-fg) 55%, transparent);
--fg-dim: color-mix(in srgb, var(--seed-fg) 35%, transparent);
--border: color-mix(in srgb, var(--seed-fg) 10%, transparent);
--border-strong: color-mix(in srgb, var(--seed-fg) 18%, transparent);
--primary: var(--seed-primary);
--primary-dim: color-mix(in srgb, var(--seed-primary) 18%, transparent);
--primary-glow: color-mix(in srgb, var(--seed-primary) 30%, transparent);
--warn: var(--seed-accent);
--warn-dim: color-mix(in srgb, var(--seed-accent) 15%, transparent);
--ok: #34d399;
--ok-dim: color-mix(in srgb, #34d399 14%, transparent);
--danger: #f87171;
--danger-dim: color-mix(in srgb, #f87171 14%, transparent);
--radius: var(--seed-radius);
--radius-sm: calc(var(--seed-radius) * 0.6);
--radius-lg: calc(var(--seed-radius) * 1.4);
--font-body: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", system-ui, sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "SF Mono", "JetBrains Mono", Consolas, monospace;
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px;
--toolbar-h: 54px;
--statusbar-h: 42px;
}
/* ═══════════════════════ RESET & BASE ═══════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body {
font-family: var(--font-body);
background: var(--bg-root);
color: var(--fg);
line-height: 1.5;
height: 100vh;
overflow: hidden;
display: grid;
grid-template-rows: var(--toolbar-h) 1fr var(--statusbar-h);
}
/* ambient grid texture */
body::before {
content: "";
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image:
linear-gradient(color-mix(in srgb, var(--seed-primary) 3%, transparent) 1px, transparent 1px),
linear-gradient(90deg, color-mix(in srgb, var(--seed-primary) 3%, transparent) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, black 20%, transparent 80%);
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--fg-dim); }
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
input, select { font: inherit; color: inherit; }
label { display: block; }
/* ═══════════════════════ TOOLBAR ═══════════════════════ */
.toolbar {
grid-row: 1;
grid-column: 1;
position: relative; z-index: 10;
display: flex; align-items: center; gap: var(--sp-4);
padding: 0 var(--sp-5);
background: linear-gradient(90deg, color-mix(in srgb, var(--seed-primary) 7%, var(--bg-panel)), var(--bg-panel) 42%);
border-bottom: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
}
.toolbar-brand {
display: flex; align-items: center; gap: var(--sp-3);
font-weight: 600; font-size: 1.05rem; letter-spacing: -0.01em;
white-space: nowrap;
}
.toolbar-brand svg { flex-shrink: 0; }
.toolbar-brand .sub {
font-size: 0.72rem; font-weight: 400; color: var(--fg-muted);
letter-spacing: 0.08em; text-transform: uppercase;
font-family: var(--font-mono);
}
.toolbar-sep { width: 1px; height: 26px; background: var(--border-strong); flex-shrink: 0; }
.conn-group { display: flex; align-items: center; gap: var(--sp-2); }
.conn-ip-label {
font-family: var(--font-mono); font-size: 0.82rem;
color: var(--fg-muted); padding: 4px 8px;
background: var(--bg-root); border: 1px solid var(--border);
border-radius: var(--radius-sm); letter-spacing: 0.02em;
min-width: 7.5em;
}
.toolbar-right a.btn {
text-decoration: none; color: var(--fg-muted);
border-color: var(--border-strong);
}
.toolbar-right a.btn:hover { color: var(--primary); border-color: color-mix(in srgb, var(--seed-primary) 40%, transparent); }
.btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 14px; border-radius: var(--radius-sm);
font-size: 0.85rem; font-weight: 550; letter-spacing: 0.02em;
border: 1px solid var(--border-strong);
background: var(--bg-raised);
transition: background .15s, border-color .15s, transform .1s;
white-space: nowrap;
}
.btn:hover { background: color-mix(in srgb, var(--seed-surface) 60%, #ffffff 8%); }
.btn:active { transform: translateY(1px); }
.btn-primary {
background: color-mix(in srgb, var(--seed-primary) 16%, var(--seed-surface));
border-color: color-mix(in srgb, var(--seed-primary) 40%, transparent);
color: var(--primary);
}
.btn-primary:hover { background: color-mix(in srgb, var(--seed-primary) 24%, var(--seed-surface)); }
.btn-danger {
background: var(--danger-dim); border-color: color-mix(in srgb, var(--danger) 40%, transparent);
color: var(--danger);
}
.btn-sm { padding: 4px 10px; font-size: 0.78rem; }
.btn:disabled { opacity: .4; pointer-events: none; }
.led {
width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
background: var(--fg-dim); transition: background .3s, box-shadow .3s;
}
.led-on { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
.led-warn { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
.led-err { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
.led-pulse { animation: ledPulse 1.2s ease-in-out infinite; }
@keyframes ledPulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
.ch-select {
padding: 5px 8px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.82rem; outline: none;
}
.ch-select:focus { border-color: var(--primary); }
/* 轮询卡顿:通道下拉框变黄并呼吸发光(fixed-tick 下某拍未按时完成) */
.ch-select.stuck {
border-color: color-mix(in srgb, var(--warn) 65%, transparent);
animation: stuckPulse 1.2s ease-in-out infinite;
}
@keyframes stuckPulse {
0%, 100% { box-shadow: 0 0 2px color-mix(in srgb, var(--warn) 15%, transparent); }
50% { box-shadow: 0 0 10px color-mix(in srgb, var(--warn) 40%, transparent); }
}
/* 刷新速度输入框:与 .ch-select 同风格,宽度收窄 */
.refresh-input {
width: 64px; padding: 5px 6px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.82rem; outline: none;
text-align: center;
}
.refresh-input:focus { border-color: var(--primary); }
.refresh-input::-webkit-inner-spin-button { opacity: .5; }
.refresh-hint { font-size: .68rem; color: var(--fg-dim); font-family: var(--font-mono); letter-spacing: .04em; }
.toolbar-right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); }
.node-badge {
font-family: var(--font-mono); font-size: 0.8rem;
padding: 4px 12px; border-radius: 99px;
background: var(--primary-dim); color: var(--primary);
border: 1px solid color-mix(in srgb, var(--seed-primary) 30%, transparent);
letter-spacing: 0.02em;
}
/* ═══════════════════════ MAIN LAYOUT ═══════════════════════ */
.main-grid {
grid-row: 2;
grid-column: 1;
position: relative; z-index: 1;
display: grid;
grid-template-columns: minmax(240px, 290px) minmax(0, 0.9fr) minmax(420px, 1.1fr);
gap: var(--sp-3);
padding: var(--sp-3);
overflow: hidden;
min-height: 0;
}
.panel {
background: linear-gradient(145deg, color-mix(in srgb, var(--bg-panel) 94%, #ffffff 3%), var(--bg-panel) 52%);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
display: flex; flex-direction: column;
min-height: 0; overflow: hidden;
box-shadow: 0 10px 28px rgba(0, 0, 0, .16), inset 0 1px 0 rgba(255, 255, 255, .025);
}
.panel-head {
display: flex; align-items: center; gap: var(--sp-2);
padding: var(--sp-3) var(--sp-4);
background: linear-gradient(90deg, var(--primary-dim), transparent 48%);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.panel-title {
font-size: 0.78rem; font-weight: 600;
letter-spacing: 0.08em; text-transform: uppercase;
color: var(--fg-muted); font-family: var(--font-mono);
}
.panel-body { flex: 1; overflow-y: auto; min-height: 0; }
/* ═══════════════════════ LEFT: NODE LIST ═══════════════════════ */
.node-list { padding: var(--sp-2); display: flex; flex-direction: column; gap: var(--sp-2); }
.node-empty { padding: var(--sp-5); text-align: center; color: var(--fg-dim); font-size: .8rem; line-height: 1.7; }
.node-card {
position: relative;
padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--bg-root);
cursor: pointer;
transition: border-color .2s, background .2s, transform .12s;
}
.node-card:hover { border-color: var(--border-strong); transform: translateX(2px); }
.node-card.active {
border-color: color-mix(in srgb, var(--seed-primary) 55%, transparent);
background: color-mix(in srgb, var(--seed-primary) 6%, var(--bg-root));
}
.node-card.fault { border-color: color-mix(in srgb, var(--danger) 55%, transparent); }
.node-card.fault.active { border-color: var(--danger); }
.node-card.offline { opacity: .5; }
/* 独立 BOOT 徽章:总线上有电机在 Bootloader(0x103 心跳),不归属任何 Nx
* 注意:不要用内联 style="display:none",否则 .show 的 display:flex 盖不过。 */
.boot-badge {
display: none; align-items: center; gap: var(--sp-2);
margin-bottom: var(--sp-2); padding: var(--sp-2) var(--sp-3);
border: 1px solid color-mix(in srgb, var(--warn) 55%, transparent);
border-radius: var(--radius); background: color-mix(in srgb, var(--warn) 12%, transparent);
font-family: var(--font-mono); font-size: .72rem; color: var(--warn);
font-weight: 600; letter-spacing: 0.02em;
}
.boot-badge.show { display: flex; }
.boot-badge .boot-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--warn); box-shadow: 0 0 6px var(--warn);
animation: bootpulse 1s ease-in-out infinite;
}
.boot-badge .boot-sub { opacity: .75; font-weight: 400; margin-left: auto; }
@keyframes bootpulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
.nc-top { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.nc-id {
font-family: var(--font-mono); font-weight: 700; font-size: 1.05rem;
letter-spacing: -0.02em; min-width: 2.2em;
}
.nc-mode {
font-size: 0.7rem; padding: 2px 8px; border-radius: 99px;
background: var(--primary-dim); color: var(--primary);
font-weight: 550; letter-spacing: 0.02em;
}
.nc-mode.mode-off { background: color-mix(in srgb, var(--fg) 8%, transparent); color: var(--fg-muted); }
.nc-mode.mode-fault { background: var(--danger-dim); color: var(--danger); }
/* 节点在其他通道在线:橙色通道标签,提示切换顶部通道查看 */
.nc-mode.mode-other-ch { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.nc-status-text {
margin-left: auto; font-size: 0.68rem; color: var(--fg-muted);
font-family: var(--font-mono); max-width: 7.5em;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
letter-spacing: 0.01em;
}
.node-card.fault .nc-status-text { color: var(--danger); font-weight: 600; }
.nc-readings {
display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-1);
font-family: var(--font-mono); font-size: 0.78rem;
}
.nc-reading { display: flex; flex-direction: column; gap: 1px; }
.nc-reading .lbl { font-size: 0.62rem; color: var(--fg-dim); letter-spacing: 0.06em; text-transform: uppercase; }
.nc-reading .val { color: var(--fg); transition: color .3s; }
.nc-faults {
position: absolute; top: var(--sp-2); right: var(--sp-2);
display: flex; gap: 3px;
}
.fault-dot {
width: 7px; height: 7px; border-radius: 2px;
background: var(--danger); opacity: .9;
animation: ledPulse 1s ease-in-out infinite;
}
.fault-dot:nth-child(2) { animation-delay: .2s; }
.fault-dot:nth-child(3) { animation-delay: .4s; }
/* ═══════════════════════ CENTER: CONTROL ═══════════════════════ */
.center-panel { display: flex; flex-direction: column; min-height: 0; }
.cmd-section { padding: var(--sp-4); flex-shrink: 0; }
.cmd-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--sp-4);
}
.field { display: flex; flex-direction: column; gap: var(--sp-1); }
.field-label {
font-size: 0.72rem; font-weight: 550; color: var(--fg-muted);
letter-spacing: 0.06em; text-transform: uppercase; font-family: var(--font-mono);
}
.field-unit { color: var(--fg-dim); text-transform: none; letter-spacing: 0; }
.field select, .field input[type="number"], .field input[type="text"] {
padding: 7px 10px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.88rem; outline: none;
transition: border-color .2s;
width: 100%;
}
.field select:focus, .field input:focus { border-color: var(--primary); }
.mode-desc {
font-size: 0.75rem; color: var(--fg-dim); margin-top: 2px;
min-height: 1.2em;
}
.pid-group {
display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-3);
}
/* 快捷运行:一键写目标值并立即发送(目标字段随控制模式联动) */
.quick-run {
display: flex; align-items: center; gap: var(--sp-3);
margin-top: var(--sp-4); flex-wrap: wrap;
}
.qr-label {
font-size: 0.72rem; font-weight: 550; color: var(--fg-muted);
letter-spacing: 0.06em; text-transform: uppercase; font-family: var(--font-mono);
user-select: none;
}
.qr-target { color: var(--fg-dim); text-transform: none; letter-spacing: 0; }
.qr-btns { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.qr-btn {
min-width: 52px; padding: 6px 12px;
background: var(--bg-root); border: 1px solid var(--border-strong);
border-radius: var(--radius-sm); color: var(--fg);
font-family: var(--font-mono); font-size: 0.88rem; font-weight: 600;
cursor: pointer; transition: border-color .15s, color .15s, background .15s;
}
.qr-btn:hover { border-color: var(--primary); color: var(--primary); }
.qr-btn:active { transform: translateY(1px); }
.qr-btn:focus-visible { outline: none; border-color: var(--primary); }
.cmd-actions {
display: flex; gap: var(--sp-3); margin-top: var(--sp-4);
padding-top: var(--sp-3); border-top: 1px solid var(--border);
align-items: center; flex-wrap: wrap;
}
.send-hint { font-size: 0.72rem; color: var(--fg-dim); margin-left: auto; font-family: var(--font-mono); }
.target-hidden { display: none !important; }
.repeat-period {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--font-mono); font-size: 0.75rem; color: var(--fg-muted);
user-select: none;
}
.repeat-period input {
width: 4.6em; padding: 5px 6px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.8rem; color: var(--fg);
outline: none; text-align: right; transition: border-color .2s;
}
.repeat-period input:focus { border-color: var(--primary); }
/* tabs */
.tabs-bar {
display: flex; gap: 2px; padding: 0 var(--sp-4);
border-top: 1px solid var(--border); flex-shrink: 0;
background: color-mix(in srgb, var(--bg-root) 50%, var(--bg-panel));
}
.tab-btn {
padding: 9px 16px; font-size: 0.8rem; font-weight: 550;
color: var(--fg-muted); border-bottom: 2px solid transparent;
transition: color .2s, border-color .2s; letter-spacing: 0.02em;
}
.tab-btn:hover { color: var(--fg); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { flex: 1; overflow-y: auto; min-height: 0; padding: var(--sp-4); }
.tab-page { display: none; }
.tab-page.active { display: block; }
/* config tab */
.config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-3); }
.config-btn {
display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
padding: var(--sp-4); border-radius: var(--radius);
border: 1px solid var(--border-strong); background: var(--bg-root);
transition: border-color .2s, background .2s, transform .1s;
}
.config-btn:hover { border-color: var(--fg-dim); transform: translateY(-1px); }
.config-btn:active { transform: translateY(1px); }
.config-btn svg { opacity: .8; }
.config-btn .cb-name { font-size: 0.82rem; font-weight: 550; }
.config-btn .cb-sig { font-size: 0.68rem; color: var(--fg-dim); font-family: var(--font-mono); }
.config-btn.danger-action:hover { border-color: color-mix(in srgb, var(--danger) 50%, transparent); }
/* version tab */
.version-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-3); }
.ver-card {
padding: var(--sp-4); border-radius: var(--radius);
border: 1px solid var(--border); background: var(--bg-root); text-align: center;
}
.ver-card .ver-type { font-size: 0.68rem; color: var(--fg-muted); letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--font-mono); margin-bottom: var(--sp-2); }
.ver-card .ver-date { font-family: var(--font-mono); font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }
.ver-empty { color: var(--fg-dim); font-size: 0.85rem; }
/* node id tab */
.nodeid-row { display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap; }
.nodeid-result {
margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius-sm); background: var(--bg-root);
border: 1px solid var(--border); font-family: var(--font-mono); font-size: 0.85rem;
color: var(--fg-muted); min-height: 2.6em;
}
/* OTA tab */
.ota-stages {
display: flex; gap: 0; margin-bottom: var(--sp-4); overflow-x: auto;
padding-bottom: var(--sp-1);
}
.ota-stage {
display: flex; align-items: center; gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
font-size: 0.72rem; color: var(--fg-dim); white-space: nowrap;
position: relative;
}
.ota-stage::after {
content: ""; width: 20px; height: 1px; background: var(--border-strong);
margin-left: var(--sp-2); flex-shrink: 0;
}
.ota-stage:last-child::after { display: none; }
.ota-stage .st-dot {
width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
border: 1.5px solid var(--border-strong);
display: flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: 0.62rem;
transition: border-color .3s, background .3s, color .3s;
}
.ota-stage.done .st-dot { border-color: var(--ok); background: var(--ok-dim); color: var(--ok); }
.ota-stage.current .st-dot { border-color: var(--primary); background: var(--primary-dim); color: var(--primary); animation: ledPulse 1.4s infinite; }
.ota-stage.current { color: var(--fg); }
.ota-stage.done { color: var(--fg-muted); }
.ota-progress-wrap { margin-bottom: var(--sp-4); }
.ota-progress-bar {
height: 8px; border-radius: 4px; background: var(--bg-root);
border: 1px solid var(--border); overflow: hidden;
}
.ota-progress-fill {
height: 100%; width: 0%; border-radius: 4px;
background: linear-gradient(90deg, color-mix(in srgb, var(--seed-primary) 70%, var(--bg-root)), var(--primary));
transition: width .3s ease;
}
.ota-stats {
display: flex; gap: var(--sp-5); margin-top: var(--sp-2);
font-family: var(--font-mono); font-size: 0.78rem; color: var(--fg-muted);
flex-wrap: wrap;
}
.ota-stats .hl { color: var(--primary); }
.ota-file-row { display: flex; gap: var(--sp-3); align-items: center; margin-bottom: var(--sp-4); flex-wrap: wrap; }
.ota-file-name { font-family: var(--font-mono); font-size: 0.8rem; color: var(--fg-muted); }
.ota-log {
margin-top: var(--sp-3); padding: var(--sp-3);
background: var(--bg-root); border: 1px solid var(--border);
border-radius: var(--radius-sm); font-family: var(--font-mono);
font-size: 0.72rem; line-height: 1.7; color: var(--fg-muted);
max-height: 160px; overflow-y: auto;
}
.ota-log .log-ok { color: var(--ok); }
.ota-log .log-err { color: var(--danger); }
.ota-log .log-info { color: var(--primary); }
.ota-log-toolbar {
display: flex; justify-content: flex-end; gap: 6px;
margin-top: var(--sp-3); margin-bottom: -2px;
}
.ota-log-toolbar button {
font-size: 0.68rem; padding: 2px 8px;
border-radius: var(--radius-sm); border: 1px solid var(--border);
background: var(--bg-card); color: var(--fg-muted);
cursor: pointer; transition: background .15s, color .15s;
}
.ota-log-toolbar button:hover { background: rgba(255,255,255,.08); color: var(--fg); }
.ota-log-toolbar button.copied { background: rgba(34,197,94,.18); color: #86efac; }
/* ═══════════════════════ RIGHT: STATUS PANEL ═══════════════════════ */
.status-panel .panel-body { padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-4); }
.big-readings { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-3); }
.big-reading {
padding: var(--sp-3) var(--sp-3); text-align: center;
background: var(--bg-root); border: 1px solid var(--border);
border-radius: var(--radius);
transition: border-color .3s, background .2s;
cursor: pointer; position: relative;
}
.big-reading:hover { border-color: var(--border-strong); background: color-mix(in srgb, var(--seed-primary) 4%, var(--bg-root)); }
.big-reading.open { border-color: color-mix(in srgb, var(--seed-primary) 50%, transparent); }
.br-toggle {
font-size: 0.55rem; color: var(--fg-dim); letter-spacing: 0.06em;
opacity: 0; transition: opacity .2s, color .2s; margin-left: 4px;
}
.big-reading:hover .br-toggle { opacity: .8; }
.big-reading.open .br-toggle { opacity: 1; color: var(--primary); }
.br-scope {
max-height: 0; overflow: hidden; margin-top: 0;
transition: max-height .35s cubic-bezier(.4,0,.2,1), margin-top .35s;
}
.big-reading.open .br-scope { max-height: 96px; margin-top: var(--sp-2); }
.br-scope canvas {
width: 100%; height: 84px; display: block;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--seed-bg) 78%, #000000 14%);
border: 1px solid var(--border);
}
.big-reading .br-val {
font-family: var(--font-mono); font-size: clamp(1.5rem, 2vw, 2.4rem);
font-weight: 700; letter-spacing: -0.02em; line-height: 1.2;
transition: color .25s;
}
.big-reading .br-val.flash { color: var(--primary); }
.big-reading .br-unit { font-size: 0.65rem; color: var(--fg-dim); font-family: var(--font-mono); }
.big-reading .br-lbl {
font-size: 0.62rem; color: var(--fg-muted); margin-top: 2px;
letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--font-mono);
}
.big-reading .br-target {
font-size: 0.65rem; color: var(--fg-dim); font-family: var(--font-mono); margin-top: 1px;
}
.big-reading .br-target span { color: var(--primary); font-weight: 600; }
.big-reading .br-target span:empty::before { content: "--"; color: var(--fg-dim); }
.status-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.temp-block {
padding: var(--sp-3); background: var(--bg-root); border: 1px solid var(--border);
border-radius: var(--radius);
transition: border-color .3s, background .2s; cursor: pointer;
}
.temp-block:hover { border-color: var(--border-strong); background: color-mix(in srgb, var(--seed-primary) 4%, var(--bg-root)); }
.temp-block.open { border-color: color-mix(in srgb, var(--seed-primary) 50%, transparent); }
.temp-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--sp-2); }
.temp-name { font-size: 0.68rem; color: var(--fg-muted); letter-spacing: 0.06em; text-transform: uppercase; font-family: var(--font-mono); }
.temp-toggle {
font-size: 0.55rem; color: var(--fg-dim); letter-spacing: 0.06em;
opacity: 0; transition: opacity .2s, color .2s; margin-left: 6px;
}
.temp-block:hover .temp-toggle { opacity: .8; }
.temp-block.open .temp-toggle { opacity: 1; color: var(--primary); }
.temp-val { font-family: var(--font-mono); font-size: 0.95rem; font-weight: 600; }
.temp-bar { height: 5px; border-radius: 3px; background: var(--border); overflow: hidden; }
.temp-block .br-scope { max-height: 0; overflow: hidden; margin-top: 0; transition: max-height .35s cubic-bezier(.4,0,.2,1), margin-top .35s; }
.temp-block.open .br-scope { max-height: 80px; margin-top: var(--sp-2); }
.temp-block .br-scope canvas { height: 68px; }
.temp-fill {
height: 100%; border-radius: 3px; transition: width .6s ease, background .6s;
background: var(--ok);
}
.temp-fill.warm { background: var(--warn); }
.temp-fill.hot { background: var(--danger); }
.phase-currents { padding: var(--sp-3); background: var(--bg-root); border: 1px solid var(--border); border-radius: var(--radius); }
.pc-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); flex-wrap: wrap; }
.pc-title { font-size: 0.68rem; color: var(--fg-muted); letter-spacing: 0.06em; text-transform: uppercase; font-family: var(--font-mono); }
.pc-live-vals { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.pc-chip {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-muted);
padding: 2px 8px; border-radius: 99px;
background: color-mix(in srgb, var(--ch-color) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--ch-color) 30%, transparent);
}
.pc-chip b { color: var(--ch-color); font-weight: 600; min-width: 4.2em; text-align: right; }
.scope-wrap { position: relative; height: 172px; border-radius: var(--radius-sm); overflow: hidden; background: color-mix(in srgb, var(--seed-bg) 80%, #000000 12%); border: 1px solid var(--border); }
.scope-wrap canvas { width: 100%; height: 100%; display: block; }
.scope-grid-labels {
position: absolute; left: 6px; top: 0; bottom: 0;
display: flex; flex-direction: column; justify-content: space-between;
font-family: var(--font-mono); font-size: 0.58rem; color: var(--fg-dim);
padding: 4px 0; pointer-events: none;
}
.misc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.misc-item {
padding: var(--sp-3); background: var(--bg-root);
border: 1px solid var(--border); border-radius: var(--radius);
display: flex; flex-direction: column; gap: 2px;
}
.misc-item .mi-lbl { font-size: 0.62rem; color: var(--fg-muted); letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--font-mono); }
.misc-item .mi-val { font-family: var(--font-mono); font-size: 1rem; font-weight: 600; }
.flag-lights { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-2); padding: var(--sp-3); background: var(--bg-root); border: 1px solid var(--border); border-radius: var(--radius); }
.flag-item {
display: flex; align-items: center; gap: var(--sp-2);
font-size: 0.75rem; font-family: var(--font-mono); color: var(--fg-muted);
padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: color-mix(in srgb, var(--seed-surface) 60%, transparent);
transition: background .3s, border-color .3s, color .3s, box-shadow .3s;
}
.flag-item .flag-state { margin-left: auto; font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase; opacity: .7; }
.flag-item.is-ok { border-color: color-mix(in srgb, var(--ok) 25%, transparent); }
.flag-item.is-ok .flag-state { color: var(--ok); }
.flag-item.is-fault {
background: color-mix(in srgb, var(--danger) 16%, var(--seed-bg));
border-color: color-mix(in srgb, var(--danger) 65%, transparent);
color: var(--danger); font-weight: 700;
box-shadow: 0 0 12px color-mix(in srgb, var(--danger) 25%, transparent), inset 0 0 8px color-mix(in srgb, var(--danger) 8%, transparent);
animation: faultPulse 1.1s ease-in-out infinite;
}
.flag-item.is-fault .flag-state { color: var(--danger); opacity: 1; }
@keyframes faultPulse {
0%, 100% { box-shadow: 0 0 12px color-mix(in srgb, var(--danger) 30%, transparent), inset 0 0 8px color-mix(in srgb, var(--danger) 10%, transparent); }
50% { box-shadow: 0 0 22px color-mix(in srgb, var(--danger) 50%, transparent), inset 0 0 12px color-mix(in srgb, var(--danger) 16%, transparent); }
}
.flag-led { width: 9px; height: 9px; border-radius: 50%; background: var(--border-strong); transition: background .3s, box-shadow .3s; flex-shrink: 0; }
.flag-led.on-ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
.flag-led.on-err { background: var(--danger); box-shadow: 0 0 8px var(--danger); animation: ledPulse .8s infinite; }
.cpu-ring-wrap { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); background: var(--bg-root); border: 1px solid var(--border); border-radius: var(--radius); }
.cpu-ring { position: relative; width: 52px; height: 52px; flex-shrink: 0; }
.cpu-ring svg { transform: rotate(-90deg); }
.cpu-ring .ring-bg { fill: none; stroke: var(--border); stroke-width: 5; }
.cpu-ring .ring-fg { fill: none; stroke: var(--primary); stroke-width: 5; stroke-linecap: round; transition: stroke-dashoffset .6s ease; }
.cpu-pct {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: 0.72rem; font-weight: 700;
}
.cpu-info { display: flex; flex-direction: column; gap: 2px; }
.cpu-info .mi-lbl { font-size: 0.62rem; color: var(--fg-muted); letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--font-mono); }
.cpu-info .mi-sub { font-size: 0.72rem; color: var(--fg-dim); font-family: var(--font-mono); }
.offline-overlay {
display: none; flex-direction: column; align-items: center; justify-content: center;
gap: var(--sp-3); padding: var(--sp-6); color: var(--fg-dim); text-align: center;
}
.offline-overlay.show { display: flex; }
.offline-overlay .off-icon { opacity: .4; }
.status-live { display: flex; flex-direction: column; gap: var(--sp-4); }
.status-live.hide { display: none; }
/* ═══════════════════════ STATUS BAR ═══════════════════════ */
.statusbar {
grid-row: 3;
grid-column: 1;
position: relative; z-index: 10;
display: flex; align-items: center; gap: var(--sp-4);
padding: 0 var(--sp-5);
background: color-mix(in srgb, var(--bg-panel) 92%, #000000 8%);
border-top: 1px solid var(--border);
font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-muted);
overflow: hidden;
}
.sb-item { display: flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
.sb-item .sb-val { color: var(--fg); }
.sb-sep { width: 1px; height: 16px; background: var(--border); flex-shrink: 0; }
.sb-last-send { margin-left: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%; color: var(--fg-dim); }
/* ═══════════════════════ TOAST & MODAL ═══════════════════════ */
.toast-container {
position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; gap: var(--sp-2); z-index: 1000;
pointer-events: none;
}
.toast {
padding: 10px 20px; border-radius: var(--radius);
background: var(--bg-raised); border: 1px solid var(--border-strong);
font-size: 0.82rem; box-shadow: 0 8px 32px rgba(0,0,0,.5);
animation: toastIn .25s ease;
display: flex; align-items: center; gap: var(--sp-2);
max-width: 80vw;
}
.toast.ok { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.toast.err { border-color: color-mix(in srgb, var(--danger) 40%, transparent); }
.toast.warn { border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
.toast.out { animation: toastOut .2s ease forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(6px); } }
.modal-mask {
position: fixed; inset: 0; z-index: 900;
background: rgba(4, 8, 14, .75);
display: none; align-items: center; justify-content: center;
}
.modal-mask.show { display: flex; }
.modal-box {
background: var(--bg-panel); border: 1px solid var(--border-strong);
border-radius: var(--radius-lg); padding: var(--sp-6);
min-width: 320px; max-width: 420px;
box-shadow: 0 24px 64px rgba(0,0,0,.6);
animation: toastIn .2s ease;
}
.modal-title { font-size: 1rem; font-weight: 600; margin-bottom: var(--sp-2); }
.modal-desc { font-size: 0.82rem; color: var(--fg-muted); margin-bottom: var(--sp-5); line-height: 1.6; }
.modal-actions { display: flex; gap: var(--sp-3); justify-content: flex-end; }
/* disconnected banner */
.net-banner {
display: none; grid-row: 1; grid-column: 1;
align-self: end; transform: translateY(100%); z-index: 20;
align-items: center; gap: var(--sp-2);
padding: 4px var(--sp-5); font-size: .72rem; font-family: var(--font-mono);
background: linear-gradient(90deg, color-mix(in srgb, var(--danger) 20%, var(--bg-panel)), var(--danger-dim)); color: var(--danger);
border-bottom: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
box-shadow: 0 8px 20px rgba(0, 0, 0, .24);
}
.net-banner.show { display: flex; }
body:has(.net-banner.show) .main-grid { padding-top: calc(var(--sp-3) + 29px); }
/* ═══════════════════════ 命令序列 tab ═══════════════════════ */
.seq-toolbar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-4); }
.seq-status { font-family: var(--font-mono); font-size: 0.78rem; color: var(--fg-dim); margin-left: auto; white-space: nowrap; }
.seq-status.on { color: var(--primary); }
.seq-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.seq-empty {
padding: var(--sp-5); text-align: center; color: var(--fg-dim); font-size: 0.8rem;
border: 1px dashed var(--border-strong); border-radius: var(--radius); line-height: 1.7;
}
.seq-step {
padding: var(--sp-3); border-radius: var(--radius);
border: 1px solid var(--border); background: var(--bg-root);
transition: border-color .2s, background .2s;
}
.seq-step:hover { border-color: var(--border-strong); }
.seq-step.current {
border-color: color-mix(in srgb, var(--seed-primary) 55%, transparent);
background: color-mix(in srgb, var(--seed-primary) 5%, var(--bg-root));
}
.seq-locked .seq-step { opacity: .55; pointer-events: none; }
.seq-step-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.seq-idx { font-family: var(--font-mono); font-size: 0.78rem; font-weight: 600; color: var(--primary); min-width: 4.2em; }
.seq-num {
padding: 4px 8px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.78rem; outline: none; color: var(--fg);
transition: border-color .2s;
}
.seq-num:focus { border-color: var(--primary); }
/* 隐藏原生上下箭头(样式丑且尺寸不可控):数值直接键入,聚焦后可用键盘 ↑↓ 微调 */
.seq-num { -moz-appearance: textfield; appearance: textfield; }
.seq-num::-webkit-inner-spin-button,
.seq-num::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.seq-step-head .seq-num { width: 6.8em; text-align: right; }
.seq-step-head select {
padding: 4px 8px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.78rem; outline: none; color: var(--fg);
}
.seq-step-head select:focus { border-color: var(--primary); }
.seq-hold-label, .seq-hold-unit { font-size: 0.68rem; color: var(--fg-dim); font-family: var(--font-mono); }
.seq-ops { margin-left: auto; display: flex; gap: 2px; }
.seq-ops button {
width: 26px; height: 26px; border-radius: var(--radius-sm);
display: inline-flex; align-items: center; justify-content: center;
color: var(--fg-dim); background: transparent; border: none;
transition: background .15s, color .15s;
}
.seq-ops button svg { flex-shrink: 0; }
.seq-ops button:hover:not(:disabled) { background: var(--primary-dim); color: var(--primary); }
.seq-ops button.seq-del:hover:not(:disabled) { background: var(--danger-dim); color: var(--danger); }
.seq-ops button:disabled { opacity: .25; cursor: default; }
.seq-step-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: var(--sp-2); }
.seq-step-fields .field-label { font-size: 0.62rem; }
.seq-step-fields .seq-num { width: 100%; }
/* 动作组:一步内多个电机动作(近同时下发),动作行嵌套在步骤卡片内 */
.seq-count {
font-family: var(--font-mono); font-size: 0.68rem; color: var(--fg-dim);
padding: 2px 8px; border: 1px solid var(--border); border-radius: 999px; white-space: nowrap;
}
.seq-actions { display: flex; flex-direction: column; gap: var(--sp-2); }
.seq-action {
padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--bg-panel);
}
.seq-action-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.seq-action-tag { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 600; color: var(--fg-muted); min-width: 4.2em; }
.seq-action-head select {
padding: 4px 8px; background: var(--bg-root);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 0.78rem; outline: none; color: var(--fg);
}
.seq-action-head select:focus { border-color: var(--primary); }
.seq-hint { margin-top: var(--sp-4); font-size: 0.72rem; color: var(--fg-dim); line-height: 1.6; }
/* ═══════════════════════ RESPONSIVE ═══════════════════════ */
@media (max-width: 1365px) {
.main-grid {
grid-template-columns: 1fr minmax(380px, 1.1fr);
grid-template-rows: auto 1fr;
}
.nodes-panel {
grid-column: 1 / -1; grid-row: 1;
}
.nodes-panel .panel-body { overflow-x: auto; overflow-y: hidden; }
.node-list { flex-direction: row; min-width: max-content; }
.node-card { min-width: 200px; }
.center-panel { grid-column: 1; grid-row: 2; }
.status-panel { grid-column: 2; grid-row: 2; }
}
@media (max-width: 900px) {
.main-grid { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
.center-panel { grid-column: 1; grid-row: 2; }
.status-panel { grid-column: 1; grid-row: 3; max-height: 380px; }
.toolbar { flex-wrap: wrap; height: auto; padding: var(--sp-2) var(--sp-4); }
body { grid-template-rows: auto 1fr var(--statusbar-h); overflow: auto; }
}
+248
View File
@@ -0,0 +1,248 @@
/* ============================================================
responsive.css — 响应式断点 + 动效(对齐新设计令牌)
============================================================ */
/* ================= 断点 ================= */
@media (max-width: 1100px) {
.main-layout { grid-template-columns: 1fr; }
.sys-drawer {
--sys-drawer-panel-w: min(340px, calc(100vw - 40px));
--sys-drawer-peek: 32px;
top: 64px; height: calc(100vh - 72px); max-height: calc(100vh - 72px);
}
.can-monitor-drawer { --sys-drawer-panel-w: min(400px, calc(100vw - 40px)); }
.badge-grid { grid-template-columns: repeat(2, 1fr); }
.can-toolbar-grid, .can-toolbar-foot, .can-editor-layout { grid-template-columns: 1fr; }
.message-list { max-height: 420px; }
.can-meta-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 640px) {
.container { padding: 16px; }
.sys-drawer {
--sys-drawer-panel-w: calc(100vw - 36px);
--sys-drawer-peek: 28px;
top: 56px; height: calc(100vh - 60px); max-height: calc(100vh - 60px);
}
.sys-drawer-rail-text { font-size: 11px; }
.header-bar { flex-direction: column; align-items: flex-start; }
.header-sub { margin-left: 0; }
.badge-grid, .metric-grid { grid-template-columns: 1fr; }
.status-kv-grid, .status-task-meta { grid-template-columns: 1fr; }
.can-grid { grid-template-columns: 1fr 1fr; }
.btn-row { flex-direction: column; }
.can-meta-grid { grid-template-columns: 1fr; }
.can-launch-top, .can-launch-actions, .can-launch-foot {
grid-template-columns: 1fr; flex-direction: column; align-items: flex-start;
}
.can-selection-bar, .can-selection-actions { flex-direction: column; align-items: flex-start; }
.can-msg-toolbar { flex-wrap: wrap; }
.history-item-row {
grid-template-columns: auto 56px minmax(0, 1fr);
grid-template-rows: auto auto;
}
.history-item-hex, .history-item-foot { grid-column: 1 / -1; text-align: left; }
.can-title { font-size: 22px; }
.can-frame-hex { font-size: 15px; }
.message-table, .signal-table { min-width: 0; max-width: 100%; }
}
/* ================= 动画关键帧 =================
* 性能约定:常驻(infinite)动画只动 transform/opacity(合成器直通,
* 不走主线程重绘);box-shadow/background-position 关键帧仅限短暂、
* 临时态(如 dragover / OTA 阶段活跃)。 */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn {
0% { opacity: 0; transform: scale(.85); }
60% { opacity: 1; transform: scale(1.04); }
100% { opacity: 1; transform: scale(1); }
}
/* 光泽扫过:由 background-position 改为伪元素自身平移(合成器友好),
* 扫动节奏与原版一致:扫过 + 停预周期。 */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes ringPulse {
0% { box-shadow: 0 0 0 0 rgba(47,216,193,.5); }
70% { box-shadow: 0 0 0 10px rgba(47,216,193,0); }
100% { box-shadow: 0 0 0 0 rgba(47,216,193,0); }
}
/* 常驻波纹/呼吸辉光的合成器友好版:叠加层只动 transform/opacity。 */
@keyframes ringRipple {
0% { transform: scale(1); opacity: .5; }
70%,100% { transform: scale(3); opacity: 0; }
}
@keyframes glowBreathFade {
0%,100% { opacity: 0; }
50% { opacity: 1; }
}
@keyframes shake {
0%,100% { transform: translateX(0); }
20% { transform: translateX(-4px); } 40% { transform: translateX(4px); }
60% { transform: translateX(-3px); } 80% { transform: translateX(3px); }
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes floatY { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes flowDash { to { background-position: 200% 0; } }
@keyframes logoPulse {
0%,100% { opacity: 0; }
50% { opacity: 1; }
}
/* ================= 入场:头部 + 卡片错落浮现 ================= */
.header-bar { animation: fadeInUp .5s ease both; }
.main-layout > div > .card { animation: fadeInUp .55s ease both; }
.main-layout > div:nth-child(1) > .card:nth-child(1) { animation-delay: .06s; }
.main-layout > div:nth-child(1) > .card:nth-child(2) { animation-delay: .13s; }
.main-layout > div:nth-child(1) > .card:nth-child(3) { animation-delay: .20s; }
.main-layout > div:nth-child(1) > .card:nth-child(4) { animation-delay: .27s; }
.main-layout > div:nth-child(2) > .card:nth-child(1) { animation-delay: .09s; }
.main-layout > div:nth-child(2) > .card:nth-child(2) { animation-delay: .16s; }
.main-layout > div:nth-child(2) > .card:nth-child(3) { animation-delay: .23s; }
.main-layout > div:nth-child(2) > .card:nth-child(4) { animation-delay: .30s; }
.main-layout > div:nth-child(2) > .card:nth-child(5) { animation-delay: .37s; }
/* ================= 卡片悬停:抬升 + 子系统色描边 ================= */
.card { transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease; }
.card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--card-accent, var(--esp32)) 32%, var(--border));
box-shadow: 0 14px 34px rgba(2,5,12,.5),
0 0 0 1px color-mix(in srgb, var(--card-accent, var(--esp32)) 12%, transparent);
}
/* ================= 品牌 Logo:呼吸辉光 =================
* box-shadow 关键帧每帧走主线程重绘;改为叠加静态辉光的伪元素
* 只动 opacity(合成器直通),基础辉光由 layout.css 静态 box-shadow 提供,
* 呼吸节奏与原 logoPulse/ringPulse/glowBreath 一致。 */
.brand-logo { position: relative; }
.brand-logo::before {
content: "";
position: absolute; inset: -1px; border-radius: inherit;
box-shadow: 0 0 26px rgba(47,216,193,.4), inset 0 0 16px rgba(47,216,193,.14);
opacity: 0; pointer-events: none;
animation: logoPulse 3.2s ease-in-out infinite;
will-change: opacity;
}
.header-brand .dot-mark { position: relative; }
.header-brand .dot-mark::after {
content: "";
position: absolute; inset: 0; border-radius: 50%;
background: rgba(47,216,193,.5); pointer-events: none;
animation: ringRipple 2.4s ease-out infinite;
will-change: transform, opacity;
}
.header-brand .dot-mark::before {
content: "";
position: absolute; inset: 0; border-radius: 50%;
box-shadow: 0 0 20px rgba(47,216,193,.45);
opacity: 0; pointer-events: none;
animation: glowBreathFade 3.6s ease-in-out infinite;
will-change: opacity;
}
/* ================= 胶囊 / 按钮微交互 ================= */
.pill:hover { transform: translateY(-1px); border-color: var(--border-strong); }
.btn { position: relative; overflow: hidden; }
.btn-primary::before, .btn-success::before, .btn-danger::before {
content: "";
position: absolute; top: 0; left: -120%;
width: 60%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.4), transparent);
transform: skewX(-20deg);
transition: left .55s ease;
pointer-events: none;
}
.btn-primary:hover:not(:disabled)::before,
.btn-success:hover:not(:disabled)::before,
.btn-danger:hover:not(:disabled)::before { left: 130%; }
/* ================= 上传区:拖拽脉冲 + 图标浮动 ================= */
.upload-zone:hover .upload-ico { animation: floatY 1.6s ease-in-out infinite; }
.upload-zone.dragover { animation: ringPulse 1.2s ease-out infinite; }
/* ================= 文件信息浮现 ================= */
.file-info.show { animation: fadeInUp .3s ease both; }
/* ================= 进度条:流动斜纹 ================= */
.progress-fill {
background-image:
linear-gradient(90deg, var(--esp32), #14b8a6),
repeating-linear-gradient(45deg, rgba(255,255,255,.2) 0 10px, rgba(255,255,255,0) 10px 20px);
background-size: 100% 100%, 200% 100%;
background-blend-mode: normal, overlay;
animation: flowDash 1.4s linear infinite;
}
.progress-fill.done { animation: none; }
/* ================= 阶段胶囊:激活弹入 ================= */
.pp-step.active, .pp-step.done { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
/* ================= 阶段流:连接线生长 + 圆点动效 ================= */
.stage-node:not(:last-child)::after {
background: linear-gradient(90deg, var(--esp32) 0%, var(--esp32) 50%, var(--border) 50%, var(--border) 100%);
background-size: 200% 100%; background-position: 100% 0;
transition: background-position .5s ease;
}
.stage-node.done:not(:last-child)::after,
.stage-node.live:not(:last-child)::after { background-position: 0% 0; }
.stage-dot { transition: transform .3s ease, background .3s, border-color .3s, color .3s, box-shadow .3s; }
.stage-node.live .stage-dot { animation: ringPulse 1.6s ease-out infinite; }
.stage-node.done .stage-dot { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
.stage-node.err .stage-dot { animation: shake .4s ease both; }
/* ================= 状态条切换浮现 ================= */
.stage-status-bar.active, .stage-status-bar.ok, .stage-status-bar.err { animation: fadeInUp .3s ease both; }
.stage-result { animation: popIn .4s cubic-bezier(.4,1.4,.5,1) both; }
/* ================= 指标数值过渡 ================= */
.metric .v, .badge-stat .v { transition: color .3s ease, transform .3s ease; }
/* ================= 连接指示 ================= */
.conn-indicator { transition: background .25s ease, border-color .25s ease; }
.conn-indicator.warn .light { animation: ringPulse 1.4s ease-out infinite; }
/* 在线呼吸辉光:常驻动画改伪元素 opacity(合成器直通),节奏同原 glowBreath */
.conn-indicator.ok .light { position: relative; }
.conn-indicator.ok .light::after {
content: "";
position: absolute; inset: 0; border-radius: 50%;
box-shadow: 0 0 20px rgba(47,216,193,.45);
opacity: 0; pointer-events: none;
animation: glowBreathFade 3s ease-in-out infinite;
will-change: opacity;
}
/* ================= 堆 / 任务条:光泽扫过 =================
* shimmer 改动 transform(见上方关键帧注释):十几根条常驻扫光
* 不再每帧触发主线程重绘,视觉效果不变。 */
.heap-bar-track, .status-task-bar { position: relative; overflow: hidden; }
.heap-bar-track::after, .status-task-bar::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.12) 50%, transparent 100%);
transform: translateX(-100%);
animation: shimmer 2.8s linear infinite;
will-change: transform;
pointer-events: none;
}
.heap-bar-fill, .status-task-bar-fill { transition: width .6s cubic-bezier(.4,0,.2,1); }
/* ================= 日志新行淡入 ================= */
.log-line { animation: fadeIn .25s ease both; }
/* ================= Toast 弹出 ================= */
.toast.show { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
/* ================= 降低动态偏好 ================= */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}