Files
Edu/apps/portal-shell/src/lib/api/analytics.ts
SpecialX 5a9f652943 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
2026-07-22 23:50:30 +08:00

246 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};
}