跳转至
DRAFT

XiStudio v3 · 前端实现规格 (Implementation Spec)

本文档配套 v3-unified-macos.html demo。 目标读者:前端智能体(Cline / Cursor / 其他 AI 编码助手)。 现有代码基线:AlgoDepartment/04_development/frontend_vue3/(Vue 3 + Vite + Pinia + ECharts + TypeScript,42 个 .vue 组件已沉淀)。 首要原则:在大框架不变的前提下融合 macOS 设计风格,复用现有组件,不推倒重做


0. 阅读顺序

按以下顺序逐章实现,每章可独立交付(每个一级 § 是一个 PR 单位):

§ 模块 输入文件 产出 估算工时
1 设计 token + 主题系统 v3-unified-macos.html (CSS) src/styles/tokens.css + useTheme.ts 1d
2 macOS 全局样式 demo CSS src/styles/macos.css 0.5d
3 顶部双行(菜单+工具栏) demo + Toolbar.vue MenuBar.vue + ToolBar.vue 重构 1d
4 主 tab + 子 tab + License demo JS (TAB_CONFIG) MainTabs.vue + SubTabs.vue + useLicense.ts 1.5d
5 三栏主体(左/中/右) LeftPanel.vue WorkspaceShell.vue + LeftSidebar.vue + RightInspector.vue 2d
6 底部面板 BottomPropertyPanel.vue BottomPanel.vue(重构) 0.5d
7 Tuning 多开浮窗 现有 12 个 *TuningDialog.vue useTuningManager.ts + 浮窗管理 1.5d
8 辅助产品悬浮窗 demo aux-window AuxWindow.vue + useAuxWindow.ts 1d
9 License 隔离 + 路由 demo TAB_CONFIG router/index.ts + 4 个 tab 模块的懒加载 1d

总工时估算:~10 人日(两个前端工程师 1 周)。


1. 设计 Token + 主题系统

1.1 文件位置

src/
├── styles/
│   ├── tokens.css           ← 三色法典 + macOS 变量(主题无关)
│   ├── themes.css           ← 6 套主题(A-F)的 CSS 变量定义
│   └── macos.css            ← macOS 全局样式(滚动条/焦点环/按钮按下)
├── composables/
│   └── useTheme.ts          ← ThemeEngine 的 Vue 组合式封装
└── stores/
    └── theme.ts             ← Pinia store(localStorage 持久化)

1.2 tokens.css(不可改 hex 的法典 + macOS 风格变量)

直接复制 v3-unified-macos.html 第 14–46 行 :root {} 块。禁止修改任何 --yin / --xi / --sheng / --night-* / --pearl / --paper / --L0~L5 / --mac-traffic-* 的 hex 值,这些是 brand-color-system v1.2 法典。

新增的 macOS 变量必须保留:

/* macOS 风格变量(主题无关) */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
--mac-shadow-sm: 0 1px 2px rgba(0,0,0,.08), 0 1px 1px rgba(0,0,0,.04);
--mac-shadow-md: 0 4px 16px rgba(0,0,0,.12), 0 1px 4px rgba(0,0,0,.08);
--mac-shadow-lg: 0 12px 48px rgba(0,0,0,.24), 0 4px 16px rgba(0,0,0,.16);
--mac-traffic-red: #FF5F57;
--mac-traffic-yellow: #FEBC2E;
--mac-traffic-green: #28C840;

1.3 themes.css(6 套主题完整 token)

直接复制 demo 第 51–217 行的 6 个 [data-theme="X"] {} 块(A 玫瑰金/B 极光青/C 米纸/D 极简白/E 赛博紫黑/F 毛玻璃)。

关键变量(每个主题必须定义):

类别 变量名 用途
背景 --bg-0/1/2/3 4 级背景层
前景 --fg-0/1/2 3 级文字(主/次/弱)
边线 --line / --line-soft 普通/弱边框
强调 --accent / --accent-2 / --accent-fg 主题强调色
状态 --info / --ok / --warn / --err 4 种状态色
阴影 --shadow 主阴影
画布 --canvas-bg / --canvas-grid-color 流图画布
节点 --node-bg / --node-border 流图节点
按钮 --primary-btn-bg / --primary-btn-fg 主按钮
全屏 --hero-grad 全屏背景渐变
面板 --panel-bg / --bottom-bg / --statusbar-bg / --menubar-bg / --toolbar-bg 5 种面板背景
毛玻璃 --frosted backdrop-filter 值(仅主题 F 不为 none)
焦点 --focus-ring 品牌色焦点环

1.4 useTheme.ts(Vue 3 组合式 API)

// src/composables/useTheme.ts
import { ref, watch, onMounted } from 'vue'

export type ThemeId = 'A' | 'B' | 'C' | 'D' | 'E' | 'F'

export interface ThemeMeta {
  id: ThemeId
  name: string
  sub: string
  group: '品牌组' | '审美组'
}

export const THEMES: ThemeMeta[] = [
  { id: 'A', name: '玫瑰金主导', sub: 'Yin Gold · 品牌默认', group: '品牌组' },
  { id: 'B', name: '极光青主导', sub: 'Sheng Cyan · 工程师风', group: '品牌组' },
  { id: 'C', name: '米纸浅色',   sub: 'Paper Light · 白天模式', group: '品牌组' },
  { id: 'D', name: '极简白',     sub: 'Linear / Notion 风', group: '审美组' },
  { id: 'E', name: '赛博紫黑',   sub: 'Cyberpunk DAW', group: '审美组' },
  { id: 'F', name: '毛玻璃',     sub: 'Vision OS', group: '审美组' },
]

