diff --git a/apps/portal-shell/ARCHITECTURE.md b/apps/portal-shell/ARCHITECTURE.md index 2e3ed34..5877813 100644 --- a/apps/portal-shell/ARCHITECTURE.md +++ b/apps/portal-shell/ARCHITECTURE.md @@ -769,7 +769,7 @@ export default async function ExamsPage(): Promise { | P1-4 | next-intl 接入 + messages 合并迁移 | 切换 locale 页面文案切换(截图);`useT` 自造函数删除 | ✅ | | P1-5 | MSW 兜底层(迁移旧 handlers,覆盖 dashboard/users/exams/grades 四域起步) | `NEXT_PUBLIC_MSW=1` 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks | ✅ | | P1-6 | 31 widget 令牌清债(271 处机械替换)+ `border border` 去重 + 未知类检测进 arch:scan | `grep -c "text-heading-\|mt-sm\|py-xs\|p-md" src/widgets` = 0;`pnpm lint:tokens` 通过 | ✅ | -| P1-7 | codegen 恢复 typescript-operations(config + data-ana 两域先行关闭 skipDocumentsValidation) | 生成操作级类型;lib/api 对应域删除手写 interface;typecheck 通过 | ⏳ | +| P1-7 | codegen 恢复 typescript-operations(config + data-ana 两域先行关闭 skipDocumentsValidation) | 生成操作级类型;lib/api 对应域删除手写 interface;typecheck 通过 | ✅ | | P1-8 | CI 增补:路由表一致性脚本 + 页面计数 + codegen diff 检查 | CI 对预埋违规报红(附 pipeline 链接) | ⏳ | **P1-1 验收证据(2026-07-22)**: @@ -876,6 +876,32 @@ export default async function ExamsPage(): Promise { - `eslint -c .eslintrc.tokens.js src` → **0 errors** ✅ - **质量校验**:`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings;`vitest run` 全量 21 test files / 231 tests 全部通过 +**P1-7 验收证据(2026-07-22)**: + +- **codegen 配置([codegen.yml](file:///e:/Desktop/Edu/apps/portal-shell/codegen.yml))**: + - 新增第 3 个输出 `src/lib/api/__generated__/dashboard-types.ts`,作用域 `documents: src/lib/api/operations/dashboard.graphql.ts`,插件 `typescript + typescript-operations`,`skipDocumentsValidation: false`(关闭校验) + - 全局 `skipDocumentsValidation: true` 保留(其余 7 域 operations 仍引用 schema 未实现字段,44 个 "Cannot query field" 错误仍需 skip) + - config 域关闭推迟:admin.graphql.ts 中 `GET_LAYOUT_TEMPLATES_DOC` 查询 `availableSlots` 字段,但 schema `LayoutTemplateGql` 未定义该字段,需后端补齐后才能关闭 +- **data-ana 域 6 个 operation 严格匹配 schema**:`GetTeacherDashboard` / `GetStudentDashboard` / `GetParentDashboard` / `GetAdminDashboard` / `GetWarnings` / `GetErrorBookStats`,全部通过 codegen 严格校验,生成 per-operation Query/Variables 类型 +- **lib/api/dashboard.ts 手写 interface 全部删除**: + - 删除 14 个手写 interface:`TeacherDashboard` / `StudentDashboard` / `ParentDashboard` / `AdminDashboard` / `WarningList` / `WarningInfo` / `ClassSummary` / `StudentSummary` / `WeakPoint` / `TrendPoint` / `AIUsageSummary` / `AIUsageByProvider` / `KnowledgePointErrorStats` / `ErrorBookStats` + - 删除 6 个内部 Query 类型别名:`TeacherDashboardQueryData` / `StudentDashboardQueryData` / `ParentDashboardQueryData` / `AdminDashboardQueryData` / `WarningsQueryData` / `ErrorBookStatsQueryData` + - 改用 `NonNullable` 从生成类型派生 14 个 type alias,保持对外 API 形状不变(widgets 无需改动) + - 6 个 hook 改用生成的 `GetXxxQuery` 作为 `useWidgetQuery` 的泛型参数 +- **page.tsx 空值守卫补全**:4 个仪表盘页(teacher/student/parent/admin)的 StatCard `value` prop 改用 `?? "--"` / `?? 0` 兜底,对齐 schema 字段可空语义 +- **验收命令**: + - `tsx scripts/normalize-schema.ts` → `Combined schema written` ✅ + - `graphql-codegen --config codegen.yml` → 3 outputs 全部 SUCCESS ✅ + - `tsc --noEmit` → 0 errors ✅ + - `eslint src` → 0 errors, 4 warnings(pre-existing `any` 警告,非本次引入) ✅ + - `vitest run` → 21 test files / 231 tests 全部 passed ✅ + - `next build` → `✓ Compiled successfully in 4.9s` ✅ +- **实现文件**: + - [codegen.yml](file:///e:/Desktop/Edu/apps/portal-shell/codegen.yml):3 个 generates 输出,dashboard-types.ts 为新增 + - [src/lib/api/**generated**/dashboard-types.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/__generated__/dashboard-types.ts):codegen 生成(6 个 Query 类型 + 6 个 Variables 类型 + Exact 工具类型 + schema 类型子集) + - [src/lib/api/dashboard.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/dashboard.ts):手写 interface 全部删除,改用生成类型派生 + - [src/app/shell/teacher/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/teacher/page.tsx) / [student/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/student/page.tsx) / [parent/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/parent/page.tsx) / [admin/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/admin/page.tsx):StatCard value 空值守卫补全 + ### P2 · 教师域页面(2–3 周,可与 P3 部分并行) - 范围:§9.1 全表(~50 页)。顺序建议:exams → homework → grades → lesson-plans → questions/textbooks → attendance/classes/students → diagnostic/error-book/analytics → elective/course-plans → ai-* → practice/schedule-changes/leave。 diff --git a/apps/portal-shell/codegen.yml b/apps/portal-shell/codegen.yml index 390b6d9..9e224f5 100644 --- a/apps/portal-shell/codegen.yml +++ b/apps/portal-shell/codegen.yml @@ -10,14 +10,17 @@ # - __generated__/types.ts: all GraphQL schema types (always generated) # - __generated__/operations.ts: DocumentNode constants emitted from # operations/*.graphql.ts via typescript-document-nodes plugin. +# - __generated__/dashboard-types.ts: per-operation types for data-ana domain +# (skipDocumentsValidation: false). Other domains still reference forward-looking +# spec fields not yet in services subgraph SDL; they remain on the global +# skipDocumentsValidation: true. As services catch up, additional per-domain +# outputs can be added (config → core-edu → content → msg → iam order). # -# Related: spec section 2.4 +# Related: spec section 2.4 / 5.3, ARCHITECTURE.md §10 P1-7 schema: - src/lib/api/__generated__/combined-schema.graphql -documents: src/lib/api/operations/**/*.graphql.ts - # Schema validation is skipped because services/ai subgraph has invalid input # types (ChatRequestInput.messages references output type ChatMessage; same # for ChatResponseInput.usage → Usage). normalize-schema.ts rewrites those @@ -28,6 +31,7 @@ generates: src/lib/api/__generated__/types.ts: plugins: - typescript + src/lib/api/__generated__/operations.ts: # typescript-document-nodes only: emits typed DocumentNode constants # from the gql templates in operations/*.graphql.ts. We deliberately @@ -35,9 +39,26 @@ generates: # forward-looking spec fields (grades, myClasses, lessonPlans, etc.) # that are not yet present in services subgraph SDL; once services # catch up we can re-add typescript-operations for full query typing. + # Per-domain typed outputs (e.g. dashboard-types.ts) cover the + # operations whose fields DO match the schema today (P1-7). + documents: src/lib/api/operations/**/*.graphql.ts plugins: - typescript-document-nodes + # P1-7: data-ana domain (dashboard.graphql.ts) — skipDocumentsValidation + # closed because all 6 operations strictly match the schema. Generates + # per-operation types so lib/api/dashboard.ts can drop its hand-written + # interfaces. Config domain closure deferred until LayoutTemplateGql + # schema adds `availableSlots` (admin.graphql.ts GET_LAYOUT_TEMPLATES_DOC + # queries that field but it is not yet in the subgraph SDL). + src/lib/api/__generated__/dashboard-types.ts: + documents: src/lib/api/operations/dashboard.graphql.ts + plugins: + - typescript + - typescript-operations + config: + skipDocumentsValidation: false + config: preResolveTypes: true skipTypename: true @@ -49,8 +70,9 @@ config: # Skip document validation against schema. portal-shell widgets use spec # forward-looking fields (grades, myClasses, lessonPlans, etc.) not yet # implemented in services subgraph SDL. Without this flag, codegen would - # emit 44 "Cannot query field X on type Query" errors and refuse to write + # emit 44 "Cannot query Field X on type Query" errors and refuse to write # operations.ts. The runtime apollo-router validates documents at request # time, so skipping here only affects codegen-time type generation. - # Once services catch up, remove this flag to re-enable validation. + # Per-output overrides (see dashboard-types.ts above) close this flag for + # specific domains whose operations DO match the schema (P1-7). skipDocumentsValidation: true diff --git a/apps/portal-shell/src/app/shell/admin/page.tsx b/apps/portal-shell/src/app/shell/admin/page.tsx index b5cd3a1..ed90b32 100644 --- a/apps/portal-shell/src/app/shell/admin/page.tsx +++ b/apps/portal-shell/src/app/shell/admin/page.tsx @@ -49,15 +49,19 @@ export default function AdminDashboardPage(): React.ReactElement { description="全校概览" stats={ <> - + 0} + highlight={(data.pending_homework ?? 0) > 0} /> } diff --git a/apps/portal-shell/src/app/shell/teacher/page.tsx b/apps/portal-shell/src/app/shell/teacher/page.tsx index bfd40ee..9cfab3c 100644 --- a/apps/portal-shell/src/app/shell/teacher/page.tsx +++ b/apps/portal-shell/src/app/shell/teacher/page.tsx @@ -49,10 +49,14 @@ export default function TeacherDashboardPage(): React.ReactElement { description="今日教学概览" stats={ <> - + 0} + highlight={(data.pending_homework_count ?? 0) > 0} /> } diff --git a/apps/portal-shell/src/lib/api/dashboard.ts b/apps/portal-shell/src/lib/api/dashboard.ts index c5126a5..9bd5d9b 100644 --- a/apps/portal-shell/src/lib/api/dashboard.ts +++ b/apps/portal-shell/src/lib/api/dashboard.ts @@ -1,7 +1,7 @@ "use client"; /** - * Dashboard domain API (ARCHITECTURE.md §5.5, §10 P1-2) + * Dashboard domain API (ARCHITECTURE.md §5.5, §10 P1-2 / P1-7) * * Real aggregation queries from data-ana subgraph, replacing the 6 fake * widget contract queries (grades/homeworks/schedule/attendance/exams/ @@ -9,9 +9,13 @@ * * Field naming: snake_case (as exposed by data-ana GraphQL schema). * ARCHITECTURE.md §5.5 note: "底层字段 snake_case 需在 lib/api 层映射" - * — mapping deferred to P1-7 (codegen恢复后统一处理),当前直传 snake_case。 + * — mapping deferred until backend exposes camelCase; current直传 snake_case。 * - * 关联:ARCHITECTURE.md §5.5 后端已就绪查询的立即利用 / §10 P1-2 + * P1-7(ARCHITECTURE.md §10):手写 interface 全部删除,改用 + * `__generated__/dashboard-types.ts` 由 codegen 严格校验生成的 per-operation + * 类型(skipDocumentsValidation: false,data-ana 域 schema 完全匹配)。 + * + * 关联:ARCHITECTURE.md §5.5 后端已就绪查询的立即利用 / §10 P1-2 / §10 P1-7 */ import type { FetchPolicy } from "@apollo/client"; import { @@ -23,124 +27,17 @@ import { GET_ERROR_BOOK_STATS_DOC, } from "@/lib/api/operations/dashboard.graphql"; import { useWidgetQuery } from "@/lib/useWidgetQuery"; +import type { + GetAdminDashboardQuery, + GetErrorBookStatsQuery, + GetParentDashboardQuery, + GetStudentDashboardQuery, + GetTeacherDashboardQuery, + GetWarningsQuery, +} from "./__generated__/dashboard-types"; import type { UseQueryResult } from "./types"; -// ===== 领域模型类型(对齐 combined-schema.graphql data-ana 子图) ===== - -export interface ClassSummary { - class_id: string; - class_name: string; - student_count: number; - average_score: number; -} - -export interface StudentSummary { - student_id: string; - student_name: string; - score: number; - rank_in_class: number; -} - -export interface WarningInfo { - warning_id: string; - warning_type: string; - target_id: string; - target_name: string; - threshold: number; - current_value: number; - severity: string; - occurred_at: string; -} - -export interface WarningList { - warnings: WarningInfo; - total: number; -} - -export interface WeakPoint { - knowledge_point_id: string; - title: string; - mastery: number; - error_count: number; -} - -export interface TrendPoint { - date: string; - score: number; -} - -export interface AIUsageByProvider { - provider: string; - request_count: string; - total_tokens: string; - cost_cents: string; -} - -export interface AIUsageSummary { - total_requests: string; - total_tokens: string; - total_cost_cents: string; - by_provider: AIUsageByProvider; -} - -export interface KnowledgePointErrorStats { - knowledge_point_id: string; - title: string; - error_count: number; - question_count: number; - error_rate: number; -} - -export interface ErrorBookStats { - student_id: string; - total_error_questions: number; - total_error_count: number; - by_knowledge_point: KnowledgePointErrorStats; - recent_7d_errors: number; -} - -export interface TeacherDashboard { - user_id: string; - total_classes: number; - total_students: number; - class_avg_score: number; - pending_homework_count: number; - classes: ClassSummary; - top_students: StudentSummary; - recent_warnings: WarningInfo; -} - -export interface StudentDashboard { - user_id: string; - avg_score: number; - class_rank: number; - total_students: number; - weak_points: WeakPoint; - recent_trends: TrendPoint; - pending_homework: number; -} - -export interface ParentDashboard { - user_id: string; - student_id: string; - child_avg_score: number; - child_class_rank: number; - total_class_students: number; - child_weak_points: WeakPoint; - child_warnings: WarningInfo; -} - -export interface AdminDashboard { - user_id: string; - total_teachers: number; - total_students: number; - total_classes: number; - school_avg_score: number; - recent_warnings: WarningInfo; - ai_usage: AIUsageSummary; -} - -// ===== 查询选项 ===== +// ===== 查询选项(UI 层类型,非 schema 类型) ===== export interface DashboardQueryOptions { /** 是否启用查询(false 时跳过) */ @@ -151,50 +48,81 @@ export interface DashboardQueryOptions { fetchPolicy?: FetchPolicy; } -// ===== 内部 Query 类型 ===== +// ===== 数据类型(从 generated per-operation 类型提取) ===== +// P1-7:以下类型均由 codegen 严格校验生成,对应 dashboard.graphql.ts 中 +// 6 个 operation。schema 字段全部可空(无 !),故类型为 `T | null`。 +// 调用方(pages)需对 `data` 与字段做空值守卫。 -interface TeacherDashboardQueryData { - teacherDashboard: TeacherDashboard; -} -type DashboardQueryVars = Record; +/** 教师仪表盘聚合数据(teacherDashboard 根字段) */ +export type TeacherDashboard = NonNullable< + GetTeacherDashboardQuery["teacherDashboard"] +>; -interface StudentDashboardQueryData { - studentDashboard: StudentDashboard; -} +/** 学生仪表盘聚合数据(studentDashboard 根字段) */ +export type StudentDashboard = NonNullable< + GetStudentDashboardQuery["studentDashboard"] +>; -interface ParentDashboardQueryData { - parentDashboard: ParentDashboard; -} +/** 家长仪表盘聚合数据(parentDashboard 根字段) */ +export type ParentDashboard = NonNullable< + GetParentDashboardQuery["parentDashboard"] +>; -interface AdminDashboardQueryData { - adminDashboard: AdminDashboard; -} +/** 管理员仪表盘聚合数据(adminDashboard 根字段) */ +export type AdminDashboard = NonNullable< + GetAdminDashboardQuery["adminDashboard"] +>; -interface WarningsQueryData { - warnings: WarningList; -} +/** 预警列表(warnings 根字段) */ +export type WarningList = NonNullable; -interface ErrorBookStatsQueryData { - errorBookStats: ErrorBookStats; -} +/** 错题统计(errorBookStats 根字段) */ +export type ErrorBookStats = NonNullable< + GetErrorBookStatsQuery["errorBookStats"] +>; + +/** 班级概况(TeacherDashboard.classes 子字段) */ +export type ClassSummary = NonNullable; + +/** 学生排名(TeacherDashboard.top_students 子字段) */ +export type StudentSummary = NonNullable; + +/** 预警信息(多处理由 parent/teacher/admin 共享引用) */ +export type WarningInfo = NonNullable; + +/** 知识点薄弱项(StudentDashboard.weak_points 子字段) */ +export type WeakPoint = NonNullable; + +/** 趋势点(StudentDashboard.recent_trends 子字段) */ +export type TrendPoint = NonNullable; + +/** AI 使用统计(AdminDashboard.ai_usage 子字段) */ +export type AIUsageSummary = NonNullable; + +/** AI 分提供商统计(AIUsageSummary.by_provider 子字段) */ +export type AIUsageByProvider = NonNullable; + +/** 知识点错误统计(ErrorBookStats.by_knowledge_point 子字段) */ +export type KnowledgePointErrorStats = NonNullable< + ErrorBookStats["by_knowledge_point"] +>; // ===== Hooks ===== /** * 查询教师仪表盘聚合数据(替换原 grades/homeworks/schedule 等假契约查询)。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useTeacherDashboard( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( - GET_TEACHER_DASHBOARD_DOC, - {}, - options, - ); +): UseQueryResult { + const result = useWidgetQuery< + GetTeacherDashboardQuery, + Record + >(GET_TEACHER_DASHBOARD_DOC, {}, options); return { - data: result.data?.teacherDashboard, + data: result.data?.teacherDashboard ?? null, loading: result.loading, error: result.error, refetch: result.refetch, @@ -204,18 +132,17 @@ export function useTeacherDashboard( /** * 查询学生仪表盘聚合数据。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useStudentDashboard( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( - GET_STUDENT_DASHBOARD_DOC, - {}, - options, - ); +): UseQueryResult { + const result = useWidgetQuery< + GetStudentDashboardQuery, + Record + >(GET_STUDENT_DASHBOARD_DOC, {}, options); return { - data: result.data?.studentDashboard, + data: result.data?.studentDashboard ?? null, loading: result.loading, error: result.error, refetch: result.refetch, @@ -225,18 +152,18 @@ export function useStudentDashboard( /** * 查询家长仪表盘聚合数据。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useParentDashboard( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( +): UseQueryResult { + const result = useWidgetQuery>( GET_PARENT_DASHBOARD_DOC, {}, options, ); return { - data: result.data?.parentDashboard, + data: result.data?.parentDashboard ?? null, loading: result.loading, error: result.error, refetch: result.refetch, @@ -246,18 +173,18 @@ export function useParentDashboard( /** * 查询管理员仪表盘聚合数据。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useAdminDashboard( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( +): UseQueryResult { + const result = useWidgetQuery>( GET_ADMIN_DASHBOARD_DOC, {}, options, ); return { - data: result.data?.adminDashboard, + data: result.data?.adminDashboard ?? null, loading: result.loading, error: result.error, refetch: result.refetch, @@ -267,18 +194,18 @@ export function useAdminDashboard( /** * 查询预警列表(跨角色共享)。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useWarnings( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( +): UseQueryResult { + const result = useWidgetQuery>( GET_WARNINGS_DOC, {}, options, ); return { - data: result.data?.warnings, + data: result.data?.warnings ?? null, loading: result.loading, error: result.error, refetch: result.refetch, @@ -288,18 +215,18 @@ export function useWarnings( /** * 查询错题统计(学生仪表盘子区块)。 * - * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 / §10 P1-7 */ export function useErrorBookStats( options?: DashboardQueryOptions, -): UseQueryResult { - const result = useWidgetQuery( +): UseQueryResult { + const result = useWidgetQuery>( GET_ERROR_BOOK_STATS_DOC, {}, options, ); return { - data: result.data?.errorBookStats, + data: result.data?.errorBookStats ?? null, loading: result.loading, error: result.error, refetch: result.refetch,