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

424 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_MAXIMIZED``send`/`on` 混合) | **废弃,改为 `invoke/handle` 对齐 ClawX** |
| 图标库 | `@iconify/vue` + `@iconify-json/material-symbols` | **废弃,替换为 `@lucide/vue`** |
### 1.3 核心差异
1. **macOS**ClawX 使用原生 traffic lightszn-ai 使用自定义按钮(将原生按钮移出可视区域),行为不完整。
2. **Linux**ClawX 保留原生边框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 既有代码可抛弃**`HeaderBar``DragRegion``useWinManager` 及相关 IPC 直接删除,不用做兼容改造。
- **UI 视觉延用 zn-ai**Windows 按钮的 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.ts``HeaderBar` / `DragRegion` 的全局注册 | 删除全局组件注册 |
### 2.3 废弃的 IPC 与常量
zn-ai 现有窗口控制 IPC基于 `send`/`on`
```ts
// constants.ts 中废弃以下事件
WINDOW_MINIMIZE = 'window-minimize'
WINDOW_MAXIMIZE = 'window-maximize'
WINDOW_CLOSE = 'window-close'
IS_WINDOW_MAXIMIZED = 'is-window-maximized'
```
**废弃后替换为 ClawX 风格的 `invoke` channel**
```ts
// 新增(对齐 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.tsx`Vue 实现。 |
| `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**
```ts
// 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`,方便模块化):
```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.ts``electron/wins/index.ts` 的适当时机调用 `registerWindowHandlers(mainWindow)`
### 2.7 Preload 改造(对齐 ClawX `invokeIpc` 风格)
```ts
// 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 建议新增该辅助函数以保持一致风格:
```ts
// 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`
```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-white``text-[#525866]`),保持视觉一致即可。
### 2.10 Layout 重写
**重写文件**`src/components/Layout/index.vue`
```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` 中删除 `HeaderBar``DragRegion` 的全局注册。
### 2.11 Icon 体系迁移iconify → @lucide/vue
**依赖变更**
```bash
# 安装
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/vue``declare module` 段落(约第 230-240 行)。
---
## 3. 改造清单Task Breakdown
### Task 1主进程窗口配置重写
- **文件**`electron/service/window-service/index.ts`
- **内容**
1. 引入 `isMac` / `isWindows` 平台判断。
2. 重写 `SHARED_WINDOW_OPTIONS``frame``titleBarStyle``trafficLightPosition`
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:minimize``window:maximize``window:close``window:isMaximized`
2.`electron/wins/index.ts``electron/main.ts` 中引入并调用 `registerWindowHandlers(mainWindow)`
- **验收标准**
- 通过 DevTools Console 测试 `await window.api.windowIsMaximized()` 返回布尔值。
- `windowMinimize` / `windowMaximize` / `windowClose` 调用后窗口行为正确。
### Task 3Preload 重写
- **文件**`electron/preload/index.ts`
- **内容**
1. 移除旧的 `minimizeWindow``maximizeWindow``closeWindow``onWindowMaximized``isWindowMaximized`
2. 新增 `platform``windowMinimize``windowMaximize``windowClose``windowIsMaximized`
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`(改用 `TitleBar`Linux 下隐藏)
- `src/main.ts`(移除 `HeaderBar` / `DragRegion` 全局注册)
- **验收标准**
- macOS 仅显示顶部 40px 拖拽区域,无自定义按钮。
- Windows 显示自定义最小化/最大化/关闭按钮,功能正常。
- Linux 不渲染任何自定义标题栏元素。
### Task 5依赖清理
- **文件**`package.json``global.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_EVENTS``WINDOW_MINIMIZE` / `WINDOW_MAXIMIZE` / `WINDOW_CLOSE` / `IS_WINDOW_MAXIMIZED` 被其他未发现的模块引用,删除后会导致编译失败。建议在删除前用全局搜索再次确认。
---
## 6. 参考资料
- ClawX 主窗口创建:`ClawX/electron/main/index.ts`
- ClawX TitleBar 组件:`ClawX/src/components/layout/TitleBar.tsx`
- ClawX 窗口 IPC`ClawX/electron/main/ipc-handlers.ts``registerWindowHandlers`
- zn-ai 现有窗口服务:`zn-ai/electron/service/window-service/index.ts`
- zn-ai 现有 HeaderBar`zn-ai/src/components/HeaderBar/index.vue`