@lobehub/lobehub 2.0.0-next.353 → 2.0.0-next.354
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/add-provider-doc/SKILL.md +90 -0
- package/.agents/skills/add-setting-env/SKILL.md +106 -0
- package/.agents/skills/debug/SKILL.md +66 -0
- package/.agents/skills/desktop/SKILL.md +78 -0
- package/.agents/skills/desktop/references/feature-implementation.md +99 -0
- package/.agents/skills/desktop/references/local-tools.md +133 -0
- package/.agents/skills/desktop/references/menu-config.md +103 -0
- package/.agents/skills/desktop/references/window-management.md +143 -0
- package/.agents/skills/drizzle/SKILL.md +129 -0
- package/.agents/skills/drizzle/references/db-migrations.md +50 -0
- package/.agents/skills/hotkey/SKILL.md +90 -0
- package/{.cursor/rules/i18n.mdc → .agents/skills/i18n/SKILL.md} +14 -23
- package/.agents/skills/linear/SKILL.md +51 -0
- package/.agents/skills/microcopy/SKILL.md +83 -0
- package/.agents/skills/modal/SKILL.md +102 -0
- package/{.cursor/rules/project-structure.mdc → .agents/skills/project-overview/SKILL.md} +65 -37
- package/.agents/skills/react/SKILL.md +73 -0
- package/.agents/skills/react/references/layout-kit.md +100 -0
- package/.agents/skills/recent-data/SKILL.md +108 -0
- package/.agents/skills/testing/SKILL.md +89 -0
- package/.agents/skills/testing/references/agent-runtime-e2e.md +126 -0
- package/.agents/skills/testing/references/db-model-test.md +124 -0
- package/.agents/skills/testing/references/desktop-controller-test.md +124 -0
- package/.agents/skills/testing/references/electron-ipc-test.md +63 -0
- package/.agents/skills/testing/references/zustand-store-action-test.md +150 -0
- package/.agents/skills/typescript/SKILL.md +52 -0
- package/.agents/skills/zustand/SKILL.md +78 -0
- package/.agents/skills/zustand/references/action-patterns.md +125 -0
- package/.agents/skills/zustand/references/slice-organization.md +125 -0
- package/AGENTS.md +42 -55
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +57 -46
- package/GEMINI.md +47 -39
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +2 -3
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +1 -2
- package/src/libs/pdfjs/index.tsx +25 -0
- package/src/store/test-coverage.md +5 -5
- package/.cursor/rules/add-provider-doc.mdc +0 -183
- package/.cursor/rules/add-setting-env.mdc +0 -175
- package/.cursor/rules/cursor-rules.mdc +0 -28
- package/.cursor/rules/db-migrations.mdc +0 -46
- package/.cursor/rules/debug-usage.mdc +0 -86
- package/.cursor/rules/desktop-controller-tests.mdc +0 -189
- package/.cursor/rules/desktop-feature-implementation.mdc +0 -155
- package/.cursor/rules/desktop-local-tools-implement.mdc +0 -81
- package/.cursor/rules/desktop-menu-configuration.mdc +0 -209
- package/.cursor/rules/desktop-window-management.mdc +0 -301
- package/.cursor/rules/drizzle-schema-style-guide.mdc +0 -218
- package/.cursor/rules/hotkey.mdc +0 -162
- package/.cursor/rules/linear.mdc +0 -53
- package/.cursor/rules/microcopy-cn.mdc +0 -158
- package/.cursor/rules/microcopy-en.mdc +0 -148
- package/.cursor/rules/modal-imperative.mdc +0 -162
- package/.cursor/rules/packages/react-layout-kit.mdc +0 -122
- package/.cursor/rules/project-introduce.mdc +0 -36
- package/.cursor/rules/react.mdc +0 -169
- package/.cursor/rules/recent-data-usage.mdc +0 -139
- package/.cursor/rules/rules-index.mdc +0 -44
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +0 -285
- package/.cursor/rules/testing-guide/db-model-test.mdc +0 -455
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +0 -80
- package/.cursor/rules/testing-guide/testing-guide.mdc +0 -534
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +0 -574
- package/.cursor/rules/typescript.mdc +0 -55
- package/.cursor/rules/zustand-action-patterns.mdc +0 -328
- package/.cursor/rules/zustand-slice-organization.mdc +0 -308
- package/src/libs/pdfjs/pdf.worker.ts +0 -1
- package/src/libs/pdfjs/worker.ts +0 -12
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-event-handler-refs.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-use-latest.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-api-routes.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-defer-await.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-parallel.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-suspense-boundaries.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-barrel-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-conditional.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-defer-third-party.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-dynamic-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-preload.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-localstorage-schema.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-passive-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-swr-dedup.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-batch-dom-css.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-function-results.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-property-access.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-storage.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-combine-iterations.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-early-exit.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-hoist-regexp.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-index-maps.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-length-check-first.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-min-max-loop.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-set-map-lookups.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-tosorted-immutable.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-activity.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-animate-svg-wrapper.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-conditional-render.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-content-visibility.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hoist-jsx.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hydration-no-flicker.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-svg-precision.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-defer-reads.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-derived-state.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-functional-setstate.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-lazy-state-init.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-memo.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-transitions.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-after-nonblocking.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-lru.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-react.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-parallel-fetching.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-serialization.md +0 -0
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description:
|
|
3
|
-
globs:
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# 桌面端菜单配置指南
|
|
8
|
-
|
|
9
|
-
## 菜单系统概述
|
|
10
|
-
|
|
11
|
-
LobeChat 桌面应用有三种主要的菜单类型:
|
|
12
|
-
|
|
13
|
-
1. **应用菜单 (App Menu)**:显示在应用窗口顶部(macOS)或窗口标题栏(Windows/Linux)
|
|
14
|
-
2. **上下文菜单 (Context Menu)**:右键点击时显示的菜单
|
|
15
|
-
3. **托盘菜单 (Tray Menu)**:点击系统托盘图标显示的菜单
|
|
16
|
-
|
|
17
|
-
## 菜单相关文件结构
|
|
18
|
-
|
|
19
|
-
```plaintext
|
|
20
|
-
apps/desktop/src/main/
|
|
21
|
-
├── menus/ # 菜单定义
|
|
22
|
-
│ ├── appMenu.ts # 应用菜单配置
|
|
23
|
-
│ ├── contextMenu.ts # 上下文菜单配置
|
|
24
|
-
│ └── factory.ts # 菜单工厂函数
|
|
25
|
-
├── controllers/
|
|
26
|
-
│ ├── MenuCtr.ts # 菜单控制器
|
|
27
|
-
│ └── TrayMenuCtr.ts # 托盘菜单控制器
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## 菜单配置流程
|
|
31
|
-
|
|
32
|
-
### 1. 应用菜单配置
|
|
33
|
-
|
|
34
|
-
应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
|
|
35
|
-
|
|
36
|
-
1. **导入依赖**
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, app } from 'electron';
|
|
40
|
-
import { is } from 'electron-util';
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
2. **定义菜单项**
|
|
44
|
-
- 使用 `MenuItemConstructorOptions` 类型定义菜单结构
|
|
45
|
-
- 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
|
|
46
|
-
|
|
47
|
-
3. **创建菜单工厂函数**
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
export const createAppMenu = (win: BrowserWindow) => {
|
|
51
|
-
const template = [
|
|
52
|
-
// 定义菜单项...
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
return Menu.buildFromTemplate(template);
|
|
56
|
-
};
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
4. **注册菜单**
|
|
60
|
-
- 在 `MenuCtr.ts` 控制器中使用 `Menu.setApplicationMenu(menu)` 设置应用菜单
|
|
61
|
-
|
|
62
|
-
### 2. 上下文菜单配置
|
|
63
|
-
|
|
64
|
-
上下文菜单通常在特定元素上右键点击时显示:
|
|
65
|
-
|
|
66
|
-
1. **在主进程中定义菜单模板**
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// apps/desktop/src/main/menus/contextMenu.ts
|
|
70
|
-
export const createContextMenu = () => {
|
|
71
|
-
const template = [
|
|
72
|
-
// 定义菜单项...
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
return Menu.buildFromTemplate(template);
|
|
76
|
-
};
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
2. **在适当的事件处理器中显示菜单**
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
const menu = createContextMenu();
|
|
83
|
-
menu.popup();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### 3. 托盘菜单配置
|
|
87
|
-
|
|
88
|
-
托盘菜单在 `TrayMenuCtr.ts` 中配置:
|
|
89
|
-
|
|
90
|
-
1. **创建托盘图标**
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
this.tray = new Tray(trayIconPath);
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
2. **定义托盘菜单**
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
const contextMenu = Menu.buildFromTemplate([
|
|
100
|
-
{ label: '显示主窗口', click: this.showMainWindow },
|
|
101
|
-
{ type: 'separator' },
|
|
102
|
-
{ label: '退出', click: () => app.quit() },
|
|
103
|
-
]);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
3. **设置托盘菜单**
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
this.tray.setContextMenu(contextMenu);
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## 多语言支持
|
|
113
|
-
|
|
114
|
-
为菜单添加多语言支持:
|
|
115
|
-
|
|
116
|
-
1. **导入本地化工具**
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { i18n } from '../locales';
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
2. **使用翻译函数**
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
const template = [
|
|
126
|
-
{
|
|
127
|
-
label: i18n.t('menu.file'),
|
|
128
|
-
submenu: [
|
|
129
|
-
{ label: i18n.t('menu.new'), click: createNew },
|
|
130
|
-
// ...
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
// ...
|
|
134
|
-
];
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
3. **在语言切换时更新菜单** 在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
|
|
138
|
-
|
|
139
|
-
## 添加新菜单项流程
|
|
140
|
-
|
|
141
|
-
1. **确定菜单位置**
|
|
142
|
-
- 决定添加到哪个菜单(应用菜单、上下文菜单或托盘菜单)
|
|
143
|
-
- 确定在菜单中的位置(主菜单项或子菜单项)
|
|
144
|
-
|
|
145
|
-
2. **定义菜单项**
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
const newMenuItem: MenuItemConstructorOptions = {
|
|
149
|
-
label: '新功能',
|
|
150
|
-
accelerator: 'CmdOrCtrl+N',
|
|
151
|
-
click: (_, window) => {
|
|
152
|
-
// 处理点击事件
|
|
153
|
-
if (window) window.webContents.send('trigger-new-feature');
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
3. **添加到菜单模板** 将新菜单项添加到相应的菜单模板中
|
|
159
|
-
|
|
160
|
-
4. **对于与渲染进程交互的功能**
|
|
161
|
-
- 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
|
|
162
|
-
- 在渲染进程中监听该消息并处理
|
|
163
|
-
|
|
164
|
-
## 菜单项启用/禁用控制
|
|
165
|
-
|
|
166
|
-
动态控制菜单项状态:
|
|
167
|
-
|
|
168
|
-
1. **保存对菜单项的引用**
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
this.menuItems = {};
|
|
172
|
-
const menu = Menu.buildFromTemplate(template);
|
|
173
|
-
this.menuItems.newFeature = menu.getMenuItemById('new-feature');
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
2. **根据条件更新状态**
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
updateMenuState(state) {
|
|
180
|
-
if (this.menuItems.newFeature) {
|
|
181
|
-
this.menuItems.newFeature.enabled = state.canUseNewFeature;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## 最佳实践
|
|
187
|
-
|
|
188
|
-
1. **使用标准角色**
|
|
189
|
-
- 尽可能使用 Electron 预定义的角色(如 `role: 'copy'`)以获得本地化和一致的行为
|
|
190
|
-
|
|
191
|
-
2. **平台特定菜单**
|
|
192
|
-
- 使用 `process.platform` 检查为不同平台提供不同菜单
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
if (process.platform === 'darwin') {
|
|
196
|
-
template.unshift({ role: 'appMenu' });
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
3. **快捷键冲突**
|
|
201
|
-
- 避免与系统快捷键冲突
|
|
202
|
-
- 使用 `CmdOrCtrl` 代替 `Ctrl` 以支持 macOS 和 Windows/Linux
|
|
203
|
-
|
|
204
|
-
4. **保持菜单简洁**
|
|
205
|
-
- 避免过多嵌套的子菜单
|
|
206
|
-
- 将相关功能分组在一起
|
|
207
|
-
|
|
208
|
-
5. **添加分隔符**
|
|
209
|
-
- 使用 `{ type: 'separator' }` 在逻辑上分隔不同组的菜单项
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description:
|
|
3
|
-
globs:
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# 桌面端窗口管理指南
|
|
8
|
-
|
|
9
|
-
## 窗口管理概述
|
|
10
|
-
|
|
11
|
-
LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。主要的窗口管理功能包括:
|
|
12
|
-
|
|
13
|
-
1. **窗口创建和配置**
|
|
14
|
-
2. **窗口状态管理**(大小、位置、最大化等)
|
|
15
|
-
3. **多窗口协调**
|
|
16
|
-
4. **窗口事件处理**
|
|
17
|
-
|
|
18
|
-
## 相关文件结构
|
|
19
|
-
|
|
20
|
-
```plaintext
|
|
21
|
-
apps/desktop/src/main/
|
|
22
|
-
├── appBrowsers.ts # 窗口管理的核心文件
|
|
23
|
-
├── controllers/
|
|
24
|
-
│ └── BrowserWindowsCtr.ts # 窗口控制器
|
|
25
|
-
└── modules/
|
|
26
|
-
└── browserWindowManager.ts # 窗口管理模块
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## 窗口管理流程
|
|
30
|
-
|
|
31
|
-
### 1. 窗口创建
|
|
32
|
-
|
|
33
|
-
在 `appBrowsers.ts` 或 `BrowserWindowsCtr.ts` 中定义窗口创建逻辑:
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
export const createMainWindow = () => {
|
|
37
|
-
const mainWindow = new BrowserWindow({
|
|
38
|
-
width: 1200,
|
|
39
|
-
height: 800,
|
|
40
|
-
minWidth: 600,
|
|
41
|
-
minHeight: 400,
|
|
42
|
-
webPreferences: {
|
|
43
|
-
preload: path.join(__dirname, '../preload/index.js'),
|
|
44
|
-
contextIsolation: true,
|
|
45
|
-
nodeIntegration: false,
|
|
46
|
-
},
|
|
47
|
-
// 其他窗口配置项...
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// 加载应用内容
|
|
51
|
-
if (isDev) {
|
|
52
|
-
mainWindow.loadURL('http://localhost:3000');
|
|
53
|
-
mainWindow.webContents.openDevTools();
|
|
54
|
-
} else {
|
|
55
|
-
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return mainWindow;
|
|
59
|
-
};
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 2. 窗口状态管理
|
|
63
|
-
|
|
64
|
-
实现窗口状态持久化保存和恢复:
|
|
65
|
-
|
|
66
|
-
1. **保存窗口状态**
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
const saveWindowState = (window: BrowserWindow) => {
|
|
70
|
-
if (!window.isMinimized() && !window.isMaximized()) {
|
|
71
|
-
const position = window.getPosition();
|
|
72
|
-
const size = window.getSize();
|
|
73
|
-
|
|
74
|
-
settings.set('windowState', {
|
|
75
|
-
x: position[0],
|
|
76
|
-
y: position[1],
|
|
77
|
-
width: size[0],
|
|
78
|
-
height: size[1],
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
2. **恢复窗口状态**
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
const restoreWindowState = (window: BrowserWindow) => {
|
|
88
|
-
const savedState = settings.get('windowState');
|
|
89
|
-
|
|
90
|
-
if (savedState) {
|
|
91
|
-
window.setBounds({
|
|
92
|
-
x: savedState.x,
|
|
93
|
-
y: savedState.y,
|
|
94
|
-
width: savedState.width,
|
|
95
|
-
height: savedState.height,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
3. **监听窗口事件**
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
window.on('close', () => saveWindowState(window));
|
|
105
|
-
window.on('moved', () => saveWindowState(window));
|
|
106
|
-
window.on('resized', () => saveWindowState(window));
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### 3. 实现多窗口管理
|
|
110
|
-
|
|
111
|
-
对于需要多窗口支持的功能:
|
|
112
|
-
|
|
113
|
-
1. **跟踪窗口**
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
export class WindowManager {
|
|
117
|
-
private windows: Map<string, BrowserWindow> = new Map();
|
|
118
|
-
|
|
119
|
-
createWindow(id: string, options: BrowserWindowConstructorOptions) {
|
|
120
|
-
const window = new BrowserWindow(options);
|
|
121
|
-
this.windows.set(id, window);
|
|
122
|
-
|
|
123
|
-
window.on('closed', () => {
|
|
124
|
-
this.windows.delete(id);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
return window;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
getWindow(id: string) {
|
|
131
|
-
return this.windows.get(id);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
getAllWindows() {
|
|
135
|
-
return Array.from(this.windows.values());
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
2. **窗口间通信**
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// 从一个窗口向另一个窗口发送消息
|
|
144
|
-
sendMessageToWindow(targetWindowId, channel, data) {
|
|
145
|
-
const targetWindow = this.getWindow(targetWindowId);
|
|
146
|
-
if (targetWindow) {
|
|
147
|
-
targetWindow.webContents.send(channel, data);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### 4. 窗口与渲染进程通信
|
|
153
|
-
|
|
154
|
-
通过 IPC 实现窗口操作:
|
|
155
|
-
|
|
156
|
-
1. **在主进程中注册 IPC 处理器**
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
|
160
|
-
import { BrowserWindow } from 'electron';
|
|
161
|
-
|
|
162
|
-
import { ControllerModule, IpcMethod } from '@/controllers';
|
|
163
|
-
|
|
164
|
-
export default class BrowserWindowsCtr extends ControllerModule {
|
|
165
|
-
static override readonly groupName = 'windows';
|
|
166
|
-
|
|
167
|
-
@IpcMethod()
|
|
168
|
-
minimizeWindow() {
|
|
169
|
-
const focusedWindow = BrowserWindow.getFocusedWindow();
|
|
170
|
-
focusedWindow?.minimize();
|
|
171
|
-
return { success: true };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
@IpcMethod()
|
|
175
|
-
maximizeWindow() {
|
|
176
|
-
const focusedWindow = BrowserWindow.getFocusedWindow();
|
|
177
|
-
if (focusedWindow?.isMaximized()) focusedWindow.restore();
|
|
178
|
-
else focusedWindow?.maximize();
|
|
179
|
-
return { success: true };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
@IpcMethod()
|
|
183
|
-
closeWindow() {
|
|
184
|
-
BrowserWindow.getFocusedWindow()?.close();
|
|
185
|
-
return { success: true };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
- `@IpcMethod()` 根据控制器的 `groupName` 自动将方法映射为 `windows.minimizeWindow` 形式的通道名称。
|
|
191
|
-
- 控制器需继承 `ControllerModule`,并在 `controllers/registry.ts` 中通过 `controllerIpcConstructors` 注册,便于类型生成。
|
|
192
|
-
|
|
193
|
-
2. **在渲染进程中调用**
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// src/services/electron/windowService.ts
|
|
197
|
-
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
|
198
|
-
|
|
199
|
-
const ipc = ensureElectronIpc();
|
|
200
|
-
|
|
201
|
-
export const windowService = {
|
|
202
|
-
minimize: () => ipc.windows.minimizeWindow(),
|
|
203
|
-
maximize: () => ipc.windows.maximizeWindow(),
|
|
204
|
-
close: () => ipc.windows.closeWindow(),
|
|
205
|
-
};
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
- `ensureElectronIpc()` 会基于 `DesktopIpcServices` 运行时生成 Proxy,并通过 `window.electronAPI.invoke` 与主进程通信;不再直接使用 `dispatch`。
|
|
209
|
-
|
|
210
|
-
### 5. 自定义窗口控制 (无边框窗口)
|
|
211
|
-
|
|
212
|
-
对于自定义窗口标题栏:
|
|
213
|
-
|
|
214
|
-
1. **创建无边框窗口**
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
const window = new BrowserWindow({
|
|
218
|
-
frame: false,
|
|
219
|
-
titleBarStyle: 'hidden',
|
|
220
|
-
// 其他选项...
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
2. **在渲染进程中实现拖拽区域**
|
|
225
|
-
|
|
226
|
-
```css
|
|
227
|
-
/* CSS */
|
|
228
|
-
.titlebar {
|
|
229
|
-
-webkit-app-region: drag;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.titlebar-button {
|
|
233
|
-
-webkit-app-region: no-drag;
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## 最佳实践
|
|
238
|
-
|
|
239
|
-
1. **性能考虑**
|
|
240
|
-
- 避免创建过多窗口
|
|
241
|
-
- 使用 `show: false` 创建窗口,在内容加载完成后再显示,避免白屏
|
|
242
|
-
|
|
243
|
-
2. **安全性**
|
|
244
|
-
- 始终设置适当的 `webPreferences` 确保安全
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
webPreferences: {
|
|
248
|
-
preload: path.join(__dirname, '../preload/index.js'),
|
|
249
|
-
contextIsolation: true,
|
|
250
|
-
nodeIntegration: false,
|
|
251
|
-
sandbox: true,
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
3. **跨平台兼容性**
|
|
256
|
-
- 考虑不同操作系统的窗口行为差异
|
|
257
|
-
- 使用 `process.platform` 为不同平台提供特定实现
|
|
258
|
-
|
|
259
|
-
4. **崩溃恢复**
|
|
260
|
-
- 监听 `webContents.on('crashed')` 事件处理崩溃
|
|
261
|
-
- 提供崩溃恢复选项
|
|
262
|
-
|
|
263
|
-
5. **内存管理**
|
|
264
|
-
- 确保窗口关闭时清理所有相关资源
|
|
265
|
-
- 使用 `window.on('closed')` 而不是 `window.on('close')` 进行最终清理
|
|
266
|
-
|
|
267
|
-
## 示例:创建设置窗口
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
|
271
|
-
import type { OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
|
|
272
|
-
|
|
273
|
-
import { ControllerModule, IpcMethod } from '@/controllers';
|
|
274
|
-
|
|
275
|
-
export default class BrowserWindowsCtr extends ControllerModule {
|
|
276
|
-
static override readonly groupName = 'windows';
|
|
277
|
-
|
|
278
|
-
@IpcMethod()
|
|
279
|
-
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
|
|
280
|
-
const normalizedOptions =
|
|
281
|
-
typeof options === 'string' || options === undefined
|
|
282
|
-
? { tab: typeof options === 'string' ? options : undefined }
|
|
283
|
-
: options;
|
|
284
|
-
|
|
285
|
-
const mainWindow = this.app.browserManager.getMainWindow();
|
|
286
|
-
const query = new URLSearchParams();
|
|
287
|
-
if (normalizedOptions.tab) query.set('active', normalizedOptions.tab);
|
|
288
|
-
if (normalizedOptions.searchParams) {
|
|
289
|
-
for (const [key, value] of Object.entries(normalizedOptions.searchParams)) {
|
|
290
|
-
if (value) query.set(key, value);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const fullPath = `/settings${query.size ? `?${query.toString()}` : ''}`;
|
|
295
|
-
await mainWindow.loadUrl(fullPath);
|
|
296
|
-
mainWindow.show();
|
|
297
|
-
|
|
298
|
-
return { success: true };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|