feat(app): add error/loading boundaries and update dashboard routes

- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add dashboard-error-fallback and dashboard-loading-skeleton components

- Add student/learning page, parent/leave routes, teacher textbook components

- Update existing app routes across auth, dashboard, and API endpoints

- Update proxy middleware and next-auth type declarations
This commit is contained in:
SpecialX
2026-06-23 17:38:28 +08:00
parent c4d3433cc9
commit 1a9377222c
90 changed files with 1690 additions and 741 deletions

View File

@@ -1,9 +1,9 @@
"use server"
import { cache } from "react"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import type { ActionState } from "@/shared/types/action-state"
import { handleActionError } from "@/shared/lib/action-utils"
import { getClassSchedule, getStudentClasses, getStudentSchedule, getTeacherClasses, getTeacherIdForMutations } from "@/modules/classes/data-access"
import {
getHomeworkAssignments,
@@ -19,6 +19,7 @@ import { getAdminDashboardData } from "./data-access"
import type {
AdminDashboardData,
StudentDashboardProps,
StudentTodayScheduleItem,
TeacherDashboardData,
} from "./types"
import type { ParentDashboardData } from "@/modules/parent/types"
@@ -35,47 +36,59 @@ import {
* 获取管理员仪表盘数据。
* 权限DASHBOARD_ADMIN_READ
*/
export async function getAdminDashboardAction(): Promise<AdminDashboardData> {
const ctx = await requirePermission(Permissions.DASHBOARD_ADMIN_READ)
return getAdminDashboardData(ctx.dataScope)
export async function getAdminDashboardAction(): Promise<ActionState<AdminDashboardData>> {
try {
const ctx = await requirePermission(Permissions.DASHBOARD_ADMIN_READ)
const data = await getAdminDashboardData(ctx.dataScope)
return { success: true, data }
} catch (e) {
return handleActionError(e)
}
}
/**
* 获取教师仪表盘数据(含派生指标)。
* 权限DASHBOARD_TEACHER_READ
*/
export async function getTeacherDashboardAction(): Promise<TeacherDashboardData & {
export async function getTeacherDashboardAction(): Promise<ActionState<TeacherDashboardData & {
metrics: TeacherDashboardMetrics
}> {
await requirePermission(Permissions.DASHBOARD_TEACHER_READ)
const teacherId = await getTeacherIdForMutations()
}>> {
try {
await requirePermission(Permissions.DASHBOARD_TEACHER_READ)
const teacherId = await getTeacherIdForMutations()
const [classes, schedule, assignments, submissions, teacherProfile, gradeTrends] = await Promise.all([
getTeacherClasses({ teacherId }),
getClassSchedule({ teacherId }),
getHomeworkAssignments({ creatorId: teacherId }),
getHomeworkSubmissions({ creatorId: teacherId }),
getUserBasicInfo(teacherId),
getTeacherGradeTrends(teacherId),
])
const [classes, schedule, assignments, submissions, teacherProfile, gradeTrends] = await Promise.all([
getTeacherClasses({ teacherId }),
getClassSchedule({ teacherId }),
getHomeworkAssignments({ creatorId: teacherId }),
getHomeworkSubmissions({ creatorId: teacherId }),
getUserBasicInfo(teacherId),
getTeacherGradeTrends(teacherId),
])
const metrics = computeTeacherMetrics(
classes,
schedule,
assignments,
submissions,
gradeTrends,
new Date(),
)
const metrics = computeTeacherMetrics(
classes,
schedule,
assignments,
submissions,
gradeTrends,
new Date(),
)
return {
classes,
schedule,
assignments,
submissions,
teacherName: teacherProfile?.name ?? "Teacher",
gradeTrends,
metrics,
return {
success: true,
data: {
classes,
schedule,
assignments,
submissions,
teacherName: teacherProfile?.name ?? "Teacher",
gradeTrends,
metrics,
},
}
} catch (e) {
return handleActionError(e)
}
}
@@ -83,40 +96,47 @@ export async function getTeacherDashboardAction(): Promise<TeacherDashboardData
* 获取学生仪表盘数据(含派生指标)。
* 权限DASHBOARD_STUDENT_READ
*/
export async function getStudentDashboardAction(): Promise<{
export async function getStudentDashboardAction(): Promise<ActionState<{
student: { id: string; name: string } | null
dashboardProps: Omit<StudentDashboardProps, "studentName"> | null
}> {
await requirePermission(Permissions.DASHBOARD_STUDENT_READ)
const student = await getCurrentStudentUser()
if (!student) {
return { student: null, dashboardProps: null }
}
}>> {
try {
await requirePermission(Permissions.DASHBOARD_STUDENT_READ)
const student = await getCurrentStudentUser()
if (!student) {
return { success: true, data: { student: null, dashboardProps: null } }
}
const [classes, schedule, assignments, grades] = await Promise.all([
getStudentClasses(student.id),
getStudentSchedule(student.id),
getStudentHomeworkAssignments(student.id),
getStudentDashboardGrades(student.id),
])
const [classes, schedule, assignments, grades] = await Promise.all([
getStudentClasses(student.id),
getStudentSchedule(student.id),
getStudentHomeworkAssignments(student.id),
getStudentDashboardGrades(student.id),
])
const now = new Date()
const stats = countStudentAssignments(assignments, now)
const todayWeekday = toWeekday(now)
const todayScheduleItems = filterTodaySchedule(schedule, todayWeekday)
const upcomingAssignments = sortUpcomingAssignments(assignments, 6)
const now = new Date()
const stats = countStudentAssignments(assignments, now)
const todayWeekday = toWeekday(now)
const todayScheduleItems = filterTodaySchedule<StudentTodayScheduleItem>(schedule, todayWeekday)
const upcomingAssignments = sortUpcomingAssignments(assignments, 6)
return {
student: { id: student.id, name: student.name },
dashboardProps: {
enrolledClassCount: classes.length,
dueSoonCount: stats.dueSoonCount,
overdueCount: stats.overdueCount,
gradedCount: stats.gradedCount,
todayScheduleItems,
upcomingAssignments,
grades,
},
return {
success: true,
data: {
student: { id: student.id, name: student.name },
dashboardProps: {
enrolledClassCount: classes.length,
dueSoonCount: stats.dueSoonCount,
overdueCount: stats.overdueCount,
gradedCount: stats.gradedCount,
todayScheduleItems,
upcomingAssignments,
grades,
},
},
}
} catch (e) {
return handleActionError(e)
}
}
@@ -124,23 +144,25 @@ export async function getStudentDashboardAction(): Promise<{
* 获取家长仪表盘数据。
* 权限DASHBOARD_PARENT_READ
*/
export async function getParentDashboardAction(): Promise<{
export async function getParentDashboardAction(): Promise<ActionState<{
data: ParentDashboardData | null
hasChildren: boolean
}> {
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
}>> {
try {
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
// 非 admin 且 dataScope 非 children 类型时,无孩子数据
if (
ctx.dataScope.type !== "all" &&
!(ctx.dataScope.type === "children" && ctx.dataScope.childrenIds.length > 0)
) {
return { data: null, hasChildren: false }
// 非 admin 且 dataScope 非 children 类型时,无孩子数据
if (
ctx.dataScope.type !== "all" &&
!(ctx.dataScope.type === "children" && ctx.dataScope.childrenIds.length > 0)
) {
return { success: true, data: { data: null, hasChildren: false } }
}
const data = await getParentDashboardData(ctx.userId)
return { success: true, data: { data, hasChildren: data.children.length > 0 } }
} catch (e) {
return handleActionError(e)
}
const data = await getParentDashboardData(ctx.userId)
return { data, hasChildren: data.children.length > 0 }
}
/** 缓存版本(用于 RSC 直接调用,不走 Server Action 协议) */
export const getCachedAdminDashboard = cache(getAdminDashboardAction)