feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components

dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
This commit is contained in:
SpecialX
2026-07-03 10:25:46 +08:00
parent dfffb61e94
commit 138b6f1b00
58 changed files with 3313 additions and 695 deletions

View File

@@ -0,0 +1,88 @@
import "server-only"
import { cache } from "react"
import { count, eq, sql } from "drizzle-orm"
import { db } from "@/shared/db"
import { courseSelections, electiveCourses } from "@/shared/db/schema"
/**
* 选课模块管理员概览统计P1-13 新增)。
* 用于 admin/teacher 列表页顶部展示关键指标。
*/
export interface ElectiveOverviewStats {
/** 课程总数 */
totalCourses: number
/** 总选课人数(已录取 + 候补 + 已选) */
totalEnrolled: number
/** 平均容量使用率百分比0-100 */
avgUtilization: number
/** 待抽签的课程数selectionMode=lottery 且 status=open 且存在 selected 记录) */
pendingLottery: number
}
/**
* 获取选修课全局概览统计admin 视角)。
*
* 实现要点:
* - 4 个独立查询合并为 3 个 SQLpendingLottery 需 join避免 N+1
* - 使用 SQL 聚合而非拉全表后 reduce避免大数据量内存峰值
* - admin 不做 scope 过滤(统计全部课程)
*/
export const getElectiveOverviewStats = cache(
async (): Promise<ElectiveOverviewStats> => {
// 并行执行聚合查询
const [totalRow, enrolledRow, utilizationRow, pendingRow] = await Promise.all([
// 1. 课程总数
db
.select({ total: count() })
.from(electiveCourses),
// 2. 总选课人数(活跃选课记录数)
db
.select({ total: count() })
.from(courseSelections)
.where(
sql`${courseSelections.status} IN ('selected', 'enrolled', 'waitlist')`
),
// 3. 平均容量使用率capacity > 0 时计算 enrolledCount/capacity 平均值)
db
.select({
avg: sql<number>`COALESCE(
AVG(
CASE
WHEN ${electiveCourses.capacity} > 0
THEN ${electiveCourses.enrolledCount}::float / ${electiveCourses.capacity}
ELSE 0
END
) * 100,
0
)`,
})
.from(electiveCourses),
// 4. 待抽签课程数lottery 模式且 status=open 且有 selected 状态的选课记录
db
.select({ total: sql<number>`count(distinct ${electiveCourses.id})` })
.from(electiveCourses)
.innerJoin(
courseSelections,
eq(courseSelections.courseId, electiveCourses.id)
)
.where(
sql`${electiveCourses.selectionMode} = 'lottery'
AND ${electiveCourses.status} = 'open'
AND ${courseSelections.status} = 'selected'`
),
])
return {
totalCourses: totalRow[0]?.total ?? 0,
totalEnrolled: enrolledRow[0]?.total ?? 0,
avgUtilization: Math.round(Number(utilizationRow[0]?.avg ?? 0)),
pendingLottery: pendingRow[0]?.total ?? 0,
}
}
)