import { useState, useEffect, useRef } from 'react' import { useAppStore } from '../stores/appStore' import { useThemeStore, themes } from '../stores/themeStore' import { useAnalyticsStore } from '../stores/analyticsStore' import { dialog } from '../services/ipc' import * as configService from '../services/config' import { Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy, RotateCcw, Trash2, Save, Plug, Check, Sun, Moon, Palette, Database, Download, HardDrive, Info, RefreshCw, ChevronDown, Mic } from 'lucide-react' import './SettingsPage.scss' type SettingsTab = 'appearance' | 'database' | 'whisper' | 'export' | 'cache' | 'about' const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, { id: 'database', label: '数据库连接', icon: Database }, { id: 'whisper', label: '语音识别模型', icon: Mic }, { id: 'export', label: '导出', icon: Download }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'about', label: '关于', icon: Info } ] interface WxidOption { wxid: string modifiedTime: number } function SettingsPage() { const { setDbConnected, setLoading, reset } = useAppStore() const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore() const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache) const [activeTab, setActiveTab] = useState('appearance') const [decryptKey, setDecryptKey] = useState('') const [imageXorKey, setImageXorKey] = useState('') const [imageAesKey, setImageAesKey] = useState('') const [dbPath, setDbPath] = useState('') const [wxid, setWxid] = useState('') const [wxidOptions, setWxidOptions] = useState([]) const [showWxidSelect, setShowWxidSelect] = useState(false) const wxidDropdownRef = useRef(null) const [showExportFormatSelect, setShowExportFormatSelect] = useState(false) const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false) const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false) const exportFormatDropdownRef = useRef(null) const exportDateRangeDropdownRef = useRef(null) const exportExcelColumnsDropdownRef = useRef(null) const [cachePath, setCachePath] = useState('') const [logEnabled, setLogEnabled] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') const [whisperModelDir, setWhisperModelDir] = useState('') const [isWhisperDownloading, setIsWhisperDownloading] = useState(false) const [whisperDownloadProgress, setWhisperDownloadProgress] = useState(0) const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null) const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false) const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) const [exportDefaultFormat, setExportDefaultFormat] = useState('excel') const [exportDefaultDateRange, setExportDefaultDateRange] = useState('today') const [exportDefaultMedia, setExportDefaultMedia] = useState(false) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) const [isDetectingPath, setIsDetectingPath] = useState(false) const [isFetchingDbKey, setIsFetchingDbKey] = useState(false) const [isFetchingImageKey, setIsFetchingImageKey] = useState(false) const [isCheckingUpdate, setIsCheckingUpdate] = useState(false) const [isDownloading, setIsDownloading] = useState(false) const [downloadProgress, setDownloadProgress] = useState(0) const [appVersion, setAppVersion] = useState('') const [updateInfo, setUpdateInfo] = useState<{ hasUpdate: boolean; version?: string; releaseNotes?: string } | null>(null) const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null) const [showDecryptKey, setShowDecryptKey] = useState(false) const [dbKeyStatus, setDbKeyStatus] = useState('') const [imageKeyStatus, setImageKeyStatus] = useState('') const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) const [isClearingAnalyticsCache, setIsClearingAnalyticsCache] = useState(false) const [isClearingImageCache, setIsClearingImageCache] = useState(false) const [isClearingAllCache, setIsClearingAllCache] = useState(false) const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache useEffect(() => { loadConfig() loadAppVersion() }, []) // 点击外部关闭下拉框 useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node if (showWxidSelect && wxidDropdownRef.current && !wxidDropdownRef.current.contains(target)) { setShowWxidSelect(false) } if (showExportFormatSelect && exportFormatDropdownRef.current && !exportFormatDropdownRef.current.contains(target)) { setShowExportFormatSelect(false) } if (showExportDateRangeSelect && exportDateRangeDropdownRef.current && !exportDateRangeDropdownRef.current.contains(target)) { setShowExportDateRangeSelect(false) } if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) { setShowExportExcelColumnsSelect(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [showWxidSelect, showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect]) useEffect(() => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => { setDbKeyStatus(payload.message) }) const removeImage = window.electronAPI.key.onImageKeyStatus((payload) => { setImageKeyStatus(payload.message) }) return () => { removeDb?.() removeImage?.() } }, []) const loadConfig = async () => { try { const savedKey = await configService.getDecryptKey() const savedPath = await configService.getDbPath() const savedWxid = await configService.getMyWxid() const savedCachePath = await configService.getCachePath() const savedExportPath = await configService.getExportPath() const savedLogEnabled = await configService.getLogEnabled() const savedImageXorKey = await configService.getImageXorKey() const savedImageAesKey = await configService.getImageAesKey() const savedWhisperModelName = await configService.getWhisperModelName() const savedWhisperModelDir = await configService.getWhisperModelDir() const savedAutoTranscribe = await configService.getAutoTranscribeVoice() const savedTranscribeLanguages = await configService.getTranscribeLanguages() const savedExportDefaultFormat = await configService.getExportDefaultFormat() const savedExportDefaultDateRange = await configService.getExportDefaultDateRange() const savedExportDefaultMedia = await configService.getExportDefaultMedia() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() if (savedKey) setDecryptKey(savedKey) if (savedPath) setDbPath(savedPath) if (savedWxid) setWxid(savedWxid) if (savedCachePath) setCachePath(savedCachePath) if (savedImageXorKey != null) { setImageXorKey(`0x${savedImageXorKey.toString(16).toUpperCase().padStart(2, '0')}`) } if (savedImageAesKey) setImageAesKey(savedImageAesKey) setLogEnabled(savedLogEnabled) setAutoTranscribeVoice(savedAutoTranscribe) setTranscribeLanguages(savedTranscribeLanguages) setExportDefaultFormat(savedExportDefaultFormat || 'excel') setExportDefaultDateRange(savedExportDefaultDateRange || 'today') setExportDefaultMedia(savedExportDefaultMedia ?? false) setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { const defaultLanguages = ['zh'] setTranscribeLanguages(defaultLanguages) await configService.setTranscribeLanguages(defaultLanguages) } if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir) } catch (e) { console.error('加载配置失败:', e) } } const refreshWhisperStatus = async (modelDirValue = whisperModelDir) => { try { const result = await window.electronAPI.whisper?.getModelStatus() if (result?.success) { setWhisperModelStatus({ exists: Boolean(result.exists), modelPath: result.modelPath, tokensPath: result.tokensPath }) } } catch { setWhisperModelStatus(null) } } const loadAppVersion = async () => { try { const version = await window.electronAPI.app.getVersion() setAppVersion(version) } catch (e) { console.error('获取版本号失败:', e) } } // 监听下载进度 useEffect(() => { const removeListener = window.electronAPI.app.onDownloadProgress?.((progress: number) => { setDownloadProgress(progress) }) return () => removeListener?.() }, []) useEffect(() => { const removeListener = window.electronAPI.whisper?.onDownloadProgress?.((payload) => { if (typeof payload.percent === 'number') { setWhisperDownloadProgress(payload.percent) } }) return () => removeListener?.() }, []) useEffect(() => { void refreshWhisperStatus(whisperModelDir) }, [whisperModelDir]) const handleCheckUpdate = async () => { setIsCheckingUpdate(true) setUpdateInfo(null) try { const result = await window.electronAPI.app.checkForUpdates() if (result.hasUpdate) { setUpdateInfo(result) showMessage(`发现新版:${result.version}`, true) } else { showMessage('当前已是最新版', true) } } catch (e) { showMessage(`检查更新失败: ${e}`, false) } finally { setIsCheckingUpdate(false) } } const handleUpdateNow = async () => { setIsDownloading(true) setDownloadProgress(0) try { showMessage('正在下载更新...', true) await window.electronAPI.app.downloadAndInstall() } catch (e) { showMessage(`更新失败: ${e}`, false) setIsDownloading(false) } } const showMessage = (text: string, success: boolean) => { setMessage({ text, success }) setTimeout(() => setMessage(null), 3000) } const handleAutoDetectPath = async () => { if (isDetectingPath) return setIsDetectingPath(true) try { const result = await window.electronAPI.dbPath.autoDetect() if (result.success && result.path) { setDbPath(result.path) await configService.setDbPath(result.path) showMessage(`自动检测成功:${result.path}`, true) const wxids = await window.electronAPI.dbPath.scanWxids(result.path) setWxidOptions(wxids) if (wxids.length === 1) { setWxid(wxids[0].wxid) await configService.setMyWxid(wxids[0].wxid) showMessage(`已检测到账号:${wxids[0].wxid}`, true) } else if (wxids.length > 1) { // 多账号时弹出选择对话框 setShowWxidSelect(true) } } else { showMessage(result.error || '未能自动检测到数据库目录', false) } } catch (e) { showMessage(`自动检测失败: ${e}`, false) } finally { setIsDetectingPath(false) } } const handleSelectDbPath = async () => { try { const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { setDbPath(result.filePaths[0]) showMessage('已选择数据库目录', true) } } catch (e) { showMessage('选择目录失败', false) } } const handleScanWxid = async (silent = false) => { if (!dbPath) { if (!silent) showMessage('请先选择数据库目录', false) return } try { const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) setWxidOptions(wxids) if (wxids.length === 1) { setWxid(wxids[0].wxid) await configService.setMyWxid(wxids[0].wxid) if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true) } else if (wxids.length > 1) { // 多账号时弹出选择对话框 setShowWxidSelect(true) } else { if (!silent) showMessage('未检测到账号目录,请检查路径', false) } } catch (e) { if (!silent) showMessage(`扫描失败: ${e}`, false) } } const handleSelectWxid = async (selectedWxid: string) => { setWxid(selectedWxid) await configService.setMyWxid(selectedWxid) setShowWxidSelect(false) showMessage(`已选择账号:${selectedWxid}`, true) } const handleSelectCachePath = async () => { try { const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { setCachePath(result.filePaths[0]) showMessage('已选择缓存目录', true) } } catch (e) { showMessage('选择目录失败', false) } } const handleSelectWhisperModelDir = async () => { try { const result = await dialog.openFile({ title: '选择 Whisper 模型下载目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { const dir = result.filePaths[0] setWhisperModelDir(dir) await configService.setWhisperModelDir(dir) showMessage('已选择 Whisper 模型目录', true) } } catch (e) { showMessage('选择目录失败', false) } } const handleWhisperModelChange = async (value: string) => { setWhisperModelName(value) setWhisperDownloadProgress(0) await configService.setWhisperModelName(value) } const handleDownloadWhisperModel = async () => { if (isWhisperDownloading) return setIsWhisperDownloading(true) setWhisperDownloadProgress(0) try { const result = await window.electronAPI.whisper.downloadModel() if (result.success) { setWhisperDownloadProgress(100) showMessage('SenseVoiceSmall 模型下载完成', true) await refreshWhisperStatus(whisperModelDir) } else { showMessage(result.error || '模型下载失败', false) } } catch (e) { showMessage(`模型下载失败: ${e}`, false) } finally { setIsWhisperDownloading(false) } } const handleResetWhisperModelDir = async () => { setWhisperModelDir('') await configService.setWhisperModelDir('') } const handleAutoGetDbKey = async () => { if (isFetchingDbKey) return setIsFetchingDbKey(true) setIsManualStartPrompt(false) setDbKeyStatus('正在连接微信进程...') try { const result = await window.electronAPI.key.autoGetDbKey() if (result.success && result.key) { setDecryptKey(result.key) setDbKeyStatus('密钥获取成功') showMessage('已自动获取解密密钥', true) await handleScanWxid(true) } else { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { setIsManualStartPrompt(true) setDbKeyStatus('需要手动启动微信') } else { showMessage(result.error || '自动获取密钥失败', false) } } } catch (e) { showMessage(`自动获取密钥失败: ${e}`, false) } finally { setIsFetchingDbKey(false) } } const handleManualConfirm = async () => { setIsManualStartPrompt(false) handleAutoGetDbKey() } const handleAutoGetImageKey = async () => { if (isFetchingImageKey) return if (!dbPath) { showMessage('请先选择数据库目录', false) return } setIsFetchingImageKey(true) setImageKeyStatus('正在准备获取图片密钥...') try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath const result = await window.electronAPI.key.autoGetImageKey(accountPath) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') { setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) } setImageAesKey(result.aesKey) setImageKeyStatus('已获取图片密钥') showMessage('已自动获取图片密钥', true) } else { showMessage(result.error || '自动获取图片密钥失败', false) } } catch (e) { showMessage(`自动获取图片密钥失败: ${e}`, false) } finally { setIsFetchingImageKey(false) } } const handleTestConnection = async () => { if (!dbPath) { showMessage('请先选择数据库目录', false); return } if (!decryptKey) { showMessage('请先输入解密密钥', false); return } if (decryptKey.length !== 64) { showMessage('密钥长度必须为64个字符', false); return } if (!wxid) { showMessage('请先输入或扫描 wxid', false); return } setIsTesting(true) try { const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid) if (result.success) { showMessage('连接测试成功!数据库可正常访问', true) } else { showMessage(result.error || '连接测试失败', false) } } catch (e) { showMessage(`连接测试失败: ${e}`, false) } finally { setIsTesting(false) } } const handleSaveConfig = async () => { if (!decryptKey) { showMessage('请输入解密密钥', false); return } if (decryptKey.length !== 64) { showMessage('密钥长度必须为64个字符', false); return } if (!dbPath) { showMessage('请选择数据库目录', false); return } if (!wxid) { showMessage('请输入 wxid', false); return } setIsLoadingState(true) setLoading(true, '正在保存配置...') try { await configService.setDecryptKey(decryptKey) await configService.setDbPath(dbPath) await configService.setMyWxid(wxid) await configService.setCachePath(cachePath) if (imageXorKey) { const parsed = parseInt(imageXorKey.replace(/^0x/i, ''), 16) if (!Number.isNaN(parsed)) { await configService.setImageXorKey(parsed) } } else { await configService.setImageXorKey(0) } if (imageAesKey) { await configService.setImageAesKey(imageAesKey) } else { await configService.setImageAesKey('') } await configService.setWhisperModelDir(whisperModelDir) await configService.setAutoTranscribeVoice(autoTranscribeVoice) await configService.setTranscribeLanguages(transcribeLanguages) await configService.setOnboardingDone(true) // 保存按钮只负责持久化配置,不做连接测试/重连,避免影响聊天页的活动连接 showMessage('配置保存成功', true) } catch (e) { showMessage(`保存配置失败: ${e}`, false) } finally { setIsLoadingState(false) setLoading(false) } } const handleClearConfig = async () => { const confirmed = window.confirm('确定要清除当前配置吗?清除后需要重新完成首次配置?') if (!confirmed) return setIsLoadingState(true) setLoading(true, '正在清除配置...') try { await window.electronAPI.wcdb.close() await configService.clearConfig() reset() setDecryptKey('') setImageXorKey('') setImageAesKey('') setDbPath('') setWxid('') setCachePath('') setLogEnabled(false) setAutoTranscribeVoice(false) setTranscribeLanguages(['zh']) setWhisperModelDir('') setWhisperModelStatus(null) setWhisperDownloadProgress(0) setIsWhisperDownloading(false) setDbConnected(false) await window.electronAPI.window.openOnboardingWindow() } catch (e) { showMessage(`清除配置失败: ${e}`, false) } finally { setIsLoadingState(false) setLoading(false) } } const handleOpenLog = async () => { try { const logPath = await window.electronAPI.log.getPath() await window.electronAPI.shell.openPath(logPath) } catch (e) { showMessage(`打开日志失败: ${e}`, false) } } const handleCopyLog = async () => { try { const result = await window.electronAPI.log.read() if (!result.success) { showMessage(result.error || '读取日志失败', false) return } await navigator.clipboard.writeText(result.content || '') showMessage('日志已复制到剪贴板', true) } catch (e) { showMessage(`复制日志失败: ${e}`, false) } } const handleClearAnalyticsCache = async () => { if (isClearingCache) return setIsClearingAnalyticsCache(true) try { const result = await window.electronAPI.cache.clearAnalytics() if (result.success) { clearAnalyticsStoreCache() showMessage('已清除分析缓存', true) } else { showMessage(`清除分析缓存失败: ${result.error || '未知错误'}`, false) } } catch (e) { showMessage(`清除分析缓存失败: ${e}`, false) } finally { setIsClearingAnalyticsCache(false) } } const handleClearImageCache = async () => { if (isClearingCache) return setIsClearingImageCache(true) try { const result = await window.electronAPI.cache.clearImages() if (result.success) { showMessage('已清除图片缓存', true) } else { showMessage(`清除图片缓存失败: ${result.error || '未知错误'}`, false) } } catch (e) { showMessage(`清除图片缓存失败: ${e}`, false) } finally { setIsClearingImageCache(false) } } const handleClearAllCache = async () => { if (isClearingCache) return setIsClearingAllCache(true) try { const result = await window.electronAPI.cache.clearAll() if (result.success) { clearAnalyticsStoreCache() showMessage('已清除所有缓存', true) } else { showMessage(`清除所有缓存失败: ${result.error || '未知错误'}`, false) } } catch (e) { showMessage(`清除所有缓存失败: ${e}`, false) } finally { setIsClearingAllCache(false) } } const renderAppearanceTab = () => (
{themes.map((theme) => (
setTheme(theme.id)}>
{theme.name} {theme.description}
{currentTheme === theme.id &&
}
))}
) const renderDatabaseTab = () => (
64位十六进制密钥
setDecryptKey(e.target.value)} />
{isManualStartPrompt ? (

未能自动启动微信,请手动启动并登录后点击下方确认

) : ( )} {dbKeyStatus &&
{dbKeyStatus}
}
xwechat_files 目录 ⚠️ 目录路径不可包含中文,如有中文请去微信-设置-存储位置点击更改,迁移至全英文目录 setDbPath(e.target.value)} />
微信账号标识
setWxid(e.target.value)} /> {showWxidSelect && wxidOptions.length > 0 && (
{wxidOptions.map((opt) => (
handleSelectWxid(opt.wxid)} > {opt.wxid} {new Date(opt.modifiedTime).toLocaleDateString()}
))}
)}
用于解密图片缓存 setImageXorKey(e.target.value)} />
16 位密钥 setImageAesKey(e.target.value)} /> {imageKeyStatus &&
{imageKeyStatus}
} {isFetchingImageKey &&
正在扫描内存,请稍候...
}
开启后写入 WCDB 调试日志,便于排查连接问题
{logEnabled ? '已开启' : '已关闭'}
) const renderWhisperTab = () => (
语音解密后自动转写为文字(需下载模型)
{autoTranscribeVoice ? '已开启' : '已关闭'}
选择需要识别的语言(至少选择一种)
{[ { code: 'zh', name: '中文' }, { code: 'yue', name: '粤语' }, { code: 'en', name: '英文' }, { code: 'ja', name: '日文' }, { code: 'ko', name: '韩文' } ].map((lang) => ( ))}
基于 Sherpa-onnx,支持中、粤、英、日、韩及情感/事件识别 模型下载目录 setWhisperModelDir(e.target.value)} onBlur={() => configService.setWhisperModelDir(whisperModelDir)} />
{whisperModelStatus?.exists ? '已下载 (240 MB)' : '未下载 (240 MB)'} {whisperModelStatus?.modelPath && {whisperModelStatus.modelPath}}
{isWhisperDownloading ? (
正在准备模型文件... {whisperDownloadProgress.toFixed(0)}%
) : ( )}
) const exportFormatOptions = [ { value: 'excel', label: 'Excel', desc: '电子表格,适合统计分析' }, { value: 'chatlab', label: 'ChatLab', desc: '标准格式,支持其他软件导入' }, { value: 'chatlab-jsonl', label: 'ChatLab JSONL', desc: '流式格式,适合大量消息' }, { value: 'json', label: 'JSON', desc: '详细格式,包含完整消息信息' }, { value: 'html', label: 'HTML', desc: '网页格式,可直接浏览' }, { value: 'txt', label: 'TXT', desc: '纯文本,通用格式' }, { value: 'sql', label: 'PostgreSQL', desc: '数据库脚本,便于导入到数据库' } ] const exportDateRangeOptions = [ { value: 'today', label: '今天' }, { value: '7d', label: '最近7天' }, { value: '30d', label: '最近30天' }, { value: '90d', label: '最近90天' }, { value: 'all', label: '全部时间' } ] const exportExcelColumnOptions = [ { value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' }, { value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' } ] const getOptionLabel = (options: { value: string; label: string }[], value: string) => { return options.find((option) => option.value === value)?.label ?? value } const renderExportTab = () => { const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full' const exportFormatLabel = getOptionLabel(exportFormatOptions, exportDefaultFormat) const exportDateRangeLabel = getOptionLabel(exportDateRangeOptions, exportDefaultDateRange) const exportExcelColumnsLabel = getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue) return (
导出页面默认选中的格式
{showExportFormatSelect && (
{exportFormatOptions.map((option) => ( ))}
)}
控制导出页面的默认时间选择
{showExportDateRangeSelect && (
{exportDateRangeOptions.map((option) => ( ))}
)}
控制图片/语音/表情的默认导出开关
{exportDefaultMedia ? '已开启' : '已关闭'}
导出时默认将语音转写为文字
{exportDefaultVoiceAsText ? '已开启' : '已关闭'}
控制 Excel 导出的列字段
{showExportExcelColumnsSelect && (
{exportExcelColumnOptions.map((option) => ( ))}
)}
) } const renderCacheTab = () => (

管理应用缓存数据

留空使用默认目录 setCachePath(e.target.value)} />

清除当前配置并重新开始首次引导

) const renderAboutTab = () => (
WeFlow

WeFlow

WeFlow

v{appVersion || '...'}

{updateInfo?.hasUpdate ? ( <>

新版 v{updateInfo.version} 可用

{isDownloading ? (
{downloadProgress.toFixed(0)}%
) : ( )} ) : ( )}
) return (
{message &&
{message.text}
} {/* 多账号选择对话框 */} {showWxidSelect && wxidOptions.length > 1 && (
setShowWxidSelect(false)}>
e.stopPropagation()}>

检测到多个微信账号

请选择要使用的账号

{wxidOptions.map((opt) => (
handleSelectWxid(opt.wxid)} > {opt.wxid} 最后修改 {new Date(opt.modifiedTime).toLocaleString()}
))}
)}

设置

{tabs.map(tab => ( ))}
{activeTab === 'appearance' && renderAppearanceTab()} {activeTab === 'database' && renderDatabaseTab()} {activeTab === 'whisper' && renderWhisperTab()} {activeTab === 'export' && renderExportTab()} {activeTab === 'cache' && renderCacheTab()} {activeTab === 'about' && renderAboutTab()}
) } export default SettingsPage