feat(portal-shell): lesson-plans 模块 6 页迁移(教师域 §9.1 B2)

§9.1 line 629 教师域 lesson-plans 行:
- /lesson-plans (列表) / /new (表单) / /library (教案库)
- /calendar (日历) / /heatmap (热力图) / /[planId]/edit (工作台)
契约:全  schema 无 → MSW 兜底 + @contract-pending

新增文件:
- src/lib/api/lesson-plans.ts (8 hooks)
- src/lib/api/operations/lesson-plans.graphql.ts (8 documents)
- src/features/teacher/lesson-plans/ (clients + transformations + tests)
- src/app/shell/teacher/lesson-plans/ (6 page.tsx + loading.tsx + error.tsx)

修改文件:
- src/mocks/graphql-data.ts (mock 数据 + handler cases)
- src/messages/{zh-CN,en}.json (lessonPlans i18n 命名空间)
- src/lib/api/{index,operations/index}.ts (导出 lesson-plans)
- src/shared/lib/route-permissions.ts (lesson-plans 路由权限声明)
- scripts/check-page-count.ts (baseline 31 → 37)

DoD 验收(§11.3 11 项):
- typecheck 0 errors
- lint 0 errors
- vitest 405 tests passed (新增 48 tests)
- lint:tokens 0 errors
- check:pages 37 PASS
- route-permissions 已声明
- 三态(loading/error/empty)齐备
- @contract-pending + MSW 兜底
- i18n zh-CN + en 同步

关联:ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §10 P2 / §11.3 / §11.4
契约工单:docs/architecture/issues/contracts/core-edu_contract.md
This commit is contained in:
SpecialX
2026-07-22 19:59:22 +08:00
parent 2f8f3f3855
commit 8ab4fae9d5
26 changed files with 3746 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ export * from "./teacher";
export * from "./exams";
export * from "./homework";
export * from "./grades";
export * from "./lesson-plans";
export * from "./student";
export * from "./parent";
export * from "./admin";

View File

