From c6f34cdf980819100a1ba18fdb9075b3dafd03fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Sat, 12 Apr 2025 02:01:03 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=9C=80=E5=A4=A7=E5=8C=96?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=90=8E=E9=9A=90=E8=97=8F=E5=9C=86=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- electron/preload/index.ts | 32 +++- src/assets/styles/main.scss | 13 ++ src/components/LinuxTitleBar.vue | 293 ++++++++++++++++++++++++------- 3 files changed, 271 insertions(+), 67 deletions(-) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index a5545696..cbea3f1d 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -9,6 +9,13 @@ import { ipcRenderer, contextBridge } from 'electron'; function validateIPC(channel: string): true | never { if (!channel || !channel.startsWith('copilot:')) { + // 允许窗口状态变化事件通过验证 + if ( + channel === 'window-maximized-change' || + channel === 'window-is-maximized' + ) { + return true; + } throw new Error(`Unsupported event IPC channel '${channel}'`); } @@ -19,9 +26,32 @@ const globals = { ipcRenderer: { invoke(channel: string, ...args: any[]): Promise { validateIPC(channel); - return ipcRenderer.invoke(channel, ...args); }, + + // 添加事件监听方法 + on(channel: string, listener: (...args: any[]) => void): void { + validateIPC(channel); + ipcRenderer.on(channel, (event, ...args) => listener(...args)); + }, + + // 添加一次性事件监听方法 + once(channel: string, listener: (...args: any[]) => void): void { + validateIPC(channel); + ipcRenderer.once(channel, (event, ...args) => listener(...args)); + }, + + // 添加移除特定事件监听器的方法 + removeListener(channel: string, listener: (...args: any[]) => void): void { + validateIPC(channel); + ipcRenderer.removeListener(channel, listener); + }, + + // 添加移除所有事件监听器的方法 + removeAllListeners(channel: string): void { + validateIPC(channel); + ipcRenderer.removeAllListeners(channel); + }, }, process: { diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index ec56b6cb..a43c2689 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -17,10 +17,23 @@ html.platform-linux { background: transparent; border-radius: 16px; overflow: hidden; + // 添加圆角过渡动画 + transition: border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); } #app { border-radius: 16px; overflow: hidden; + // 添加圆角过渡动画 + transition: border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + + // 窗口最大化状态下取消圆角 + &.window-maximized { + body, #app { + border-radius: 0 !important; + // 保持过渡动画一致性 + transition: border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } } } diff --git a/src/components/LinuxTitleBar.vue b/src/components/LinuxTitleBar.vue index 82f07a0c..87a72109 100644 --- a/src/components/LinuxTitleBar.vue +++ b/src/components/LinuxTitleBar.vue @@ -60,6 +60,7 @@ import { useChangeThemeStore } from '@/store/conversation'; const isMaximized = ref(false); const themeStore = useChangeThemeStore(); +const overlayRef = ref(null); // 动态监听主题变化 const isDarkTheme = computed(() => themeStore.theme === 'dark'); @@ -96,6 +97,169 @@ const sendWindowControlCommand = (command) => { } }; +// 更新窗口最大化状态并设置CSS类 +const updateMaximizedState = (maximized: boolean) => { + isMaximized.value = maximized; + + // 设置或移除窗口最大化的CSS类 + if (maximized) { + document.documentElement.classList.add('window-maximized'); + } else { + document.documentElement.classList.remove('window-maximized'); + } + + console.log('Window maximized state:', maximized); +}; + +// 创建固定的窗口控制按钮 +const createWindowControls = () => { + // 移除可能已存在的控制按钮 + const existingOverlay = document.getElementById('linux-titlebar-overlay'); + if (existingOverlay) { + document.body.removeChild(existingOverlay); + } + + // 创建新的覆盖层 + const overlay = document.createElement('div'); + overlay.id = 'linux-titlebar-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + right: 0; + padding: 12px; + width: 120px; + height: 48px; + z-index: 9999999; + pointer-events: auto; + -webkit-app-region: no-drag; + display: flex; + justify-content: flex-end; + `; + + // 创建最小化按钮 + const minimizeBtn = document.createElement('button'); + minimizeBtn.className = 'linux-control-button minimize'; + minimizeBtn.title = '最小化'; + minimizeBtn.style.cssText = buttonBaseStyle; + minimizeBtn.innerHTML = ` + + + + `; + minimizeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + sendWindowControlCommand('minimize'); + }); + + // 创建最大化按钮 + const maximizeBtn = document.createElement('button'); + maximizeBtn.className = 'linux-control-button maximize'; + maximizeBtn.title = isMaximized.value ? '还原' : '最大化'; + maximizeBtn.style.cssText = buttonBaseStyle; + maximizeBtn.innerHTML = isMaximized.value + ? ` + + + + ` + : ` + + + + `; + maximizeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + sendWindowControlCommand('maximize'); + }); + + // 创建关闭按钮 + const closeBtn = document.createElement('button'); + closeBtn.className = 'linux-control-button close'; + closeBtn.title = '关闭'; + closeBtn.style.cssText = buttonBaseStyle; + closeBtn.innerHTML = ` + + + + `; + closeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + sendWindowControlCommand('close'); + }); + + // 添加鼠标悬停效果 + const addHoverEffects = (button, isCloseButton = false) => { + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = isCloseButton + ? '#e81123' + : 'rgba(128, 128, 128, 0.2)'; + if (isCloseButton) button.style.color = 'white'; + }); + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = 'rgba(128, 128, 128, 0.1)'; + if (isCloseButton) button.style.color = 'inherit'; + }); + }; + + addHoverEffects(minimizeBtn); + addHoverEffects(maximizeBtn); + addHoverEffects(closeBtn, true); + + // 添加按钮到覆盖层 + overlay.appendChild(minimizeBtn); + overlay.appendChild(maximizeBtn); + overlay.appendChild(closeBtn); + + // 添加覆盖层到文档 + document.body.appendChild(overlay); + + // 保存引用以便之后可以更新 + overlayRef.value = overlay; +}; + +// 更新最大化按钮的图标 +const updateMaximizeButton = () => { + if (!overlayRef.value) return; + + const maximizeBtn = overlayRef.value.querySelector( + '.linux-control-button.maximize', + ) as HTMLButtonElement; + if (!maximizeBtn) return; + + maximizeBtn.title = isMaximized.value ? '还原' : '最大化'; + maximizeBtn.innerHTML = isMaximized.value + ? ` + + + + ` + : ` + + + + `; +}; + +// 按钮基础样式 +const buttonBaseStyle = ` + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + outline: none; + cursor: pointer; + border-radius: 12px; + margin-left: 8px; + background-color: rgba(128, 128, 128, 0.1); + color: inherit; + padding: 0; +`; + // 监听主题变化 watch( () => themeStore.theme, @@ -105,53 +269,63 @@ watch( { immediate: true }, ); -// 组件挂载时在Document上直接添加一个覆盖层用于捕获事件 +// 监听窗口最大化状态变化 +watch( + () => isMaximized.value, + () => { + updateMaximizeButton(); + }, +); + +// 组件挂载时创建控制按钮 onMounted(() => { console.log('LinuxTitleBar mounted'); - // 创建一个DOM覆盖层,直接添加到body上 - setTimeout(() => { - // 延迟添加以确保Vue已完成渲染 - const overlay = document.createElement('div'); - overlay.className = 'linux-titlebar-overlay'; - overlay.style.cssText = ` - position: fixed; - top: 0; - right: 0; - padding: 12px; - width: 120px; - height: 48px; - z-index: 999999; - pointer-events: auto; - -webkit-app-region: no-drag; - `; - document.body.appendChild(overlay); - - // 将控制按钮复制到这个覆盖层 - const controls = document.querySelector('.window-controls'); - if (controls) { - const clonedControls = controls.cloneNode(true); - overlay.appendChild(clonedControls); - - // 添加事件监听器到克隆的按钮 - const buttons = overlay.querySelectorAll('button'); - buttons[0].addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - sendWindowControlCommand('minimize'); - }); - buttons[1].addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - sendWindowControlCommand('maximize'); - }); - buttons[2].addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - sendWindowControlCommand('close'); + // 监听窗口最大化状态变化事件 + if (window.eulercopilot && window.eulercopilot.ipcRenderer) { + // 首先获取当前窗口是否已经最大化 + window.eulercopilot.ipcRenderer + .invoke('copilot:window-is-maximized') + .then((maximized) => { + updateMaximizedState(maximized); + + // 创建控制按钮 + createWindowControls(); + }) + .catch((error) => { + console.error('Failed to get window maximized state:', error); + + // 即使获取状态失败也创建控制按钮 + createWindowControls(); }); - } - }, 1000); + + // 监听窗口最大化状态变化 + window.eulercopilot.ipcRenderer.on( + 'window-maximized-change', + (maximized) => { + console.log('Window maximized state changed:', maximized); + updateMaximizedState(maximized); + }, + ); + } else { + // 如果没有IPC通道,仍然创建控制按钮 + createWindowControls(); + } +}); + +// 组件卸载时移除事件监听器和覆盖层 +onBeforeUnmount(() => { + if (window.eulercopilot && window.eulercopilot.ipcRenderer) { + window.eulercopilot.ipcRenderer.removeAllListeners( + 'window-maximized-change', + ); + } + + // 移除覆盖层 + const overlay = document.getElementById('linux-titlebar-overlay'); + if (overlay && overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } }); @@ -202,28 +376,15 @@ onMounted(() => { -- Gitee From 4eb0ff930fc0478812e3fcb375b286adb287f1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Sat, 12 Apr 2025 02:19:17 +0800 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20=E5=8F=AA=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=88=9B=E5=BB=BA=E6=8C=89=E9=92=AE=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/components/LinuxTitleBar.vue | 102 +++---------------------------- src/views/chat/Welcome.vue | 2 +- 2 files changed, 11 insertions(+), 93 deletions(-) diff --git a/src/components/LinuxTitleBar.vue b/src/components/LinuxTitleBar.vue index 87a72109..d61fa11b 100644 --- a/src/components/LinuxTitleBar.vue +++ b/src/components/LinuxTitleBar.vue @@ -1,57 +1,9 @@