feat: Admin 管理后台学习功能 - 路由、菜单、类型定义
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 12s

- App.tsx: 主应用更新
- menu.tsx: 菜单配置新增学习模块
- routes/index.tsx: 路由注册
- types/learning.ts: 学习相关类型定义

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-18 11:23:01 +08:00
parent 6ad24aabe4
commit e69930f7f4
4 changed files with 153 additions and 0 deletions

View File

@ -44,6 +44,8 @@ const ReviewAdmin = lazy(() => import('./pages/ReviewAdmin'))
const NotificationAdmin = lazy(() => import('./pages/NotificationAdmin')) const NotificationAdmin = lazy(() => import('./pages/NotificationAdmin'))
const CacheAdmin = lazy(() => import('./pages/CacheAdmin')) const CacheAdmin = lazy(() => import('./pages/CacheAdmin'))
const LearningData = lazy(() => import('./pages/LearningData')) 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 BackupAdmin = lazy(() => import('./pages/BackupAdmin'))
const ReportingAdmin = lazy(() => import('./pages/ReportingAdmin')) const ReportingAdmin = lazy(() => import('./pages/ReportingAdmin'))
@ -182,6 +184,8 @@ function App() {
/> />
<Route path="reviews" element={<Suspense fallback={<PageLoading />}><ReviewAdmin /></Suspense>} /> <Route path="reviews" element={<Suspense fallback={<PageLoading />}><ReviewAdmin /></Suspense>} />
<Route path="learning-data" element={<Suspense fallback={<PageLoading />}><LearningData /></Suspense>} /> <Route path="learning-data" element={<Suspense fallback={<PageLoading />}><LearningData /></Suspense>} />
<Route path="learning" element={<Suspense fallback={<PageLoading />}><LearningDashboard /></Suspense>} />
<Route path="learning/replay" element={<Suspense fallback={<PageLoading />}><LearningReplay /></Suspense>} />
<Route path="backup" element={<Suspense fallback={<PageLoading />}><BackupAdmin /></Suspense>} /> <Route path="backup" element={<Suspense fallback={<PageLoading />}><BackupAdmin /></Suspense>} />
<Route path="reporting" element={<Suspense fallback={<PageLoading />}><ReportingAdmin /></Suspense>} /> <Route path="reporting" element={<Suspense fallback={<PageLoading />}><ReportingAdmin /></Suspense>} />
<Route path="release" element={<Suspense fallback={<PageLoading />}><ReleaseAdmin /></Suspense>} /> <Route path="release" element={<Suspense fallback={<PageLoading />}><ReleaseAdmin /></Suspense>} />

View File

@ -47,6 +47,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/learning/anomalies', name: '异常数据' }, { path: '/learning/anomalies', name: '异常数据' },
{ path: '/learning/user-diagnose', name: '用户诊断' }, { path: '/learning/user-diagnose', name: '用户诊断' },
{ path: '/learning/material-diagnose', name: '资料诊断' }, { path: '/learning/material-diagnose', name: '资料诊断' },
{ path: '/learning/replay', name: '事件重放' },
], ],
}, },

View File

@ -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 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 })))
const LearningReplay = lazy(() => import('@/pages/learning/ReplayPage'))
export interface RouteConfig { export interface RouteConfig {
path: string path: string
@ -47,4 +48,5 @@ export const routeConfig: RouteConfig[] = [
{ 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 },
{ path: '/learning/replay', title: '事件重放', element: LearningReplay },
] ]

146
src/types/learning.ts Normal file
View File

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