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

@@ -3,6 +3,7 @@
import { useEffect, useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { Bell, CheckCheck, MessageSquare, Megaphone, PenTool, GraduationCap } from "lucide-react"
import { Badge } from "@/shared/components/ui/badge"
@@ -34,6 +35,7 @@ const TYPE_ICON: Record<NotificationType, typeof Bell> = {
}
export function NotificationDropdown() {
const t = useTranslations("messages")
const router = useRouter()
const [notifications, setNotifications] = useState<Notification[]>([])
const [unreadCount, setUnreadCount] = useState(0)
@@ -96,8 +98,8 @@ export function NotificationDropdown() {
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative text-muted-foreground">
<Bell className="size-5" />
<Button variant="ghost" size="icon" className="relative text-muted-foreground" aria-label={t("title.notifications")}>
<Bell className="size-5" aria-hidden="true" />
{unreadCount > 0 ? (
<Badge
variant="destructive"
@@ -106,20 +108,20 @@ export function NotificationDropdown() {
{unreadCount > 9 ? "9+" : unreadCount}
</Badge>
) : null}
<span className="sr-only">Notifications</span>
<span className="sr-only">{t("title.notifications")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 p-0">
<DropdownMenuLabel className="flex items-center justify-between">
<span>Notifications</span>
<span>{t("title.notifications")}</span>
{unreadCount > 0 ? (
<button
type="button"
onClick={handleMarkAllRead}
className="text-primary text-xs hover:underline"
>
<CheckCheck className="mr-1 inline h-3 w-3" />
Mark all read
<CheckCheck className="mr-1 inline h-3 w-3" aria-hidden="true" />
{t("actions.markAllRead")}
</button>
) : null}
</DropdownMenuLabel>
@@ -127,7 +129,7 @@ export function NotificationDropdown() {
<ScrollArea className="max-h-[320px]">
{notifications.length === 0 ? (
<div className="text-muted-foreground px-4 py-8 text-center text-sm">
No notifications
{t("empty.noNotificationsDropdown")}
</div>
) : (
notifications.map((n) => {
@@ -144,12 +146,12 @@ export function NotificationDropdown() {
}}
>
<div className="bg-muted mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full">
<Icon className="h-3.5 w-3.5" />
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<div className="flex items-center gap-1.5">
{!n.isRead ? (
<span className="bg-primary size-1.5 shrink-0 rounded-full" />
<span className="bg-primary size-1.5 shrink-0 rounded-full" aria-hidden="true" />
) : null}
<span className={cn("text-xs", !n.isRead ? "font-semibold" : "font-medium")}>
{n.title}
@@ -170,7 +172,7 @@ export function NotificationDropdown() {
<DropdownMenuSeparator className="mt-0" />
<DropdownMenuItem asChild>
<Link href="/messages" className="text-primary justify-center text-xs">
View all notifications
{t("actions.viewAll")}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>