const STORAGE_KEY = 'xistudio.v3.preferences.v1'

export function useTheme() {
  const theme = ref<ThemeId>('A')
  const fontScale = ref(100)
  const density = ref(100)

  function load() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
      if (saved.theme) theme.value = saved.theme
      if (saved.fontScale) fontScale.value = saved.fontScale
      if (saved.density) density.value = saved.density
    } catch {}
  }

  function save() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({
      theme: theme.value,
      fontScale: fontScale.value,
      density: density.value,
    }))
  }

  function apply() {
    document.body.setAttribute('data-theme', theme.value)
    document.documentElement.style.setProperty('--font-scale', String(fontScale.value / 100))
    document.documentElement.style.setProperty('--density', String(density.value / 100))
  }

  function setTheme(id: ThemeId) { theme.value = id }

  watch([theme, fontScale, density], () => { save(); apply() }, { immediate: false })

  onMounted(() => { load(); apply() })

  return { theme, fontScale, density, THEMES, setTheme }
}

1.5 stores/theme.ts(Pinia 替代方案,团队偏好哪个用哪个)

// src/stores/theme.ts
import { defineStore } from 'pinia'

export const useThemeStore = defineStore('theme', {
  state: () => ({
    theme: 'A' as 'A'|'B'|'C'|'D'|'E'|'F',
    fontScale: 100,
    density: 100,
  }),
  actions: {
    setTheme(id: typeof this.theme) {
      this.theme = id
      this.persist()
      this.apply()
    },
    apply() {
      document.body.setAttribute('data-theme', this.theme)
      document.documentElement.style.setProperty('--font-scale', String(this.fontScale / 100))
      document.documentElement.style.setProperty('--density', String(this.density / 100))
    },
    persist() {
      localStorage.setItem('xistudio.v3.preferences.v1', JSON.stringify(this.$state))
    },
    hydrate() {
      try {
        const saved = JSON.parse(localStorage.getItem('xistudio.v3.preferences.v1') || '{}')
        Object.assign(this.$state, saved)
      } catch {}
      this.apply()
    },
  },
})

main.ts 启动时调用useThemeStore().hydrate()


2. macOS 全局样式

2.1 macos.css

复制 demo 的以下片段到 src/styles/macos.css

  • 字体栈(demo L228–229):-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", ...
  • 滚动条(demo L242–246):细化 8px + hover 才显示
  • 焦点环(demo L249–253):*:focus-visiblevar(--focus-ring) 而非系统蓝
  • 按钮按下缩放(demo L257–262):button:active { transform: scale(0.97); }
  • 红黄绿 traffic-light 三色按钮样式(demo L304–323):所有窗口/对话框头部复用

2.2 在 main.ts 中按顺序引入

import './styles/tokens.css'
import './styles/themes.css'
import './styles/macos.css'

3. 顶部双行(菜单 + 工具栏)

3.1 组件树

src/components/shell/
├── MenuBar.vue          ← 第 1 行(28px):traffic-lights + logo + 9 菜单项 + 右侧 tag/demo-link
└── ToolBar.vue          ← 第 2 行(44px):3-4 个 toolbar-group + 搜索框

3.2 MenuBar.vue 设计

Props: 无(顶级组件)。

Emits: - menu-click(menu: 'file'|'edit'|'view'|'project'|'build'|'debug'|'tools'|'window'|'help') - theme-trigger-click() - traffic-click(color: 'red'|'yellow'|'green')

Template 骨架(参考 demo L1057–L1085): - 左:.traffic-lights 三色按钮(点击 emit traffic-click,预留窗口控制接口) - Logo ◆ XiStudio--accent 色 - 9 个 .menu-item(文件/编辑/视图/项目/构建/调试/工具/窗口/帮助) - 右:AI 状态 tag + 当前主题 tag + 🎨 切换 按钮 + v2 demo 链接(生产环境删除)

关键样式:背景 var(--menubar-bg) + backdrop-filter: var(--frosted),毛玻璃必备。

