import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Activity, ArrowRightLeft, CheckCircle2, Database, HardDrive, HelpCircle, Loader2, RefreshCw, Save, Server, ShieldCheck, Zap, } from 'lucide-react' import { adminAPI, type DatabaseConnectionPayload, type DatabaseMigrationResult, type DatabaseStatus, type PostgresTestResult, } from '../api/admin' import { confirmAction } from '../components/confirmAction' export function DatabaseSettingsPanel() { const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [mode, setMode] = useState<'form' | 'dsn'>('form') // 表单状态 const [formData, setFormData] = useState({ host: '127.0.0.1', port: 5432, user: 'postgres', password: '', dbname: 'mebox', sslmode: 'disable', dsn: '', }) // 测试与操作状态 const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState(null) const [migrating, setMigrating] = useState(false) const [migrationResult, setMigrationResult] = useState(null) const [saving, setSaving] = useState(false) const refreshStatus = () => { setLoading(true) return adminAPI .getDatabaseStatus() .then(setStatus) .catch((err) => toast.error('获取数据库状态失败: ' + (err.message || '网络错误'))) .finally(() => setLoading(false)) } useEffect(() => { refreshStatus().catch(() => undefined) }, []) const getPayload = (): DatabaseConnectionPayload => { if (mode === 'dsn') { return { type: 'postgres', dsn: formData.dsn?.trim() || '' } } return { type: 'postgres', host: formData.host?.trim() || '', port: Number(formData.port) || 5432, user: formData.user?.trim() || '', password: formData.password || '', dbname: formData.dbname?.trim() || 'mebox', sslmode: formData.sslmode || 'disable', } } const handleTestConnection = async () => { const payload = getPayload() if (mode === 'form' && (!payload.host || !payload.user)) { toast.error('请填写 PostgreSQL 主机和用户名') return } if (mode === 'dsn' && !payload.dsn) { toast.error('请填写 PostgreSQL DSN') return } setTesting(true) setTestResult(null) try { const res = await adminAPI.testDatabaseConnection(payload) setTestResult(res) if (res.success) { toast.success(`连接成功!延迟: ${res.latency_ms}ms`) } else { toast.error(res.error || '连接失败') } } catch (err: any) { const errorMsg = err.response?.data?.error || err.message || '测试连接异常' setTestResult({ success: false, error: errorMsg }) toast.error(errorMsg) } finally { setTesting(false) } } const handleMigrate = async () => { const payload = getPayload() if (status?.type === 'postgres') { const ok = await confirmAction({ title: '覆盖/同步确认', message: '当前已经处于 PostgreSQL 模式,继续迁移将覆盖/合并目标库的数据,确定继续吗?', }) if (!ok) return } else { const ok = await confirmAction({ title: '开始数据库迁移', message: '即将把当前 SQLite 数据库中的所有媒体、用户、播放记录、设置等全量迁移到目标 PostgreSQL 数据库。确定开始吗?', }) if (!ok) return } setMigrating(true) setMigrationResult(null) try { const res = await adminAPI.migrateDatabase(payload) setMigrationResult(res) if (res.success) { toast.success(`数据迁移完成!共迁移 ${res.total_rows} 条记录`) } else { toast.error(res.error || '数据迁移失败') } } catch (err: any) { const errorMsg = err.response?.data?.error || err.message || '迁移发生错误' setMigrationResult({ success: false, total_rows: 0, duration_ms: 0, error: errorMsg }) toast.error(errorMsg) } finally { setMigrating(false) } } const handleSaveAndSwitch = async () => { const payload = getPayload() const ok = await confirmAction({ title: '切换数据库', message: '保存后系统配置将更新为使用 PostgreSQL。需要重启 MeBox 服务使新数据库生效。确定保存吗?', }) if (!ok) return setSaving(true) try { const res = await adminAPI.saveDatabaseConfig(payload) toast.success(res.message || '数据库配置已保存,请重启服务生效') refreshStatus() } catch (err: any) { toast.error(err.response?.data?.error || err.message || '保存配置失败') } finally { setSaving(false) } } return (
{/* 头部标题 */}

数据库设置与迁移

管理系统底层数据库,支持在 SQLite(本地嵌入式)与 PostgreSQL(高性能关系库)之间平滑切换与数据迁移

{/* 当前数据库状态卡片 */}
当前运行引擎
{status?.type === 'postgres' ? 'PostgreSQL' : 'SQLite (WAL 优化)'}

存储位置 / 连接

{status?.type === 'postgres' ? status.dsn || '配置的 PostgreSQL 实例' : status?.db_path || './data/mebox.db'}

连接池活跃 / 最大

活跃: {status?.in_use ?? 0} · 空闲: {status?.idle ?? 0} · 上限:{' '} {status?.max_open_conns ?? 16}

核心表记录概览

媒体: {status?.table_counts?.media ?? 0} · 用户: {status?.table_counts?.users ?? 0} · 播放记录: {status?.table_counts?.playback_histories ?? 0}

{/* 配置 PostgreSQL */}
配置目标 PostgreSQL
{mode === 'form' ? (
setFormData({ ...formData, host: e.target.value })} placeholder="例如 127.0.0.1 或 postgres" className="w-full rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
setFormData({ ...formData, port: Number(e.target.value) })} placeholder="5432" className="w-full rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
setFormData({ ...formData, dbname: e.target.value })} placeholder="mebox" className="w-full rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
setFormData({ ...formData, user: e.target.value })} placeholder="postgres" className="w-full rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
setFormData({ ...formData, password: e.target.value })} placeholder="••••••••" className="w-full rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
) : (
setFormData({ ...formData, dsn: e.target.value })} placeholder="postgres://user:password@127.0.0.1:5432/mebox?sslmode=disable" className="w-full font-mono text-xs rounded-lg border border-gray-200 bg-sand-200/40 px-3 py-2 text-ink-600 placeholder:text-gray-400 focus:border-brand-500 focus:outline-none" />
)} {/* 测试结果卡片 */} {testResult && (
{testResult.success ? ( ) : ( )}

{testResult.success ? `连接测试通过 (${testResult.latency_ms} ms)` : '连接测试未通过'}

{testResult.version &&

{testResult.version}

} {testResult.error &&

{testResult.error}

}
)} {/* 迁移结果卡片 */} {migrationResult && (
{migrationResult.message || (migrationResult.success ? '迁移完成' : '迁移失败')} {migrationResult.success && ( (耗时: {migrationResult.duration_ms} ms) )}
{migrationResult.table_rows && Object.keys(migrationResult.table_rows).length > 0 && (
{Object.entries(migrationResult.table_rows).map(([tbl, count]) => (
{tbl}: {count} 条
))}
)} {migrationResult.error && (

{migrationResult.error}

)}
)} {/* 操作按钮区 */}
) }