diff --git a/.gitignore b/.gitignore index 3458a1c..0623e78 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ chatlab-format.md *.bak AGENTS.md .claude/ +CLAUDE.md .agents/ resources/wx_send 概述.md diff --git a/electron/main.ts b/electron/main.ts index 57331bf..bb6576f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2018,7 +2018,6 @@ function registerIpcHandlers() { dbPath, decryptKey, wxid, - nativeTimeoutMs: 5000, onProgress: (progress) => { if (isYearsLoadCanceled(taskId)) return const snapshot = updateTaskSnapshot({ diff --git a/src/components/Sidebar.scss b/src/components/Sidebar.scss index 810815b..31d4725 100644 --- a/src/components/Sidebar.scss +++ b/src/components/Sidebar.scss @@ -61,6 +61,16 @@ gap: 4px; padding: 6px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); + opacity: 0; + transform: translateY(8px) scale(0.95); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + + &.open { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; + } } .sidebar-user-menu-item { @@ -265,6 +275,185 @@ gap: 4px; } +.sidebar-dialog-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 1100; + padding: 20px; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.sidebar-dialog { + width: min(420px, 100%); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 16px; + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24); + padding: 18px 18px 16px; + animation: slideUp 0.25s ease; + + h3 { + margin: 0; + font-size: 16px; + color: var(--text-primary); + } + + p { + margin: 10px 0 0; + font-size: 13px; + line-height: 1.6; + color: var(--text-secondary); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.sidebar-wxid-list { + margin-top: 14px; + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.sidebar-wxid-item { + width: 100%; + padding: 12px 14px; + border: 1px solid var(--border-color); + border-radius: 10px; + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 13px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + border-color: rgba(99, 102, 241, 0.32); + background: var(--bg-tertiary); + } + + &.current { + border-color: rgba(99, 102, 241, 0.5); + background: var(--bg-tertiary); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + .wxid-avatar { + width: 40px; + height: 40px; + border-radius: 10px; + overflow: hidden; + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + color: var(--on-primary); + font-size: 16px; + font-weight: 600; + } + } + + .wxid-info { + flex: 1; + min-width: 0; + text-align: left; + } + + .wxid-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .wxid-id { + margin-top: 2px; + font-size: 12px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .current-badge { + padding: 4px 10px; + border-radius: 6px; + background: var(--primary); + color: var(--on-primary); + font-size: 11px; + font-weight: 600; + flex-shrink: 0; + } +} + +.sidebar-dialog-actions { + margin-top: 18px; + display: flex; + justify-content: flex-end; + gap: 10px; + + button { + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 8px 14px; + font-size: 13px; + cursor: pointer; + background: var(--bg-secondary); + color: var(--text-primary); + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background: var(--bg-tertiary); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + } +} + .sidebar-clear-dialog-overlay { position: fixed; inset: 0; @@ -274,6 +463,7 @@ justify-content: center; z-index: 1100; padding: 20px; + animation: fadeIn 0.2s ease; } .sidebar-clear-dialog { @@ -283,6 +473,7 @@ border-radius: 16px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24); padding: 18px 18px 16px; + animation: slideUp 0.25s ease; h3 { margin: 0; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e215448..e6d0147 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,9 @@ import { useState, useEffect, useRef } from 'react' import { NavLink, useLocation, useNavigate } from 'react-router-dom' -import { Home, MessageSquare, BarChart3, FileText, Settings, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, Trash2 } from 'lucide-react' +import { Home, MessageSquare, BarChart3, FileText, Settings, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, RefreshCw } from 'lucide-react' import { useAppStore } from '../stores/appStore' +import { useChatStore } from '../stores/chatStore' +import { useAnalyticsStore } from '../stores/analyticsStore' import * as configService from '../services/config' import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge' @@ -15,11 +17,28 @@ interface SidebarUserProfile { } const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1' +const ACCOUNT_PROFILES_CACHE_KEY = 'account_profiles_cache_v1' interface SidebarUserProfileCache extends SidebarUserProfile { updatedAt: number } +interface AccountProfilesCache { + [wxid: string]: { + displayName: string + avatarUrl?: string + alias?: string + updatedAt: number + } +} + +interface WxidOption { + wxid: string + modifiedTime: number + displayName?: string + avatarUrl?: string +} + const readSidebarUserProfileCache = (): SidebarUserProfile | null => { try { const raw = window.localStorage.getItem(SIDEBAR_USER_PROFILE_CACHE_KEY) @@ -46,11 +65,32 @@ const writeSidebarUserProfileCache = (profile: SidebarUserProfile): void => { updatedAt: Date.now() } window.localStorage.setItem(SIDEBAR_USER_PROFILE_CACHE_KEY, JSON.stringify(payload)) + + // 同时写入账号缓存池 + const accountsCache = readAccountProfilesCache() + accountsCache[profile.wxid] = { + displayName: profile.displayName, + avatarUrl: profile.avatarUrl, + alias: profile.alias, + updatedAt: Date.now() + } + window.localStorage.setItem(ACCOUNT_PROFILES_CACHE_KEY, JSON.stringify(accountsCache)) } catch { // 忽略本地缓存失败,不影响主流程 } } +const readAccountProfilesCache = (): AccountProfilesCache => { + try { + const raw = window.localStorage.getItem(ACCOUNT_PROFILES_CACHE_KEY) + if (!raw) return {} + const parsed = JSON.parse(raw) + return typeof parsed === 'object' && parsed ? parsed : {} + } catch { + return {} + } +} + const normalizeAccountId = (value?: string | null): string => { const trimmed = String(value || '').trim() if (!trimmed) return '' @@ -76,12 +116,14 @@ function Sidebar({ collapsed }: SidebarProps) { displayName: '未识别用户' }) const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false) - const [showClearAccountDialog, setShowClearAccountDialog] = useState(false) - const [shouldClearCacheData, setShouldClearCacheData] = useState(false) - const [shouldClearExportData, setShouldClearExportData] = useState(false) - const [isClearingAccountData, setIsClearingAccountData] = useState(false) + const [showSwitchAccountDialog, setShowSwitchAccountDialog] = useState(false) + const [wxidOptions, setWxidOptions] = useState([]) + const [isSwitchingAccount, setIsSwitchingAccount] = useState(false) const accountCardWrapRef = useRef(null) const setLocked = useAppStore(state => state.setLocked) + const isDbConnected = useAppStore(state => state.isDbConnected) + const resetChatStore = useChatStore(state => state.reset) + const clearAnalyticsStoreCache = useAnalyticsStore(state => state.clearCache) useEffect(() => { window.electronAPI.auth.verifyEnabled().then(setAuthEnabled) @@ -143,6 +185,9 @@ function Sidebar({ collapsed }: SidebarProps) { const resolvedWxidRaw = String(wxid || '').trim() const cleanedWxid = normalizeAccountId(resolvedWxidRaw) const resolvedWxid = cleanedWxid || resolvedWxidRaw + + if (!resolvedWxidRaw && !resolvedWxid) return + const wxidCandidates = new Set([ resolvedWxidRaw.toLowerCase(), resolvedWxid.trim().toLowerCase(), @@ -168,77 +213,36 @@ function Sidebar({ collapsed }: SidebarProps) { return undefined } - const fallbackDisplayName = resolvedWxid || '未识别用户' + // 并行获取名称和头像 + const [contactResult, avatarResult] = await Promise.allSettled([ + (async () => { + const candidates = Array.from(new Set([resolvedWxidRaw, resolvedWxid, cleanedWxid].filter(Boolean))) + for (const candidate of candidates) { + const contact = await window.electronAPI.chat.getContact(candidate) + if (contact?.remark || contact?.nickName || contact?.alias) { + return contact + } + } + return null + })(), + window.electronAPI.chat.getMyAvatarUrl() + ]) + + const myContact = contactResult.status === 'fulfilled' ? contactResult.value : null + const displayName = pickFirstValidName( + myContact?.remark, + myContact?.nickName, + myContact?.alias + ) || resolvedWxid || '未识别用户' - // 第一阶段:先把 wxid/名称打上,保证侧边栏第一时间可见。 patchUserProfile({ wxid: resolvedWxid, - displayName: fallbackDisplayName + displayName, + alias: myContact?.alias, + avatarUrl: avatarResult.status === 'fulfilled' && avatarResult.value.success + ? avatarResult.value.avatarUrl + : undefined }) - - if (!resolvedWxidRaw && !resolvedWxid) return - - // 第二阶段:后台补齐名称(不会阻塞首屏)。 - void (async () => { - try { - let myContact: Awaited> | null = null - for (const candidate of Array.from(new Set([resolvedWxidRaw, resolvedWxid, cleanedWxid].filter(Boolean)))) { - const contact = await window.electronAPI.chat.getContact(candidate) - if (!contact) continue - if (!myContact) myContact = contact - if (contact.remark || contact.nickName || contact.alias) { - myContact = contact - break - } - } - const fromContact = pickFirstValidName( - myContact?.remark, - myContact?.nickName, - myContact?.alias - ) - - if (fromContact) { - patchUserProfile({ displayName: fromContact }, resolvedWxid) - // 同步补充微信号(alias) - if (myContact?.alias) { - patchUserProfile({ alias: myContact.alias }, resolvedWxid) - } - return - } - - const enrichTargets = Array.from(new Set([resolvedWxidRaw, resolvedWxid, cleanedWxid, 'self'].filter(Boolean))) - const enrichedResult = await window.electronAPI.chat.enrichSessionsContactInfo(enrichTargets) - const enrichedDisplayName = pickFirstValidName( - enrichedResult.contacts?.[resolvedWxidRaw]?.displayName, - enrichedResult.contacts?.[resolvedWxid]?.displayName, - enrichedResult.contacts?.[cleanedWxid]?.displayName, - enrichedResult.contacts?.self?.displayName, - myContact?.alias - ) - const bestName = enrichedDisplayName - if (bestName) { - patchUserProfile({ displayName: bestName }, resolvedWxid) - } - // 降级分支也补充微信号 - if (myContact?.alias) { - patchUserProfile({ alias: myContact.alias }, resolvedWxid) - } - } catch (nameError) { - console.error('加载侧边栏用户昵称失败:', nameError) - } - })() - - // 第二阶段:后台补齐头像(不会阻塞首屏)。 - void (async () => { - try { - const avatarResult = await window.electronAPI.chat.getMyAvatarUrl() - if (avatarResult.success && avatarResult.avatarUrl) { - patchUserProfile({ avatarUrl: avatarResult.avatarUrl }, resolvedWxid) - } - } catch (avatarError) { - console.error('加载侧边栏用户头像失败:', avatarError) - } - })() } catch (error) { console.error('加载侧边栏用户信息失败:', error) } @@ -246,10 +250,7 @@ function Sidebar({ collapsed }: SidebarProps) { const cachedProfile = readSidebarUserProfileCache() if (cachedProfile) { - setUserProfile(prev => ({ - ...prev, - ...cachedProfile - })) + setUserProfile(cachedProfile) } void loadCurrentUser() @@ -263,23 +264,107 @@ function Sidebar({ collapsed }: SidebarProps) { return [...name][0] || '?' } - const isActive = (path: string) => { - return location.pathname === path || location.pathname.startsWith(`${path}/`) - } - const exportTaskBadge = activeExportTaskCount > 99 ? '99+' : `${activeExportTaskCount}` - const canConfirmClear = shouldClearCacheData || shouldClearExportData - - const resetClearDialogState = () => { - setShouldClearCacheData(false) - setShouldClearExportData(false) - setShowClearAccountDialog(false) - } - - const openClearAccountDialog = () => { + const openSwitchAccountDialog = async () => { setIsAccountMenuOpen(false) - setShouldClearCacheData(false) - setShouldClearExportData(false) - setShowClearAccountDialog(true) + if (!isDbConnected) { + window.alert('数据库未连接,无法切换账号') + return + } + const dbPath = await configService.getDbPath() + if (!dbPath) { + window.alert('请先在设置中配置数据库路径') + return + } + try { + const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) + const accountsCache = readAccountProfilesCache() + console.log('[切换账号] 账号缓存:', accountsCache) + + const enrichedWxids = wxids.map(option => { + const normalizedWxid = normalizeAccountId(option.wxid) + const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid] + + if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) { + return { + ...option, + displayName: userProfile.displayName, + avatarUrl: userProfile.avatarUrl + } + } + if (cached) { + console.log('[切换账号] 使用缓存:', option.wxid, cached) + return { + ...option, + displayName: cached.displayName, + avatarUrl: cached.avatarUrl + } + } + return { ...option, displayName: option.wxid } + }) + + setWxidOptions(enrichedWxids) + setShowSwitchAccountDialog(true) + } catch (error) { + console.error('扫描账号失败:', error) + window.alert('扫描账号失败,请稍后重试') + } + } + + const handleSwitchAccount = async (selectedWxid: string) => { + if (!selectedWxid || isSwitchingAccount) return + setIsSwitchingAccount(true) + try { + console.log('[切换账号] 开始切换到:', selectedWxid) + const currentWxid = userProfile.wxid + if (currentWxid === selectedWxid) { + console.log('[切换账号] 已经是当前账号,跳过') + setShowSwitchAccountDialog(false) + setIsSwitchingAccount(false) + return + } + + console.log('[切换账号] 设置新 wxid') + await configService.setMyWxid(selectedWxid) + + console.log('[切换账号] 获取账号配置') + const wxidConfig = await configService.getWxidConfig(selectedWxid) + console.log('[切换账号] 配置内容:', wxidConfig) + if (wxidConfig?.decryptKey) { + console.log('[切换账号] 设置 decryptKey') + await configService.setDecryptKey(wxidConfig.decryptKey) + } + if (typeof wxidConfig?.imageXorKey === 'number') { + console.log('[切换账号] 设置 imageXorKey:', wxidConfig.imageXorKey) + await configService.setImageXorKey(wxidConfig.imageXorKey) + } + if (wxidConfig?.imageAesKey) { + console.log('[切换账号] 设置 imageAesKey') + await configService.setImageAesKey(wxidConfig.imageAesKey) + } + + console.log('[切换账号] 检查数据库连接状态') + console.log('[切换账号] 数据库连接状态:', isDbConnected) + if (isDbConnected) { + console.log('[切换账号] 关闭数据库连接') + await window.electronAPI.chat.close() + } + + console.log('[切换账号] 清除缓存') + window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY) + clearAnalyticsStoreCache() + resetChatStore() + + console.log('[切换账号] 触发 wxid-changed 事件') + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: selectedWxid } })) + + console.log('[切换账号] 切换成功') + setShowSwitchAccountDialog(false) + } catch (error) { + console.error('[切换账号] 失败:', error) + window.alert('切换账号失败,请稍后重试') + } finally { + setIsSwitchingAccount(false) + } } const openSettingsFromAccountMenu = () => { @@ -291,167 +376,128 @@ function Sidebar({ collapsed }: SidebarProps) { }) } - const handleConfirmClearAccountData = async () => { - if (!canConfirmClear || isClearingAccountData) return - setIsClearingAccountData(true) - try { - const result = await window.electronAPI.chat.clearCurrentAccountData({ - clearCache: shouldClearCacheData, - clearExports: shouldClearExportData - }) - if (!result.success) { - window.alert(result.error || '清理失败,请稍后重试。') - return - } - window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY) - setUserProfile({ wxid: '', displayName: '未识别用户' }) - window.dispatchEvent(new Event('wxid-changed')) - - const removedPaths = Array.isArray(result.removedPaths) ? result.removedPaths : [] - const selectedScopes = [ - shouldClearCacheData ? '缓存数据' : '', - shouldClearExportData ? '导出数据' : '' - ].filter(Boolean) - const detailLines: string[] = [ - `清理范围:${selectedScopes.join('、') || '未选择'}`, - `已清理项目:${removedPaths.length} 项` - ] - if (removedPaths.length > 0) { - detailLines.push('', '清理明细(最多显示 8 项):') - for (const [index, path] of removedPaths.slice(0, 8).entries()) { - detailLines.push(`${index + 1}. ${path}`) - } - if (removedPaths.length > 8) { - detailLines.push(`... 其余 ${removedPaths.length - 8} 项已省略`) - } - } - if (result.warning) { - detailLines.push('', `注意:${result.warning}`) - } - const followupHint = shouldClearCacheData - ? '若需再次获取数据,请手动登录微信客户端并重新在 WeFlow 完成配置。' - : '你可以继续使用当前登录状态,无需重新登录。' - window.alert(`账号数据清理完成。\n\n${detailLines.join('\n')}\n\n为保障数据安全,WeFlow 已清除该账号本地缓存/导出相关数据。${followupHint}`) - resetClearDialogState() - if (shouldClearCacheData) { - window.location.reload() - } - } catch (error) { - console.error('清理账号数据失败:', error) - window.alert('清理失败,请稍后重试。') - } finally { - setIsClearingAccountData(false) - } + const isActive = (path: string) => { + return location.pathname === path || location.pathname.startsWith(`${path}/`) } + const exportTaskBadge = activeExportTaskCount > 99 ? '99+' : `${activeExportTaskCount}` return ( - - {showClearAccountDialog && ( -
!isClearingAccountData && resetClearDialogState()}> -
event.stopPropagation()}> -

清除此账号所有数据

-

- 操作后可将该账户在 weflow 下产生的所有缓存文件、导出文件等彻底清除。 - 清除后必须手动登录微信客户端 weflow 才能再次获取,保障你的数据安全。 -

-
- - + {showSwitchAccountDialog && ( +
!isSwitchingAccount && setShowSwitchAccountDialog(false)}> +
event.stopPropagation()}> +

切换账号

+

选择要切换的微信账号

+
+ {wxidOptions.map((option) => ( + + ))}
-
- - +
+
)} - + ) } diff --git a/src/pages/AnnualReportPage.tsx b/src/pages/AnnualReportPage.tsx index 018fbdb..88f77d0 100644 --- a/src/pages/AnnualReportPage.tsx +++ b/src/pages/AnnualReportPage.tsx @@ -209,16 +209,7 @@ function AnnualReportPage() { return (
-

正在加载年份数据(首批)...

-
-

加载方式:{getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut })}

-

状态:{loadStatusText || '正在加载年份数据...'}

-

- 原生耗时:{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '} - 扫表耗时:{formatLoadElapsed(scanElapsedMs)} |{' '} - 总耗时:{formatLoadElapsed(totalElapsedMs)} -

-
+

正在准备年度报告...

) } @@ -264,30 +255,6 @@ function AnnualReportPage() {

年度报告

选择年份,回顾你在微信里的点点滴滴

- {loadedYearCount > 0 && ( -

- {isYearStatusComplete ? ( - <>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(totalElapsedMs)} - ) : ( - <> - 已显示 {loadedYearCount} 个年份,正在补充更多年份 - (已耗时 {formatLoadElapsed(totalElapsedMs)}) - - )} -

- )} -
-

