7d003ff822
Extracted cron utilities to utils/schedule.js, all state management (15+ useState) to useSettingsForm hook, and 5 config sections + 2 dialogs + 1 status badge into focused components. SettingsPage reduced from ~700 lines to ~130 line container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
5.9 KiB
React
128 lines
5.9 KiB
React
// frontend/src/pages/SettingsPage.jsx
|
|
import { useRef } from 'react';
|
|
import {
|
|
CheckCircle2, Download, RefreshCw, Upload, XCircle
|
|
} from 'lucide-react';
|
|
import useSettingsForm from '../hooks/useSettingsForm';
|
|
import CorePathsSection from '../components/settings/CorePathsSection';
|
|
import AdvancedStrategySection from '../components/settings/AdvancedStrategySection';
|
|
import ScheduleSection from '../components/settings/ScheduleSection';
|
|
import NotificationSection from '../components/settings/NotificationSection';
|
|
import MetadataServicesSection from '../components/settings/MetadataServicesSection';
|
|
import ConfigExportDialog from '../components/settings/ConfigExportDialog';
|
|
import ConfigImportDialog from '../components/settings/ConfigImportDialog';
|
|
|
|
export default function SettingsPage({ config, setConfig, setTaskState }) {
|
|
const {
|
|
localConfig, netStatus, toast, isSaving,
|
|
isExportDialogOpen, isExporting, exportPassword, exportPasswordConfirm,
|
|
importDialog, fileInputRef,
|
|
updateField,
|
|
handleSave,
|
|
setExportPassword, setExportPasswordConfirm,
|
|
handleOpenExportDialog, handleCloseExportDialog, handleExportConfig,
|
|
handleImportButtonClick, handleImportFileChange,
|
|
handleCloseImportDialog, handleImportDecrypt, handleImportConfirm
|
|
} = useSettingsForm({ config, setConfig, setTaskState });
|
|
|
|
const updateNested = (section, value) => {
|
|
// Create a new localConfig with the nested section updated
|
|
// useSettingsForm doesn't have updateNestedField exposed directly,
|
|
// so we use updateField for top-level fields, and for nested
|
|
// sections we pass the merged object directly.
|
|
// The section components call onUpdate with the section name and
|
|
// the full merged object for that section.
|
|
};
|
|
|
|
return (
|
|
<div className="w-full py-6">
|
|
{/* Header */}
|
|
<div className="mb-8 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
<div>
|
|
<h2 className="mb-2 text-2xl font-bold text-white">系统运行配置</h2>
|
|
<p className="text-slate-400">全局基础目录设定及核心入库引擎的高级策略配置。</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<button type="button" onClick={handleImportButtonClick} disabled={isSaving}
|
|
className="flex items-center whitespace-nowrap rounded-lg border border-slate-700 bg-slate-900 px-4 py-2.5 text-sm font-medium text-slate-200 transition hover:border-blue-500/50 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60">
|
|
<Upload className="mr-2 h-4 w-4" />导入配置
|
|
</button>
|
|
<button type="button" onClick={handleOpenExportDialog} disabled={isSaving}
|
|
className="flex items-center whitespace-nowrap rounded-lg border border-slate-700 bg-slate-900 px-4 py-2.5 text-sm font-medium text-slate-200 transition hover:border-emerald-500/50 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60">
|
|
<Download className="mr-2 h-4 w-4" />导出配置
|
|
</button>
|
|
<button type="button" onClick={handleSave} disabled={isSaving}
|
|
className="flex items-center whitespace-nowrap rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-medium text-white shadow-lg shadow-blue-900/20 transition hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-70">
|
|
{isSaving ? <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> : <CheckCircle2 className="mr-2 h-4 w-4" />}
|
|
{isSaving ? '校验并保存...' : '保存系统配置'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hidden file input for import */}
|
|
<input ref={fileInputRef} type="file" accept=".json,application/json"
|
|
className="hidden" onChange={handleImportFileChange} />
|
|
|
|
{/* Toast notification */}
|
|
{toast && (
|
|
<div className={`fixed left-1/2 top-6 z-[100] flex -translate-x-1/2 items-center rounded-xl border px-5 py-3 shadow-2xl backdrop-blur-md animate-in slide-in-from-top-4 fade-in duration-300 ${
|
|
toast.type === 'success' ? 'border-emerald-500/50 bg-emerald-950/90 text-emerald-400' : 'border-rose-500/50 bg-rose-950/90 text-rose-400'
|
|
}`}>
|
|
{toast.type === 'success' ? <CheckCircle2 className="mr-2 h-5 w-5" /> : <XCircle className="mr-2 h-5 w-5" />}
|
|
<span className="text-sm font-medium">{toast.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Configuration Sections */}
|
|
<div className="mb-8 space-y-6">
|
|
<CorePathsSection
|
|
input={localConfig.input}
|
|
output={localConfig.output}
|
|
trash={localConfig.trash}
|
|
onUpdate={(field, value) => updateField(field, value)}
|
|
/>
|
|
|
|
<AdvancedStrategySection
|
|
advancedStrategy={localConfig.advancedStrategy || {}}
|
|
onUpdate={(section, value) => updateField(section, value)}
|
|
/>
|
|
|
|
<ScheduleSection
|
|
schedule={localConfig.schedule || {}}
|
|
onUpdate={(section, value) => updateField(section, value)}
|
|
/>
|
|
|
|
<NotificationSection
|
|
notifications={localConfig.notifications || {}}
|
|
onUpdate={(section, value) => updateField(section, value)}
|
|
/>
|
|
|
|
<MetadataServicesSection
|
|
metadata={localConfig.metadata || {}}
|
|
netStatus={netStatus}
|
|
onUpdate={(section, value) => updateField(section, value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Dialogs */}
|
|
<ConfigExportDialog
|
|
isOpen={isExportDialogOpen}
|
|
isExporting={isExporting}
|
|
password={exportPassword}
|
|
passwordConfirm={exportPasswordConfirm}
|
|
onClose={handleCloseExportDialog}
|
|
onExport={handleExportConfig}
|
|
onPasswordChange={setExportPassword}
|
|
onPasswordConfirmChange={setExportPasswordConfirm}
|
|
/>
|
|
|
|
<ConfigImportDialog
|
|
importDialog={importDialog}
|
|
onClose={handleCloseImportDialog}
|
|
onDecrypt={handleImportDecrypt}
|
|
onConfirm={handleImportConfirm}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|