feat(portal-shell): error-book + diagnostic + analytics 模块 5 页迁移(教师域 §9.1 B2)

§9.1 line 637-639 教师域:
- /shell/teacher/error-book (列表,1 页)
- /shell/teacher/diagnostic (列表) + /class/[classId] (详情) — 2 页
- /shell/teacher/analytics (概览) + /[studentId] (详情) — 2 页
契约:
- error-book  真实 errorBookItems/errorBookStats
- diagnostic  真实 diagnosticReports 列表 + 详情  MSW
- analytics 🟡 真实 learningTrend/studentWeakness + 概览  MSW

新增文件(24 个):
- src/lib/api/{error-book,diagnostic,analytics}.ts (hooks)
- src/lib/api/operations/{error-book,diagnostic,analytics}.graphql.ts (documents)
- src/features/teacher/{error-book,diagnostic,analytics}/ (clients + transformations + tests)
- src/app/shell/teacher/{error-book,diagnostic,analytics}/ (5 page.tsx + 3 loading + 3 error)

修改文件(7 个):
- src/mocks/graphql-data.ts (7 handler cases)
- src/messages/{zh-CN,en}.json (errorBook/diagnostic/analytics i18n)
- src/lib/api/{index,operations/index}.ts (导出)
- src/shared/lib/route-permissions.ts (3 EXACT + 3 PREFIX 路由权限)
- scripts/check-page-count.ts (baseline 53 → 58)

DoD 验收(§11.3 11 项):
- typecheck 0 errors
- lint 0 errors
- vitest 734 tests passed
- lint:tokens 0 errors
- check:pages 58 PASS
- route-permissions 已声明
- 三态齐备
- @contract-pending + MSW 兜底(仅对 schema 不存在的字段)
- i18n zh-CN + en 同步

设计决策:
- 类型命名冲突解决:ErrorBookItem → ErrorBookEntry;ErrorBookStats → TeacherErrorBookStats;
  KnowledgePointErrorStats → ErrorBookKpStats;useErrorBookStats → useTeacherErrorBookStats
  (避免与 dashboard.ts/student.ts 同名类型冲突)
- analytics TrendPoint/WeakPoint 形状相同,从 dashboard.ts import 复用
- error-book/analytics 复用 CLASS_READ/CLASS_MANAGE(无专用教师权限点);
  diagnostic 复用 DIAGNOSTIC_READ/DIAGNOSTIC_MANAGE

关联:ARCHITECTURE.md §5.3 / §5.4 / §5.5 / §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 23:50:30 +08:00
parent 80cc1d2461
commit 5a9f652943
35 changed files with 4207 additions and 8 deletions

View File

