Files
NextEdu/src/app/(dashboard)/announcements/page.tsx
SpecialX fde711ce46 feat(announcements,messaging): 公告与消息模块审计重构 — i18n + Error Boundary + a11y
- 新增审计报告 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 清单 + 已知问题修复记录
2026-06-22 16:02:07 +08:00

105 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}