diff --git a/src/App.tsx b/src/App.tsx
index ecf0f1d..e7503bb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -42,6 +42,7 @@ const ReviewAdmin = lazy(() => import('./pages/ReviewAdmin'))
const NotificationAdmin = lazy(() => import('./pages/NotificationAdmin'))
const CacheAdmin = lazy(() => import('./pages/CacheAdmin'))
const LearningData = lazy(() => import('./pages/LearningData'))
+const BackupAdmin = lazy(() => import('./pages/BackupAdmin'))
const queryClient = new QueryClient()
@@ -178,6 +179,7 @@ function App() {
/>
}>} />
}>} />
+ }>} />
}>} />
} />
diff --git a/src/config/menu.tsx b/src/config/menu.tsx
index 7c92e82..8df465b 100644
--- a/src/config/menu.tsx
+++ b/src/config/menu.tsx
@@ -41,6 +41,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/events', name: '事件队列' },
{ path: '/vector', name: '向量检索' },
{ path: '/config', name: '配置管理' },
+ { path: '/backup', name: '备份清理' },
{ path: '/cache', name: '缓存管理' },
{ path: '/notification-admin', name: '通知管理' },
{ path: '/safety', name: '内容安全' },
diff --git a/src/pages/BackupAdmin.tsx b/src/pages/BackupAdmin.tsx
new file mode 100644
index 0000000..75f8f8c
--- /dev/null
+++ b/src/pages/BackupAdmin.tsx
@@ -0,0 +1,91 @@
+import { useState } from 'react'
+import { Table, Button, Tag, Space, Typography, Tabs, message } from 'antd'
+import { CloudUploadOutlined, DeleteOutlined } from '@ant-design/icons'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { api } from '@/services/http-client'
+
+const { Title } = Typography
+
+const statusColors: Record = { RUNNING: 'blue', COMPLETED: 'green', FAILED: 'red' }
+
+export default function BackupAdmin() {
+ const [tab, setTab] = useState('backups')
+ const qc = useQueryClient()
+
+ const { data: backups, isLoading: bLoading } = useQuery({
+ queryKey: ['admin', 'backup-jobs'],
+ queryFn: () => api.get('/admin-api/backup/jobs'),
+ enabled: tab === 'backups',
+ })
+
+ const { data: cleanups, isLoading: cLoading } = useQuery({
+ queryKey: ['admin', 'cleanup-jobs'],
+ queryFn: () => api.get('/admin-api/backup/cleanup'),
+ enabled: tab === 'cleanup',
+ })
+
+ const triggerBackup = useMutation({
+ mutationFn: (type: string) => api.post(`/admin-api/backup/trigger/${type}`),
+ onSuccess: () => { message.success('备份任务已触发'); qc.invalidateQueries({ queryKey: ['admin', 'backup-jobs'] }) },
+ })
+
+ const triggerCleanup = useMutation({
+ mutationFn: (type: string) => api.post(`/admin-api/backup/cleanup/${type}`),
+ onSuccess: () => { message.success('清理任务已触发'); qc.invalidateQueries({ queryKey: ['admin', 'cleanup-jobs'] }) },
+ })
+
+ const backupColumns = [
+ { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true },
+ { title: '类型', dataIndex: 'type', width: 80 },
+ { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => {s} },
+ { title: '本地路径', dataIndex: 'localPath', width: 200, ellipsis: true, render: (p: string | null) => p || '-' },
+ { title: 'COS Key', dataIndex: 'cosObjectKey', width: 180, ellipsis: true, render: (k: string | null) => k || '-' },
+ { title: '大小', dataIndex: 'fileSizeBytes', width: 80, render: (s: number) => s ? `${(Number(s) / 1048576).toFixed(1)} MB` : '-' },
+ { title: '开始时间', dataIndex: 'startedAt', width: 120, render: (d: string) => new Date(d).toLocaleString() },
+ { title: '完成时间', dataIndex: 'completedAt', width: 120, render: (d: string | null) => d ? new Date(d).toLocaleString() : '-' },
+ ]
+
+ const cleanupColumns = [
+ { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true },
+ { title: '类型', dataIndex: 'type', width: 100 },
+ { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => {s} },
+ { title: '目标', dataIndex: 'target', width: 120, ellipsis: true, render: (t: string | null) => t || '-' },
+ { title: '影响行数', dataIndex: 'rowsAffected', width: 80 },
+ { title: '开始时间', dataIndex: 'startedAt', width: 120, render: (d: string) => new Date(d).toLocaleString() },
+ { title: '完成时间', dataIndex: 'completedAt', width: 120, render: (d: string | null) => d ? new Date(d).toLocaleString() : '-' },
+ ]
+
+ return (
+
+
备份与清理
+
+
+ } onClick={() => triggerBackup.mutate('mysql')} loading={triggerBackup.isPending}>MySQL 备份
+ } onClick={() => triggerBackup.mutate('qdrant')} loading={triggerBackup.isPending}>Qdrant Snapshot
+ } onClick={() => triggerBackup.mutate('files')} loading={triggerBackup.isPending}>文件备份
+
+
+
+ ),
+ },
+ {
+ key: 'cleanup', label: '清理任务',
+ children: (
+
+
+ } onClick={() => triggerCleanup.mutate('soft-delete')} loading={triggerCleanup.isPending}>软删除清理
+ } onClick={() => triggerCleanup.mutate('api-metrics')} loading={triggerCleanup.isPending}>指标清理
+ } onClick={() => triggerCleanup.mutate('task-logs')} loading={triggerCleanup.isPending}>任务日志清理
+
+
+
+ ),
+ },
+ ]} />
+
+ )
+}