mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
945 lines
36 KiB
TypeScript
945 lines
36 KiB
TypeScript
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' | '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: '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<SettingsTab>('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<WxidOption[]>([])
|
||
const [showWxidSelect, setShowWxidSelect] = useState(false)
|
||
const wxidDropdownRef = useRef<HTMLDivElement>(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 [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) => {
|
||
if (showWxidSelect && wxidDropdownRef.current && !wxidDropdownRef.current.contains(e.target as Node)) {
|
||
setShowWxidSelect(false)
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', handleClickOutside)
|
||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||
}, [showWxidSelect])
|
||
|
||
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()
|
||
|
||
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)
|
||
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.setOnboardingDone(true)
|
||
|
||
showMessage('配置保存成功,正在测试连接...', true)
|
||
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
|
||
|
||
if (result.success) {
|
||
setDbConnected(true, dbPath)
|
||
showMessage('配置保存成功!数据库连接正常', true)
|
||
} else {
|
||
showMessage(result.error || '数据库连接失败,请检查配置', false)
|
||
}
|
||
} 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)
|
||
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 = () => (
|
||
<div className="tab-content">
|
||
<div className="theme-mode-toggle">
|
||
<button className={`mode-btn ${themeMode === 'light' ? 'active' : ''}`} onClick={() => setThemeMode('light')}>
|
||
<Sun size={16} /> 浅色
|
||
</button>
|
||
<button className={`mode-btn ${themeMode === 'dark' ? 'active' : ''}`} onClick={() => setThemeMode('dark')}>
|
||
<Moon size={16} /> 深色
|
||
</button>
|
||
</div>
|
||
<div className="theme-grid">
|
||
{themes.map((theme) => (
|
||
<div key={theme.id} className={`theme-card ${currentTheme === theme.id ? 'active' : ''}`} onClick={() => setTheme(theme.id)}>
|
||
<div className="theme-preview" style={{ background: themeMode === 'dark' ? 'linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%)' : `linear-gradient(135deg, ${theme.bgColor} 0%, ${theme.bgColor}dd 100%)` }}>
|
||
<div className="theme-accent" style={{ background: theme.primaryColor }} />
|
||
</div>
|
||
<div className="theme-info">
|
||
<span className="theme-name">{theme.name}</span>
|
||
<span className="theme-desc">{theme.description}</span>
|
||
</div>
|
||
{currentTheme === theme.id && <div className="theme-check"><Check size={14} /></div>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
const renderDatabaseTab = () => (
|
||
<div className="tab-content">
|
||
<div className="form-group">
|
||
<label>解密密钥</label>
|
||
<span className="form-hint">64位十六进制密钥</span>
|
||
<div className="input-with-toggle">
|
||
<input type={showDecryptKey ? 'text' : 'password'} placeholder="例如: a1b2c3d4e5f6..." value={decryptKey} onChange={(e) => setDecryptKey(e.target.value)} />
|
||
<button type="button" className="toggle-visibility" onClick={() => setShowDecryptKey(!showDecryptKey)}>
|
||
{showDecryptKey ? <EyeOff size={14} /> : <Eye size={14} />}
|
||
</button>
|
||
</div>
|
||
{isManualStartPrompt ? (
|
||
<div className="manual-prompt">
|
||
<p className="prompt-text">未能自动启动微信,请手动启动并登录后点击下方确认</p>
|
||
<button className="btn btn-primary btn-sm" onClick={handleManualConfirm}>
|
||
我已启动微信,继续检测
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button className="btn btn-secondary btn-sm" onClick={handleAutoGetDbKey} disabled={isFetchingDbKey}>
|
||
<Plug size={14} /> {isFetchingDbKey ? '获取中...' : '自动获取密钥'}
|
||
</button>
|
||
)}
|
||
{dbKeyStatus && <div className="form-hint status-text">{dbKeyStatus}</div>}
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>数据库根目录</label>
|
||
<span className="form-hint">xwechat_files 目录</span>
|
||
<span className="form-hint" style={{ color: '#ff6b6b' }}>⚠️ 目录路径不可包含中文,如有中文请去微信-设置-存储位置点击更改,迁移至全英文目录</span>
|
||
<input type="text" placeholder="例如: C:\Users\xxx\Documents\xwechat_files" value={dbPath} onChange={(e) => setDbPath(e.target.value)} />
|
||
<div className="btn-row">
|
||
<button className="btn btn-primary" onClick={handleAutoDetectPath} disabled={isDetectingPath}>
|
||
<FolderSearch size={16} /> {isDetectingPath ? '检测中...' : '自动检测'}
|
||
</button>
|
||
<button className="btn btn-secondary" onClick={handleSelectDbPath}><FolderOpen size={16} /> 浏览选择</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>账号 wxid</label>
|
||
<span className="form-hint">微信账号标识</span>
|
||
<div className="wxid-input-wrapper" ref={wxidDropdownRef}>
|
||
<input
|
||
type="text"
|
||
placeholder="例如: wxid_xxxxxx"
|
||
value={wxid}
|
||
onChange={(e) => setWxid(e.target.value)}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className={`wxid-dropdown-btn ${showWxidSelect ? 'open' : ''}`}
|
||
onClick={() => wxidOptions.length > 0 ? setShowWxidSelect(!showWxidSelect) : handleScanWxid()}
|
||
title={wxidOptions.length > 0 ? "选择已检测到的账号" : "扫描账号"}
|
||
>
|
||
<ChevronDown size={16} />
|
||
</button>
|
||
{showWxidSelect && wxidOptions.length > 0 && (
|
||
<div className="wxid-dropdown">
|
||
{wxidOptions.map((opt) => (
|
||
<div
|
||
key={opt.wxid}
|
||
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
|
||
onClick={() => handleSelectWxid(opt.wxid)}
|
||
>
|
||
<span className="wxid-value">{opt.wxid}</span>
|
||
<span className="wxid-time">
|
||
{new Date(opt.modifiedTime).toLocaleDateString()}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> 扫描 wxid</button>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>图片 XOR 密钥 <span className="optional">(可选)</span></label>
|
||
<span className="form-hint">用于解密图片缓存</span>
|
||
<input type="text" placeholder="例如: 0xA4" value={imageXorKey} onChange={(e) => setImageXorKey(e.target.value)} />
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>图片 AES 密钥 <span className="optional">(可选)</span></label>
|
||
<span className="form-hint">16 位密钥</span>
|
||
<input type="text" placeholder="16 位 AES 密钥" value={imageAesKey} onChange={(e) => setImageAesKey(e.target.value)} />
|
||
<button className="btn btn-secondary btn-sm" onClick={handleAutoGetImageKey} disabled={isFetchingImageKey}>
|
||
<Plug size={14} /> {isFetchingImageKey ? '获取中...' : '自动获取图片密钥'}
|
||
</button>
|
||
{imageKeyStatus && <div className="form-hint status-text">{imageKeyStatus}</div>}
|
||
{isFetchingImageKey && <div className="form-hint status-text">正在扫描内存,请稍候...</div>}
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>调试日志</label>
|
||
<span className="form-hint">开启后写入 WCDB 调试日志,便于排查连接问题</span>
|
||
<div className="log-toggle-line">
|
||
<span className="log-status">{logEnabled ? '已开启' : '已关闭'}</span>
|
||
<label className="switch" htmlFor="log-enabled-toggle">
|
||
<input
|
||
id="log-enabled-toggle"
|
||
className="switch-input"
|
||
type="checkbox"
|
||
checked={logEnabled}
|
||
onChange={async (e) => {
|
||
const enabled = e.target.checked
|
||
setLogEnabled(enabled)
|
||
await configService.setLogEnabled(enabled)
|
||
showMessage(enabled ? '已开启日志' : '已关闭日志', true)
|
||
}}
|
||
/>
|
||
<span className="switch-slider" />
|
||
</label>
|
||
</div>
|
||
<div className="log-actions">
|
||
<button className="btn btn-secondary" onClick={handleOpenLog}>
|
||
<FolderOpen size={16} /> 打开日志文件
|
||
</button>
|
||
<button className="btn btn-secondary" onClick={handleCopyLog}>
|
||
<Copy size={16} /> 复制日志内容
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
const renderWhisperTab = () => (
|
||
<div className="tab-content">
|
||
<div className="form-group">
|
||
<label>自动语音转文字</label>
|
||
<span className="form-hint">语音解密后自动转写为文字(需下载模型)</span>
|
||
<div className="log-toggle-line">
|
||
<span className="log-status">{autoTranscribeVoice ? '已开启' : '已关闭'}</span>
|
||
<label className="switch" htmlFor="auto-transcribe-toggle">
|
||
<input
|
||
id="auto-transcribe-toggle"
|
||
className="switch-input"
|
||
type="checkbox"
|
||
checked={autoTranscribeVoice}
|
||
onChange={async (e) => {
|
||
const enabled = e.target.checked
|
||
setAutoTranscribeVoice(enabled)
|
||
await configService.setAutoTranscribeVoice(enabled)
|
||
showMessage(enabled ? '已开启自动转文字' : '已关闭自动转文字', true)
|
||
}}
|
||
/>
|
||
<span className="switch-slider" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div className="form-group whisper-section">
|
||
<label>语音识别模型 (SenseVoiceSmall)</label>
|
||
<span className="form-hint">基于 Sherpa-onnx,支持中文、英文、日文、韩文</span>
|
||
<span className="form-hint">模型下载目录</span>
|
||
<input
|
||
type="text"
|
||
placeholder="留空使用默认目录"
|
||
value={whisperModelDir}
|
||
onChange={(e) => setWhisperModelDir(e.target.value)}
|
||
onBlur={() => configService.setWhisperModelDir(whisperModelDir)}
|
||
/>
|
||
<div className="btn-row">
|
||
<button className="btn btn-secondary" onClick={handleSelectWhisperModelDir}><FolderOpen size={16} /> 选择目录</button>
|
||
<button className="btn btn-secondary" onClick={handleResetWhisperModelDir}><RotateCcw size={16} /> 默认目录</button>
|
||
</div>
|
||
<div className="whisper-status-line">
|
||
<span className={`status ${whisperModelStatus?.exists ? 'ok' : 'warn'}`}>
|
||
{whisperModelStatus?.exists ? '已下载 (240 MB)' : '未下载 (240 MB)'}
|
||
</span>
|
||
{whisperModelStatus?.modelPath && <span className="path">{whisperModelStatus.modelPath}</span>}
|
||
</div>
|
||
{isWhisperDownloading ? (
|
||
<div className="whisper-progress">
|
||
<div className="progress-bar">
|
||
<div className="progress-fill" style={{ width: `${whisperDownloadProgress}%` }} />
|
||
</div>
|
||
<span>{whisperDownloadProgress.toFixed(0)}%</span>
|
||
</div>
|
||
) : (
|
||
<button className="btn btn-primary" onClick={handleDownloadWhisperModel}>
|
||
<Download size={16} /> 下载模型
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
const renderCacheTab = () => (
|
||
<div className="tab-content">
|
||
<p className="section-desc">管理应用缓存数据</p>
|
||
<div className="form-group">
|
||
<label>缓存目录 <span className="optional">(可选)</span></label>
|
||
<span className="form-hint">留空使用默认目录</span>
|
||
<input type="text" placeholder="留空使用默认目录" value={cachePath} onChange={(e) => setCachePath(e.target.value)} />
|
||
<div className="btn-row">
|
||
<button className="btn btn-secondary" onClick={handleSelectCachePath}><FolderOpen size={16} /> 浏览选择</button>
|
||
<button className="btn btn-secondary" onClick={() => setCachePath('')}><RotateCcw size={16} /> 恢复默认</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="btn-row">
|
||
<button className="btn btn-secondary" onClick={handleClearAnalyticsCache} disabled={isClearingCache}>
|
||
<Trash2 size={16} /> 清除分析缓存
|
||
</button>
|
||
<button className="btn btn-secondary" onClick={handleClearImageCache} disabled={isClearingCache}>
|
||
<Trash2 size={16} /> 清除图片缓存
|
||
</button>
|
||
<button className="btn btn-danger" onClick={handleClearAllCache} disabled={isClearingCache}>
|
||
<Trash2 size={16} /> 清除所有缓存</button>
|
||
</div>
|
||
<div className="divider" />
|
||
<p className="section-desc">清除当前配置并重新开始首次引导</p>
|
||
<div className="btn-row">
|
||
<button className="btn btn-danger" onClick={handleClearConfig}>
|
||
<RefreshCw size={16} /> 清除当前配置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
const renderAboutTab = () => (
|
||
<div className="tab-content about-tab">
|
||
<div className="about-card">
|
||
<div className="about-logo">
|
||
<img src="./logo.png" alt="WeFlow" />
|
||
</div>
|
||
<h2 className="about-name">WeFlow</h2>
|
||
<p className="about-slogan">WeFlow</p>
|
||
<p className="about-version">v{appVersion || '...'}</p>
|
||
|
||
<div className="about-update">
|
||
{updateInfo?.hasUpdate ? (
|
||
<>
|
||
<p className="update-hint">新版 v{updateInfo.version} 可用</p>
|
||
{isDownloading ? (
|
||
<div className="download-progress">
|
||
<div className="progress-bar">
|
||
<div className="progress-fill" style={{ width: `${downloadProgress}%` }} />
|
||
</div>
|
||
<span>{downloadProgress.toFixed(0)}%</span>
|
||
</div>
|
||
) : (
|
||
<button className="btn btn-primary" onClick={handleUpdateNow}>
|
||
<Download size={16} /> 立即更新
|
||
</button>
|
||
)}
|
||
</>
|
||
) : (
|
||
<button className="btn btn-secondary" onClick={handleCheckUpdate} disabled={isCheckingUpdate}>
|
||
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
|
||
{isCheckingUpdate ? '检查中...' : '检查更新'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="about-footer">
|
||
<p className="about-desc">微信聊天记录分析工具</p>
|
||
<div className="about-links">
|
||
<a href="#" onClick={(e) => { e.preventDefault(); window.electronAPI.shell.openExternal('https://github.com/hicccc77/WeFlow') }}>官网</a>
|
||
<span>·</span>
|
||
<a href="#" onClick={(e) => { e.preventDefault(); window.electronAPI.shell.openExternal('https://chatlab.fun') }}>ChatLab</a>
|
||
<span>·</span>
|
||
<a href="#" onClick={(e) => { e.preventDefault(); window.electronAPI.window.openAgreementWindow() }}>用户协议</a>
|
||
</div>
|
||
<p className="copyright">© 2025 WeFlow. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
return (
|
||
<div className="settings-page">
|
||
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
|
||
|
||
{/* 多账号选择对话框 */}
|
||
{showWxidSelect && wxidOptions.length > 1 && (
|
||
<div className="wxid-dialog-overlay" onClick={() => setShowWxidSelect(false)}>
|
||
<div className="wxid-dialog" onClick={(e) => e.stopPropagation()}>
|
||
<div className="wxid-dialog-header">
|
||
<h3>检测到多个微信账号</h3>
|
||
<p>请选择要使用的账号</p>
|
||
</div>
|
||
<div className="wxid-dialog-list">
|
||
{wxidOptions.map((opt) => (
|
||
<div
|
||
key={opt.wxid}
|
||
className={`wxid-dialog-item ${opt.wxid === wxid ? 'active' : ''}`}
|
||
onClick={() => handleSelectWxid(opt.wxid)}
|
||
>
|
||
<span className="wxid-id">{opt.wxid}</span>
|
||
<span className="wxid-date">最后修改 {new Date(opt.modifiedTime).toLocaleString()}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="wxid-dialog-footer">
|
||
<button className="btn btn-secondary" onClick={() => setShowWxidSelect(false)}>取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="settings-header">
|
||
<h1>设置</h1>
|
||
<div className="settings-actions">
|
||
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={isLoading || isTesting}>
|
||
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
|
||
</button>
|
||
<button className="btn btn-primary" onClick={handleSaveConfig} disabled={isLoading}>
|
||
<Save size={16} /> {isLoading ? '保存中...' : '保存配置'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="settings-tabs">
|
||
{tabs.map(tab => (
|
||
<button key={tab.id} className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`} onClick={() => setActiveTab(tab.id)}>
|
||
<tab.icon size={16} />
|
||
<span>{tab.label}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="settings-body">
|
||
{activeTab === 'appearance' && renderAppearanceTab()}
|
||
{activeTab === 'database' && renderDatabaseTab()}
|
||
{activeTab === 'whisper' && renderWhisperTab()}
|
||
{activeTab === 'cache' && renderCacheTab()}
|
||
{activeTab === 'about' && renderAboutTab()}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default SettingsPage
|
||
|
||
|