Files
NextEdu/src/modules/announcements/schema.ts
SpecialX 1fe30984b6 refactor(announcements,messaging,notifications): V1+V2 审计重构 — i18n 命名空间独立 + 通知标题 i18n 化 + 服务端过滤 + 编排下沉 + 表单错误展示 + 架构图同步
V1 改进(已完成):
- P0-4/P1-4/P1-5: 通知组件和 CRUD Action 从 messaging 迁移至 notifications 模块
- P1-5: 新增 getMessagesPageData / getAdminAnnouncementsPageData 编排函数
- P1-6: announcements schema 添加 superRefine 条件校验
- P1-7: 新增 useMessageSearch hook(防抖 + 请求竞态取消)+ 客户端分页 UI
- P1-9: deleteMessage 事务化
- P2-11: 全模块 trackEvent 埋点
- 全模块 i18n 接入 + Error Boundary + a11y 改进

V2 改进(本次完成):
- V2-P0-1: 通知 i18n 命名空间独立(notifications.json),useTranslations 从 "messages" 切换到 "notifications"
- V2-P0-2: 公告/消息通知标题 i18n 化,Server Action 中使用 getTranslations 生成通知标题
- V2-P1-1: AnnouncementList 纯服务端过滤,移除客户端 useState/useMemo
- V2-P1-2: MessageList 客户端过滤仅在初始数据时执行,搜索结果由服务端按 tab 过滤
- V2-P1-3: 消息详情页编排下沉,新增 getMessageDetailPageData 编排函数
- V2-P1-4: 表单服务端校验错误展示(fieldErrors + aria-invalid)
- V2-P2-1: 轮询间隔常量化(POLL_INTERVAL_MS)
- V2-P2-2: 架构图同步(004 + 005)
2026-06-22 18:43:12 +08:00

96 lines
3.1 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 { z } from "zod"
/**
* 公告类型与目标受众的联合校验:
* - type=schooltargetGradeId / targetClassId 必须为空
* - type=gradetargetGradeId 必填targetClassId 必须为空
* - type=classtargetClassId 必填
*
* 避免"创建无受众公告"的数据完整性问题。
*/
const refineAudience = (
data: {
type?: "school" | "grade" | "class"
targetGradeId?: string | null
targetClassId?: string | null
},
ctx: z.RefinementCtx
): void => {
const type = data.type ?? "school"
const hasGrade = (data.targetGradeId ?? "").trim().length > 0
const hasClass = (data.targetClassId ?? "").trim().length > 0
if (type === "school" && (hasGrade || hasClass)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetGradeId"],
message: "全校公告不应指定目标年级或班级",
})
return
}
if (type === "grade" && !hasGrade) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetGradeId"],
message: "年级公告必须指定目标年级",
})
return
}
if (type === "class" && !hasClass) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetClassId"],
message: "班级公告必须指定目标班级",
})
return
}
}
export const CreateAnnouncementSchema = z
.object({
title: z.string().trim().min(1).max(255),
content: z.string().trim().min(1),
type: z.enum(["school", "grade", "class"]).optional(),
status: z.enum(["draft", "published", "archived"]).optional(),
targetGradeId: z.string().trim().optional().nullable(),
targetClassId: z.string().trim().optional().nullable(),
publishedAt: z.string().optional().nullable(),
})
.superRefine(refineAudience)
.transform((v) => ({
title: v.title,
content: v.content,
type: v.type ?? "school",
status: v.status ?? "draft",
targetGradeId: v.targetGradeId && v.targetGradeId.length > 0 ? v.targetGradeId : null,
targetClassId: v.targetClassId && v.targetClassId.length > 0 ? v.targetClassId : null,
publishedAt: v.publishedAt && v.publishedAt.length > 0 ? v.publishedAt : null,
}))
export type CreateAnnouncementInput = z.infer<typeof CreateAnnouncementSchema>
export const UpdateAnnouncementSchema = z
.object({
title: z.string().trim().min(1).max(255),
content: z.string().trim().min(1),
type: z.enum(["school", "grade", "class"]).optional(),
status: z.enum(["draft", "published", "archived"]).optional(),
targetGradeId: z.string().trim().optional().nullable(),
targetClassId: z.string().trim().optional().nullable(),
publishedAt: z.string().optional().nullable(),
})
.superRefine(refineAudience)
.transform((v) => ({
title: v.title,
content: v.content,
type: v.type ?? "school",
status: v.status ?? "draft",
targetGradeId: v.targetGradeId && v.targetGradeId.length > 0 ? v.targetGradeId : null,
targetClassId: v.targetClassId && v.targetClassId.length > 0 ? v.targetClassId : null,
publishedAt: v.publishedAt && v.publishedAt.length > 0 ? v.publishedAt : null,
}))
export type UpdateAnnouncementInput = z.infer<typeof UpdateAnnouncementSchema>