3.3 ToolBar.vue 设计(重构现有 Toolbar.vue

Props:

interface Props {
  isCompiling?: boolean
  isRunning?: boolean
  currentMode?: 'edit' | 'tune' | 'test' | 'deploy'
}

Emits: - compile() / run() / stop() / flash() - undo() / redo() - mode-change(mode: 'tune'|'test'|'deploy') - cmd-palette-open()(搜索框 focus 时触发)

4 个 .toolbar-group(demo L1090–L1116):

Group 按钮 图标 Emit
1 文件 新建/打开/保存 📄📂💾 new/open/save
2 构建 编译(primary)/运行/停止/烧录 ⚙▶■⚡ compile/run/stop/flash
3 编辑 撤销/重做 ↶↷ undo/redo
4 模式 调音/测试/部署 🎚🔍🚀 mode-change

右侧搜索框:.toolbar-search + 显示 ⌘K 提示,onfocus 触发命令面板。


4. 主 Tab + 子 Tab + License 隔离

4.1 路由设计(vue-router v4)

// src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'

const routes = [
  { path: '/', redirect: '/xistudio' },
  {
    path: '/xistudio',
    component: () => import('@/views/xistudio/XiStudioWorkspace.vue'),
    meta: { license: 'studio-pro', label: 'XiStudio (L4)', icon: '🎛' },
    children: [
      { path: '', redirect: 'flow' },
      { path: 'flow',   component: () => import('@/views/xistudio/sub/FlowView.vue') },
      { path: 'tune',   component: () => import('@/views/xistudio/sub/TuneView.vue') },
      { path: 'sim',    component: () => import('@/views/xistudio/sub/SimView.vue') },
      { path: 'deploy', component: () => import('@/views/xistudio/sub/DeployView.vue') },
    ],
  },
  {
    path: '/xiforge',
    component: () => import('@/views/xiforge/XiForgeWorkspace.vue'),
    meta: { license: 'forge-pro', label: 'XiForge (L4)', icon: '🔨' },
    children: [
      { path: '', redirect: 'design' },
      { path: 'design', component: () => import('@/views/xiforge/sub/DesignView.vue') },
      { path: 'source', component: () => import('@/views/xiforge/sub/SourceView.vue') },
      { path: 'sim',    component: () => import('@/views/xiforge/sub/SimView.vue') },
    ],
  },
  {
    path: '/xitune',
    component: () => import('@/views/xitune/XiTuneWorkspace.vue'),
    meta: { license: 'tune-basic', label: 'XiTune (L2)', icon: '🎚' },
    children: [
      { path: '', redirect: 'chain' },
      { path: 'chain', component: () => import('@/views/xitune/sub/ChainView.vue') },
      { path: 'curve', component: () => import('@/views/xitune/sub/CurveView.vue') },
      { path: 'abx',   component: () => import('@/views/xitune/sub/ABXView.vue') },
    ],
  },
  {
    path: '/xiprobe',
    component: () => import('@/views/xiprobe/XiProbeWorkspace.vue'),
    meta: { license: 'probe-basic', label: 'XiProbe (L2)', icon: '📡' },
    children: [
      { path: '', redirect: 'freq' },
      { path: 'freq',  component: () => import('@/views/xiprobe/sub/FreqView.vue') },
      { path: 'thd',   component: () => import('@/views/xiprobe/sub/THDView.vue') },
      { path: 'snr',   component: () => import('@/views/xiprobe/sub/SNRView.vue') },
      { path: 'batch', component: () => import('@/views/xiprobe/sub/BatchView.vue') },
    ],
  },
]

const router = createRouter({ history: createWebHashHistory(), routes })

// License 路由守卫
router.beforeEach((to, from, next) => {
  const lic = useLicenseStore()
  const required = to.meta.license as string | undefined
  if (required && !lic.has(required)) {
    showToast(`License "${required}" 未授权,无法进入 ${to.meta.label}`)
    return next(false)
  }
  next()
})

export default router

关键点: - 每个主 tab 是一个异步加载的 chunk() => import(...)),未授权用户根本不下载该 tab 的 JS(满足"代码架构上做隔离"要求)。 - VST 开发场景 = 用户 license 仅含 forge-pro,启动后只能进入 /xiforge。 - 用 meta.license 而不是硬编码,方便后端动态下发。

4.2 useLicense composable / stores/license.ts

// src/stores/license.ts
import { defineStore } from 'pinia'

export type LicenseTier =
  | 'studio-pro' | 'forge-pro' | 'tune-basic' | 'probe-basic'
  | 'xicloud-enterprise' | 'ximind-pro'

export const useLicenseStore = defineStore('license', {
  state: () => ({
    tiers: [] as LicenseTier[],
    expiresAt: null as Date | null,
  }),
  getters: {
    has: (s) => (tier: LicenseTier) => s.tiers.includes(tier),
    enabledTabs: (s): Array<'xistudio'|'xiforge'|'xitune'|'xiprobe'> => {
      const out: any[] = []
      if (s.tiers.includes('studio-pro')) out.push('xistudio')
      if (s.tiers.includes('forge-pro')) out.push('xiforge')
      if (s.tiers.includes('tune-basic')) out.push('xitune')
      if (s.tiers.includes('probe-basic')) out.push('xiprobe')
      return out
    },
  },
  actions: {
    async fetch() {
      const res = await fetch('/api/license')
      const data = await res.json()
      this.tiers = data.tiers
      this.expiresAt = new Date(data.expires_at)
    },
  },
})

4.3 MainTabs.vue(demo 第 1140–1158 行)

Template:v-for 渲染路由配置(含 meta.license / meta.label / meta.icon),过滤未授权 tab 显示为 disabled 灰显态。

响应式状态:通过 useRoute() 取当前路径前缀决定 active。点击调用 router.push('/' + tabId)

4.4 SubTabs.vue(demo 第 1163–1166 行 + JS L1551)

Template:v-for 渲染当前路由的 children(去掉 redirect 项),每个子 tab 是一个 <router-link><button @click="router.push(child.path)">

关键样式

.sub-tab.active {
  color: var(--accent);
  border-bottom: 2px solid var(--tab-active-line);
  font-weight: 500;
}


5. 三栏主体(左 + 中 + 右)

5.1 总壳 WorkspaceShell.vue

每个 tab 的 *Workspace.vue(4 个)都使用同一个 WorkspaceShell.vue 作为骨架,通过 slot 注入差异内容:

<!-- src/components/shell/WorkspaceShell.vue -->
<template>
  <div class="workspace-shell">
    <slot name="left" />     <!-- 左侧栏 240px -->
    <slot name="center" />   <!-- 中央画布 1fr -->
    <slot name="right" />    <!-- 右侧栏 320px -->
  </div>
</template>

