# macOS-web 纯 Vue 重构手册 > **目标**:去除原生 JS 思路和实现,去掉 `window` 全局调用,将项目改造为纯 Vue 3 项目。 > **范围**:全量重构,不考虑成本。 --- ## 目录 1. [现状诊断](#1-现状诊断) 2. [目标架构](#2-目标架构) 3. [前端架构设计](#3-前端架构设计) 4. [重构总路线](#4-重构总路线) 5. [阶段一:基础设施层](#5-阶段一基础设施层) 6. [阶段二:系统 UI 组件化](#6-阶段二系统-ui-组件化) 7. [阶段三:窗口管理器现代化](#7-阶段三窗口管理器现代化) 8. [阶段四:应用层优化](#8-阶段四应用层优化) 9. [阶段五:清理与收尾](#9-阶段五清理与收尾) 10. [附录:新旧对照表](#10-附录新旧对照表) --- ## 1. 现状诊断 ### 1.1 双渲染系统并存(核心问题) | 层 | 当前渲染方式 | 问题 | |---|---|---| | **系统 UI 层**(菜单栏/Dock/对话框/菜单/通知/Spotlight/控制中心/锁屏/屏保/启动界面) | `el()` 命令式 DOM 构建 + `$()` 直接 DOM 查询 | 非 Vue,绕过虚拟 DOM | | **窗口 chrome 层**(traffic lights/标题栏/resize handles) | `el()` 命令式 DOM 构建 | 非 Vue | | **应用层**(Finder/Calculator/Safari 等 33 个应用) | Vue 3 SFC + `h()`/`render()` 手动挂载 | 部分合理 | | **容器层**(DesktopArea/MenuBarArea 等) | Vue 模板(仅作为 DOM 挂载锚点) | 合理 | ### 1.2 原生 JS 使用分布 | 模块 | 原生 API | 严重度 | |---|---|---| | `useSys.ts` (~800 行) | `$`, `$$`, `el`, `addEventListener`, `matchMedia`, `MutationObserver`, `requestAnimationFrame`, `setInterval`, `Image()`, `document.activeElement`, `getBoundingClientRect` | 🔴 最重度 | | `useWM.ts` (~400 行) | `el()`, `addEventListener('pointermove/up')`, `document.getElementById`, `document.body.classList`, `setPointerCapture`, `getBoundingClientRect` | 🔴 重度 | | `useUI.ts` (~200 行) | `document.createElement/addEventListener/body.append/remove` + `window.innerWidth/innerHeight` | 🔴 全部原生 | | `useNotify.ts` (~200 行) | `$`, `$$`, `el`, `setTimeout`, `querySelector` | 🔴 重度 | | `useSpotlight.ts` (~150 行) | `$`, `$$`, `el`, `addEventListener`, `navigator.clipboard`, `scrollIntoView` | 🔴 重度 | | `useApps.ts` (~120 行) | `document.activeElement`, `document.execCommand`, `navigator.clipboard` | 🟡 中度 | | `system/index.ts` (~160 行) | `$`, `$$`, `el`, `document.addEventListener`, 14 个 `window` 全局暴露 | 🔴 重度 | | `utils/index.ts` (~100 行) | `document.querySelector/querySelectorAll/createElement` | 🔴 基础依赖 | ### 1.3 关键反模式 ```mermaid graph TD A["window 全局暴露
14 个全局变量"] --> B["隐式依赖,模块封装破坏"] C["懒引用 lazy ref
let _Sys: any = null"] --> D["循环依赖 workaround
类型不安全"] E["el() 命令式 DOM
微型虚拟 DOM 替代品"] --> F["绕过 Vue 响应式
6+ 模块重度依赖"] G["$('') 直接 DOM 查询"] --> H["绕过 Vue ref 系统"] I["bus.on/emit"] --> J["类型不安全
非 Vue 标准模式"] K["33 个副作用 import"] --> L["无懒加载
全量打包"] ``` ### 1.4 核心循环依赖 ``` useSys ←→ useWM ←→ useApps ←→ useFS ``` 通过 `setSysRef()` / `setSysForNotify()` / `setAppsRef()` 等 lazy setter 打破。 --- ## 2. 目标架构 ### 2.1 总览 ```mermaid graph TD subgraph "Vue 应用入口" main.ts --> App.vue end subgraph "状态管理 Pinia" settingsStore["settingsStore
(原 useSettings + useStore)"] fsStore["fsStore
(原 useFS)"] appStore["appStore
(原 useApps 注册表部分)"] wmStore["wmStore
(原 useWM 状态部分)"] notifyStore["notifyStore
(原 useNotify 数据部分)"] sysStore["sysStore
(原 useSys 设置/电源部分)"] end subgraph "系统 UI 组件(纯 Vue SFC)" MenuBar["<MenuBar />"] Dock["<Dock />"] DesktopIcons["<DesktopIcons />"] Spotlight["<Spotlight />"] NotificationCenter["<NotificationCenter />"] ControlCenter["<ControlCenter />"] LockScreen["<LockScreen />"] Screensaver["<Screensaver />"] BootScreen["<BootScreen />"] ContextMenu["<ContextMenu />"] DialogLayer["<DialogLayer />"] end subgraph "窗口系统" WindowLayer["<WindowLayer />"] WindowFrame["<WindowFrame />
(chrome: 标题栏/traffic lights/resize)"] end subgraph "应用层(懒加载)" AppLoader["<AppLoader />
defineAsyncComponent"] Finder["Finder.vue"] Safari["Safari.vue"] TxtEdit["...33 个应用"] end subgraph "组合式函数(纯逻辑,无 DOM)" useKeyboard["useKeyboard(全局快捷键)"] useIdleWatch["useIdleWatch(屏保计时)"] useTheme["useTheme(暗色模式/壁纸)"] useVolume["useVolume(音量控制)"] useEventBus["useEventBus(mitt 替代 bus)"] end App.vue --> MenuBar App.vue --> Dock App.vue --> DesktopIcons App.vue --> WindowLayer App.vue --> Spotlight App.vue --> NotificationCenter App.vue --> ControlCenter App.vue --> LockScreen App.vue --> Screensaver App.vue --> BootScreen App.vue --> ContextMenu App.vue --> DialogLayer WindowLayer --> WindowFrame WindowLayer --> AppLoader ``` ### 2.2 核心原则 1. **一切 UI 皆 Vue 组件**:不再使用 `el()` 创建任何 DOM 元素 2. **状态归 Pinia**:跨模块共享状态走 Pinia store,不挂 `window` 3. **通信走 provide/inject + mitt**:父子用 provide/inject,跨层级用 mitt(类型安全的事件总线) 4. **无直接 DOM 查询**:用 Vue `ref` / `template ref` 替代 `$()` / `$$()` 5. **应用懒加载**:`defineAsyncComponent` + 动态 import 6. **仅保留必要的原生 API**:`pointer events` 拖拽、`localStorage`、`navigator.clipboard` 等不可避免的部分,封装在专用 composable 中 --- ## 3. 前端架构设计 ### 3.1 组件层级树 ```mermaid graph TD subgraph "App.vue(根组件)" direction TB A[App.vue] end subgraph "Layer 1:背景层 z-0" B1["<OverlayBrightness />"] B2["<OverlayNightShift />"] B3["<DesktopArea />"] end subgraph "Layer 2:内容层 z-1~99" C1["<WindowLayer />"] end subgraph "Layer 3:系统 UI 层 z-100~499" D1["<MenuBar />"] D2["<Dock />"] end subgraph "Layer 4:浮层 z-500~899" E1["<Spotlight />"] E2["<ControlCenter />"] E3["<NotificationCenter />"] E4["<ContextMenu />"] E5["<DialogLayer />"] E6["<BannerContainer />"] end subgraph "Layer 5:全屏覆盖层 z-900+" F1["<LockScreen />"] F2["<Screensaver />"] F3["<BootScreen />"] F4["<PowerOff />"] end A --> B1 & B2 & B3 A --> C1 A --> D1 & D2 A --> E1 & E2 & E3 & E4 & E5 & E6 A --> F1 & F2 & F3 & F4 C1 --> C1A["<WindowFrame /> ×N"] C1A --> C1A1["<TrafficLights />"] C1A --> C1A2["<AppLoader />"] C1A2 --> C1A2A["应用 SFC
(Finder/Safari/...)"] ``` **z-index 分层规范**: | 层 | z-index 范围 | 组件 | 说明 | |---|---|---|---| | 背景层 | 0 | OverlayBrightness, OverlayNightShift, DesktopArea | 静态背景,无交互 | | 内容层 | 1–99 | WindowLayer → WindowFrame ×N | 窗口动态分配 z-index | | 系统 UI 层 | 100–499 | MenuBar(100), Dock(200) | 始终在窗口之上 | | 浮层 | 500–899 | Spotlight(500), ControlCenter(510), NotificationCenter(510), ContextMenu(600), DialogLayer(700), BannerContainer(800) | 临时弹出内容 | | 全屏覆盖层 | 900+ | LockScreen(900), Screensaver(950), BootScreen(999), PowerOff(998) | 独占式全屏状态 | ### 3.2 数据流架构 ```mermaid flowchart LR subgraph "数据源" LS[localStorage] UA[用户操作] SYS[系统事件
resize/keyboard/idle] end subgraph "Pinia Stores(单一数据源)" direction TB SS["settingsStore
设置/外观/音量/壁纸"] FS["fsStore
虚拟文件系统"] WS["wmStore
窗口状态"] AS["appStore
应用注册表"] NS["notifyStore
通知数据"] US["uiStore
菜单/对话框状态"] end subgraph "Composables(纯逻辑)" UK[useKeyboard] UI[useIdleWatch] UT[useTheme] UD[useDrag / useResize] UC[useClipboard] end subgraph "Vue 组件(视图层)" direction TB MB["<MenuBar />"] DK["<Dock />"] SP["<Spotlight />"] CC["<ControlCenter />"] NC["<NotificationCenter />"] WF["<WindowFrame />"] APPS["33 个应用组件"] end subgraph "跨组件通信" MITT["mitt 事件总线
(wm:focus, fs:changed,
volume:changed, apps:ready)"] PI["provide / inject
(窗口级上下文)"] end LS -->|"watch 持久化"| SS LS --> FS LS --> NS UA -->|"@click / v-model"| SS UA --> WS UA --> AS UA --> US SYS --> UK SYS --> UI SYS --> UT SS -->|"reactive state"| MB SS --> DK SS --> CC SS --> NC SS --> WF SS --> APPS WS -->|"reactive state"| WF WS --> DK FS -->|"reactive state"| SP FS --> APPS NS -->|"reactive state"| NC NS --> DK AS -->|"registry lookup"| SP AS --> WF AS --> DK US -->|"reactive state"| MB US --> SP UK -->|"快捷键路由"| SS UK --> WS UK --> AS UI -->|"空闲超时"| SS UT -->|"外观变更"| SS MITT -->|"事件订阅"| WS MITT --> FS MITT --> DK MITT --> APPS PI -->|"窗口上下文"| APPS ``` **核心数据流原则**: 1. **单向数据流**:`用户操作 → Store 更新 → 组件响应式重渲染` 2. **Store 是唯一真相源**:所有共享状态必须位于 Pinia store 中 3. **组件不直接修改其他组件的状态**:通过 store action 或事件总线 4. **事件总线仅用于通知,不用于状态传递**:`emitter.emit('fs:changed')` 是"文件变了,你们自己去看",而非"文件变了,这是新数据" ### 3.3 路由与懒加载策略 本项目是**桌面模拟器**,不使用 vue-router(无需 URL 路由)。应用加载使用以下策略: ```typescript // src/apps/loaders.ts import { defineAsyncComponent, type AsyncComponentLoader } from 'vue' /** * 应用懒加载映射表 * * 策略: * - 核心应用(Finder)→ 立即加载(Eager) * - 系统应用(前 7 个)→ 空闲时预加载(Idle Preload) * - 其他应用 → 按需加载(Lazy) */ // 加载策略枚举 enum LoadStrategy { Eager, // 立即加载 IdlePreload, // requestIdleCallback 预加载 Lazy, // 首次打开时加载 } export const appLoaders: Record = { // === 核心应用(Eager:启动即加载) === finder: { loader: () => import('./finder/Finder.vue'), strategy: LoadStrategy.Eager, }, // === 系统应用(IdlePreload:空闲时后台加载) === settings: { loader: () => import('./settings/Settings.vue'), strategy: LoadStrategy.IdlePreload, }, safari: { loader: () => import('./safari/Safari.vue'), strategy: LoadStrategy.IdlePreload, }, mail: { loader: () => import('./mail/Mail.vue'), strategy: LoadStrategy.IdlePreload, }, messages: { loader: () => import('./messages/Messages.vue'), strategy: LoadStrategy.IdlePreload, }, music: { loader: () => import('./music/Music.vue'), strategy: LoadStrategy.IdlePreload, }, photos: { loader: () => import('./photos/Photos.vue'), strategy: LoadStrategy.IdlePreload, }, notes: { loader: () => import('./notes/Notes.vue'), strategy: LoadStrategy.IdlePreload, }, // === 其他应用(Lazy:按需加载) === calculator: { loader: () => import('./calculator/Calculator.vue'), strategy: LoadStrategy.Lazy, }, calendar: { loader: () => import('./calendar/Calendar.vue'), strategy: LoadStrategy.Lazy, }, // ... 其余 24 个应用 } /** * 在 appStore.init() 之后调用,利用浏览器空闲时间预加载常用应用 */ export function initPreload() { const idleLoaders = Object.values(appLoaders) .filter(a => a.strategy === LoadStrategy.IdlePreload) idleLoaders.forEach(({ loader }) => { requestIdleCallback(() => { // 触发异步加载但不等待结果 ;(loader as () => Promise)() }, { timeout: 3000 }) }) } ``` **加载状态处理**: ```vue ``` ### 3.4 状态管理设计 #### 3.4.1 Store 职责矩阵 | Store | 拥有的状态 | 不拥有的状态 | 持久化 | |---|---|---|---| | `settingsStore` | 用户设置、外观、音量、壁纸、登录项 | 窗口状态、通知数据 | ✅ localStorage | | `fsStore` | 虚拟文件系统树、当前目录 | 应用注册信息 | ✅ localStorage | | `wmStore` | 窗口列表、activeWin、z-index、cascade | 窗口 DOM 元素 | ❌ 仅会话 | | `appStore` | 应用注册表、打开队列 | 应用内部状态 | ❌ 仅会话 | | `notifyStore` | 通知列表、已读状态 | 通知 DOM、横幅定时器 | ✅ localStorage | | `uiStore` | 菜单/对话框显隐、位置、回调 | 菜单/对话框 DOM | ❌ 仅会话 | #### 3.4.2 Store 间依赖规则 ``` settingsStore ← 无依赖(叶子 store) fsStore ← 无依赖 notifyStore ← 依赖 settingsStore(权限检查) appStore ← 依赖 wmStore(open 时创建窗口) wmStore ← 依赖 settingsStore(可用区域计算)、appStore(窗口应用信息) uiStore ← 无依赖 ``` 规则:**禁止 Store 间循环依赖**。如果 A 需要 B 的数据而 B 也需要 A 的数据,应提取到第三个 Store C 或使用事件总线解耦。 #### 3.4.3 Pinia 插件:持久化 ```typescript // src/stores/plugins/persist.ts import type { PiniaPluginContext } from 'pinia' const PERSISTED_STORES = ['settings', 'fs', 'notify'] export function persistPlugin({ store }: PiniaPluginContext) { if (!PERSISTED_STORES.includes(store.$id)) return // 初始化:从 localStorage 恢复 const saved = localStorage.getItem(`macos-web:${store.$id}`) if (saved) { try { store.$patch(JSON.parse(saved)) } catch { /* ignore */ } } // 自动持久化 store.$subscribe((_, state) => { localStorage.setItem(`macos-web:${store.$id}`, JSON.stringify(state)) }, { detached: true }) } ``` ### 3.5 事件通信设计 #### 3.5.1 通信方式选型 | 场景 | 方式 | 示例 | |---|---|---| | 父子组件通信 | `props` + `emits` | `` | | 祖先→后代(跨层级) | `provide` / `inject` | 窗口级上下文(win 对象、clipboard 等) | | 兄弟/任意组件(通知类) | `mitt` 事件总线 | `emitter.emit('fs:changed', { paths })` | | 全局状态共享 | Pinia store | `settingsStore.settings.darkMode` | | 浏览器级事件 | `composable` 封装 | `useKeyboard()` 监听 keydown | #### 3.5.2 mitt 事件契约 ```typescript // src/composables/useEventBus.ts import mitt from 'mitt' // ⚠️ 所有事件必须在此声明类型,保证端到端类型安全 type Events = { // 窗口事件 'wm:focus': { winId: string; appId: string } 'wm:changed': void 'wm:closed': { winId: string } // 文件系统事件 'fs:changed': { paths: string[] } 'trash:changed': void // 系统事件 'apps:ready': void 'volume:changed': number 'unlocked': void 'locked': void // 应用间事件 'spotlight:open': void 'spotlight:close': void } export const emitter = mitt() ``` #### 3.5.3 provide / inject 设计 ```typescript // 窗口级上下文 —— 由 WindowFrame 提供,所有应用组件注入 // src/composables/useWindowContext.ts import { provide, inject, type InjectionKey, type Ref } from 'vue' interface WindowContext { winId: string appId: string clipboard: Ref // 应用内剪贴板 isActive: Ref // 窗口是否聚焦 closeWindow: () => void setTitle: (title: string) => void } export const WIN_CTX_KEY: InjectionKey = Symbol('winCtx') export function provideWindowContext(ctx: WindowContext) { provide(WIN_CTX_KEY, ctx) } export function useWindowContext(): WindowContext { const ctx = inject(WIN_CTX_KEY) if (!ctx) throw new Error('useWindowContext() must be used inside a WindowFrame') return ctx } ``` ### 3.6 组件分类与职责 ```mermaid quadrantChart title 组件分类矩阵 x-axis "有状态" --> "无状态" y-axis "有 DOM" --> "无 DOM" quadrant-1 "容器组件" quadrant-2 "展示组件" quadrant-3 "逻辑 composable" quadrant-4 "纯工具函数" "WindowFrame": [0.2, 0.85] "MenuBar": [0.25, 0.9] "Dock": [0.3, 0.9] "Spotlight": [0.25, 0.85] "ControlCenter": [0.2, 0.85] "Finder": [0.35, 0.8] "TrafficLights": [0.85, 0.55] "DockIcon": [0.75, 0.6] "Banner": [0.9, 0.5] "CCToggle": [0.85, 0.55] "useDrag": [0.95, 0.15] "useKeyboard": [0.9, 0.1] "useTheme": [0.85, 0.1] "useClipboard": [0.95, 0.05] "emitter": [0.98, 0.02] "stdMenus": [0.98, 0.02] ``` | 分类 | 特征 | 示例 | 规则 | |---|---|---|---| | **容器组件** | 连接 Store,管理子组件状态,有 DOM 输出 | `WindowFrame`, `MenuBar`, `Dock`, `Spotlight` | 可访问 Pinia store,可 emit 事件 | | **展示组件** | 纯 props 输入 + emits 输出,无 Store 依赖 | `TrafficLights`, `DockIcon`, `Banner`, `CCToggle` | 禁止直接访问 Pinia store | | **逻辑 composable** | 封装有状态逻辑,无 DOM 输出 | `useDrag`, `useKeyboard`, `useTheme`, `useClipboard` | 可访问 Pinia store,不输出 DOM | | **纯工具函数** | 纯函数,无副作用,无状态 | `stdMenus`, `fmtDateCN`, `clamp` | 禁止访问任何 store 或 DOM | ### 3.7 目录结构设计 ``` src/ ├── main.ts # 入口:createApp + Pinia + 全局样式 ├── App.vue # 根布局组件 │ ├── stores/ # Pinia 状态管理 │ ├── index.ts # createPinia + 插件注册 │ ├── plugins/ │ │ └── persist.ts # 持久化插件 │ ├── settings.ts # useSettingsStore │ ├── fs.ts # useFSStore │ ├── wm.ts # useWMStore │ ├── apps.ts # useAppStore │ ├── notify.ts # useNotifyStore │ └── ui.ts # useUIStore │ ├── composables/ # 纯逻辑组合式函数(无 DOM) │ ├── useEventBus.ts # mitt 实例 + 类型定义 │ ├── useWindowContext.ts # provide/inject 窗口上下文 │ ├── useKeyboard.ts # 全局快捷键 │ ├── useIdleWatch.ts # 空闲计时 & 屏保触发 │ ├── useTheme.ts # 暗色模式 & 壁纸 │ ├── useVolume.ts # 音量 & 媒体注册 │ ├── useDrag.ts # 窗口拖拽(pointer events 封装) │ ├── useResize.ts # 窗口缩放(pointer events 封装) │ ├── useClipboard.ts # 剪贴板操作封装 │ ├── useClickOutside.ts # 外部点击检测 │ ├── useStdMenus.ts # 标准菜单模板(纯函数) │ └── useDockMag.ts # Dock 放大效果 │ ├── components/ # Vue 组件 │ ├── system/ # 系统 UI 组件 │ │ ├── MenuBar.vue # 菜单栏(容器) │ │ ├── MenuBarItem.vue # 菜单栏单项(展示) │ │ ├── Dock.vue # 程序坞(容器) │ │ ├── DockIcon.vue # 程序坞图标(展示) │ │ ├── DesktopIcons.vue # 桌面图标区(容器) │ │ ├── DesktopIcon.vue # 单个桌面图标(展示) │ │ ├── Spotlight.vue # 聚焦搜索(容器) │ │ ├── SpotlightItem.vue # 搜索结果项(展示) │ │ ├── ControlCenter.vue # 控制中心(容器) │ │ ├── CCToggle.vue # 控制中心开关(展示) │ │ ├── CCSlider.vue # 控制中心滑块(展示) │ │ ├── NotificationCenter.vue # 通知中心(容器) │ │ ├── BannerContainer.vue # 横幅容器(容器) │ │ ├── Banner.vue # 单个横幅(展示) │ │ ├── LockScreen.vue # 锁屏界面(容器) │ │ ├── Screensaver.vue # 屏保(容器) │ │ ├── BootScreen.vue # 启动动画(展示) │ │ └── PowerOff.vue # 关机提示(展示) │ ├── wm/ # 窗口管理组件 │ │ ├── WindowLayer.vue # 窗口层容器(容器) │ │ ├── WindowFrame.vue # 窗口框架(容器) │ │ ├── TrafficLights.vue # 红绿灯按钮(展示) │ │ └── AppLoader.vue # 应用加载器(容器) │ └── ui/ # 通用 UI 组件 │ ├── ContextMenu.vue # 右键/下拉菜单(容器) │ ├── ContextMenuItem.vue # 菜单项(展示) │ ├── DialogLayer.vue # 对话框层(容器) │ ├── AlertDialog.vue # 警告框(展示) │ ├── ConfirmDialog.vue # 确认框(展示) │ └── PromptDialog.vue # 输入框(展示) │ ├── apps/ # 应用层 │ ├── loaders.ts # 懒加载映射 + 预加载策略 │ ├── finder/ │ │ ├── index.ts # AppDefinition(纯数据) │ │ └── Finder.vue # Finder SFC │ ├── safari/ │ │ ├── index.ts │ │ └── Safari.vue │ └── ...(其余 31 个应用,结构相同) │ ├── styles/ # 样式(不变) │ ├── base.css │ ├── desktop.css │ ├── window.css │ ├── apps.css │ └── apps2.css │ ├── types/ # 全局类型定义 │ ├── app.ts # AppDefinition, MenuItem │ ├── window.ts # WinState, Rect │ ├── fs.ts # FSNode, FSEntry │ └── settings.ts # Settings, Wallpaper │ └── utils/ # 纯工具函数(无 DOM) ├── format.ts # fmtDateCN, fmtTime, fmtBytes ├── math.ts # clamp ├── misc.ts # uid, debounce, esc └── index.ts # 统一导出 ``` ### 3.8 关键技术决策 | 决策点 | 选择 | 理由 | |---|---|---| | 路由方案 | **不使用 vue-router** | 桌面模拟器无 URL 路由需求,应用切换由 `wmStore` 窗口管理 + `AppLoader` 动态组件实现 | | 状态管理 | **Pinia**(非 Vuex) | Vue 3 官方推荐,完整 TS 支持,模块化设计 | | 跨组件通信 | **mitt + provide/inject** | mitt 用于全局通知,provide/inject 用于窗口级上下文,避免过度使用全局事件 | | CSS 方案 | **纯 CSS(不变)** | 项目样式已成熟,无需引入 Tailwind/UnoCSS,减少重构范围 | | 构建工具 | **Vite(不变)** | 当前已使用 Vite,保留配置 | | 测试框架 | **Vitest(不变)** | 当前已使用 Vitest | | 窗口拖拽/缩放 | **原生 Pointer Events + composable 封装** | 这是性能关键路径,Vue 事件系统不适合高频 pointermove | | 持久化 | **Pinia watch + localStorage** | 简单可靠,无需引入额外的持久化库 | | 应用加载 | **defineAsyncComponent + 分级策略** | Eager/IdlePreload/Lazy 三级策略兼顾首屏速度与体验 | | 类型安全 | **全局禁止 `any`**(eslint rule) | 强制所有接口、store、事件使用精确类型 | ### 3.9 性能策略 ```mermaid flowchart TB subgraph "首屏加载" A1["仅加载 Eager 应用(Finder)"] A2["系统 UI 组件同步渲染"] A3["requestIdleCallback 预加载 7 个常用应用"] end subgraph "窗口操作" B1["打开应用 → 检查是否已加载"] B2["未加载 → Suspense + loading 占位"] B3["已加载 → 立即显示 <component :is=>"] end subgraph "关闭优化" C1["关闭窗口 → 组件 v-if 卸载"] C2["已加载的异步 chunk 保留在浏览器缓存"] C3["再次打开 → 不重新下载 JS"] end subgraph "内存管理" D1["窗口关闭 → clearInterval/timeout"] D2["onUnmounted → 移除事件监听"] D3["Pinia store 按需持久化,不存储 DOM 引用"] end A1 --> A2 --> A3 B1 --> B2 B1 --> B3 C1 --> C2 --> C3 ``` --- ## 4. 重构总路线 ``` Phase 1: 基础设施层 Phase 2: 系统 UI 组件化 Phase 3: WM 现代化 ───────────────────── ──────────────────────── ──────────────── Pinia stores MenuBar → SFC WindowFrame → SFC mitt 事件总线 Dock → SFC TrafficLights → SFC 消除 window 全局暴露 DesktopIcons → SFC 拖拽/缩放 composable 消除 lazy ref Spotlight → SFC resize handles → SFC NotificationCenter → SFC ControlCenter → SFC Phase 5: 清理 LockScreen → SFC ─────────── Screensaver → SFC 删除 utils/index.ts BootScreen → SFC 删除 system/index.ts ContextMenu → SFC 删除 el() 函数 Dialog/Alert/Confirm → SFC 删除 $/$$ 全局类型检查 Phase 4: 应用层优化 ───────────────────── defineAsyncComponent 移除 (window as any).Sys 移除 h()/render() 手动挂载 stdMenus 纯函数化 ``` --- ## 5. 阶段一:基础设施层 ### 4.1 建立 Pinia Stores #### 4.1.1 `settingsStore` — 替代 useSettings + useStore ```typescript // src/stores/settings.ts import { defineStore } from 'pinia' import { reactive, watch } from 'vue' export const WALLPAPERS = [ /* 原数据 */ ] export const DEFAULT_SETTINGS = { /* 原数据 */ } export const useSettingsStore = defineStore('settings', () => { const settings = reactive(structuredClone(DEFAULT_SETTINGS)) // 持久化 watch(替代 store.set/load) watch(settings, (v) => { localStorage.setItem('macos-web:settings', JSON.stringify(v)) }, { deep: true }) // 计算属性 const isDark = computed(() => { /* 原 Sys.isDark() */ }) const wallpaperSrc = computed(() => { /* 原 Sys.wallpaperSrc() */ }) // 方法 function applyAppearance() { /* 操作 document.body.classList */ } function applyWallpaper() { /* ... */ } return { settings, isDark, wallpaperSrc, applyAppearance, applyWallpaper } }) ``` **改动要点**: - `store.get/set` → Pinia `$state` + watch 持久化 - `Sys.settings.xxx` → `settingsStore.settings.xxx` - `Sys.isDark()` → `settingsStore.isDark` (computed) - `Sys.save()` → 自动 watch 持久化 #### 4.1.2 `fsStore` — 替代 useFS ```typescript // src/stores/fs.ts export const useFSStore = defineStore('fs', () => { const root = ref(null) const HOME = '/Users/guest' const TRASH = computed(() => HOME + '/.Trash') function init() { /* 原 fs.init() */ } function list(path: string) { /* ... */ } function node(path: string) { /* ... */ } function mkdir(path: string) { /* ... */ } function rename(path: string, newName: string) { /* ... */ } function trash(path: string) { /* ... */ } function emptyTrash() { /* ... */ } // 用 mitt 替代 bus.emit('fs:changed') function notifyChange(paths: string[]) { emitter.emit('fs:changed', { paths }) } return { root, HOME, TRASH, init, list, node, mkdir, rename, trash, emptyTrash } }) ``` **改动要点**: - `fs.node(path)` → `fsStore.node(path)`,响应式数据 - `bus.emit('fs:changed')` → `emitter.emit('fs:changed')`(mitt) - 文件系统树用 `ref` / `reactive` 保持响应式 #### 4.1.3 `wmStore` — 替代 useWM 的状态部分 ```typescript // src/stores/wm.ts export const useWMStore = defineStore('wm', () => { const windows = reactive([]) const activeWin = ref(null) const zTop = ref(100) const cascadeCount = ref(0) function addWindow(win: WinState) { windows.push(win) } function removeWindow(id: string) { /* splice */ } function focus(win: WinState) { /* 更新 z-index + activeWin */ } return { windows, activeWin, zTop, cascadeCount, addWindow, removeWindow, focus } }) ``` **改动要点**: - WM `windows`/`activeWin`/`zTop` → Pinia store - 窗口 DOM 创建逻辑分离到 `` 组件 #### 4.1.4 `appStore` — 替代 useApps 注册表 ```typescript // src/stores/apps.ts export const useAppStore = defineStore('apps', () => { const registry = reactive>({}) function register(def: AppDefinition) { registry[def.id] = def } function get(id: string) { return registry[id] } // open 方法:不再手动调用 wm.openWindow + h() + render() // 而是设置一个"待打开"信号,由 组件消费 const pendingOpen = ref<{ id: string; args?: any } | null>(null) function open(id: string, args?: any) { pendingOpen.value = { id, args } } return { registry, register, get, pendingOpen, open } }) ``` #### 4.1.5 `notifyStore` — 替代 useNotify 数据部分 ```typescript // src/stores/notify.ts export const useNotifyStore = defineStore('notify', () => { const notifications = ref([]) function send(n: Notification) { /* push + 持久化 */ } function markRead(id: string) { /* ... */ } function remove(id: string) { /* ... */ } function clearAll() { /* ... */ } function badgeCount(appId: string) { /* computed */ } return { notifications, send, markRead, remove, clearAll, badgeCount } }) ``` ### 4.2 引入 mitt 替代 useBus ```bash pnpm add mitt ``` ```typescript // src/composables/useEventBus.ts import mitt from 'mitt' type Events = { 'wm:focus': WinState 'wm:changed': void 'fs:changed': { paths: string[] } 'apps:ready': void 'volume:changed': number 'trash:changed': void // ... 所有事件类型 } export const emitter = mitt() ``` **替代映射**: | 原 bus 用法 | 新 mitt 用法 | |---|---| | `bus.on('wm:focus', fn)` | `emitter.on('wm:focus', fn)` | | `bus.emit('wm:focus', win)` | `emitter.emit('wm:focus', win)` | | `bus.off('wm:focus', fn)` | `emitter.off('wm:focus', fn)` | ### 4.3 消除 `window` 全局暴露 **删除 `system/index.ts` 中的所有 `(window as any)` 赋值**。 替代方案: - 模块间引用 → Pinia store / provide-inject - App 组件引用 → 通过 `useAppStore().get(id)` 获取 - 工具函数引用 → 直接 import ### 4.4 消除 lazy ref **删除所有 `let _Xxx: any = null` + `setXxxRef()` 模式**。 原懒引用的用途和替代方案: | 原用途 | 替代 | |---|---| | `setSysRef(wm)` — WM 需要 Sys.settings | WM 直接 import `useSettingsStore` | | `setSysForNotify(sys)` — Notify 需要 Sys.settings | Notify 直接 import `useSettingsStore` | | `setSysForSpotlight(sys)` — Spotlight 需要 Sys | Spotlight 直接 import 对应 store | | `setAppsRef(apps)` — FS 需要 Apps 注册表 | FS 直接 import `useAppStore` | | `setAppStoreRef(a)` — Sys 需要 AppStore | 延迟访问 `useAppStore().get('appstore')` | 解决循环依赖的根本方法:**拆分模块**。例如 `useSys.ts` 应该拆分为: - `stores/settings.ts` — 设置管理 - `composables/useKeyboard.ts` — 全局快捷键 - `composables/useIdleWatch.ts` — 空闲计时/屏保 - `composables/useTheme.ts` — 外观/壁纸应用 - `` / `` / `` 等 Vue 组件 --- ## 6. 阶段二:系统 UI 组件化 ### 5.1 `` — 替代 Sys.buildMenubar() **原实现**:`useSys.ts` 中 `buildMenubar()` 用 `el()` 构建整个菜单栏 DOM,操作 `#menubar-left` / `#menubar-right`。 **目标实现**: ```vue ``` **改动要点**: - `el()` 构建 DOM → Vue 模板循环 - `(this as any)._mbClock` → `ref` + `setInterval` - `addEventListener('click')` → `@click` - 菜单栏左侧应用菜单 → 根据 `wmStore.activeWin` 计算 ### 5.2 `` — 替代 Sys.renderDock() ```vue ``` **改动要点**: - `dock.innerHTML = ''` + `el()` 循环构建 → `v-for` 组件 - `ic.addEventListener('click')` → `@click` emit - `ic.addEventListener('contextmenu')` → `@contextmenu` emit - Dock 放大动画用 CSS + 少量 pointer 事件(封装 composable) - `updateDockDots()` → 基于 `wmStore.windows` 的 computed ### 5.3 `` — 替代 Sys.renderDesktopIcons() ```vue ``` **改动要点**: - 每个桌面图标是一个独立的 `` SFC - 拖拽移动用 pointer events(保留原生,封装在 composable 中) - 双击/右键 → emit 到父组件处理 ### 5.4 `` / `` — 替代 useUI 这是最关键的变革之一:**将动态创建的弹出菜单和对话框改为 Vue 组件 + Teleport**。 ```vue ``` **全局菜单/对话框管理**: ```typescript // src/stores/ui.ts export const useUIStore = defineStore('ui', () => { // 弹出菜单 const menuVisible = ref(false) const menuItems = ref([]) const menuPosition = ref({ x: 0, y: 0 }) const menuOnClose = ref<(() => void) | null>(null) function showMenu(items: MenuItem[], x: number, y: number, onClose?: () => void) { closeMenu() menuItems.value = items menuPosition.value = { x, y } menuVisible.value = true menuOnClose.value = onClose ?? null } function closeMenu() { menuOnClose.value?.() menuVisible.value = false } // 对话框 const dialogVisible = ref(false) const dialogConfig = ref({ /* title, msg, buttons, ... */ }) let dialogResolve: ((v: any) => void) | null = null function showDialog(config: DialogConfig): Promise { return new Promise(resolve => { dialogConfig.value = config dialogVisible.value = true dialogResolve = resolve }) } return { menuVisible, menuItems, menuPosition, showMenu, closeMenu, dialogVisible, dialogConfig, showDialog, /* ... */ } }) ``` **改动要点**: - `ui.menu(items, x, y)` → `uiStore.showMenu(items, x, y)` - `ui.dialog({...})` → `uiStore.showDialog({...})`,返回 Promise 不变 - `ui.alert/confirm/prompt` → 基于 `showDialog` 的语法糖 - 菜单嵌套(submenu)用递归组件 - 全局 `pointerdown` 关闭菜单 → 在 `` 组件内部用 `onClickOutside` ### 5.5 `` — 替代 useSpotlight ```vue ``` **改动要点**: - HTML 结构在模板中定义(当前已在 `SpotlightArea.vue` 中有静态模板,但内容仍由 JS 动态填充) - `input.oninput` → `v-model` + `watch` - `input.onkeydown` → `@keydown` - `render()` 中的 `el()` 构建 → computed `results` + `v-for` - `scrollIntoView` → `nextTick` + Vue 方式滚动 ### 5.6 `` / `` — 替代 useNotify 渲染 ```vue ``` **改动要点**: - `el()` 创建横幅 DOM → `` SFC - `setTimeout` 自动消失 → `watch` + `setTimeout`,配合 `` - 通知中心 `nc.innerHTML = ''` 重建 → computed 列表 + `v-for` ### 5.7 `` / `` / `` / `` 这些容器组件当前已有 Vue 模板(如 `LockScreenArea.vue`),但内部内容(锁屏按钮、屏保幻灯片等)全由 `el()` 动态构建。需改为: - `` 模板中包含完整的 HTML 结构,用 `v-if`/`v-show` 控制显示 - `` 中的时钟/幻灯片 → 用计算属性 + `v-if` 选择类型 - `` 中的进度条动画 → CSS transition + `v-show` ### 5.8 `` — 替代 Sys.renderControlCenter() ```vue ``` **改动要点**: - `ccToggle()` 函数生成 DOM → `` SFC,用 `v-model` 双向绑定 - `cc.innerHTML = ''` 重建 → 响应式数据驱动 - 外点击关闭 → `onClickOutside` 指令/composable --- ## 7. 阶段三:窗口管理器现代化 ### 6.1 `` — 替代 useWM 的窗口 chrome 构建 **原实现**:`openWindow()` 中用 `el()` 创建 traffic lights、标题栏、body、resize handles,全部命令式。 **目标实现**: ```vue ``` ### 6.2 `` — 独立组件 ```vue ``` ### 6.3 `` — 容器组件 ```vue ``` ### 6.4 拖拽/缩放 composable 拖拽和缩放必须用原生 pointer events(这是性能关键路径,不可用 Vue 事件替代),但封装为纯 composable: ```typescript // src/composables/useDrag.ts export function useDrag(elRef: Ref, win: WinState) { function onDragStart(e: PointerEvent) { if (win.state === 'fullscreen') return const el = elRef.value! el.setPointerCapture(e.pointerId) const startX = e.clientX const startY = e.clientY const origX = win.rect.x const origY = win.rect.y function onMove(e: PointerEvent) { win.rect.x = origX + e.clientX - startX win.rect.y = origY + e.clientY - startY } function onUp() { el.removeEventListener('pointermove', onMove) el.removeEventListener('pointerup', onUp) } el.addEventListener('pointermove', onMove) el.addEventListener('pointerup', onUp) } return { onDragStart } } ``` ```typescript // src/composables/useResize.ts export function useResize(elRef: Ref, win: WinState) { function onResizeStart(e: PointerEvent, dir: string) { // 类似 useDrag,但修改 width/height/x/y } return { onResizeStart } } ``` ### 6.5 窗口关闭流程现代化 原 `close()` 方法的 Promise + confirm 流程保持不变,但 `closePromise` 管理移到 `wmStore`: ```typescript // stores/wm.ts async function close(winId: string): Promise { const win = windows.find(w => w.id === winId) if (!win) return 'alreadyClosed' // 如果有确认回调 if (win.confirmClose) { return new Promise(resolve => { win.confirmClose!( () => { doClose(win); resolve('closed') }, () => resolve('cancelled'), ) }) } doClose(win) return 'closed' } ``` --- ## 8. 阶段四:应用层优化 ### 7.1 `` — 替代手动 `h()` + `render()` **原实现**:每个 app 的 `index.ts` 中: ```typescript render(win) { const vnode = h(XxxComponent, { win }) render(vnode, win.body) } ``` **目标实现**:用 `defineAsyncComponent` + `` 动态加载: ```vue ``` 应用懒加载映射表: ```typescript // src/apps/loaders.ts import { defineAsyncComponent } from 'vue' export const appLoaders: Record = { finder: defineAsyncComponent(() => import('./finder/Finder.vue')), safari: defineAsyncComponent(() => import('./safari/Safari.vue')), calculator: defineAsyncComponent(() => import('./calculator/Calculator.vue')), settings: defineAsyncComponent(() => import('./settings/Settings.vue')), // ... 33 个应用 } ``` ### 7.2 应用注册简化 **原 `index.ts`**: ```typescript export const FinderApp = { id:'finder', ..., render(win) { ... } } Apps.register(FinderApp) ``` **改为纯数据定义**: ```typescript // src/apps/finder/index.ts import type { AppDefinition } from '@/stores/apps' export const finderDef: AppDefinition = { id: 'finder', name: '访达', icon: '/assets/icons/finder.png', w: 800, h: 550, minW: 480, minH: 300, singleton: true, menus(win) { return stdMenus('finder', { /* ... */ }) }, // 不再需要 render 方法! } ``` ```typescript // src/stores/apps.ts import { finderDef } from '@/apps/finder' // 在 store 初始化时注册 function init() { register(finderDef) register(safariDef) // ... 33 个应用 } ``` **关键变化**:每个 app 不再包含 `render(win)` 方法,而是由 `` 根据 `appId` 查找对应的异步组件来渲染。 ### 7.3 移除应用中的 `(window as any)` 当前多个应用访问 `(window as any).Sys` / `(window as any).Notify` 等: | 文件 | 全局引用 | 替代方案 | |---|---|---| | `finder/Finder.vue` | `(window as any).__finderClipboard` | Pinia store 或 provide/inject | | `safari/Safari.vue` | `window.open(url, '_blank')` | 允许(无替代方案) | | `calculator/Calculator.vue` | `props.win.el?.addEventListener` | 用 `@keydown` 在模板中处理 | | 多个应用 | `bus.on('fs:changed')` | `emitter.on('fs:changed')` | | 多个应用 | `ui.alert/confirm/prompt` | `uiStore.showDialog()` | | 多个应用 | `wm.setTitle(props.win, ...)` | `wmStore.setTitle(winId, title)` | | 多个应用 | `props.win.appState = st` | 通过 `win.data` 或 provide | ### 7.4 `stdMenus()` 纯函数化 **原实现**使用 `document.activeElement` / `document.execCommand`: ```typescript // 原:直接访问 document const editBase = () => { const a = document.activeElement as any const isText = a && (a.tagName === 'INPUT' || a.tagName === 'TEXTAREA' || a.isContentEditable) // ... } ``` **改进**:接受 `isTextFocused: boolean` 参数,由调用方传入: ```typescript // src/composables/useStdMenus.ts export function stdMenus(appDef: AppDef, options: StdMenuOptions = {}) { return function (win: WinState | null) { return [ { label: '编辑', items: () => [ { label: '剪切', key: '⌘X', action: () => clipboard.cut() }, { label: '拷贝', key: '⌘C', action: () => clipboard.copy() }, { label: '粘贴', key: '⌘V', action: () => clipboard.paste() }, // ... ] }, // ... ] } } ``` 剪贴板操作封装为独立的 `useClipboard` composable(内部使用 `navigator.clipboard` API)。 --- ## 9. 阶段五:清理与收尾 ### 8.1 删除文件 ``` 删除: src/utils/index.ts — el/$/$$ 函数,完全由 Vue 替代 src/system/index.ts — 启动序列逻辑分散到各初始化的 store/composable src/composables/useBus.ts — 由 mitt 替代 src/composables/useStore.ts — 由 Pinia 持久化插件替代 src/composables/useUI.ts — 由 / + uiStore 替代 src/composables/useSys.ts — 拆分为多个 store + composable + Vue 组件 ``` ### 8.2 重构后的目录结构 ``` src/ ├── main.ts ├── App.vue ├── stores/ # Pinia stores(新增) │ ├── settings.ts # 设置管理 │ ├── fs.ts # 虚拟文件系统 │ ├── wm.ts # 窗口管理器状态 │ ├── apps.ts # 应用注册表 │ ├── notify.ts # 通知数据 │ └── ui.ts # 菜单/对话框状态 ├── composables/ # 纯逻辑 composable(无 DOM) │ ├── useKeyboard.ts # 全局快捷键 │ ├── useIdleWatch.ts # 空闲计时/屏保 │ ├── useTheme.ts # 外观/壁纸 │ ├── useVolume.ts # 音量/媒体控制 │ ├── useDrag.ts # 窗口拖拽(pointer events 封装) │ ├── useResize.ts # 窗口缩放(pointer events 封装) │ ├── useClipboard.ts # 剪贴板操作封装 │ ├── useStdMenus.ts # 标准菜单模板(纯函数) │ ├── useEventBus.ts # mitt 事件总线实例 │ └── useClickOutside.ts # 外点击检测 ├── components/ │ ├── system/ # 系统 UI 组件 │ │ ├── MenuBar.vue │ │ ├── MenuBarItem.vue │ │ ├── Dock.vue │ │ ├── DockIcon.vue │ │ ├── DesktopIcons.vue │ │ ├── DesktopIcon.vue │ │ ├── Spotlight.vue │ │ ├── SpotlightItem.vue │ │ ├── ControlCenter.vue │ │ ├── CCToggle.vue │ │ ├── CCSlider.vue │ │ ├── NotificationCenter.vue │ │ ├── Banner.vue │ │ ├── BannerContainer.vue │ │ ├── LockScreen.vue │ │ ├── Screensaver.vue │ │ ├── BootScreen.vue │ │ └── PowerOff.vue │ ├── wm/ # 窗口管理组件 │ │ ├── WindowLayer.vue │ │ ├── WindowFrame.vue │ │ ├── TrafficLights.vue │ │ └── AppLoader.vue │ └── ui/ # 通用 UI 组件 │ ├── ContextMenu.vue │ ├── ContextMenuItem.vue │ ├── DialogLayer.vue │ ├── AlertDialog.vue │ ├── ConfirmDialog.vue │ └── PromptDialog.vue ├── apps/ # 应用层(基本不变,去掉 render 方法) │ ├── loaders.ts # 懒加载映射表(新增) │ ├── finder/ │ │ ├── index.ts # 仅数据定义(AppDefinition) │ │ └── Finder.vue │ ├── calculator/ │ │ ├── index.ts │ │ └── Calculator.vue │ └── ...(33 个应用) ├── styles/ # 不变 └── types/ # 类型定义(从各模块提取) ├── app.ts ├── window.ts ├── fs.ts └── settings.ts ``` ### 8.3 启动流程重构 **原**:`system/index.ts` → `initSystem()` → 手动串联 `fs.init → Sys.init → Notify.init → Sys.renderDock → initDesktop → boot` **新**:在 `App.vue` 的 `onMounted` 中: ```typescript // App.vue onMounted(async () => { // 1. 初始化 stores const fsStore = useFSStore() await fsStore.init() const settingsStore = useSettingsStore() settingsStore.init() const appStore = useAppStore() appStore.init() // 注册所有 33 个应用 const notifyStore = useNotifyStore() notifyStore.init() // 2. 初始化全局 composable useKeyboard() useIdleWatch() useTheme() // 3. 触发启动动画 const sysStore = useSysStore() sysStore.boot() // 4. 通知应用就绪 emitter.emit('apps:ready') }) ``` ### 8.4 测试适配 测试文件(`tests/cases/` 下 47 个)需要相应更新: - `(window as any).Sys` / `(window as any).WM` → 通过 Pinia store 操作 - `(window as any).FS` → `useFSStore()` - `(window as any).Notify` → `useNotifyStore()` - `(window as any).Apps` → `useAppStore()` - `bus.on/emit` → `emitter.on/emit` 建议在 `tests/setup.ts` 中创建一个测试辅助工具,提供简化的 store 访问。 --- ## 10. 附录:新旧对照表 ### 9.1 文件映射 | 原文件 | 重构后 | 说明 | |---|---|---| | `src/utils/index.ts` | **删除** | `el()`/`$`/`$$` 由 Vue 模板/ref 替代;格式化函数移入 `src/utils/format.ts` | | `src/system/index.ts` | **删除** | 启动逻辑分散到 store 初始化和 App.vue | | `src/composables/useBus.ts` | **删除** | 由 `src/composables/useEventBus.ts` (mitt) 替代 | | `src/composables/useStore.ts` | **删除** | 由 Pinia 持久化 watch 替代 | | `src/composables/useUI.ts` | `src/stores/ui.ts` + `src/components/ui/*.vue` | 拆为状态 store + Vue 组件 | | `src/composables/useSys.ts` | 拆分为 7 个模块 | 见下一节 | | `src/composables/useWM.ts` | `src/stores/wm.ts` + `src/components/wm/*.vue` + `src/composables/useDrag.ts` + `src/composables/useResize.ts` | 状态/UI/拖拽分离 | | `src/composables/useNotify.ts` | `src/stores/notify.ts` + `src/components/system/Banner.vue` + `src/components/system/NotificationCenter.vue` | 数据与视图分离 | | `src/composables/useSpotlight.ts` | `src/stores/spotlight.ts` + `src/components/system/Spotlight.vue` | 数据与视图分离 | | `src/composables/useApps.ts` | `src/stores/apps.ts` + `src/composables/useStdMenus.ts` | 注册表 + 菜单模板分离 | | `src/composables/useFS.ts` | `src/stores/fs.ts` | 直接迁移到 Pinia | | `src/composables/useSettings.ts` | `src/stores/settings.ts` | 合并 useStore 到 Pinia | ### 9.2 useSys.ts 拆分 | 原负责内容 | 目标位置 | |---|---| | `settings` / `save` / `set` / `applyAll` / `isDark` / `wallpaperSrc` / `applyAppearance` / `applyWallpaper` / `applyBrightness` / `applyNightShift` / `applyVolume` / `registerMedia` | `src/stores/settings.ts` | | `boot` / `showLock` / `closeOverlays` / `powerOff` / `restart` / `logout` / `sleep` / `forceQuitDialog` | `src/stores/sys.ts` | | `globalKeys` | `src/composables/useKeyboard.ts` | | `initIdleWatch` / `resetIdle` / `showScreensaver` / `hideScreensaver` | `src/composables/useIdleWatch.ts` | | `buildMenubar` / `wifiSvg` / `tickClock` / `openAppleMenu` / `setActiveApp` | `src/components/system/MenuBar.vue` | | `dockPinned` / `dockItems` / `renderDock` / `effDockSize` / `layoutDock` / `dockPeek` / `dockHide` / `dockResetMag` / `initDockMag` / `updateDockDots` / `dockClick` / `dockIconMenu` / `dockBgMenu` | `src/components/system/Dock.vue` + `src/composables/useDockMag.ts` | | `toggleControlCenter` / `ccToggle` / `renderControlCenter` | `src/components/system/ControlCenter.vue` | | `toggleNotificationCenter` | `src/components/system/MenuBar.vue`(时钟点击) | | `resetAll` | `src/stores/settings.ts` | ### 9.3 API 对照 | 原 API | 新 API | |---|---| | `Sys.settings.xxx` | `settingsStore.settings.xxx` | | `Sys.isDark()` | `settingsStore.isDark` (computed) | | `Sys.set('key', val)` | `settingsStore.settings.key = val` | | `Sys.renderDesktopIcons()` | `emitter.emit('fs:changed')` → 自动更新 | | `fs.node(path)` | `fsStore.node(path)` | | `fs.list(path)` | `fsStore.list(path)` | | `bus.emit('fs:changed', data)` | `emitter.emit('fs:changed', data)` | | `ui.menu(items, x, y)` | `uiStore.showMenu(items, x, y)` | | `ui.dialog({...})` | `uiStore.showDialog({...})` | | `ui.alert(title, msg)` | `uiStore.alert(title, msg)` | | `ui.confirm(title, msg)` | `uiStore.confirm(title, msg)` | | `wm.openWindow(opts)` | `wmStore.openWindow(opts)` | | `wm.close(win)` | `wmStore.close(winId)` | | `wm.focus(win)` | `wmStore.focus(winId)` | | `wm.activeWin.value` | `wmStore.activeWin` | | `Apps.open('finder', args)` | `appStore.open('finder', args)` | | `Apps.get('finder')` | `appStore.get('finder')` | | `Apps.register(def)` | `appStore.register(def)` | | `Notify.send({...})` | `notifyStore.send({...})` | | `Notify.badgeCount('mail')` | `notifyStore.badgeCount('mail')` | | `Spotlight.toggle()` | `spotlightStore.toggle()` | ### 9.4 保留的原生 API 以下原生 API 是必须保留的(封装在专用 composable 中): | API | 用途 | 封装位置 | |---|---|---| | `PointerEvent` (pointerdown/move/up/capture) | 窗口拖拽、缩放、桌面图标拖拽 | `useDrag.ts` / `useResize.ts` | | `localStorage` | 持久化(由 Pinia watch 自动处理) | 各 store 内部 | | `matchMedia('prefers-color-scheme: dark')` | 系统暗色模式检测 | `useTheme.ts` | | `navigator.clipboard` | 剪贴板操作 | `useClipboard.ts` | | `window.open(url, '_blank')` | Safari 外链打开 | `useSafari.ts` | | `requestAnimationFrame` | 启动动画 | `` 组件内 | | `Image()` | 壁纸预加载 | `useTheme.ts` | --- ## 11. 执行建议 ### 10.1 执行顺序 1. **安装依赖**:`pnpm add pinia mitt` 2. **阶段一**:创建所有 Pinia stores + mitt 事件总线,确保与原功能等价(此时还依赖 `el()` 等旧 API) 3. **阶段二**:逐个将系统 UI 组件化,每完成一个就用新组件替换旧的 `el()` 渲染,保持可运行 4. **阶段三**:窗口管理器重构 5. **阶段四**:应用层优化 + lazy load 6. **阶段五**:清理删除旧文件,更新测试 ### 10.2 关键原则 - **每步保持可运行**:不要一次性改完所有东西再测试;每个组件替换后立即验证 - **先测试后重构**:在重构前确保现有 47 个测试用例全部通过,重构后逐批更新测试 - **类型安全优先**:所有新增模块用完整 TypeScript 类型定义,消除 `any` - **CSS 不变**:现有 `styles/*.css` 无需修改,CSS 类名保持不变 ### 10.3 预估工作量 | 阶段 | 描述 | 预估 | |---|---|---| | 阶段一 | Pinia stores + mitt | 1-2 天 | | 阶段二 | 系统 UI 组件化(MenuBar/Dock/Spotlight/CC/通知/锁屏/屏保/启动/对话框/右键菜单 — 共 10+ 个组件) | 3-5 天 | | 阶段三 | WM 重构(WindowFrame/TrafficLights/AppLoader + drag/resize composable) | 1-2 天 | | 阶段四 | 33 个应用适配(去 render 方法、去 window 引用、lazy load) | 2-3 天 | | 阶段五 | 清理 + 测试更新 | 1-2 天 | | **总计** | | **8-14 天** |