feat: UserTimelinePage with Timeline component (ADMIN-INFO-009)
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 8s
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:
parent
a2cb0681d9
commit
0fd0709b8b
@ -1,5 +1,7 @@
|
|||||||
import { useState } from 'react';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { learningAdminAPI } from '../../services/learningAdmin';
|
import { learningAdminAPI } from '../../services/learningAdmin';
|
||||||
import dayjs from 'dayjs';
|
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 ──
|
// ── User Diagnose ──
|
||||||
|
|
||||||
export function UserDiagnosePage() {
|
export function UserDiagnosePage() {
|
||||||
|
|||||||
@ -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 DailyActivityPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.DailyActivityPage })))
|
||||||
const RecordPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.RecordPage })))
|
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 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 UserDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.UserDiagnosePage })))
|
||||||
const MaterialDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.MaterialDiagnosePage })))
|
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/progress', title: '阅读进度', element: ProgressPage },
|
||||||
{ path: '/learning/daily', title: '每日活动', element: DailyActivityPage },
|
{ path: '/learning/daily', title: '每日活动', element: DailyActivityPage },
|
||||||
{ path: '/learning/records', title: '学习记录', element: RecordPage },
|
{ path: '/learning/records', title: '学习记录', element: RecordPage },
|
||||||
|
{ path: '/learning/timeline', title: '用户时间线', element: UserTimelinePage },
|
||||||
{ path: '/learning/anomalies', title: '异常数据', element: AnomalyPage },
|
{ path: '/learning/anomalies', title: '异常数据', element: AnomalyPage },
|
||||||
{ path: '/learning/user-diagnose', title: '用户诊断', element: UserDiagnosePage },
|
{ path: '/learning/user-diagnose', title: '用户诊断', element: UserDiagnosePage },
|
||||||
{ path: '/learning/material-diagnose', title: '资料诊断', element: MaterialDiagnosePage },
|
{ path: '/learning/material-diagnose', title: '资料诊断', element: MaterialDiagnosePage },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user