feat: 添加系统托盘功能,支持窗口最小化到托盘及配置保存状态监控

This commit is contained in:
ILoveBingLu
2026-03-07 17:22:20 +08:00
parent fa48ac4abe
commit ca559a094e
10 changed files with 571 additions and 24 deletions
+13 -13
View File
@@ -12,13 +12,13 @@ function WhatsNewModal({ onClose, version }: WhatsNewModalProps) {
{
icon: <Package size={20} />,
title: '优化',
desc: '优化聊天消息去重逻辑。'
desc: '优化html导出。'
},
{
icon: <Package size={20} />,
title: '优化',
desc: '优化多个界面样式,重点优化设置界面。'
},
desc: '优化最小化至托盘功能。'
}
// {
// icon: <Image size={20} />,
// title: '聊天内图片',
@@ -29,16 +29,16 @@ function WhatsNewModal({ onClose, version }: WhatsNewModalProps) {
// title: '语音导出',
// desc: '支持将语音消息解码为 WAV 格式导出,含转写文字。'
// },
{
icon: <Filter size={20} />,
title: '新增',
desc: '新增API端点等功能。'
},
{
icon: <Aperture size={20} />,
title: '朋友圈',
desc: '优化样式!'
}
// {
// icon: <Filter size={20} />,
// title: '新增',
// desc: '新增API端点等功能。'
// },
// {
// icon: <Aperture size={20} />,
// title: '朋友圈',
// desc: '优化样式!'
// }
]
const handleTelegram = () => {
+119 -1
View File
@@ -3154,7 +3154,14 @@ to {
// 用极淡的底框包裹整体,去除左上角的硬高光边框
border: 1px solid rgba(255, 255, 255, 0.15);
// 根据主题模式调整图标颜色
color: white;
[data-mode="light"] & {
color: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.1);
}
display: flex;
align-items: center;
justify-content: center;
@@ -3166,6 +3173,13 @@ to {
inset 0 4px 10px -2px rgba(255, 255, 255, 0.4),
inset 0 -2px 10px rgba(0, 0, 0, 0.05);
[data-mode="light"] & {
box-shadow:
0 8px 32px rgba(var(--primary-rgb), 0.25),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.6),
inset 0 -2px 10px rgba(0, 0, 0, 0.08);
}
transition: all 0.3s cubic-bezier(0.25, 1.5, 0.5, 1);
z-index: 1000;
@@ -3176,6 +3190,13 @@ to {
0 12px 40px rgba(var(--primary-rgb), 0.4),
inset 0 6px 14px -3px rgba(255, 255, 255, 0.5),
inset 0 -2px 10px rgba(0, 0, 0, 0.05);
[data-mode="light"] & {
box-shadow:
0 12px 40px rgba(var(--primary-rgb), 0.35),
inset 0 6px 14px -3px rgba(255, 255, 255, 0.7),
inset 0 -2px 10px rgba(0, 0, 0, 0.1);
}
}
&:active:not(:disabled) {
@@ -3183,6 +3204,12 @@ to {
box-shadow:
0 4px 16px rgba(var(--primary-rgb), 0.2),
inset 0 1px 2px rgba(255, 255, 255, 0.2);
[data-mode="light"] & {
box-shadow:
0 4px 16px rgba(var(--primary-rgb), 0.15),
inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
&:disabled {
@@ -3193,4 +3220,95 @@ to {
box-shadow: none;
color: var(--text-tertiary);
}
}
}
// 保存按钮有未保存更改时的样式
.floating-save-btn.has-changes {
// 深色模式:红色渐变
background: linear-gradient(135deg, rgba(239, 68, 68, 0.7) 0%, rgba(220, 38, 38, 0.4) 100%);
border-color: rgba(255, 255, 255, 0.25);
animation: pulse-save 2s ease-in-out infinite;
color: white;
box-shadow:
0 8px 32px rgba(239, 68, 68, 0.4),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.5),
inset 0 -2px 10px rgba(0, 0, 0, 0.1);
// 浅色模式:更鲜艳的红色,深色图标
[data-mode="light"] & {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.85) 0%, rgba(220, 38, 38, 0.6) 100%);
border-color: rgba(0, 0, 0, 0.15);
color: white;
animation: pulse-save-light 2s ease-in-out infinite;
box-shadow:
0 8px 32px rgba(239, 68, 68, 0.35),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.7),
inset 0 -2px 10px rgba(0, 0, 0, 0.15);
}
&:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.85) 0%, rgba(220, 38, 38, 0.5) 100%);
animation: none;
box-shadow:
0 12px 40px rgba(239, 68, 68, 0.5),
inset 0 6px 14px -3px rgba(255, 255, 255, 0.6),
inset 0 -2px 10px rgba(0, 0, 0, 0.1);
[data-mode="light"] & {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.95) 0%, rgba(220, 38, 38, 0.7) 100%);
box-shadow:
0 12px 40px rgba(239, 68, 68, 0.45),
inset 0 6px 14px -3px rgba(255, 255, 255, 0.8),
inset 0 -2px 10px rgba(0, 0, 0, 0.2);
}
}
&:active:not(:disabled) {
transform: translateY(2px) scale(0.95);
box-shadow:
0 4px 16px rgba(239, 68, 68, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.3);
[data-mode="light"] & {
box-shadow:
0 4px 16px rgba(239, 68, 68, 0.25),
inset 0 1px 2px rgba(0, 0, 0, 0.15);
}
}
}
// 保存按钮闪烁动画 - 深色模式
@keyframes pulse-save {
0%, 100% {
box-shadow:
0 8px 32px rgba(239, 68, 68, 0.4),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.5),
inset 0 -2px 10px rgba(0, 0, 0, 0.1);
}
50% {
box-shadow:
0 8px 40px rgba(239, 68, 68, 0.6),
0 0 20px rgba(239, 68, 68, 0.4),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.6),
inset 0 -2px 10px rgba(0, 0, 0, 0.1);
}
}
// 保存按钮闪烁动画 - 浅色模式
@keyframes pulse-save-light {
0%, 100% {
box-shadow:
0 8px 32px rgba(239, 68, 68, 0.35),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.7),
inset 0 -2px 10px rgba(0, 0, 0, 0.15);
}
50% {
box-shadow:
0 8px 40px rgba(239, 68, 68, 0.5),
0 0 20px rgba(239, 68, 68, 0.35),
inset 0 4px 10px -2px rgba(255, 255, 255, 0.8),
inset 0 -2px 10px rgba(0, 0, 0, 0.15);
}
}
+120 -1
View File
@@ -150,6 +150,10 @@ function SettingsPage() {
const [logSize, setLogSize] = useState<number>(0)
const [currentLogLevel, setCurrentLogLevel] = useState<string>('WARN')
// 配置变化状态
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [initialConfig, setInitialConfig] = useState<any>(null)
useEffect(() => {
loadConfig()
loadDefaultExportPath()
@@ -230,6 +234,37 @@ function SettingsPage() {
const savedCloseToTray = await configService.getCloseToTray()
setCloseToTray(savedCloseToTray)
// 保存初始配置用于比较
setInitialConfig({
decryptKey: savedKey || '',
dbPath: savedPath || '',
wxid: savedWxid || '',
cachePath: savedCachePath || '',
imageXorKey: savedXorKey || '',
imageAesKey: savedAesKey || '',
exportPath: savedExportPath || '',
sttLanguages: savedSttLanguages && savedSttLanguages.length > 0 ? savedSttLanguages : ['zh'],
sttModelType: savedSttModelType,
skipIntegrityCheck: savedSkipIntegrityCheck,
autoUpdateDatabase: savedAutoUpdateDatabase,
autoUpdateCheckInterval: savedCheckInterval,
autoUpdateMinInterval: savedMinInterval,
autoUpdateDebounceTime: savedDebounceTime,
quoteStyle: savedQuoteStyle,
exportDefaultDateRange: savedExportDefaultDateRange,
exportDefaultAvatars: savedExportDefaultAvatars,
aiProvider: savedAiProvider,
aiApiKey: savedAiApiKey,
aiModel: savedAiModel,
aiDefaultTimeRange: savedAiDefaultTimeRange,
aiSummaryDetail: savedAiSummaryDetail,
aiSystemPromptPreset: savedAiSystemPromptPreset,
aiCustomSystemPrompt: savedAiCustomSystemPrompt,
aiEnableThinking: savedAiEnableThinking,
aiMessageLimit: savedAiMessageLimit,
closeToTray: savedCloseToTray
})
} catch (e) {
console.error('加载配置失败:', e)
}
@@ -244,6 +279,53 @@ function SettingsPage() {
}
}
// 监听配置变化
useEffect(() => {
if (!initialConfig) return
const currentConfig = {
decryptKey,
dbPath,
wxid,
cachePath,
imageXorKey,
imageAesKey,
exportPath,
sttLanguages,
sttModelType,
skipIntegrityCheck,
autoUpdateDatabase,
autoUpdateCheckInterval,
autoUpdateMinInterval,
autoUpdateDebounceTime,
quoteStyle,
exportDefaultDateRange,
exportDefaultAvatars,
aiProvider,
aiApiKey,
aiModel,
aiDefaultTimeRange,
aiSummaryDetail,
aiSystemPromptPreset,
aiCustomSystemPrompt,
aiEnableThinking,
aiMessageLimit,
closeToTray
}
// 深度比较配置是否有变化
const hasChanges = JSON.stringify(currentConfig) !== JSON.stringify(initialConfig)
setHasUnsavedChanges(hasChanges)
}, [
decryptKey, dbPath, wxid, cachePath, imageXorKey, imageAesKey, exportPath,
sttLanguages, sttModelType, skipIntegrityCheck, autoUpdateDatabase,
autoUpdateCheckInterval, autoUpdateMinInterval, autoUpdateDebounceTime,
quoteStyle, exportDefaultDateRange, exportDefaultAvatars,
aiProvider, aiApiKey, aiModel, aiDefaultTimeRange, aiSummaryDetail,
aiSystemPromptPreset, aiCustomSystemPrompt, aiEnableThinking, aiMessageLimit,
closeToTray, initialConfig
])
const loadAppVersion = async () => {
try {
const version = await window.electronAPI.app.getVersion()
@@ -776,6 +858,38 @@ function SettingsPage() {
}
showMessage('配置保存成功', true)
// 保存成功后更新初始配置,重置变化状态
setInitialConfig({
decryptKey,
dbPath,
wxid,
cachePath,
imageXorKey,
imageAesKey,
exportPath,
sttLanguages,
sttModelType,
skipIntegrityCheck,
autoUpdateDatabase,
autoUpdateCheckInterval,
autoUpdateMinInterval,
autoUpdateDebounceTime,
quoteStyle,
exportDefaultDateRange,
exportDefaultAvatars,
aiProvider,
aiApiKey,
aiModel,
aiDefaultTimeRange,
aiSummaryDetail,
aiSystemPromptPreset,
aiCustomSystemPrompt,
aiEnableThinking,
aiMessageLimit,
closeToTray
})
setHasUnsavedChanges(false)
} catch (e) {
showMessage(`保存配置失败: ${e}`, false)
} finally {
@@ -2682,7 +2796,12 @@ function SettingsPage() {
</div>
{/* 悬浮保存按钮 */}
<button className="floating-save-btn" onClick={handleSaveConfig} disabled={isLoading} title="保存配置">
<button
className={`floating-save-btn ${hasUnsavedChanges ? 'has-changes' : ''}`}
onClick={handleSaveConfig}
disabled={isLoading}
title={hasUnsavedChanges ? '有未保存的更改,点击保存' : '保存配置'}
>
<Save size={20} />
</button>