From 0fd0709b8b567b94566c91c2a9c614b52bf3f68b Mon Sep 17 00:00:00 2001 From: wangdl Date: Mon, 15 Jun 2026 21:34:49 +0800 Subject: [PATCH] feat: UserTimelinePage with Timeline component (ADMIN-INFO-009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/pages/learning/DataPages.tsx | 45 +++++++++++++++++++++++++++++++- src/routes/index.tsx | 2 ++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/pages/learning/DataPages.tsx b/src/pages/learning/DataPages.tsx index 53b2e25..c175cf2 100644 --- a/src/pages/learning/DataPages.tsx +++ b/src/pages/learning/DataPages.tsx @@ -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: ( +
+ {entry.occurredAt ? dayjs(entry.occurredAt).format('YYYY-MM-DD HH:mm:ss') : '-'} +
{entry.recordType}
+ {entry.title || '-'} + {entry.durationSeconds != null &&
时长 {entry.durationSeconds}s
} + {entry.materialId &&
资料 {entry.materialId}
} +
+ ), + })) + : []; + + return ( + + + setUserId(e.target.value)} style={{ width: 300 }} /> + + + {data === undefined && !isFetching && 输入用户 ID 后点击查询} + {isFetching && 加载中...} + {data && !Array.isArray(data) ?
{JSON.stringify(data as any, null, 2)}
: null} + {Array.isArray(data) && data.length === 0 && !isFetching && 该用户暂无学习记录} + {items.length > 0 && } +
+ ); +} + // ── User Diagnose ── export function UserDiagnosePage() { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index cf35bf7..712b5b0 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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 },