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 清单 + 已知问题修复记录
This commit is contained in:
SpecialX
2026-06-22 16:02:07 +08:00
parent 21c1e7a286
commit fde711ce46
30 changed files with 1085 additions and 261 deletions

View File

@@ -1,32 +1,13 @@
"use client"
import Link from "next/link"
import { useTranslations } from "next-intl"
import { Badge } from "@/shared/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { formatDate } from "@/shared/lib/utils"
import type { Announcement } from "../types"
const STATUS_LABEL: Record<Announcement["status"], string> = {
draft: "Draft",
published: "Published",
archived: "Archived",
}
const STATUS_VARIANT: Record<
Announcement["status"],
"default" | "secondary" | "outline"
> = {
draft: "secondary",
published: "default",
archived: "outline",
}
const TYPE_LABEL: Record<Announcement["type"], string> = {
school: "School",
grade: "Grade",
class: "Class",
}
export function AnnouncementCard({
announcement,
href,
@@ -34,12 +15,20 @@ export function AnnouncementCard({
announcement: Announcement
href?: string
}) {
const t = useTranslations("announcements")
const statusVariant: Record<Announcement["status"], "default" | "secondary" | "outline"> = {
draft: "secondary",
published: "default",
archived: "outline",
}
const card = (
<Card className="h-full transition-colors hover:bg-accent/50">
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0">
<CardTitle className="line-clamp-2 text-base">{announcement.title}</CardTitle>
<Badge variant={STATUS_VARIANT[announcement.status]} className="shrink-0">
{STATUS_LABEL[announcement.status]}
<Badge variant={statusVariant[announcement.status]} className="shrink-0">
{t(`status.${announcement.status}`)}
</Badge>
</CardHeader>
<CardContent className="space-y-2">
@@ -48,15 +37,15 @@ export function AnnouncementCard({
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="capitalize">
{TYPE_LABEL[announcement.type]}
{t(`type.${announcement.type}`)}
</Badge>
<span>
{announcement.publishedAt
? `Published ${formatDate(announcement.publishedAt)}`
: `Updated ${formatDate(announcement.updatedAt)}`}
? t("meta.publishedAt", { date: formatDate(announcement.publishedAt) })
: t("meta.updatedAt", { date: formatDate(announcement.updatedAt) })}
</span>
{announcement.authorName ? (
<span className="ml-auto">by {announcement.authorName}</span>
<span className="ml-auto">{t("meta.author", { name: announcement.authorName })}</span>
) : null}
</div>
</CardContent>
@@ -65,7 +54,7 @@ export function AnnouncementCard({
if (href) {
return (
<Link href={href} className="block h-full">
<Link href={href} className="block h-full" aria-label={announcement.title}>
{card}
</Link>
)