§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
246 lines
6.9 KiB
TypeScript
246 lines
6.9 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* Analytics domain API(ARCHITECTURE.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 LearningTrend,points 运行时按列表处理) */
|
||
export interface LearningTrend {
|
||
student_id: string;
|
||
points: TrendPoint[];
|
||
}
|
||
|
||
/** 学生薄弱点(对齐 schema StudentWeakness,weak_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 =====
|
||
|
||
/**
|
||
* 查询学习趋势(✅ 真实 schema,learningTrend)。
|
||
*
|
||
* 关联: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,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 查询学生薄弱知识点(✅ 真实 schema,studentWeakness)。
|
||
*
|
||
* 关联: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-pending,MSW 兜底)。
|
||
*
|
||
* 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-pending,MSW 兜底)。
|
||
*
|
||
* 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,
|
||
};
|
||
}
|