mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-20 03:23:21 +08:00
feat: 添加系统托盘功能,支持窗口最小化到托盘及配置保存状态监控
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user