feat(web): migrate statistics analytics workflows

This commit is contained in:
biss committed 2026-10-04 08:54:15 +08:00
1 parent 069e2b300b
commit d3ff37516b
7 files changed
+205 -2

No files matched your search

+2
View File
@@ -57,6 +57,8 @@
| `/classroom-repairs` | 教室报修与维修 | SuperAdmin、AcademicAdmin、MaintenanceAdmin、MaintenanceStaff | 工单筛选、分派/开工/完工、详情、设备档案与扫码报修二维码 | verification |
| `/grades/:sheetId/statistics` | 课程成绩统计 | 具备该成绩单数据范围的已登录用户 | 班级/专业/学院/全校分数分布、对比、课程讨论与互动 | verification |
| `/event-analytics` | 运行数据分析 | SuperAdmin、AcademicAdmin、CollegeAdmin、Leader | ClickHouse 考勤、成绩与全校范围访问审计只读聚合 | verification |
| `/statistics` | 统计报表 | SuperAdmin、AcademicAdmin、CollegeAdmin、Leader | 学生、课程、成绩、通过率、教师工作量和教室利用率;筛选、可视化和 Excel 导出 | verification |
| `/grade-analytics` | 成绩分析中心 | SuperAdmin、AcademicAdmin、CollegeAdmin、Leader、Teacher | 教学班/课程成绩比较、刷新设置、Word 报告与课程讨论 | verification |
| `/plugins` | 统一插件平台 | SuperAdmin | 运行状态、签名包暂存/激活/回滚、无数据删除的安全停用 | verification |
| `/activate` | 学生账号激活 | 公开 | 学籍选项、身份匹配、强密码和一次性激活 | verification |
| `/verify/:verificationCode?` | 电子凭证验真 | 公开 | 唯一编号、实时状态、签发信息和 PDF 哈希 | verification |
+14 -2
View File
@@ -1,9 +1,10 @@
import { lazy, Suspense, useEffect } from 'react'
import { lazy, Suspense, useEffect, type ReactNode } from 'react'
import { App as AntdApp, ConfigProvider, Skeleton } from 'antd'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import zhCN from 'antd/locale/zh_CN'
import { useAuthStore } from '../stores/auth'
import http from '../api/http'
const LoginPage = lazy(() => import('../pages/LoginPage'))
const DashboardPage = lazy(() => import('../pages/DashboardPage'))
@@ -57,6 +58,8 @@ const CourseGradeStatisticsPage = lazy(() => import('../pages/CourseGradeStatist
const EventAnalyticsPage = lazy(() => import('../pages/EventAnalyticsPage'))
const PluginsPage = lazy(() => import('../pages/PluginsPage'))
const OperationsPage = lazy(() => import('../pages/OperationsPage'))
const StatisticsPage = lazy(() => import('../pages/StatisticsPage'))
const GradeAnalyticsPage = lazy(() => import('../pages/GradeAnalyticsPage'))
const AccountActivationPage = lazy(() => import('../pages/AccountActivationPage'))
const OfficialDocumentVerifyPage = lazy(() => import('../pages/OfficialDocumentVerifyPage'))
const ClassroomRepairReportPage = lazy(() => import('../pages/ClassroomRepairReportPage'))
@@ -92,6 +95,13 @@ function SessionExpiryBridge() {
return null
}
function PluginRoute({ pluginId, children }: { pluginId: string; children: ReactNode }) {
const runtime = useQuery({ queryKey: ['plugin-runtime'], queryFn: async () => (await http.get<{ id: string; isEnabled: boolean }[]>('/plugins/runtime')).data })
if (runtime.isLoading) return <div style={{ maxWidth: 1180, margin: '64px auto', padding: 24 }}><Skeleton active paragraph={{ rows: 5 }} /></div>
if (runtime.data?.some((item) => item.id === pluginId && !item.isEnabled)) return <Navigate to="/dashboard" replace />
return <>{children}</>
}
function AppRoutes() {
return (
<Suspense fallback={<div style={{ maxWidth: 1180, margin: '64px auto', padding: 24 }}><Skeleton active paragraph={{ rows: 8 }} /></div>}>
@@ -162,6 +172,8 @@ function AppRoutes() {
<Route path="/event-analytics" element={<EventAnalyticsPage />} />
<Route path="/plugins" element={<PluginsPage />} />
<Route path="/operations" element={<OperationsPage />} />
<Route path="/statistics" element={<PluginRoute pluginId="mingxu.statistics"><StatisticsPage /></PluginRoute>} />
<Route path="/grade-analytics" element={<PluginRoute pluginId="mingxu.statistics"><GradeAnalyticsPage /></PluginRoute>} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
+9
View File
@@ -37,6 +37,7 @@ import {
CarryOutOutlined,
SettingOutlined,
ToolOutlined,
BarChartOutlined,
} from '@ant-design/icons'
import { Avatar, Badge, Button, Layout, Menu, Tag } from 'antd'
import { useQuery } from '@tanstack/react-query'
@@ -63,6 +64,8 @@ export default function AppShell() {
queryFn: async () => (await http.get<{ count: number }>('/notifications/unread-count')).data.count,
refetchInterval: 60_000,
})
const pluginRuntime = useQuery({ queryKey: ['plugin-runtime'], queryFn: async () => (await http.get<{ id: string; isEnabled: boolean }[]>('/plugins/runtime')).data })
const statisticsEnabled = pluginRuntime.data?.find((item) => item.id === 'mingxu.statistics')?.isEnabled !== false
useEffect(() => { void refreshUser().catch(() => undefined) }, [refreshUser])
@@ -122,6 +125,12 @@ export default function AppShell() {
{ key: '/course-enrollments', icon: <BookOutlined />, label: '修读课程管理' },
{ key: '/campus-wallets', icon: <WalletOutlined />, label: '学生余额管理' },
] : []),
...(statisticsEnabled && roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(role)) ? [
{ key: '/statistics', icon: <BarChartOutlined />, label: '统计报表' },
] : []),
...(statisticsEnabled && roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader', 'Teacher'].includes(role)) ? [
{ key: '/grade-analytics', icon: <LineChartOutlined />, label: roles.includes('Teacher') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(role)) ? '教学班成绩分析' : '成绩分析中心' },
] : []),
...(roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ? [
{ key: '/venue-displays', icon: <FundProjectionScreenOutlined />, label: '场地信息展牌' },
{ key: '/base-data/organization', icon: <ApartmentOutlined />, label: '组织机构' },
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff. Show more