import { useEffect, useRef, useState } from 'react' import { Save, RotateCcw, Upload, Download } from 'lucide-react' import { Card, Button, FormItem, Input, Select, Switch, ThemeSwitcher, toast, Modal, Progress } from '../../shared/components' import { fetchSettings, saveSettings, resetSettings, initializeSystemData, exportSystemConfig, importSystemConfig } from './api' import type { AppSettings } from './types' import { defaultSettings } from './types' import { EventsOn, EventsOff } from '../../wailsjs/runtime/runtime' import { useBackupStore } from '../../store/backupStore' interface BackupExportProgress { phase: string progress: number message: string componentId?: string componentName?: string entryIndex?: number entryTotal?: number timestamp?: string } interface BackupExportLogItem { id: number phase: string time: string text: string } export function SettingsPage() { const [settings, setSettings] = useState(defaultSettings) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [hasChanges, setHasChanges] = useState(false) const [importModalOpen, setImportModalOpen] = useState(false) const [actionLoading, setActionLoading] = useState<'none' | 'init' | 'export' | 'import-reset' | 'import-merge'>('none') const [exportProgress, setExportProgress] = useState(null) const [importProgress, setImportProgress] = useState(null) const [exportLogs, setExportLogs] = useState([]) const exportLogsRef = useRef(null) const setImportState = useBackupStore((s) => s.setImportState) const clearImportState = useBackupStore((s) => s.clearImportState) useEffect(() => { loadSettings() }, []) useEffect(() => { const onExportProgress = (payload: BackupExportProgress) => { if (!payload || typeof payload !== 'object') { return } const phase = typeof payload.phase === 'string' ? payload.phase : 'writing' if (phase === 'cancelled') { setExportProgress(null) setExportLogs([]) return } const progress = Number.isFinite(payload.progress) ? Math.max(0, Math.min(100, Math.round(payload.progress))) : 0 const message = typeof payload.message === 'string' && payload.message.trim() ? payload.message.trim() : '正在导出...' const componentId = typeof payload.componentId === 'string' ? payload.componentId.trim() : '' const componentName = typeof payload.componentName === 'string' ? payload.componentName.trim() : '' const entryIndex = Number.isFinite(payload.entryIndex) ? Math.max(0, Math.round(payload.entryIndex || 0)) : 0 const entryTotal = Number.isFinite(payload.entryTotal) ? Math.max(0, Math.round(payload.entryTotal || 0)) : 0 const timestamp = typeof payload.timestamp === 'string' && payload.timestamp.trim() ? payload.timestamp.trim() : new Date().toLocaleTimeString('zh-CN', { hour12: false }) setExportProgress({ phase, progress, message, componentId: componentId || undefined, componentName: componentName || undefined, entryIndex: entryIndex || undefined, entryTotal: entryTotal || undefined, timestamp, }) const prefix = componentName ? `[${componentName}] ` : componentId ? `[${componentId}] ` : '' const text = `${prefix}${message}` setExportLogs(prev => { const last = prev[prev.length - 1] if (last && last.text === text && last.phase === phase) { return prev } const next = [...prev, { id: Date.now() + Math.floor(Math.random() * 1000), phase, time: timestamp, text }] return next.length > 120 ? next.slice(next.length - 120) : next }) } EventsOn('backup:export:progress', onExportProgress) return () => { EventsOff('backup:export:progress') } }, []) useEffect(() => { const onImportProgress = (payload: BackupExportProgress) => { if (!payload || typeof payload !== 'object') { return } const phase = typeof payload.phase === 'string' ? payload.phase : 'importing' if (phase === 'cancelled') { setImportProgress(null) return } const progress = Number.isFinite(payload.progress) ? Math.max(0, Math.min(100, Math.round(payload.progress))) : 0 const message = typeof payload.message === 'string' && payload.message.trim() ? payload.message.trim() : '正在加载配置...' const componentId = typeof payload.componentId === 'string' ? payload.componentId.trim() : '' const componentName = typeof payload.componentName === 'string' ? payload.componentName.trim() : '' const entryIndex = Number.isFinite(payload.entryIndex) ? Math.max(0, Math.round(payload.entryIndex || 0)) : 0 const entryTotal = Number.isFinite(payload.entryTotal) ? Math.max(0, Math.round(payload.entryTotal || 0)) : 0 const timestamp = typeof payload.timestamp === 'string' && payload.timestamp.trim() ? payload.timestamp.trim() : new Date().toLocaleTimeString('zh-CN', { hour12: false }) setImportProgress({ phase, progress, message, componentId: componentId || undefined, componentName: componentName || undefined, entryIndex: entryIndex || undefined, entryTotal: entryTotal || undefined, timestamp, }) } EventsOn('backup:import:progress', onImportProgress) return () => { EventsOff('backup:import:progress') } }, []) useEffect(() => { const isImporting = actionLoading === 'import-reset' || actionLoading === 'import-merge' if (isImporting) { setImportState({ inProgress: true, progress: importProgress?.progress ?? 0, message: importProgress?.message || '正在加载配置...', }) return } clearImportState() }, [actionLoading, importProgress?.progress, importProgress?.message, setImportState, clearImportState]) useEffect(() => { return () => { clearImportState() } }, [clearImportState]) useEffect(() => { if (!exportLogsRef.current) { return } exportLogsRef.current.scrollTop = exportLogsRef.current.scrollHeight }, [exportLogs]) const loadSettings = async () => { setLoading(true) try { const data = await fetchSettings() setSettings(data) } finally { setLoading(false) } } const handleChange = (key: K, value: AppSettings[K]) => { setSettings(prev => ({ ...prev, [key]: value })) setHasChanges(true) } const handleSave = async () => { setSaving(true) try { const success = await saveSettings(settings) if (success) { setHasChanges(false) toast.success('设置已保存') } } catch (error: any) { toast.error(error?.message || '保存失败,请检查配置') } finally { setSaving(false) } } const handleReset = async () => { if (confirm('确定要重置所有设置吗?')) { const data = await resetSettings() setSettings(data) setHasChanges(false) } } const handleInitializeSystem = async () => { if (!confirm('初始化会清空当前数据并恢复默认状态,是否继续?')) { return } setActionLoading('init') try { const res = await initializeSystemData() if (res.cancelled) { toast.info('已取消初始化') return } toast.success(res.message || '初始化完成') } catch (error: any) { toast.error(error?.message || '初始化失败') } finally { setActionLoading('none') } } const handleExportSystem = async () => { setActionLoading('export') setExportLogs([]) setExportProgress({ phase: 'starting', progress: 0, message: '准备导出...' }) try { const res = await exportSystemConfig() if (res.cancelled) { setExportProgress(null) setExportLogs([]) toast.info('已取消导出') return } setExportProgress(prev => prev?.phase === 'done' ? prev : { phase: 'done', progress: 100, message: res.message || '导出完成' }) toast.success(res.message || '导出完成') } catch (error: any) { setExportProgress(prev => ({ phase: 'error', progress: prev?.progress ?? 0, message: error?.message || '导出失败', })) setExportLogs(prev => { const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) const text = error?.message || '导出失败' const next = [...prev, { id: Date.now() + Math.floor(Math.random() * 1000), phase: 'error', time: timestamp, text }] return next.length > 120 ? next.slice(next.length - 120) : next }) toast.error(error?.message || '导出失败') } finally { setActionLoading('none') } } const handleImportSystem = async (resetFirst: boolean) => { setActionLoading(resetFirst ? 'import-reset' : 'import-merge') setImportProgress({ phase: 'starting', progress: 0, message: resetFirst ? '等待选择 ZIP 配置(先初始化后加载)...' : '等待选择 ZIP 配置(判重合并)...', }) try { const res = await importSystemConfig(resetFirst) if (res.cancelled) { setImportProgress(null) toast.info('已取消加载') return } const imported = res.imported ?? 0 const skipped = res.skipped ?? 0 const conflicts = res.conflicts ?? 0 const componentFailed = Number.isFinite(res.componentFailed) ? Math.max(0, Math.round(res.componentFailed || 0)) : 0 const componentTotal = Number.isFinite(res.componentTotal) ? Math.max(0, Math.round(res.componentTotal || 0)) : 0 const failedComponents = Array.isArray(res.failedComponents) ? res.failedComponents : [] if (res.partial || componentFailed > 0) { const moduleNames = failedComponents .map(item => (item?.componentName || item?.componentId || '').trim()) .filter(Boolean) const moduleHint = moduleNames.length > 0 ? `:${moduleNames.slice(0, 3).join('、')}${moduleNames.length > 3 ? ` 等 ${moduleNames.length} 个模块` : ''}` : '' if (componentTotal > 0) { const componentSuccess = Math.max(0, componentTotal - componentFailed) toast.warning(`加载完成(部分成功):模块成功 ${componentSuccess}/${componentTotal},异常 ${componentFailed}${moduleHint}`) } else { toast.warning(`加载完成(部分成功):异常模块 ${componentFailed}${moduleHint}`) } } else { toast.success(`加载完成:导入 ${imported},跳过 ${skipped},冲突 ${conflicts}`) } setImportModalOpen(false) setImportProgress(null) } catch (error: any) { setImportProgress(prev => ({ phase: 'error', progress: prev?.progress ?? 0, message: error?.message || '加载失败', })) toast.error(error?.message || '加载失败') } finally { setActionLoading('none') } } const importRunning = actionLoading === 'import-reset' || actionLoading === 'import-merge' if (loading) { return (
) } return (
{/* 页面标题 */}

系统设置

配置应用的各项参数

{/* 主题设置 */} {/* 基础设置 */}
handleChange('appName', e.target.value)} placeholder="请输入应用名称" /> handleChange('appDescription', e.target.value)} placeholder="请输入应用描述" />
{/* 功能设置 */}

启用通知

接收系统通知和提醒

handleChange('enableNotifications', v)} />

自动保存

自动保存编辑中的内容

handleChange('enableAutoSave', v)} />
{settings.enableAutoSave && (
handleChange('autoSaveInterval', parseInt(e.target.value) || 30)} min={5} max={300} className="max-w-[120px]" />
)}

启用缓存

缓存数据以提高性能

handleChange('cacheEnabled', v)} />
{/* 高级设置 */}
handleChange('maxUploadSize', parseInt(e.target.value) || 10)} min={1} max={100} /> handleChange('sessionTimeout', parseInt(e.target.value) || 30)} min={5} max={120} />