fix: 统一独立窗口跨平台标题栏布局并升级到 4.1.3

- 为独立窗口统一引入跨平台 window chrome 安全区和标题栏高度变量
- 优化朋友圈、聊天记录、浏览器、AI 摘要、协议页、图片/视频窗口等标题栏在 Windows/macOS 下的布局表现
- 统一主进程独立窗口 titleBarOverlay 高度为 40,减少首屏偏移和抖动
- 升级版本号到 4.1.3,并补充 README 与 CHANGELOG 记录
This commit is contained in:
ILoveBingLu
2026-04-08 19:47:09 +08:00
parent 9acf1d7bac
commit ca8caaabbc
24 changed files with 505 additions and 196 deletions

View File

@@ -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
### 变更

View File

@@ -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)]()

View File

@@ -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',

4
package-lock.json generated
View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
{
"name": "ciphertalk",
"version": "4.1.2",
"version": "4.1.3",
"description": "密语 - 微信聊天记录查看工具",
"author": "ILoveBingLu",
"license": "CC-BY-NC-SA-4.0",

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 ? (
<div className="update-status">
<RefreshCw
@@ -37,23 +30,28 @@ function TitleBar({ rightContent, title }: TitleBarProps) {
</div>
) : null
const titleNode = (
<>
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
<span className="titles">{title || 'CipherTalk'}</span>
</>
)
return (
<div className={`title-bar ${isMac ? 'is-mac' : 'is-win'}`}>
<div className={`title-bar variant-${variant} ${isMac ? 'is-mac' : 'is-win'}`}>
<div className="title-bar-left">
{isMac ? (
<div className="title-bar-traffic-spacer" aria-hidden="true" />
) : (
<>
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
<span className="titles">{title || 'CipherTalk'}</span>
{titleNode}
{updateStatusNode}
</>
)}
</div>
{isMac && (
<div className="title-bar-center">
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
<span className="titles">{title || 'CipherTalk'}</span>
{titleNode}
</div>
)}
<div className="title-bar-right">

View File

@@ -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<WindowPlatform>(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'
}
}

View File

@@ -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);
}
}

View File

