Compare commits

..
Author SHA1 Message Date
be647bbaba feat(HEL-529): 数据中枢后台改造为「第七版·轨道机芯」全动效单页
按已通过样图(HEL-527,白栖知“按照这个试试吧”)重做 xiaobai-datahub
管理后台(8766/admin/),仅改 admin/index.html、admin/styles.css、
admin/app.js 三个文件,零新依赖、不改构建流程、不触碰 datahub/ 后端、
Docker/Compose、8765 主站与冻结区“问天”。

核心实现:
- 单个 76vh sticky 空间舞台(#stageWrap,position: sticky 钉在视口),
  #track 六段 130vh 透明占位撑高文档、驱动滚动进度,舞台本体在滚动期间
  保持不动,直到六幕滚完才随之离场。
- 一枚持续旋转的 3D 数据机芯(canvas 2D 手工透视投影,无 WebGL/新依赖),
  六幕分别对应总览/数据源/调度任务/盘后发布/数据集/审计,切换幕时机芯
  换面/爆炸展开/合拢,4 条数据流通道持续汇入机芯。
- A2 路由式 LINK/ACT 双灯:LINK 是稳态真实连通性(取自 /admin/api/sources
  健康探测结果,仅 tushare/eastmoney/tencent/ifind 四路可流动,ths/xgb/
  akshare 属永久预留源,不参与流动动画);ACT 严格由真实事件驱动——
  tushare 靠 recent_calls 增量 diff,其余三源靠健康探测“探测动作本身
  就是一次真实网络调用”,手动“探测一次”按钮同样触发真实后端请求
  (已用真实浏览器验证会打到 /admin/api/sources/<provider>/probe)。
  同源事件簇最多闪 3 次,全站闪烁令牌桶限流 ≤3 簇/秒,各源不共享时钟。
- 动效令牌统一:进出用强 ease-out,屏内位移用强 ease-in-out,持续流动用
  linear;全文件禁止 ease-in、禁止 transition: all。
- prefers-reduced-motion:静态六幕面板(StaticShell)与动效版共用同一套
  cabin/detail/bind 渲染函数,信息与交互完全对等,媒体查询变化时可不
  刷新页面实时切换;系统级 CSS 兜底同样生效。
- 页面隐藏 / 断网即暂停所有轮询与 rAF 循环、清空未播闪烁队列,恢复时
  只静默重建基线、不补播错过的事件。
- 保留原有登录/改密/登出、数据源探测、任务重跑、盘后发布二次确认
  (密码+确认词)、回滚/补数等全部后端接口调用与危险操作确认流程。

自测(均在本地临时环境完成,未连接生产库/生产网络):
- `python -m unittest discover -s xiaobai-datahub/tests -v`:133 项全过。
- `node --check xiaobai-datahub/admin/app.js`:语法通过。
- `git diff --check`:无空白/换行问题;`git status`:仅上述 3 个文件改动。
- 起本地 datahub 服务 + Playwright 真实无头浏览器,22 项端到端断言全过:
  画面渲染、六幕滚动到底/导航跳转、日夜切换、机芯点击开合详情、后台
  切换(RAF 真停)、断网/恢复、reduced-motion 实时切换、探测按钮触发
  真实后端调用等。过程中定位并修复两处真实缺陷:
  1) #stageWrap 原为 position: relative,未真正钉住舞台,滚动时机芯会
     随页面滚走——已改为 sticky,现验证滚动任意距离机芯位置不变。
  2) 点击机芯打开详情硬编码成“数据源”,已改为按当前所在幕动态选择。
  另外补上了此前遗漏的 #phase 幕序指示(如“3 / 6”),静态版切幕同步
  更新顶部 crumb/phase。

未覆盖:未在 1440/1280/1024 三档做像素级视觉走查(仅验证 1024 无横向
溢出),未做真实弱网/高延迟环境下的手动观察,只做了断网模拟。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-12 23:35:51 +08:00
4a90c32fcc feat(HEL-524): 板块轮动落地 B 五彩版,侧栏/品牌去独立底色
- 新增 5 档强度色 token(日/夜两套,含渐变填充与光晕 alpha),tokens.css
- 板块轮动卡片改为 heat-tier-1..5,固定阈值 <65/65-74/75-84/85-94/95+
  映射青/蓝/紫/橙/红,9 个日期列共用同一套映射,与涨跌无关
- 悬停/选中/追踪三态用 --tier-* 局部变量统一驱动,选中态(当次点击格)
  用渐变实色+2px 亮边+光晕,追踪态(其余同名格)保留档位底色+2px 描边
  +圆点标记,不新增业务状态;修复 hover 规则与 selected 同优先级导致
  悬停会覆盖选中/追踪样式的层叠顺序问题(:hover 增加 :not(.selected))
- 图例改为 5 档 + “颜色=强度档位,与涨跌无关” 说明,禁止对普通卡使用绿色
- 侧栏/品牌区去掉独立色块:.module-nav 融入页面 --canvas,
  品牌区与顶栏同层(--header-bg),收起侧栏整行透明;仅作用于
  非移动端且非「问天」路由,问天页面零改动
- 清理死代码:未生效的 .selected 旧规则与引用未定义变量的
  --rotation-heat 声明
- 更新 e2e 断言以匹配新 tier 类名;重新生成 architecture-inventory.json

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-10 21:10:14 +08:00
5385bad9ec fix(HEL-519): 问师页面补齐标题,消除双重顶部留白
- 取消 .mentor-page-header 的 display:none,按 .redesigned-page-head 的
  上下间距规则(min-height 34px + margin-bottom 12px)显示已有的"问师"标题
  与数据日期副标题,颜色/字号复用既有 qp-text-1/qp-text-3 令牌,日夜主题均生效
- 桌面态取消 workspace-view 顶部多加的 40px 占位留白(改回 var(--page-pad-y)),
  避免留白+标题叠加造成双重间距;总顶部空间(14+34+12=60px)与其他已统一页面
  (workspace-view padding + redesigned-page-head)完全一致
