feat(portal-shell): 管理域 §9.4 B5 全量迁移(24 + 21 补充批次共 44 页 + 42 features)

按 ARCHITECTURE.md §9.4 规划口径 + admin-NeedTodo.md §四补充批次完成管理域全量页面迁移:

【§9.4 规划 24 页(B5)】
- users(2) + roles(1) + permissions(1) + audit-logs(4) + invitation-codes(1)
- school(6: redirect/schools/classes/departments/academic-year/grades)
- announcements(1) + files(1) + ai-settings(1) + system(1) + viewports(1)
- students(1) + teachers(1) + organization(1) + plugins(1, config-service)
- 仪表盘已存在(/shell/admin/page.tsx)

【§四补充批次 21 页】
- course-plans(4) + elective(4) + questions(1) + lesson-plans(2) + error-book(1)
- scheduling(3: auto/changes/rules) + attendance(1) + curriculum-map(1)
- announcements 详情/编辑(2) + roles/[id] 详情(1) + users/import(1)

【实现要点】
- 全部使用 ListPageShell + loading/error/empty 三态规范(§11.3 DoD)
- 走 lib/api hooks;未就绪契约走 MSW + @contract-pending 注释(§11.4)
- 文案走 useTranslations(zh-CN + en 两份同步更新)
- 42 个 features/<domain>/transformations.ts 纯函数 + 配套 vitest 单测
- catch 块统一 notify.error;无空 catch;lint:tokens 通过
- 路由全部登记到 route-permissions.ts(39 EXACT + 8 PREFIX)

【验收】
- tsc --noEmit: 0 errors
- ESLint src: 0 errors (4 generated-files warnings, pre-existing)
- lint:tokens: 0 errors
- vitest: 1639/1639 passed (含 23 admin 测试文件 671 用例)
- check:routes: PASS (143 routes, 4 ghost entries pre-existing)
- check:pages: PASS (146 pages)
- check:codegen: PASS
- arch:scan: 24 modules, 8262 symbols

关联:ARCHITECTURE.md §9.4 / §10 P5 / §11.3 DoD / §11.6
This commit is contained in:
SpecialX
2026-07-24 23:07:20 +08:00
parent 5a9f652943
commit 062d9e9582
394 changed files with 60468 additions and 118 deletions

View File

@@ -0,0 +1,115 @@
/**
* Admin Lesson Plans 数据变换工具ARCHITECTURE.md §11.3 DoD - 纯函数单测)
*
* 所有格式化/映射函数均为纯函数,便于 vitest 单测。
* 关联ARCHITECTURE.md §11.3 DoD "数据变换/权限判断等纯函数有 vitest 单测"
*/
import type { AdminLessonPlanListItem } from "@/lib/api/admin-p5";
/** 教案状态中文标签映射 */
export const LESSON_PLAN_STATUS_LABEL: Record<string, string> = {
DRAFT: "草稿",
PUBLISHED: "已发布",
ARCHIVED: "已归档",
SUBMITTED: "已提交",
};
/**
* 将教案状态枚举值映射为中文标签。
* 未知状态回退为原始值。
*/
export function formatLessonPlanStatus(status: string): string {
return LESSON_PLAN_STATUS_LABEL[status] ?? status;
}
/**
* 格式化 ISO 日期字符串为本地化展示zh-CN含年月日时分
* 输入无效null/undefined/空/非法)时返回占位符 "--"。
*/
export function formatLessonPlanDate(
isoDate: string | null | undefined,
): string {
if (!isoDate) return "--";
const d = new Date(isoDate);
if (Number.isNaN(d.getTime())) return "--";
return d.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
/**
* 判断教案是否可编辑(草稿与已发布状态允许编辑,归档后不可编辑)。
*/
export function isLessonPlanEditable(status: string): boolean {
return status === "DRAFT" || status === "PUBLISHED";
}
/**
* 判断教案是否已发布PUBLISHED
*/
export function isLessonPlanPublished(status: string): boolean {
return status === "PUBLISHED";
}
/**
* 判断教案是否已归档ARCHIVED
*/
export function isLessonPlanArchived(status: string): boolean {
return status === "ARCHIVED";
}
/**
* 从教案详情中提取列表项视图模型(裁剪字段)。
*
* 用于详情→列表裁剪场景详情字段textbookId/chapterId/content 等)被剥离。
*/
export function toAdminLessonPlanListItem(detail: {
id: string;
title: string;
subjectId: string;
subjectName: string;
teacherId: string;
teacherName: string;
classId: string;
className: string;
status: string;
createdAt: string;
updatedAt: string;
}): AdminLessonPlanListItem {
return {
id: detail.id,
title: detail.title,
subjectId: detail.subjectId,
subjectName: detail.subjectName,
teacherId: detail.teacherId,
teacherName: detail.teacherName,
classId: detail.classId,
className: detail.className,
status: detail.status,
createdAt: detail.createdAt,
updatedAt: detail.updatedAt,
};
}
/**
* 根据教案状态返回 Tailwind 徽章语义类名。
*/
export function lessonPlanStatusToBadgeClass(status: string): string {
switch (status) {
case "DRAFT":
return "bg-muted text-muted-foreground";
case "PUBLISHED":
return "bg-primary/10 text-primary";
case "ARCHIVED":
return "bg-muted text-muted-foreground";
case "SUBMITTED":
return "bg-amber-500/10 text-amber-600 dark:text-amber-400";
default:
return "bg-muted text-muted-foreground";
}
}