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, 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' import { UserRound } from 'lucide-react' import './Sidebar.scss' interface SidebarUserProfile { wxid: string displayName: string alias?: string avatarUrl?: string } 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 nickname?: string displayName?: string avatarUrl?: string } const readSidebarUserProfileCache = (): SidebarUserProfile | null => { try { const raw = window.localStorage.getItem(SIDEBAR_USER_PROFILE_CACHE_KEY) if (!raw) return null const parsed = JSON.parse(raw) as SidebarUserProfileCache if (!parsed || typeof parsed !== 'object') return null if (!parsed.wxid || !parsed.displayName) return null return { wxid: parsed.wxid, displayName: parsed.displayName, alias: parsed.alias, avatarUrl: parsed.avatarUrl } } catch { return null } } const writeSidebarUserProfileCache = (profile: SidebarUserProfile): void => { if (!profile.wxid || !profile.displayName) return try { const payload: SidebarUserProfileCache = { ...profile, 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 '' if (trimmed.toLowerCase().startsWith('wxid_')) { const match = trimmed.match(/^(wxid_[^_]+)/i) return match?.[1] || trimmed } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) return suffixMatch ? suffixMatch[1] : trimmed } interface SidebarProps { collapsed: boolean } function Sidebar({ collapsed }: SidebarProps) { const location = useLocation() const navigate = useNavigate() const [authEnabled, setAuthEnabled] = useState(false) const [activeExportTaskCount, setActiveExportTaskCount] = useState(0) const [userProfile, setUserProfile] = useState({ wxid: '', displayName: '未识别用户' }) const [isAccountMenuOpen, setIsAccountMenuOpen] = 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) }, []) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (!isAccountMenuOpen) return const target = event.target as Node | null if (accountCardWrapRef.current && target && !accountCardWrapRef.current.contains(target)) { setIsAccountMenuOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [isAccountMenuOpen]) useEffect(() => { const unsubscribe = onExportSessionStatus((payload) => { const countFromPayload = typeof payload?.activeTaskCount === 'number' ? payload.activeTaskCount : Array.isArray(payload?.inProgressSessionIds) ? payload.inProgressSessionIds.length : 0 const normalized = Math.max(0, Math.floor(countFromPayload)) setActiveExportTaskCount(normalized) }) requestExportSessionStatus() const timer = window.setTimeout(() => requestExportSessionStatus(), 120) return () => { unsubscribe() window.clearTimeout(timer) } }, []) useEffect(() => { const loadCurrentUser = async () => { const patchUserProfile = (patch: Partial, expectedWxid?: string) => { setUserProfile(prev => { if (expectedWxid && prev.wxid && prev.wxid !== expectedWxid) { return prev } const next: SidebarUserProfile = { ...prev, ...patch } if (!next.displayName) { next.displayName = next.wxid || '未识别用户' } writeSidebarUserProfileCache(next) return next }) } try { const wxid = await configService.getMyWxid() 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(), cleanedWxid.trim().toLowerCase() ].filter(Boolean)) const normalizeName = (value?: string | null): string | undefined => { if (!value) return undefined const trimmed = value.trim() if (!trimmed) return undefined const lowered = trimmed.toLowerCase() if (lowered === 'self') return undefined if (lowered.startsWith('wxid_')) return undefined if (wxidCandidates.has(lowered)) return undefined return trimmed } const pickFirstValidName = (...candidates: Array): string | undefined => { for (const candidate of candidates) { const normalized = normalizeName(candidate) if (normalized) return normalized } return undefined } // 并行获取名称和头像 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 || '未识别用户' patchUserProfile({ wxid: resolvedWxid, displayName, alias: myContact?.alias, avatarUrl: avatarResult.status === 'fulfilled' && avatarResult.value.success ? avatarResult.value.avatarUrl : undefined }) } catch (error) { console.error('加载侧边栏用户信息失败:', error) } } const cachedProfile = readSidebarUserProfileCache() if (cachedProfile) { setUserProfile(cachedProfile) } void loadCurrentUser() const onWxidChanged = () => { void loadCurrentUser() } window.addEventListener('wxid-changed', onWxidChanged as EventListener) return () => window.removeEventListener('wxid-changed', onWxidChanged as EventListener) }, []) const getAvatarLetter = (name: string): string => { if (!name) return '?' return [...name][0] || '?' } const openSwitchAccountDialog = async () => { setIsAccountMenuOpen(false) 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: WxidOption) => { const normalizedWxid = normalizeAccountId(option.wxid) const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid] let displayName = option.nickname || option.wxid let avatarUrl = option.avatarUrl if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) { displayName = userProfile.displayName || displayName avatarUrl = userProfile.avatarUrl || avatarUrl } else if (cached) { displayName = cached.displayName || displayName avatarUrl = cached.avatarUrl || avatarUrl } return { ...option, displayName, avatarUrl } }) 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 = () => { setIsAccountMenuOpen(false) navigate('/settings', { state: { backgroundLocation: location } }) } const isActive = (path: string) => { return location.pathname === path || location.pathname.startsWith(`${path}/`) } const exportTaskBadge = activeExportTaskCount > 99 ? '99+' : `${activeExportTaskCount}` return ( <> {showSwitchAccountDialog && (
!isSwitchingAccount && setShowSwitchAccountDialog(false)}>
event.stopPropagation()}>

切换账号

选择要切换的微信账号

{wxidOptions.map((option) => ( ))}
)} ) } export default Sidebar