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[];
+}