diff --git a/src/App.tsx b/src/App.tsx
index e7503bb..d9a8f8c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,6 +43,7 @@ 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 ReportingAdmin = lazy(() => import('./pages/ReportingAdmin'))
const queryClient = new QueryClient()
@@ -180,6 +181,7 @@ function App() {
}>} />
}>} />
}>} />
+ }>} />
}>} />
} />
diff --git a/src/config/menu.tsx b/src/config/menu.tsx
index 8df465b..37f6daa 100644
--- a/src/config/menu.tsx
+++ b/src/config/menu.tsx
@@ -27,6 +27,7 @@ export const adminMenuItems: AdminMenuItem[] = [
]},
{ path: '/imports', name: '文档导入', icon: },
{ path: '/files', name: '文件与 COS', icon: },
+ { path: '/reporting', name: '报表导出', icon: , requiredRole: 'ADMIN' },
{ path: '/audit', name: '审计日志', icon: , requiredRole: 'ADMIN' },
{ path: '/billing', name: 'API 用量', icon: , requiredRole: 'SUPER_ADMIN' },
{ path: '/git', name: '代码仓库', icon: },
diff --git a/src/pages/ReportingAdmin.tsx b/src/pages/ReportingAdmin.tsx
new file mode 100644
index 0000000..ab2574d
--- /dev/null
+++ b/src/pages/ReportingAdmin.tsx
@@ -0,0 +1,72 @@
+import { Card, Button, Space, Typography, Table, message, Select } from 'antd'
+import { DownloadOutlined } from '@ant-design/icons'
+import { useQuery } from '@tanstack/react-query'
+import { api } from '@/services/http-client'
+import { useState } from 'react'
+
+const { Title } = Typography
+
+function downloadBlob(data: string, filename: string) {
+ const blob = new Blob(['' + data], { type: 'text/csv;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url; a.download = filename
+ a.click(); URL.revokeObjectURL(url)
+}
+
+export default function ReportingAdmin() {
+ const [days, setDays] = useState(30)
+
+ const { data: jobs } = useQuery({
+ queryKey: ['admin', 'export-jobs'],
+ queryFn: () => api.get('/admin-api/reporting/jobs'),
+ })
+
+ const download = async (type: string) => {
+ try {
+ const res = await fetch(`/admin-api/reporting/export/${type}?days=${days}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('accessToken') || ''}` },
+ })
+ if (!res.ok) throw new Error('下载失败')
+ const text = await res.text()
+ downloadBlob(text, `${type}-report-${days}d.csv`)
+ } catch {
+ message.error('导出失败')
+ }
+ }
+
+ const jobColumns = [
+ { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true },
+ { title: '类型', dataIndex: 'type', width: 80 },
+ { title: '状态', dataIndex: 'status', width: 80 },
+ { title: '格式', dataIndex: 'format', width: 60 },
+ { title: '文件大小', dataIndex: 'fileSize', width: 90, render: (s: number) => s ? `${(s / 1024).toFixed(1)} KB` : '-' },
+ { title: '创建时间', dataIndex: 'createdAt', width: 120, render: (d: string) => new Date(d).toLocaleString() },
+ ]
+
+ return (
+
+
报表与导出
+
+
+
+ 时间范围:
+
+
+
+ } onClick={() => download('users')}>用户数据 CSV
+ } onClick={() => download('learning')}>学习数据 CSV
+ } onClick={() => download('reviews')}>复习数据 CSV
+
+
+
+
+
+
+
+ )
+}