feat(app): add error/loading boundaries across all dashboard routes and new routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes - Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes - Add admin elective detail and components, files, course-plans, users, scheduling boundaries - Add messages group-compose route - Add parent course-plans, elective, grades report-card, practice routes - Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries - Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar - Add auth loading, onboarding loading, api cron
This commit is contained in:
@@ -3,102 +3,68 @@ 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 { getUserAnnouncementsPageData } from "@/modules/announcements/data-access"
|
||||
import { AnnouncementList } from "@/modules/announcements/components/announcement-list"
|
||||
import {
|
||||
getStudentActiveClassId,
|
||||
getStudentActiveGradeId,
|
||||
getClassGradeId,
|
||||
} from "@/modules/classes/data-access"
|
||||
import { AnnouncementsServiceProvider } from "@/modules/announcements/components/announcements-service-context"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("announcements")
|
||||
return { title: t("title.list") }
|
||||
return { title: t("title.list"), description: t("description.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
|
||||
const PAGE_SIZE = 12
|
||||
|
||||
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() {
|
||||
export default async function AnnouncementsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string; status?: string }>
|
||||
}) {
|
||||
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,
|
||||
})
|
||||
const sp = await searchParams
|
||||
const requestedPage = Number(sp.page)
|
||||
const page = Number.isFinite(requestedPage) && requestedPage > 0 ? Math.floor(requestedPage) : 1
|
||||
|
||||
const { items, total, page: currentPage, pageSize } = await getUserAnnouncementsPageData(
|
||||
ctx.userId,
|
||||
ctx.dataScope,
|
||||
page,
|
||||
PAGE_SIZE
|
||||
)
|
||||
|
||||
// 构建分页 URL:保留 ?status= 过滤参数(与 AnnouncementList 的过滤机制一致)
|
||||
const buildPageHref = (targetPage: number): string => {
|
||||
const params = new URLSearchParams()
|
||||
if (sp.status) params.set("status", sp.status)
|
||||
if (targetPage > 1) params.set("page", String(targetPage))
|
||||
const qs = params.toString()
|
||||
return qs ? `/announcements?${qs}` : "/announcements"
|
||||
}
|
||||
|
||||
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>
|
||||
<AnnouncementsServiceProvider>
|
||||
<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={items}
|
||||
detailHrefPrefix="/announcements"
|
||||
initialStatus={sp.status === "published" || sp.status === "draft" || sp.status === "archived" ? sp.status : "all"}
|
||||
pagination={{
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
total,
|
||||
buildPageHref,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AnnouncementList
|
||||
announcements={announcements}
|
||||
detailHrefPrefix="/announcements"
|
||||
/>
|
||||
</div>
|
||||
</AnnouncementsServiceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user