加载方式:{strategyLabel}

-

- 状态: - {loadStatusText || (isYearStatusComplete ? '全部年份已加载完毕' : '正在加载年份数据...')} -

-

- 原生耗时:{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '} - 扫表耗时:{formatLoadElapsed(scanElapsedMs)} |{' '} - 总耗时:{formatLoadElapsed(totalElapsedMs)} -

-
@@ -311,7 +278,6 @@ function AnnualReportPage() {
))}
- {renderYearLoadStatus()}
- {renderYearLoadStatus()}
- {isSessionListSyncing && ( -
- - 同步中 -
- )} {/* 折叠群 header */} diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index cc7e86d..35e5a1d 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -891,28 +891,6 @@ function ContactsPage() { -
- 共 {filteredContacts.length} / {contacts.length} 个联系人 - {contactsUpdatedAt && ( - - {contactsDataSource === 'cache' ? '缓存' : '最新'} · 更新于 {contactsUpdatedAtLabel} - - )} - {contacts.length > 0 && ( - - 头像缓存 {avatarCachedCount}/{contacts.length} - {avatarCacheUpdatedAtLabel ? ` · 更新于 ${avatarCacheUpdatedAtLabel}` : ''} - - )} - {isLoading && contacts.length > 0 && ( - 后台同步中... - )} - {avatarEnrichProgress.running && ( - - 头像补全中 {avatarEnrichProgress.loaded}/{avatarEnrichProgress.total} - - )} -
{exportMode && (
diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index fa710b6..d1608eb 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -11,6 +11,33 @@ padding: 28px 32px; background: rgba(15, 23, 42, 0.28); backdrop-filter: blur(10px); + animation: settingsFadeIn 0.2s ease; + + &.closing { + animation: settingsFadeOut 0.2s ease forwards; + } +} + +@keyframes settingsFadeIn { + from { + opacity: 0; + backdrop-filter: blur(0); + } + to { + opacity: 1; + backdrop-filter: blur(10px); + } +} + +@keyframes settingsFadeOut { + from { + opacity: 1; + backdrop-filter: blur(10px); + } + to { + opacity: 0; + backdrop-filter: blur(0); + } } .settings-page { @@ -25,6 +52,33 @@ border-radius: 24px; box-shadow: 0 28px 80px rgba(15, 23, 42, 0.22); overflow: hidden; + animation: settingsSlideUp 0.3s ease; + + &.closing { + animation: settingsSlideDown 0.2s ease forwards; + } +} + +@keyframes settingsSlideUp { + from { + opacity: 0; + transform: translateY(30px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes settingsSlideDown { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(20px) scale(0.98); + } } .settings-header { diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 9c5e664..82526a5 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -134,6 +134,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [isClearingAnalyticsCache, setIsClearingAnalyticsCache] = useState(false) const [isClearingImageCache, setIsClearingImageCache] = useState(false) const [isClearingAllCache, setIsClearingAllCache] = useState(false) + const [isClosing, setIsClosing] = useState(false) const saveTimersRef = useRef>>({}) // 安全设置 state @@ -203,7 +204,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { if (!onClose) return const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - onClose() + handleClose() } } document.addEventListener('keydown', handleKeyDown) @@ -445,6 +446,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setTimeout(() => setMessage(null), 3000) } + const handleClose = () => { + if (!onClose) return + setIsClosing(true) + setTimeout(() => { + onClose() + }, 200) + } + type WxidKeys = { decryptKey: string imageXorKey: number | null @@ -2076,8 +2085,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ) return ( -
onClose?.()}> -
event.stopPropagation()}> +
+
event.stopPropagation()}> {message &&
{message.text}
} {/* 多账号选择对话框 */} @@ -2116,7 +2125,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {isTesting ? '测试中...' : '测试连接'} {onClose && ( - )}