feat: M4-02 — LearningData admin page with sessions, analysis, AI usage tabs
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 10s

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 17:45:53 +08:00
parent 2118adfb66
commit 35a3f40ef8
3 changed files with 109 additions and 0 deletions

View File

@ -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() {
}
/>
<Route path="reviews" element={<Suspense fallback={<PageLoading />}><ReviewAdmin /></Suspense>} />
<Route path="learning-data" element={<Suspense fallback={<PageLoading />}><LearningData /></Suspense>} />
<Route path="notification-admin" element={<Suspense fallback={<PageLoading />}><NotificationAdmin /></Suspense>} />
<Route path="*" element={<NotFoundPage />} />
</Route>

View File

@ -46,6 +46,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/safety', name: '内容安全' },
]},
{ path: '/reviews', name: '复习数据', icon: <BookOutlined />, requiredRole: 'ADMIN' },
{ path: '/learning-data', name: '学习数据', icon: <BookOutlined />, requiredRole: 'ADMIN' },
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
]

106
src/pages/LearningData.tsx Normal file
View File

@ -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<string, string> = { 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<any>(`/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<any>(`/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<any>(`/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) => <Tag color={statusColors[s] || 'default'}>{s}</Tag> },
{ 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) => <Tag color={s ? 'green' : 'red'}>{s ? '是' : '否'}</Tag> },
{ title: '时间', dataIndex: 'createdAt', width: 120, render: (d: string) => new Date(d).toLocaleString() },
]
return (
<div>
<Title level={4}></Title>
<Space style={{ marginBottom: 16 }}>
<Input
placeholder="按用户 ID 搜索"
prefix={<SearchOutlined />}
value={search}
onChange={e => setSearch(e.target.value)}
allowClear
style={{ width: 240 }}
/>
</Space>
<Tabs activeKey={tab} onChange={setTab} items={[
{
key: 'sessions', label: '学习会话',
children: <Table dataSource={sessions?.items || []} columns={sessionColumns} rowKey="id" loading={sLoading} pagination={{ total: sessions?.total || 0 }} size="small" scroll={{ x: 900 }} />,
},
{
key: 'analysis', label: 'AI 分析结果',
children: <Table dataSource={analysis?.items || []} columns={analysisColumns} rowKey="id" loading={aLoading} pagination={{ total: analysis?.total || 0 }} size="small" scroll={{ x: 900 }} />,
},
{
key: 'ai-usage', label: 'AI 调用日志',
children: <Table dataSource={aiUsage?.items || []} columns={aiUsageColumns} rowKey="id" loading={uLoading} pagination={{ total: aiUsage?.total || 0 }} size="small" scroll={{ x: 1000 }} />,
},
]} />
</div>
)
}