@@ -0,0 +1,245 @@
"use client";
/**
* Analytics domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域学情分析模块)
*
* 契约状态:混合(🟡 部分真实 + 部分契约待补)
* - learningTrend: ✅ 真实schema 已就绪LearningTrend无参数
* - studentWeakness: ✅ 真实schema 已就绪StudentWeakness无参数
* - analyticsOverview: ❌ schema 无此聚合根字段 → MSW 兜底(@contract-pending
* - studentAnalytics(studentId): ❌ schema 无此根字段 → MSW 兜底(@contract-pending
*
* Schema 缺陷处理data-ana 子图):
* - LearningTrend.points 在 schema 中是单数 TrendPoint 类型(应为列表)
* - StudentWeakness.weak_points 在 schema 中是单数 WeakPoint 类型(应为列表)
* - lib/api 层 TypeScript 类型按业务语义定义为数组MSW 返回数组形状
*
* 契约工单docs/architecture/issues/contracts/data-ana_contract.md#analytics
* 关联ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §5.5 后端已就绪查询 / §9.1 / §11.4
*/
import type { FetchPolicy } from "@apollo/client";
import { useWidgetQuery } from "../useWidgetQuery";
import {
GET_ANALYTICS_OVERVIEW_DOC,
GET_LEARNING_TREND_DOC,
GET_STUDENT_ANALYTICS_DOC,
GET_STUDENT_WEAKNESS_DOC,
} from "./operations/analytics.graphql";
import type { TrendPoint, WeakPoint } from "./dashboard";
import type { UseQueryResult } from "./types";
// ===== 数据类型(对齐 data-ana 子图snake_case=====
// TrendPoint / WeakPoint 复用 dashboard.ts 的定义(同 schema 字段,避免 barrel 导出冲突)
/** 学习趋势(对齐 schema LearningTrendpoints 运行时按列表处理) */
export interface LearningTrend {
student_id: string;
points: TrendPoint[];
}
/** 学生薄弱点(对齐 schema StudentWeaknessweak_points 运行时按列表处理) */
export interface StudentWeakness {
student_id: string;
weak_points: WeakPoint[];
}
/** 总览趋势点(@contract-pending 扩展,含 avg_score */
export interface OverviewTrendPoint {
date: string;
avg_score: number;
}
/** 总览薄弱知识点(@contract-pending 扩展) */
export interface OverviewWeakPoint {
knowledge_point_id: string;
title: string;
error_count: number;
mastery: number;
}
/** 班级学情分解(@contract-pending 扩展) */
export interface AnalyticsClassBreakdown {
class_id: string;
class_name: string;
avg_score: number;
student_count: number;
at_risk_count: number;
}
/** 学情分析总览(@contract-pending 聚合数据) */
export interface AnalyticsOverview {
total_students: number;
avg_score: number;
avg_mastery: number;
at_risk_count: number;
weak_points: OverviewWeakPoint[];
trends: OverviewTrendPoint[];
class_breakdown: AnalyticsClassBreakdown[];
}
/** 学生近期考试(@contract-pending 扩展) */
export interface StudentRecentExam {
exam_id: string;
exam_title: string;
score: number;
total_score: number;
date: string;
}
/** 单生学情分析详情(@contract-pending 聚合数据) */
export interface StudentAnalytics {
student_id: string;
student_name: string;
student_no: string;
class_id: string;
class_name: string;
avg_score: number;
class_rank: number;
total_students: number;
weak_points: WeakPoint[];
trends: TrendPoint[];
recent_exams: StudentRecentExam[];
}
// ===== 响应类型 =====
/** learningTrend 查询响应 */
interface LearningTrendResponse {
learningTrend: LearningTrend | null;
}
/** studentWeakness 查询响应 */
interface StudentWeaknessResponse {
studentWeakness: StudentWeakness | null;
}
/** analyticsOverview 查询响应(@contract-pending */
interface AnalyticsOverviewResponse {
analyticsOverview: AnalyticsOverview | null;
}
/** studentAnalytics 查询响应(@contract-pending */
interface StudentAnalyticsResponse {
studentAnalytics: StudentAnalytics | null;
}
// ===== 查询选项 =====
export interface AnalyticsQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 查询学习趋势(✅ 真实 schemalearningTrend
*
* 关联ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1
*/
export function useLearningTrend(
options?: AnalyticsQueryOptions,
): UseQueryResult<LearningTrend | null> {
const result = useWidgetQuery<LearningTrendResponse>(
GET_LEARNING_TREND_DOC,
{},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.learningTrend ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询学生薄弱知识点(✅ 真实 schemastudentWeakness
*
* 关联ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1
*/
export function useStudentWeakness(
options?: AnalyticsQueryOptions,
): UseQueryResult<StudentWeakness | null> {
const result = useWidgetQuery<StudentWeaknessResponse>(
GET_STUDENT_WEAKNESS_DOC,
{},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.studentWeakness ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询学情分析总览(@contract-pendingMSW 兜底)。
*
* schema 无 analyticsOverview 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/analytics 总览页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 总览页 / §11.4 契约工单
*/
export function useAnalyticsOverview(
options?: AnalyticsQueryOptions,
): UseQueryResult<AnalyticsOverview | null> {
const result = useWidgetQuery<AnalyticsOverviewResponse>(
GET_ANALYTICS_OVERVIEW_DOC,
{},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.analyticsOverview ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 按 studentId 查询单生学情分析(@contract-pendingMSW 兜底)。
*
* schema 无 studentAnalytics(studentId) 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/analytics/[studentId] 学生详情页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单
*/
export function useStudentAnalytics(
studentId: string,
options?: AnalyticsQueryOptions,
): UseQueryResult<StudentAnalytics | null> {
const result = useWidgetQuery<
StudentAnalyticsResponse,
{ studentId: string }
>(
GET_STUDENT_ANALYTICS_DOC,
{ studentId },
{
...options,
enabled: options?.enabled ?? studentId.length > 0,
},
);
return {
data: result.data?.studentAnalytics ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}