- 767px 及以下窄视口保持原有隐藏标题+40px留白行为不变,不引入未审视觉改动
- 未改动联系人列表/搜索/对话区/输入框/快捷按钮/底部栏/数据交互,问天板块未触碰

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-10 11:29:45 +08:00
955785b9fd fix(HEL-509): 四处已通过界面限域修补
- 情绪周期: 移除暗色模式下标题栏多余的背景色块,与其他页面一致
- 智能选股: 阶段选股/策略选股/自定义选股改为3个独立圆角按钮(复用control-surface/radius-md等token);修复策略选股面板模块零间距穿模问题
- 问师: 顶部补齐与其他页面一致的留白,联系人列表/搜索/聊天区/输入区结构不变
- 系统配置: 复核后确认已在早期B-147阶段完成token化统一,未发现需改动项

同步重建 config/architecture-inventory.json 清单。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-10 10:19:49 +08:00
13 changed files with 1843 additions and 356 deletions
+9 -9
View File
@@ -458,8 +458,8 @@
},
{
"path": "frontend/pages/screener/foundation.css",
"bytes": 103547,
"lines": 6576
"bytes": 103683,
"lines": 6582
},
{
"path": "frontend/pages/heaven/page.js",
@@ -468,8 +468,8 @@
},
{
"path": "frontend/shared/shell.css",
"bytes": 63733,
"lines": 3767
"bytes": 64839,
"lines": 3794
},
{
"path": "backend/features/heaven/engine.py",
@@ -766,16 +766,16 @@
"bytes": 3316,
"lines": 55
},
{
"path": "frontend/pages/rotation/page.html",
"bytes": 3299,
"lines": 57
},
{
"path": "backend/features/market/insights_context.py",
"bytes": 3175,
"lines": 84
},
{
"path": "frontend/pages/rotation/page.html",
"bytes": 3031,
"lines": 54
},
{
"path": "frontend/pages/market/bindings.js",
"bytes": 2663,
+10 -1
View File
@@ -100,7 +100,12 @@ body[data-active-view="mentorView"] .app-page-context span {
}
#mentorView .mentor-page-header {
display: none;
flex: 0 0 auto;
display: flex;
align-items: center;
min-height: 34px;
margin: 0 0 12px;
padding: 0 16px;
}
#mentorView .mentor-page-title {
@@ -1360,6 +1365,10 @@ body[data-active-view="mentorView"] .app-page-context span {
}
@media (max-width: 767px) {
#mentorView.workspace-view {
padding: calc(var(--page-pad-y) + 40px) 0 0;
}
#mentorView .mentor-layout {
grid-template-columns: minmax(0, 1fr);
overflow: visible;
+137 -79
View File
@@ -28,14 +28,6 @@
background: var(--selected);
}
.rotation-sector-chip.selected {
border-color: var(--action);
background: var(--action-soft);
color: var(--action-hover);
}
.rotation-sector-chip > span {
color: var(--text-secondary);
@@ -158,8 +150,6 @@
cursor: pointer;
border-bottom: 1px dashed var(--border);
background: color-mix(in srgb, var(--accent) calc(4% + var(--rotation-heat) * 46%), transparent);
}
.redesigned-rotation-view {
@@ -397,8 +387,12 @@
display: flex;
flex-wrap: wrap;
align-items: center;
row-gap: 4px;
gap: 14px;
padding: 8px 14px;
@@ -434,18 +428,32 @@
border-radius: 2px;
}
#rotationView .rotation-swatch.strong {
background: var(--accent);
#rotationView .rotation-swatch.tier-1 {
background: var(--rotation-tier1-bar);
}
#rotationView .rotation-swatch.warm {
background: var(--accent-soft);
#rotationView .rotation-swatch.tier-2 {
background: var(--rotation-tier2-bar);
}
#rotationView .rotation-swatch.mild {
background: var(--surface-muted);
#rotationView .rotation-swatch.tier-3 {
background: var(--rotation-tier3-bar);
}
border: 1px solid var(--border);
#rotationView .rotation-swatch.tier-4 {
background: var(--rotation-tier4-bar);
}
#rotationView .rotation-swatch.tier-5 {
background: var(--rotation-tier5-bar);
}
#rotationView .rotation-legend-note {
margin-left: auto;
color: var(--r2-faint);
white-space: nowrap;
}
#rotationView .rotation-tracker {
@@ -818,10 +826,59 @@
#rotationView .rotation-history.tracking .rotation-sector-chip.selected {
opacity: 1;
}
border-color: var(--r2-blue);
/* 追踪态:命中被追踪板块名、但不是当次点击的那一格 —— 保持档位底色,2px 同档位描边(含 80% 不透明度),
连点标记不做动画,仅描边淡入 200ms */
#rotationView .rotation-sector-chip.selected:not(.current-cell) {
border-width: 2px;
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px var(--accent-soft);
border-color: color-mix(in srgb, var(--tier-bar) 80%, transparent);
transition: border-color 200ms var(--ease-out);
}
/* 选中态:仅当次点击的日期+板块这一格 —— 同档位实色渐变 + 2px 亮描边 + 克制光晕,全页最多一张 */
#rotationView .rotation-sector-chip.selected.current-cell {
border-width: 2px;
border-color: var(--tier-text);
background: linear-gradient(135deg, var(--tier-fill-start), var(--tier-fill-end));
box-shadow: 0 0 16px color-mix(in srgb, var(--tier-bar) var(--rotation-tier-glow-alpha), transparent);
color: var(--text-inverse);
transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out), box-shadow 160ms var(--ease-out);
}
#rotationView .rotation-sector-chip.selected.current-cell strong,
#rotationView .rotation-sector-chip.selected.current-cell small,
#rotationView .rotation-sector-chip.selected.current-cell small b {
color: var(--text-inverse);
}
#rotationView .rotation-track-dot {
width: 6px;
height: 6px;
position: absolute;
top: 6px;
right: 6px;
border-radius: 50%;
background: var(--tier-bar);
}
@media (prefers-reduced-motion: reduce) {
#rotationView .rotation-sector-chip.selected.current-cell {
box-shadow: none;
}
}
.rotation-cell-tooltip {
@@ -1050,40 +1107,79 @@
}
}
#rotationView .rotation-sector-chip.heat-mild {
border-color: var(--border);
background: var(--surface-muted);
box-shadow: none;
/* HEL-524: 五档强度色 —— 同一强度值在 9 个日期列中颜色完全一致,仅代表强度档位,与涨跌无关。
普通卡只用低透明着色,页面/主容器/日期分栏的层级由 HEL-522 的明度骨架承担,这里不重复叠加。 */
#rotationView .rotation-sector-chip.heat-tier-1 {
--tier-bar: var(--rotation-tier1-bar);
--tier-bg: var(--rotation-tier1-bg);
--tier-border: var(--rotation-tier1-border);
--tier-text: var(--rotation-tier1-text);
--tier-fill-start: var(--rotation-tier1-fill-start);
--tier-fill-end: var(--rotation-tier1-fill-end);
}
#rotationView .rotation-sector-chip.heat-strong {
border-color: var(--action-line);
background: var(--accent-soft);
box-shadow: none;
#rotationView .rotation-sector-chip.heat-tier-2 {
--tier-bar: var(--rotation-tier2-bar);
--tier-bg: var(--rotation-tier2-bg);
--tier-border: var(--rotation-tier2-border);
--tier-text: var(--rotation-tier2-text);
--tier-fill-start: var(--rotation-tier2-fill-start);
--tier-fill-end: var(--rotation-tier2-fill-end);
}
#rotationView .rotation-sector-chip.heat-warm {
border-color: var(--action-line);
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
box-shadow: none;
#rotationView .rotation-sector-chip.heat-tier-3 {
--tier-bar: var(--rotation-tier3-bar);
--tier-bg: var(--rotation-tier3-bg);
--tier-border: var(--rotation-tier3-border);
--tier-text: var(--rotation-tier3-text);
--tier-fill-start: var(--rotation-tier3-fill-start);
--tier-fill-end: var(--rotation-tier3-fill-end);
}
#rotationView .rotation-sector-chip:hover {
#rotationView .rotation-sector-chip.heat-tier-4 {
--tier-bar: var(--rotation-tier4-bar);
--tier-bg: var(--rotation-tier4-bg);
--tier-border: var(--rotation-tier4-border);
--tier-text: var(--rotation-tier4-text);
--tier-fill-start: var(--rotation-tier4-fill-start);
--tier-fill-end: var(--rotation-tier4-fill-end);
}
#rotationView .rotation-sector-chip.heat-tier-5 {
--tier-bar: var(--rotation-tier5-bar);
--tier-bg: var(--rotation-tier5-bg);
--tier-border: var(--rotation-tier5-border);
--tier-text: var(--rotation-tier5-text);
--tier-fill-start: var(--rotation-tier5-fill-start);
--tier-fill-end: var(--rotation-tier5-fill-end);
}
#rotationView .rotation-sector-chip[class*="heat-tier-"] {
border-color: var(--tier-border);
background: var(--tier-bg);
box-shadow: inset 4px 0 0 0 var(--tier-bar);
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1), background-color 120ms var(--ease-out), border-color 120ms var(--ease-out), box-shadow 120ms var(--ease-out), filter 240ms, opacity 220ms;
}
#rotationView .rotation-sector-chip[class*="heat-tier-"] .rotation-strength {
color: var(--tier-text);
}
/* 悬停态:卡底与描边 alpha 适度提升,120ms ease-out,可打断,不打乱档位色含义;
已经处于选中/追踪态的卡片保持其自身样式,不叠加普通悬停处理 */
#rotationView .rotation-sector-chip[class*="heat-tier-"]:hover:not(.selected) {
z-index: 6;
border-color: var(--accent);
background: color-mix(in srgb, var(--tier-bar) 8%, var(--tier-bg) 92%);
border-color: color-mix(in srgb, var(--tier-bar) 26%, var(--tier-border) 74%);
filter: saturate(1.06);
transform: translate3d(2px, -2px, 0px) scale(1.015);
box-shadow: none;
}
#rotationView .rotation-table {
@@ -1207,30 +1303,6 @@
box-shadow: var(--control-shadow);
}
:root[data-theme="dark"] #rotationView .rotation-sector-chip.heat-strong {
border-color: var(--blue-line);
background: var(--heat-strong-bg);
color: var(--heat-strong-ink);
}
:root[data-theme="dark"] #rotationView .rotation-sector-chip.heat-warm {
border-color: var(--border-strong);
background: var(--heat-warm-bg);
color: var(--heat-warm-ink);
}
:root[data-theme="dark"] #rotationView .rotation-sector-chip.heat-mild {
border-color: var(--border);
background: var(--heat-mild-bg);
color: var(--heat-mild-ink);
}
:root[data-theme="dark"] #rotationView :is(.rotation-rank, .rotation-table thead th, .trend-flat) {
border-color: var(--border);
@@ -1243,20 +1315,6 @@
background: var(--action-soft);
}
:root[data-theme="dark"] #rotationView :is(.rotation-swatch.warm) {
border-color: var(--blue-line);
background: var(--action-soft);
color: var(--action);
}
:root[data-theme="dark"] #rotationView :is(.rotation-swatch.mild) {
border-color: var(--border);
background: var(--surface-muted);
}
@media (max-width: 767px) {
#rotationView :is(.rotation-table-frame, .rotation-detail-card) {
max-height: none;
+6 -3
View File
@@ -22,9 +22,12 @@
<span class="rotation-top-tag">每日 Top 12 热点</span>
</header>
<div class="rotation-legend" aria-label="板块强度图例">
<span><i class="rotation-swatch strong"></i>强度高(90+</span>
<span><i class="rotation-swatch warm"></i>强度中(7089</span>
<span><i class="rotation-swatch mild"></i>强度低(&lt;70</span>
<span><i class="rotation-swatch tier-1"></i>低·青(&lt;65</span>
<span><i class="rotation-swatch tier-2"></i>中低·蓝(6574</span>
<span><i class="rotation-swatch tier-3"></i>中高·紫(7584</span>
<span><i class="rotation-swatch tier-4"></i>高·橙(8594</span>
<span><i class="rotation-swatch tier-5"></i>最高·红(95+</span>
<span class="rotation-legend-note">颜色=强度档位,与涨跌无关</span>
</div>
<div id="rotationTracker" class="rotation-tracker" hidden></div>
<div id="rotationHistory" class="rotation-history"><div class="empty-state">正在读取轮动历史</div></div>
+17 -3
View File
@@ -3,6 +3,16 @@ window.XiaobaiPageModules.register("rotation", ["rotationView"], {
enter: ["loadRotation"],
});
// HEL-524: 五档强度色固定映射,所有日期列共用同一套阈值 —— <65 青,65-74 蓝,75-84 紫,85-94 橙,95+ 红。
function rotationHeatTier(strengthValue) {
const value = clamp(number(strengthValue), 0, 100);
if (value >= 95) return 5;
if (value >= 85) return 4;
if (value >= 75) return 3;
if (value >= 65) return 2;
return 1;
}
async function loadRotationHistory(force = false) {
if (!state.dashboard || state.rotationLoading) return;
const key = `${elements.tradeDate.value}:9`;
@@ -87,10 +97,14 @@ function renderRotationHistory() {
<header><time>${escapeHtml(displayCompactDate(day.trade_date).slice(5))}</time><span>${(day.sectors || []).length} 个热点</span></header>
<div class="rotation-day-sectors">${(day.sectors || []).map((sector) => {
const strength = clamp(number(sector.strength), 0, 100);
const heatClass = strength >= 90 ? "heat-strong" : strength >= 70 ? "heat-warm" : "heat-mild";
const heatClass = `heat-tier-${rotationHeatTier(strength)}`;
const isTrackedName = Boolean(selected) && selected === sector.name;
const isCurrentCell = isTrackedName && day.trade_date === state.rotationSelectedDate;
const stateClass = isCurrentCell ? "selected current-cell" : isTrackedName ? "selected" : "";
return `
<button type="button" class="rotation-sector-chip ${heatClass} ${selected === sector.name ? "selected" : ""}" data-rotation-sector="${escapeHtml(sector.name)}" data-rotation-date="${escapeHtml(day.trade_date)}">
<span class="rotation-rank rank-${Math.min(number(sector.rank), 4)}">${number(sector.rank)}</span><strong>${escapeHtml(sector.name)}</strong><small><b>${number(sector.count)}</b> 家 · ${formatNumber(sector.strength, 0)}</small>
<button type="button" class="rotation-sector-chip ${heatClass} ${stateClass}" data-rotation-sector="${escapeHtml(sector.name)}" data-rotation-date="${escapeHtml(day.trade_date)}">
<span class="rotation-rank rank-${Math.min(number(sector.rank), 4)}">${number(sector.rank)}</span><strong>${escapeHtml(sector.name)}</strong><small><b>${number(sector.count)}</b> 家 · <b class="rotation-strength">${formatNumber(sector.strength, 0)}</b></small>
${isTrackedName && !isCurrentCell ? '<span class="rotation-track-dot" aria-hidden="true"></span>' : ""}
<span class="rotation-cell-tooltip">${escapeHtml(displayCompactDate(day.trade_date).slice(5))} · 第 ${number(sector.rank)} 名 · 涨停 ${number(sector.count)} 家 · 强度 ${formatNumber(sector.strength, 0)}</span>
</button>`;
}).join("")}</div>
+17 -11
View File
@@ -2287,8 +2287,6 @@ body[data-active-view="screenerView"] .workspace-view {
box-shadow: none;
overflow: visible;
gap: 16px;
}
:where(#screenerView) .curated-library-pane {
@@ -2546,7 +2544,7 @@ body[data-active-view="screenerView"] .workspace-view {
flex: 0 0 auto;
gap: 2px;
gap: 8px;
margin: 0px;
@@ -2566,11 +2564,11 @@ body[data-active-view="screenerView"] .workspace-view {
padding: 0 14px;
border: 0px;
border: 1px solid var(--control-border);
border-radius: 0;
border-radius: var(--radius-md);
background: transparent;
background: var(--control-surface);
color: var(--r2-sub);
@@ -2586,15 +2584,19 @@ body[data-active-view="screenerView"] .workspace-view {
}
#screenerView .screener-mode-tabs button:hover {
background: var(--surface-hover);
border-color: var(--r2-blue-line);
background: var(--control-hover);
color: var(--r2-ink);
}
#screenerView .screener-mode-tabs button.active {
background: transparent;
border-color: var(--scr-blue);
color: var(--scr-blue);
background: var(--scr-blue);
color: var(--on-action);
font-weight: var(--font-weight-semibold);
}
@@ -3135,7 +3137,11 @@ body[data-active-view="screenerView"] .workspace-view {
}
#screenerView .curated-screener-panel {
display: block;
display: flex;
flex-direction: column;
gap: 10px;
}
#screenerView .curated-workspace {
@@ -6184,7 +6190,7 @@ body[data-active-view="screenerView"] .workspace-view {
background: var(--surface-muted);
}
:root[data-theme="dark"] #screenerView :is(.screener-mode-tabs, .screener-stepper, .regime-selector, .regime-option, .screener-results-view, .result-toolbar) {
:root[data-theme="dark"] #screenerView :is(.screener-stepper, .regime-selector, .regime-option, .screener-results-view, .result-toolbar) {
border-color: var(--border);
background: var(--surface);
+2 -2
View File
@@ -1596,7 +1596,7 @@
background: var(--surface);
}
:root[data-theme="dark"] :is(.sentiment-cycle-toolbar, .tbl-tools) {
:root[data-theme="dark"] :is(.tbl-tools) {
border-color: var(--line-soft);
background: var(--surface);
@@ -1610,7 +1610,7 @@
color: var(--warning-color);
}
:root[data-theme="dark"] #sentimentCycleView :is(.sentiment-current-tag, .sentiment-auto-tag, .sentiment-detail-toolbar, .section-toolbar) {
:root[data-theme="dark"] #sentimentCycleView :is(.sentiment-current-tag, .sentiment-auto-tag, .sentiment-detail-toolbar) {
border-color: var(--border);
background: var(--surface-muted);
+27
View File
@@ -3765,3 +3765,30 @@ body.mobile-shell[data-active-view="heavenView"] .app-header .overview-strip[dat
overflow: hidden;
}
/* HEL-524: 侧栏去掉作为整块独立色板的背景,融入页面底层;品牌区与顶栏保持同层;
收起侧栏整行不再单独着色。展开/收起态均生效。「问天」冻结路由维持原有外观。 */
body:not(.mobile-shell):not([data-active-view="heavenView"]) .module-nav,
body.sidebar-collapsed:not(.mobile-shell):not([data-active-view="heavenView"]) .module-nav {
background: var(--canvas);
}
body:not(.mobile-shell):not([data-active-view="heavenView"]) .sidebar-brand {
background: var(--header-bg);
}
body:not(.mobile-shell):not([data-active-view="heavenView"]) .sidebar-collapse-button {
background: transparent;
}
:root[data-theme="dark"] body:not(.mobile-shell):not([data-active-view="heavenView"]) .module-nav {
background: var(--canvas);
}
:root[data-theme="dark"] body:not(.mobile-shell):not([data-active-view="heavenView"]) .sidebar-brand {
background-color: var(--header-bg);
}
:root[data-theme="dark"] body:not(.mobile-shell):not([data-active-view="heavenView"]) :is(.nav-group, .sidebar-collapse-button) {
background-color: transparent;
}
+68
View File
@@ -381,6 +381,40 @@
--r2-radius: var(--size-radius-lg);
--r2-shadow: var(--elevation-card);
/* HEL-524: 板块轮动五档强度色(日间)。五档单调递进:青→蓝→紫→橙→红,冷到暖;
同一强度值在所有日期列使用完全相同的颜色,颜色仅代表强度档位,与涨跌无关。 */
--rotation-tier1-bar: #0D9488;
--rotation-tier1-bg: rgba(13, 148, 136, .08);
--rotation-tier1-border: rgba(13, 148, 136, .25);
--rotation-tier1-text: #0F766E;
--rotation-tier1-fill-start: #11766D;
--rotation-tier1-fill-end: #0C443F;
--rotation-tier2-bar: #2563EB;
--rotation-tier2-bg: rgba(37, 99, 235, .07);
--rotation-tier2-border: rgba(37, 99, 235, .22);
--rotation-tier2-text: #1D4ED8;
--rotation-tier2-fill-start: #2059D7;
--rotation-tier2-fill-end: #1D46A2;
--rotation-tier3-bar: #7C3AED;
--rotation-tier3-bg: rgba(124, 58, 237, .07);
--rotation-tier3-border: rgba(124, 58, 237, .22);
--rotation-tier3-text: #6D28D9;
--rotation-tier3-fill-start: #6F2DE0;
--rotation-tier3-fill-end: #5720B5;
--rotation-tier4-bar: #EA580C;
--rotation-tier4-bg: rgba(234, 88, 12, .08);
--rotation-tier4-border: rgba(234, 88, 12, .25);
--rotation-tier4-text: #C2410C;
--rotation-tier4-fill-start: #C85215;
--rotation-tier4-fill-end: #913E13;
--rotation-tier5-bar: #DC2626;
--rotation-tier5-bg: rgba(220, 38, 38, .08);
--rotation-tier5-border: rgba(220, 38, 38, .25);
--rotation-tier5-text: #B91C1C;
--rotation-tier5-fill-start: #BE2B2B;
--rotation-tier5-fill-end: #8C2424;
--rotation-tier-glow-alpha: 22%;
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
font-size: 14px;
}
@@ -557,6 +591,40 @@
--heat-warm-ink: #dfeaf7;
--heat-mild-bg: #293440;
--heat-mild-ink: #c7d2dc;
/* HEL-524: 板块轮动五档强度色(夜间)。五档单调递进:青→蓝→紫→橙→红,冷到暖;
同一强度值在所有日期列使用完全相同的颜色,颜色仅代表强度档位,与涨跌无关。 */
--rotation-tier1-bar: #2DD4BF;
--rotation-tier1-bg: rgba(45, 212, 191, .09);
--rotation-tier1-border: rgba(45, 212, 191, .20);
--rotation-tier1-text: #5EEAD4;
--rotation-tier1-fill-start: #32B6A5;
--rotation-tier1-fill-end: #2A867A;
--rotation-tier2-bar: #60A5FA;
--rotation-tier2-bg: rgba(96, 165, 250, .10);
--rotation-tier2-border: rgba(96, 165, 250, .22);
--rotation-tier2-text: #93C5FD;
--rotation-tier2-fill-start: #5098F1;
--rotation-tier2-fill-end: #217AE7;
--rotation-tier3-bar: #A78BFA;
--rotation-tier3-bg: rgba(167, 139, 250, .11);
--rotation-tier3-border: rgba(167, 139, 250, .24);
--rotation-tier3-text: #C4B5FD;
--rotation-tier3-fill-start: #9879F2;
--rotation-tier3-fill-end: #724AE9;
--rotation-tier4-bar: #FB923C;
--rotation-tier4-bg: rgba(251, 146, 60, .12);
--rotation-tier4-border: rgba(251, 146, 60, .26);
--rotation-tier4-text: #FDBA74;
--rotation-tier4-fill-start: #F07C2D;
--rotation-tier4-fill-end: #D05A14;
--rotation-tier5-bar: #F87171;
--rotation-tier5-bg: rgba(248, 113, 113, .13);
--rotation-tier5-border: rgba(248, 113, 113, .28);
--rotation-tier5-text: #FCA5A5;
--rotation-tier5-fill-start: #EF6161;
--rotation-tier5-fill-end: #E53333;
--rotation-tier-glow-alpha: 35%;
--heaven-field-bg: #23241f;
--shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
--shadow: 0 18px 50px rgba(0, 0, 0, .46);
+3 -3
View File
@@ -1260,9 +1260,9 @@ test("sector rotation transfers the nine-day matrix, tracking and sortable detai
await expect(page.locator("#rotationHistory .rotation-day").last()).toHaveClass(/latest-day/);
await expect(page.locator("#rotationView .rotation-legend")).not.toContainText("单元格 =");
const firstDayCells = page.locator("#rotationHistory .rotation-day").first().locator(".rotation-sector-chip");
await expect(firstDayCells.nth(0)).toHaveClass(/heat-strong/);
await expect(firstDayCells.nth(1)).toHaveClass(/heat-warm/);
await expect(firstDayCells.nth(2)).toHaveClass(/heat-mild/);
await expect(firstDayCells.nth(0)).toHaveClass(/heat-tier-4/); // strength 92 → 85-94 档(高·橙)
await expect(firstDayCells.nth(1)).toHaveClass(/heat-tier-3/); // strength 76 → 75-84 档(中高·紫)
await expect(firstDayCells.nth(2)).toHaveClass(/heat-tier-1/); // strength 58 → <65 档(低·青)
const cellVisuals = await firstDayCells.evaluateAll((cells) => cells.map((cell) => {
const style = getComputedStyle(cell);
return { background: style.backgroundColor, radius: parseFloat(style.borderRadius), duration: style.transitionDuration };
+1215 -173
View File
File diff suppressed because it is too large Load Diff
+39 -14
View File
@@ -3,14 +3,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>xiaobai-datahub 管理后台</title>
<title>xiaobai-datahub 管理后台 · 数据中枢</title>
<link rel="stylesheet" href="/admin/styles.css" />
</head>
<body>
<div id="app">
<section id="login-view" class="panel auth-panel">
<span class="badge-sim">内网 · 8766</span>
<h1>数据中枢</h1>
<p class="muted">内网管理后台,用于查看源状态、调度和盘后发布批次</p>
<p class="muted">四路来源持续汇流、调度、发布与审计的运转空间</p>
<form id="login-form">
<label>账号 <input name="username" value="hub_admin" autocomplete="username" /></label>
<label>密码 <input name="password" type="password" autocomplete="current-password" /></label>
@@ -30,22 +31,46 @@
</section>
<section id="shell" hidden>
<header class="top">
<strong>xiaobai-datahub</strong>
<header id="topbar">
<div class="logo">小白复盘 <em>·</em> 数据中枢</div>
<span class="crumb" id="crumb">8766 · 四源汇流 · 持续运转</span>
<span id="phase" class="pill"></span>
<span id="who" class="muted"></span>
<span class="spacer"></span>
<span id="who" class="muted who"></span>
<button type="button" id="theme-btn" class="ghost">夜间</button>
<button type="button" id="logout-btn" class="ghost">退出</button>
</header>
<nav>
<button data-page="overview" class="active">总览</button>
<button data-page="sources">数据源</button>
<button data-page="jobs">调度任务</button>
<button data-page="release">盘后发布</button>
<button data-page="datasets">数据集</button>
<button data-page="audit">审计</button>
</nav>
<main id="page"></main>
<!-- 动效版:一台连续空间舞台,六幕滚动切换,同一核心与流道贯穿始终 -->
<div id="stageWrap">
<canvas id="scene"></canvas>
<div id="cabin"></div>
<div id="legend">
<span class="lampico li-link"></span>LINK 链路常亮(低亮)<br>
<span class="lampico li-act"></span>ACT 活动灯 · 事件成簇短闪
</div>
<div id="rail"></div>
<div id="hint">滚 动 推 进 镜 头</div>
<div id="detail" class="closed">
<div class="dtag"></div>
<button id="detailClose" type="button" aria-label="关闭详情">×</button>
<div id="detailBody"></div>
</div>
</div>
<div id="track" aria-hidden="true">
<section data-scene="0"></section>
<section data-scene="1"></section>
<section data-scene="2"></section>
<section data-scene="3"></section>
<section data-scene="4"></section>
<section data-scene="5"></section>
</div>
<!-- 减少动态效果版:六幕静态空间图 + 固定文字状态,信息与功能完全等价 -->
<main id="staticShell" hidden>
<nav id="staticNav"></nav>
<div id="staticScenes"></div>
</main>
</section>
</div>
<script src="/admin/app.js"></script>
+272 -37
View File
@@ -1,51 +1,286 @@
/* xiaobai-datahub 数据中枢后台 —— 第七版「轨道机芯」
统一动效 Token:进入/退出用强 ease-out,屏内移动用强 ease-in-out,持续流动用 linear。
禁止 ease-in、禁止 transition:all —— 全文件遵守。 */
:root {
color-scheme: light;
--bg: #f4f5f7;
--surface: #ffffff;
--text: #1f2329;
--muted: #646a73;
--line: #dee0e3;
--action: #3370ff;
--danger: #e04536;
--ok: #16a34a;
--warn: #b45309;
--radius: 8px;
--bar-h: 52px;
--radius: 14px;
--pad: 16px;
font-family: "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
--font-cn: "Segoe UI", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
--font-mono: "JetBrains Mono", "DejaVu Sans Mono", Consolas, monospace;
--ease-out: cubic-bezier(.22, 1, .36, 1);
--ease-in-out: cubic-bezier(.65, 0, .35, 1);
--ease-linear: linear;
/* day (default) */
--bg0: #EDF0F7;
--bg1: #F8FAFF;
--surface: #ffffff;
--stage-edge: rgba(47, 99, 214, .16);
--text: #1B2547;
--muted: #5B6785;
--faint: #93A0BD;
--line: rgba(47, 99, 214, .18);
--action: #2F63D6;
--cyan: #0A9C93;
--amber: #B97A0E;
--danger: #D64F4F;
--ok: #1E9E6A;
--warn: #B97A0E;
--glass: rgba(255, 255, 255, .82);
--chip: rgba(47, 99, 214, .08);
--face: rgba(120, 160, 235,);
--edge: rgba(47, 99, 214,);
--slab: rgba(10, 156, 147,);
--ring: rgba(47, 99, 214,);
--chan1: rgba(47, 99, 214,);
--chan2: rgba(10, 124, 146,);
--chan3: rgba(109, 93, 214,);
--chan4: rgba(190, 120, 10,);
}
:root[data-theme="night"] {
color-scheme: dark;
--bg: #111318;
--surface: #1b1e24;
--text: #e8eaed;
--muted: #9aa0a6;
--line: #2a2f38;
--action: #5b8cff;
--bg0: #05080F;
--bg1: #0A1020;
--surface: #12172A;
--stage-edge: rgba(90, 140, 255, .14);
--text: #E8EEFC;
--muted: #8B97B8;
--faint: #59637F;
--line: rgba(120, 160, 255, .16);
--action: #4A86FF;
--cyan: #2FD8CE;
--amber: #FFB84D;
--danger: #FF6B6B;
--ok: #3ED598;
--warn: #FFB84D;
--glass: rgba(13, 20, 38, .72);
--chip: rgba(74, 134, 255, .10);
--face: rgba(58, 96, 180,);
--edge: rgba(140, 190, 255,);
--slab: rgba(47, 216, 206,);
--ring: rgba(120, 170, 255,);
--chan1: rgba(150, 205, 255,);
--chan2: rgba(120, 180, 255,);
--chan3: rgba(185, 175, 255,);
--chan4: rgba(255, 214, 160,);
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); }
.panel, header.top, nav, main { background: var(--surface); }
.auth-panel { max-width: 420px; margin: 12vh auto; padding: 28px; border-radius: var(--radius); border: 1px solid var(--line); }
label { display: block; margin: 12px 0; }
input, select { width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--text); }
button { background: var(--action); color: #fff; border: 0; border-radius: 4px; padding: 8px 14px; cursor: pointer; }
button.ghost { background: transparent; color: var(--text); border: 1px solid var(--line); }
button.danger { background: var(--danger); }
html, body { margin: 0; background: var(--bg0); color: var(--text); font-family: var(--font-cn); }
body { overflow-x: hidden; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.error { color: var(--danger); }
.top { display: flex; gap: 12px; align-items: center; padding: 10px var(--pad); border-bottom: 1px solid var(--line); }
nav { display: flex; gap: 4px; padding: 8px var(--pad); border-bottom: 1px solid var(--line); }
nav button { background: transparent; color: var(--muted); }
nav button.active { color: var(--action); background: transparent; font-weight: 600; }
main { padding: var(--pad); min-height: calc(100vh - 96px); }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px; }
.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
.pill { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.fail { color: var(--danger); }
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; align-items: end; }
.toolbar label { margin: 0; }
/* ---------- 登录 / 改密(未进入舞台的前置流程) ---------- */
.auth-panel {
max-width: 420px; margin: 12vh auto; padding: 30px 32px; border-radius: var(--radius);
background: var(--glass); border: 1px solid var(--line); backdrop-filter: blur(14px);
box-shadow: 0 30px 70px -32px rgba(20, 30, 60, .35);
}
.auth-panel h1 { margin: 0 0 6px; font-size: 21px; }
.auth-panel label { display: block; margin: 12px 0; font-size: 13px; color: var(--muted); }
.auth-panel input {
width: 100%; margin-top: 6px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 8px;
background: var(--chip); color: var(--text); font-family: var(--font-cn);
}
.auth-panel input:focus { outline: 2px solid var(--action); outline-offset: 1px; }
.auth-panel .badge-sim { display: inline-block; margin-bottom: 10px; }
button { font-family: var(--font-cn); cursor: pointer; }
.btn, button.primary, .auth-panel button[type="submit"] {
background: linear-gradient(135deg, var(--action), var(--cyan)); color: #fff; border: 0; border-radius: 8px;
padding: 8px 16px; font-size: 13px; transition: transform .12s var(--ease-out), opacity .12s var(--ease-out);
}
.btn:active, button:active { transform: translateY(1px); }
.btn.ghost, button.ghost {
background: var(--chip); color: var(--text); border: 1px solid var(--line);
}
.btn.warn { background: transparent; color: var(--warn); border: 1px solid var(--warn); }
.btn.danger, button.danger { background: var(--danger); color: #fff; border: 0; }
.btn.pri { background: linear-gradient(135deg, var(--action), var(--cyan)); color: #fff; border: 0; }
.btn { padding: 6px 13px; border-radius: 8px; font-size: 12px; }
.badge-sim {
font-size: 11px; padding: 3px 10px; border-radius: 99px; letter-spacing: .1em;
color: var(--amber); border: 1px solid var(--amber); opacity: .85;
}
/* ---------- 通用表格 / 卡片(cabin、detail、reduced 三处共用同一来源) ---------- */
table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
th, td { text-align: left; padding: 7px 6px; border-bottom: 1px dashed var(--line); vertical-align: top; }
th { color: var(--faint); font-weight: 500; font-size: 11px; letter-spacing: .04em; }
.pill { font-size: 12px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-bottom: 14px; }
.card { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: var(--chip); }
.card strong { font-size: 18px; }
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; align-items: center; }
.toolbar label { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; }
.toolbar input { padding: 6px 9px; border: 1px solid var(--line); border-radius: 6px; background: var(--chip); color: var(--text); }
h2 { font-size: 16px; margin: 4px 0 10px; }
h3 { font-size: 13px; margin: 14px 0 6px; color: var(--muted); }
/* ---------- 顶部总控条 ---------- */
#topbar {
height: var(--bar-h); display: flex; align-items: center; gap: 14px; padding: 0 22px;
border-bottom: 1px solid var(--line); background: linear-gradient(180deg, var(--bg1), transparent);
backdrop-filter: blur(10px);
position: sticky; top: 0; z-index: 4;
}
#topbar .logo { font-weight: 700; font-size: 15px; letter-spacing: .04em; }
#topbar .logo em { font-style: normal; color: var(--cyan); }
#topbar .crumb { font-size: 12px; color: var(--faint); }
#topbar .spacer { flex: 1; }
#topbar .who { font-size: 12px; }
/* ---------- 固定空间舞台(76vh,六幕连续场景) ----------
#track 的六段高幕(每段 130vh)在它身后正常参与文档流把页面撑高,
#stageWrap 用 sticky 钉在视口里,滚动时只有 #track 的高度被“划过”,
舞台本体在这段距离内始终可见、位置不变,直到 #track 撑出的空间耗尽才随之离场。 */
#stageWrap {
flex: none; height: 76vh; min-height: 480px; margin: 0 18px; border-radius: 18px;
overflow: hidden; border: 1px solid var(--stage-edge);
box-shadow: 0 30px 80px -30px rgba(0, 0, 0, .45), inset 0 0 120px rgba(0, 0, 0, .08);
position: sticky; top: var(--bar-h); z-index: 2;
}
#scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; cursor: crosshair; }
/* ---------- 控制舱:当前场景的关键数据与操作 ---------- */
#cabin {
position: absolute; left: 22px; top: 22px; width: 300px; z-index: 5; max-height: calc(100% - 44px);
overflow: auto; background: var(--glass); border: 1px solid var(--line); border-radius: 14px;
backdrop-filter: blur(14px); padding: 16px 18px;
transition: opacity .45s var(--ease-out), transform .45s var(--ease-out);
}
#cabin.hide { opacity: 0; transform: translateX(-14px); pointer-events: none; }
#cabin .eyebrow { font-size: 10px; letter-spacing: .22em; color: var(--cyan); margin-bottom: 6px; }
#cabin h2 { font-size: 18px; margin: 0 0 4px; }
#cabin .sub { font-size: 11.5px; color: var(--muted); line-height: 1.6; margin-bottom: 10px; }
#cabin .rows { display: flex; flex-direction: column; gap: 7px; margin-bottom: 4px; }
#cabin .row { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
#cabin .row b { color: var(--text); font-weight: 600; }
#cabin .row .fill { flex: 1; }
#cabin .dotlamp { width: 7px; height: 7px; border-radius: 99px; background: var(--ok); box-shadow: 0 0 8px var(--ok); flex: none; }
#cabin .dotlamp.warn { background: var(--warn); box-shadow: 0 0 8px var(--warn); }
#cabin .dotlamp.fail { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
#cabin .actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
/* ---------- 底部章节轨(导航等同滚动跳转) ---------- */
#rail {
position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 5;
display: flex; gap: 6px; align-items: center; padding: 8px 12px; border-radius: 99px;
background: var(--glass); border: 1px solid var(--line); backdrop-filter: blur(14px);
}
#rail .stop {
display: flex; align-items: center; gap: 7px; padding: 5px 11px; border-radius: 99px; cursor: pointer;
font-size: 11.5px; color: var(--faint); background: transparent; border: 0;
transition: color .25s var(--ease-in-out), background .25s var(--ease-in-out);
font-family: var(--font-cn); white-space: nowrap;
}
#rail .stop i { width: 6px; height: 6px; border-radius: 99px; background: var(--faint); transition: background .25s var(--ease-in-out), box-shadow .25s var(--ease-in-out); display: inline-block; }
#rail .stop.on { color: var(--text); background: var(--chip); }
#rail .stop.on i { background: var(--cyan); box-shadow: 0 0 10px var(--cyan); }
#rail .sep { width: 12px; height: 1px; background: var(--line); }
/* ---------- 图例 ---------- */
#legend {
position: absolute; right: 22px; top: 22px; z-index: 5; font-size: 10.5px; color: var(--muted);
background: var(--glass); border: 1px solid var(--line); border-radius: 12px; padding: 10px 13px;
backdrop-filter: blur(14px); line-height: 1.9;
}
#legend .lampico { display: inline-block; width: 14px; height: 5px; border-radius: 99px; vertical-align: middle; margin-right: 6px; }
#legend .li-link { background: rgba(47, 216, 206, .5); box-shadow: 0 0 6px rgba(47, 216, 206, .6); }
#legend .li-act { background: var(--amber); box-shadow: 0 0 8px var(--amber); }
/* ---------- 详情面板:从端口/来源空间位置展开的功能抽屉 ---------- */
#detail {
position: absolute; z-index: 8; width: 360px; max-width: calc(100% - 40px); max-height: calc(100% - 40px);
overflow: auto; pointer-events: auto; background: var(--glass); border: 1px solid var(--line);
border-radius: 14px; backdrop-filter: blur(16px); padding: 16px 18px;
transition: transform .4s var(--ease-out), opacity .3s var(--ease-out);
box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .45);
}
#detail.closed { transform: scale(.14); opacity: 0; pointer-events: none; }
#detail h3 { font-size: 14px; margin: 0 0 2px; color: var(--text); }
#detail .dsub { font-size: 10.5px; color: var(--faint); margin-bottom: 10px; letter-spacing: .04em; }
#detailClose {
position: absolute; right: 10px; top: 10px; width: 24px; height: 24px; border-radius: 8px;
border: 1px solid var(--line); background: transparent; color: var(--muted); cursor: pointer; font-size: 13px; line-height: 1;
}
#detail .dtag {
position: absolute; left: -7px; top: 26px; width: 14px; height: 14px; transform: rotate(45deg);
background: var(--glass); border-left: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
/* ---------- 滚动提示 ---------- */
#hint {
position: absolute; left: 50%; bottom: 100px; transform: translateX(-50%); z-index: 5;
font-size: 11px; letter-spacing: .3em; color: var(--faint); transition: opacity .5s var(--ease-out);
}
#hint.off { opacity: 0; }
/* 滚动章节占位:仅提供滚动行程,无可见内容 */
#track { position: relative; z-index: 1; pointer-events: none; }
#track section { height: 130vh; }
/* ---------- 危险操作 / 确认弹层,复用 dialog ---------- */
dialog { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 20px; }
/* ================================================================
减少动态效果(prefers-reduced-motion):舞台切换为六幕静态空间图 + 固定文字状态
信息与功能与动效版完全一致,只是去掉滚动镜头、旋转与闪烁。
================================================================ */
#staticShell { display: none; }
html.reduced #stageWrap, html.reduced #track, html.reduced #hint { display: none !important; }
html.reduced #staticShell { display: block; }
#staticNav {
display: flex; gap: 4px; padding: 8px var(--pad); border-bottom: 1px solid var(--line);
background: var(--surface); flex-wrap: wrap; position: sticky; top: 0; z-index: 3;
}
#staticNav button {
background: transparent; color: var(--muted); border: 0; padding: 7px 12px; border-radius: 8px; font-size: 13px;
}
#staticNav button.active { color: var(--action); font-weight: 600; background: var(--chip); }
.scene-static { padding: var(--pad); border-bottom: 1px solid var(--line); }
.scene-static .scene-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.scene-static .scene-head .eyebrow { font-size: 11px; letter-spacing: .18em; color: var(--cyan); }
.scene-static .diagram {
width: 100%; max-width: 620px; height: 190px; border-radius: 12px; border: 1px solid var(--line);
background: var(--chip); display: block; margin-bottom: 12px;
}
.scene-static .lamprow { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.lampchip {
display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted);
border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; background: var(--chip);
}
.lampchip .lk, .lampchip .ac { width: 7px; height: 7px; border-radius: 99px; background: var(--faint); display: inline-block; }
.lampchip .lk.on { background: var(--cyan); box-shadow: 0 0 5px var(--cyan); }
.lampchip .ac.on { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.lampchip .ac.recent { outline: 1px solid var(--amber); transition: outline-color .8s var(--ease-out); }
/* ---------- 主内容区(main/reduced 内容容器统一样式,供动效版 detail 与静态版共用) ---------- */
main#page { padding: var(--pad); min-height: calc(100vh - 96px); background: var(--surface); }
/* ---------- 响应式:1440 / 1280 / 1024 常见桌面宽 ---------- */
@media (max-width: 1280px) {
#cabin { width: 260px; }
#detail { width: 320px; }
}
@media (max-width: 1100px) {
#stageWrap { margin: 0 10px; }
#cabin { width: 230px; padding: 13px 14px; }
#legend { display: none; }
}
/* ---------- 系统级兜底:即便 JS 未及时接管,也不留半成品动效 ---------- */
@media (prefers-reduced-motion: reduce) {
#stageWrap, #track, #hint { display: none !important; }
#staticShell { display: block !important; }
* { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}