mirror of
https://mirror.skon.top/github.com/ILoveBingLu/CipherTalk
synced 2026-04-30 13:51:50 +08:00
fix: 统一独立窗口跨平台标题栏布局并升级到 4.1.3
- 为独立窗口统一引入跨平台 window chrome 安全区和标题栏高度变量 - 优化朋友圈、聊天记录、浏览器、AI 摘要、协议页、图片/视频窗口等标题栏在 Windows/macOS 下的布局表现 - 统一主进程独立窗口 titleBarOverlay 高度为 40,减少首屏偏移和抖动 - 升级版本号到 4.1.3,并补充 README 与 CHANGELOG 记录
This commit is contained in:
11
CHANGELOG.md
11
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
|
||||
|
||||
### 变更
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
**一款现代化的微信聊天记录查看与分析工具**
|
||||
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ciphertalk",
|
||||
"version": "4.1.2",
|
||||
"version": "4.1.3",
|
||||
"description": "密语 - 微信聊天记录查看工具",
|
||||
"author": "ILoveBingLu",
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
|
||||
20
src/App.tsx
20
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
36
src/hooks/usePlatformInfo.ts
Normal file
36
src/hooks/usePlatformInfo.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
}} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
54
src/utils/windowChrome.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user