feat: M4-05 — reporting admin page with CSV download buttons
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s
- ReportingAdmin page: download user/learning/review CSV with day range selector Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b36924d9b3
commit
4b14178574
@ -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() {
|
||||
<Route path="reviews" element={<Suspense fallback={<PageLoading />}><ReviewAdmin /></Suspense>} />
|
||||
<Route path="learning-data" element={<Suspense fallback={<PageLoading />}><LearningData /></Suspense>} />
|
||||
<Route path="backup" element={<Suspense fallback={<PageLoading />}><BackupAdmin /></Suspense>} />
|
||||
<Route path="reporting" element={<Suspense fallback={<PageLoading />}><ReportingAdmin /></Suspense>} />
|
||||
<Route path="notification-admin" element={<Suspense fallback={<PageLoading />}><NotificationAdmin /></Suspense>} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@ -27,6 +27,7 @@ export const adminMenuItems: AdminMenuItem[] = [
|
||||
]},
|
||||
{ path: '/imports', name: '文档导入', icon: <ImportOutlined /> },
|
||||
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
|
||||
{ path: '/reporting', name: '报表导出', icon: <FileOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
||||
|
||||
72
src/pages/ReportingAdmin.tsx
Normal file
72
src/pages/ReportingAdmin.tsx
Normal file
@ -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<any>('/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 (
|
||||
<div>
|
||||
<Title level={4}>报表与导出</Title>
|
||||
|
||||
<Card title="数据导出" style={{ marginBottom: 24 }}>
|
||||
<Space>
|
||||
<span>时间范围:</span>
|
||||
<Select value={days} onChange={setDays} style={{ width: 120 }} options={[
|
||||
{ label: '近 7 天', value: 7 },
|
||||
{ label: '近 30 天', value: 30 },
|
||||
{ label: '近 90 天', value: 90 },
|
||||
]} />
|
||||
</Space>
|
||||
<Space style={{ marginTop: 16 }}>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => download('users')}>用户数据 CSV</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => download('learning')}>学习数据 CSV</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => download('reviews')}>复习数据 CSV</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="导出任务历史">
|
||||
<Table dataSource={jobs?.items || []} columns={jobColumns} rowKey="id" size="small" pagination={{ total: jobs?.total || 0 }} scroll={{ x: 600 }} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user