"use client"; /** * Textbooks domain API(ARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域教材模块) * * 三类操作: * 1. useTextbook(按 id 单查):✅ 真实查询 textbook(id: ID!),schema 已就绪 * 2. useTextbooks(列表查询):❌ schema 无 textbooks(...) 根字段 → MSW 兜底(@contract-pending) * 3. useTextbookChapters(章节列表):❌ schema 无 chapters(textbookId) → MSW 兜底(@contract-pending) * 4. useCreate/useUpdateTextbook(mutation):❌ schema 无 Mutation → MSW 兜底(@contract-pending) * * 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#textbooks * 后端补齐后:重跑 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_TEXTBOOK_DOC, GET_TEXTBOOK_CHAPTERS_DOC, GET_TEXTBOOK_DOC, GET_TEXTBOOKS_DOC, UPDATE_TEXTBOOK_DOC, } from "./operations/textbooks.graphql"; import type { UseQueryResult } from "./types"; // ===== 数据类型(对齐 schema Textbook / Chapter 类型)===== /** * 教材实体(对齐 combined-schema.graphql Textbook 类型,content 子图) * * 字段命名 camelCase(与 schema 一致)。 */ export interface Textbook { id: string; title: string; subjectId: string; gradeId: string; version: string; status: string; tenantId: string | null; createdAt: string; updatedAt: string; } /** 教材列表项(与 Textbook 同构,列表页直接复用) */ export type TextbookListItem = Textbook; /** 列表查询响应(@contract-pending 假契约形状,MSW 返回此结构) */ interface TextbooksListResponse { textbooks: { items: TextbookListItem[]; total: number; }; } /** 单查响应(真实 schema) */ interface TextbookResponse { textbook: Textbook | null; } /** * 章节实体(对齐 combined-schema.graphql Chapter 类型,content 子图) * * schema Chapter 字段:id/textbookId/title/order/parentId/status/createdAt/updatedAt */ export interface TextbookChapter { id: string; textbookId: string; title: string; order: number; parentId: string | null; status: string; createdAt: string; updatedAt: string; } /** 章节列表查询响应(@contract-pending) */ interface TextbookChaptersResponse { textbookChapters: { items: TextbookChapter[]; total: number; }; } /** 列表筛选条件 */ export interface TextbooksListFilter { subjectId?: string; gradeId?: string; q?: string; limit?: number; offset?: number; } /** 新建教材输入 */ export interface CreateTextbookInput { title: string; subjectId: string; gradeId: string; version: string; status?: string; } /** 更新教材输入 */ export interface UpdateTextbookInput { title?: string; subjectId?: string; gradeId?: string; version?: string; status?: string; } /** 新建教材 mutation 响应(@contract-pending) */ interface CreateTextbookResponse { createTextbook: { id: string } | null; } /** 更新教材 mutation 响应(@contract-pending) */ interface UpdateTextbookResponse { updateTextbook: { id: string } | null; } // ===== 查询选项 ===== export interface TextbookQueryOptions { enabled?: boolean; pollInterval?: number; fetchPolicy?: FetchPolicy; } // ===== Hooks ===== /** * 按 id 查询教材详情(真实 schema,✅ 契约已就绪)。 * * 关联:ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1 详情页 */ export function useTextbook( id: string, options?: TextbookQueryOptions, ): UseQueryResult { const result = useWidgetQuery( GET_TEXTBOOK_DOC, { id }, { ...options, enabled: options?.enabled ?? id.length > 0, }, ); return { data: result.data?.textbook ?? null, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 查询教材列表(@contract-pending,MSW 兜底)。 * * schema 无 textbooks(...) 根字段,由 MSW handlers 返回 mock 数据。 * 后端补齐列表查询后切换到真实 fetcher,页面无需改动。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单 */ export function useTextbooks( filter: TextbooksListFilter, options?: TextbookQueryOptions, ): UseQueryResult<{ items: TextbookListItem[]; total: number }> { const result = useWidgetQuery< TextbooksListResponse, { subjectId?: string; gradeId?: string; q?: string; limit?: number; offset?: number; } >( GET_TEXTBOOKS_DOC, { subjectId: filter.subjectId, gradeId: filter.gradeId, q: filter.q, limit: filter.limit, offset: filter.offset, }, { enabled: options?.enabled ?? true, fetchPolicy: options?.fetchPolicy, pollInterval: options?.pollInterval, }, ); return { data: result.data?.textbooks, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 查询教材章节列表(@contract-pending,MSW 兜底)。 * * schema 无 chapters(textbookId) 根字段,Textbook 类型也无 chapters 字段。 * 详情页章节列表通过 MSW 返回 mock 数据,后端补齐后切换 fetcher。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单 */ export function useTextbookChapters( textbookId: string, options?: TextbookQueryOptions, ): UseQueryResult<{ items: TextbookChapter[]; total: number }> { const result = useWidgetQuery< TextbookChaptersResponse, { textbookId: string } >( GET_TEXTBOOK_CHAPTERS_DOC, { textbookId }, { ...options, enabled: options?.enabled ?? textbookId.length > 0, }, ); return { data: result.data?.textbookChapters, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 创建教材 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * 后端补齐 mutation 后切换到真实 fetcher。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单 */ export function useCreateTextbook(): { run: (input: CreateTextbookInput) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( CREATE_TEXTBOOK_DOC, ); const run = async (input: CreateTextbookInput): Promise<{ id: string }> => { const data = await rawRun({ input }); if (!data?.createTextbook) { throw new ApiError("Failed to create textbook", "INTERNAL_ERROR"); } return data.createTextbook; }; return { run, loading, error }; } /** * 更新教材 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单 */ export function useUpdateTextbook(): { run: (id: string, input: UpdateTextbookInput) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< UpdateTextbookResponse, { id: string; input: UpdateTextbookInput } >(UPDATE_TEXTBOOK_DOC); const run = async ( id: string, input: UpdateTextbookInput, ): Promise<{ id: string }> => { const data = await rawRun({ id, input }); if (!data?.updateTextbook) { throw new ApiError("Failed to update textbook", "INTERNAL_ERROR"); } return data.updateTextbook; }; return { run, loading, error }; }