<style scoped>
.workspace-shell {
  display: grid;
  grid-template-columns: 240px 1fr 320px;
  grid-template-rows: 1fr;
  height: 100%;
  /* 注意: WorkspaceShell 自己 height:100%, 由父级 body-wrap 用 absolute 定位让出底部 220px */
}
@media (max-width: 1280px) {
  .workspace-shell { grid-template-columns: 220px 1fr 280px; }
}
</style>

5.2 LeftSidebar.vue(macOS Sidebar 风)

重构现有 LeftPanel.vue 的方式:保留现有 5 tab 的内部组件(ProjectPanel / ModuleLibraryPanel / ConnectionPanel / ProfileSidebar / AudioEnginePanel),但外壳重写为 macOS 风格 sidebar。

新组件结构:

src/components/shell/LeftSidebar.vue                  ← 新外壳
├── (复用) ProjectPanel.vue                            ← 工程 tab 内容
├── (复用) ModuleLibraryPanel.vue                      ← IP库 tab 内容
├── (复用) ConnectionPanel.vue                         ← 连接 tab 内容
├── (复用) ProfileSidebar.vue                          ← 音效 tab 内容
└── (复用) AudioEnginePanel.vue                        ← 引擎 tab 内容

Props:

interface Props {
  tabs: Array<{ id: string; label: string; icon: string }>
  modelValue: string  // 当前 active tab id
}

Emits: update:modelValue(id)

Template 骨架(参考 demo L1188–L1197):

<template>
  <div class="panel-left">
    <div class="sidebar-tabs">
      <div v-for="t in tabs" :key="t.id"
           :class="['sidebar-tab', { active: modelValue === t.id }]"
           @click="$emit('update:modelValue', t.id)">
        <span class="ico">{{ t.icon }}</span>{{ t.label }}
      </div>
    </div>
    <div class="sidebar-body">
      <slot :active="modelValue" />
    </div>
  </div>
</template>

用法(XiStudio Workspace 内)

<LeftSidebar
  :tabs="[
    { id: 'proj',    label: '工程',   icon: '📁' },
    { id: 'lib',     label: 'IP库',   icon: '🧩' },
    { id: 'conn',    label: '连接',   icon: '🔌' },
    { id: 'profile', label: '音效',   icon: '🎵' },
    { id: 'engine', label: '引擎',   icon: '⚙' },
  ]"
  v-model="leftTab"
>
  <template #default="{ active }">
    <ProjectPanel       v-show="active === 'proj'" />
    <ModuleLibraryPanel v-show="active === 'lib'" />
    <ConnectionPanel    v-show="active === 'conn'" />
    <ProfileSidebar     v-show="active === 'profile'" />
    <AudioEnginePanel   v-show="active === 'engine'" />
  </template>
</LeftSidebar>

5.3 RightInspector.vue

结构同 LeftSidebar.vue,但 tab 数量与内容随主 tab 而异:

主 tab Right tabs
XiStudio 仿真 📈 / 检视 🔍 / A/B 🅰🅱 / 辅助 ⚙
XiForge 布局 📐 / 样式 🎨 / 绑定 🔗 / 事件 ⚡
XiTune 频响 📈 / RTA 📊 / A/B 🅰🅱
XiProbe 结果 📊 / 信号源 🌊 / 辅助 ⚙

仿真示波器卡片:使用 ECharts 实例(项目已有依赖),不要继续用 demo 的静态 SVG。组件 ScopeCard.vue

<!-- ScopeCard.vue -->
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import * as echarts from 'echarts/core'

const props = defineProps<{
  title: string
  badge?: string
  type: 'freq' | 'rta' | 'wave' | 'phase'
  data: number[]
}>()

const chartEl = ref<HTMLElement>()
let chart: echarts.ECharts | null = null

function makeOption() {
  return {
    grid: { left: 4, right: 4, top: 6, bottom: 6 },
    xAxis: { show: false, type: 'category', data: props.data.map((_, i) => i) },
    yAxis: { show: false, type: 'value' },
    series: [{ type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 1.4 }, data: props.data }],
    color: [getComputedStyle(document.body).getPropertyValue('--accent').trim()],
  }
}

onMounted(() => {
  chart = echarts.init(chartEl.value!)
  chart.setOption(makeOption())
})

watch(() => props.data, () => chart?.setOption(makeOption()))
</script>

<template>
  <div class="scope-card">
    <div class="cap"><span>{{ title }}</span><span class="badge">{{ badge }}</span></div>
    <div ref="chartEl" style="width:100%;height:78px"></div>
  </div>
</template>

5.4 中央画布

每个 tab 的中央画布是完全独立的组件,不共享:

Tab 中央组件 复用现有
XiStudio FlowCanvas.vue(流图) 复用 LinkEditor.vue + WorkspacePanel.vue
XiForge UIDesignerCanvas.vue(UI 设计器) 新写,参考 v2-xiforge.html
XiTune TuningChainCanvas.vue(调音链) 新写,调用 *TuningDialog 系列
XiProbe ProbeTestCanvas.vue(测试图表 + ECharts) 新写

6. 底部面板

