feat(esp32): 添加多电机OTA中继功能并优化调试日志系统

- 新增多电机状态、版本和OTA中继监控模块(monitor/motor_state.cpp,
  monitor/motor_version.cpp, monitor/motor_ota_relay.cpp)
- 在CAN反馈任务中添加0xC5/0xC6/0xD2类型消息处理,支持多电机状态推送
- 重构调试日志系统,将一次性JSON分配改为512B栈缓冲流式发送,降低内存峰值
- 调试UART波特率从115200提升至30000
- 修改版本管理方式,从自动生version.h改为手动维护
- 更新VSCode设置以支持ESP-IDF开发环境配置
- 在OTA协调器中添加电机OTA互斥机制,防止并发冲突
- 完善中继状态机异常处理,添加远程取消通知机制
This commit is contained in:
小龙虾
2026-07-26 20:40:16 +08:00
parent 770ae14156
commit 2138a572ee
47 changed files with 6469 additions and 799 deletions
+8 -6
View File
@@ -36,6 +36,9 @@
--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);
@@ -57,13 +60,12 @@
--radius-lg: 16px;
/* ---- 字体 ----
display:Chakra Petch(棱角科技感,用于标题/大数字/按钮;
无中文字形,中文自动回退苹方/雅黑)
mono:JetBrains Mono(SHA / HEX / 日志 / 数值)
body:系统中文字体优先,保证正文可读性 */
--font-display: "Chakra Petch","PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
沿用原始系统字体方案:中文优先苹方/雅黑,西文 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: "JetBrains Mono","SF Mono","Cascadia Code","Consolas",monospace;
--font-mono: "SF Mono","Fira Code","Cascadia Code","Consolas",monospace;
/* ---- 间距 / 阴影 ---- */
--gap: 18px;
+70 -16
View File
@@ -5,9 +5,9 @@
}
.can-launch-card {
background:
radial-gradient(circle at top right, rgba(34,211,238,.18), transparent 28%),
radial-gradient(circle at top right, rgba(67,217,123,.16), transparent 28%),
linear-gradient(138deg, rgba(18,29,41,.96), rgba(20,29,40,.92) 48%, rgba(14,21,31,.98) 100%);
border: 1px solid rgba(34,211,238,.14);
border: 1px solid rgba(67,217,123,.16);
border-radius: 14px;
padding: 20px;
box-shadow:
@@ -21,7 +21,7 @@
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: linear-gradient(180deg, rgba(34,211,238,.9), rgba(34,211,238,.18));
background: linear-gradient(180deg, rgba(67,217,123,.9), rgba(67,217,123,.18));
}
.can-launch-copy {
position: relative;
@@ -46,7 +46,7 @@
max-width: 700px;
}
.can-launch-kicker {
color: #88ecff;
color: #8ef0b4;
font-size: 10px;
letter-spacing: .18em;
text-transform: uppercase;
@@ -57,9 +57,9 @@
align-items: center;
padding: 7px 12px;
border-radius: 999px;
background: rgba(34,211,238,.08);
border: 1px solid rgba(34,211,238,.18);
color: #a9f4ff;
background: rgba(67,217,123,.09);
border: 1px solid rgba(67,217,123,.22);
color: #a9f5c8;
font-size: 11px;
font-weight: 700;
letter-spacing: .12em;
@@ -88,8 +88,8 @@
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px rgba(34,211,238,.35);
background: var(--can);
box-shadow: 0 0 10px rgba(67,217,123,.35);
}
.can-launch-actions {
display: grid;
@@ -104,7 +104,9 @@
min-height: 42px;
}
.can-launch-primary {
box-shadow: 0 10px 24px rgba(34,211,238,.18);
background: linear-gradient(135deg, var(--can) 0%, #22b45f 100%);
color: #04240f;
box-shadow: 0 10px 24px rgba(67,217,123,.2);
}
.can-launch-secondary {
color: #d5e4f4;
@@ -113,7 +115,7 @@
}
.can-launch-secondary:hover:not(:disabled) {
background: rgba(255,255,255,.05);
border-color: rgba(34,211,238,.18);
border-color: rgba(67,217,123,.25);
}
.can-launch-foot {
display: flex;
@@ -128,6 +130,35 @@
position: relative;
z-index: 1;
}
/* 电机入口:复用 can-launch 布局,换青色强调 */
.motor-launch-card {
background:
radial-gradient(circle at top right, rgba(34,211,238,.16), transparent 28%),
linear-gradient(138deg, rgba(18,29,41,.96), rgba(20,29,40,.92) 48%, rgba(14,21,31,.98) 100%);
border-color: rgba(34,211,238,.16);
}
.motor-launch-card::before {
background: linear-gradient(180deg, rgba(34,211,238,.9), rgba(34,211,238,.18));
}
.motor-launch-card .can-launch-kicker { color: #67e8f9; }
.motor-launch-card .can-launch-badge {
background: rgba(34,211,238,.09);
border-color: rgba(34,211,238,.22);
color: #a5f3fc;
}
.motor-launch-card .can-launch-point::before {
background: var(--motor, #22d3ee);
box-shadow: 0 0 10px rgba(34,211,238,.35);
}
.motor-launch-primary {
background: linear-gradient(135deg, var(--motor, #22d3ee) 0%, #0891b2 100%) !important;
color: #042f2e !important;
box-shadow: 0 10px 24px rgba(34,211,238,.2);
}
.motor-launch-card .can-launch-secondary:hover:not(:disabled) {
border-color: rgba(34,211,238,.25);
}
.can-launch-foot-label {
flex: none;
display: inline-flex;
@@ -160,7 +191,6 @@ body.can-page {
.can-container {
max-width: min(1700px, 100%);
width: 100%;
overflow-x: hidden;
box-sizing: border-box;
}
.can-header {
@@ -170,6 +200,9 @@ body.can-page {
backdrop-filter: blur(14px);
background: rgba(15,22,31,.86);
}
/* sticky+backdrop-filter 合成层冲突致 fadeInUp 卡首帧(translateY(10px)+opacity:0),
* 禁用入场动画。选择器需 .header-bar.can-header 以胜过 responsive.css 的 .header-bar 规则 */
.header-bar.can-header { animation: none; }
.can-header-actions {
margin-left: auto;
}
@@ -942,9 +975,9 @@ body.can-page {
grid-template-columns:
auto
56px
minmax(0, 0.9fr)
minmax(0, 1.4fr)
minmax(0, 0.8fr);
minmax(0, 0.7fr)
minmax(0, 0.8fr)
minmax(0, 1.6fr);
align-items: center;
gap: 8px 10px;
min-width: 0;
@@ -1008,6 +1041,24 @@ body.can-page {
min-width: 0;
text-align: right;
}
/* Click-to-Copy 样式 */
.history-item-row .copyable {
cursor: pointer;
border-radius: 4px;
padding: 1px 4px;
margin: -1px -4px;
transition: background .15s, color .15s;
}
.history-item-row .copyable:hover {
background: rgba(255,255,255,.08);
}
.history-item-row .copyable:active {
background: rgba(255,255,255,.14);
}
.history-item-row .copyable.copied {
background: rgba(34,197,94,.18);
color: #86efac;
}
/* === CAN 1Hz FEEDBACK === */
.can-feedback-card { margin-top: 18px; }
@@ -1095,7 +1146,10 @@ body.can-page {
}
.feedback-row:last-child { border-bottom: none; }
.fb-label { color: var(--muted, #8892a4); }
.fb-val { font-weight: 600; font-variant-numeric: tabular-nums; }
.fb-val { font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer; border-radius: 4px; padding: 1px 4px; margin: -1px -4px; transition: background .15s; }
.fb-val:hover { background: rgba(255,255,255,.08); }
.fb-val:active { background: rgba(255,255,255,.14); }
.fb-val.copied { background: rgba(34,197,94,.18); color: #86efac; }
.fb-val.mono { font-family: monospace; }
.fb-val.ok { color: #34d399; }
.fb-val.warn { color: #f59e0b; }
+7 -6
View File
@@ -136,14 +136,15 @@
box-shadow: 0 8px 20px rgba(3,7,16,.4);
}
.metric .k {
font-family: var(--font-display);
font-size: 10px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .13em; margin-bottom: 5px;
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-display);
font-size: 21px; font-weight: 700; word-break: break-word;
font-variant-numeric: tabular-nums; letter-spacing: .01em;
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); }
+6 -11
View File
@@ -5,14 +5,16 @@
/* ================= HEADER — 命令栏 ================= */
.header-bar {
position: relative;
z-index: 20; /* 高于下方卡片,保证 pill tooltip 不被遮挡 */
display: flex; align-items: center; gap: 16px;
padding: 16px 22px; margin-bottom: 26px;
background: linear-gradient(160deg, var(--bg-panel) 0%, var(--bg-card) 70%);
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;
overflow: hidden;
}
/* 顶部三色霓虹描边:ESP32 青绿 → BLE 蓝 → GD32 琥珀,一条细线点出子系统谱系 */
.header-bar::before {
@@ -21,15 +23,7 @@
background: linear-gradient(90deg,
var(--esp32) 0%, var(--ble) 38%, var(--gd32) 78%, transparent 100%);
opacity: .85;
}
/* 右上角极淡的芯片电路装饰 */
.header-bar::after {
content: "";
position: absolute; right: -40px; top: -40px;
width: 220px; height: 220px; pointer-events: none;
background:
radial-gradient(circle at 30% 30%, rgba(47,216,193,.10), transparent 55%);
border-radius: 50%;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.header-brand {
@@ -139,6 +133,7 @@
.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); }
+139 -172
View File
@@ -1,24 +1,19 @@
/* === responsive.css — 响应式断点 + 增强动画 === */
/* === RESPONSIVE === */
/* ============================================================
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));
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-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) {
@@ -26,254 +21,226 @@
.sys-drawer {
--sys-drawer-panel-w: calc(100vw - 36px);
--sys-drawer-peek: 28px;
top: 56px;
height: calc(100vh - 60px);
max-height: calc(100vh - 60px);
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;
.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;
}
/* 禁止再强制 980px 最小宽,避免报文/信号表横向滑动 */
.message-table,
.signal-table {
min-width: 0;
max-width: 100%;
}
.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%; }
}
/* ===== ENHANCED ANIMATIONS (v20260625b) ===== */
/* Global keyframes */
/* ================= 动画关键帧 =================
* 性能约定:常驻(infinite)动画只动 transform/opacity(合成器直通,
* 不走主线程重绘);box-shadow/background-position 关键帧仅限短暂、
* 临时态(如 dragover / OTA 阶段活跃)。 */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@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% { background-position: -200% 0; }
100% { background-position: 200% 0; }
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes ringPulse {
0% { box-shadow: 0 0 0 0 rgba(34,211,238,.55); }
70% { box-shadow: 0 0 0 10px rgba(34,211,238,0); }
100% { box-shadow: 0 0 0 0 rgba(34,211,238,0); }
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); }
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 glowBreath {
0%, 100% { box-shadow: 0 0 8px var(--accent-dim); }
50% { box-shadow: 0 0 18px rgba(34,211,238,.45); }
}
@keyframes flowDash {
to { background-position: 200% 0; }
@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; }
}
/* Page entrance — header + cards staggered */
.header-bar { animation: fadeInUp .45s ease both; }
.main-layout > div > .card { animation: fadeInUp .5s ease both; }
.main-layout > div:nth-child(1) > .card:nth-child(1) { animation-delay: .05s; }
.main-layout > div:nth-child(1) > .card:nth-child(2) { animation-delay: .12s; }
.main-layout > div:nth-child(1) > .card:nth-child(3) { animation-delay: .19s; }
.main-layout > div:nth-child(1) > .card:nth-child(4) { animation-delay: .26s; }
.main-layout > div:nth-child(2) > .card:nth-child(1) { animation-delay: .08s; }
.main-layout > div:nth-child(2) > .card:nth-child(2) { animation-delay: .15s; }
.main-layout > div:nth-child(2) > .card:nth-child(3) { animation-delay: .22s; }
.main-layout > div:nth-child(2) > .card:nth-child(4) { animation-delay: .29s; }
/* ================= 入场:头部 + 卡片错落浮现 ================= */
.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 hover — lift + glow */
.card {
transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease;
}
/* ================= 卡片悬停:抬升 + 子系统色描边 ================= */
.card { transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease; }
.card:hover {
transform: translateY(-2px);
border-color: rgba(34,211,238,.28);
box-shadow: 0 10px 28px rgba(0,0,0,.35), 0 0 0 1px rgba(34,211,238,.08);
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);
}
/* Brand dot — gentle ring pulse */
.header-brand .dot-mark {
animation: ringPulse 2.2s ease-out infinite, glowBreath 3.6s ease-in-out infinite;
/* ================= 品牌 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;
}
/* Pills — subtle hover */
.pill { transition: transform .2s ease, border-color .2s ease, background .2s ease; }
.pill:hover { transform: translateY(-1px); border-color: rgba(34,211,238,.3); }
/* ================= 胶囊 / 按钮微交互 ================= */
.pill:hover { transform: translateY(-1px); border-color: var(--border-strong); }
/* Button — press + sheen on hover for primary */
.btn { position: relative; overflow: hidden; }
.btn:active:not(:disabled) { transform: translateY(0) scale(.97); transition: transform .08s ease; }
.btn-primary::before,
.btn-success::before,
.btn-danger::before {
.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,.35), transparent);
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%;
}
.btn-danger:hover:not(:disabled)::before { left: 130%; }
/* Upload zone — drag pulse + icon float */
.upload-zone { transition: border-color .2s, background .2s, transform .2s; }
.upload-zone:hover { transform: translateY(-1px); }
.upload-zone .upload-ico {
display: inline-block;
transition: transform .25s ease;
}
/* ================= 上传区:拖拽脉冲 + 图标浮动 ================= */
.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 appearance */
/* ================= 文件信息浮现 ================= */
.file-info.show { animation: fadeInUp .3s ease both; }
/* Progress bar — striped flow on active */
/* ================= 进度条:流动斜纹 ================= */
.progress-fill {
background-image:
linear-gradient(90deg, #22d3ee, #06b6d4),
repeating-linear-gradient(45deg,
rgba(255,255,255,.18) 0 10px,
rgba(255,255,255,0) 10px 20px);
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; }
/* Phase step capsule — pop when becoming active/done */
.pp-step.active,
.pp-step.done { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
/* ================= 阶段胶囊:激活弹入 ================= */
.pp-step.active, .pp-step.done { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
/* Stage flow — connector grow + dot pop */
/* ================= 阶段流:连接线生长 + 圆点动效 ================= */
.stage-node:not(:last-child)::after {
background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 50%, var(--border) 50%, var(--border) 100%);
background-size: 200% 100%;
background-position: 100% 0;
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-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.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 — slide in when state changes */
.stage-status-bar.active,
.stage-status-bar.ok,
.stage-status-bar.err { animation: fadeInUp .3s ease both; }
/* Stage result panel */
/* ================= 状态条切换浮现 ================= */
.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 / badge values — number tick highlight */
.metric .v,
.badge-stat .v {
transition: color .3s ease, transform .3s ease;
}
.metric:hover,
.badge-stat:hover {
border-color: rgba(34,211,238,.25);
transition: border-color .25s ease;
}
/* ================= 指标数值过渡 ================= */
.metric .v, .badge-stat .v { transition: color .3s ease, transform .3s ease; }
/* Conn indicator — light ring pulse when warn */
/* ================= 连接指示 ================= */
.conn-indicator { transition: background .25s ease, border-color .25s ease; }
.conn-indicator.warn .light { animation: ringPulse 1.4s ease-out infinite; }
.conn-indicator.ok .light { animation: glowBreath 3s ease-in-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;
}
/* Heap / task bars — sheen */
.heap-bar-track,
.status-task-bar { position: relative; overflow: hidden; }
.heap-bar-track::after,
.status-task-bar::after {
/* ================= 堆 / 任务条:光泽扫过 =================
* 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.6s linear infinite;
background-size: 200% 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); }
.heap-bar-fill, .status-task-bar-fill { transition: width .6s cubic-bezier(.4,0,.2,1); }
/* Log lines — fresh entry fade-in */
/* ================= 日志新行淡入 ================= */
.log-line { animation: fadeIn .25s ease both; }
/* Toast — pop and float */
/* ================= Toast 弹出 ================= */
.toast.show { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
/* Reduced motion — respect user preference */
/* ================= 降低动态偏好 ================= */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;