diff --git a/src/App.tsx b/src/App.tsx index a00130a..a759757 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,8 @@ 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 LearningDashboard = lazy(() => import('./pages/learning/Dashboard')) +const LearningReplay = lazy(() => import('./pages/learning/ReplayPage')) const BackupAdmin = lazy(() => import('./pages/BackupAdmin')) const ReportingAdmin = lazy(() => import('./pages/ReportingAdmin')) @@ -182,6 +184,8 @@ function App() { /> }>} /> }>} /> + }>} /> + }>} /> }>} /> }>} /> }>} /> diff --git a/src/config/menu.tsx b/src/config/menu.tsx index d1c97a9..9836137 100644 --- a/src/config/menu.tsx +++ b/src/config/menu.tsx @@ -47,6 +47,7 @@ export const adminMenuItems: AdminMenuItem[] = [ { path: '/learning/anomalies', name: '异常数据' }, { path: '/learning/user-diagnose', name: '用户诊断' }, { path: '/learning/material-diagnose', name: '资料诊断' }, + { path: '/learning/replay', name: '事件重放' }, ], }, diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 343361b..9150b97 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -15,6 +15,7 @@ const AnomalyPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ 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 }))) +const LearningReplay = lazy(() => import('@/pages/learning/ReplayPage')) export interface RouteConfig { path: string @@ -47,4 +48,5 @@ export const routeConfig: RouteConfig[] = [ { path: '/learning/anomalies', title: '异常数据', element: AnomalyPage }, { path: '/learning/user-diagnose', title: '用户诊断', element: UserDiagnosePage }, { path: '/learning/material-diagnose', title: '资料诊断', element: MaterialDiagnosePage }, + { path: '/learning/replay', title: '事件重放', element: LearningReplay }, ] diff --git a/src/types/learning.ts b/src/types/learning.ts new file mode 100644 index 0000000..cf5da19 --- /dev/null +++ b/src/types/learning.ts @@ -0,0 +1,146 @@ +// ── Reading Event ── +export interface ReadingEvent { + id: string; + userId: string; + eventId: string; + clientSessionId: string; + readingTargetType: string; + materialId: string; + knowledgeBaseId?: string; + eventType: string; + position?: unknown; + activeSecondsDelta: number; + clientTimestampMs: number; + clientTimezoneOffsetMinutes?: number; + sequence: number; + platform?: string; + appVersion?: string; + status: string; + errorCode?: string; + warningCodes?: unknown; + serverReceivedAt: string; + processedAt?: string; + createdAt: string; +} + +// ── Learning Session ── +export interface LearningSession { + id: string; + userId: string; + knowledgeBaseId?: string; + knowledgeItemId?: string; + mode: string; + status: string; + startedAt: string; + endedAt?: string; + durationSeconds: number; + focusMinutes?: number; + totalActiveSeconds: number; + clientSessionId?: string; + materialId?: string; + readingTargetType?: string; + lastPosition?: unknown; + lastEventAt?: string; + createdAt: string; +} + +// ── Material Reading Progress ── +export interface MaterialReadingProgress { + id: string; + userId: string; + readingTargetType: string; + materialId: string; + knowledgeBaseId?: string; + lastClientSessionId?: string; + lastPosition?: unknown; + lastProgress?: number; + totalActiveSeconds: number; + sessionCount: number; + status: string; + firstOpenedAt?: string; + lastOpenedAt?: string; + lastReadAt?: string; + isMarkedRead: boolean; + markedReadAt?: string; + createdAt: string; +} + +// ── Daily Learning Activity ── +export interface DailyLearningActivity { + id: string; + userId: string; + activityDate: string; + durationSeconds: number; + sessionsCount: number; + activeRecallCount: number; + reviewCount: number; + aiAnalysisCount: number; + completedLoopCount: number; + activityLevel: number; + readingSeconds: number; + materialsReadCount: number; + markedReadCount: number; + createdAt: string; +} + +// ── Learning Record ── +export interface LearningRecord { + id: string; + userId: string; + sessionId?: string; + recordType: string; + title: string; + description?: string; + durationSeconds: number; + occurredAt: string; + createdAt: string; +} + +// ── Dashboard ── +export interface AdminDashboard { + overview: { totalEvents: number; todayEvents: number; failedEvents: number; duplicateEvents: number }; + sessions: { active: number; interrupted: number; completed: number; total: number }; + users: { activeToday: number; totalWithEvents: number }; + materials: { totalRead: number; totalMarkedRead: number }; +} + +// ── Anomalies ── +export interface AnomalyData { + failed: { items: ReadingEvent[]; total: number }; + duplicates: { items: ReadingEvent[]; total: number }; + outliers: { items: ReadingEvent[]; note: string }; + stuckSessions: LearningSession[]; + page: number; + limit: number; +} + +// ── Timeline ── +export interface UserTimeline { + events: { eventId: string; eventType: string; materialId: string; clientTimestampMs: number; status: string }[]; + sessions: { id: string; mode: string; status: string; startedAt: string; endedAt?: string; totalActiveSeconds: number }[]; +} + +// ── Diagnose ── +export interface UserDiagnose { + userId: string; + totalEvents: number; + recentSessions: { id: string; status: string; startedAt: string; totalActiveSeconds: number }[]; + progress: { materialId: string; status: string; totalActiveSeconds: number; isMarkedRead: boolean }[]; + dailyActivity: DailyLearningActivity[]; +} + +export interface MaterialDiagnose { + materialId: string; + events: ReadingEvent[]; + sessions: LearningSession[]; + progress: MaterialReadingProgress | null; +} + +// ── Export ── +export interface ExportResult { + exported: number; + total: number; + truncated: boolean; + format: string; + data: ReadingEvent[]; +}