6.1 BottomPanel.vue(重构 BottomPropertyPanel.vue

关键变化:从原来的 view-pane 内子组件 → 跨 tab 共享的全局组件absolute 定位在 body-wrap 底部 220px(参考 demo CSS L668–L678)。

Tab 列表(demo L1517–L1525): - 📋 Module 属性(默认) - ⚙ 编译 / 📜 日志 / ⚠ 问题 / $ 终端 / 🤖 XiMind(共用 LogPanel.vue) - { } 生成代码(仅 XiForge tab 显示,复用 id="codeTab" 逻辑) - 🪟 Tuning 托盘按钮(右侧 tray-btn)

Props:

interface Props {
  selectedNode?: NodeRef  // 来自 stores/workspace
  showCodeTab?: boolean   // computed: route.path.startsWith('/xiforge')
}

Emits: tray-toggle()


7. Tuning 多开浮窗(核心交互)

7.1 组件树

src/composables/useTuningManager.ts    ← Map<id, TuningInstance> 全局管理
src/components/tuning/
├── TuningContainer.vue                 ← 全局 mounted 在 App.vue,渲染所有打开的浮窗
├── TuningDialogShell.vue               ← macOS 风格外壳(traffic-lights + 标题栏 + 拖拽)
└── (复用现有 12 个) *TuningDialog.vue   ← 内容(GainTuningDialog / GEQTuningDialog 等)

7.2 useTuningManager.ts

// src/composables/useTuningManager.ts
import { reactive, markRaw } from 'vue'
import type { Component } from 'vue'

export interface TuningInstance {
  id: string                 // 唯一 ID, e.g. 'PEQ#1'
  title: string              // 显示名, e.g. 'PEQ × 8 段'
  component: Component       // 内部内容组件
  props?: Record<string, any>
  x: number; y: number
  zIndex: number
  minimized: boolean
}

const tunings = reactive(new Map<string, TuningInstance>())
let zCounter = 50

export function useTuningManager() {
  function open(id: string, title: string, component: Component, props?: any, fromEl?: HTMLElement) {
    if (tunings.has(id)) {
      const t = tunings.get(id)!
      t.minimized = false
      t.zIndex = ++zCounter
      return
    }
    const offset = tunings.size * 30
    let x = 380 + offset, y = 180 + offset
    if (fromEl) {
      const r = fromEl.getBoundingClientRect()
      x = Math.min(r.right + 20, window.innerWidth - 340)
      y = Math.max(r.top - 10, 130)
    }
    tunings.set(id, {
      id, title, component: markRaw(component), props,
      x, y, zIndex: ++zCounter, minimized: false,
    })
  }

  function close(id: string) { tunings.delete(id) }

  function minimize(id: string) {
    const t = tunings.get(id); if (t) t.minimized = true
  }

  function show(id: string) {
    const t = tunings.get(id); if (t) { t.minimized = false; t.zIndex = ++zCounter }
  }

  function bringToFront(id: string) {
    const t = tunings.get(id); if (t) t.zIndex = ++zCounter
  }

  function closeAll() { tunings.clear() }

  return { tunings, open, close, minimize, show, bringToFront, closeAll }
}

7.3 TuningDialogShell.vue(macOS 外壳)

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps<{
  id: string; title: string
  x: number; y: number; zIndex: number
}>()
const emit = defineEmits<{
  close: []
  minimize: []
  'update:position': [x: number, y: number]
  'bring-to-front': []
}>()

const dialogEl = ref<HTMLElement>()

function startDrag(e: MouseEvent) {
  if ((e.target as HTMLElement).tagName === 'BUTTON') return
  if ((e.target as HTMLElement).classList.contains('traffic')) return
  emit('bring-to-front')
  const r = dialogEl.value!.getBoundingClientRect()
  const ox = e.clientX - r.left, oy = e.clientY - r.top
  document.body.style.userSelect = 'none'

  function move(ev: MouseEvent) {
    const x = Math.max(0, Math.min(window.innerWidth - 100, ev.clientX - ox))
    const y = Math.max(28, Math.min(window.innerHeight - 60, ev.clientY - oy))
    emit('update:position', x, y)
  }
  function up() {
    document.removeEventListener('mousemove', move)
    document.removeEventListener('mouseup', up)
    document.body.style.userSelect = ''
  }
  document.addEventListener('mousemove', move)
  document.addEventListener('mouseup', up)
}
</script>

<template>
  <div ref="dialogEl" class="tuning-dialog"
       :style="{ left: x + 'px', top: y + 'px', zIndex }">
    <div class="td-head" @mousedown="startDrag">
      <div class="traffic-lights">
        <div class="traffic red" @click="emit('close')"></div>
        <div class="traffic yellow" @click="emit('minimize')"></div>
        <div class="traffic green"></div>
      </div>
      <span class="td-title">🎚 {{ id }} · {{ title }}</span>
      <button title="最小化" @click="emit('minimize')"></button>
      <button title="关闭" @click="emit('close')">×</button>
    </div>
    <div class="td-body"><slot /></div>
  </div>
</template>

7.4 TuningContainer.vue(全局浮窗渲染层)

<script setup lang="ts">
import { useTuningManager } from '@/composables/useTuningManager'
import TuningDialogShell from './TuningDialogShell.vue'

const { tunings, close, minimize, bringToFront } = useTuningManager()

function updatePos(id: string, x: number, y: number) {
  const t = tunings.get(id); if (t) { t.x = x; t.y = y }
}
</script>

<template>
  <Teleport to="body">
    <TuningDialogShell
      v-for="[id, t] in tunings"
      :key="id"
      v-show="!t.minimized"
      :id="t.id" :title="t.title"
      :x="t.x" :y="t.y" :z-index="t.zIndex"
      @close="close(id)"
      @minimize="minimize(id)"
      @update:position="(x, y) => updatePos(id, x, y)"
      @bring-to-front="bringToFront(id)"
    >
      <component :is="t.component" v-bind="t.props" />
    </TuningDialogShell>
  </Teleport>
</template>

挂载位置:在 App.vue 根节点最后一层,紧邻 <RouterView /> 之后。

7.5 触发示例(XiStudio 流图节点双击打开)

<script setup lang="ts">
import { useTuningManager } from '@/composables/useTuningManager'
import GEQTuningDialog from '@/components/GEQTuningDialog.vue'

const { open } = useTuningManager()
function onNodeDblClick(node: { id: string; type: string }, ev: MouseEvent) {
  if (node.type === 'GEQ') open(node.id, 'GEQ × 31', GEQTuningDialog, { nodeId: node.id }, ev.target as HTMLElement)
}
</script>

7.6 Tuning 托盘 + 计数

BottomPanel.vue 右侧 tray-btn:

<button class="tray-btn" @click="trayOpen = !trayOpen">
  🪟 Tuning <span class="count">{{ tunings.size }}</span>
</button>

托盘弹出层渲染 tunings.values(),每行支持:显示/最小化/关闭。


8. 辅助产品悬浮窗

8.1 组件设计

辅助产品(XiCal/XiMic/XiBox/XiAmp/XiDSP/XiCore)= 小型可拖拽悬浮窗口,与 Tuning 浮窗共享拖拽/traffic-lights 模式。

src/composables/useAuxWindow.ts
src/components/aux/
├── AuxWindowShell.vue            ← macOS 外壳
├── XiCalWindow.vue               ← 麦克风校准
├── XiMicWindow.vue               ← 麦克风测试
├── XiBoxWindow.vue               ← 测试夹具
└── (按需扩展)

与 Tuning 的差别: - aux-window 是轻量级辅助(设备控制 + 状态),不像 Tuning 那样有大量参数滑块 - 默认位置在屏幕右下角而不是中央 - 不进入 Tuning 托盘(独立管理)

8.2 useAuxWindow.ts

import { reactive, markRaw } from 'vue'
import type { Component } from 'vue'

const auxWindows = reactive(new Map<string, {
  id: string; title: string; component: Component
  x: number; y: number; zIndex: number; open: boolean
}>())

let z = 150
export function useAuxWindow() {
  function open(id: string, title: string, comp: Component) {
    if (!auxWindows.has(id)) {
      auxWindows.set(id, {
        id, title, component: markRaw(comp),
        x: window.innerWidth - 300, y: window.innerHeight - 280,
        zIndex: ++z, open: true,
      })
    } else {
      const w = auxWindows.get(id)!; w.open = true; w.zIndex = ++z
    }
  }
  function close(id: string) {
    const w = auxWindows.get(id); if (w) w.open = false
  }
  return { auxWindows, open, close }
}

9. 命令面板 + 全局快捷键

9.1 文件位置

src/composables/useCommandPalette.ts
src/components/global/
├── CommandPalette.vue
└── ShortcutHandler.vue       ← 挂在 App.vue,无 UI,只绑定 keydown

9.2 命令注册表

// src/composables/useCommandPalette.ts
import { reactive } from 'vue'

export interface Command {
  id: string
  cat: '导航' | '主题' | '调音' | '辅助' | '构建' | '文件'
  name: string
  kbd?: string
  action: () => void
}

const commands = reactive<Command[]>([])

export function registerCommand(cmd: Command) { commands.push(cmd) }
export function useCommands() { return commands }

主 tab 切换、主题切换、调音对话框、辅助窗口、编译/运行全部通过 registerCommand 注册(demo L1859–L1875 是完整范例)。

9.3 全局快捷键映射

快捷键 行为
Ctrl+1 / Ctrl+2 / Ctrl+3 / Ctrl+4 切换主 tab(XiStudio / XiForge / XiTune / XiProbe)
Ctrl+K T 打开主题选择 popover
Ctrl+Shift+P 打开命令面板
Ctrl+, 打开设置(P2 实现)
F7 编译
F5 运行仿真
Shift+F5 停止
Ctrl+S 保存当前文件
Esc 关闭所有浮层(命令面板/popover/aux)

10. 与现有 frontend_vue3 组件的映射

按行动分 3 类:

10.1 直接复用(不改)— 24 个

AtmosEngineTuningDialog / AudioEnginePanel / ChannelMapTuningDialog / CommonEQTuningDialog / ConnectionPanel / DelayTuningDialog / GainTuningDialog / GenericTuningDialog / GEQTuningDialog / LinkEditor / LogPanel / MDRCTuningDialog / MixerTuningDialog / ModuleLibraryPanel / ModuleTuningDialog / PresetPanel / ProfileSidebar / SinkTuningDialog / SourceDeviceDialog / SourceTuningDialog / ThirdPartyModuleDialog / VirtualBassTuningDialog / WorkspacePanel / LimiterTuningDialog

注意:所有 *TuningDialog.vue 现在改为只渲染内容,不再包含自己的标题栏 / 关闭按钮 / 拖拽 —— 这部分由新增的 TuningDialogShell.vue 统一接管。如果现有组件已经包含标题栏,需要剥离(约 1h/组件 × 12 个 = 1.5d 工作量)。

10.2 重写(新外壳,复用内部内容)— 5 个

旧组件 新组件 改造点
LeftPanel.vue LeftSidebar.vue macOS Sidebar 风外壳,内容用 slot 注入
Toolbar.vue ToolBar.vue 4 个 toolbar-group 圆角组合 + 搜索框
BottomPropertyPanel.vue BottomPanel.vue 跨 tab 共享 + 7 tab + 代码生成 pane
FloatingPropertyPanel.vue TuningDialogShell.vue 升级为通用浮窗外壳,traffic-lights
PropertyPanel.vue (并入 RightInspector.vue slot 之一) Inspector 只是其中一个 right-tab

10.3 新增 — 16 个

src/components/shell/
├── MenuBar.vue
├── ToolBar.vue                       (重构)
├── MainTabs.vue
├── SubTabs.vue
├── WorkspaceShell.vue
├── LeftSidebar.vue                   (重构)
├── RightInspector.vue
├── BottomPanel.vue                   (重构)
└── StatusBar.vue                     (现有可用,加 macOS 微调即可)

src/components/tuning/
├── TuningDialogShell.vue
└── TuningContainer.vue

src/components/aux/
├── AuxWindowShell.vue
├── XiCalWindow.vue
├── XiMicWindow.vue
└── XiBoxWindow.vue

src/components/global/
├── ThemePopover.vue
├── CommandPalette.vue
└── ShortcutHandler.vue

10.4 现有 TT* 控件(直接复用作为 XiForge 控件库素材)

TTButton / TTLabel / TTSlider / TTToggle / TTTabsWidget / TTLabelComboBoxWidget / TTProgressBarWidget / TTChartTableWidget / TTMixerWidget / TTGGEQWidget:这些是 XiForge UI 设计器的"基础控件库",前端智能体在实现 XiForge 的左控件库面板时直接 v-for 渲染即可(参考 demo data-side-pane="ctrls" 块)。


11. Pinia Store 总清单

Store 文件 关键 state 关键 actions
theme stores/theme.ts theme, fontScale, density setTheme, hydrate
license stores/license.ts tiers, expiresAt fetch, has(tier), enabledTabs
workspace stores/workspace.ts currentTab, currentSubtab, selectedNode switchTab, selectNode
project stores/project.ts projectFile, chains[], modules[] load, save
engine stores/engine.ts running, sampleRate, blockSize, latency, cpu, memPool start, stop, restart
connection stores/connection.ts devices[](含 led 状态) scan, connect, disconnect
profile stores/profile.ts activeProfile, list[], abState load, save, abToggle
tuning stores/tuning.tscomposables/useTuningManager.ts Map open/close/minimize/show
auxWindow composables/useAuxWindow.ts Map open/close
commands composables/useCommandPalette.ts commands[] register, execute

12. macOS 设计风格关键 CSS 实现

完整 CSS 已在 v3-unified-macos.html 第 7–940 行。前端智能体直接拷贝这一段到 src/styles/ 三个 css 文件即可。下表是 9 个必须正确实现的 macOS 关键点的速查:

设计点 demo 行号 实现要点
1. 圆角 L29–32 --radius-sm/md/lg/xl 4 级,普通卡片 10px、浮窗 14px、引导 20px
2. 毛玻璃 L80,L271,L295 等 backdrop-filter: var(--frosted)--frosted 仅主题 F 设为 blur(28px) saturate(180%),其他可设为 none 或保留 blur(20px) saturate(180%)
3. 红黄绿三色按钮 L304–323 三个 div.traffic.red/yellow/green,hover 加暗色叠加
4. SF Pro 字体栈 L228–229 -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", "PingFang SC", ...
5. SFSymbols 风图标 全局 用 emoji 占位(📄📂💾⚙▶■⚡↶↷🎚🔍🚀),生产环境用 SVG 替换。禁止用 Material/Bootstrap icon
6. 滚动条悬停才显 L242–246 *:hover::-webkit-scrollbar-thumb { background: rgba(127,127,127,.35); }
7. 按钮按下缩放 L257–262 button:active { transform: scale(0.97); } + transition: transform .08s ease
8. 焦点环品牌色 L249–253 *:focus-visible { box-shadow: var(--focus-ring); } 而非系统蓝
9. 主题 Pill Tab L450–488 主 tab 使用 border-radius: 999px 胶囊,激活态填充 accent 实色

13. 主要交互逻辑速查

13.1 状态变更触发链路

用户点击主 tab
  → router.push('/xiforge')
  → MainTabs.vue 监听 useRoute(),更新 active 类
  → 路由懒加载 XiForgeWorkspace.vue (License 检查 → 渲染)
  → SubTabs.vue 自动渲染 children 子 tab
  → BottomPanel.vue 计算 showCodeTab=true(因为 xiforge)
  → useTuningManager 不变(浮窗跨 tab 持续存在)
  → showToast('已切换到 XiForge')

13.2 用户切换主题

点击 🎨 → ThemePopover 打开
  → 点击主题卡 → useThemeStore().setTheme('B')
  → store 内部: 写 localStorage + body.setAttribute('data-theme', 'B')
  → CSS 变量级联生效(无重渲染),所有组件颜色实时更新
  → showToast('已切换到主题 B · 极光青主导')

13.3 用户双击节点开 Tuning

FlowCanvas 节点 ondblclick
  → useTuningManager().open('GEQ#1', 'GEQ × 31', GEQTuningDialog, props, evt.target)
  → tunings Map 新增条目, x/y 计算自触发节点位置
  → TuningContainer (Teleport) v-for 渲染新 dialog
  → BottomPanel 的 tray-btn count 计数 +1

13.4 用户最小化 Tuning

点击黄色 traffic 或 ─ 按钮
  → emit minimize → useTuningManager().minimize('GEQ#1')
  → t.minimized = true
  → TuningContainer 中 v-show="!t.minimized" 隐藏
  → tray-btn count 不变(还在打开列表)
  → 在 tray 弹出层中点"显示" → useTuningManager().show('GEQ#1') 恢复

13.5 License 流程

用户登录后 → useLicenseStore().fetch() 拉取 tiers
  → MainTabs.vue computed 过滤 disabled tabs(无 license 的灰显加 🔒)
  → 用户点击未授权 tab → router beforeEach 拦截 → showToast 提示
  → VST 开发场景: 用户 tiers=['forge-pro'] → 仅 /xiforge 可访问
    → 启动后默认重定向到 /xiforge
    → 其他 3 个 tab 全部灰显

14. 验收标准(Definition of Done)

每个 § 章交付时必须满足:

  1. 能跑npm run dev 启动无报错,浏览器打开默认重定向到 /xistudio,4 主 tab 切换正常。
  2. macOS 风:检查 9 个关键点(§12 表格)全部实现,截图对比 demo 应"一眼看到 macOS 味"。
  3. 主题切换:6 套主题逐一切换不卡顿,刷新页面后从 localStorage 恢复。
  4. License 隔离:mock 一个 tiers=['forge-pro'] 场景,验证只能进入 /xiforge,其他 tab 灰显,路由守卫拦截。
  5. Tuning 多开:双击 3 个节点能同时弹出 3 个浮窗,可拖拽、最小化到托盘、托盘恢复、关闭。
  6. 快捷键:Ctrl+½/¾ / Ctrl+K T / Ctrl+Shift+P / F7 / F5 / Esc 全部可用。
  7. 现有组件:12 个 *TuningDialog 内容能渲染(即使数据是 mock),证明现有逻辑没破坏。
  8. 响应式:1280px 以下三栏自动收窄到 220/1fr/280。
  9. 可离线打开:v3-unified-macos.html 双击直接能演示(已通过)。

15. 不在本规格范围(P2 阶段)

  • 实际后端联调(设备连接、引擎控制、license API):本规格只定 mock 数据形状
  • 国际化 i18n
  • 主题 D/E 在 XiStudio tab 的细节微调(节点底色、连线对比)
  • macOS 真窗口控制(traffic-lights 实际关窗/最小化/最大化,需要 Electron)
  • VST3 wrapper 的代码生成实际产出

附录 A · demo 与规格的对应索引

demo 文件 规格章节
L14–46 三色法典 §1.2 tokens.css
L51–217 6 主题 §1.3 themes.css
L228–262 macOS 全局 §2 macos.css
L304–388 菜单栏 §3.2 MenuBar.vue
L390–446 工具栏 §3.3 ToolBar.vue
L450–500 主 tab §4.3 MainTabs.vue
L502–532 子 tab §4.4 SubTabs.vue
L535–662 三栏 §5 WorkspaceShell + LeftSidebar + RightInspector
L668–784 底部 §6 BottomPanel.vue
L800–906 Tuning 浮窗 §7 TuningDialogShell + Container
L1531–L1647 Tuning JS §7.2 useTuningManager.ts
L1648–L1701 命令面板 JS §9 useCommandPalette.ts

附录 B · 文件创建清单(前端智能体复制粘贴用)

# 新建目录
mkdir -p src/styles
mkdir -p src/composables
mkdir -p src/stores
mkdir -p src/components/shell
mkdir -p src/components/tuning
mkdir -p src/components/aux
mkdir -p src/components/global
mkdir -p src/views/xistudio/sub
mkdir -p src/views/xiforge/sub
mkdir -p src/views/xitune/sub
mkdir -p src/views/xiprobe/sub
mkdir -p src/router

# 新建文件 (按 §0 顺序)
touch src/styles/{tokens.css,themes.css,macos.css}
touch src/composables/{useTheme.ts,useTuningManager.ts,useAuxWindow.ts,useCommandPalette.ts}
touch src/stores/{theme.ts,license.ts,workspace.ts}
touch src/components/shell/{MenuBar.vue,ToolBar.vue,MainTabs.vue,SubTabs.vue,WorkspaceShell.vue,LeftSidebar.vue,RightInspector.vue,BottomPanel.vue,ScopeCard.vue}
touch src/components/tuning/{TuningDialogShell.vue,TuningContainer.vue}
touch src/components/aux/{AuxWindowShell.vue,XiCalWindow.vue,XiMicWindow.vue,XiBoxWindow.vue}
touch src/components/global/{ThemePopover.vue,CommandPalette.vue,ShortcutHandler.vue}
touch src/views/xistudio/XiStudioWorkspace.vue
touch src/views/xistudio/sub/{FlowView.vue,TuneView.vue,SimView.vue,DeployView.vue}
touch src/views/xiforge/XiForgeWorkspace.vue
touch src/views/xiforge/sub/{DesignView.vue,SourceView.vue,SimView.vue}
touch src/views/xitune/XiTuneWorkspace.vue
touch src/views/xitune/sub/{ChainView.vue,CurveView.vue,ABXView.vue}
touch src/views/xiprobe/XiProbeWorkspace.vue
touch src/views/xiprobe/sub/{FreqView.vue,THDView.vue,SNRView.vue,BatchView.vue}
touch src/router/index.ts

实现规格 v0.1 · 2026-05-14 · 基于 v3-unified-macos.html 同步生成