feat: UserTimelinePage with Timeline component (ADMIN-INFO-009)
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 8s

- UserTimelinePage: userId input → API query → Ant Design Timeline
- Supports array timeline + fallback JSON view for unknown formats
- Loading / empty / error states
- Route: /learning/timeline

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-15 21:34:49 +08:00
parent a2cb0681d9
commit 0fd0709b8b
2 changed files with 46 additions and 1 deletions

View File

@ -1,5 +1,7 @@
import { useState } from 'react';
import { Table, Input, Select, Space, Tag, Button, Tabs, Drawer, Descriptions, DatePicker } from 'antd';
import { Table, Input, Select, Space, Tag, Button, Tabs, Drawer, Descriptions, DatePicker, Timeline, Typography } from 'antd';
const { Text } = Typography;
import { useQuery } from '@tanstack/react-query';
import { learningAdminAPI } from '../../services/learningAdmin';
import dayjs from 'dayjs';
@ -410,6 +412,47 @@ export function AnomalyPage() {
);
}
// ── User Timeline ──
export function UserTimelinePage() {
const [userId, setUserId] = useState('');
const { data, isFetching, refetch } = useQuery({
queryKey: ['admin-user-timeline', userId],
queryFn: () => learningAdminAPI.getUserTimeline(userId),
enabled: false,
});
const items = Array.isArray(data)
? (data as any[]).map((entry: any, i: number) => ({
key: i,
color: entry.recordType === 'session' ? 'blue' : entry.recordType === 'reading' ? 'green' : 'gray',
children: (
<div>
<Text type="secondary" style={{ fontSize: 12 }}>{entry.occurredAt ? dayjs(entry.occurredAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</Text>
<div><Tag style={{ marginTop: 4 }}>{entry.recordType}</Tag></div>
<Text strong>{entry.title || '-'}</Text>
{entry.durationSeconds != null && <div><Text type="secondary"> {entry.durationSeconds}s</Text></div>}
{entry.materialId && <div><Text type="secondary"> {entry.materialId}</Text></div>}
</div>
),
}))
: [];
return (
<PageWrapper title="用户学习时间线">
<Space style={{ marginBottom: 16 }}>
<Input placeholder="User ID" value={userId} onChange={e => setUserId(e.target.value)} style={{ width: 300 }} />
<Button type="primary" onClick={() => refetch()} loading={isFetching}></Button>
</Space>
{data === undefined && !isFetching && <Text type="secondary"> ID </Text>}
{isFetching && <Text type="secondary">...</Text>}
{data && !Array.isArray(data) ? <pre style={{ background: '#f5f5f5', padding: 16, borderRadius: 8, overflow: 'auto', maxHeight: 600 }}>{JSON.stringify(data as any, null, 2)}</pre> : null}
{Array.isArray(data) && data.length === 0 && !isFetching && <Text type="secondary"></Text>}
{items.length > 0 && <Timeline items={items} />}
</PageWrapper>
);
}
// ── User Diagnose ──
export function UserDiagnosePage() {

View File

@ -13,6 +13,7 @@ const ProgressPage = lazy(() => import('@/pages/learning/DataPages').then(m => (
const DailyActivityPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.DailyActivityPage })))
const RecordPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.RecordPage })))
const AnomalyPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.AnomalyPage })))
const UserTimelinePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.UserTimelinePage })))
const UserDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.UserDiagnosePage })))
const MaterialDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.MaterialDiagnosePage })))
@ -44,6 +45,7 @@ export const routeConfig: RouteConfig[] = [
{ path: '/learning/progress', title: '阅读进度', element: ProgressPage },
{ path: '/learning/daily', title: '每日活动', element: DailyActivityPage },
{ path: '/learning/records', title: '学习记录', element: RecordPage },
{ path: '/learning/timeline', title: '用户时间线', element: UserTimelinePage },
{ path: '/learning/anomalies', title: '异常数据', element: AnomalyPage },
{ path: '/learning/user-diagnose', title: '用户诊断', element: UserDiagnosePage },
{ path: '/learning/material-diagnose', title: '资料诊断', element: MaterialDiagnosePage },