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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user