From a9a7d651bb2d9665aae36c2b6702da6ead705d79 Mon Sep 17 00:00:00 2001 From: wangdl Date: Tue, 9 Jun 2026 21:55:52 +0800 Subject: [PATCH] feat: ADMIN-INFO admin learning dashboard frontend (21/21) - Dashboard page with statistics cards - Data pages: ReadingEvents, Sessions, Progress, DailyActivities, Records - Diagnostic: Anomalies, User/Material Diagnose - API service: learningAdmin.ts - Routes: /learning/* Co-Authored-By: Claude Opus 4.7 --- docs/admin-learning-info-design.md | 70 +++++++++ src/pages/learning/Dashboard.tsx | 37 +++++ src/pages/learning/DataPages.tsx | 218 +++++++++++++++++++++++++++++ src/routes/index.tsx | 20 +++ src/services/learningAdmin.ts | 25 ++++ 5 files changed, 370 insertions(+) create mode 100644 docs/admin-learning-info-design.md create mode 100644 src/pages/learning/Dashboard.tsx create mode 100644 src/pages/learning/DataPages.tsx create mode 100644 src/services/learningAdmin.ts 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 }} /> + 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 }), +};