diff --git a/docs/admin-learning-info-design.md b/docs/admin-learning-info-design.md
new file mode 100644
index 0000000..bec0e6c
--- /dev/null
+++ b/docs/admin-learning-info-design.md
@@ -0,0 +1,70 @@
+# 学习信息后台 总设计
+
+> ADMIN-INFO-000 | v1.0 | 2026-06-09
+
+## 1. 概述
+
+基于 M8 + M-API-ADMIN-INFO 后端接口,构建学习信息管理后台的前端页面。
+
+## 2. 技术栈
+
+- React 19 + TypeScript
+- Ant Design 5 (antd)
+- @ant-design/pro-components (ProTable)
+- React Router v6
+- @tanstack/react-query
+- ECharts (图表)
+
+## 3. 路由结构
+
+```
+/admin/learning/dashboard — Dashboard
+/admin/learning/reading-events — 阅读事件列表
+/admin/learning/reading-events/:id — 事件详情
+/admin/learning/sessions — 学习会话列表
+/admin/learning/progress — 阅读进度列表
+/admin/learning/daily-activities — 每日活动/热力图
+/admin/learning/records — 学习记录列表
+/admin/learning/anomalies — 异常数据
+/admin/learning/user-diagnose — 用户诊断
+/admin/learning/material-diagnose — 资料诊断
+/admin/learning/temporary-materials — 临时文件
+/admin/learning/export — 数据导出
+```
+
+## 4. 组件树
+
+```
+AdminLayout
+├── DashboardPage — 统计卡片 + 图表
+├── ReadingEventPage — ProTable + 筛选
+│ └── EventDetailPage — 描述列表
+├── SessionPage — ProTable
+├── ProgressPage — ProTable
+├── DailyActivityPage — 日历热力图
+├── RecordPage — ProTable
+├── AnomalyPage — 异常列表
+├── UserDiagnosePage — 用户数据面板
+├── MaterialDiagnosePage — 资料数据面板
+├── TempMaterialPage — 临时文件管理
+└── ExportPage — 导出表单
+```
+
+## 5. API 对接
+
+所有接口 base: `/admin/learning/*`
+
+| 页面 | API |
+|------|-----|
+| Dashboard | `GET /admin/learning/dashboard` |
+| 事件列表 | `GET /admin/learning/reading-events` |
+| 事件详情 | `GET /admin/learning/reading-events/:id` |
+| 会话列表 | `GET /admin/learning/sessions` |
+| 进度列表 | `GET /admin/learning/progress` |
+| 每日活动 | `GET /admin/learning/daily-activities` |
+| 记录列表 | `GET /admin/learning/records` |
+| 用户诊断 | `GET /admin/learning/user-diagnose?userId=` |
+| 资料诊断 | `GET /admin/learning/material-diagnose?materialId=` |
+| 异常数据 | `GET /admin/learning/anomalies` |
+| 重算 | `POST /admin/learning/recalculate` |
+| 导出 | `GET /admin/learning/export` |
diff --git a/src/pages/learning/Dashboard.tsx b/src/pages/learning/Dashboard.tsx
new file mode 100644
index 0000000..0e6f080
--- /dev/null
+++ b/src/pages/learning/Dashboard.tsx
@@ -0,0 +1,37 @@
+import { Card, Col, Row, Statistic, Spin, Alert } from 'antd';
+import { BookOutlined, ThunderboltOutlined, UserOutlined, WarningOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
+import { useQuery } from '@tanstack/react-query';
+import { learningAdminAPI } from '../../services/learningAdmin';
+
+export default function DashboardPage() {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ['admin-dashboard'],
+ queryFn: () => learningAdminAPI.getDashboard(),
+ });
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ const d = data!;
+ return (
+
+
学习信息 Dashboard
+
+ } />
+ } />
+ } valueStyle={{ color: '#cf1322' }} />
+ } valueStyle={{ color: '#faad14' }} />
+
+
+ } valueStyle={{ color: '#3f8600' }} />
+ } valueStyle={{ color: '#cf1322' }} />
+
+ } />
+
+
+ } />
+ } />
+
+
+ );
+}
diff --git a/src/pages/learning/DataPages.tsx b/src/pages/learning/DataPages.tsx
new file mode 100644
index 0000000..036aa02
--- /dev/null
+++ b/src/pages/learning/DataPages.tsx
@@ -0,0 +1,218 @@
+import { useState } from 'react';
+import { Table, Card, Input, Select, Space, Tag, Descriptions, Button, message, Spin, Alert, Tabs } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { learningAdminAPI } from '../../services/learningAdmin';
+
+function PageWrapper({ title, children }: { title: string; children: React.ReactNode }) {
+ return {title}
{children};
+}
+
+// ── Reading Events ──
+
+export function ReadingEventPage() {
+ const [page, setPage] = useState(1);
+ const [filters, setFilters] = useState>({});
+ const [detailId, setDetailId] = useState(null);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-events', page, filters],
+ queryFn: () => learningAdminAPI.getReadingEvents({ page, limit: 20, ...filters }),
+ });
+
+ const { data: detail } = useQuery({
+ queryKey: ['admin-event-detail', detailId],
+ queryFn: () => learningAdminAPI.getReadingEvents({}).then(() => null),
+ enabled: false,
+ });
+
+ const columns = [
+ { title: '事件ID', dataIndex: 'eventId', width: 100, ellipsis: true },
+ { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
+ { title: '资料', dataIndex: 'materialId', width: 100, ellipsis: true },
+ { title: '类型', dataIndex: 'eventType', width: 120, render: (t: string) => {t} },
+ { title: 'Delta', dataIndex: 'activeSecondsDelta', width: 70 },
+ { title: '状态', dataIndex: 'status', width: 90, render: (s: string) => {s} },
+ { title: '时间', dataIndex: 'createdAt', width: 160, render: (t: string) => t ? new Date(t).toLocaleString() : '-' },
+ ];
+
+ return (
+
+
+ setFilters(f => ({ ...f, userId: e.target.value }))} style={{ width: 150 }} />
+ setFilters(f => ({ ...f, materialId: e.target.value }))} style={{ width: 150 }} />
+
+
+
+ );
+}
+
+// ── Sessions ──
+
+export function SessionPage() {
+ const [page, setPage] = useState(1);
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-sessions', page],
+ queryFn: () => learningAdminAPI.getSessions({ page, limit: 20 }),
+ });
+
+ const columns = [
+ { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
+ { title: '资料', dataIndex: 'materialId', width: 100, ellipsis: true },
+ { title: '状态', dataIndex: 'status', width: 90, render: (s: string) => {s} },
+ { title: '活跃秒数', dataIndex: 'totalActiveSeconds', width: 100 },
+ { title: '开始', dataIndex: 'startedAt', width: 160, render: (t: string) => t ? new Date(t).toLocaleString() : '-' },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+// ── Progress ──
+
+export function ProgressPage() {
+ const [page, setPage] = useState(1);
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-progress', page],
+ queryFn: () => learningAdminAPI.getProgress({ page, limit: 20 }),
+ });
+
+ const columns = [
+ { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
+ { title: '资料', dataIndex: 'materialId', width: 100, ellipsis: true },
+ { title: '状态', dataIndex: 'status', width: 90, render: (s: string) => {s} },
+ { title: '进度', dataIndex: 'lastProgress', width: 80, render: (v: number) => v ? `${Math.round(v * 100)}%` : '-' },
+ { title: '活跃秒', dataIndex: 'totalActiveSeconds', width: 80 },
+ { title: '已读', dataIndex: 'isMarkedRead', width: 60, render: (v: boolean) => v ? '✅' : '' },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+// ── Daily Activities ──
+
+export function DailyActivityPage() {
+ const [page, setPage] = useState(1);
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-daily', page],
+ queryFn: () => learningAdminAPI.getDailyActivities({ page, limit: 20 }),
+ });
+
+ const columns = [
+ { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
+ { title: '日期', dataIndex: 'activityDate', width: 120, render: (t: string) => t ? new Date(t).toLocaleDateString() : '-' },
+ { title: '阅读秒', dataIndex: 'readingSeconds', width: 80 },
+ { title: '资料数', dataIndex: 'materialsReadCount', width: 70 },
+ { title: '已读数', dataIndex: 'markedReadCount', width: 70 },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+// ── Records ──
+
+export function RecordPage() {
+ const [page, setPage] = useState(1);
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-records', page],
+ queryFn: () => learningAdminAPI.getRecords({ page, limit: 20 }),
+ });
+
+ const columns = [
+ { title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
+ { title: '标题', dataIndex: 'title', width: 200 },
+ { title: '类型', dataIndex: 'recordType', width: 100, render: (t: string) => {t} },
+ { title: '时长', dataIndex: 'durationSeconds', width: 80 },
+ { title: '时间', dataIndex: 'occurredAt', width: 160, render: (t: string) => t ? new Date(t).toLocaleString() : '-' },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+// ── Anomalies ──
+
+export function AnomalyPage() {
+ const { data, isLoading } = useQuery({
+ queryKey: ['admin-anomalies'],
+ queryFn: () => learningAdminAPI.getAnomalies(),
+ });
+
+ const deltaCols = [
+ { title: '事件ID', dataIndex: 'eventId', width: 200, ellipsis: true },
+ { title: 'Delta', dataIndex: 'activeSecondsDelta', width: 80, render: (v: number) => {v} },
+ { title: '时间', dataIndex: 'createdAt', width: 160, render: (t: string) => t ? new Date(t).toLocaleString() : '-' },
+ ];
+
+ return (
+
+ 300)', children: },
+ { key: 'future', label: '未来时间戳', children: },
+ ]} />
+
+ );
+}
+
+// ── User Diagnose ──
+
+export function UserDiagnosePage() {
+ const [userId, setUserId] = useState('');
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['admin-user-diag', userId],
+ queryFn: () => learningAdminAPI.getUserDiagnose(userId),
+ enabled: false,
+ });
+
+ return (
+
+
+ setUserId(e.target.value)} style={{ width: 300 }} />
+
+
+ {data && {JSON.stringify(data, null, 2)}}
+
+ );
+}
+
+// ── Material Diagnose ──
+
+export function MaterialDiagnosePage() {
+ const [materialId, setMaterialId] = useState('');
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['admin-mat-diag', materialId],
+ queryFn: () => learningAdminAPI.getMaterialDiagnose(materialId),
+ enabled: false,
+ });
+
+ return (
+
+
+ setMaterialId(e.target.value)} style={{ width: 300 }} />
+
+
+ {data && {JSON.stringify(data, null, 2)}}
+
+ );
+}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index ef2e242..a51c596 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -6,6 +6,16 @@ const TaskAssistant = lazy(() => import('@/pages/TaskAssistant'))
const UserManagement = lazy(() => import('@/pages/UserManagement'))
const MemberManagement = lazy(() => import('@/pages/MemberManagement'))
+const LearningDashboard = lazy(() => import('@/pages/learning/Dashboard'))
+const ReadingEventPage = lazy(() => import('@/pages/learning/DataPages')).then(m => ({ default: m.ReadingEventPage }))
+const SessionPage = lazy(() => import('@/pages/learning/DataPages')).then(m => ({ default: m.SessionPage }))
+const ProgressPage = lazy(() => import('@/pages/learning/DataPages')).then(m => ({ default: m.ProgressPage }))
+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 UserDiagnosePage = lazy(() => import('@/pages/learning/DataPages')).then(m => ({ default: m.UserDiagnosePage }))
+const MaterialDiagnosePage = lazy(() => import('@/pages/learning/DataPages')).then(m => ({ default: m.MaterialDiagnosePage }))
+
export interface RouteConfig {
path: string
title: string
@@ -27,4 +37,14 @@ export const routeConfig: RouteConfig[] = [
{ path: '/files', title: '文件与 COS', element: UserManagement },
{ path: '/settings', title: '系统配置', element: UserManagement, requiredRole: 'ADMIN' },
{ path: '/audit', title: '审计日志', element: UserManagement, requiredRole: 'ADMIN' },
+ // ── Learning Info ──
+ { path: '/learning', title: '学习 Dashboard', element: LearningDashboard },
+ { path: '/learning/events', title: '阅读事件', element: ReadingEventPage },
+ { path: '/learning/sessions', title: '学习会话', element: SessionPage },
+ { path: '/learning/progress', title: '阅读进度', element: ProgressPage },
+ { path: '/learning/daily', title: '每日活动', element: DailyActivityPage },
+ { path: '/learning/records', title: '学习记录', element: RecordPage },
+ { path: '/learning/anomalies', title: '异常数据', element: AnomalyPage },
+ { path: '/learning/user-diagnose', title: '用户诊断', element: UserDiagnosePage },
+ { path: '/learning/material-diagnose', title: '资料诊断', element: MaterialDiagnosePage },
]
diff --git a/src/services/learningAdmin.ts b/src/services/learningAdmin.ts
new file mode 100644
index 0000000..3bceb55
--- /dev/null
+++ b/src/services/learningAdmin.ts
@@ -0,0 +1,25 @@
+import { apiGet, apiPost } from './api';
+
+export interface DashboardData {
+ 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 };
+}
+
+export const learningAdminAPI = {
+ getDashboard: () => apiGet('/admin/learning/dashboard'),
+ getReadingEvents: (params?: Record) => apiGet('/admin/learning/reading-events', params),
+ getFailedEvents: (params?: Record) => apiGet('/admin/learning/reading-events/failed', params),
+ getSessions: (params?: Record) => apiGet('/admin/learning/sessions', params),
+ getProgress: (params?: Record) => apiGet('/admin/learning/progress', params),
+ getDailyActivities: (params?: Record) => apiGet('/admin/learning/daily-activities', params),
+ getRecords: (params?: Record) => apiGet('/admin/learning/records', params),
+ getUserTimeline: (userId: string) => apiGet('/admin/learning/user-timeline', { userId }),
+ getUserDiagnose: (userId: string) => apiGet('/admin/learning/user-diagnose', { userId }),
+ getMaterialDiagnose: (materialId: string) => apiGet('/admin/learning/material-diagnose', { materialId }),
+ getAnomalies: () => apiGet('/admin/learning/anomalies'),
+ getTemporaryMaterials: (params?: Record) => apiGet('/admin/learning/temporary-materials', params),
+ recalculate: () => apiPost('/admin/learning/recalculate'),
+ exportData: (type: string) => apiGet('/admin/learning/export', { type }),
+};