- 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
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import type { Metadata } from "next"
|
||
import { getTranslations } from "next-intl/server"
|
||
|
||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||
import { Permissions } from "@/shared/types/permissions"
|
||
import { getAnnouncements } from "@/modules/announcements/data-access"
|
||
import { AnnouncementList } from "@/modules/announcements/components/announcement-list"
|
||
import {
|
||
getStudentActiveClassId,
|
||
getStudentActiveGradeId,
|
||
getClassGradeId,
|
||
} from "@/modules/classes/data-access"
|
||
|
||
export const dynamic = "force-dynamic"
|
||
|
||
export async function generateMetadata(): Promise<Metadata> {
|
||
const t = await getTranslations("announcements")
|
||
return { title: t("title.list") }
|
||
}
|
||
|
||
/**
|
||
* 根据当前用户身份解析受众信息(gradeId / classId)。
|
||
* - admin:返回 null(管理端可见所有公告)
|
||
* - student / teacher:使用首个 classId 并查询其 gradeId
|
||
* - grade_head / teaching_head:使用首个 gradeId
|
||
* - parent:使用首个孩子的活跃班级信息
|
||
* - 其他:返回 null(仅显示 school 类型公告由 audience.gradeId/classId 均缺失时的兜底处理)
|
||
*/
|
||
async function resolveAudience(ctx: {
|
||
userId: string
|
||
dataScope:
|
||
| { type: "all" }
|
||
| { type: "owned"; userId: string }
|
||
| { type: "class_members"; classIds: string[] }
|
||
| { type: "grade_managed"; gradeIds: string[] }
|
||
| { type: "class_taught"; classIds: string[]; subjectIds?: string[] }
|
||
| { type: "children"; childrenIds: string[] }
|
||
}): Promise<{ gradeId?: string; classId?: string } | null> {
|
||
const { dataScope } = ctx
|
||
|
||
if (dataScope.type === "all") return null
|
||
|
||
if (dataScope.type === "grade_managed") {
|
||
const gradeId = dataScope.gradeIds[0]
|
||
return gradeId ? { gradeId } : null
|
||
}
|
||
|
||
if (dataScope.type === "class_members" || dataScope.type === "class_taught") {
|
||
const classId = dataScope.classIds[0]
|
||
if (!classId) return null
|
||
const gradeId = await getClassGradeId(classId)
|
||
return { classId, gradeId: gradeId ?? undefined }
|
||
}
|
||
|
||
if (dataScope.type === "children") {
|
||
const childId = dataScope.childrenIds[0]
|
||
if (!childId) return null
|
||
const [classId, gradeId] = await Promise.all([
|
||
getStudentActiveClassId(childId),
|
||
getStudentActiveGradeId(childId),
|
||
])
|
||
return {
|
||
classId: classId ?? undefined,
|
||
gradeId: gradeId ?? undefined,
|
||
}
|
||
}
|
||
|
||
// owned / 其他:尝试用当前 userId 查询(兼容 student 角色直接访问)
|
||
const [classId, gradeId] = await Promise.all([
|
||
getStudentActiveClassId(ctx.userId),
|
||
getStudentActiveGradeId(ctx.userId),
|
||
])
|
||
if (!classId && !gradeId) return null
|
||
return {
|
||
classId: classId ?? undefined,
|
||
gradeId: gradeId ?? undefined,
|
||
}
|
||
}
|
||
|
||
export default async function AnnouncementsPage() {
|
||
const t = await getTranslations("announcements")
|
||
const ctx = await requirePermission(Permissions.ANNOUNCEMENT_READ)
|
||
const audience = await resolveAudience(ctx)
|
||
|
||
const announcements = await getAnnouncements({
|
||
status: "published",
|
||
audience: audience ?? undefined,
|
||
})
|
||
|
||
return (
|
||
<div className="flex h-full flex-col space-y-8 p-8">
|
||
<div>
|
||
<h2 className="text-2xl font-bold tracking-tight">{t("title.list")}</h2>
|
||
<p className="text-muted-foreground">
|
||
{t("description.list")}
|
||
</p>
|
||
</div>
|
||
<AnnouncementList
|
||
announcements={announcements}
|
||
detailHrefPrefix="/announcements"
|
||
/>
|
||
</div>
|
||
)
|
||
}
|