From ca8caaabbc535bec38fe2559f8d84b4f356b54ab Mon Sep 17 00:00:00 2001 From: ILoveBingLu Date: Wed, 8 Apr 2026 19:47:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E8=B7=A8=E5=B9=B3=E5=8F=B0=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E6=A0=8F=E5=B8=83=E5=B1=80=E5=B9=B6=E5=8D=87=E7=BA=A7=E5=88=B0?= =?UTF-8?q?=204.1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为独立窗口统一引入跨平台 window chrome 安全区和标题栏高度变量 - 优化朋友圈、聊天记录、浏览器、AI 摘要、协议页、图片/视频窗口等标题栏在 Windows/macOS 下的布局表现 - 统一主进程独立窗口 titleBarOverlay 高度为 40,减少首屏偏移和抖动 - 升级版本号到 4.1.3,并补充 README 与 CHANGELOG 记录 --- CHANGELOG.md | 11 ++ README.md | 2 +- electron/main.ts | 14 +- package-lock.json | 4 +- package.json | 2 +- src/App.tsx | 20 +++ src/components/TitleBar.scss | 76 +++++++-- src/components/TitleBar.tsx | 32 ++-- src/hooks/usePlatformInfo.ts | 36 ++++ src/pages/AISummaryWindow.scss | 269 ++++++++++++++++++------------ src/pages/AISummaryWindow.tsx | 52 +++--- src/pages/AgreementPage.scss | 29 +++- src/pages/AgreementPage.tsx | 6 +- src/pages/AnnualReportWindow.scss | 8 +- src/pages/BrowserWindowPage.tsx | 4 +- src/pages/ChatHistoryPage.tsx | 2 +- src/pages/ChatPage.scss | 4 +- src/pages/GroupAnalyticsPage.scss | 4 +- src/pages/ImageWindow.scss | 12 +- src/pages/MomentsWindow.scss | 31 +++- src/pages/MomentsWindow.tsx | 3 +- src/pages/VideoWindow.scss | 7 +- src/styles/main.scss | 19 +++ src/utils/windowChrome.ts | 54 ++++++ 24 files changed, 505 insertions(+), 196 deletions(-) create mode 100644 src/hooks/usePlatformInfo.ts create mode 100644 src/utils/windowChrome.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 293fc3e..4ec7bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ ### 变更 - 暂无 +## [4.1.3] - 2026-04-08 + +### 修复 +- 修复独立朋友圈窗口顶部标题栏在 Windows 和 macOS 下的控件预留与标题布局错位问题 +- 修复聊天记录、内置浏览器、AI 摘要、协议页、图片查看器、视频播放页等独立窗口标题栏高度不一致导致的偏移和首屏抖动问题 + +### 变更 +- 为独立窗口统一引入跨平台 window chrome 安全区变量,按平台区分左侧 traffic lights / 右侧系统按钮预留 +- 通用 `TitleBar` 新增独立窗口布局模式,统一 Windows 左对齐标题和 macOS 居中标题策略 +- 优化朋友圈标题栏右侧操作区在 macOS 下的收缩规则,避免按钮区挤压标题 + ## [4.1.1] - 2026-04-08 ### 变更 diff --git a/README.md b/README.md index 6c94268..27be525 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **一款现代化的微信聊天记录查看与分析工具** [![License](https://img.shields.io/badge/license-CC--BY--NC--SA--4.0-blue.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-4.1.2-green.svg)](package.json) +[![Version](https://img.shields.io/badge/version-4.1.3-green.svg)](package.json) [![Platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows)]() [![Electron](https://img.shields.io/badge/Electron-39-47848F.svg?logo=electron)]() [![React](https://img.shields.io/badge/React-19-61DAFB.svg?logo=react)]() diff --git a/electron/main.ts b/electron/main.ts index c4586e6..fb8c18b 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -462,7 +462,7 @@ function createChatWindow() { titleBarOverlay: { color: '#00000000', symbolColor: '#666666', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0' @@ -537,7 +537,7 @@ function createGroupAnalyticsWindow() { titleBarOverlay: { color: '#00000000', symbolColor: '#666666', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0' @@ -615,7 +615,7 @@ function createMomentsWindow(filterUsername?: string) { titleBarOverlay: { color: '#00000000', symbolColor: '#666666', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0' @@ -704,7 +704,7 @@ function createChatHistoryWindow(sessionId: string, messageId: number) { titleBarOverlay: { color: '#00000000', symbolColor: isDark ? '#ffffff' : '#1a1a1a', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0', @@ -770,7 +770,7 @@ function createAnnualReportWindow(year: number) { titleBarOverlay: { color: '#00000000', symbolColor: isDark ? '#FFFFFF' : '#333333', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#F9F8F6' @@ -842,7 +842,7 @@ function createAgreementWindow() { titleBarOverlay: { color: '#00000000', symbolColor: isDark ? '#FFFFFF' : '#333333', - height: 32 + height: 40 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#FFFFFF' @@ -993,7 +993,7 @@ function createImageViewerWindow( titleBarOverlay: { color: '#00000000', symbolColor: '#ffffff', - height: 32 + height: 40 }, show: false, backgroundColor: '#000000', diff --git a/package-lock.json b/package-lock.json index 2bd12e2..396d81d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ciphertalk", - "version": "4.1.2", + "version": "4.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ciphertalk", - "version": "4.1.2", + "version": "4.1.3", "hasInstallScript": true, "license": "CC-BY-NC-SA-4.0", "dependencies": { diff --git a/package.json b/package.json index 11bebe5..fb07245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ciphertalk", - "version": "4.1.2", + "version": "4.1.3", "description": "密语 - 微信聊天记录查看工具", "author": "ILoveBingLu", "license": "CC-BY-NC-SA-4.0", diff --git a/src/App.tsx b/src/App.tsx index 8fca0a5..2e2bccf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import { initTldList } from './utils/linkify' import LockScreen from './pages/LockScreen' import { useAuthStore } from './stores/authStore' import { X, Shield, Loader2 } from 'lucide-react' +import { applyWindowChromeToDocument } from './utils/windowChrome' import './App.scss' type AppUpdateInfo = { @@ -119,6 +120,25 @@ function App() { initAuth() }, [loadTheme]) + useEffect(() => { + let cancelled = false + + const applyPlatformChrome = (platform?: string) => { + if (cancelled) return + applyWindowChromeToDocument(platform) + } + + void window.electronAPI.app.getPlatformInfo().then((info) => { + applyPlatformChrome(info.platform) + }).catch(() => { + applyPlatformChrome('win32') + }) + + return () => { + cancelled = true + } + }, []) + // 应用主题 useEffect(() => { if (!isLoaded) return diff --git a/src/components/TitleBar.scss b/src/components/TitleBar.scss index 3af43d9..bf5ba2b 100644 --- a/src/components/TitleBar.scss +++ b/src/components/TitleBar.scss @@ -1,19 +1,40 @@ .title-bar { - height: 41px; background: var(--bg-secondary); - display: grid; - grid-template-columns: minmax(140px, 1fr) auto minmax(140px, 1fr); - align-items: center; - padding-left: 16px; - padding-right: 144px; // 为窗口控件留空间 border-bottom: 1px solid var(--border-color); -webkit-app-region: drag; flex-shrink: 0; + user-select: none; - &.is-mac { + &.variant-app { + height: 41px; + display: grid; + grid-template-columns: minmax(140px, 1fr) auto minmax(140px, 1fr); + align-items: center; + padding-left: 16px; + padding-right: 144px; // 为窗口控件留空间 + } + + &.variant-app.is-mac { padding-left: 84px; padding-right: 16px; } + + &.variant-standalone { + height: var(--window-chrome-height); + min-height: var(--window-chrome-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--window-toolbar-gap); + padding-left: 16px; + padding-right: var(--window-controls-right-safe); + } + + &.variant-standalone.is-mac { + display: grid; + grid-template-columns: minmax(calc(var(--window-controls-left-safe) - 16px), 1fr) auto minmax(0, 1fr); + padding-right: var(--window-controls-right-safe); + } } .title-bar-left { @@ -36,7 +57,8 @@ } .title-bar-traffic-spacer { - width: 68px; + width: calc(var(--window-controls-left-safe) - 16px); + min-width: 68px; height: 12px; flex-shrink: 0; } @@ -78,7 +100,7 @@ display: flex; align-items: center; justify-content: flex-end; - gap: 10px; + gap: var(--window-toolbar-gap); min-width: 0; -webkit-app-region: no-drag; } @@ -99,6 +121,23 @@ max-width: 600px; } +.title-bar.variant-standalone.is-win { + .title-bar-left { + flex: 1; + min-width: 0; + } + + .title-bar-right { + flex-shrink: 0; + max-width: min(56vw, 520px); + overflow: hidden; + } + + .titles { + max-width: min(46vw, 420px); + } +} + .title-bar.is-mac { .title-bar-left { justify-content: flex-start; @@ -114,6 +153,11 @@ } } +.title-bar.variant-standalone.is-mac { + .title-bar-center .titles { + max-width: 320px; + } +} // 导出页面标签切换 .export-tabs { @@ -125,15 +169,23 @@ } @media (max-width: 960px) { - .title-bar { + .title-bar.variant-app { grid-template-columns: minmax(90px, 1fr) auto minmax(90px, 1fr); } - .title-bar.is-mac { + .title-bar.variant-app.is-mac { padding-left: 76px; } - .title-bar.is-mac .title-bar-center .titles { + .title-bar.variant-app.is-mac .title-bar-center .titles { + max-width: 220px; + } + + .title-bar.variant-standalone.is-win .titles { + max-width: 240px; + } + + .title-bar.variant-standalone.is-mac .title-bar-center .titles { max-width: 220px; } diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index cb69aff..84851c2 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,5 +1,6 @@ -import { ReactNode, useEffect, useState } from 'react' +import { ReactNode } from 'react' import { RefreshCw } from 'lucide-react' +import { usePlatformInfo } from '../hooks/usePlatformInfo' import { useTitleBarStore } from '../stores/titleBarStore' import { useUpdateStatusStore } from '../stores/updateStatusStore' import { useThemeStore } from '../stores/themeStore' @@ -8,24 +9,16 @@ import './TitleBar.scss' interface TitleBarProps { rightContent?: ReactNode title?: string + variant?: 'app' | 'standalone' } -function TitleBar({ rightContent, title }: TitleBarProps) { +function TitleBar({ rightContent, title, variant = 'app' }: TitleBarProps) { const storeRightContent = useTitleBarStore(state => state.rightContent) const displayContent = rightContent ?? storeRightContent const isUpdating = useUpdateStatusStore(state => state.isUpdating) const appIcon = useThemeStore(state => state.appIcon) - const [platform, setPlatform] = useState<'win32' | 'darwin' | 'linux'>('win32') + const { isMac } = usePlatformInfo() - useEffect(() => { - void window.electronAPI.app.getPlatformInfo().then((info) => { - setPlatform((info.platform as 'win32' | 'darwin' | 'linux') || 'win32') - }).catch(() => { - // ignore - }) - }, []) - - const isMac = platform === 'darwin' const updateStatusNode = isUpdating ? (
) : null + const titleNode = ( + <> + 密语 + {title || 'CipherTalk'} + + ) + return ( -
+
{isMac ? ( {isMac && (
- 密语 - {title || 'CipherTalk'} + {titleNode}
)}
diff --git a/src/hooks/usePlatformInfo.ts b/src/hooks/usePlatformInfo.ts new file mode 100644 index 0000000..7ea4ab3 --- /dev/null +++ b/src/hooks/usePlatformInfo.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react' +import { normalizeWindowPlatform, type WindowPlatform } from '../utils/windowChrome' + +const getInitialPlatform = (): WindowPlatform => { + if (typeof document === 'undefined') { + return 'win32' + } + + return normalizeWindowPlatform(document.documentElement.dataset.windowPlatform) +} + +export function usePlatformInfo() { + const [platform, setPlatform] = useState(getInitialPlatform) + + useEffect(() => { + let cancelled = false + + void window.electronAPI.app.getPlatformInfo().then((info) => { + if (cancelled) return + setPlatform(normalizeWindowPlatform(info.platform)) + }).catch(() => { + // ignore + }) + + return () => { + cancelled = true + } + }, []) + + return { + platform, + isMac: platform === 'darwin', + isWindows: platform === 'win32', + isLinux: platform === 'linux' + } +} diff --git a/src/pages/AISummaryWindow.scss b/src/pages/AISummaryWindow.scss index 9037050..f4fc727 100644 --- a/src/pages/AISummaryWindow.scss +++ b/src/pages/AISummaryWindow.scss @@ -7,137 +7,190 @@ overflow: hidden; .title-bar { - display: flex; align-items: center; - justify-content: space-between; - height: 40px; - padding: 0 140px 0 12px; // 右侧留出空间给原生窗口控件 + height: var(--window-chrome-height); + min-height: var(--window-chrome-height); background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); -webkit-app-region: drag; user-select: none; - gap: 12px; + } - .title-content { + .title-bar-leading-spacer { + width: calc(var(--window-controls-left-safe) - 16px); + min-width: 68px; + height: 12px; + } + + .title-bar-center { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + } + + .title-content { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex-shrink: 1; + + .session-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; + } + + .multiply-symbol { + font-size: 14px; + color: var(--text-tertiary); + font-weight: 300; + margin: 0 -2px; + flex-shrink: 0; + } + + .ai-provider-badge { + width: 24px; + height: 24px; + border-radius: 50%; display: flex; align-items: center; - gap: 8px; + justify-content: center; flex-shrink: 0; - .session-avatar { - width: 24px; - height: 24px; - border-radius: 50%; - object-fit: cover; - flex-shrink: 0; - } + .ai-provider-logo { + width: 100%; + height: 100%; + object-fit: contain; - .multiply-symbol { - font-size: 14px; - color: var(--text-tertiary); - font-weight: 300; - margin: 0 -2px; - } - - .ai-provider-badge { - width: 24px; - height: 24px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - .ai-provider-logo { - width: 100%; - height: 100%; - object-fit: contain; - - // 对于不带 -color 的 logo,根据主题自适应颜色 - &:not([src*="-color"]) { - filter: brightness(0) saturate(100%) invert(var(--logo-invert, 0)); - opacity: 0.85; - } + &:not([src*="-color"]) { + filter: brightness(0) saturate(100%) invert(var(--logo-invert, 0)); + opacity: 0.85; } } - - .session-name { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); - white-space: nowrap; - } } - .message-count { - font-size: 11px; - color: var(--primary); - background: var(--primary-light); - padding: 2px 8px; - border-radius: 10px; + .session-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); white-space: nowrap; - flex-shrink: 0; - font-weight: 500; - margin-right: auto; // 推到左边 + overflow: hidden; + text-overflow: ellipsis; + max-width: min(34vw, 300px); + } + } + + .message-count { + font-size: 11px; + color: var(--primary); + background: var(--primary-light); + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + flex-shrink: 0; + font-weight: 500; + } + + .title-actions { + display: flex; + align-items: center; + gap: 4px; + -webkit-app-region: no-drag; + flex-shrink: 0; + + .generating-status { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin-right: 8px; + position: relative; + + .spinner { + animation: spin 1s linear infinite; + color: var(--primary); + } + + &:hover::after { + content: attr(data-tooltip); + position: absolute; + top: 100%; + right: 0; + transform: translateY(8px); + padding: 4px 8px; + background: var(--bg-tooltip, rgba(0, 0, 0, 0.8)); + color: white; + font-size: 11px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + z-index: 1000; + animation: tooltipFadeIn 0.2s; + } + } + + .title-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + color: var(--primary); + } + } + } + + &.is-win { + .title-bar { + display: flex; + justify-content: space-between; + gap: var(--window-toolbar-gap); + padding: 0 var(--window-controls-right-safe) 0 12px; + } + + .title-bar-center { + flex: 1; + margin-right: auto; } .title-actions { - display: flex; - align-items: center; - gap: 4px; - -webkit-app-region: no-drag; - flex-shrink: 0; + margin-left: auto; + } + } - .generating-status { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - margin-right: 8px; - position: relative; + &.is-mac { + .title-bar { + display: grid; + grid-template-columns: minmax(calc(var(--window-controls-left-safe) - 16px), 1fr) auto minmax(0, 1fr); + gap: var(--window-toolbar-gap); + padding: 0 var(--window-controls-right-safe) 0 16px; + } - .spinner { - animation: spin 1s linear infinite; - color: var(--primary); - } + .title-bar-center { + justify-self: center; + } - &:hover::after { - content: attr(data-tooltip); - position: absolute; - top: 100%; - right: 0; - transform: translateY(8px); - padding: 4px 8px; - background: var(--bg-tooltip, rgba(0, 0, 0, 0.8)); - color: white; - font-size: 11px; - border-radius: 4px; - white-space: nowrap; - pointer-events: none; - z-index: 1000; - animation: tooltipFadeIn 0.2s; - } - } + .title-actions { + justify-self: end; + overflow: hidden; + } - .title-btn { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: none; - border-radius: 4px; - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s; - - &:hover { - background: var(--bg-hover); - color: var(--primary); - } - } + .title-content .session-name { + max-width: min(28vw, 240px); } } diff --git a/src/pages/AISummaryWindow.tsx b/src/pages/AISummaryWindow.tsx index cb42816..e4bdd78 100644 --- a/src/pages/AISummaryWindow.tsx +++ b/src/pages/AISummaryWindow.tsx @@ -3,10 +3,12 @@ import { Copy, Download, RefreshCw, Loader2, Send, ArrowLeft, Trash2, LoaderPinw import { marked } from 'marked' import DOMPurify from 'dompurify' import { TIME_RANGE_OPTIONS, type SummaryResult } from '../types/ai' +import { usePlatformInfo } from '../hooks/usePlatformInfo' import AIProviderLogo from '../components/ai/AIProviderLogo' import './AISummaryWindow.scss' function AISummaryWindow() { + const { isMac } = usePlatformInfo() const [sessionId, setSessionId] = useState('') const [sessionName, setSessionName] = useState('') const [avatarUrl, setAvatarUrl] = useState('') @@ -363,33 +365,37 @@ function AISummaryWindow() { } return ( -
+
{/* 自定义标题栏 */}
-
- {avatarUrl && ( - + {isMac &&