@@ -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<string>('')
const [sessionName, setSessionName] = useState<string>('')
const [avatarUrl, setAvatarUrl] = useState<string>('')
@@ -363,33 +365,37 @@ function AISummaryWindow() {
}
return (
<div className="ai-summary-window">
<div className={`ai-summary-window ${isMac ? 'is-mac' : 'is-win'}`}>
{/* 自定义标题栏 */}
<div className="title-bar">
<div className="title-content">
{avatarUrl && (
<img src={avatarUrl} alt="" className="session-avatar" />
{isMac && <div className="title-bar-leading-spacer" aria-hidden="true" />}
<div className="title-bar-center">
<div className="title-content">
{avatarUrl && (
<img src={avatarUrl} alt="" className="session-avatar" />
)}
{aiProviderInfo && (
<>
<span className="multiply-symbol">×</span>
<div className="ai-provider-badge">
<AIProviderLogo
providerId={aiProviderInfo.id}
logo={aiProviderInfo.logo}
alt={aiProviderInfo.displayName}
className="ai-provider-logo"
size={24}
/>
</div>
</>
)}
<span className="session-name">{sessionName}</span>
</div>
{result && (
<span className="message-count">{result.messageCount}</span>
)}
{aiProviderInfo && (
<>
<span className="multiply-symbol">×</span>
<div className="ai-provider-badge">
<AIProviderLogo
providerId={aiProviderInfo.id}
logo={aiProviderInfo.logo}
alt={aiProviderInfo.displayName}
className="ai-provider-logo"
size={24}
/>
</div>
</>
)}
<span className="session-name">{sessionName}</span>
</div>
{result && (
<span className="message-count">{result.messageCount}</span>
)}
<div className="title-actions">
{isGenerating && (

View File

@@ -6,19 +6,42 @@
}
.agreement-titlebar {
height: 40px;
height: var(--window-chrome-height);
min-height: var(--window-chrome-height);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
flex-shrink: 0;
user-select: none;
&.is-win {
justify-content: flex-start;
padding-left: 16px;
padding-right: var(--window-controls-right-safe);
}
&.is-mac {
display: grid;
grid-template-columns: minmax(calc(var(--window-controls-left-safe) - 16px), 1fr) auto minmax(0, 1fr);
padding-left: 16px;
padding-right: var(--window-controls-right-safe);
}
.agreement-titlebar-spacer {
width: calc(var(--window-controls-left-safe) - 16px);
min-width: 68px;
height: 12px;
}
span {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
@@ -92,4 +115,4 @@
background: var(--text-tertiary);
}
}
}
}

View File

@@ -1,10 +1,14 @@
import './AgreementPage.scss'
import * as configService from '../services/config'
import { usePlatformInfo } from '../hooks/usePlatformInfo'
function AgreementPage() {
const { isMac } = usePlatformInfo()
return (
<div className="agreement-page">
<div className="agreement-titlebar">
<div className={`agreement-titlebar ${isMac ? 'is-mac' : 'is-win'}`}>
{isMac && <div className="agreement-titlebar-spacer" aria-hidden="true" />}
<span></span>
</div>
<div className="agreement-content">

View File

@@ -804,12 +804,16 @@
position: fixed;
top: 0;
left: 0;
right: 138px;
height: 32px;
right: var(--window-controls-right-safe);
height: var(--window-chrome-height);
-webkit-app-region: drag !important;
z-index: 100;
}
html[data-window-platform="darwin"] .annual-report-window .drag-region {
left: var(--window-controls-left-safe);
}
// 浮动操作按钮
.fab-container {
position: fixed;

View File

@@ -74,7 +74,7 @@ const BrowserWindowPage = () => {
return (
<div className="browser-window" style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#fff' }}>
<TitleBar title={pageTitle} />
<TitleBar title={pageTitle} variant="standalone" />
{/* 简单的进度条 */}
{isLoading && (
@@ -83,7 +83,7 @@ const BrowserWindowPage = () => {
background: 'var(--primary)',
width: '100%',
position: 'fixed',
top: '32px', // TitleBar height
top: 'var(--window-chrome-height)',
zIndex: 9999,
animation: 'loading 2s infinite linear'
}} />

View File

@@ -153,7 +153,7 @@ export default function ChatHistoryPage() {
return (
<div className="chat-history-page">
<TitleBar title={title} />
<TitleBar title={title} variant="standalone" />
<div className="history-list">
{loading ? (
<div className="status-msg">...</div>

View File

@@ -18,7 +18,7 @@
backdrop-filter: blur(20px);
.session-header {
padding-top: 38px;
padding-top: calc(var(--window-chrome-height) + 6px);
padding-bottom: 12px;
padding-left: 16px;
padding-right: 16px;
@@ -248,7 +248,7 @@
.message-header {
height: auto;
padding: 0 24px;
padding-top: 38px;
padding-top: calc(var(--window-chrome-height) + 6px);
padding-bottom: 12px;
background: var(--card-bg);
border-bottom: 1px solid var(--border-color);

View File

@@ -13,7 +13,7 @@
-webkit-app-region: no-drag;
.sidebar-header {
padding-top: 38px;
padding-top: calc(var(--window-chrome-height) + 6px);
padding-bottom: 12px;
padding-left: 16px;
padding-right: 16px;
@@ -428,7 +428,7 @@
align-items: center;
gap: 12px;
padding: 0 24px;
padding-top: 38px;
padding-top: calc(var(--window-chrome-height) + 6px);
padding-bottom: 12px;
background: var(--card-bg);
border-bottom: 1px solid var(--border-color);

View File

@@ -9,14 +9,15 @@
// -webkit-app-region: drag; // Removed global drag to prevent unwanted resize/drag behaviors
.title-bar {
height: 40px;
min-height: 40px;
height: var(--window-chrome-height);
min-height: var(--window-chrome-height);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding-right: 140px; // 为原生窗口控件留出空间
padding-left: var(--window-controls-left-safe);
padding-right: var(--window-controls-right-safe);
-webkit-app-region: drag; // Only title bar is natively draggable
z-index: 500; // Even higher to stay above image content
position: relative;
@@ -30,9 +31,8 @@
.title-bar-controls {
display: flex;
align-items: center;
gap: 8px;
gap: var(--window-toolbar-gap);
-webkit-app-region: no-drag;
margin-right: 16px;
button {
background: transparent;
@@ -275,4 +275,4 @@
@keyframes tooltipFadeInSns {
// Deprecated, using transitions to avoid transform conflicts
}
}

View File

@@ -1241,7 +1241,9 @@
.title-actions {
display: flex;
align-items: center;
gap: 8px;
gap: var(--window-toolbar-gap);
min-width: 0;
max-width: 100%;
.export-btn {
display: flex;
@@ -1264,6 +1266,7 @@
line-height: 1;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 1.5, 0.5, 1);
flex-shrink: 0;
&:hover {
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.25) 0%, rgba(var(--primary-rgb), 0.1) 100%);
@@ -1295,6 +1298,7 @@
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
&:hover {
background: var(--hover-bg);
@@ -1333,6 +1337,29 @@
}
}
html[data-window-platform="darwin"] .moments-window .title-actions {
gap: 6px;
}
@media (max-width: 1180px) {
html[data-window-platform="darwin"] .moments-window .title-actions .divider {
display: none;
}
}
@media (max-width: 1040px) {
html[data-window-platform="darwin"] .moments-window .title-actions .export-btn {
width: 30px;
padding: 6px;
gap: 0;
justify-content: center;
}
html[data-window-platform="darwin"] .moments-window .title-actions .export-btn span {
display: none;
}
}
.icon-btn {
width: 30px;
height: 30px;
@@ -1784,4 +1811,4 @@
100% {
background-position: -200% 0;
}
}
}

View File

@@ -1535,9 +1535,10 @@ document.querySelectorAll('.vi video').forEach(function(v) {
<div className="moments-window">
<TitleBar
title="朋友圈"
variant="standalone"
rightContent={
<div className="title-actions">
<button className="export-btn" onClick={() => setShowExportOptions(true)}>
<button className="export-btn" onClick={() => setShowExportOptions(true)} data-tooltip="导出">
<FileDown size={14} />
<span></span>
</button>

View File

@@ -8,11 +8,12 @@
user-select: none;
.title-bar {
height: 40px;
min-height: 40px;
height: var(--window-chrome-height);
min-height: var(--window-chrome-height);
display: flex;
background: #1a1a1a;
padding-right: 140px;
padding-left: var(--window-controls-left-safe);
padding-right: var(--window-controls-right-safe);
position: relative;
z-index: 10;

View File

@@ -40,6 +40,25 @@
// Logo 颜色反转(浅色主题用 0深色主题用 1
--logo-invert: 0;
// 独立窗口标题栏安全区
--window-chrome-height: 40px;
--window-controls-left-safe: 16px;
--window-controls-right-safe: 144px;
--window-toolbar-gap: 10px;
}
:root[data-window-platform="darwin"] {
--window-controls-left-safe: 84px;
--window-controls-right-safe: 16px;
--window-toolbar-gap: 8px;
}
:root[data-window-platform="linux"],
:root[data-window-platform="win32"] {
--window-controls-left-safe: 16px;
--window-controls-right-safe: 144px;
--window-toolbar-gap: 10px;
}
// ==================== 浅色主题 ====================

54
src/utils/windowChrome.ts Normal file
View File

@@ -0,0 +1,54 @@
export type WindowPlatform = 'win32' | 'darwin' | 'linux'
type WindowChromeMetrics = {
controlsLeftSafe: string
controlsRightSafe: string
toolbarGap: string
}
const DEFAULT_PLATFORM: WindowPlatform = 'win32'
const WINDOW_CHROME_HEIGHT = '40px'
const WINDOW_CHROME_METRICS: Record<WindowPlatform, WindowChromeMetrics> = {
win32: {
controlsLeftSafe: '16px',
controlsRightSafe: '144px',
toolbarGap: '10px'
},
darwin: {
controlsLeftSafe: '84px',
controlsRightSafe: '16px',
toolbarGap: '8px'
},
linux: {
controlsLeftSafe: '16px',
controlsRightSafe: '144px',
toolbarGap: '10px'
}
}
export function normalizeWindowPlatform(platform?: string | null): WindowPlatform {
if (platform === 'darwin' || platform === 'linux' || platform === 'win32') {
return platform
}
return DEFAULT_PLATFORM
}
export function getWindowChromeMetrics(platform?: string | null) {
const normalizedPlatform = normalizeWindowPlatform(platform)
return {
platform: normalizedPlatform,
chromeHeight: WINDOW_CHROME_HEIGHT,
...WINDOW_CHROME_METRICS[normalizedPlatform]
}
}
export function applyWindowChromeToDocument(platform?: string | null, root: HTMLElement = document.documentElement) {
const metrics = getWindowChromeMetrics(platform)
root.dataset.windowPlatform = metrics.platform
root.style.setProperty('--window-chrome-height', metrics.chromeHeight)
root.style.setProperty('--window-controls-left-safe', metrics.controlsLeftSafe)
root.style.setProperty('--window-controls-right-safe', metrics.controlsRightSafe)
root.style.setProperty('--window-toolbar-gap', metrics.toolbarGap)
}