From acf3231e2c5532d4de1ada2101150125cbffdfec Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Mon, 23 Jun 2025 15:22:56 +0800 Subject: [PATCH 1/7] feat: floating window --- .vscode/settings.json | 3 +- electron.vite.config.ts | 3 +- src/main/index.ts | 116 +++++++++++++++++++++++++++++++----- src/main/presenter/index.ts | 3 + src/preload/index.d.ts | 3 +- 5 files changed, 109 insertions(+), 19 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e69f4107c..99d93244b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "i18n-ally.keystyle": "nested", "i18n-ally.sourceLanguage": "zh-CN", "i18n-ally.namespace": true, - "i18n-ally.pathMatcher": "{locale}/{namespaces}.json" + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "vue-vine.dataTrack": false } diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5540916bc..944f139a7 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -77,7 +77,8 @@ export default defineConfig({ rollupOptions: { input: { shell: resolve('src/renderer/shell/index.html'), - index: resolve('src/renderer/index.html') + index: resolve('src/renderer/index.html'), + floating: resolve('src/renderer/floating/index.html') } } } diff --git a/src/main/index.ts b/src/main/index.ts index d6e5f82ac..a6baeee70 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { app, protocol } from 'electron' +import { app, protocol, ipcMain } from 'electron' import { electronApp, optimizer } from '@electron-toolkit/utils' import { presenter } from './presenter' import { ProxyMode, proxyConfig } from './presenter/proxyConfig' @@ -8,6 +8,7 @@ import { eventBus } from './eventbus' import { WINDOW_EVENTS, TRAY_EVENTS } from './events' import { setLoggingEnabled } from '@shared/logger' import { is } from '@electron-toolkit/utils' // 确保导入 is +import { floatingButtonPresenter } from './presenter/floatingButtonPresenter' // 导入悬浮按钮 presenter // 设置应用命令行参数 app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') // 允许视频自动播放 @@ -30,7 +31,7 @@ if (process.platform === 'darwin') { presenter.deeplinkPresenter.init() // 等待 Electron 初始化完成 -app.whenReady().then(() => { +app.whenReady().then(async () => { // Set app user model id for windows electronApp.setAppUserModelId('com.wefonk.deepchat') @@ -41,6 +42,9 @@ app.whenReady().then(() => { // 初始化托盘图标和菜单,并存储 presenter 实例 presenter.setupTray() + // 立即进行基本初始化,不等待窗口ready-to-show事件 + presenter.init() + // 从配置中读取代理设置并初始化 const proxyMode = presenter.configPresenter.getProxyMode() as ProxyMode const customProxyUrl = presenter.configPresenter.getCustomProxyUrl() @@ -80,16 +84,82 @@ app.whenReady().then(() => { // 如果没有窗口,创建主窗口 (应用首次启动时) if (presenter.windowPresenter.getAllWindows().length === 0) { - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' + console.log('Main: Creating initial shell window on app startup') + try { + const windowId = await presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + if (windowId) { + console.log(`Main: Initial shell window created successfully with ID: ${windowId}`) + } else { + console.error('Main: Failed to create initial shell window - returned null') } - }) + } catch (error) { + console.error('Main: Error creating initial shell window:', error) + } + } else { + console.log('Main: Shell windows already exist, skipping initial window creation') } // 注册全局快捷键 presenter.shortcutPresenter.registerShortcuts() + // 初始化悬浮按钮功能 + try { + await floatingButtonPresenter.initialize() + console.log('Main: Floating button initialized successfully') + } catch (error) { + console.error('Failed to initialize floating button:', error) + } + + // 设置悬浮按钮点击事件的 IPC 处理器 + ipcMain.on('floating-button-click', () => { + console.log('Main: Floating button clicked via IPC') + // 触发内置事件处理器 + app.emit('floating-button-clicked' as any) + }) + + // 监听悬浮按钮点击事件 + app.on('floating-button-clicked' as any, () => { + const allWindows = presenter.windowPresenter.getAllWindows() + if (allWindows.length === 0) { + // 如果没有窗口,创建新窗口 + presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + } else { + // 显示并聚焦第一个窗口 + const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] + if (!targetWindow.isDestroyed()) { + presenter.windowPresenter.show(targetWindow.id) + targetWindow.focus() + } else { + // 如果窗口已销毁,创建新窗口 + presenter.windowPresenter.createShellWindow({ + initialTab: { url: 'local://chat' } + }) + } + } + }) + + // 监听窗口显示/隐藏状态变化 + const handleWindowVisibilityChange = () => { + const allWindows = presenter.windowPresenter.getAllWindows() + const hasVisibleWindow = allWindows.some(win => !win.isDestroyed() && win.isVisible()) + floatingButtonPresenter.onMainWindowVisibilityChanged(hasVisibleWindow) + } + + // 监听窗口显示事件 + app.on('browser-window-created', (_, window) => { + window.on('show', handleWindowVisibilityChange) + window.on('hide', handleWindowVisibilityChange) + window.on('closed', handleWindowVisibilityChange) + }) + // 托盘 检测更新 eventBus.on(TRAY_EVENTS.CHECK_FOR_UPDATES, () => { const allWindows = presenter.windowPresenter.getAllWindows() @@ -267,15 +337,23 @@ app.whenReady().then(() => { }) }) // app.whenReady().then 结束 -// 当所有窗口都关闭时,不退出应用。macOS 平台会保留在 Dock 中,Windows 会保留在托盘。 -// 用户需要通过托盘菜单或 Cmd+Q 来真正退出应用。 -// 因此移除 'window-all-closed' 事件监听 -/* +// 当所有主窗口都关闭时的处理逻辑 +// macOS 平台会保留在 Dock 中,Windows 会保留在托盘。 +// 悬浮按钮窗口不计入主窗口数量 app.on('window-all-closed', () => { - presenter.destroy() - // trayPresenter.destroy() // <-- 已移动到 will-quit + // 检查是否还有非悬浮按钮的窗口 + const mainWindows = presenter.windowPresenter.getAllWindows() + + if (mainWindows.length === 0) { + // 只有悬浮按钮窗口时,在非 macOS 平台退出应用 + if (process.platform !== 'darwin') { + console.log('main: All main windows closed on non-macOS platform, quitting app') + app.quit() + } else { + console.log('main: All main windows closed on macOS, keeping app running in dock') + } + } }) -*/ // 在应用即将退出时触发,适合进行最终的资源清理 (如销毁托盘) // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -298,9 +376,15 @@ app.on('will-quit', (_event) => { }) // 在应用退出之前触发,早于 will-quit。通常不如 will-quit 适合资源清理。 -// 移除在此处销毁托盘的逻辑。 +// 在这里销毁悬浮按钮,确保应用能正常退出 app.on('before-quit', () => { console.log('main: app before-quit event triggered.') // 保留关键日志 - // presenter.destroy() // 如果需要在 will-quit 之前清理 presenter,可以保留 - // trayPresenter.destroy() // <-- 从此处移除托盘销毁 + + // 销毁悬浮按钮窗口,确保应用能正常退出 + try { + floatingButtonPresenter.destroy() + console.log('main: Floating button destroyed during before-quit') + } catch (error) { + console.error('main: Error destroying floating button during before-quit:', error) + } }) diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 6e726af7e..6e6cb1fec 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -19,6 +19,7 @@ import { NotificationPresenter } from './notifactionPresenter' import { TabPresenter } from './tabPresenter' import { TrayPresenter } from './trayPresenter' import { OAuthPresenter } from './oauthPresenter' +import { FloatingButtonPresenter } from './floatingButtonPresenter' import { CONFIG_EVENTS, WINDOW_EVENTS } from '@/events' // IPC调用上下文接口 @@ -52,6 +53,7 @@ export class Presenter implements IPresenter { tabPresenter: TabPresenter trayPresenter: TrayPresenter oauthPresenter: OAuthPresenter + floatingButtonPresenter: FloatingButtonPresenter // llamaCppPresenter: LlamaCppPresenter // 保留原始注释 constructor() { @@ -79,6 +81,7 @@ export class Presenter implements IPresenter { this.notificationPresenter = new NotificationPresenter() this.oauthPresenter = new OAuthPresenter() this.trayPresenter = new TrayPresenter() + this.floatingButtonPresenter = new FloatingButtonPresenter() // this.llamaCppPresenter = new LlamaCppPresenter() // 保留原始注释 this.setupEventBus() // 设置事件总线监听 diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index b1ed41146..fbba44e45 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -9,6 +9,7 @@ declare global { getPathForFile(file: File): string getWindowId(): number | null getWebContentsId(): number - } + }, + floatingButtonAPI: typeof floatingButtonAPI; } } From 97691dd468869fc200b03ef2dab2e1a1ae8adb6a Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Mon, 23 Jun 2025 15:23:16 +0800 Subject: [PATCH 2/7] feat: floating button presenter --- .../FloatingButtonWindow.ts | 219 +++++++++++++++ .../floatingButtonPresenter/index.ts | 160 +++++++++++ .../floatingButtonPresenter/types.ts | 56 ++++ src/preload/floating-preload.ts | 24 ++ src/renderer/floating.html | 227 ++++++++++++++++ src/renderer/floating/FloatingButton.vue | 255 ++++++++++++++++++ src/renderer/floating/env.d.ts | 21 ++ src/renderer/floating/index.html | 27 ++ src/renderer/floating/main.ts | 6 + 9 files changed, 995 insertions(+) create mode 100644 src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts create mode 100644 src/main/presenter/floatingButtonPresenter/index.ts create mode 100644 src/main/presenter/floatingButtonPresenter/types.ts create mode 100644 src/preload/floating-preload.ts create mode 100644 src/renderer/floating.html create mode 100644 src/renderer/floating/FloatingButton.vue create mode 100644 src/renderer/floating/env.d.ts create mode 100644 src/renderer/floating/index.html create mode 100644 src/renderer/floating/main.ts diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts new file mode 100644 index 000000000..50d7f93b9 --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -0,0 +1,219 @@ +import { BrowserWindow, screen } from 'electron'; +import path from 'path'; +import { FloatingButtonConfig, FloatingButtonState } from './types'; +import logger from '../../../shared/logger'; + +export class FloatingButtonWindow { + private window: BrowserWindow | null = null; + private config: FloatingButtonConfig; + private state: FloatingButtonState; + + constructor(config: FloatingButtonConfig) { + this.config = config; + this.state = { + isVisible: false, + bounds: { + x: 0, + y: 0, + width: config.size.width, + height: config.size.height + } + }; + } + + /** + * 创建悬浮窗口 + */ + public async create(): Promise { + if (this.window) { + return; + } + + try { + const position = this.calculatePosition(); + + this.window = new BrowserWindow({ + width: this.config.size.width, + height: this.config.size.height, + x: position.x, + y: position.y, + frame: false, + transparent: true, + alwaysOnTop: this.config.alwaysOnTop, + skipTaskbar: true, + resizable: false, + minimizable: false, + maximizable: false, + closable: false, + show: false, + movable: true, // 允许拖拽 + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../../preload/floating.mjs'), + webSecurity: true + } + }); + + // 设置窗口透明度 + this.window.setOpacity(this.config.opacity); + + // 加载悬浮按钮页面 + const isDev = process.env.NODE_ENV === 'development'; + if (isDev) { + await this.window.loadURL('http://localhost:5173/floating/index.html'); + } else { + await this.window.loadFile(path.join(__dirname, '../../../renderer/floating/index.html')); + } + + // 监听窗口事件 + this.setupWindowEvents(); + + logger.info('FloatingButtonWindow created successfully'); + } catch (error) { + logger.error('Failed to create FloatingButtonWindow:', error); + throw error; + } + } + + /** + * 显示悬浮窗口 + */ + public show(): void { + if (!this.window) { + return; + } + + this.window.show(); + this.state.isVisible = true; + logger.debug('FloatingButtonWindow shown'); + } + + /** + * 隐藏悬浮窗口 + */ + public hide(): void { + if (!this.window) { + return; + } + + this.window.hide(); + this.state.isVisible = false; + logger.debug('FloatingButtonWindow hidden'); + } + + /** + * 销毁悬浮窗口 + */ + public destroy(): void { + if (this.window) { + this.window.destroy(); + this.window = null; + this.state.isVisible = false; + logger.debug('FloatingButtonWindow destroyed'); + } + } + + /** + * 更新配置 + */ + public updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + + if (this.window) { + // 更新窗口属性 + if (config.size) { + this.window.setSize(this.config.size.width, this.config.size.height); + this.state.bounds.width = this.config.size.width; + this.state.bounds.height = this.config.size.height; + } + + if (config.position || config.offset) { + const position = this.calculatePosition(); + this.window.setPosition(position.x, position.y); + this.state.bounds.x = position.x; + this.state.bounds.y = position.y; + } + + if (config.opacity !== undefined) { + this.window.setOpacity(this.config.opacity); + } + + if (config.alwaysOnTop !== undefined) { + this.window.setAlwaysOnTop(this.config.alwaysOnTop); + } + } + } + + /** + * 获取当前状态 + */ + public getState(): FloatingButtonState { + return { ...this.state }; + } + + /** + * 检查窗口是否存在 + */ + public exists(): boolean { + return this.window !== null && !this.window.isDestroyed(); + } + + /** + * 计算悬浮按钮位置 + */ + private calculatePosition(): { x: number; y: number } { + const primaryDisplay = screen.getPrimaryDisplay(); + const { workAreaSize } = primaryDisplay; + + let x: number, y: number; + + switch (this.config.position) { + case 'top-left': + x = this.config.offset.x; + y = this.config.offset.y; + break; + case 'top-right': + x = workAreaSize.width - this.config.size.width - this.config.offset.x; + y = this.config.offset.y; + break; + case 'bottom-left': + x = this.config.offset.x; + y = workAreaSize.height - this.config.size.height - this.config.offset.y; + break; + case 'bottom-right': + default: + x = workAreaSize.width - this.config.size.width - this.config.offset.x; + y = workAreaSize.height - this.config.size.height - this.config.offset.y; + break; + } + + return { x, y }; + } + + /** + * 设置窗口事件监听 + */ + private setupWindowEvents(): void { + if (!this.window) { + return; + } + + // 窗口关闭事件 + this.window.on('closed', () => { + this.window = null; + this.state.isVisible = false; + }); + + // 窗口移动事件 + this.window.on('moved', () => { + if (this.window) { + const bounds = this.window.getBounds(); + this.state.bounds.x = bounds.x; + this.state.bounds.y = bounds.y; + } + }); + + // 注意:悬浮按钮点击事件的 IPC 处理器在主进程的 index.ts 中设置 + } +} diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts new file mode 100644 index 000000000..855e02d73 --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -0,0 +1,160 @@ +import { FloatingButtonWindow } from './FloatingButtonWindow'; +import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types'; +import logger from '../../../shared/logger'; + +export class FloatingButtonPresenter { + private floatingWindow: FloatingButtonWindow | null = null; + private config: FloatingButtonConfig; + + constructor(initialConfig?: Partial) { + this.config = { + ...DEFAULT_FLOATING_BUTTON_CONFIG, + ...initialConfig + }; + } + + /** + * 初始化悬浮按钮功能 + */ + public async initialize(): Promise { + try { + if (!this.config.enabled) { + logger.debug('FloatingButton is disabled'); + return; + } + + await this.createFloatingWindow(); + logger.info('FloatingButtonPresenter initialized'); + } catch (error) { + logger.error('Failed to initialize FloatingButtonPresenter:', error); + throw error; + } + } + + /** + * 销毁悬浮按钮功能 + */ + public destroy(): void { + if (this.floatingWindow) { + this.floatingWindow.destroy(); + this.floatingWindow = null; + } + logger.debug('FloatingButtonPresenter destroyed'); + } + + /** + * 当主窗口显示状态改变时调用 + */ + public onMainWindowVisibilityChanged(visible: boolean): void { + if (!this.config.enabled || !this.floatingWindow) { + return; + } + + // 悬浮按钮始终显示,不受主窗口状态影响 + // 这样用户可以随时看到悬浮按钮 + this.floatingWindow.show(); + + logger.debug(`FloatingButton always visible (main window: ${visible})`); + } + + /** + * 更新悬浮按钮配置 + */ + public async updateConfig(newConfig: Partial): Promise { + const oldEnabled = this.config.enabled; + this.config = { ...this.config, ...newConfig }; + + try { + // 如果启用状态发生变化 + if (oldEnabled !== this.config.enabled) { + if (this.config.enabled) { + // 启用悬浮按钮 + if (!this.floatingWindow) { + await this.createFloatingWindow(); + } + // 启用时立即显示 + this.floatingWindow?.show(); + } else { + // 禁用悬浮按钮 + this.destroyFloatingWindow(); + } + } else if (this.config.enabled && this.floatingWindow) { + // 更新现有窗口配置 + this.floatingWindow.updateConfig(newConfig); + } + + logger.debug('FloatingButton config updated:', this.config); + } catch (error) { + logger.error('Failed to update FloatingButton config:', error); + throw error; + } + } + + /** + * 获取当前配置 + */ + public getConfig(): FloatingButtonConfig { + return { ...this.config }; + } + + /** + * 获取当前状态 + */ + public getState(): FloatingButtonState | null { + return this.floatingWindow?.getState() || null; + } + + /** + * 检查悬浮按钮是否可用 + */ + public isAvailable(): boolean { + return this.config.enabled && this.floatingWindow?.exists() || false; + } + + /** + * 手动显示悬浮按钮 + */ + public show(): void { + if (this.config.enabled && this.floatingWindow) { + this.floatingWindow.show(); + } + } + + /** + * 手动隐藏悬浮按钮 + */ + public hide(): void { + if (this.floatingWindow) { + this.floatingWindow.hide(); + } + } + + /** + * 创建悬浮窗口 + */ + private async createFloatingWindow(): Promise { + if (this.floatingWindow) { + return; + } + + this.floatingWindow = new FloatingButtonWindow(this.config); + await this.floatingWindow.create(); + + // 悬浮按钮创建后立即显示 + this.floatingWindow.show(); + logger.debug('FloatingButtonWindow created and shown'); + } + + /** + * 销毁悬浮窗口 + */ + private destroyFloatingWindow(): void { + if (this.floatingWindow) { + this.floatingWindow.destroy(); + this.floatingWindow = null; + } + } +} + +// 导出单例实例 +export const floatingButtonPresenter = new FloatingButtonPresenter(); diff --git a/src/main/presenter/floatingButtonPresenter/types.ts b/src/main/presenter/floatingButtonPresenter/types.ts new file mode 100644 index 000000000..2662716e0 --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/types.ts @@ -0,0 +1,56 @@ +export interface FloatingButtonConfig { + /** 是否启用悬浮按钮 */ + enabled: boolean; + /** 悬浮按钮位置 */ + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + /** 距离边缘的偏移量 */ + offset: { + x: number; + y: number; + }; + /** 悬浮按钮大小 */ + size: { + width: number; + height: number; + }; + /** 是否置顶显示 */ + alwaysOnTop: boolean; + /** 透明度 (0-1) */ + opacity: number; +} + +export interface FloatingButtonState { + /** 是否正在显示 */ + isVisible: boolean; + /** 当前位置 */ + bounds: { + x: number; + y: number; + width: number; + height: number; + }; +} + +export interface FloatingButtonEvents { + /** 悬浮按钮被点击 */ + 'floating-button-clicked': void; + /** 悬浮按钮显示状态改变 */ + 'floating-button-visibility-changed': { visible: boolean }; + /** 悬浮按钮位置改变 */ + 'floating-button-position-changed': { x: number; y: number }; +} + +export const DEFAULT_FLOATING_BUTTON_CONFIG: FloatingButtonConfig = { + enabled: true, + position: 'bottom-right', + offset: { + x: 20, + y: 20 + }, + size: { + width: 60, + height: 60 + }, + alwaysOnTop: true, + opacity: 0.8 +}; diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts new file mode 100644 index 000000000..e873efe3d --- /dev/null +++ b/src/preload/floating-preload.ts @@ -0,0 +1,24 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +// 定义悬浮按钮的 API +const floatingButtonAPI = { + // 通知主进程悬浮按钮被点击 + onClick: () => { + ipcRenderer.send('floating-button-click'); + }, + + // 监听来自主进程的事件 + onConfigUpdate: (callback: (config: any) => void) => { + ipcRenderer.on('floating-button-config-update', (_event, config) => { + callback(config); + }); + }, + + // 移除事件监听器 + removeAllListeners: () => { + ipcRenderer.removeAllListeners('floating-button-config-update'); + } +}; + +// 将 API 暴露给渲染进程 +contextBridge.exposeInMainWorld('floatingButtonAPI', floatingButtonAPI); diff --git a/src/renderer/floating.html b/src/renderer/floating.html new file mode 100644 index 000000000..4d8f3a402 --- /dev/null +++ b/src/renderer/floating.html @@ -0,0 +1,227 @@ + + + + + + Floating Button + + + +
+ + + +
打开 DeepChat
+
+ + + + diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue new file mode 100644 index 000000000..5d853b611 --- /dev/null +++ b/src/renderer/floating/FloatingButton.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/src/renderer/floating/env.d.ts b/src/renderer/floating/env.d.ts new file mode 100644 index 000000000..5c6d1342e --- /dev/null +++ b/src/renderer/floating/env.d.ts @@ -0,0 +1,21 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + const component: DefineComponent<{}, {}, any> + export default component +} + +// 悬浮按钮 API 类型声明 +declare global { + interface Window { + floatingButtonAPI: { + onClick: () => void + onConfigUpdate: (callback: (config: any) => void) => void + removeAllListeners: () => void + } + } +} + +export {} diff --git a/src/renderer/floating/index.html b/src/renderer/floating/index.html new file mode 100644 index 000000000..77e312490 --- /dev/null +++ b/src/renderer/floating/index.html @@ -0,0 +1,27 @@ + + + + + + + Floating Button + + + +
+ + + diff --git a/src/renderer/floating/main.ts b/src/renderer/floating/main.ts new file mode 100644 index 000000000..2bc217d4c --- /dev/null +++ b/src/renderer/floating/main.ts @@ -0,0 +1,6 @@ +import '@/assets/main.css' +import { createApp } from 'vue' +import FloatingButton from './FloatingButton.vue' + +const app = createApp(FloatingButton) +app.mount('#app') From 72e998ca10fe7761d460d2a627339d2c0224c48b Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Mon, 23 Jun 2025 17:46:12 +0800 Subject: [PATCH 3/7] feat: floating button show main window --- electron.vite.config.ts | 8 + src/main/events.ts | 7 + src/main/index.ts | 73 +++--- .../FloatingButtonWindow.ts | 18 +- src/preload/floating-preload.ts | 24 +- src/renderer/floating.html | 227 ------------------ src/renderer/floating/FloatingButton.vue | 149 +++--------- src/renderer/floating/main.ts | 2 +- 8 files changed, 109 insertions(+), 399 deletions(-) delete mode 100644 src/renderer/floating.html diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 944f139a7..bea67835c 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -33,6 +33,14 @@ export default defineConfig({ alias: { '@shared': resolve('src/shared') } + }, + build: { + rollupOptions: { + input: { + index: resolve('src/preload/index.ts'), + floating: resolve('src/preload/floating-preload.ts') + } + } } }, renderer: { diff --git a/src/main/events.ts b/src/main/events.ts index 67710a04f..12f21e37b 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -164,3 +164,10 @@ export const TRAY_EVENTS = { export const MEETING_EVENTS = { INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令 } + +// 悬浮按钮相关事件 +export const FLOATING_BUTTON_EVENTS = { + CLICKED: 'floating-button:clicked', // 悬浮按钮被点击 + VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变 + POSITION_CHANGED: 'floating-button:position-changed' // 悬浮按钮位置改变 +} diff --git a/src/main/index.ts b/src/main/index.ts index a6baeee70..229f1ac42 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,7 @@ import { ProxyMode, proxyConfig } from './presenter/proxyConfig' import path from 'path' import fs from 'fs' import { eventBus } from './eventbus' -import { WINDOW_EVENTS, TRAY_EVENTS } from './events' +import { WINDOW_EVENTS, TRAY_EVENTS, FLOATING_BUTTON_EVENTS } from './events' import { setLoggingEnabled } from '@shared/logger' import { is } from '@electron-toolkit/utils' // 确保导入 is import { floatingButtonPresenter } from './presenter/floatingButtonPresenter' // 导入悬浮按钮 presenter @@ -115,36 +115,47 @@ app.whenReady().then(async () => { } // 设置悬浮按钮点击事件的 IPC 处理器 - ipcMain.on('floating-button-click', () => { - console.log('Main: Floating button clicked via IPC') - // 触发内置事件处理器 - app.emit('floating-button-clicked' as any) + // 先移除可能存在的旧监听器 + ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED) + ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, () => { + try { + // 触发内置事件处理器 + handleShowHiddenWindow(true) + } catch (error) { + } }) - // 监听悬浮按钮点击事件 - app.on('floating-button-clicked' as any, () => { + function handleShowHiddenWindow(mustShow: boolean) { const allWindows = presenter.windowPresenter.getAllWindows() if (allWindows.length === 0) { - // 如果没有窗口,创建新窗口 presenter.windowPresenter.createShellWindow({ initialTab: { url: 'local://chat' } }) } else { - // 显示并聚焦第一个窗口 + // 查找目标窗口 (焦点窗口或第一个窗口) const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] + if (!targetWindow.isDestroyed()) { - presenter.windowPresenter.show(targetWindow.id) - targetWindow.focus() + // 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶 + if (targetWindow.isVisible() && !mustShow) { + presenter.windowPresenter.hide(targetWindow.id) + } else { + presenter.windowPresenter.show(targetWindow.id) + targetWindow.focus() // 确保窗口置顶 + } } else { - // 如果窗口已销毁,创建新窗口 + console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn + // 如果目标窗口已销毁,创建新窗口 presenter.windowPresenter.createShellWindow({ - initialTab: { url: 'local://chat' } + initialTab: { + url: 'local://chat' + } }) } } - }) + } // 监听窗口显示/隐藏状态变化 const handleWindowVisibilityChange = () => { @@ -173,38 +184,8 @@ app.whenReady().then(async () => { presenter.upgradePresenter.checkUpdate() }) - // 监听显示/隐藏窗口事件 (从托盘或快捷键触发) - eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, (trayClick: boolean) => { - const allWindows = presenter.windowPresenter.getAllWindows() - if (allWindows.length === 0) { - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' - } - }) - } else { - // 查找目标窗口 (焦点窗口或第一个窗口) - const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] - - if (!targetWindow.isDestroyed()) { - // 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶 - if (targetWindow.isVisible() && !trayClick) { - presenter.windowPresenter.hide(targetWindow.id) - } else { - presenter.windowPresenter.show(targetWindow.id) - targetWindow.focus() // 确保窗口置顶 - } - } else { - console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn - // 如果目标窗口已销毁,创建新窗口 - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' - } - }) - } - } - }) + // 监听显示/隐藏窗口事件 (从托盘或快捷键或悬浮窗口触发) + eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, handleShowHiddenWindow) // 监听浏览器窗口获得焦点事件 app.on('browser-window-focus', () => { diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts index 50d7f93b9..f99b938af 100644 --- a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -1,5 +1,6 @@ import { BrowserWindow, screen } from 'electron'; import path from 'path'; +import fs from 'fs'; import { FloatingButtonConfig, FloatingButtonState } from './types'; import logger from '../../../shared/logger'; @@ -32,6 +33,12 @@ export class FloatingButtonWindow { try { const position = this.calculatePosition(); + // 根据环境选择正确的预加载脚本路径 + const isDev = process.env.NODE_ENV === 'development'; + const preloadPath = isDev + ? path.join(process.cwd(), 'out/preload/floating.mjs') + : path.join(__dirname, '../../preload/floating.mjs'); + this.window = new BrowserWindow({ width: this.config.size.width, height: this.config.size.height, @@ -50,8 +57,10 @@ export class FloatingButtonWindow { webPreferences: { nodeIntegration: false, contextIsolation: true, - preload: path.join(__dirname, '../../preload/floating.mjs'), - webSecurity: true + preload: preloadPath, + webSecurity: false, // 开发模式下允许跨域 + devTools: true, // 开发模式下启用开发者工具 + sandbox: false // 禁用沙盒模式,确保预加载脚本能正常工作 } }); @@ -59,9 +68,10 @@ export class FloatingButtonWindow { this.window.setOpacity(this.config.opacity); // 加载悬浮按钮页面 - const isDev = process.env.NODE_ENV === 'development'; if (isDev) { - await this.window.loadURL('http://localhost:5173/floating/index.html'); + await this.window.loadURL('http://localhost:5173/floating/'); + // 开发模式下可选择性打开开发者工具(暂时禁用,避免影响拖拽) + this.window.webContents.openDevTools({ mode: 'detach' }); } else { await this.window.loadFile(path.join(__dirname, '../../../renderer/floating/index.html')); } diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts index e873efe3d..9fa578ba7 100644 --- a/src/preload/floating-preload.ts +++ b/src/preload/floating-preload.ts @@ -1,10 +1,15 @@ +import { FLOATING_BUTTON_EVENTS } from '@/events'; import { contextBridge, ipcRenderer } from 'electron'; // 定义悬浮按钮的 API const floatingButtonAPI = { // 通知主进程悬浮按钮被点击 onClick: () => { - ipcRenderer.send('floating-button-click'); + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.CLICKED); + } catch (error) { + console.error('FloatingPreload: Error sending IPC message:', error); + } }, // 监听来自主进程的事件 @@ -16,9 +21,22 @@ const floatingButtonAPI = { // 移除事件监听器 removeAllListeners: () => { + console.log('FloatingPreload: Removing all listeners'); ipcRenderer.removeAllListeners('floating-button-config-update'); } }; -// 将 API 暴露给渲染进程 -contextBridge.exposeInMainWorld('floatingButtonAPI', floatingButtonAPI); +// 尝试不同的方式暴露API +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('floatingButtonAPI', floatingButtonAPI); + } catch (error) { + console.error('=== FloatingPreload: Error exposing API via contextBridge ===:', error); + } +} else { + try { + (window as any).floatingButtonAPI = floatingButtonAPI; + } catch (error) { + console.error('=== FloatingPreload: Error attaching API to window ===:', error); + } +} diff --git a/src/renderer/floating.html b/src/renderer/floating.html deleted file mode 100644 index 4d8f3a402..000000000 --- a/src/renderer/floating.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - Floating Button - - - -
- - - -
打开 DeepChat
-
- - - - diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue index 5d853b611..ad483f008 100644 --- a/src/renderer/floating/FloatingButton.vue +++ b/src/renderer/floating/FloatingButton.vue @@ -1,17 +1,19 @@ @@ -22,46 +24,8 @@ import { ref, onMounted, onUnmounted } from 'vue' // 响应式状态 const isPulsing = ref(true) const floatingButton = ref() -let dragStartPos = { x: 0, y: 0 } -let isDragging = false - -// 鼠标按下处理 - 记录起始位置 -const handleMouseDown = (event: MouseEvent) => { - dragStartPos.x = event.clientX - dragStartPos.y = event.clientY - isDragging = false - - // 添加鼠标释放监听器 - document.addEventListener('mouseup', handleMouseUp) - document.addEventListener('mousemove', handleMouseMove) -} - -// 鼠标移动处理 - 检测是否为拖拽 -const handleMouseMove = (event: MouseEvent) => { - const deltaX = Math.abs(event.clientX - dragStartPos.x) - const deltaY = Math.abs(event.clientY - dragStartPos.y) - - // 如果移动超过5像素,认为是拖拽 - if (deltaX > 5 || deltaY > 5) { - isDragging = true - } -} - -// 鼠标释放处理 - 判断是点击还是拖拽 -const handleMouseUp = () => { - // 移除监听器 - document.removeEventListener('mouseup', handleMouseUp) - document.removeEventListener('mousemove', handleMouseMove) - - // 如果不是拖拽,则处理点击 - if (!isDragging) { - handleClick() - } - - isDragging = false -} -// 点击处理 +// 点击处理 - 专注于唤起主窗口 const handleClick = () => { // 点击反馈动画 if (floatingButton.value) { @@ -73,9 +37,14 @@ const handleClick = () => { }, 150) } - // 通知主进程 if (window.floatingButtonAPI) { - window.floatingButtonAPI.onClick() + try { + window.floatingButtonAPI.onClick() + } catch (error) { + console.error('=== FloatingButton: Error calling onClick API ===:', error) + } + } else { + console.error('=== FloatingButton: floatingButtonAPI not available ===') } } @@ -93,7 +62,6 @@ const handleMouseLeave = () => { // 配置更新处理 const handleConfigUpdate = (config: any) => { - console.log('Config updated:', config) // 根据配置更新按钮样式 if (config.opacity !== undefined && document.body) { document.body.style.opacity = config.opacity.toString() @@ -101,22 +69,12 @@ const handleConfigUpdate = (config: any) => { } onMounted(() => { - // 监听配置更新 if (window.floatingButtonAPI) { - window.floatingButtonAPI.onConfigUpdate(handleConfigUpdate) + window.floatingButtonAPI.onConfigUpdate(handleConfigUpdate); } - - // 添加初始动画延迟 - setTimeout(() => { - isPulsing.value = true - }, 1000) }) onUnmounted(() => { - // 清理拖拽事件监听器 - document.removeEventListener('mouseup', handleMouseUp) - document.removeEventListener('mousemove', handleMouseMove) - // 清理配置更新监听器 if (window.floatingButtonAPI) { window.floatingButtonAPI.removeAllListeners() @@ -125,44 +83,24 @@ onUnmounted(() => {