@@ -0,0 +1,517 @@
"use client";
/**
* Lesson Plans domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域教案模块)
*
* 契约状态:全 ❌schema 无 lessonPlan(id) / lessonPlans / lessonPlanLibrary /
* lessonPlanCalendar / lessonPlanHeatmap 根字段,也无 Mutation 类型)
* → 所有查询与 mutation 走 MSW 兜底(@contract-pending
*
* 命名说明:本域 hook 命名遵循 homework.ts 模式(`useLessonPlansList` 而非
* `useLessonPlans`),以避免与 teacher.ts 中存量 `useLessonPlans(classId)`
* (用于 lesson-plan-editor widget冲突。详情类型用 `LessonPlanDetail`
* 同样为避免与 teacher.ts 的 `LessonPlan` 接口冲突。
*
* 契约工单docs/architecture/issues/contracts/core-edu_contract.md#lesson-plans
* 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock
*
* 关联ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §9.1 / §11.4 契约工单
*/
import type { FetchPolicy } from "@apollo/client";
import { useWidgetMutation } from "../useWidgetMutation";
import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
CREATE_LESSON_PLAN_DOC,
DELETE_LESSON_PLAN_DOC,
GET_LESSON_PLAN_CALENDAR_DOC,
GET_LESSON_PLAN_DOC,
GET_LESSON_PLAN_HEATMAP_DOC,
GET_LESSON_PLAN_LIBRARY_DOC,
GET_LESSON_PLANS_LIST_DOC,
UPDATE_LESSON_PLAN_DOC,
} from "./operations/lesson-plans.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型(@contract-pending与 MSW mock 数据形状对齐)=====
/** 教案状态枚举 */
export type LessonPlanStatus = "DRAFT" | "PUBLISHED" | "ARCHIVED";
/** 教案大纲节点(编辑器左侧树) */
export interface LessonPlanOutlineNode {
id: string;
title: string;
type: "section" | "topic" | "activity" | "assessment";
order: number;
children?: LessonPlanOutlineNode[];
}
/** 教案关联资源 */
export interface LessonPlanResource {
id: string;
name: string;
type: "link" | "file" | "video" | "image";
url: string;
}
/**
* 教案实体(完整字段,用于编辑工作台)。
*
* 命名为 `LessonPlanDetail` 以避免与 teacher.ts 的 `LessonPlan` 接口冲突
* (后者是 widget 时代的简化 stub被 lesson-plan-editor widget 使用)。
*/
export interface LessonPlanDetail {
id: string;
title: string;
gradeId: string;
subjectId: string;
objectives: string;
content: string;
attachments: string[];
duration: number;
status: LessonPlanStatus;
outline: LessonPlanOutlineNode[];
resources: LessonPlanResource[];
createdAt: string;
updatedAt: string;
}
/** 教案列表项(轻量字段集,用于列表渲染) */
export interface LessonPlanListItem {
id: string;
title: string;
gradeId: string;
subjectId: string;
objectives: string;
duration: number;
status: LessonPlanStatus;
createdAt: string;
updatedAt: string;
}
/** 列表查询响应(@contract-pending 假契约形状MSW 返回此结构) */
interface LessonPlansListResponse {
lessonPlans: {
items: LessonPlanListItem[];
total: number;
};
}
/** 单查响应(@contract-pendingMSW 返回此结构) */
interface LessonPlanResponse {
lessonPlan: LessonPlanDetail | null;
}
/** 创建教案输入 */
export interface CreateLessonPlanInput {
title: string;
gradeId: string;
subjectId: string;
objectives?: string;
content?: string;
attachments?: string[];
duration?: number;
}
/** 创建教案 mutation 响应(@contract-pending */
interface CreateLessonPlanResponse {
createLessonPlan: { id: string } | null;
}
/** 更新教案输入 */
export interface UpdateLessonPlanInput {
id: string;
title?: string;
objectives?: string;
content?: string;
attachments?: string[];
duration?: number;
status?: LessonPlanStatus;
outline?: LessonPlanOutlineNode[];
resources?: LessonPlanResource[];
}
/** 更新教案 mutation 响应(@contract-pending */
interface UpdateLessonPlanResponse {
updateLessonPlan: { id: string } | null;
}
/** 删除教案 mutation 响应(@contract-pending */
interface DeleteLessonPlanResponse {
deleteLessonPlan: { id: string } | null;
}
// ===== 教案库类型(@contract-pending 全 MSW=====
/** 教案库列表项(含共享库展示字段) */
export interface LessonPlanLibraryItem {
id: string;
title: string;
subjectId: string;
subjectName: string;
grade: string;
duration: number;
authorName: string;
downloadCount: number;
rating: number;
updatedAt: string;
}
/** 教案库查询响应 */
interface LessonPlanLibraryResponse {
lessonPlanLibrary: {
items: LessonPlanLibraryItem[];
total: number;
};
}
// ===== 日历类型(@contract-pending 全 MSW=====
/** 日历单条教案排期 */
export interface CalendarLessonPlan {
id: string;
title: string;
className: string;
subjectName: string;
startTime: string;
endTime: string;
status: LessonPlanStatus;
}
/** 日历单日条目 */
export interface CalendarDay {
date: string;
lessonPlans: CalendarLessonPlan[];
}
/** 日历完整数据 */
export interface LessonPlanCalendar {
month: string;
days: CalendarDay[];
}
/** 日历查询响应 */
interface LessonPlanCalendarResponse {
lessonPlanCalendar: LessonPlanCalendar | null;
}
// ===== 热力图类型(@contract-pending 全 MSW=====
/** 热力图单元格 */
export interface HeatmapCell {
date: string;
count: number;
intensity: 0 | 1 | 2 | 3 | 4;
}
/** 热力图完整数据 */
export interface LessonPlanHeatmap {
startDate: string;
endDate: string;
cells: HeatmapCell[];
maxCount: number;
}
/** 热力图查询响应 */
interface LessonPlanHeatmapResponse {
lessonPlanHeatmap: LessonPlanHeatmap | null;
}
// ===== 查询选项 =====
export interface LessonPlanQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 查询教案列表(@contract-pendingMSW 兜底)。
*
* schema 无 lessonPlans 根字段,由 MSW handlers 返回 mock 数据。
* 后端补齐列表查询后切换到真实 fetcher页面无需改动。
*
* 命名为 `useLessonPlansList` 以避免与 teacher.ts 的 `useLessonPlans` 冲突
* (对齐 homework.ts 的 `useHomeworkList` 模式)。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useLessonPlansList(
filter: {
gradeId?: string;
subjectId?: string;
status?: string;
limit?: number;
offset?: number;
},
options?: LessonPlanQueryOptions,
): UseQueryResult<{ items: LessonPlanListItem[]; total: number }> {
const result = useWidgetQuery<
LessonPlansListResponse,
{
gradeId?: string;
subjectId?: string;
status?: string;
limit?: number;
offset?: number;
}
>(
GET_LESSON_PLANS_LIST_DOC,
{
gradeId: filter.gradeId,
subjectId: filter.subjectId,
status: filter.status,
limit: filter.limit,
offset: filter.offset,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.lessonPlans,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 按 id 查询教案详情(@contract-pendingMSW 兜底)。
*
* schema 无 lessonPlan(id) 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/[planId]/edit 工作台页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 工作台页 / §11.4 契约工单
*/
export function useLessonPlan(
id: string,
options?: LessonPlanQueryOptions,
): UseQueryResult<LessonPlanDetail | null> {
const result = useWidgetQuery<LessonPlanResponse, { id: string }>(
GET_LESSON_PLAN_DOC,
{ id },
{
...options,
enabled: options?.enabled ?? id.length > 0,
},
);
return {
data: result.data?.lessonPlan ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 创建教案 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/new 表单页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 表单页 / §11.4 契约工单
*/
export function useCreateLessonPlan(): {
run: (input: CreateLessonPlanInput) => Promise<{ id: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
CreateLessonPlanResponse,
{ input: CreateLessonPlanInput }
>(CREATE_LESSON_PLAN_DOC);
const run = async (input: CreateLessonPlanInput): Promise<{ id: string }> => {
const data = await rawRun({ input });
if (!data?.createLessonPlan) {
throw new ApiError("Failed to create lesson plan", "INTERNAL_ERROR");
}
return data.createLessonPlan;
};
return { run, loading, error };
}
/**
* 更新教案 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/[planId]/edit 工作台页保存。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 工作台页 / §11.4 契约工单
*/
export function useUpdateLessonPlan(): {
run: (input: UpdateLessonPlanInput) => Promise<{ id: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
UpdateLessonPlanResponse,
{ input: UpdateLessonPlanInput }
>(UPDATE_LESSON_PLAN_DOC);
const run = async (input: UpdateLessonPlanInput): Promise<{ id: string }> => {
const data = await rawRun({ input });
if (!data?.updateLessonPlan) {
throw new ApiError("Failed to update lesson plan", "INTERNAL_ERROR");
}
return data.updateLessonPlan;
};
return { run, loading, error };
}
/**
* 删除教案 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于列表/编辑页删除按钮。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useDeleteLessonPlan(): {
run: (id: string) => Promise<{ id: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<DeleteLessonPlanResponse, { id: string }>(
DELETE_LESSON_PLAN_DOC,
);
const run = async (id: string): Promise<{ id: string }> => {
const data = await rawRun({ id });
if (!data?.deleteLessonPlan) {
throw new ApiError("Failed to delete lesson plan", "INTERNAL_ERROR");
}
return data.deleteLessonPlan;
};
return { run, loading, error };
}
/**
* 查询教案库列表(@contract-pendingMSW 兜底)。
*
* schema 无 lessonPlanLibrary 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/library 教案库列表页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 教案库页 / §11.4 契约工单
*/
export function useLessonPlanLibrary(
filter: {
subjectId?: string;
grade?: string;
limit?: number;
offset?: number;
},
options?: LessonPlanQueryOptions,
): UseQueryResult<{ items: LessonPlanLibraryItem[]; total: number }> {
const result = useWidgetQuery<
LessonPlanLibraryResponse,
{
subjectId?: string;
grade?: string;
limit?: number;
offset?: number;
}
>(
GET_LESSON_PLAN_LIBRARY_DOC,
{
subjectId: filter.subjectId,
grade: filter.grade,
limit: filter.limit,
offset: filter.offset,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.lessonPlanLibrary,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询教案日历数据(@contract-pendingMSW 兜底)。
*
* schema 无 lessonPlanCalendar 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/calendar 日历视图。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 日历视图 / §11.4 契约工单
*/
export function useLessonPlanCalendar(
month: string,
options?: LessonPlanQueryOptions,
): UseQueryResult<LessonPlanCalendar | null> {
const result = useWidgetQuery<LessonPlanCalendarResponse, { month: string }>(
GET_LESSON_PLAN_CALENDAR_DOC,
{ month },
{
...options,
enabled: options?.enabled ?? month.length > 0,
},
);
return {
data: result.data?.lessonPlanCalendar ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询教案热力图数据(@contract-pendingMSW 兜底)。
*
* schema 无 lessonPlanHeatmap 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/lesson-plans/heatmap 热力图视图。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 热力图视图 / §11.4 契约工单
*/
export function useLessonPlanHeatmap(
range: { startDate: string; endDate: string },
options?: LessonPlanQueryOptions,
): UseQueryResult<LessonPlanHeatmap | null> {
const result = useWidgetQuery<
LessonPlanHeatmapResponse,
{ startDate: string; endDate: string }
>(
GET_LESSON_PLAN_HEATMAP_DOC,
{ startDate: range.startDate, endDate: range.endDate },
{
...options,
enabled:
options?.enabled ??
(range.startDate.length > 0 && range.endDate.length > 0),
},
);
return {
data: result.data?.lessonPlanHeatmap ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}

View File

@@ -7,6 +7,7 @@ export * from "./teacher.graphql";
export * from "./exams.graphql";
export * from "./homework.graphql";
export * from "./grades.graphql";
export * from "./lesson-plans.graphql";
export * from "./student.graphql";
export * from "./parent.graphql";
export * from "./admin.graphql";

View File

@@ -0,0 +1,207 @@
// Lesson Plans domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
//
// 拆分原则:
// - 全部 8 个查询/mutation❌ schema 无对应字段/Mutation 类型
// → 走 MSW 兜底(@contract-pending等待后端补齐契约
//
// 注combined-schema.graphql 中 lessonPlanStatus 仅在 ai 子图存在(状态轮询用),
// core-edu 子图无 lessonPlan(id) / lessonPlans / lessonPlanLibrary / lessonPlanCalendar /
// lessonPlanHeatmap 根字段,也无 Mutation 类型 → 全部走 MSW 兜底。
//
// 契约工单docs/architecture/issues/contracts/core-edu_contract.md#lesson-plans
// 关联ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
import { gql } from "@apollo/client";
// ── 假契约查询(@contract-pending─────────────────────────────
// 列表查询schema 无 lessonPlans 根字段
// 页面通过 MSW 兜底获取列表数据,后端补齐后切换 fetcher 指向真实查询
//
// 命名说明operation name 与常量名使用 `GetLessonPlansList` /
// `GET_LESSON_PLANS_LIST_DOC`,以避免与 teacher.graphql.ts 中存量
// `GetLessonPlans` / `GET_LESSON_PLANS_DOC`widget lesson-plan-editor 用)
// 冲突。MSW handlers 也通过不同 operationName 区分两种响应形状。
//
// 契约工单core-edu_contract.md#lesson-plans-list
export const GET_LESSON_PLANS_LIST_DOC = gql`
query GetLessonPlansList(
$gradeId: ID
$subjectId: ID
$status: String
$limit: Int
$offset: Int
) {
lessonPlans(
gradeId: $gradeId
subjectId: $subjectId
status: $status
limit: $limit
offset: $offset
) {
items {
id
title
gradeId
subjectId
objectives
duration
status
createdAt
updatedAt
}
total
}
}
`;
// ── 单查(@contract-pending────────────────────────────────────
// schema 无 lessonPlan(id) 根字段 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/[planId]/edit 工作台页
// 契约工单core-edu_contract.md#lesson-plan-detail
export const GET_LESSON_PLAN_DOC = gql`
query GetLessonPlan($id: ID!) {
lessonPlan(id: $id) {
id
title
gradeId
subjectId
objectives
content
attachments
duration
status
outline {
id
title
type
order
children {
id
title
type
order
}
}
resources {
id
name
type
url
}
createdAt
updatedAt
}
}
`;
// ── 假契约变更(@contract-pending─────────────────────────────
// 创建教案schema 无 Mutation 类型
// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
// 契约工单core-edu_contract.md#create-lesson-plan
export const CREATE_LESSON_PLAN_DOC = gql`
mutation CreateLessonPlan($input: CreateLessonPlanInput!) {
createLessonPlan(input: $input) {
id
}
}
`;
// ── 更新教案 mutation@contract-pending──────────────────────
// schema 无 Mutation 类型 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/[planId]/edit 工作台页保存
// 契约工单core-edu_contract.md#update-lesson-plan
export const UPDATE_LESSON_PLAN_DOC = gql`
mutation UpdateLessonPlan($input: UpdateLessonPlanInput!) {
updateLessonPlan(input: $input) {
id
}
}
`;
// ── 删除教案 mutation@contract-pending──────────────────────
// schema 无 Mutation 类型 → MSW 兜底
// 用于列表/编辑页删除按钮
// 契约工单core-edu_contract.md#delete-lesson-plan
export const DELETE_LESSON_PLAN_DOC = gql`
mutation DeleteLessonPlan($id: ID!) {
deleteLessonPlan(id: $id) {
id
}
}
`;
// ── 教案库(@contract-pending 全 MSW──────────────────────────
// schema 无 lessonPlanLibrary 根字段 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/library 教案库列表页
// 契约工单core-edu_contract.md#lesson-plan-library
export const GET_LESSON_PLAN_LIBRARY_DOC = gql`
query GetLessonPlanLibrary(
$subjectId: ID
$grade: String
$limit: Int
$offset: Int
) {
lessonPlanLibrary(
subjectId: $subjectId
grade: $grade
limit: $limit
offset: $offset
) {
items {
id
title
subjectId
subjectName
grade
duration
authorName
downloadCount
rating
updatedAt
}
total
}
}
`;
// ── 教案日历(@contract-pending 全 MSW────────────────────────
// schema 无 lessonPlanCalendar 根字段 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/calendar 日历视图
// 契约工单core-edu_contract.md#lesson-plan-calendar
export const GET_LESSON_PLAN_CALENDAR_DOC = gql`
query GetLessonPlanCalendar($month: String!) {
lessonPlanCalendar(month: $month) {
month
days {
date
lessonPlans {
id
title
className
subjectName
startTime
endTime
status
}
}
}
}
`;
// ── 教案热力图(@contract-pending 全 MSW──────────────────────
// schema 无 lessonPlanHeatmap 根字段 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/heatmap 热力图视图
// 契约工单core-edu_contract.md#lesson-plan-heatmap
export const GET_LESSON_PLAN_HEATMAP_DOC = gql`
query GetLessonPlanHeatmap($startDate: String!, $endDate: String!) {
lessonPlanHeatmap(startDate: $startDate, endDate: $endDate) {
startDate
endDate
cells {
date
count
intensity
}
maxCount
}
}
`;