Files
zn-ai/docs/WindowControlsMigrationPlan.md
duanshuwen b5a67ff650 feat: implement custom window controls and replace header bar with title bar
- Add window handlers for minimize, maximize, close, and check if maximized in ipcMain.
- Update preload script to use new window control IPC events.
- Refactor window service to remove old IPC event handlers and use new handlers.
- Remove old HeaderBar and DragRegion components, replacing them with a new TitleBar component.
- Update Layout component to use TitleBar instead of HeaderBar.
- Remove useWinManager hook as its functionality is now integrated into TitleBar.
- Update login page to remove HeaderBar and adjust layout accordingly.
- Update constants to remove old window IPC events.
- Update package dependencies to replace @iconify/vue with @lucide/vue.
2026-04-14 23:38:42 +08:00

17 KiB
Raw Blame History

Window Controls 迁移计划(对齐 ClawX

目标:将 zn-ai 的窗口控制体系全面对齐 ClawX,废弃 zn-ai 现有相关代码按平台重建跨平台标题栏方案。UI 视觉延用 zn-ai 设计,图标库统一使用 @lucide/vue


1. 现状分析

1.1 ClawX 实现思路

平台 方案 关键配置
macOS 原生 traffic lights titleBarStyle: 'hiddenInset'
Windows 自定义 React 标题栏 titleBarStyle: 'hidden' + frame: false
Linux 保留原生窗口边框 frame: true(默认)
  • macOS:前端仅渲染一个 drag-region(高度 40px红绿灯由系统原生绘制。
  • Windows:前端自定义最小化、最大化/恢复、关闭三个按钮,通过 invoke IPC 调用主进程 BrowserWindow API。
  • Linux:出于 IME 兼容性考虑,不隐藏原生边框,前端不渲染任何自定义标题栏。

1.2 zn-ai 现状(待废弃)

现状 决策
主进程窗口配置 所有平台统一 frame: false + titleBarStyle: 'hidden' 废弃,重写
前端组件 HeaderBar/index.vue 同时包含 macOS 自定义按钮 + Windows 自定义按钮 废弃,删除
窗口管理 Hook useWinManager.ts 封装 ref + onMounted 状态管理 废弃,删除
拖拽区域组件 DragRegion/index.vue 独立组件 废弃,删除(直接内联到 TitleBar
Layout 集成 Layout/index.vue 嵌套 header-bar + drag-region 重写
IPC 通信 WINDOW_CLOSE / WINDOW_MINIMIZE / WINDOW_MAXIMIZE / IS_WINDOW_MAXIMIZEDsend/on 混合) 废弃,改为 invoke/handle 对齐 ClawX
图标库 @iconify/vue + @iconify-json/material-symbols 废弃,替换为 @lucide/vue

1.3 核心差异

  1. macOSClawX 使用原生 traffic lightszn-ai 使用自定义按钮(将原生按钮移出可视区域),行为不完整。
  2. LinuxClawX 保留原生边框zn-ai 隐藏了原生边框,存在 IME 兼容性风险。
  3. IPC 模式ClawX 使用 ipcMain.handle + invokeIpc(请求-响应zn-ai 使用 ipcMain.on + ipcRenderer.send(事件驱动),模式不统一。
  4. 组件层级ClawX 的 TitleBar 是一个自包含组件,内部自带 drag-region/no-drag 逻辑zn-ai 将其拆成了 HeaderBar + DragRegion + useWinManager 三个文件,过度拆分。

2. 迁移方案

2.1 设计原则

  • 全面对齐 ClawX架构、IPC 模式、平台分支逻辑直接复刻 ClawX 实现。
  • zn-ai 既有代码可抛弃HeaderBarDragRegionuseWinManager 及相关 IPC 直接删除,不用做兼容改造。
  • UI 视觉延用 zn-aiWindows 按钮的 hover 颜色、尺寸保持 zn-ai 现有设计(#999 / #ff0000)。
  • 图标统一使用 @lucide/vue:废弃 @iconify/vue

2.2 废弃清单Delete List

文件/目录 说明
src/components/HeaderBar/index.vue 既有自定义按钮组件,完全废弃
src/components/DragRegion/index.vue 拖拽区域组件,逻辑内联到新的 TitleBar
src/hooks/useWinManager.ts 窗口管理 Hook逻辑内联到新的 TitleBar
src/main.tsHeaderBar / DragRegion 的全局注册 删除全局组件注册

2.3 废弃的 IPC 与常量

zn-ai 现有窗口控制 IPC基于 send/on

// constants.ts 中废弃以下事件
WINDOW_MINIMIZE = 'window-minimize'
WINDOW_MAXIMIZE = 'window-maximize'
WINDOW_CLOSE    = 'window-close'
IS_WINDOW_MAXIMIZED = 'is-window-maximized'

废弃后替换为 ClawX 风格的 invoke channel

// 新增(对齐 ClawX
'window:minimize'
'window:maximize'
'window:close'
'window:isMaximized'

若项目中其他模块也使用了旧的 WINDOW_MINIMIZE 等常量,需要一并迁移。经排查,旧常量仅在 HeaderBar / useWinManager / window-service / preload 中使用,可随本次重构一并删除。

2.4 新建/重写清单Create/Rewrite List

新建/重写项 说明
src/components/layout/TitleBar/index.vue 新建。对齐 ClawX TitleBar.tsxVue 实现。
src/components/layout/Layout/index.vue 重写。移除 header-bar/drag-region 引用,改用 TitleBar
electron/service/window-service/index.ts 重写 SHARED_WINDOW_OPTIONS。按平台设置 frame/titleBarStyle/trafficLightPosition
electron/main.ts 或新建 electron/main/ipc-handlers.ts 新增/重写。注册 window:*ipcMain.handle
electron/preload/index.ts 重写。移除旧 send/on API新增 invoke('window:*')platform
src/lib/api-client.ts(或类似文件) 新增 invokeIpc 辅助函数。对齐 ClawX 的 invokeIpc 调用风格。
src/styles/index.css 保留 .drag-region / .no-drag,供 TitleBar 使用。

2.5 主进程窗口配置改造

目标代码(对齐 ClawX

// electron/service/window-service/index.ts
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const useCustomTitleBar = isWindows;

const SHARED_WINDOW_OPTIONS = {
  frame: isMac || !useCustomTitleBar,
  titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default',
  trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
  show: false,
  title: 'NIANXX',
  darkTheme: themeManager.isDark,
  backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF',
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    sandbox: true,
    backgroundThrottling: false,
    preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
      ? path.join(process.cwd(), 'dist-electron/preload/preload.js')
      : path.join(__dirname, 'preload.js'),
  },
} as BrowserWindowConstructorOptions;

注意

  • 删除旧的 trafficLightPosition: { x: -100, y: -100 }
  • macOS 使用 hiddenInset 后,红绿灯由系统原生绘制,前端无需硬编码左侧占位。

2.6 新增 IPC Handlers对齐 ClawX

新建或复用文件(建议新建 electron/ipc/window-handlers.ts,方便模块化):

import { ipcMain, BrowserWindow } from 'electron';

export function registerWindowHandlers(mainWindow: BrowserWindow): void {
  ipcMain.handle('window:minimize', () => {
    mainWindow.minimize();
  });

  ipcMain.handle('window:maximize', () => {
    if (mainWindow.isMaximized()) {
      mainWindow.unmaximize();
    } else {
      mainWindow.maximize();
    }
  });

  ipcMain.handle('window:close', () => {
    mainWindow.close();
  });

  ipcMain.handle('window:isMaximized', () => {
    return mainWindow.isMaximized();
  });
}

electron/main.tselectron/wins/index.ts 的适当时机调用 registerWindowHandlers(mainWindow)

2.7 Preload 改造(对齐 ClawX invokeIpc 风格)

// electron/preload/index.ts
const api: WindowApi = {
  // ... 保留既有 API

  platform: process.platform,

  // 窗口控制 — 对齐 ClawX 的 invoke 风格
  windowMinimize: () => ipcRenderer.invoke('window:minimize'),
  windowMaximize: () => ipcRenderer.invoke('window:maximize'),
  windowClose: () => ipcRenderer.invoke('window:close'),
  windowIsMaximized: () => ipcRenderer.invoke('window:isMaximized'),

  // 移除旧的 minimizeWindow / maximizeWindow / closeWindow / onWindowMaximized / isWindowMaximized
}

2.8 前端 invokeIpc 辅助函数(可选,强烈建议)

ClawX 使用统一的 invokeIpc 函数调用主进程zn-ai 建议新增该辅助函数以保持一致风格:

// src/lib/api-client.ts
export function invokeIpc(channel: string, ...args: any[]): Promise<any> {
  return window.api.invoke(channel, ...args);
}

如果 zn-ai 已有类似封装(如 window.api.invoke),可直接使用,无需额外文件。

2.9 新建 TitleBar 组件(对齐 ClawX

新建文件src/components/layout/TitleBar/index.vue

<template>
  <!-- macOS: 仅渲染拖拽区域 -->
  <div v-if="platform === 'darwin'" class="drag-region h-10 shrink-0 border-b bg-background" />

  <!-- Linux: 不渲染任何自定义标题栏 -->
  <template v-else-if="platform !== 'win32'" />

  <!-- Windows: 自定义标题栏 -->
  <div v-else class="drag-region flex h-10 shrink-0 items-center justify-end border-b bg-background">
    <div class="no-drag flex h-full">
      <button
        @click="handleMinimize"
        class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#999] hover:text-white transition-colors"
        title="Minimize"
      >
        <Minus class="h-4 w-4" />
      </button>
      <button
        @click="handleMaximize"
        class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#999] hover:text-white transition-colors"
        :title="maximized ? 'Restore' : 'Maximize'"
      >
        <Copy v-if="maximized" class="h-3.5 w-3.5" />
        <Square v-else class="h-3.5 w-3.5" />
      </button>
      <button
        @click="handleClose"
        class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#ff0000] hover:text-white transition-colors"
        title="Close"
      >
        <X class="h-4 w-4" />
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Minus, Square, Copy, X } from '@lucide/vue'

const platform = window.api?.platform ?? '';
const maximized = ref(false);

onMounted(async () => {
  maximized.value = await window.api.windowIsMaximized();
});

const handleMinimize = () => {
  window.api.windowMinimize();
};

const handleMaximize = async () => {
  await window.api.windowMaximize();
  maximized.value = await window.api.windowIsMaximized();
};

const handleClose = () => {
  window.api.windowClose();
};
</script>

样式说明

  • .drag-region / .no-drag 复用 src/styles/index.css 中已有定义。
  • bg-background / text-muted-foreground 等 Token 若 zn-ai 未定义,可替换为具体色值(如 bg-whitetext-[#525866]),保持视觉一致即可。

2.10 Layout 重写

重写文件src/components/Layout/index.vue

<template>
  <div class="bg-color h-screen flex flex-col">
    <!-- Linux 不渲染 TitleBar -->
    <title-bar v-if="platform !== 'linux'" />

    <main class="box-border w-full flex pt-2 pb-2 pl-2"
      :style="{ height: platform === 'linux' ? '100vh' : 'calc(100vh - 40px)' }">
      <div class="flex-1 flex">
        <slot />
      </div>
      <SideMenus />
    </main>
  </div>
</template>

<script setup lang="ts" name="Layout">
import { computed } from 'vue'
import SideMenus from '@src/components/SideMenus/index.vue'
import TitleBar from '@components/layout/TitleBar/index.vue'

const platform = computed(() => window.api?.platform ?? '')
</script>

注意src/main.ts 中删除 HeaderBarDragRegion 的全局注册。

2.11 Icon 体系迁移iconify → @lucide/vue

依赖变更

# 安装
pnpm add @lucide/vue

# 卸载(确认项目内无其他 iconify 引用后执行)
pnpm remove @iconify/vue @iconify-json/material-symbols

图标映射关系

功能 原 iconify icon lucide-vue 替代
最小化 material-symbols:check-indeterminate-small Minus
最大化 material-symbols:chrome-maximize-outline-sharp Square
恢复 material-symbols:chrome-restore-outline-sharp Copy
关闭 material-symbols:close X

类型声明清理 删除 global.d.ts@iconify/vuedeclare module 段落(约第 230-240 行)。


3. 改造清单Task Breakdown

Task 1主进程窗口配置重写

  • 文件electron/service/window-service/index.ts
  • 内容
    1. 引入 isMac / isWindows 平台判断。
    2. 重写 SHARED_WINDOW_OPTIONSframetitleBarStyletrafficLightPosition
    3. 移除 trafficLightPosition: { x: -100, y: -100 }
  • 验收标准
    • macOS 窗口出现原生 traffic lights。
    • Windows 窗口无原生标题栏。
    • Linux 窗口保留原生标题栏。

Task 2新增 IPC Handlers对齐 ClawX

  • 新建文件electron/ipc/window-handlers.ts(或写入现有 ipc 文件)
  • 内容
    1. 使用 ipcMain.handle 注册 window:minimizewindow:maximizewindow:closewindow:isMaximized
    2. electron/wins/index.tselectron/main.ts 中引入并调用 registerWindowHandlers(mainWindow)
  • 验收标准
    • 通过 DevTools Console 测试 await window.api.windowIsMaximized() 返回布尔值。
    • windowMinimize / windowMaximize / windowClose 调用后窗口行为正确。

Task 3Preload 重写

  • 文件electron/preload/index.ts
  • 内容
    1. 移除旧的 minimizeWindowmaximizeWindowcloseWindowonWindowMaximizedisWindowMaximized
    2. 新增 platformwindowMinimizewindowMaximizewindowClosewindowIsMaximized
    3. 同步更新 global.d.ts 中的 WindowApi 类型定义。
  • 验收标准
    • 渲染进程 TypeScript 无报错。
    • window.api.platform 能正确读取当前平台。

Task 4前端组件废弃与重建

  • 废弃文件
    • src/components/HeaderBar/index.vue(删除)
    • src/components/DragRegion/index.vue(删除)
    • src/hooks/useWinManager.ts(删除)
  • 新建文件
    • src/components/layout/TitleBar/index.vue(对齐 ClawX TitleBar.tsx
  • 重写文件
    • src/components/Layout/index.vue(改用 TitleBarLinux 下隐藏)
    • src/main.ts(移除 HeaderBar / DragRegion 全局注册)
  • 验收标准
    • macOS 仅显示顶部 40px 拖拽区域,无自定义按钮。
    • Windows 显示自定义最小化/最大化/关闭按钮,功能正常。
    • Linux 不渲染任何自定义标题栏元素。

Task 5依赖清理

  • 文件package.jsonglobal.d.ts
  • 内容
    1. 安装 @lucide/vue
    2. 卸载 @iconify/vue@iconify-json/material-symbols
    3. 删除 global.d.ts@iconify/vue 的类型声明。
  • 验收标准
    • pnpm install 后无 iconify 包残留。
    • 项目编译通过。

Task 6跨平台回归测试

  • 内容
    1. macOS:验证 traffic lights 位置、拖拽行为、窗口最大化/恢复、关闭。
    2. Windows:验证自定义按钮 hover 样式、最大化/恢复状态切换、最小化到任务栏、关闭。
    3. Linux:验证原生边框存在、无自定义标题栏残留、窗口操作正常。

4. 工作量评估Sub Agent 分配)

本任务涉及 Electron 主进程前端组件重建跨平台回归 三个独立领域,推荐由 3 个 Sub Agent 并行/串行完成:

Agent 1Electron 主进程专家

  • 职责Task 1 + Task 2 + Task 3
  • 技能要求:熟悉 Electron BrowserWindow 选项、IPC handle/invoke 模式、preload 安全模型。
  • 预计耗时23 小时
  • 产出window-service + ipc-handlers + preload 的改造 PR。

Agent 2Vue 前端组件专家

  • 职责Task 4 + Task 5
  • 技能要求:熟悉 Vue 3、组件重构、Tailwind CSS、依赖清理。
  • 预计耗时24 小时
  • 产出TitleBar / Layout 重建 PR + 依赖清理补丁。

Agent 3跨平台测试与集成专家

  • 职责Task 6回归测试+ 边缘情况修复
  • 技能要求:能在 macOS / Windows / Linux 环境下运行 Electron 应用,熟悉 frameless 窗口陷阱。
  • 预计耗时24 小时(取决于平台覆盖度)
  • 产出:测试报告 + 对 Agent 1/2 PR 的修复补丁。

5. 风险与注意事项

  1. macOS traffic light 位置hiddenInset 默认内边距可能因 Electron 版本略有差异,建议通过 trafficLightPosition 微调至 {x: 16, y: 16}(与 ClawX 一致)。
  2. Linux 原生边框与主题:若 zn-ai 深色模式下 Linux 原生标题栏颜色不匹配,可后续通过 GTK 主题或 darkTheme 选项优化,但不在本次核心迁移范围内。
  3. Windows 最大化白边frame: false 的窗口在 Windows 最大化时可能出现 1px 白边。ClawX 未做特殊处理,可后续按需优化。
  4. 关闭行为一致性zn-ai 主窗口关闭逻辑原会根据 MINIMIZE_TO_TRAY 配置决定 hide()close()。改造后 window:close 直接调用 mainWindow.close(),若需保留托盘最小化逻辑,可在 window:close handler 中判断 MINIMIZE_TO_TRAY 并调用 mainWindow.hide() 阻止默认关闭。
  5. 旧常量清理:若 IPC_EVENTSWINDOW_MINIMIZE / WINDOW_MAXIMIZE / WINDOW_CLOSE / IS_WINDOW_MAXIMIZED 被其他未发现的模块引用,删除后会导致编译失败。建议在删除前用全局搜索再次确认。

6. 参考资料

  • ClawX 主窗口创建:ClawX/electron/main/index.ts
  • ClawX TitleBar 组件:ClawX/src/components/layout/TitleBar.tsx
  • ClawX 窗口 IPCClawX/electron/main/ipc-handlers.tsregisterWindowHandlers
  • zn-ai 现有窗口服务:zn-ai/electron/service/window-service/index.ts
  • zn-ai 现有 HeaderBarzn-ai/src/components/HeaderBar/index.vue