- 新增审计报告 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 清单 + 已知问题修复记录
150 lines
4.5 KiB
TypeScript
150 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import Link from "next/link"
|
|
import { useRouter } from "next/navigation"
|
|
import { toast } from "sonner"
|
|
import { useTranslations } from "next-intl"
|
|
import { ArrowLeft, Send } from "lucide-react"
|
|
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import { Label } from "@/shared/components/ui/label"
|
|
import { Textarea } from "@/shared/components/ui/textarea"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/components/ui/select"
|
|
|
|
import { sendMessageAction } from "../actions"
|
|
import type { RecipientOption } from "../types"
|
|
|
|
export function MessageCompose({
|
|
recipients,
|
|
parentMessageId,
|
|
defaultReceiverId,
|
|
defaultSubject,
|
|
backHref = "/messages",
|
|
}: {
|
|
recipients: RecipientOption[]
|
|
parentMessageId?: string
|
|
defaultReceiverId?: string
|
|
defaultSubject?: string
|
|
backHref?: string
|
|
}) {
|
|
const t = useTranslations("messages")
|
|
const router = useRouter()
|
|
const [isWorking, setIsWorking] = useState(false)
|
|
const [receiverId, setReceiverId] = useState(defaultReceiverId ?? "")
|
|
|
|
const handleSubmit = async (formData: FormData) => {
|
|
if (!receiverId) {
|
|
toast.error(t("messages.selectRecipient"))
|
|
return
|
|
}
|
|
formData.set("receiverId", receiverId)
|
|
if (parentMessageId) {
|
|
formData.set("parentMessageId", parentMessageId)
|
|
}
|
|
|
|
setIsWorking(true)
|
|
try {
|
|
const res = await sendMessageAction(null, formData)
|
|
if (res.success) {
|
|
toast.success(res.message)
|
|
router.push("/messages")
|
|
router.refresh()
|
|
} else {
|
|
toast.error(res.message || t("messages.sendFailed"))
|
|
}
|
|
} catch {
|
|
toast.error(t("messages.sendFailed"))
|
|
} finally {
|
|
setIsWorking(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Button asChild variant="ghost" size="icon" aria-label={t("actions.back")}>
|
|
<Link href={backHref}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
<CardTitle>{parentMessageId ? t("title.reply") : t("title.newMessage")}</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form action={handleSubmit} className="space-y-6">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="receiverId">{t("form.to")}</Label>
|
|
<Select value={receiverId} onValueChange={setReceiverId} disabled={!!defaultReceiverId}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t("form.toPlaceholder")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{recipients.map((r) => (
|
|
<SelectItem key={r.id} value={r.id}>
|
|
{r.name}
|
|
{r.role ? ` (${r.role})` : ""}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<input type="hidden" name="receiverId" value={receiverId} />
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="subject">{t("form.subject")}</Label>
|
|
<Input
|
|
id="subject"
|
|
name="subject"
|
|
placeholder={t("form.subjectPlaceholder")}
|
|
defaultValue={defaultSubject ?? ""}
|
|
maxLength={255}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="content">{t("form.content")}</Label>
|
|
<Textarea
|
|
id="content"
|
|
name="content"
|
|
placeholder={t("form.contentPlaceholder")}
|
|
className="min-h-[200px]"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<CardFooter className="justify-end gap-2 px-0">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => router.push(backHref)}
|
|
disabled={isWorking}
|
|
>
|
|
{t("actions.cancel")}
|
|
</Button>
|
|
<Button type="submit" disabled={isWorking || !receiverId}>
|
|
{isWorking ? (
|
|
t("actions.sending")
|
|
) : (
|
|
<>
|
|
<Send className="mr-2 h-4 w-4" />
|
|
{t("actions.send")}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|