From a6f83fdfc3fe3d1215a9cab2fd3ffe4ca559a28c Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:40:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AA=97=E5=8F=A3=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E6=A0=8F=E5=92=8C=E7=AA=97=E5=8F=A3=E6=8E=A7=E5=88=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现自定义标题栏组件,包含最小化、最大化/还原和关闭按钮 添加窗口管理相关IPC通信接口 在设置页面增加开发者工具切换按钮 更新.gitignore忽略.trae目录 --- .gitignore | 1 + src/main/services/WindowManager.ts | 50 +++++++ src/preload/index.ts | 10 ++ src/preload/types.ts | 6 + src/renderer/src/assets/logo.svg | 37 +++++ src/renderer/src/components/Settings.tsx | 27 ++++ src/renderer/src/components/TitleBar.tsx | 135 ++++++++++++++++++ .../src/components/WindowControls.tsx | 80 +++++++++++ 8 files changed, 346 insertions(+) create mode 100644 src/renderer/src/assets/logo.svg create mode 100644 src/renderer/src/components/TitleBar.tsx create mode 100644 src/renderer/src/components/WindowControls.tsx diff --git a/.gitignore b/.gitignore index 1ab4fe2..338e429 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ db.sqlite !.vscode/extensions.json !.vscode/launch.json !.vscode/settings.json +/.trae/ \ No newline at end of file diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index 394236b..861ab55 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -62,6 +62,7 @@ export class WindowManager extends Service { height: 670, show: false, autoHideMenuBar: true, + frame: false, // Custom title bar icon: this.opts.icon, title: input.title, webPreferences: { @@ -80,6 +81,14 @@ export class WindowManager extends Service { win.show() }) + // Notify renderer about maximize state changes + win.on('maximize', () => { + win.webContents.send('window:maximized-changed', true) + }) + win.on('unmaximize', () => { + win.webContents.send('window:maximized-changed', false) + }) + win.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } @@ -142,5 +151,46 @@ export class WindowManager extends Service { const ok = this.navigateWindow(win, route) return ok ? { success: true } : { success: false, message: 'Window not found' } }) + + // Window controls + this.mainCtx.handle('window:minimize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) win.minimize() + }) + + this.mainCtx.handle('window:maximize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + if (win.isMaximized()) { + win.unmaximize() + return false + } else { + win.maximize() + return true + } + } + return false + }) + + this.mainCtx.handle('window:close', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) win.close() + }) + + this.mainCtx.handle('window:isMaximized', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + return win ? win.isMaximized() : false + }) + + this.mainCtx.handle('window:toggle-devtools', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + if (win.webContents.isDevToolsOpened()) { + win.webContents.closeDevTools() + } else { + win.webContents.openDevTools() + } + } + }) } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 30ffdfc..b8fc4fa 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -73,6 +73,16 @@ const api = { ipcRenderer.invoke('window:open', input), navigateWindow: (input: { key?: string; route: string }) => ipcRenderer.invoke('window:navigate', input), + windowMinimize: () => ipcRenderer.invoke('window:minimize'), + windowMaximize: () => ipcRenderer.invoke('window:maximize'), + windowClose: () => ipcRenderer.invoke('window:close'), + windowIsMaximized: () => ipcRenderer.invoke('window:isMaximized'), + onWindowMaximizedChanged: (callback: (maximized: boolean) => void) => { + const subscription = (_event: any, maximized: boolean) => callback(maximized) + ipcRenderer.on('window:maximized-changed', subscription) + return () => ipcRenderer.removeListener('window:maximized-changed', subscription) + }, + toggleDevTools: () => ipcRenderer.invoke('window:toggle-devtools'), // Logger queryLogs: (lines?: number) => ipcRenderer.invoke('log:query', lines), diff --git a/src/preload/types.ts b/src/preload/types.ts index 757bb23..a6f7e69 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -116,6 +116,12 @@ export interface electronApi { options?: any }) => Promise> navigateWindow: (input: { key?: string; route: string }) => Promise> + windowMinimize: () => Promise + windowMaximize: () => Promise + windowClose: () => Promise + windowIsMaximized: () => Promise + onWindowMaximizedChanged: (callback: (maximized: boolean) => void) => () => void + toggleDevTools: () => Promise // Logger queryLogs: (lines?: number) => Promise> diff --git a/src/renderer/src/assets/logo.svg b/src/renderer/src/assets/logo.svg new file mode 100644 index 0000000..39a5ac6 --- /dev/null +++ b/src/renderer/src/assets/logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 1e231f3..0c15db9 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -501,6 +501,33 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
{(window as any).electron?.process?.versions?.chrome || '-'}
Node
{(window as any).electron?.process?.versions?.node || '-'}
+
IPC 状态
+
+ + {(window as any).api ? '已连接' : '未连接 (Preload 失败)'} + +
+
环境
+
+ + {import.meta.env.DEV ? 'Development' : 'Production'} + +
+ + +
+
diff --git a/src/renderer/src/components/TitleBar.tsx b/src/renderer/src/components/TitleBar.tsx new file mode 100644 index 0000000..a8322ef --- /dev/null +++ b/src/renderer/src/components/TitleBar.tsx @@ -0,0 +1,135 @@ +import { Button } from 'tdesign-react' +import { RemoveIcon, RectangleIcon, CloseIcon, FullscreenExitIcon } from 'tdesign-icons-react' +import { useEffect, useState } from 'react' +import electronLogo from '../assets/electron.svg' + +interface TitleBarProps { + children?: React.ReactNode +} + +export function TitleBar({ children }: TitleBarProps): React.JSX.Element { + const [isMaximized, setIsMaximized] = useState(false) + + useEffect(() => { + if (!(window as any).api) return // Check initial state + ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v)) + + // Subscribe to changes + const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => { + setIsMaximized(maximized) + }) + return cleanup + }, []) + + const minimize = () => { + ;(window as any).api?.windowMinimize() + } + + const maximize = async () => { + if (!(window as any).api) return + const newState = await (window as any).api.windowMaximize() + setIsMaximized(newState) + } + + const close = () => { + ;(window as any).api?.windowClose() + } + + return ( +
+
+ logo + SecScore +
+ +
+ +
+ {children} +
+ +
+ + + +
+ + +
+ ) +} diff --git a/src/renderer/src/components/WindowControls.tsx b/src/renderer/src/components/WindowControls.tsx new file mode 100644 index 0000000..89da08b --- /dev/null +++ b/src/renderer/src/components/WindowControls.tsx @@ -0,0 +1,80 @@ +import { Button } from 'tdesign-react' +import { RemoveIcon, RectangleIcon, CloseIcon, FullscreenExitIcon } from 'tdesign-icons-react' +import { useEffect, useState } from 'react' + +export function WindowControls(): React.JSX.Element { + const [isMaximized, setIsMaximized] = useState(false) + + useEffect(() => { + if (!(window as any).api) return // Check initial state + ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v)) + + // Subscribe to changes + const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => { + setIsMaximized(maximized) + }) + return cleanup + }, []) + + const minimize = () => { + ;(window as any).api?.windowMinimize() + } + + const maximize = async () => { + if (!(window as any).api) return + const newState = await (window as any).api.windowMaximize() + setIsMaximized(newState) + } + + const close = () => { + ;(window as any).api?.windowClose() + } + + return ( +
+ + + + +
+ ) +}