- 新增审计报告 docs/architecture/audit/announcements-messages-audit-report.md - 新增中英双语 i18n 字典 announcements.json / messages.json(11/13 个命名空间) - 重构所有 announcements 和 messaging 组件接入 next-intl(useTranslations) - 所有页面 page.tsx 使用 generateMetadata + getTranslations 替代硬编码 metadata - 新增 7 个 error.tsx 错误边界(4 公告 + 3 消息),统一 EmptyState + i18n + 重试 - a11y 改进:announcement-card / message-list / notification-dropdown 添加 aria-label - 同步架构图 004 和 005:i18n.messages 清单 + 已知问题修复记录
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}
|
||
detailHrefBuilder={(id) => `/announcements/${id}`}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|