diff --git a/src/App.tsx b/src/App.tsx index 0426f02..ecf0f1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ const ServerErrorPage = lazy(() => import('./pages/500')) 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 queryClient = new QueryClient() @@ -176,6 +177,7 @@ function App() { } /> }>} /> + }>} /> }>} /> } /> diff --git a/src/config/menu.tsx b/src/config/menu.tsx index 8fcbaa4..7c92e82 100644 --- a/src/config/menu.tsx +++ b/src/config/menu.tsx @@ -46,6 +46,7 @@ export const adminMenuItems: AdminMenuItem[] = [ { path: '/safety', name: '内容安全' }, ]}, { path: '/reviews', name: '复习数据', icon: , requiredRole: 'ADMIN' }, + { path: '/learning-data', name: '学习数据', icon: , requiredRole: 'ADMIN' }, { path: '/settings', name: '系统配置', icon: , requiredRole: 'ADMIN' }, ] diff --git a/src/pages/LearningData.tsx b/src/pages/LearningData.tsx new file mode 100644 index 0000000..a523440 --- /dev/null +++ b/src/pages/LearningData.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react' +import { Table, Tabs, Tag, Space, Input, Typography } from 'antd' +import { SearchOutlined } from '@ant-design/icons' +import { useQuery } from '@tanstack/react-query' +import { api } from '@/services/http-client' + +const { Title } = Typography + +const statusColors: Record = { active: 'blue', completed: 'green', cancelled: 'red' } + +export default function LearningData() { + const [tab, setTab] = useState('sessions') + const [search, setSearch] = useState('') + + const { data: sessions, isLoading: sLoading } = useQuery({ + queryKey: ['admin', 'learning-sessions', search], + queryFn: () => { + const p = new URLSearchParams() + if (search) p.set('userId', search) + return api.get(`/admin-api/learning/sessions?${p.toString()}`) + }, + enabled: tab === 'sessions', + }) + + const { data: analysis, isLoading: aLoading } = useQuery({ + queryKey: ['admin', 'analysis-results', search], + queryFn: () => { + const p = new URLSearchParams() + if (search) p.set('userId', search) + return api.get(`/admin-api/learning/analysis?${p.toString()}`) + }, + enabled: tab === 'analysis', + }) + + const { data: aiUsage, isLoading: uLoading } = useQuery({ + queryKey: ['admin', 'ai-usage', search], + queryFn: () => { + const p = new URLSearchParams() + if (search) p.set('userId', search) + return api.get(`/admin-api/learning/ai-usage?${p.toString()}`) + }, + enabled: tab === 'ai-usage', + }) + + const sessionColumns = [ + { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true }, + { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true }, + { title: '知识库', dataIndex: 'knowledgeBaseId', width: 100, ellipsis: true }, + { title: '模式', dataIndex: 'mode', width: 80 }, + { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => {s} }, + { title: '时长(分)', dataIndex: 'durationSeconds', width: 80, render: (s: number) => s ? Math.round(s / 60) : '-' }, + { title: '开始时间', dataIndex: 'startedAt', width: 120, render: (d: string) => d ? new Date(d).toLocaleString() : '-' }, + { title: '结束时间', dataIndex: 'endedAt', width: 120, render: (d: string) => d ? new Date(d).toLocaleString() : '-' }, + ] + + const analysisColumns = [ + { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true }, + { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true }, + { title: '摘要', dataIndex: 'summary', width: 200, ellipsis: true }, + { title: '掌握度', dataIndex: 'masteryScore', width: 80, render: (s: number | null) => s != null ? `${s}%` : '-' }, + { title: '薄弱点', dataIndex: 'weaknesses', width: 200, render: (w: string[]) => (w || []).join('、') || '-' }, + { title: '时间', dataIndex: 'createdAt', width: 120, render: (d: string) => new Date(d).toLocaleString() }, + ] + + const aiUsageColumns = [ + { title: 'ID', dataIndex: 'id', width: 100, ellipsis: true }, + { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true }, + { title: '模型', dataIndex: 'model', width: 140, ellipsis: true }, + { title: '服务商', dataIndex: 'provider', width: 80 }, + { title: '输入 Token', dataIndex: 'inputTokens', width: 90 }, + { title: '输出 Token', dataIndex: 'outputTokens', width: 90 }, + { title: '费用', dataIndex: 'estimatedCost', width: 70, render: (c: number) => c != null ? `¥${c}` : '-' }, + { title: '成功', dataIndex: 'success', width: 60, render: (s: boolean) => {s ? '是' : '否'} }, + { title: '时间', dataIndex: 'createdAt', width: 120, render: (d: string) => new Date(d).toLocaleString() }, + ] + + return ( +
+ 学习数据 + + } + value={search} + onChange={e => setSearch(e.target.value)} + allowClear + style={{ width: 240 }} + /> + + , + }, + { + key: 'analysis', label: 'AI 分析结果', + children: , + }, + { + key: 'ai-usage', label: 'AI 调用日志', + children:
, + }, + ]} /> + + ) +}