XiStudio Layout Demo · 顶栏架构重构 v4 方案
状态:📋 待评审
创建:2026-05-19
范围:Shellindex.html+ 4 个 stage(XiLink / XiTune / XiForge / XiTest)+(可选)xistudio-codex.css微调
目标:把分散在各 stage 内的 doc-tabs / mode-bar / 通用工具按钮,按职责重新归位到 Shell 顶栏的"三段式",让顶栏成为 Stage 切换 + 主界面切换 + 全局工具的统一入口。
1. 用户决策回顾
| 决策点 | 用户选择 |
|---|---|
| A. doc-tabs 去留 | 保留 XiLink / XiTune(子图切换需要);XiForge / XiTest 删除 |
| B. mode-bar 位置 | 移到 Shell 顶栏右半部分(不再嵌在 stage 内) |
| C. 改造范围 | Y · 一次到位完整重构 所有 4 个 stage |
| D. 音频引擎 | Shell 全局右段共用,3 按钮(▶ Start / ■ Stop / 🔄 Restart)+ 状态灯 |
2. 现状诊断(基于代码精确读取)
2.1 Shell 顶栏现状(index.html L1040-1077)
[traffic-lights] [logo] [☰menu] [.main-tabs#stagePills (4 stage Pill)]
[.toolbar-group#stageToolbar ← Stage 注入]
[.float-mgr 浮窗] [.lock-toggle 锁定] [.lock-toggle 主题] [.lic-tag]
<div class="toolbar-group" id="stageToolbar"> 是当前唯一的 stage 注入位(被 4 stage 都用作"通用工具按钮区")
- L1072:主题/锁定/浮窗都是 Shell 全局按钮(保持不动)
2.2 codex.css 已预留双行顶栏机制(L243-310)✨ 关键发现
.ide.with-subtabs-bar { --topbar-h: 70px; } /* 顶栏 44 → 70px */
.ide.with-subtabs-bar .topbar { height: 44px; }
.sub-tabs-bar {
grid-area: topbar; /* 同一个 grid 区域 */
align-self: end; /* 贴底部 */
height: 26px;
/* 玫瑰金小三角::before 指向 active 主 tab */
}
.with-subtabs-bar class,并在 .topbar 之后插入 <div class="sub-tabs-bar"> 即可。
2.3 Shell 现有清理逻辑(L2141-2161)
switchStage(stageKey) 已经在切 iframe.src 之前清空:
- #stageToolbar.innerHTML = ''
- #stageStatusSlot.innerHTML = ''
- #stageHintSlot.innerHTML = ''
- resetStageDock()
→ 我们扩展时只需在这里追加一行清空 #modeSwitcher。
2.4 各 stage toolbar-inject 按钮现状
| Stage | 按钮(id · label) | 重新归类建议 |
|---|---|---|
| XiLink | xl-update 更新链路 / xl-verify 拓扑校验 / xl-align 节点对齐 / xl-compile 编译 / xl-flash 烧录 | 全归"通用工具"(链路操作) |
| XiForge | (子代理截断未拿全,重构时按代码现读) | 模块开发操作归"通用工具" |
| XiTune | xt-autoeq 自动 EQ / xt-ear 黄金耳 / xt-ab A/B / xt-export 导出 / xt-reset 重置 | 全归"通用工具"(调音操作) |
| XiTest | xt-run 运行 / xt-stop 停止 / xt-rerun 重跑 / xt-capture 捕获 / xt-export 导出 | 全归"通用工具"(测试操作) |
音频引擎 ▶/■/🔄 在 4 个 stage 都通用,所以不放任何 stage 的 toolbar,而是 Shell 顶栏直接渲染。
3. 新顶栏架构设计
3.1 顶栏 DOM 三段式(重构后)
┌────────────────────── 行 1 (44px) ──────────────────────────────────────┐
│ [🚦] XiStudio [☰] │
│ ├─ 左段 ─┤ [🔗XL][🔨XF][🎚XT][🧪XS] ← stage 切换器(保持不变) │
│ ├─ 中段 ─┤ ★ 留空 / 由 stage 通过 mode-switcher-inject 注入 │
│ ├─ 右段 ─┤ [🛠 通用工具组 #stageToolbar] | [▶ ■ 🔄 ●] [🪟] [🔒] [🎨] [Demo] │
└─────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 行 2 (26px · 仅 XL/XT) ─────────────────────────────┐
│ ▾ DOC TABS · main_chain.xilink ● subgraph_eq.xilink multi_band.xilink + │
└─────────────────────────────────────────────────────────────────────────┘
关键说明:
1. 行 2(sub-tabs-bar)只对 XiLink / XiTune 显示——通过 .ide.with-subtabs-bar class 切换。XiForge/XiTest active 时移除该 class,顶栏回到单行 44px。
2. 中段(mode-switcher-slot)只对需要的 stage 显示——目前只有 XiTest 的 5 个 mode chip(SMOKE/INTEG/ELEC/ACOU/LIVE)。XiLink/XiTune/XiForge 注入空数组或不发协议,中段为空。
3. 右段顺序固定:#stageToolbar(stage 通用工具,可变)→ 音频引擎组(Shell 固定)→ 浮窗/锁/主题/lic(Shell 固定)。
3.2 新增 DOM 元素(index.html)
在 L1063 </div> 之后(.main-tabs 闭合后)插入"中段 mode-switcher-slot":
<!-- 中段:mode-switcher(由当前 Stage 通过 mode-switcher-inject 注入;空时不占位) -->
<div class="mode-switcher" id="modeSwitcher"><!-- empty by default --></div>
在 L1066 #stageToolbar 之后插入"音频引擎全局组":
<!-- Shell 全局:音频引擎控制(所有 Stage 共用 · 不归任何 Stage) -->
<div class="audio-engine" id="audioEngine" data-state="stopped">
<button class="ae-btn ae-start" id="aeStart" title="启动音频引擎">▶</button>
<button class="ae-btn ae-stop" id="aeStop" title="停止音频引擎">■</button>
<button class="ae-btn ae-restart" id="aeRestart" title="重启音频引擎">🔄</button>
<span class="ae-state" id="aeState" title="引擎状态">●</span>
</div>
在 </div> 顶栏闭合(L1077)之后插入"sub-tabs-bar 行 2":
</div><!-- /.topbar 行 1 闭合 -->
<!-- 行 2:doc-tabs(仅 XL/XT 显示 · 由 .ide.with-subtabs-bar 控制可见性) -->
<div class="sub-tabs-bar" id="docTabsBar">
<!-- 由当前 Stage 通过 doc-tabs-inject 注入;XF/XS 时清空,且 .ide 移除 with-subtabs-bar -->
</div>
3.3 CSS 微调(追加到 index.html <style> 内或 codex.css 末尾)
/* 顶栏中段:mode-switcher(chip 风格,对齐 XiTest 现有 mode-bar 视觉) */
.topbar .mode-switcher {
display: inline-flex; align-items: center; gap: 4px;
margin-left: 6px; flex-shrink: 1;
}
.topbar .mode-switcher:empty { display: none; }
.topbar .mode-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 999px;
background: transparent; border: 1px solid var(--line);
color: var(--fg-1); font-size: 11px; cursor: pointer;
white-space: nowrap;
}
.topbar .mode-chip:hover { border-color: var(--accent); color: var(--accent); }
.topbar .mode-chip.active {
background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
}
.topbar .mode-chip .chip-tag {
font-size: 8.5px; padding: 1px 4px; border-radius: 3px;
background: var(--bg-2); color: var(--fg-2); font-weight: 700;
font-family: ui-monospace, monospace;
}
.topbar .mode-chip.active .chip-tag {
background: rgba(255,255,255,.25); color: #fff;
}
/* 顶栏右段:音频引擎全局组 */
.topbar .audio-engine {
display: inline-flex; align-items: center; gap: 2px;
margin-left: 8px; flex-shrink: 0;
padding: 2px 6px; border-radius: 6px;
background: var(--bg-2); border: 1px solid var(--line);
}
.topbar .audio-engine .ae-btn {
background: transparent; border: none; color: var(--fg-1);
width: 22px; height: 22px; border-radius: 4px;
font-size: 11px; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center;
}
.topbar .audio-engine .ae-btn:hover { background: var(--bg-3); color: var(--accent); }
.topbar .audio-engine .ae-state {
font-size: 10px; margin-left: 4px;
width: 10px; text-align: center;
}
.topbar .audio-engine[data-state="running"] .ae-state { color: var(--ok); text-shadow: 0 0 4px var(--ok); }
.topbar .audio-engine[data-state="stopped"] .ae-state { color: var(--fg-2); }
.topbar .audio-engine[data-state="error"] .ae-state { color: var(--err); text-shadow: 0 0 4px var(--err); }
.topbar .audio-engine[data-state="running"] .ae-start { color: var(--ok); }
.topbar .audio-engine[data-state="stopped"] .ae-stop { opacity: .4; cursor: default; }
.topbar .audio-engine[data-state="running"] .ae-stop { color: var(--err); opacity: 1; cursor: pointer; }
/* sub-tabs-bar 内的 doc-tabs(搬过来,复用 stage 现有 .doc-tab 样式) */
.sub-tabs-bar .doc-tab {
display: inline-flex; align-items: center; gap: 5px;
padding: 0 10px; height: 22px;
font-size: 11px; color: var(--fg-2);
border: 1px solid transparent; border-radius: 4px 4px 0 0;
background: transparent; cursor: pointer;
white-space: nowrap;
}
.sub-tabs-bar .doc-tab:hover { background: var(--bg-2); color: var(--fg-1); }
.sub-tabs-bar .doc-tab.active {
background: var(--canvas-bg); color: var(--accent);
border-color: var(--line); font-weight: 600;
}
.sub-tabs-bar .doc-tab.dirty::after {
content: '●'; color: var(--accent); font-size: 8px; margin-left: 2px;
}
.sub-tabs-bar .doc-tab .close {
font-size: 10px; color: var(--fg-2); margin-left: 2px;
}
.sub-tabs-bar .new-tab {
width: 22px; height: 22px;
background: transparent; border: none; color: var(--fg-2);
font-size: 14px; cursor: pointer; border-radius: 4px;
}
.sub-tabs-bar .new-tab:hover { background: var(--bg-2); color: var(--accent); }
4. 新协议规范(4 个新增 + 1 个新增反向)
4.1 mode-switcher-inject (Stage → Shell)
用途:Stage 主动声明它需要在顶栏中段显示一组"主界面切换 chip"。
字段:
window.parent.postMessage({
type: 'mode-switcher-inject',
stage: STAGE,
chips: [ // 必填,数组;空数组 = 清空中段
{ key: 'smoke', icon: '🧪', label: '冒烟', tag: 'SMOKE' },
{ key: 'integration', icon: '🔗', label: '集成', tag: 'INTEG' },
{ key: 'electrical', icon: '⚡', label: '电性能', tag: 'ELEC' },
{ key: 'profile', icon: '🎯', label: 'Profile', tag: 'ACOU' },
{ key: 'realtime', icon: '📡', label: 'Live', tag: 'LIVE' }
],
activeKey: 'realtime' // 可选,默认 chips[0].key
}, '*');
Shell 渲染逻辑:清空 #modeSwitcher → forEach 创建 .mode-chip[data-key],含 chip-tag → 点击转发 mode-switch-click。
点击反向消息(Shell → Stage):
4.2 doc-tabs-inject (Stage → Shell)
用途:仅 XiLink / XiTune 调用,把 stage 内的 doc-tabs 搬到 Shell 行 2。
字段:
window.parent.postMessage({
type: 'doc-tabs-inject',
stage: STAGE,
tabs: [ // 必填,数组;空数组 = 隐藏行 2
{ key: 'main', icon: '🔗', label: 'main_chain.xilink', dirty: true },
{ key: 'subgraph_eq', icon: '📐', label: 'subgraph_eq.xilink', dirty: false },
{ key: 'multi_band', icon: '🎚', label: 'multi_band_dyn.xilink', dirty: false }
],
activeKey: 'main',
showNewTabButton: true // 可选,是否显示尾部 "+" 按钮
}, '*');
Shell 渲染逻辑:
- 若 tabs.length > 0 → 给 .ide 加 with-subtabs-bar class,渲染 #docTabsBar 内容;
- 若 tabs.length === 0 → 移除 with-subtabs-bar,清空 #docTabsBar。
反向消息:
{ type: 'doc-tab-click', stage, key: 'subgraph_eq' } // 切换
{ type: 'doc-tab-close', stage, key: 'subgraph_eq' } // 关闭按钮
{ type: 'doc-tab-new', stage } // + 按钮
4.3 engine-state-changed (Shell → 所有 Stage 广播)
用途:Shell 音频引擎状态变化时,向当前激活 stage(也可考虑广播到所有 stage)发送状态。
字段:
frame.contentWindow.postMessage({
type: 'engine-state-changed',
state: 'running', // 'running' | 'stopped' | 'restarting' | 'error'
sampleRate: 48000,
bufferSize: 256,
latencyMs: 1.3
}, '*');
Stage 接收后:可在自身 UI(如 XiTune io-bar 的 LIVE 灯、XiTest mode-bar 的 LIVE 标记)联动显示。
音频引擎按钮反向:Shell 内部按钮直接改 data-state 即可,不需要 stage 触发;但若 stage 想主动启停,可发反向消息 engine-control:
5. 各 stage 改造点对照表
| Stage | 改造动作 | 涉及代码 | doc-tabs | mode-switcher | 通用工具按钮 |
|---|---|---|---|---|---|
| XiLink | 删除 stage 内 .doc-tabs HTML(L284-302)+ 改用 doc-tabs-inject 协议;toolbar-inject 不变(5 个链路操作按钮归通用工具) |
stage-xilink.html L284-302 删除 + 启动时 postMessage | 通过协议注入 | 不需要 | 5 个保留 |
| XiTune | 同上:删除 stage 内 .doc-tabs + 改 doc-tabs-inject 协议;同时实现"doc-tab-click 反向消息 → renderMiniBar 联动"(替换原 stage 内 .doc-tab click 监听) |
stage-xitune.html 删 doc-tabs DOM + 改 message handler | 通过协议注入 | 不需要 | 5 个保留 |
| XiForge | 删除 stage 内 doc-tabs(一次只开一个 module 项目) | stage-xiforge.html | 无 | 不需要 | 现有按钮归通用工具 |
| XiTest | 删除 stage 内 doc-tabs + 删除 stage 内 mode-bar(搬到 Shell 顶栏中段) + 改 mode-switcher-inject 协议 + mode-switch-click 反向消息处理 → renderMode |
stage-xitest.html 删 L800-880 mode-bar HTML + 改 message handler | 无 | 通过协议注入 5 chip | 5 个保留 |
6. Shell 改造点(index.html)
6.1 DOM 修改
| 行号 | 动作 | 内容 |
|---|---|---|
| L1063 后 | 新增 | <div class="mode-switcher" id="modeSwitcher"></div> |
| L1066 | 保持 | <div class="toolbar-group" id="stageToolbar">(不动) |
| L1066 后 | 新增 | <div class="audio-engine" id="audioEngine" data-state="stopped">…</div> |
| L1077 后 | 新增 | <div class="sub-tabs-bar" id="docTabsBar"></div> |
6.2 JS 修改
| 函数 / 位置 | 动作 |
|---|---|
switchStage() L2141-2161 |
追加 清空逻辑:document.getElementById('modeSwitcher').innerHTML = ''; document.getElementById('docTabsBar').innerHTML = ''; document.querySelector('.ide').classList.remove('with-subtabs-bar'); |
| message handler L2210-2245 | 新增 case 'mode-switcher-inject' → injectModeSwitcher(msg) 'doc-tabs-inject' → injectDocTabs(msg) 'engine-control' → handleEngineControl(msg) |
| 新增函数 | injectModeSwitcher(msg) 渲染 mode-chip + 点击转发 mode-switch-click injectDocTabs(msg) 渲染 sub-tabs-bar + 切换 with-subtabs-bar class + 点击转发 doc-tab-click/close/new setEngineState(state) 改 #audioEngine[data-state] + 广播 engine-state-changed handleEngineControl(msg) 接收 stage 主动控制(可选) |
| 启动时绑定 | #aeStart #aeStop #aeRestart 的 click → 调 setEngineState(demo 直接切状态 + 广播) |
7. 风险与回滚点
7.1 主要风险
.ide.with-subtabs-bar切换时机:必须在switchStage()切 iframe.src 之前就移除 class,避免视觉抖动;新 stage 加载后通过doc-tabs-inject重新加 class。- mode-switcher 闪烁:stage iframe 加载到 stage-ready 之间会有短暂空窗,中段会闪一下"空"。可接受(XiTest 切到 XiLink 等场景)。
- 音频引擎状态广播:当 stage iframe 还未 ready 时,Shell 不应发送
engine-state-changed。建议在 stage 发出stage-ready后再广播一次当前 state("晚加入也能拿到状态")。 - doc-tabs 协议双向状态同步:stage 内部维护"当前 active doc",Shell 渲染成"active class",必须保证两边一致。建议 stage 在收到
doc-tab-click后,再发一次doc-tabs-inject更新 activeKey(避免双源 state)。 - XiForge / XiTest 中 stage 内被删除的 doc-tabs DOM 残留 CSS 必须一并删除,否则未使用样式留在文件里。
7.2 回滚点
- 每个改动文件都保留 v3.x 的注释签名(如 stage-xitest 当前
v3.0.0→ 改后v4.0.0),出问题可 git diff 回滚。 - 新协议是追加式(
mode-switcher-inject/doc-tabs-inject/engine-state-changed全是新 type),不破坏 v1.2.2 的 9 种现有协议。
8. 改造提交顺序(一次到位但内部步骤)
按依赖关系:
1. Shell index.html(基础设施先行):DOM 三段式 + CSS + 5 个新函数(injectModeSwitcher / injectDocTabs / setEngineState / handleEngineControl + switchStage 扩展清理)
2. stage-xitest.html(验证 mode-switcher 协议):删 doc-tabs + 删 mode-bar HTML/CSS + 启动时发 mode-switcher-inject + 处理 mode-switch-click 反向消息
3. stage-xiforge.html(验证 XF/XS 删 doc-tabs 路径):删 doc-tabs HTML/CSS(不发 doc-tabs-inject 协议)
4. stage-xilink.html(验证 doc-tabs 协议):删 stage 内 doc-tabs HTML/CSS + 启动时发 doc-tabs-inject + 处理 doc-tab-click / doc-tab-close / doc-tab-new 反向
5. stage-xitune.html(同 XiLink + 联动 mini-bar):处理 doc-tab-click 后 renderMiniBar(key)
9. 验收 checklist(用户可逐项核对)
启动 index.html,依次:
- [ ] 默认进入 XiLink,顶栏行 2 显示 3 个 doc-tabs(main/subgraph_eq/multi_band),玫瑰金小三角指向 XiLink Pill
- [ ] 切到 XiForge:顶栏回到单行 44px(无行 2),中段 mode-switcher 为空,右段 #stageToolbar 显示 XiForge 自己的按钮组
- [ ] 切到 XiTune:顶栏回到双行,行 2 显示 XiTune 的 3 个子图 doc-tabs;切 doc-tab 时主区 mini-bar 联动
- [ ] 切到 XiTest:顶栏单行 44px(无行 2),中段显示 5 个 mode-chip(SMOKE/INTEG/ELEC/ACOU/LIVE),默认 LIVE active;点击不同 chip 切换主区
- [ ] 顶栏右段稳定显示音频引擎组 ▶/■/🔄/●:点 ▶ 状态变 running(绿灯),■ 变 stopped(灰灯),🔄 短暂 restarting 后回 running
- [ ] 音频引擎状态变化时,XiTune 的 io-bar LIVE 灯 / XiTest 的 mode-bar LIVE tag 跟随变化(联动通过 engine-state-changed 协议)
- [ ] Tuning Dialog 多实例不受影响(双击 stage 内某节点仍能弹浮窗)
- [ ] 控制台无报错;4 stage 切换无视觉抖动
10. 待用户确认的设计细节(动手前最后确认)
- 音频引擎状态广播范围:仅当前激活 stage?还是 4 个 stage 都广播?(推荐仅激活 stage,省消息)
- doc-tab-close / doc-tab-new 是否需要本轮实现?还是先只做 click 切换,close/new 留 v4.1?(推荐 close/new 本轮也实现,但仅前端 UI 联动,不做后端持久化)
- 音频引擎"重启"
🔄的视觉表达:纯按钮即时执行?还是带restarting中间态(按钮转圈 1 秒后自动 running)?(推荐带中间态,演示更真实) - 顶栏右段在屏幕较窄时的折叠策略:是否需要把音频引擎组 + 浮窗/锁/主题塞进溢出菜单?(本轮先不做溢出处理,后续 v4.1 再做)
- mode-switcher 视觉风格:用我设计的 chip 风格(borderless + active 实心 accent)?还是借用现有
.main-tab的 Pill 风格保持一致性?(推荐独立 chip 风格,与 stage 切换器视觉区分)
评审通过后,我将一气改 5 个文件提交。预计代码净修改 ~900 行。