@@ -96,6 +170,12 @@ export function ScheduleChangesListClient(): React.ReactElement {
title={t("list.title")}
description={t("list.description")}
icon={}
+ actions={
+
+ }
filters={
<>
}
>
-
+ refetch?.()}
+ submitting={reviewing}
+ />
+
+
);
}
-
-/**
- * 调课列表表格(纯展示组件,对齐 §8.2 排版规范)。
- */
-function ScheduleChangeTable({
- items,
-}: {
- items: ScheduleChangeListItem[];
-}): React.ReactElement {
- const t = useTranslations("scheduleChanges");
- return (
-
-
-
-
- |
- {t("list.colSummary")}
- |
-
- {t("list.colClassName")}
- |
- {t("list.colType")} |
-
- {t("list.colOriginalDate")}
- |
-
- {t("list.colNewDate")}
- |
- {t("list.colReason")} |
- {t("list.colStatus")} |
-
- {t("list.colApplicant")}
- |
-
- {t("list.colCreatedAt")}
- |
-
-
-
- {items.map((change) => (
-
- |
- {getScheduleChangeSummary(change)}
- |
- {change.className} |
-
-
- |
-
- {formatScheduleChangeDay(change.originalDate)}
- |
-
- {formatScheduleChangeDay(change.newDate)}
- {change.newPeriod ? ` ${change.newPeriod}` : ""}
- |
-
-
- {change.reason}
-
- |
-
-
- |
- {change.applicantName} |
-
- {formatScheduleChangeDate(change.createdAt)}
- |
-
- ))}
-
-
-
- );
-}
-
-/**
- * 调课状态徽章。
- */
-function ScheduleChangeStatusBadge({
- status,
-}: {
- status: string;
-}): React.ReactElement {
- const label = formatScheduleChangeStatus(status);
- const cls = scheduleChangeStatusToBadgeClass(status);
- return (
-
- {label}
-
- );
-}
-
-/**
- * 调课类型徽章。
- */
-function ScheduleChangeTypeBadge({
- type,
-}: {
- type: string;
-}): React.ReactElement {
- const label = formatScheduleChangeType(type);
- const cls = scheduleChangeTypeToBadgeClass(type);
- return (
-
- {label}
-
- );
-}
diff --git a/apps/portal-shell/src/features/teacher/textbooks/textbook-detail-client.tsx b/apps/portal-shell/src/features/teacher/textbooks/textbook-detail-client.tsx
index db5cae9..bd7249b 100644
--- a/apps/portal-shell/src/features/teacher/textbooks/textbook-detail-client.tsx
+++ b/apps/portal-shell/src/features/teacher/textbooks/textbook-detail-client.tsx
@@ -38,6 +38,7 @@ import {
isTextbookEditable,
sortChaptersByOrder,
} from "@/features/teacher/textbooks/transformations";
+import { KnowledgeGraph } from "./components/knowledge-graph";
/**
* 详情客户端主体。需由 server page 包裹在 中。
@@ -111,10 +112,33 @@ export function TextbookDetailClient(): React.ReactElement {
mswNotice={t("detail.chaptersMswNotice")}
/>
) : null}
+ {data ? : null}
);
}
+/**
+ * 知识图谱区(详情页内嵌,使用迁移后的 KnowledgeGraph 组件)。
+ *
+ * 容器使用 h-96(Tailwind 默认阶梯 24rem = 384px),
+ * 内部 KnowledgeGraph 通过 h-full 撑满容器;force-graph 同样使用
+ * ResizeObserver 自适应容器尺寸。遵守 §3.10 设计令牌:不使用任意值。
+ */
+function KnowledgeGraphSection({
+ textbookId,
+}: {
+ textbookId: string;
+}): React.ReactElement {
+ const t = useTranslations("textbooks");
+ return (
+
+
+
+
+
+ );
+}
+
/**
* 详情基本信息区(对齐 §7.3 详情页模板)。
*/
diff --git a/apps/portal-shell/src/lib/api/admin-p5.ts b/apps/portal-shell/src/lib/api/admin-p5.ts
index 5c3aec1..e67d164 100644
--- a/apps/portal-shell/src/lib/api/admin-p5.ts
+++ b/apps/portal-shell/src/lib/api/admin-p5.ts
@@ -2327,31 +2327,21 @@ export function useUpdateViewport(): {
// ============================================================
export function useAdminStudents(
filter: StudentFilter,
- pagination: Pagination,
): UseQueryResult> {
const result = useWidgetQuery<
{ adminStudents: PaginatedResult },
- { filter: StudentFilter; limit: number; offset: number }
- >(GET_ADMIN_STUDENTS_DOC, {
- filter,
- limit: pagination.limit,
- offset: pagination.offset,
- });
+ { filter: StudentFilter }
+ >(GET_ADMIN_STUDENTS_DOC, { filter });
return { ...result, data: result.data?.adminStudents };
}
export function useAdminTeachers(
filter: TeacherFilter,
- pagination: Pagination,
): UseQueryResult> {
const result = useWidgetQuery<
{ adminTeachers: PaginatedResult },
- { filter: TeacherFilter; limit: number; offset: number }
- >(GET_ADMIN_TEACHERS_DOC, {
- filter,
- limit: pagination.limit,
- offset: pagination.offset,
- });
+ { filter: TeacherFilter }
+ >(GET_ADMIN_TEACHERS_DOC, { filter });
return { ...result, data: result.data?.adminTeachers };
}
diff --git a/apps/portal-shell/src/lib/api/admin.ts b/apps/portal-shell/src/lib/api/admin.ts
index 3f545f1..0d788b0 100644
--- a/apps/portal-shell/src/lib/api/admin.ts
+++ b/apps/portal-shell/src/lib/api/admin.ts
@@ -19,8 +19,12 @@ import { ApiError } from "./errors";
import type { PaginatedResult, Pagination, UseQueryResult } from "./types";
import {
GET_USERS_DOC,
+ GET_USER_DOC,
UPDATE_USER_STATUS_DOC,
UPDATE_USER_ROLE_DOC,
+ UPDATE_USER_DOC,
+ DELETE_USER_DOC,
+ ASSIGN_USER_ROLES_DOC,
GET_ROLES_DOC,
GET_PERMISSIONS_DOC,
UPDATE_ROLE_PERMISSIONS_DOC,
@@ -28,6 +32,8 @@ import {
GET_INVITATION_CODES_DOC,
CREATE_INVITATION_CODE_DOC,
REVOKE_INVITATION_CODE_DOC,
+ GENERATE_INVITATION_CODES_DOC,
+ DELETE_INVITATION_CODES_DOC,
GET_SCHOOL_DOC,
UPDATE_SCHOOL_DOC,
GET_PLUGIN_REGISTRY_DOC,
@@ -50,14 +56,48 @@ export interface User {
role: string;
status: string;
createdAt: string;
+ /**
+ * 手机号(@contract-pending:schema 未就绪,MSW 兜底)。
+ * 缺省视为未知,UI 用 "-" 占位。
+ */
+ phone?: string;
+ /**
+ * 多角色列表(@contract-pending:schema 未就绪,MSW 兜底)。
+ * 替换语义:与 `role` 共存,UI 优先渲染 `roles`,缺省回退 `[role]`。
+ */
+ roles?: string[];
+ /**
+ * 更新时间 ISO 字符串(列表查询可选返回)。
+ * 缺省视为未知,UI 用 `-` 占位。
+ * @contract-pending schema 无此字段,MSW 兜底
+ */
+ updatedAt?: string;
+ /**
+ * 用户类型(internal/external,@contract-pending:schema 未就绪,MSW 兜底)。
+ * 缺省视为未知,UI 不渲染类型徽章。
+ */
+ userType?: string;
}
export type UserQueryVars = {
role: string | null;
- limit: number;
- offset: number;
+ /** @contract-pending 真实分页待 IAM 契约就绪,MSW 兜底时忽略 */
+ limit?: number;
+ offset?: number;
};
+/**
+ * 用户编辑输入(@contract-pending:schema 未就绪,MSW 兜底)。
+ * 所有字段可选,仅传入需变更的字段。
+ */
+export interface UserUpdateInput {
+ name?: string;
+ email?: string;
+ phone?: string;
+ role?: string;
+ status?: string;
+}
+
// ============================================================
// Types: RBAC
// ============================================================
@@ -67,6 +107,11 @@ export interface Permission {
resource: string;
action: string;
description: string;
+ /**
+ * 权限值(如 `user:create`,@contract-pending:schema 未就绪,MSW 兜底)。
+ * 缺省视为未知,UI 用 `-` 占位。
+ */
+ value?: string;
}
export interface RolePermission {
@@ -90,6 +135,29 @@ export interface Role {
* 列表 MSW 兜底可能缺省,UI 需容忍 undefined。
*/
isLocked?: boolean;
+ /**
+ * 是否启用(列表查询可选返回)。
+ * 缺省视为 true(兼容旧 MSW 数据)。
+ * @contract-pending schema 无此字段,MSW 兜底
+ */
+ isEnabled?: boolean;
+ /**
+ * 关联用户数(列表查询可选返回)。
+ * 缺省视为 0。
+ * @contract-pending schema 无此字段,MSW 兜底
+ */
+ userCount?: number;
+ /**
+ * 更新时间 ISO 字符串(列表查询可选返回)。
+ * 缺省视为未知,UI 用 "-" 占位。
+ * @contract-pending schema 无此字段,MSW 兜底
+ */
+ updatedAt?: string;
+ /**
+ * 角色值(如 `role:teacher`,@contract-pending:schema 未就绪,MSW 兜底)。
+ * 缺省视为未知,UI 用 `-` 占位。
+ */
+ value?: string;
}
// ============================================================
@@ -111,6 +179,11 @@ export interface AuditLog {
* 列表 StatusBadge 显示 "--" 占位;契约补齐后切换为真实值。
*/
status?: string | null;
+ /**
+ * 错误信息(status=failure/error 时有值)。
+ * @contract-pending schema 未提供此字段,MSW 兜底;详情对话框展示用。
+ */
+ errorMessage?: string | null;
}
export interface AuditLogFilter {
@@ -126,9 +199,25 @@ export interface InvitationCode {
id: string;
code: string;
role: string;
+ /** 角色展示名(@contract-pending,MSW 兜底) */
+ roleName?: string | null;
+ /** 关联班级 ID(@contract-pending,可选) */
+ classId?: string | null;
+ /** 关联班级名称(@contract-pending,可选) */
+ className?: string | null;
+ /** 关联邮箱(@contract-pending,可选) */
+ email?: string | null;
+ /** 批次 ID(@contract-pending,MSW 兜底) */
+ batchId?: string | null;
status: string;
usedCount: number;
maxUses: number;
+ /** 使用者用户 ID(@contract-pending,MSW 兜底) */
+ usedBy?: string | null;
+ /** 使用者姓名(@contract-pending,MSW 兜底) */
+ usedByName?: string | null;
+ /** 使用时间 ISO(@contract-pending,MSW 兜底) */
+ usedAt?: string | null;
expiresAt: string;
createdAt: string;
createdBy: string;
@@ -145,6 +234,51 @@ export type CreatedInvitationCode = Pick<
"id" | "code" | "role" | "maxUses" | "expiresAt"
>;
+/**
+ * 批量生成邀请码输入(对齐 CICD GenerateInvitationCodesInput)。
+ *
+ * 字段说明:
+ * - batchName:批次名称(可选,用于标识本次生成用途)
+ * - count:生成数量(1-1000)
+ * - role:角色代码(teacher/student/parent/admin)
+ * - expireDays:过期天数(与 expiresAt 二选一,expireDays 优先)
+ * - expiresAt:ISO 过期时间(与 expireDays 二选一)
+ * - email:关联邮箱(可选,CICD 字段)
+ * - classId:关联班级 ID(可选,CICD 字段)
+ * - note:备注(可选)
+ * - purpose:用途说明(可选,如"新教师入职")
+ */
+export interface GenerateInvitationCodesInput {
+ count: number;
+ role: string;
+ batchName?: string;
+ expireDays?: number;
+ expiresAt?: string;
+ email?: string;
+ classId?: string;
+ note?: string;
+ purpose?: string;
+}
+
+export interface GeneratedInvitationCode {
+ code: string;
+ role: string;
+ expiresAt: string;
+}
+
+export interface GenerateInvitationCodesResult {
+ success: boolean;
+ /** 批次 ID(@contract-pending,MSW 兜底) */
+ batchId?: string;
+ generated: GeneratedInvitationCode[];
+}
+
+/** 批量删除邀请码结果(@contract-pending,MSW 兜底) */
+export interface DeleteInvitationCodesResult {
+ success: boolean;
+ deletedCount: number;
+}
+
// ============================================================
// Types: School
// ============================================================
@@ -169,6 +303,8 @@ export interface SchoolInput {
currentTerm?: string;
semesterStart?: string;
semesterEnd?: string;
+ /** 学校编码(@contract-pending,MSW 兜底) */
+ code?: string;
}
// ============================================================
@@ -252,6 +388,18 @@ export function useUsers(
};
}
+/**
+ * 单查用户详情(@contract-pending:schema 无 user(id) 根字段,MSW 兜底)。
+ * 契约工单:iam_contract.md#user-detail
+ */
+export function useUser(id: string): UseQueryResult {
+ const result = useWidgetQuery<{ user: User | null }, { id: string }>(
+ GET_USER_DOC,
+ { id },
+ );
+ return { ...result, data: result.data?.user ?? null };
+}
+
export function useUpdateUserStatus(): {
run: (id: string, status: string) => Promise<{ id: string; status: string }>;
loading: boolean;
@@ -308,6 +456,101 @@ export function useUpdateUserRole(): {
return { run, loading, error };
}
+/**
+ * 删除用户(@contract-pending:MSW 兜底,schema 无 deleteUser 根字段)。
+ * 契约工单:iam_contract.md#user-delete
+ */
+export function useDeleteUser(): {
+ run: (id: string) => Promise<{ id: string; success: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { deleteUser: { id: string; success: boolean } },
+ { id: string }
+ >(DELETE_USER_DOC);
+
+ const run = async (id: string): Promise<{ id: string; success: boolean }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteUser) {
+ throw new ApiError("Failed to delete user", "INTERNAL_ERROR");
+ }
+ return data.deleteUser;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 多角色分配(@contract-pending:MSW 兜底,schema 无 assignUserRoles 根字段)。
+ * 替换语义:传入完整角色名数组,覆盖原有角色。
+ * 契约工单:iam_contract.md#user-assign-roles
+ */
+export function useAssignUserRoles(): {
+ run: (
+ userId: string,
+ roleNames: string[],
+ ) => Promise<{ userId: string; roleNames: string[] }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { assignUserRoles: { userId: string; roleNames: string[] } },
+ { userId: string; roleNames: string[] }
+ >(ASSIGN_USER_ROLES_DOC);
+
+ const run = async (
+ userId: string,
+ roleNames: string[],
+ ): Promise<{ userId: string; roleNames: string[] }> => {
+ const data = await rawRun({ userId, roleNames });
+ if (!data?.assignUserRoles) {
+ throw new ApiError("Failed to assign user roles", "INTERNAL_ERROR");
+ }
+ return data.assignUserRoles;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 编辑用户基本信息(name/email/phone/role/status)。
+ * @contract-pending:schema 无 updateUser 根字段,MSW 兜底。
+ * 契约工单:iam_contract.md#user-update
+ */
+export function useUpdateUser(): {
+ run: (id: string, input: UserUpdateInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { updateUser: User },
+ { id: string; input: UserUpdateInput }
+ >(UPDATE_USER_DOC);
+
+ const run = async (id: string, input: UserUpdateInput): Promise => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateUser) {
+ throw new ApiError("Failed to update user", "INTERNAL_ERROR");
+ }
+ return data.updateUser;
+ };
+
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: RBAC
// ============================================================
@@ -450,6 +693,69 @@ export function useRevokeInvitationCode(): {
return { run, loading, error };
}
+export function useGenerateInvitationCodes(): {
+ run: (
+ input: GenerateInvitationCodesInput,
+ ) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { generateInvitationCodes: GenerateInvitationCodesResult },
+ { input: GenerateInvitationCodesInput }
+ >(GENERATE_INVITATION_CODES_DOC);
+
+ const run = async (
+ input: GenerateInvitationCodesInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.generateInvitationCodes) {
+ throw new ApiError(
+ "Failed to generate invitation codes",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.generateInvitationCodes;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 批量删除邀请码(@contract-pending,MSW 兜底)。
+ * 对齐 CICD deleteInvitationCodes:接受 ID 列表,返回删除条数。
+ */
+export function useDeleteInvitationCodes(): {
+ run: (ids: string[]) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<{ deleteInvitationCodes: number }, { ids: string[] }>(
+ DELETE_INVITATION_CODES_DOC,
+ );
+
+ const run = async (ids: string[]): Promise => {
+ const data = await rawRun({ ids });
+ if (data?.deleteInvitationCodes === undefined) {
+ throw new ApiError("Failed to delete invitation codes", "INTERNAL_ERROR");
+ }
+ return {
+ success: true,
+ deletedCount: data.deleteInvitationCodes,
+ };
+ };
+
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: School settings
// ============================================================
diff --git a/apps/portal-shell/src/lib/api/attendance.ts b/apps/portal-shell/src/lib/api/attendance.ts
index f078d6a..38996fc 100644
--- a/apps/portal-shell/src/lib/api/attendance.ts
+++ b/apps/portal-shell/src/lib/api/attendance.ts
@@ -25,6 +25,7 @@ import {
GET_ATTENDANCE_REPORT_DOC,
GET_ATTENDANCE_SHEET_DOC,
GET_ATTENDANCE_STATS_DOC,
+ GET_CLASS_COMPARISON_DOC,
SAVE_ATTENDANCE_SHEET_DOC,
} from "./operations/attendance.graphql";
import type { UseQueryResult } from "./types";
@@ -382,3 +383,38 @@ export function useSaveAttendanceSheet(): {
return { run, loading, error };
}
+
+// ===== 班级对比(@contract-pending)=====
+
+/** 班级对比项(与 GET_CLASS_COMPARISON_DOC 返回结构一致) */
+export interface ClassComparisonItem {
+ className: string;
+ attendanceRate: number;
+ totalStudents: number;
+ presentStudents: number;
+}
+
+interface ClassComparisonResponse {
+ classComparison: ClassComparisonItem[];
+}
+
+/**
+ * 查询班级出勤率对比(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 classComparison 根字段,由 MSW handlers 返回 mock 数据。
+ * 管理端考勤页班级对比卡通过此 hook 获取数据。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useClassComparison(): UseQueryResult {
+ const result = useWidgetQuery>(
+ GET_CLASS_COMPARISON_DOC,
+ {},
+ );
+ return {
+ data: result.data?.classComparison ?? [],
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
diff --git a/apps/portal-shell/src/lib/api/classes.ts b/apps/portal-shell/src/lib/api/classes.ts
index 2951cbf..8d77544 100644
--- a/apps/portal-shell/src/lib/api/classes.ts
+++ b/apps/portal-shell/src/lib/api/classes.ts
@@ -18,12 +18,23 @@
import type { FetchPolicy } from "@apollo/client";
import { useWidgetQuery } from "../useWidgetQuery";
+import { useWidgetMutation } from "../useWidgetMutation";
+import { ApiError } from "./errors";
import {
+ CREATE_CLASS_DOC,
+ CREATE_CLASS_SCHEDULE_DOC,
+ DELETE_CLASS_SCHEDULE_DOC,
+ GENERATE_CLASS_INVITATION_CODE_DOC,
GET_CLASS_INFO_DOC,
+ GET_CLASS_INVITATION_CODES_DOC,
GET_CLASS_SCHEDULE_DOC,
GET_CLASS_STUDENTS_DOC,
GET_CLASS_TEACHERS_DOC,
GET_CLASSES_DOC,
+ REVOKE_CLASS_INVITATION_CODE_DOC,
+ UPDATE_CLASS_DOC,
+ DELETE_CLASS_DOC,
+ UPDATE_CLASS_SCHEDULE_DOC,
} from "./operations/classes.graphql";
import type { UseQueryResult } from "./types";
@@ -318,3 +329,374 @@ export function useClassTeachers(
refetch: result.refetch,
};
}
+
+// ===== 课表 CRUD 输入类型(@contract-pending)=====
+
+/** 创建/更新课表条目输入 */
+export interface ClassScheduleInput {
+ classId: string;
+ weekday: number;
+ period: number;
+ subjectName: string;
+ teacherName: string;
+ classroom: string | null;
+ startTime: string;
+ endTime: string;
+}
+
+// ===== 班级邀请码类型(@contract-pending)=====
+
+/** 班级邀请码 */
+export interface ClassInvitationCode {
+ id: string;
+ code: string;
+ status: string;
+ usedCount: number;
+ maxUses: number | null;
+ expiresAt: string | null;
+ createdAt: string;
+ revokedAt: string | null;
+ note: string | null;
+}
+
+/** 生成班级邀请码输入 */
+export interface GenerateClassInvitationCodeInput {
+ classId: string;
+ expiresInHours: number | null;
+ maxUses: number | null;
+ note: string | null;
+}
+
+// ===== 响应类型(@contract-pending)=====
+
+interface ClassInvitationCodesResponse {
+ classInvitationCodes: ClassInvitationCode[];
+}
+
+interface CreateClassScheduleResponse {
+ createClassSchedule: ClassScheduleItem;
+}
+
+interface UpdateClassScheduleResponse {
+ updateClassSchedule: ClassScheduleItem;
+}
+
+// ===== Hooks: 课表 CRUD(@contract-pending,MSW 兜底)=====
+
+/**
+ * 查询班级课表条目列表(@contract-pending,MSW 兜底)。
+ * 复用 GET_CLASS_SCHEDULE_DOC,返回 items 数组,便于管理端按条目 CRUD。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 管理端课表编辑 / §11.4 契约工单
+ */
+export function useClassSchedules(
+ classId: string,
+ options?: ClassQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery(
+ GET_CLASS_SCHEDULE_DOC,
+ { classId },
+ {
+ ...options,
+ enabled: options?.enabled ?? classId.length > 0,
+ },
+ );
+ return {
+ data: result.data?.classSchedule?.items ?? [],
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 创建班级课表条目(@contract-pending,MSW 兜底)。
+ */
+export function useCreateClassSchedule(): {
+ run: (input: ClassScheduleInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ CreateClassScheduleResponse,
+ { input: ClassScheduleInput }
+ >(CREATE_CLASS_SCHEDULE_DOC);
+
+ const run = async (input: ClassScheduleInput): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.createClassSchedule) {
+ throw new ApiError("Failed to create class schedule", "INTERNAL_ERROR");
+ }
+ return data.createClassSchedule;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 更新班级课表条目(@contract-pending,MSW 兜底)。
+ */
+export function useUpdateClassSchedule(): {
+ run: (id: string, input: ClassScheduleInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ UpdateClassScheduleResponse,
+ { id: string; input: ClassScheduleInput }
+ >(UPDATE_CLASS_SCHEDULE_DOC);
+
+ const run = async (
+ id: string,
+ input: ClassScheduleInput,
+ ): Promise => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateClassSchedule) {
+ throw new ApiError("Failed to update class schedule", "INTERNAL_ERROR");
+ }
+ return data.updateClassSchedule;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 删除班级课表条目(@contract-pending,MSW 兜底)。
+ */
+export function useDeleteClassSchedule(): {
+ run: (id: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<{ deleteClassSchedule: boolean }, { id: string }>(
+ DELETE_CLASS_SCHEDULE_DOC,
+ );
+
+ const run = async (id: string): Promise => {
+ const data = await rawRun({ id });
+ if (
+ data?.deleteClassSchedule === undefined ||
+ data?.deleteClassSchedule === null
+ ) {
+ throw new ApiError("Failed to delete class schedule", "INTERNAL_ERROR");
+ }
+ return data.deleteClassSchedule;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== Hooks: 班级邀请码(@contract-pending,MSW 兜底)=====
+
+/**
+ * 查询班级邀请码列表(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 管理端邀请码管理 / §11.4 契约工单
+ */
+export function useClassInvitationCodes(
+ classId: string,
+ options?: ClassQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ ClassInvitationCodesResponse,
+ { classId: string }
+ >(
+ GET_CLASS_INVITATION_CODES_DOC,
+ { classId },
+ {
+ ...options,
+ enabled: options?.enabled ?? classId.length > 0,
+ },
+ );
+ return {
+ data: result.data?.classInvitationCodes ?? [],
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 生成班级邀请码(@contract-pending,MSW 兜底)。
+ */
+export function useGenerateClassInvitationCode(): {
+ run: (
+ input: GenerateClassInvitationCodeInput,
+ ) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { generateClassInvitationCode: ClassInvitationCode },
+ { input: GenerateClassInvitationCodeInput }
+ >(GENERATE_CLASS_INVITATION_CODE_DOC);
+
+ const run = async (
+ input: GenerateClassInvitationCodeInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.generateClassInvitationCode) {
+ throw new ApiError(
+ "Failed to generate class invitation code",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.generateClassInvitationCode;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 撤销班级邀请码(@contract-pending,MSW 兜底)。
+ */
+export function useRevokeClassInvitationCode(): {
+ run: (codeId: string) => Promise<{ id: string; status: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { revokeClassInvitationCode: { id: string; status: string } },
+ { codeId: string }
+ >(REVOKE_CLASS_INVITATION_CODE_DOC);
+
+ const run = async (
+ codeId: string,
+ ): Promise<{ id: string; status: string }> => {
+ const data = await rawRun({ codeId });
+ if (!data?.revokeClassInvitationCode) {
+ throw new ApiError(
+ "Failed to revoke class invitation code",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.revokeClassInvitationCode;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== 班级 CRUD 输入类型(@contract-pending)=====
+
+/** 创建/更新班级输入 */
+export interface ClassInput {
+ name: string;
+ gradeId: string;
+ headTeacherId: string | null;
+ description: string | null;
+}
+
+// ===== 响应类型(@contract-pending)=====
+
+interface CreateClassResponse {
+ createClass: ClassInfo;
+}
+
+interface UpdateClassResponse {
+ updateClass: ClassInfo;
+}
+
+// ===== Hooks: 班级 CRUD(@contract-pending,MSW 兜底)=====
+
+/**
+ * 创建班级(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 管理端班级编辑 / §11.4 契约工单
+ */
+export function useCreateClass(): {
+ run: (input: ClassInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ CREATE_CLASS_DOC,
+ );
+
+ const run = async (input: ClassInput): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.createClass) {
+ throw new ApiError("Failed to create class", "INTERNAL_ERROR");
+ }
+ return data.createClass;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 更新班级(@contract-pending,MSW 兜底)。
+ */
+export function useUpdateClass(): {
+ run: (id: string, input: ClassInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ UPDATE_CLASS_DOC,
+ );
+
+ const run = async (id: string, input: ClassInput): Promise => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateClass) {
+ throw new ApiError("Failed to update class", "INTERNAL_ERROR");
+ }
+ return data.updateClass;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 删除班级(@contract-pending,MSW 兜底)。
+ */
+export function useDeleteClass(): {
+ run: (id: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<{ deleteClass: boolean }, { id: string }>(
+ DELETE_CLASS_DOC,
+ );
+
+ const run = async (id: string): Promise => {
+ const data = await rawRun({ id });
+ if (data?.deleteClass === undefined || data?.deleteClass === null) {
+ throw new ApiError("Failed to delete class", "INTERNAL_ERROR");
+ }
+ return data.deleteClass;
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/course-plans.ts b/apps/portal-shell/src/lib/api/course-plans.ts
index 8ed2bde..dba43a3 100644
--- a/apps/portal-shell/src/lib/api/course-plans.ts
+++ b/apps/portal-shell/src/lib/api/course-plans.ts
@@ -19,6 +19,7 @@ import { ApiError } from "./errors";
import {
CREATE_COURSE_PLAN_DOC,
GET_COURSE_PLAN_DOC,
+ GET_COURSE_PLAN_TEMPLATES_DOC,
GET_COURSE_PLANS_DOC,
UPDATE_COURSE_PLAN_DOC,
} from "./operations/course-plans.graphql";
@@ -77,6 +78,20 @@ export interface CoursePlanListItem {
updatedAt: string;
}
+/**
+ * 课程计划模板(@contract-pending,MSW 兜底)。
+ *
+ * 用于 create 页"从模板创建"对话框。模板是可复用的课程计划骨架,
+ * 字段最小化(不含 units / progress 等详情字段)。
+ */
+export interface CoursePlanTemplate {
+ id: string;
+ name: string;
+ description: string;
+ subject: string;
+ gradeLevel: string;
+}
+
/** 列表查询响应(@contract-pending 假契约形状,MSW 返回此结构) */
interface CoursePlansListResponse {
coursePlans: {
@@ -90,6 +105,11 @@ interface CoursePlanResponse {
coursePlan: CoursePlanDetail | null;
}
+/** 模板列表查询响应(@contract-pending,MSW 返回此结构) */
+interface CoursePlanTemplatesResponse {
+ coursePlanTemplates: CoursePlanTemplate[];
+}
+
/** 创建课程计划输入 */
export interface CreateCoursePlanInput {
name: string;
@@ -130,6 +150,12 @@ export interface CoursePlansListFilter {
offset?: number;
}
+/** 模板查询筛选(按学科 / 年级过滤,均可选) */
+export interface CoursePlanTemplatesFilter {
+ subjectId?: string;
+ gradeLevel?: string;
+}
+
// ===== 查询选项 =====
export interface CoursePlanQueryOptions {
@@ -214,6 +240,41 @@ export function useCoursePlan(
};
}
+/**
+ * 查询课程计划模板列表(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 coursePlanTemplates 根字段,由 MSW handlers 返回 mock 数据。
+ * 用于 create 页"从模板创建"对话框,列出可复用的课程计划模板。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useCoursePlanTemplates(
+ filter?: CoursePlanTemplatesFilter,
+ options?: CoursePlanQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ CoursePlanTemplatesResponse,
+ { subjectId?: string; gradeLevel?: string }
+ >(
+ GET_COURSE_PLAN_TEMPLATES_DOC,
+ {
+ subjectId: filter?.subjectId,
+ gradeLevel: filter?.gradeLevel,
+ },
+ {
+ enabled: options?.enabled ?? true,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.coursePlanTemplates,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
/**
* 创建课程计划 mutation(@contract-pending,MSW 兜底)。
*
diff --git a/apps/portal-shell/src/lib/api/error-book.ts b/apps/portal-shell/src/lib/api/error-book.ts
index a123caa..e207d0f 100644
--- a/apps/portal-shell/src/lib/api/error-book.ts
+++ b/apps/portal-shell/src/lib/api/error-book.ts
@@ -20,7 +20,10 @@ import type { FetchPolicy } from "@apollo/client";
import { useWidgetQuery } from "../useWidgetQuery";
import { GET_ERROR_BOOK_STATS_DOC } from "./operations/dashboard.graphql";
-import { GET_ERROR_BOOK_ITEMS_DOC } from "./operations/error-book.graphql";
+import {
+ GET_ERROR_BOOK_ITEMS_DOC,
+ GET_ERROR_BOOK_DETAIL_DOC,
+} from "./operations/error-book.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型(对齐 data-ana 子图,snake_case)=====
@@ -149,3 +152,309 @@ export function useTeacherErrorBookStats(
refetch: result.refetch,
};
}
+
+// ===== 错题详情(@contract-pending MSW 兜底)=====
+
+/** 错题详情(camelCase 对齐 admin 子图风格,用于详情对话框)*/
+export interface ErrorBookItemDetail {
+ itemId: string;
+ content: string;
+ subjectId: string;
+ subjectName: string;
+ studentId: string;
+ studentName: string;
+ className: string;
+ errorCount: number;
+ lastErrorTime: string;
+ knowledgePointId: string;
+ knowledgePointTitle: string;
+ analysis: string;
+ correctAnswer: string;
+}
+
+/** errorBookDetail 查询响应 */
+interface ErrorBookDetailResponse {
+ errorBookDetail: ErrorBookItemDetail | null;
+}
+
+/**
+ * 查询错题详情(@contract-pending MSW 兜底)。
+ *
+ * 按 itemId 拉取错题完整详情,用于详情对话框。
+ * itemId 为空时跳过查询(enabled=false)。
+ *
+ * 关联:ARCHITECTURE.md §5.3 契约纪律 / §9.1
+ */
+export function useErrorBookDetail(
+ itemId: string,
+ options?: ErrorBookQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery(
+ GET_ERROR_BOOK_DETAIL_DOC,
+ { itemId },
+ {
+ enabled: (options?.enabled ?? true) && itemId.length > 0,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.errorBookDetail ?? null,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+// ===== Mutation Hooks(@contract-pending MSW 兜底,迁移自 CICD actions.ts)=====
+//
+// portal-shell 使用 Apollo Client,CICD 使用 Server Actions。
+// 以下 hooks 等价封装 CICD actions.ts 的 5 个 mutation 操作。
+import { useWidgetMutation } from "../useWidgetMutation";
+import {
+ CREATE_ERROR_BOOK_ITEM_DOC,
+ UPDATE_ERROR_BOOK_NOTE_DOC,
+ REVIEW_ERROR_BOOK_ITEM_DOC,
+ ARCHIVE_ERROR_BOOK_ITEM_DOC,
+ DELETE_ERROR_BOOK_ITEM_DOC,
+} from "./operations/error-book-mutations.graphql";
+import { ApiError } from "./errors";
+
+/** 创建错题输入(对齐 CreateErrorBookItemInput) */
+export interface CreateErrorBookItemInput {
+ questionId: string;
+ studentAnswer?: unknown;
+ correctAnswer?: unknown;
+ subjectId?: string;
+ knowledgePointIds?: string[];
+ note?: string;
+ errorTags?: string[];
+}
+
+/** 更新笔记输入 */
+export interface UpdateErrorBookNoteInput {
+ itemId: string;
+ note?: string;
+ errorTags?: string[];
+}
+
+/** 复习结果输入 */
+export interface ReviewErrorBookItemInput {
+ itemId: string;
+ result: "again" | "hard" | "good" | "easy";
+}
+
+/** 复习结果响应 */
+export interface ReviewErrorBookItemResponse {
+ itemId: string;
+ newInterval: number;
+ newMasteryLevel: number;
+ newStatus: string;
+ nextReviewAt: string;
+}
+
+interface CreateErrorBookItemResponse {
+ createErrorBookItem: { itemId: string } | null;
+}
+
+interface UpdateErrorBookNoteResponse {
+ updateErrorBookNote: { itemId: string } | null;
+}
+
+interface ReviewErrorBookItemMutationResponse {
+ reviewErrorBookItem: ReviewErrorBookItemResponse | null;
+}
+
+interface ArchiveErrorBookItemResponse {
+ archiveErrorBookItem: { itemId: string } | null;
+}
+
+interface DeleteErrorBookItemResponse {
+ deleteErrorBookItem: { itemId: string } | null;
+}
+
+/** ActionState 兼容返回类型(对齐 CICD ActionState) */
+export interface ActionState {
+ success: boolean;
+ data?: T;
+ message?: string;
+}
+
+/**
+ * 创建错题(手动添加)mutation(@contract-pending MSW 兜底)。
+ */
+export function useAddErrorBook(): {
+ run: (input: CreateErrorBookItemInput) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ CreateErrorBookItemResponse,
+ { input: CreateErrorBookItemInput }
+ >(CREATE_ERROR_BOOK_ITEM_DOC);
+
+ const run = async (
+ input: CreateErrorBookItemInput,
+ ): Promise> => {
+ try {
+ const data = await rawRun({ input });
+ if (!data?.createErrorBookItem) {
+ throw new ApiError("Failed to add error book item", "INTERNAL_ERROR");
+ }
+ return {
+ success: true,
+ data: data.createErrorBookItem.itemId,
+ message: "错题已添加",
+ };
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "添加错题失败";
+ return { success: false, message };
+ }
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 更新错题笔记 mutation(@contract-pending MSW 兜底)。
+ */
+export function useUpdateErrorBookNote(): {
+ run: (input: UpdateErrorBookNoteInput) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ UpdateErrorBookNoteResponse,
+ { input: UpdateErrorBookNoteInput }
+ >(UPDATE_ERROR_BOOK_NOTE_DOC);
+
+ const run = async (
+ input: UpdateErrorBookNoteInput,
+ ): Promise> => {
+ try {
+ const data = await rawRun({ input });
+ if (!data?.updateErrorBookNote) {
+ throw new ApiError("Failed to update note", "INTERNAL_ERROR");
+ }
+ return { success: true, message: "笔记已更新" };
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "更新笔记失败";
+ return { success: false, message };
+ }
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 记录复习结果 mutation(@contract-pending MSW 兜底)。
+ */
+export function useReviewErrorBook(): {
+ run: (
+ input: ReviewErrorBookItemInput,
+ ) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ ReviewErrorBookItemMutationResponse,
+ { input: ReviewErrorBookItemInput }
+ >(REVIEW_ERROR_BOOK_ITEM_DOC);
+
+ const run = async (
+ input: ReviewErrorBookItemInput,
+ ): Promise> => {
+ try {
+ const data = await rawRun({ input });
+ if (!data?.reviewErrorBookItem) {
+ throw new ApiError("Failed to record review", "INTERNAL_ERROR");
+ }
+ return {
+ success: true,
+ data: data.reviewErrorBookItem,
+ message: "复习结果已记录",
+ };
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "记录复习结果失败";
+ return { success: false, message };
+ }
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 归档错题 mutation(@contract-pending MSW 兜底)。
+ */
+export function useArchiveErrorBook(): {
+ run: (itemId: string) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ ARCHIVE_ERROR_BOOK_ITEM_DOC,
+ );
+
+ const run = async (itemId: string): Promise> => {
+ try {
+ const data = await rawRun({ itemId });
+ if (!data?.archiveErrorBookItem) {
+ throw new ApiError("Failed to archive item", "INTERNAL_ERROR");
+ }
+ return { success: true, message: "错题已归档" };
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "归档错题失败";
+ return { success: false, message };
+ }
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 删除错题 mutation(@contract-pending MSW 兜底)。
+ */
+export function useDeleteErrorBook(): {
+ run: (itemId: string) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DELETE_ERROR_BOOK_ITEM_DOC,
+ );
+
+ const run = async (itemId: string): Promise> => {
+ try {
+ const data = await rawRun({ itemId });
+ if (!data?.deleteErrorBookItem) {
+ throw new ApiError("Failed to delete item", "INTERNAL_ERROR");
+ }
+ return { success: true, message: "错题已删除" };
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "删除错题失败";
+ return { success: false, message };
+ }
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/exams.ts b/apps/portal-shell/src/lib/api/exams.ts
index 8199afd..db1b365 100644
--- a/apps/portal-shell/src/lib/api/exams.ts
+++ b/apps/portal-shell/src/lib/api/exams.ts
@@ -20,6 +20,8 @@ import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
CREATE_EXAM_DOC,
+ DELETE_EXAM_DOC,
+ DUPLICATE_EXAM_DOC,
GET_EXAM_ANALYTICS_DOC,
GET_EXAM_BUILD_DOC,
GET_EXAM_DOC,
@@ -28,6 +30,7 @@ import {
GET_QUESTIONS_LIBRARY_DOC,
SAVE_EXAM_BUILD_DOC,
SAVE_EXAM_RICH_CONTENT_DOC,
+ UPDATE_EXAM_STATUS_DOC,
} from "./operations/exams.graphql";
import type { UseQueryResult } from "./types";
@@ -542,3 +545,118 @@ export function useSaveExamRichContent(): {
return { run, loading, error };
}
+
+// ===== Delete / UpdateStatus / Duplicate Hooks(@contract-pending)=====
+
+/** 删除考试响应 */
+interface DeleteExamResponse {
+ deleteExam: { id: string } | null;
+}
+
+/** 更新考试状态响应 */
+interface UpdateExamStatusResponse {
+ updateExamStatus: { id: string; status: string } | null;
+}
+
+/** 复制考试响应 */
+interface DuplicateExamResponse {
+ duplicateExam: { id: string } | null;
+}
+
+/**
+ * 删除考试(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
+ * 后端补齐 mutation 后切换到真实 fetcher。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
+ */
+export function useDeleteExam(): {
+ run: (id: string) => Promise<{ id: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(DELETE_EXAM_DOC);
+
+ const run = async (id: string): Promise<{ id: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteExam) {
+ throw new ApiError("Failed to delete exam", "INTERNAL_ERROR");
+ }
+ return data.deleteExam;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 更新考试状态(@contract-pending,MSW 兜底)。
+ *
+ * 用于发布/归档/恢复草稿等状态流转。
+ * 后端补齐 mutation 后切换到真实 fetcher。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
+ */
+export function useUpdateExamStatus(): {
+ run: (id: string, status: string) => Promise<{ id: string; status: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ UpdateExamStatusResponse,
+ { id: string; status: string }
+ >(UPDATE_EXAM_STATUS_DOC);
+
+ const run = async (
+ id: string,
+ status: string,
+ ): Promise<{ id: string; status: string }> => {
+ const data = await rawRun({ id, status });
+ if (!data?.updateExamStatus) {
+ throw new ApiError("Failed to update exam status", "INTERNAL_ERROR");
+ }
+ return data.updateExamStatus;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 复制考试(@contract-pending,MSW 兜底)。
+ *
+ * 复制考试基本信息生成新考试(不含题目结构)。
+ * 后端补齐 mutation 后切换到真实 fetcher。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
+ */
+export function useDuplicateExam(): {
+ run: (id: string) => Promise<{ id: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DUPLICATE_EXAM_DOC,
+ );
+
+ const run = async (id: string): Promise<{ id: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.duplicateExam) {
+ throw new ApiError("Failed to duplicate exam", "INTERNAL_ERROR");
+ }
+ return data.duplicateExam;
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/grades.ts b/apps/portal-shell/src/lib/api/grades.ts
index 109ccd5..3e103cf 100644
--- a/apps/portal-shell/src/lib/api/grades.ts
+++ b/apps/portal-shell/src/lib/api/grades.ts
@@ -22,12 +22,25 @@ import { useWidgetMutation } from "../useWidgetMutation";
import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
+ ACQUIRE_DRAFT_LOCK_DOC,
+ BATCH_CREATE_GRADE_RECORDS_BY_EXAM_DOC,
+ BULK_DELETE_GRADES_DOC,
CREATE_GRADE_DOC,
+ DELETE_GRADE_RECORD_DOC,
+ DOWNLOAD_GRADE_IMPORT_TEMPLATE_DOC,
+ GET_DRAFT_LOCK_DOC,
+ GET_EXAM_FOR_GRADE_ENTRY_DOC,
+ GET_EXAM_OPTIONS_FOR_ENTRY_DOC,
GET_GRADE_ANALYTICS_DOC,
GET_GRADE_DOC,
GET_GRADES_LIST_DOC,
GET_GRADE_STATS_DOC,
GET_REPORT_CARD_DOC,
+ GET_SCHOOL_WIDE_GRADE_SUMMARY_DOC,
+ IMPORT_GRADES_FROM_EXCEL_DOC,
+ RELEASE_DRAFT_LOCK_DOC,
+ UNDO_GRADE_ENTRY_DOC,
+ UPDATE_GRADE_RECORD_DOC,
} from "./operations/grades.graphql";
import type { UseQueryResult } from "./types";
@@ -424,3 +437,647 @@ export function useReportCard(
refetch: result.refetch,
};
}
+
+// ===== 更新/删除/批量删除(@contract-pending 全 MSW)=====
+
+/** 更新成绩输入 */
+export interface UpdateGradeInput {
+ title?: string;
+ score?: number;
+ totalScore?: number;
+ type?: string;
+ semester?: string;
+ remark?: string | null;
+}
+
+/** UpdateGrade mutation 响应 */
+interface UpdateGradeResponse {
+ updateGrade: { gradeId: string } | null;
+}
+
+/** DeleteGrade mutation 响应 */
+interface DeleteGradeResponse {
+ deleteGrade: { gradeId: string } | null;
+}
+
+/** BulkDeleteGrades mutation 响应 */
+interface BulkDeleteGradesResponse {
+ bulkDeleteGrades: { count: number } | null;
+}
+
+/**
+ * 更新成绩记录 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 注:命名为 useUpdateGradeRecord 以区分 admin-p5.ts 中的 useUpdateGrade
+ * (后者用于年级 Grade 实体管理,非成绩记录)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useUpdateGradeRecord(): {
+ run: (id: string, input: UpdateGradeInput) => Promise<{ gradeId: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ UpdateGradeResponse,
+ { id: string; input: UpdateGradeInput }
+ >(UPDATE_GRADE_RECORD_DOC);
+
+ const run = async (
+ id: string,
+ input: UpdateGradeInput,
+ ): Promise<{ gradeId: string }> => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateGrade) {
+ throw new ApiError("Failed to update grade", "INTERNAL_ERROR");
+ }
+ return data.updateGrade;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 删除成绩记录 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 注:命名为 useDeleteGradeRecord 以区分 admin-p5.ts 中的 useDeleteGrade
+ * (后者用于年级 Grade 实体管理,非成绩记录)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useDeleteGradeRecord(): {
+ run: (id: string) => Promise<{ gradeId: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DELETE_GRADE_RECORD_DOC,
+ );
+
+ const run = async (id: string): Promise<{ gradeId: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteGrade) {
+ throw new ApiError("Failed to delete grade", "INTERNAL_ERROR");
+ }
+ return data.deleteGrade;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 批量删除成绩 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useBulkDeleteGrades(): {
+ run: (ids: string[]) => Promise<{ count: number }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ BULK_DELETE_GRADES_DOC,
+ );
+
+ const run = async (ids: string[]): Promise<{ count: number }> => {
+ const data = await rawRun({ ids });
+ if (!data?.bulkDeleteGrades) {
+ throw new ApiError("Failed to bulk delete grades", "INTERNAL_ERROR");
+ }
+ return data.bulkDeleteGrades;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== 按试卷批量录入(@contract-pending 全 MSW)=====
+
+/** 批量录入单条记录输入 */
+export interface BatchGradeRecordInput {
+ studentId: string;
+ answers: Array<{ questionId: string; score: number }>;
+ remark?: string;
+}
+
+/** BatchCreateGradeRecordsByExam mutation 响应 */
+interface BatchCreateGradeRecordsByExamResponse {
+ batchCreateGradeRecordsByExam: { gradeIds: string[] } | null;
+}
+
+/** UndoGradeEntry mutation 响应 */
+interface UndoGradeEntryResponse {
+ undoGradeEntry: { count: number } | null;
+}
+
+/**
+ * 按试卷批量录入成绩 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useBatchCreateGradeRecordsByExam(): {
+ run: (
+ examId: string,
+ classId: string,
+ records: BatchGradeRecordInput[],
+ ) => Promise<{ gradeIds: string[] }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ BatchCreateGradeRecordsByExamResponse,
+ {
+ examId: string;
+ classId: string;
+ records: BatchGradeRecordInput[];
+ }
+ >(BATCH_CREATE_GRADE_RECORDS_BY_EXAM_DOC);
+
+ const run = async (
+ examId: string,
+ classId: string,
+ records: BatchGradeRecordInput[],
+ ): Promise<{ gradeIds: string[] }> => {
+ const data = await rawRun({ examId, classId, records });
+ if (!data?.batchCreateGradeRecordsByExam) {
+ throw new ApiError("Failed to batch create grades", "INTERNAL_ERROR");
+ }
+ return data.batchCreateGradeRecordsByExam;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 撤销成绩录入 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useUndoGradeEntry(): {
+ run: (gradeIds: string[]) => Promise<{ count: number }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ UNDO_GRADE_ENTRY_DOC,
+ );
+
+ const run = async (gradeIds: string[]): Promise<{ count: number }> => {
+ const data = await rawRun({ gradeIds });
+ if (!data?.undoGradeEntry) {
+ throw new ApiError("Failed to undo grade entry", "INTERNAL_ERROR");
+ }
+ return data.undoGradeEntry;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== Excel 导入/模板下载(@contract-pending 全 MSW)=====
+
+/** Excel 导入无效行 */
+export interface GradeImportInvalidRow {
+ row: number;
+ studentName: string;
+ score: string;
+ errors: string[];
+}
+
+/** Excel 导入结果 */
+export interface GradeImportResult {
+ success: boolean;
+ totalRows: number;
+ successCount: number;
+ failedCount: number;
+ unmatchedStudents: string[];
+ invalidRows: GradeImportInvalidRow[];
+}
+
+/** 模板下载响应 */
+interface DownloadGradeImportTemplateResponse {
+ downloadGradeImportTemplate: { buffer: string; filename: string } | null;
+}
+
+/** Excel 导入响应 */
+interface ImportGradesFromExcelResponse {
+ importGradesFromExcel: GradeImportResult | null;
+}
+
+/** 模板下载结果 */
+export interface GradeImportTemplate {
+ buffer: string;
+ filename: string;
+}
+
+/**
+ * 下载成绩导入模板(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useDownloadGradeImportTemplate(): {
+ run: (classId: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ DownloadGradeImportTemplateResponse,
+ { classId: string }
+ >(DOWNLOAD_GRADE_IMPORT_TEMPLATE_DOC);
+
+ const run = async (classId: string): Promise => {
+ const data = await rawRun({ classId });
+ if (!data?.downloadGradeImportTemplate) {
+ throw new ApiError("Failed to download template", "INTERNAL_ERROR");
+ }
+ return data.downloadGradeImportTemplate;
+ };
+
+ return { run, loading, error };
+}
+
+/** Excel 导入输入 */
+export interface ImportGradesFromExcelInput {
+ file: string;
+ classId: string;
+ subjectId: string;
+ title: string;
+ examId?: string;
+ fullScore?: number;
+ type?: string;
+ semester?: string;
+}
+
+/**
+ * 从 Excel 导入成绩(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useImportGradesFromExcel(): {
+ run: (input: ImportGradesFromExcelInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ ImportGradesFromExcelResponse,
+ ImportGradesFromExcelInput
+ >(IMPORT_GRADES_FROM_EXCEL_DOC);
+
+ const run = async (
+ input: ImportGradesFromExcelInput,
+ ): Promise => {
+ const data = await rawRun(input);
+ if (!data?.importGradesFromExcel) {
+ throw new ApiError("Failed to import grades", "INTERNAL_ERROR");
+ }
+ return data.importGradesFromExcel;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== 成绩录入用考试/学生查询(@contract-pending 全 MSW)=====
+
+/** 成绩录入用考试选项 */
+export interface ExamOptionForEntry {
+ id: string;
+ title: string;
+ subjectName: string;
+ questionCount: number;
+ totalScore: number;
+ classId: string;
+ gradeId?: string;
+}
+
+/** 批量录入试卷题目 */
+export interface ExamForGradeEntryQuestion {
+ id: string;
+ title: string;
+ score: number;
+}
+
+/** 批量录入试卷学生 */
+export interface ExamForGradeEntryStudent {
+ id: string;
+ name: string;
+ studentNo: string;
+}
+
+/** 批量录入用试卷实体 */
+export interface ExamForGradeEntry {
+ id: string;
+ title: string;
+ fullScore: number;
+ classId: string;
+ gradeId?: string;
+ questions: ExamForGradeEntryQuestion[];
+ students: ExamForGradeEntryStudent[];
+}
+
+/** ExamOptionsForEntry 查询响应 */
+interface ExamOptionsForEntryResponse {
+ examOptionsForEntry: ExamOptionForEntry[];
+}
+
+/** ExamForGradeEntry 查询响应 */
+interface ExamForGradeEntryResponse {
+ examForGradeEntry: ExamForGradeEntry | null;
+}
+
+/**
+ * 查询成绩录入用考试选项列表(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useExamOptionsForEntry(
+ options?: GradeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ ExamOptionsForEntryResponse,
+ Record
+ >(
+ GET_EXAM_OPTIONS_FOR_ENTRY_DOC,
+ {},
+ {
+ ...options,
+ enabled: options?.enabled ?? true,
+ },
+ );
+ return {
+ data: result.data?.examOptionsForEntry ?? [],
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 查询批量录入用试卷详情(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useExamForGradeEntry(
+ examId: string,
+ options?: GradeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery(
+ GET_EXAM_FOR_GRADE_ENTRY_DOC,
+ { examId },
+ {
+ ...options,
+ enabled: options?.enabled ?? examId.length > 0,
+ },
+ );
+ return {
+ data: result.data?.examForGradeEntry ?? null,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+// ===== 草稿锁(@contract-pending 全 MSW)=====
+
+/** 草稿锁状态 */
+export interface DraftLockStatus {
+ isLocked: boolean;
+ lockedBy: string | null;
+ lockedByName: string | null;
+ lockedAt: string | null;
+ isMine: boolean;
+}
+
+/** DraftLock 查询响应 */
+interface DraftLockResponse {
+ draftLock: DraftLockStatus | null;
+}
+
+/** AcquireDraftLock mutation 响应 */
+interface AcquireDraftLockResponse {
+ acquireDraftLock: DraftLockStatus | null;
+}
+
+/** ReleaseDraftLock mutation 响应 */
+interface ReleaseDraftLockResponse {
+ releaseDraftLock: DraftLockStatus | null;
+}
+
+/**
+ * 查询草稿锁状态(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useDraftLock(
+ scope: string,
+ options?: GradeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery(
+ GET_DRAFT_LOCK_DOC,
+ { scope },
+ {
+ ...options,
+ enabled: options?.enabled ?? scope.length > 0,
+ },
+ );
+ return {
+ data: result.data?.draftLock ?? null,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 获取草稿锁 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useAcquireDraftLock(): {
+ run: (scope: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ ACQUIRE_DRAFT_LOCK_DOC,
+ );
+
+ const run = async (scope: string): Promise => {
+ const data = await rawRun({ scope });
+ if (!data?.acquireDraftLock) {
+ throw new ApiError("Failed to acquire draft lock", "INTERNAL_ERROR");
+ }
+ return data.acquireDraftLock;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 释放草稿锁 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4
+ */
+export function useReleaseDraftLock(): {
+ run: (scope: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ RELEASE_DRAFT_LOCK_DOC,
+ );
+
+ const run = async (scope: string): Promise => {
+ const data = await rawRun({ scope });
+ if (!data?.releaseDraftLock) {
+ throw new ApiError("Failed to release draft lock", "INTERNAL_ERROR");
+ }
+ return data.releaseDraftLock;
+ };
+
+ return { run, loading, error };
+}
+
+// ===== 全校年级成绩洞察(@contract-pending 全 MSW)=====
+
+/** 全校年级成绩汇总统计 */
+export interface SchoolWideOverallStats {
+ averageScore: number;
+ passRate: number;
+ excellenceRate: number;
+ totalParticipants: number;
+ /** 全校班级总数(@contract-pending,MSW 兜底) */
+ classes?: number;
+ /** 全校学生总数(@contract-pending,MSW 兜底) */
+ students?: number;
+ /** 整体平均分(@contract-pending,MSW 兜底) */
+ overallAvg?: number;
+ /** 最新一次作业平均分(@contract-pending,MSW 兜底) */
+ latestAvg?: number;
+}
+
+/** 班级排名项(年级洞察页 classRankings) */
+export interface GradeInsightsClassRanking {
+ classId: string;
+ className: string;
+ averageScore: number;
+ passRate: number;
+ studentCount: number;
+ rank: number;
+ delta: number;
+ /** 上次平均分(@contract-pending,MSW 兜底) */
+ prevAvg?: number;
+ /** 整体平均分(@contract-pending,MSW 兜底) */
+ overallAvg?: number;
+}
+
+/** 年级洞察项(年级洞察页 grades[]) */
+export interface GradeInsightsGrade {
+ gradeId: string;
+ gradeName: string;
+ averageScore: number;
+ passRate: number;
+ excellenceRate: number;
+ participantCount: number;
+ classRankings: GradeInsightsClassRanking[];
+}
+
+/** 最近作业项(年级洞察页 recentAssignments[]) */
+export interface GradeInsightsAssignment {
+ assignmentId: string;
+ title: string;
+ subjectName: string;
+ gradeName: string;
+ averageScore: number;
+ submitCount: number;
+ totalStudents: number;
+ status: string;
+ createdAt: string;
+ /** 应交人数(@contract-pending,MSW 兜底) */
+ targeted?: number;
+ /** 已批改人数(@contract-pending,MSW 兜底) */
+ graded?: number;
+ /** 中位数(@contract-pending,MSW 兜底) */
+ median?: number;
+}
+
+/** 全校年级成绩洞察聚合结果 */
+export interface SchoolWideGradeInsights {
+ overallStats: SchoolWideOverallStats;
+ grades: GradeInsightsGrade[];
+ recentAssignments: GradeInsightsAssignment[];
+}
+
+/** SchoolWideGradeSummary 查询响应 */
+interface SchoolWideGradeInsightsResponse {
+ schoolWideGradeSummary: SchoolWideGradeInsights | null;
+}
+
+/**
+ * 查询全校年级成绩洞察(@contract-pending,MSW 兜底)。
+ *
+ * 用于 /shell/admin/school/grades/insights 年级洞察页:
+ * - overallStats:全校平均分、及格率、优秀率、参考人数
+ * - grades[]:各年级统计 + 班级排名
+ * - recentAssignments[]:最近作业列表
+ *
+ * schema 无 schoolWideGradeSummary 根字段,由 MSW handlers 返回 mock 数据。
+ * 后端补齐查询后切换到真实 fetcher,页面无需改动。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 管理域 / §11.4 契约工单
+ */
+export function useSchoolWideGradeSummary(
+ gradeId?: string,
+ options?: GradeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ SchoolWideGradeInsightsResponse,
+ { gradeId?: string }
+ >(
+ GET_SCHOOL_WIDE_GRADE_SUMMARY_DOC,
+ { gradeId },
+ {
+ ...options,
+ enabled: options?.enabled ?? true,
+ },
+ );
+ return {
+ data: result.data?.schoolWideGradeSummary ?? null,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
diff --git a/apps/portal-shell/src/lib/api/homework.ts b/apps/portal-shell/src/lib/api/homework.ts
index e0ad48c..2e7c641 100644
--- a/apps/portal-shell/src/lib/api/homework.ts
+++ b/apps/portal-shell/src/lib/api/homework.ts
@@ -19,15 +19,21 @@ import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
ASSIGN_HOMEWORK_DOC,
+ BATCH_AUTO_GRADE_DOC,
+ DELETE_SCAN_DOC,
GET_AI_BATCH_GRADING_DOC,
GET_ASSIGNMENT_SUBMISSIONS_DOC,
GET_HOMEWORK_DOC,
GET_HOMEWORK_LIST_DOC,
GET_HOMEWORK_SUBMISSIONS_DOC,
+ GET_SCANS_DOC,
GET_SUBMISSION_DETAIL_DOC,
GRADE_SUBMISSION_DOC,
RECORD_GRADE_DOC,
+ SAVE_HOMEWORK_ANSWER_DOC,
SAVE_SCAN_GRADING_DOC,
+ START_HOMEWORK_SUBMISSION_DOC,
+ SUBMIT_HOMEWORK_DOC,
} from "./operations/homework.graphql";
import type { UseQueryResult } from "./types";
@@ -610,3 +616,295 @@ export function useRecordGrade(): {
return { run, loading, error };
}
+
+// ===== 学生作答相关 hooks(@contract-pending 全 MSW)=====
+// 用于 /shell/teacher/homework/[id]/take 学生作答页
+// 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+
+/** 扫描图(与 ScanImage 对齐,供组件直接消费) */
+export interface HomeworkScan {
+ fileId: string;
+ url: string;
+ filename: string;
+ originalName?: string;
+ page: number;
+}
+
+/** 开始作答输入 */
+export interface StartHomeworkSubmissionInput {
+ assignmentId: string;
+}
+
+/** 开始作答响应 */
+interface StartHomeworkSubmissionResponse {
+ startHomeworkSubmission: { submissionId: string } | null;
+}
+
+/** 保存单题答案输入 */
+export interface SaveHomeworkAnswerInput {
+ submissionId: string;
+ questionId: string;
+ answerJson: string;
+}
+
+/** 保存单题答案响应 */
+interface SaveHomeworkAnswerResponse {
+ saveHomeworkAnswer: { submissionId: string; questionId: string } | null;
+}
+
+/** 提交作业输入 */
+export interface SubmitHomeworkInput {
+ submissionId: string;
+}
+
+/** 提交作业响应 */
+interface SubmitHomeworkResponse {
+ submitHomework: { submissionId: string; totalScore: number | null } | null;
+}
+
+/** 扫描图查询响应 */
+interface GetScansResponse {
+ scans: HomeworkScan[];
+}
+
+/** 删除扫描图输入 */
+export interface DeleteScanInput {
+ submissionId: string;
+ fileId: string;
+}
+
+/** 删除扫描图响应 */
+interface DeleteScanResponse {
+ deleteScan: { fileId: string; success: boolean } | null;
+}
+
+/** 批量自动批改输入 */
+export interface BatchAutoGradeInput {
+ submissionIds: string[];
+}
+
+/** 批量自动批改响应 */
+interface BatchAutoGradeResponse {
+ batchAutoGrade: {
+ processedCount: number;
+ successCount: number;
+ failedCount: number;
+ } | null;
+}
+
+/**
+ * 开始作答 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 学生进入作答页点击"开始作答"时调用,创建一条 started 状态的提交。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+ */
+export function useStartHomeworkSubmission(): {
+ run: (
+ input: StartHomeworkSubmissionInput,
+ ) => Promise<{ submissionId: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ StartHomeworkSubmissionResponse,
+ { input: StartHomeworkSubmissionInput }
+ >(START_HOMEWORK_SUBMISSION_DOC);
+
+ const run = async (
+ input: StartHomeworkSubmissionInput,
+ ): Promise<{ submissionId: string }> => {
+ const data = await rawRun({ input });
+ if (!data?.startHomeworkSubmission) {
+ throw new ApiError(
+ "Failed to start homework submission",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.startHomeworkSubmission;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 保存单题答案 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 学生作答过程中(debounced)保存每题答案。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+ */
+export function useSaveHomeworkAnswer(): {
+ run: (
+ input: SaveHomeworkAnswerInput,
+ ) => Promise<{ submissionId: string; questionId: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ SaveHomeworkAnswerResponse,
+ { input: SaveHomeworkAnswerInput }
+ >(SAVE_HOMEWORK_ANSWER_DOC);
+
+ const run = async (
+ input: SaveHomeworkAnswerInput,
+ ): Promise<{ submissionId: string; questionId: string }> => {
+ const data = await rawRun({ input });
+ if (!data?.saveHomeworkAnswer) {
+ throw new ApiError("Failed to save homework answer", "INTERNAL_ERROR");
+ }
+ return data.saveHomeworkAnswer;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 提交作业 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 学生点击"提交全部"后锁定提交并触发服务端自动判分。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+ */
+export function useSubmitHomework(): {
+ run: (
+ input: SubmitHomeworkInput,
+ ) => Promise<{ submissionId: string; totalScore: number | null }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ SUBMIT_HOMEWORK_DOC,
+ );
+
+ const run = async (
+ input: SubmitHomeworkInput,
+ ): Promise<{ submissionId: string; totalScore: number | null }> => {
+ const data = await rawRun({ input });
+ if (!data?.submitHomework) {
+ throw new ApiError("Failed to submit homework", "INTERNAL_ERROR");
+ }
+ return data.submitHomework;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 查询某次提交的答题扫描图(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 scans 根字段,由 MSW handlers 返回 mock 数据。
+ * submissionId 为 null/空时禁用查询。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+ */
+export function useGetScans(
+ submissionId: string | null,
+ options?: HomeworkQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery(
+ GET_SCANS_DOC,
+ { submissionId: submissionId ?? "" },
+ {
+ ...options,
+ enabled:
+ (options?.enabled ?? true) &&
+ submissionId !== null &&
+ submissionId.length > 0,
+ },
+ );
+ return {
+ data: result.data?.scans,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 删除扫描图 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 学生在作答页删除已上传的扫描图。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 学生作答页 / §11.4 契约工单
+ */
+export function useDeleteScan(): {
+ run: (
+ input: DeleteScanInput,
+ ) => Promise<{ fileId: string; success: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DELETE_SCAN_DOC,
+ );
+
+ const run = async (
+ input: DeleteScanInput,
+ ): Promise<{ fileId: string; success: boolean }> => {
+ const data = await rawRun({ input });
+ if (!data?.deleteScan) {
+ throw new ApiError("Failed to delete scan", "INTERNAL_ERROR");
+ }
+ return data.deleteScan;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 批量自动批改 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 教师在按作业批量批改页一键自动批改多份提交的客观题。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 批量批改页 / §11.4 契约工单
+ */
+export function useBatchAutoGrade(): {
+ run: (input: BatchAutoGradeInput) => Promise<{
+ processedCount: number;
+ successCount: number;
+ failedCount: number;
+ }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ BATCH_AUTO_GRADE_DOC,
+ );
+
+ const run = async (
+ input: BatchAutoGradeInput,
+ ): Promise<{
+ processedCount: number;
+ successCount: number;
+ failedCount: number;
+ }> => {
+ const data = await rawRun({ input });
+ if (!data?.batchAutoGrade) {
+ throw new ApiError("Failed to batch auto grade", "INTERNAL_ERROR");
+ }
+ return data.batchAutoGrade;
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/index.ts b/apps/portal-shell/src/lib/api/index.ts
index 7501dca..c69f869 100644
--- a/apps/portal-shell/src/lib/api/index.ts
+++ b/apps/portal-shell/src/lib/api/index.ts
@@ -46,3 +46,4 @@ export * from "./leave";
// B1 共享页:通知中心 + 设置(@contract-pending 全 MSW)
export * from "./notifications";
export * from "./settings";
+export * from "./profile";
diff --git a/apps/portal-shell/src/lib/api/operations/admin.graphql.ts b/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
index 35f3d8c..6614fa8 100644
--- a/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
@@ -40,6 +40,71 @@ export const UPDATE_USER_ROLE_DOC = gql`
}
`;
+// ── 用户删除(@contract-pending)──
+// schema 无 deleteUser 根字段,MSW 兜底
+// 契约工单:iam_contract.md#user-delete
+export const DELETE_USER_DOC = gql`
+ mutation DeleteUser($id: ID!) {
+ deleteUser(id: $id) {
+ id
+ success
+ }
+ }
+`;
+
+// ── 多角色分配(@contract-pending)──
+// schema 无 assignUserRoles 根字段,MSW 兜底
+// 替换语义:传入完整角色名数组,覆盖原有角色
+// 契约工单:iam_contract.md#user-assign-roles
+export const ASSIGN_USER_ROLES_DOC = gql`
+ mutation AssignUserRoles($userId: ID!, $roleNames: [String!]!) {
+ assignUserRoles(userId: $userId, roleNames: $roleNames) {
+ userId
+ roleNames
+ }
+ }
+`;
+
+// ── 用户单查(@contract-pending)──
+// schema 无 user(id) 根字段,MSW 兜底
+// 契约工单:iam_contract.md#user-detail
+export const GET_USER_DOC = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ role
+ roles
+ phone
+ status
+ userType
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+// ── 用户编辑(@contract-pending)──
+// schema 无 updateUser 根字段,MSW 兜底
+// 契约工单:iam_contract.md#user-update
+export const UPDATE_USER_DOC = gql`
+ mutation UpdateUser($id: ID!, $input: UserUpdateInput!) {
+ updateUser(id: $id, input: $input) {
+ id
+ name
+ email
+ role
+ roles
+ phone
+ status
+ userType
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
// From widgets/admin/rbac-manager
export const GET_ROLES_DOC = gql`
query GetRoles {
@@ -105,9 +170,17 @@ export const GET_INVITATION_CODES_DOC = gql`
id
code
role
+ roleName
+ classId
+ className
+ email
+ batchId
status
usedCount
maxUses
+ usedBy
+ usedByName
+ usedAt
expiresAt
createdAt
createdBy
@@ -138,6 +211,30 @@ export const REVOKE_INVITATION_CODE_DOC = gql`
}
`;
+// From widgets/admin/invitation-codes(批量生成邀请码,@contract-pending MSW 兜底)
+// input 支持 roleId / classId / email / expiresAt(ISO)/ note 字段,返回生成的码列表与批次 ID
+export const GENERATE_INVITATION_CODES_DOC = gql`
+ mutation GenerateInvitationCodes($input: GenerateInvitationCodesInput!) {
+ generateInvitationCodes(input: $input) {
+ success
+ batchId
+ generated {
+ code
+ role
+ expiresAt
+ }
+ }
+ }
+`;
+
+// DeleteInvitationCodes:批量删除邀请码(@contract-pending MSW 兜底)
+// 接受 ID 列表,返回删除的数量
+export const DELETE_INVITATION_CODES_DOC = gql`
+ mutation DeleteInvitationCodes($ids: [ID!]!) {
+ deleteInvitationCodes(ids: $ids)
+ }
+`;
+
// From widgets/admin/school-settings
export const GET_SCHOOL_DOC = gql`
query GetSchool {
@@ -337,6 +434,18 @@ export const DELETE_ROLE_DOC = gql`
}
`;
+// ── RBAC: 角色启停切换(@contract-pending)──
+// schema 无 toggleRoleEnabled 根字段,MSW 兜底
+// 契约工单:iam_contract.md#toggle-role-enabled
+export const TOGGLE_ROLE_ENABLED_DOC = gql`
+ mutation ToggleRoleEnabled($id: ID!, $isEnabled: Boolean!) {
+ toggleRoleEnabled(id: $id, isEnabled: $isEnabled) {
+ id
+ isEnabled
+ }
+ }
+`;
+
// ── RBAC: Permission catalog role counts ──
export const GET_PERMISSION_ROLE_COUNTS_DOC = gql`
query GetPermissionRoleCounts {
@@ -350,6 +459,40 @@ export const GET_PERMISSION_ROLE_COUNTS_DOC = gql`
}
`;
+// RBAC: Role permission matrix (CRUD actions per permission point)
+// contract-pending: MSW fallback. Returns permissions grouped by module with read/create/update/delete actions.
+export const GET_ROLE_PERMISSIONS_DOC = gql`
+ query GetRolePermissions($roleId: ID!) {
+ rolePermissions(roleId: $roleId) {
+ permissionId
+ label
+ module
+ actions {
+ read
+ create
+ update
+ delete
+ }
+ }
+ }
+`;
+
+// RBAC: Update role permission actions (matrix save)
+// contract-pending: MSW fallback. Accepts roleId + permission action updates, returns affected count.
+export const UPDATE_ROLE_PERMISSION_ACTIONS_DOC = gql`
+ mutation UpdateRolePermissionActions(
+ $roleId: ID!
+ $permissionUpdates: [PermissionActionInput!]!
+ ) {
+ updateRolePermissionActions(
+ roleId: $roleId
+ permissionUpdates: $permissionUpdates
+ ) {
+ id
+ updatedCount
+ }
+ }
+`;
// ── Audit logs: overview ──
export const GET_AUDIT_OVERVIEW_STATS_DOC = gql`
query GetAuditOverviewStats {
@@ -358,6 +501,9 @@ export const GET_AUDIT_OVERVIEW_STATS_DOC = gql`
totalToday
totalErrors
totalUsers
+ auditEventsToday
+ failedLoginsToday
+ dataChangesToday
}
}
`;
@@ -393,6 +539,7 @@ export const GET_LOGIN_LOGS_DOC = gql`
ip
userAgent
timestamp
+ errorMessage
}
total
}
@@ -416,6 +563,8 @@ export const GET_DATA_CHANGE_LOGS_DOC = gql`
userName
changes
timestamp
+ oldValue
+ newValue
}
total
}
@@ -444,6 +593,42 @@ export const GET_AUDIT_MODULE_OPTIONS_DOC = gql`
}
`;
+// ── Audit logs: export (CSV 在 MSW 层用 transformations 纯函数构造) ──
+// 契约状态:schema 未就绪 → MSW 兜底(@contract-pending)
+// 返回 { success, count, filename, csv }:csv 为字符串,前端直接触发下载
+export const EXPORT_AUDIT_LOGS_DOC = gql`
+ mutation ExportAuditLogs($filter: AuditLogFilter) {
+ exportAuditLogs(filter: $filter) {
+ success
+ count
+ filename
+ csv
+ }
+ }
+`;
+
+export const EXPORT_LOGIN_LOGS_DOC = gql`
+ mutation ExportLoginLogs($filter: LoginLogFilter) {
+ exportLoginLogs(filter: $filter) {
+ success
+ count
+ filename
+ csv
+ }
+ }
+`;
+
+export const EXPORT_DATA_CHANGES_DOC = gql`
+ mutation ExportDataChanges($filter: DataChangeLogFilter) {
+ exportDataChanges(filter: $filter) {
+ success
+ count
+ filename
+ csv
+ }
+ }
+`;
+
// ── School: schools CRUD ──
export const GET_SCHOOLS_DOC = gql`
query GetSchools {
@@ -455,6 +640,8 @@ export const GET_SCHOOLS_DOC = gql`
email
currentAcademicYear
currentTerm
+ code
+ updatedAt
}
}
`;
@@ -497,6 +684,8 @@ export const GET_DEPARTMENTS_DOC = gql`
headId
headName
memberCount
+ description
+ updatedAt
}
}
`;
@@ -579,6 +768,10 @@ export const GET_GRADES_DOC = gql`
headStaffName
classCount
studentCount
+ order
+ teachingHead
+ teachingHeadName
+ updatedAt
}
}
`;
@@ -635,6 +828,38 @@ export const GET_ADMIN_CLASSES_DOC = gql`
headTeacherName
studentCount
subjectCount
+ homeroomLabel
+ room
+ homeroom
+ homeroomName
+ subjectTeachers
+ updatedAt
+ }
+ }
+`;
+
+export const CREATE_ADMIN_CLASS_DOC = gql`
+ mutation CreateAdminClass($input: AdminClassInput!) {
+ createAdminClass(input: $input) {
+ id
+ name
+ }
+ }
+`;
+
+export const UPDATE_ADMIN_CLASS_DOC = gql`
+ mutation UpdateAdminClass($id: ID!, $input: AdminClassInput!) {
+ updateAdminClass(id: $id, input: $input) {
+ id
+ name
+ }
+ }
+`;
+
+export const DELETE_ADMIN_CLASS_DOC = gql`
+ mutation DeleteAdminClass($id: ID!) {
+ deleteAdminClass(id: $id) {
+ id
}
}
`;
@@ -659,8 +884,8 @@ export const GET_STAFF_OPTIONS_DOC = gql`
// ── Announcements: list + detail + CRUD ──
export const GET_ADMIN_ANNOUNCEMENTS_DOC = gql`
- query GetAdminAnnouncements($status: String) {
- adminAnnouncements(status: $status) {
+ query GetAdminAnnouncements($status: String, $limit: Int, $offset: Int) {
+ adminAnnouncements(status: $status, limit: $limit, offset: $offset) {
items {
id
title
@@ -669,11 +894,14 @@ export const GET_ADMIN_ANNOUNCEMENTS_DOC = gql`
audience
pinnedAt
publishedAt
+ readCount
+ totalCount
createdAt
updatedAt
authorName
}
total
+ hasNext
}
}
`;
@@ -688,6 +916,8 @@ export const GET_ADMIN_ANNOUNCEMENT_DETAIL_DOC = gql`
audience
pinnedAt
publishedAt
+ readCount
+ totalCount
createdAt
updatedAt
authorName
@@ -741,6 +971,18 @@ export const PIN_ANNOUNCEMENT_DOC = gql`
}
`;
+// PublishAnnouncement:发布公告(@contract-pending,MSW 兜底)
+// 草稿态公告通过此 mutation 发布,返回最新状态与发布时间
+export const PUBLISH_ANNOUNCEMENT_DOC = gql`
+ mutation PublishAnnouncement($id: ID!) {
+ publishAnnouncement(id: $id) {
+ id
+ status
+ publishedAt
+ }
+ }
+`;
+
// ── Files ──
export const GET_FILE_ATTACHMENTS_DOC = gql`
query GetFileAttachments($filter: FileFilter) {
@@ -769,6 +1011,29 @@ export const GET_FILE_STATS_DOC = gql`
}
`;
+// UploadFile:单文件上传(@contract-pending,MSW 兜底)
+// 接受文件元信息(filename/mimeType/size),返回上传结果
+export const UPLOAD_FILE_DOC = gql`
+ mutation UploadFile($input: UploadFileInput!) {
+ uploadFile(input: $input) {
+ id
+ url
+ filename
+ }
+ }
+`;
+
+// BatchDeleteFiles:批量删除文件(@contract-pending,MSW 兜底)
+// 接受文件 ID 列表,返回删除结果
+export const BATCH_DELETE_FILES_DOC = gql`
+ mutation BatchDeleteFiles($fileIds: [ID!]!) {
+ batchDeleteFiles(fileIds: $fileIds) {
+ success
+ deletedCount
+ }
+ }
+`;
+
// ── AI settings: providers + usage ──
export const GET_AI_PROVIDERS_DOC = gql`
query GetAiProviders($scope: String) {
@@ -779,6 +1044,8 @@ export const GET_AI_PROVIDERS_DOC = gql`
scope
ownerId
isActive
+ isDefault
+ visibility
model
apiBase
config
@@ -813,6 +1080,18 @@ export const DELETE_AI_PROVIDER_DOC = gql`
}
`;
+// TestAiProvider:测试 AI Provider 连通性(@contract-pending,MSW 兜底)
+// 接受 Provider ID,返回 ok 状态与延迟、错误消息
+export const TEST_AI_PROVIDER_DOC = gql`
+ mutation TestAiProvider($id: ID!) {
+ testAiProvider(id: $id) {
+ ok
+ latencyMs
+ message
+ }
+ }
+`;
+
export const GET_AI_USAGE_DASHBOARD_DOC = gql`
query GetAiUsageDashboard($range: String) {
aiUsageDashboard(range: $range) {
@@ -999,6 +1278,8 @@ export const GET_STANDARDS_COVERAGE_HEATMAP_DOC = gql`
gradeName
coverageRate
lessonPlanCount
+ total
+ linked
}
}
`;
@@ -1082,6 +1363,34 @@ export const GET_ADMIN_QUESTIONS_DOC = gql`
}
`;
+// ── 题库单条详情(@contract-pending)──
+// schema 无 adminQuestion(id) 根字段,MSW 兜底
+// 返回完整字段:含 answer/explanation/knowledgePointTitle/textbookTitle 等详情字段
+// 契约工单:core-edu_contract.md#question-detail
+export const GET_ADMIN_QUESTION_DOC = gql`
+ query GetAdminQuestion($id: ID!) {
+ adminQuestion(id: $id) {
+ id
+ type
+ content
+ difficulty
+ subjectId
+ subjectName
+ textbookId
+ textbookTitle
+ knowledgePointId
+ knowledgePointTitle
+ status
+ answer
+ explanation
+ source
+ createdAt
+ updatedAt
+ createdBy
+ }
+ }
+`;
+
// 教案(admin scope)
export const GET_ADMIN_LESSON_PLANS_DOC = gql`
query GetAdminLessonPlans {
@@ -1142,6 +1451,13 @@ export const GET_ADMIN_ERROR_BOOK_STATS_DOC = gql`
questionCount
errorRate
}
+ byClass {
+ classId
+ className
+ errorCount
+ questionCount
+ errorRate
+ }
topStudents {
studentId
studentName
@@ -1159,6 +1475,21 @@ export const GET_ADMIN_ERROR_BOOK_STATS_DOC = gql`
}
`;
+// ── 错题本 CSV 导出 mutation(@contract-pending)──
+// schema 无 exportErrorBookCsv 根字段,MSW 兜底
+// 返回 csv 字符串,前端直接触发下载(与 ExportAuditLogs 模式一致)
+// 契约工单:core-edu_contract.md#error-book-export
+export const EXPORT_ERROR_BOOK_CSV_DOC = gql`
+ mutation ExportErrorBookCsv($filter: ErrorBookExportFilter) {
+ exportErrorBookCsv(filter: $filter) {
+ success
+ count
+ filename
+ csv
+ }
+ }
+`;
+
// 排课(admin scope)
export const GET_ADMIN_SCHEDULE_CHANGES_DOC = gql`
query GetAdminScheduleChanges {
@@ -1278,6 +1609,7 @@ export const GET_ADMIN_ATTENDANCE_RECORDS_DOC = gql`
studentName
classId
className
+ gradeId
date
status
recordedBy
@@ -1299,3 +1631,218 @@ export const GET_ATTENDANCE_GRADE_CORRELATION_DOC = gql`
}
}
`;
+
+// ── Audit logs: retention config + purge(@contract-pending,schema 未就绪,MSW 兜底)──
+// GetAuditRetentionConfig:查询审计日志保留期配置
+export const GET_AUDIT_RETENTION_CONFIG_DOC = gql`
+ query GetAuditRetentionConfig {
+ auditRetentionConfig {
+ retentionDays
+ loginLogRetentionDays
+ autoCleanupEnabled
+ }
+ }
+`;
+
+// SaveAuditRetentionConfig:保存审计日志保留期配置
+export const SAVE_AUDIT_RETENTION_CONFIG_DOC = gql`
+ mutation SaveAuditRetentionConfig($input: AuditRetentionConfigInput!) {
+ saveAuditRetentionConfig(input: $input) {
+ retentionDays
+ loginLogRetentionDays
+ autoCleanupEnabled
+ }
+ }
+`;
+
+// PurgeExpiredAuditLogs:手动清理过期审计日志
+export const PURGE_EXPIRED_AUDIT_LOGS_DOC = gql`
+ mutation PurgeExpiredAuditLogs(
+ $retentionDays: Int!
+ $loginLogRetentionDays: Int
+ ) {
+ purgeExpiredAuditLogs(
+ retentionDays: $retentionDays
+ loginLogRetentionDays: $loginLogRetentionDays
+ ) {
+ auditLogsDeleted
+ loginLogsDeleted
+ dataChangeLogsDeleted
+ }
+ }
+`;
+
+// ── §四补充批次:管理员课程计划周计划管理(@contract-pending,MSW 兜底)──
+// schema 无 CoursePlanItem 类型与对应 Mutation,MSW 兜底返回 mock 数据
+// 契约工单:core-edu_contract.md#course-plan-items
+
+// 创建周计划项
+export const CREATE_COURSE_PLAN_ITEM_DOC = gql`
+ mutation CreateCoursePlanItem($input: CreateCoursePlanItemInput!) {
+ createCoursePlanItem(input: $input) {
+ id
+ planId
+ week
+ topic
+ content
+ hours
+ textbookChapter
+ notes
+ isCompleted
+ completedAt
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+// 更新周计划项
+export const UPDATE_COURSE_PLAN_ITEM_DOC = gql`
+ mutation UpdateCoursePlanItem($id: ID!, $input: UpdateCoursePlanItemInput!) {
+ updateCoursePlanItem(id: $id, input: $input) {
+ id
+ planId
+ week
+ topic
+ content
+ hours
+ textbookChapter
+ notes
+ isCompleted
+ completedAt
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+// 删除周计划项
+export const DELETE_COURSE_PLAN_ITEM_DOC = gql`
+ mutation DeleteCoursePlanItem($id: ID!) {
+ deleteCoursePlanItem(id: $id) {
+ id
+ success
+ }
+ }
+`;
+
+// 切换周计划项完成状态
+export const TOGGLE_COURSE_PLAN_ITEM_COMPLETED_DOC = gql`
+ mutation ToggleCoursePlanItemCompleted($id: ID!, $isCompleted: Boolean!) {
+ toggleCoursePlanItemCompleted(id: $id, isCompleted: $isCompleted) {
+ id
+ isCompleted
+ completedAt
+ updatedAt
+ }
+ }
+`;
+
+// 批量重排序周计划项
+export const REORDER_COURSE_PLAN_ITEMS_DOC = gql`
+ mutation ReorderCoursePlanItems(
+ $planId: ID!
+ $items: [ReorderCoursePlanItemInput!]!
+ ) {
+ reorderCoursePlanItems(planId: $planId, items: $items) {
+ id
+ week
+ updatedAt
+ }
+ }
+`;
+
+// ── §四补充批次:课程计划 / 选修课 CRUD + 业务动作扩展(@contract-pending,MSW 兜底)──
+// schema 无对应 Mutation 根字段,全部由 MSW handlers 返回 mock 数据
+// 契约工单:core-edu_contract.md#admin-course-plans-crud / admin-electives-crud
+
+// 删除课程计划
+export const DELETE_COURSE_PLAN_DOC = gql`
+ mutation DeleteCoursePlan($id: ID!) {
+ deleteCoursePlan(id: $id) {
+ id
+ success
+ }
+ }
+`;
+
+// 批量切换周计划项完成状态
+export const BULK_TOGGLE_COURSE_PLAN_ITEMS_DOC = gql`
+ mutation BulkToggleCoursePlanItems(
+ $planId: ID!
+ $itemIds: [ID!]!
+ $isCompleted: Boolean!
+ ) {
+ bulkToggleCoursePlanItems(
+ planId: $planId
+ itemIds: $itemIds
+ isCompleted: $isCompleted
+ ) {
+ id
+ isCompleted
+ completedAt
+ updatedAt
+ }
+ }
+`;
+
+// 创建选修课 / 更新选修课:已在 elective.graphql.ts 中定义
+// (CREATE_ELECTIVE_DOC / UPDATE_ELECTIVE_DOC),由 operations/index.ts 统一导出。
+// admin 域与教师域共用同一 mutation 契约,仅 hook scope 不同(见 admin-p5.ts)。
+
+// 删除选修课
+export const DELETE_ELECTIVE_DOC = gql`
+ mutation DeleteElective($id: ID!) {
+ deleteElective(id: $id) {
+ id
+ success
+ }
+ }
+`;
+
+// 开放选课
+export const OPEN_ELECTIVE_SELECTION_DOC = gql`
+ mutation OpenElectiveSelection($id: ID!) {
+ openElectiveSelection(id: $id) {
+ id
+ status
+ updatedAt
+ }
+ }
+`;
+
+// 关闭选课
+export const CLOSE_ELECTIVE_SELECTION_DOC = gql`
+ mutation CloseElectiveSelection($id: ID!) {
+ closeElectiveSelection(id: $id) {
+ id
+ status
+ updatedAt
+ }
+ }
+`;
+
+// 运行选课抽签
+export const RUN_ELECTIVE_LOTTERY_DOC = gql`
+ mutation RunElectiveLottery($id: ID!) {
+ runElectiveLottery(id: $id) {
+ id
+ status
+ selectedCount
+ updatedAt
+ }
+ }
+`;
+
+// 选修课总览统计
+export const GET_ELECTIVE_OVERVIEW_STATS_DOC = gql`
+ query GetElectiveOverviewStats {
+ electiveOverviewStats {
+ totalCourses
+ totalCapacity
+ totalEnrolled
+ totalDraft
+ totalOpen
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/attendance.graphql.ts b/apps/portal-shell/src/lib/api/operations/attendance.graphql.ts
index b11a9d9..cfb88e3 100644
--- a/apps/portal-shell/src/lib/api/operations/attendance.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/attendance.graphql.ts
@@ -140,6 +140,21 @@ export const GET_ATTENDANCE_STATS_DOC = gql`
}
`;
+// ── 班级对比查询(@contract-pending)─────────────────────────────
+// schema 无 classComparison 根字段
+// 管理端考勤页通过 MSW 兜底获取班级出勤率对比,后端补齐后切换 fetcher
+// 契约工单:classes_contract.md#attendance-class-comparison
+export const GET_CLASS_COMPARISON_DOC = gql`
+ query GetClassComparison {
+ classComparison {
+ className
+ attendanceRate
+ totalStudents
+ presentStudents
+ }
+ }
+`;
+
// ── 假契约变更(@contract-pending)─────────────────────────────
// 保存点名表:schema 无 Mutation 类型
// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
diff --git a/apps/portal-shell/src/lib/api/operations/classes.graphql.ts b/apps/portal-shell/src/lib/api/operations/classes.graphql.ts
index f0afa40..06cb54f 100644
--- a/apps/portal-shell/src/lib/api/operations/classes.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/classes.graphql.ts
@@ -132,3 +132,132 @@ export const GET_CLASS_TEACHERS_DOC = gql`
}
}
`;
+
+// ── 班级课表 CRUD mutation(@contract-pending)────────────────────
+// schema 无 createClassSchedule/updateClassSchedule/deleteClassSchedule 根字段
+// 管理端课表编辑通过 MSW 兜底,后端补齐 mutation 后切换 fetcher
+// 契约工单:classes_contract.md#class-schedule-mutation
+export const CREATE_CLASS_SCHEDULE_DOC = gql`
+ mutation CreateClassSchedule($input: CreateClassScheduleInput!) {
+ createClassSchedule(input: $input) {
+ id
+ weekday
+ period
+ subjectId
+ subjectName
+ teacherId
+ teacherName
+ classroom
+ startTime
+ endTime
+ }
+ }
+`;
+
+export const UPDATE_CLASS_SCHEDULE_DOC = gql`
+ mutation UpdateClassSchedule($id: ID!, $input: UpdateClassScheduleInput!) {
+ updateClassSchedule(id: $id, input: $input) {
+ id
+ weekday
+ period
+ subjectId
+ subjectName
+ teacherId
+ teacherName
+ classroom
+ startTime
+ endTime
+ }
+ }
+`;
+
+export const DELETE_CLASS_SCHEDULE_DOC = gql`
+ mutation DeleteClassSchedule($id: ID!) {
+ deleteClassSchedule(id: $id)
+ }
+`;
+
+// ── 班级邀请码查询与 mutation(@contract-pending)────────────────
+// schema 无 classInvitationCodes/generateClassInvitationCode/revokeClassInvitationCode 根字段
+// 管理端班级邀请码管理通过 MSW 兜底,后端补齐后切换 fetcher
+// 契约工单:classes_contract.md#class-invitation-codes
+export const GET_CLASS_INVITATION_CODES_DOC = gql`
+ query GetClassInvitationCodes($classId: ID!) {
+ classInvitationCodes(classId: $classId) {
+ id
+ code
+ status
+ usedCount
+ maxUses
+ expiresAt
+ createdAt
+ revokedAt
+ note
+ }
+ }
+`;
+
+export const GENERATE_CLASS_INVITATION_CODE_DOC = gql`
+ mutation GenerateClassInvitationCode(
+ $input: GenerateClassInvitationCodeInput!
+ ) {
+ generateClassInvitationCode(input: $input) {
+ id
+ code
+ status
+ usedCount
+ maxUses
+ expiresAt
+ createdAt
+ revokedAt
+ note
+ }
+ }
+`;
+
+export const REVOKE_CLASS_INVITATION_CODE_DOC = gql`
+ mutation RevokeClassInvitationCode($codeId: ID!) {
+ revokeClassInvitationCode(codeId: $codeId) {
+ id
+ status
+ }
+ }
+`;
+
+// ── 班级 CRUD mutation(@contract-pending)──────────────────────
+// schema 无 createClass/updateClass/deleteClass 根字段
+// 管理端班级 CRUD 通过 MSW 兜底,后端补齐 mutation 后切换 fetcher
+// 契约工单:classes_contract.md#class-crud
+export const CREATE_CLASS_DOC = gql`
+ mutation CreateClass($input: CreateClassInput!) {
+ createClass(input: $input) {
+ id
+ name
+ gradeId
+ headTeacherId
+ description
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+export const UPDATE_CLASS_DOC = gql`
+ mutation UpdateClass($id: ID!, $input: UpdateClassInput!) {
+ updateClass(id: $id, input: $input) {
+ id
+ name
+ gradeId
+ headTeacherId
+ description
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+export const DELETE_CLASS_DOC = gql`
+ mutation DeleteClass($id: ID!) {
+ deleteClass(id: $id)
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/course-plans.graphql.ts b/apps/portal-shell/src/lib/api/operations/course-plans.graphql.ts
index 9cf28ea..a4ff7e0 100644
--- a/apps/portal-shell/src/lib/api/operations/course-plans.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/course-plans.graphql.ts
@@ -98,3 +98,19 @@ export const UPDATE_COURSE_PLAN_DOC = gql`
}
}
`;
+
+// ── 模板候选查询(@contract-pending)────────────────────────────
+// schema 无 coursePlanTemplates 根字段 → MSW 兜底
+// 用于 create 页"从模板创建"对话框,列出可复用的课程计划模板
+// 契约工单:core-edu_contract.md#course-plan-templates
+export const GET_COURSE_PLAN_TEMPLATES_DOC = gql`
+ query GetCoursePlanTemplates($subjectId: ID, $gradeLevel: String) {
+ coursePlanTemplates(subjectId: $subjectId, gradeLevel: $gradeLevel) {
+ id
+ name
+ description
+ subject
+ gradeLevel
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts b/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts
index f0eee9e..aa88c8b 100644
--- a/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts
@@ -48,10 +48,20 @@ export const GET_TEACHER_DASHBOARD_DOC = gql`
`;
// ── Student Dashboard ──────────────────────────────────────────
+// P3 扩展:新增 enrolled_classes_count / graded_count / due_soon_count /
+// overdue_count / grades / upcoming_assignments / today_schedule 字段,
+// 对齐 CICD student-dashboard-view 的 6 StatCard + 3 section 能力。
+// 这些字段尚未在 data-ana subgraph SDL 中定义(@contract-pending),
+// 由 MSW mock 兜底;codegen 已将 dashboard-types.ts 的
+// skipDocumentsValidation 改为 true 以容许前向字段。
+//
+// P3+ 扩展:新增 student_name(时段问候)与 grades.trend.class_avg
+// (成绩趋势图班级均分虚线),仍由 MSW 兜底(@contract-pending)。
export const GET_STUDENT_DASHBOARD_DOC = gql`
query GetStudentDashboard {
studentDashboard {
user_id
+ student_name
avg_score
class_rank
total_students
@@ -66,6 +76,51 @@ export const GET_STUDENT_DASHBOARD_DOC = gql`
score
}
pending_homework
+ enrolled_classes_count
+ graded_count
+ due_soon_count
+ overdue_count
+ grades {
+ ranking {
+ rank
+ class_size
+ percentage
+ }
+ trend {
+ assignment_id
+ assignment_title
+ score
+ max_score
+ percentage
+ class_avg
+ submitted_at
+ }
+ recent {
+ assignment_id
+ assignment_title
+ score
+ max_score
+ percentage
+ submitted_at
+ }
+ }
+ upcoming_assignments {
+ id
+ title
+ subject_name
+ due_at
+ progress_status
+ latest_score
+ }
+ today_schedule {
+ id
+ class_id
+ class_name
+ course
+ start_time
+ end_time
+ location
+ }
}
}
`;
diff --git a/apps/portal-shell/src/lib/api/operations/error-book.graphql.ts b/apps/portal-shell/src/lib/api/operations/error-book.graphql.ts
index 213be71..16743d4 100644
--- a/apps/portal-shell/src/lib/api/operations/error-book.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/error-book.graphql.ts
@@ -1,9 +1,10 @@
-// Error Book domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
+// Error Book domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
//
// 拆分原则:
// - errorBookItems:✅ combined-schema 中真实存在(errorBookItems: [ErrorBookList!]!)
// - errorBookStats:✅ 真实存在,但已由 dashboard.graphql.ts 定义(GET_ERROR_BOOK_STATS_DOC)
// → 此处不重复定义,error-book.ts 直接从 dashboard.graphql 导入复用
+// - errorBookDetail:❌ schema 未就绪 → MSW 兜底(@contract-pending),用于详情对话框
//
// Schema 缺陷说明(data-ana 子图):
// - ErrorBookList.items 在 schema 中是单数 ErrorBookItem 类型(应为列表)
@@ -13,7 +14,7 @@
// 关联:ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
import { gql } from "@apollo/client";
-// ── 真实查询:errorBookItems(无参数)─────────────────────────
+// ── 真实查询:errorBookItems(无参数)──
// schema: errorBookItems: [ErrorBookList!]!
// 每个 ErrorBookList { student_id, items: ErrorBookItem, total }
// 字段全部 snake_case 对齐 data-ana 子图
@@ -34,5 +35,29 @@ export const GET_ERROR_BOOK_ITEMS_DOC = gql`
}
`;
+// ── MSW 兜底查询:errorBookDetail(按 itemId 查询错题完整详情)──
+// @contract-pending:schema 未就绪 → MSW 兜底
+// 字段 camelCase 对齐 admin 子图风格(与 AdminErrorBookStats 一致)
+// 用于错题本列表页查看详情对话框
+export const GET_ERROR_BOOK_DETAIL_DOC = gql`
+ query GetErrorBookDetail($itemId: ID!) {
+ errorBookDetail(itemId: $itemId) {
+ itemId
+ content
+ subjectId
+ subjectName
+ studentId
+ studentName
+ className
+ errorCount
+ lastErrorTime
+ knowledgePointId
+ knowledgePointTitle
+ analysis
+ correctAnswer
+ }
+ }
+`;
+
// 注:GET_ERROR_BOOK_STATS_DOC 已在 dashboard.graphql.ts 定义并 export,
// 通过 operations/index.ts 的 `export *` 暴露,error-book.ts 直接 import 复用。
diff --git a/apps/portal-shell/src/lib/api/operations/exams.graphql.ts b/apps/portal-shell/src/lib/api/operations/exams.graphql.ts
index 78dbe06..e0695c9 100644
--- a/apps/portal-shell/src/lib/api/operations/exams.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/exams.graphql.ts
@@ -191,3 +191,37 @@ export const SAVE_EXAM_RICH_CONTENT_DOC = gql`
}
}
`;
+
+// ── 删除考试(@contract-pending mutation)──────────────────────
+// schema 无 Mutation 类型 → MSW 兜底
+// 契约工单:core-edu_contract.md#delete-exam
+export const DELETE_EXAM_DOC = gql`
+ mutation DeleteExam($id: ID!) {
+ deleteExam(id: $id) {
+ id
+ }
+ }
+`;
+
+// ── 更新考试状态(@contract-pending mutation)─────────────────
+// schema 无 Mutation 类型 → MSW 兜底
+// 契约工单:core-edu_contract.md#update-exam-status
+export const UPDATE_EXAM_STATUS_DOC = gql`
+ mutation UpdateExamStatus($id: ID!, $status: String!) {
+ updateExamStatus(id: $id, status: $status) {
+ id
+ status
+ }
+ }
+`;
+
+// ── 复制考试(@contract-pending mutation)─────────────────────
+// schema 无 Mutation 类型 → MSW 兜底
+// 契约工单:core-edu_contract.md#duplicate-exam
+export const DUPLICATE_EXAM_DOC = gql`
+ mutation DuplicateExam($id: ID!) {
+ duplicateExam(id: $id) {
+ id
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/grades.graphql.ts b/apps/portal-shell/src/lib/api/operations/grades.graphql.ts
index 89d274c..e3adeca 100644
--- a/apps/portal-shell/src/lib/api/operations/grades.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/grades.graphql.ts
@@ -177,3 +177,235 @@ export const GET_REPORT_CARD_DOC = gql`
}
}
`;
+
+// ── 成绩更新/删除/批量删除(@contract-pending 全 MSW)────────
+// schema 无 Mutation 类型 → MSW 兜底
+// 契约工单:core-edu_contract.md#grade-mutations
+// 注:命名为 GRADE_RECORD 以区分 admin-p5.ts 中的年级(Grade)管理 mutation
+export const UPDATE_GRADE_RECORD_DOC = gql`
+ mutation UpdateGradeRecord($id: ID!, $input: UpdateGradeInput!) {
+ updateGrade(id: $id, input: $input) {
+ gradeId
+ }
+ }
+`;
+
+export const DELETE_GRADE_RECORD_DOC = gql`
+ mutation DeleteGradeRecord($id: ID!) {
+ deleteGrade(id: $id) {
+ gradeId
+ }
+ }
+`;
+
+export const BULK_DELETE_GRADES_DOC = gql`
+ mutation BulkDeleteGrades($ids: [ID!]!) {
+ bulkDeleteGrades(ids: $ids) {
+ count
+ }
+ }
+`;
+
+// ── 按试卷批量录入(@contract-pending 全 MSW)────────────────
+// 契约工单:core-edu_contract.md#batch-grade-entry
+export const BATCH_CREATE_GRADE_RECORDS_BY_EXAM_DOC = gql`
+ mutation BatchCreateGradeRecordsByExam(
+ $examId: ID!
+ $classId: ID!
+ $records: [BatchGradeRecordInput!]!
+ ) {
+ batchCreateGradeRecordsByExam(
+ examId: $examId
+ classId: $classId
+ records: $records
+ ) {
+ gradeIds
+ }
+ }
+`;
+
+export const UNDO_GRADE_ENTRY_DOC = gql`
+ mutation UndoGradeEntry($gradeIds: [ID!]!) {
+ undoGradeEntry(gradeIds: $gradeIds) {
+ count
+ }
+ }
+`;
+
+// ── Excel 导入/模板下载(@contract-pending 全 MSW)──────────
+// 契约工单:core-edu_contract.md#excel-import
+export const DOWNLOAD_GRADE_IMPORT_TEMPLATE_DOC = gql`
+ query DownloadGradeImportTemplate($classId: ID!) {
+ downloadGradeImportTemplate(classId: $classId) {
+ buffer
+ filename
+ }
+ }
+`;
+
+export const IMPORT_GRADES_FROM_EXCEL_DOC = gql`
+ mutation ImportGradesFromExcel(
+ $file: String!
+ $classId: ID!
+ $subjectId: ID!
+ $title: String!
+ $examId: ID
+ $fullScore: Float
+ $type: String
+ $semester: String
+ ) {
+ importGradesFromExcel(
+ file: $file
+ classId: $classId
+ subjectId: $subjectId
+ title: $title
+ examId: $examId
+ fullScore: $fullScore
+ type: $type
+ semester: $semester
+ ) {
+ success
+ totalRows
+ successCount
+ failedCount
+ unmatchedStudents
+ invalidRows {
+ row
+ studentName
+ score
+ errors
+ }
+ }
+ }
+`;
+
+// ── 成绩录入用考试/学生查询(@contract-pending 全 MSW)──────
+// 契约工单:core-edu_contract.md#exam-options-for-entry
+export const GET_EXAM_OPTIONS_FOR_ENTRY_DOC = gql`
+ query GetExamOptionsForEntry {
+ examOptionsForEntry {
+ id
+ title
+ subjectName
+ questionCount
+ totalScore
+ classId
+ gradeId
+ }
+ }
+`;
+
+export const GET_EXAM_FOR_GRADE_ENTRY_DOC = gql`
+ query GetExamForGradeEntry($examId: ID!) {
+ examForGradeEntry(examId: $examId) {
+ id
+ title
+ fullScore
+ classId
+ gradeId
+ questions {
+ id
+ title
+ score
+ }
+ students {
+ id
+ name
+ studentNo
+ }
+ }
+ }
+`;
+
+// ── 草稿锁(@contract-pending 全 MSW)──────────────────────
+// 用于批量录入协同保护
+// 契约工单:core-edu_contract.md#draft-lock
+export const GET_DRAFT_LOCK_DOC = gql`
+ query GetDraftLock($scope: String!) {
+ draftLock(scope: $scope) {
+ isLocked
+ lockedBy
+ lockedByName
+ lockedAt
+ isMine
+ }
+ }
+`;
+
+export const ACQUIRE_DRAFT_LOCK_DOC = gql`
+ mutation AcquireDraftLock($scope: String!) {
+ acquireDraftLock(scope: $scope) {
+ isLocked
+ lockedBy
+ lockedByName
+ lockedAt
+ isMine
+ }
+ }
+`;
+
+export const RELEASE_DRAFT_LOCK_DOC = gql`
+ mutation ReleaseDraftLock($scope: String!) {
+ releaseDraftLock(scope: $scope) {
+ isLocked
+ lockedBy
+ lockedByName
+ lockedAt
+ isMine
+ }
+ }
+`;
+
+// ── 全校年级成绩洞察(@contract-pending 全 MSW)──────────────
+// 用于 /shell/admin/school/grades/insights 年级洞察页
+// 返回全校汇总 + 各年级统计(含班级排名)+ 最近作业列表
+// 契约工单:core-edu_contract.md#school-wide-grade-insights
+export const GET_SCHOOL_WIDE_GRADE_SUMMARY_DOC = gql`
+ query GetSchoolWideGradeSummary($gradeId: ID) {
+ schoolWideGradeSummary(gradeId: $gradeId) {
+ overallStats {
+ averageScore
+ passRate
+ excellenceRate
+ totalParticipants
+ classes
+ students
+ overallAvg
+ latestAvg
+ }
+ grades {
+ gradeId
+ gradeName
+ averageScore
+ passRate
+ excellenceRate
+ participantCount
+ classRankings {
+ classId
+ className
+ averageScore
+ passRate
+ studentCount
+ rank
+ delta
+ prevAvg
+ overallAvg
+ }
+ }
+ recentAssignments {
+ assignmentId
+ title
+ subjectName
+ gradeName
+ averageScore
+ submitCount
+ totalStudents
+ status
+ createdAt
+ targeted
+ graded
+ median
+ }
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/homework.graphql.ts b/apps/portal-shell/src/lib/api/operations/homework.graphql.ts
index cfe3fcc..d9ce7ed 100644
--- a/apps/portal-shell/src/lib/api/operations/homework.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/homework.graphql.ts
@@ -259,3 +259,70 @@ export const RECORD_GRADE_DOC = gql`
}
}
`;
+
+// ── 学生作答相关 operations(@contract-pending 全 MSW)─────────
+// 学生作答流程涉及的 5 个 operation(start/save/submit/scans/deleteScan)
+// schema 无对应字段/Mutation 类型 → MSW 兜底
+// 用于 /shell/teacher/homework/[id]/take 学生作答页
+// 契约工单:core-edu_contract.md#homework-take-flow
+
+export const START_HOMEWORK_SUBMISSION_DOC = gql`
+ mutation StartHomeworkSubmission($input: StartHomeworkSubmissionInput!) {
+ startHomeworkSubmission(input: $input) {
+ submissionId
+ }
+ }
+`;
+
+export const SAVE_HOMEWORK_ANSWER_DOC = gql`
+ mutation SaveHomeworkAnswer($input: SaveHomeworkAnswerInput!) {
+ saveHomeworkAnswer(input: $input) {
+ submissionId
+ questionId
+ }
+ }
+`;
+
+export const SUBMIT_HOMEWORK_DOC = gql`
+ mutation SubmitHomework($input: SubmitHomeworkInput!) {
+ submitHomework(input: $input) {
+ submissionId
+ totalScore
+ }
+ }
+`;
+
+export const GET_SCANS_DOC = gql`
+ query GetScans($submissionId: ID!) {
+ scans(submissionId: $submissionId) {
+ fileId
+ url
+ filename
+ originalName
+ page
+ }
+ }
+`;
+
+export const DELETE_SCAN_DOC = gql`
+ mutation DeleteScan($input: DeleteScanInput!) {
+ deleteScan(input: $input) {
+ fileId
+ success
+ }
+ }
+`;
+
+// ── 批量自动批改 mutation(@contract-pending)──────────────────
+// schema 无 Mutation 类型 → MSW 兜底
+// 用于按作业批量批改页一键自动批改
+// 契约工单:core-edu_contract.md#batch-auto-grade
+export const BATCH_AUTO_GRADE_DOC = gql`
+ mutation BatchAutoGrade($input: BatchAutoGradeInput!) {
+ batchAutoGrade(input: $input) {
+ processedCount
+ successCount
+ failedCount
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/index.ts b/apps/portal-shell/src/lib/api/operations/index.ts
index 5e4a30b..a8508b6 100644
--- a/apps/portal-shell/src/lib/api/operations/index.ts
+++ b/apps/portal-shell/src/lib/api/operations/index.ts
@@ -32,3 +32,4 @@ export * from "./leave.graphql";
// B1 共享页:通知中心 + 设置(@contract-pending 全 MSW)
export * from "./notifications.graphql";
export * from "./settings.graphql";
+export * from "./profile.graphql";
diff --git a/apps/portal-shell/src/lib/api/operations/lesson-plans.graphql.ts b/apps/portal-shell/src/lib/api/operations/lesson-plans.graphql.ts
index fe62102..bef5400 100644
--- a/apps/portal-shell/src/lib/api/operations/lesson-plans.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/lesson-plans.graphql.ts
@@ -129,6 +129,19 @@ export const DELETE_LESSON_PLAN_DOC = gql`
}
`;
+// 软删除教案 mutation(@contract-pending)→ MSW 兜底
+// 用于管理端 admin/lesson-plans 列表/详情页软删除(对齐 CICD softDeleteLessonPlan:
+// 将 status 置为 archived,返回 { success, deletedAt })
+// 契约工单:core-edu_contract.md#soft-delete-lesson-plan
+export const SOFT_DELETE_LESSON_PLAN_DOC = gql`
+ mutation SoftDeleteLessonPlan($planId: ID!) {
+ softDeleteLessonPlan(planId: $planId) {
+ success
+ deletedAt
+ }
+ }
+`;
+
// ── 教案库(@contract-pending 全 MSW)──────────────────────────
// schema 无 lessonPlanLibrary 根字段 → MSW 兜底
// 用于 /shell/teacher/lesson-plans/library 教案库列表页
diff --git a/apps/portal-shell/src/lib/api/operations/practice.graphql.ts b/apps/portal-shell/src/lib/api/operations/practice.graphql.ts
index 151724c..0d92717 100644
--- a/apps/portal-shell/src/lib/api/operations/practice.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/practice.graphql.ts
@@ -59,3 +59,145 @@ export const CREATE_PRACTICE_DOC = gql`
}
}
`;
+
+// ════════════════════════════════════════════════════════════════
+// Adaptive Practice(自适应练习)操作(@contract-pending,全部 MSW 兜底)
+//
+// 从 CICD src/modules/adaptive-practice 迁移。
+// schema 无 adaptivePracticeSessions / adaptivePracticeSession(id) 根字段,
+// 也无对应 Mutation 类型,全部通过 MSW handlers 返回 mock 数据。
+// 后端补齐契约后切换真实 fetcher,组件无需改动。
+// ════════════════════════════════════════════════════════════════
+
+// 查询:自适应练习会话列表
+// 契约工单:core-edu_contract.md#adaptive-practice-sessions
+export const GET_ADAPTIVE_PRACTICE_SESSIONS_DOC = gql`
+ query GetAdaptivePracticeSessions($studentId: ID, $limit: Int, $offset: Int) {
+ adaptivePracticeSessions(
+ studentId: $studentId
+ limit: $limit
+ offset: $offset
+ ) {
+ items {
+ id
+ studentId
+ subjectId
+ practiceType
+ status
+ totalQuestions
+ answeredQuestions
+ correctCount
+ accuracy
+ startedAt
+ completedAt
+ createdAt
+ }
+ total
+ }
+ }
+`;
+
+// 查询:自适应练习会话详情(含答题记录)
+// 契约工单:core-edu_contract.md#adaptive-practice-session-detail
+export const GET_ADAPTIVE_PRACTICE_SESSION_DETAIL_DOC = gql`
+ query GetAdaptivePracticeSessionDetail($sessionId: ID!, $studentId: ID) {
+ adaptivePracticeSession(id: $sessionId, studentId: $studentId) {
+ id
+ studentId
+ subjectId
+ practiceType
+ status
+ totalQuestions
+ answeredQuestions
+ correctCount
+ accuracy
+ startedAt
+ completedAt
+ createdAt
+ sourceMeta
+ answers {
+ id
+ sessionId
+ questionId
+ variantContent
+ isVariant
+ orderIndex
+ status
+ studentAnswer
+ isCorrect
+ score
+ maxScore
+ answeredAt
+ question {
+ id
+ content
+ type
+ difficulty
+ }
+ }
+ }
+ }
+`;
+
+// 查询:自适应练习统计
+// 契约工单:core-edu_contract.md#adaptive-practice-stats
+export const GET_ADAPTIVE_PRACTICE_STATS_DOC = gql`
+ query GetAdaptivePracticeStats($studentId: ID) {
+ adaptivePracticeStats(studentId: $studentId) {
+ totalSessions
+ completedSessions
+ totalQuestionsAnswered
+ totalCorrect
+ overallAccuracy
+ byType {
+ practiceType
+ sessionCount
+ totalQuestions
+ correctCount
+ accuracy
+ }
+ }
+ }
+`;
+
+// 变更:创建自适应练习会话
+// 契约工单:core-edu_contract.md#create-adaptive-practice-session
+export const CREATE_ADAPTIVE_PRACTICE_SESSION_DOC = gql`
+ mutation CreateAdaptivePracticeSession(
+ $input: CreateAdaptivePracticeSessionInput!
+ ) {
+ createAdaptivePracticeSession(input: $input) {
+ sessionId
+ selectedCount
+ }
+ }
+`;
+
+// 变更:提交单题答案
+// 契约工单:core-edu_contract.md#submit-adaptive-practice-answer
+export const SUBMIT_ADAPTIVE_PRACTICE_ANSWER_DOC = gql`
+ mutation SubmitAdaptivePracticeAnswer(
+ $input: SubmitAdaptivePracticeAnswerInput!
+ ) {
+ submitAdaptivePracticeAnswer(input: $input) {
+ isCorrect
+ score
+ }
+ }
+`;
+
+// 变更:完成练习会话
+// 契约工单:core-edu_contract.md#complete-adaptive-practice-session
+export const COMPLETE_ADAPTIVE_PRACTICE_SESSION_DOC = gql`
+ mutation CompleteAdaptivePracticeSession($sessionId: ID!) {
+ completeAdaptivePracticeSession(sessionId: $sessionId)
+ }
+`;
+
+// 变更:放弃练习会话
+// 契约工单:core-edu_contract.md#abandon-adaptive-practice-session
+export const ABANDON_ADAPTIVE_PRACTICE_SESSION_DOC = gql`
+ mutation AbandonAdaptivePracticeSession($sessionId: ID!) {
+ abandonAdaptivePracticeSession(sessionId: $sessionId)
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/questions.graphql.ts b/apps/portal-shell/src/lib/api/operations/questions.graphql.ts
index 7fe8272..ebf03f1 100644
--- a/apps/portal-shell/src/lib/api/operations/questions.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/questions.graphql.ts
@@ -6,6 +6,8 @@
// → 走 MSW 兜底(@contract-pending),等待后端补齐列表契约
// - Create/Update/DeleteQuestion(mutation):❌ schema 无 Mutation 类型
// → 走 MSW 兜底(@contract-pending),等待后端补齐 mutation 契约
+// - ImportQuestions/ExportQuestions/BatchDeleteQuestions:❌ schema 无对应根字段
+// → 走 MSW 兜底(@contract-pending),等待后端补齐契约
//
// 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#questions
// 关联:ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
@@ -103,3 +105,62 @@ export const DELETE_QUESTION_DOC = gql`
}
}
`;
+
+// 批量导入题目:schema 无 Mutation 类型
+// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
+// 契约工单:core-edu_contract.md#import-questions-mutation
+export const IMPORT_QUESTIONS_DOC = gql`
+ mutation ImportQuestions($input: ImportQuestionsInput!) {
+ importQuestions(input: $input) {
+ imported
+ skipped
+ errors
+ }
+ }
+`;
+
+// 导出题目:schema 无 Query 根字段
+// 页面通过 MSW 兜底获取导出数据,后端补齐后切换 fetcher
+// 契约工单:core-edu_contract.md#export-questions-query
+export const EXPORT_QUESTIONS_DOC = gql`
+ query ExportQuestions(
+ $type: String
+ $difficulty: String
+ $subjectId: String
+ $q: String
+ ) {
+ exportQuestions(
+ type: $type
+ difficulty: $difficulty
+ subjectId: $subjectId
+ q: $q
+ ) {
+ items {
+ id
+ type
+ content
+ difficulty
+ answer
+ explanation
+ knowledgePointId
+ subjectId
+ source
+ status
+ createdAt
+ }
+ total
+ }
+ }
+`;
+
+// 批量删除题目:schema 无 Mutation 类型
+// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
+// 契约工单:core-edu_contract.md#batch-delete-questions-mutation
+export const BATCH_DELETE_QUESTIONS_DOC = gql`
+ mutation BatchDeleteQuestions($ids: [ID!]!) {
+ batchDeleteQuestions(ids: $ids) {
+ deleted
+ failed
+ }
+ }
+`;
diff --git a/apps/portal-shell/src/lib/api/operations/student.graphql.ts b/apps/portal-shell/src/lib/api/operations/student.graphql.ts
index d8fd6d1..ba82179 100644
--- a/apps/portal-shell/src/lib/api/operations/student.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/student.graphql.ts
@@ -60,6 +60,10 @@ export const GET_ELECTIVE_COURSES_DOC = gql`
schedule
credits
category
+ subjectName
+ description
+ selectionMode
+ status
}
}
`;
@@ -73,8 +77,28 @@ export const ENROLL_COURSE_DOC = gql`
// From widgets/student/elective-selector
export const DROP_COURSE_DOC = gql`
- mutation DropCourse($courseId: ID!) {
- dropCourse(courseId: $courseId)
+ mutation DropCourse($courseId: ID!, $reason: String) {
+ dropCourse(courseId: $courseId, reason: $reason)
+ }
+`;
+
+// @contract-pending: studentSelections, MSW fallback
+export const GET_STUDENT_SELECTIONS_DOC = gql`
+ query StudentSelections($termId: ID!) {
+ studentSelections(termId: $termId) {
+ id
+ name
+ teacher
+ capacity
+ enrolled
+ schedule
+ credits
+ category
+ subjectName
+ description
+ selectionMode
+ status
+ }
}
`;
@@ -120,6 +144,27 @@ export const GET_STUDENT_GRADES_DOC = gql`
level
date
}
+ rankingTrend {
+ period
+ rank
+ totalStudents
+ }
+ distribution {
+ ranges {
+ range
+ count
+ isStudentIn
+ }
+ studentRank
+ studentTotal
+ }
+ growthArchive {
+ period
+ subjectScores {
+ subject
+ score
+ }
+ }
}
}
`;
@@ -129,6 +174,12 @@ export const GET_STUDENT_REPORT_CARD_DOC = gql`
studentReportCard(academicYearId: $academicYearId, semester: $semester) {
academicYear
semester
+ studentName
+ className
+ classTeacherName
+ generatedAt
+ overallRank
+ classTotalStudents
subjects {
subject
score
@@ -136,6 +187,16 @@ export const GET_STUDENT_REPORT_CARD_DOC = gql`
scoreRate
level
teacherComment
+ rankInSubject
+ totalStudentsInSubject
+ subjectAvg
+ records {
+ type
+ title
+ score
+ totalScore
+ remark
+ }
}
}
}
@@ -213,6 +274,9 @@ export const GET_STUDENT_HOMEWORK_DOC = gql`
dueDate
status
score
+ attemptsUsed
+ maxAttempts
+ latestScore
}
}
}
@@ -223,6 +287,10 @@ export const GET_STUDENT_HOMEWORK_SUBMIT_DOC = gql`
studentHomeworkSubmit(id: $id) {
title
dueDate
+ submissionStatus
+ maxAttempts
+ attemptsUsed
+ description
questions {
questionNo
question
@@ -264,6 +332,8 @@ export const GET_STUDENT_SCHEDULE_DOC = gql`
classroom
time
weekday
+ classId
+ startTime
}
}
}
@@ -272,20 +342,25 @@ export const GET_STUDENT_SCHEDULE_DOC = gql`
export const GET_STUDENT_ATTENDANCE_DOC = gql`
query GetStudentAttendance {
studentAttendance {
+ studentName
rate
lateCount
earlyLeaveCount
leaveCount
absentCount
+ totalRecords
+ presentCount
+ schoolActivityCount
+ lateRate
records {
date
status
remark
+ className
}
}
}
`;
-
export const GET_STUDENT_CLASSES_DOC = gql`
query GetStudentClasses {
studentClasses {
@@ -311,11 +386,26 @@ export const GET_STUDENT_COURSES_DOC = gql`
teacher
school
headTeacher
+ teacherEmail
+ room
+ grade
+ isActive
}
}
}
`;
+// Join class by invitation code (@contract-pending, MSW fallback)
+export const JOIN_CLASS_BY_INVITATION_CODE_DOC = gql`
+ mutation JoinClassByInvitationCode($input: JoinClassInput!) {
+ joinClassByInvitationCode(input: $input) {
+ success
+ classId
+ message
+ }
+ }
+`;
+
export const GET_STUDENT_COURSE_DETAIL_DOC = gql`
query GetStudentCourseDetail($id: ID!) {
studentCourseDetail(id: $id) {
@@ -335,11 +425,41 @@ export const GET_STUDENT_COURSE_DETAIL_DOC = gql`
teacher
classroom
time
+ startTime
+ endTime
+ course
}
}
}
`;
+// elective-detail-DOC:学生选课详情(@contract-pending,MSW 兜底)
+// schema 无 studentElectiveDetail 根字段,由 MSW handlers 返回 mock 数据
+export const GET_STUDENT_ELECTIVE_DETAIL_DOC = gql`
+ query GetStudentElectiveDetail($id: ID!) {
+ studentElectiveDetail(id: $id) {
+ id
+ name
+ teacher
+ capacity
+ enrolled
+ schedule
+ credits
+ category
+ subject
+ grade
+ classroom
+ selectionMode
+ startDate
+ endDate
+ selectionStartAt
+ selectionEndAt
+ description
+ status
+ }
+ }
+`;
+
// ── course-plans / lesson-plans / textbooks ─────────────────────
// @contract-pending: schema 无 studentCoursePlans / studentCoursePlanDetail /
// studentLessonPlans / studentLessonPlanView / studentTextbooks / studentTextbookChapters 根字段
@@ -352,11 +472,16 @@ export const GET_STUDENT_COURSE_PLANS_DOC = gql`
subject
grade
status
+ className
+ semester
+ teacherName
+ completedHours
+ totalHours
+ createdAt
}
}
}
`;
-
export const GET_STUDENT_COURSE_PLAN_DETAIL_DOC = gql`
query GetStudentCoursePlanDetail($id: ID!) {
studentCoursePlanDetail(id: $id) {
@@ -365,6 +490,27 @@ export const GET_STUDENT_COURSE_PLAN_DETAIL_DOC = gql`
grade
description
textbooks
+ className
+ teacherName
+ createdAt
+ startDate
+ endDate
+ completedHours
+ totalHours
+ completedItems
+ totalItems
+ syllabus
+ objectives
+ semester
+ status
+ items {
+ id
+ week
+ topic
+ hours
+ chapter
+ status
+ }
}
}
`;
@@ -378,11 +524,23 @@ export const GET_STUDENT_LESSON_PLANS_DOC = gql`
subject
grade
updatedAt
+ status
}
}
}
`;
+// 学科选项列表(@contract-pending,MSW 兜底;用于教案等列表的学科筛选器)
+// schema 无 subjectOptions 根字段 → MSW 兜底
+export const GET_SUBJECT_OPTIONS_DOC = gql`
+ query GetSubjectOptions {
+ subjectOptions {
+ id
+ name
+ }
+ }
+`;
+
export const GET_STUDENT_LESSON_PLAN_VIEW_DOC = gql`
query GetStudentLessonPlanView($planId: ID!) {
studentLessonPlanView(planId: $planId) {
@@ -390,6 +548,9 @@ export const GET_STUDENT_LESSON_PLAN_VIEW_DOC = gql`
subject
grade
content
+ status
+ textbookTitle
+ chapterTitle
}
}
`;
@@ -417,6 +578,15 @@ export const GET_STUDENT_TEXTBOOK_CHAPTERS_DOC = gql`
id
title
content
+ subject
+ grade
+ structuredContent {
+ sections {
+ heading
+ paragraphs
+ list
+ }
+ }
}
}
}
@@ -454,6 +624,20 @@ export const GET_STUDENT_ERROR_BOOK_V2_DOC = gql`
lastErrorAt
status
source
+ difficulty
+ errorTags
+ note
+ masteryLevel
+ reviewCount
+ nextReviewAt
+ createdAt
+ questionDetail {
+ id
+ content
+ type
+ options
+ answer
+ }
}
}
}
@@ -518,6 +702,38 @@ export const GET_STUDENT_PORTAL_WEAKNESS_DOC = gql`
}
`;
+// ── diagnostic:学生自我诊断(@contract-pending,MSW 兜底)─────────
+// schema 无 studentDiagnostic 根字段 → MSW 兜底
+// 契约工单:docs/architecture/issues/contracts/data-ana_contract.md#diagnostic-student-detail
+// 注意:与 diagnostic.graphql.ts 的 GET_STUDENT_DIAGNOSTIC_DOC(教师端,按 studentId 查询)
+// 是两个不同契约,此处为学生自我诊断视图(无参数),重命名以避免 barrel 导出冲突
+export const GET_STUDENT_SELF_DIAGNOSTIC_DOC = gql`
+ query GetStudentSelfDiagnostic {
+ studentDiagnostic {
+ summary {
+ studentName
+ overallMastery
+ strengthCount
+ weaknessCount
+ }
+ masteryPoints {
+ name
+ masteryRate
+ isStrength
+ }
+ reports {
+ id
+ status
+ confidence
+ period
+ score
+ summary
+ recommendations
+ }
+ }
+ }
+`;
+
// ── practice / leave / messages / announcements ─────────────────
// @contract-pending: schema 无 studentPractice / studentPracticeSession /
// studentLeave / studentMessages / studentAnnouncements / studentAnnouncementDetail 根字段
@@ -559,9 +775,25 @@ export const GET_STUDENT_PRACTICE_SESSION_DOC = gql`
}
`;
+// 学生练习可选知识点列表(@contract-pending,MSW 兜底)
+// schema 无 studentKnowledgePoints 根字段 → MSW 兜底
+// 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#student-practice
+export const GET_STUDENT_KNOWLEDGE_POINTS_DOC = gql`
+ query GetStudentKnowledgePoints($subject: String) {
+ studentKnowledgePoints(subject: $subject) {
+ id
+ name
+ subject
+ grade
+ difficulty
+ description
+ }
+ }
+`;
+
export const GET_STUDENT_LEAVE_DOC = gql`
- query GetStudentLeave {
- studentLeave {
+ query GetStudentLeave($page: Int, $pageSize: Int) {
+ studentLeave(page: $page, pageSize: $pageSize) {
items {
id
startDate
@@ -570,6 +802,9 @@ export const GET_STUDENT_LEAVE_DOC = gql`
reason
status
}
+ total
+ page
+ pageSize
}
}
`;
@@ -612,6 +847,49 @@ export const GET_STUDENT_ANNOUNCEMENT_DETAIL_DOC = gql`
}
`;
+// ── 消息详情 / 撰写 / 群发(@contract-pending,MSW 兜底)──────────
+// 单查消息详情:schema 无 studentMessageDetail(id) 根字段 → MSW 兜底
+// 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#messages
+export const GET_STUDENT_MESSAGE_DETAIL_DOC = gql`
+ query GetStudentMessageDetail($id: ID!) {
+ studentMessageDetail(id: $id) {
+ id
+ from
+ from_id
+ to
+ to_id
+ subject
+ body
+ date
+ status
+ is_starred
+ }
+ }
+`;
+
+// 消息收件人列表:schema 无 messageRecipients 根字段 → MSW 兜底
+// 用于撰写消息页选择收件人
+export const GET_MESSAGE_RECIPIENTS_DOC = gql`
+ query GetMessageRecipients {
+ messageRecipients {
+ id
+ name
+ role
+ avatar_url
+ }
+ }
+`;
+
+// 发送消息:schema 无 Mutation.sendMessage 类型 → MSW 兜底
+// 用于撰写消息页提交表单
+export const SEND_STUDENT_MESSAGE_DOC = gql`
+ mutation SendMessage($input: SendMessageInput!) {
+ sendMessage(input: $input) {
+ id
+ success
+ }
+ }
+`;
// ── Mutations(@contract-pending,schema 无 Mutation 类型)─────────
// @contract-pending: 以下 mutation 均不存在于 schema,由 MSW 兜底
// 提交考试作答:answers 为 Record,使用 JSON 标量透传
@@ -630,8 +908,8 @@ export const SUBMIT_STUDENT_HOMEWORK_DOC = gql`
// 开始练习会话:返回 session id
export const START_PRACTICE_SESSION_DOC = gql`
- mutation StartPracticeSession($knowledgePointId: ID!) {
- startPracticeSession(knowledgePointId: $knowledgePointId)
+ mutation StartPracticeSession($knowledgePointIds: [ID!]!) {
+ startPracticeSession(knowledgePointIds: $knowledgePointIds)
}
`;
diff --git a/apps/portal-shell/src/lib/api/practice.ts b/apps/portal-shell/src/lib/api/practice.ts
index 0e632a6..269e639 100644
--- a/apps/portal-shell/src/lib/api/practice.ts
+++ b/apps/portal-shell/src/lib/api/practice.ts
@@ -17,8 +17,15 @@ import { useWidgetMutation } from "../useWidgetMutation";
import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
+ ABANDON_ADAPTIVE_PRACTICE_SESSION_DOC,
+ COMPLETE_ADAPTIVE_PRACTICE_SESSION_DOC,
+ CREATE_ADAPTIVE_PRACTICE_SESSION_DOC,
CREATE_PRACTICE_DOC,
+ GET_ADAPTIVE_PRACTICE_SESSIONS_DOC,
+ GET_ADAPTIVE_PRACTICE_SESSION_DETAIL_DOC,
+ GET_ADAPTIVE_PRACTICE_STATS_DOC,
GET_PRACTICES_DOC,
+ SUBMIT_ADAPTIVE_PRACTICE_ANSWER_DOC,
} from "./operations/practice.graphql";
import type { UseQueryResult } from "./types";
@@ -162,3 +169,356 @@ export function useCreatePractice(): {
return { run, loading, error };
}
+
+// ════════════════════════════════════════════════════════════════
+// Adaptive Practice(自适应练习)Hooks(@contract-pending,全部 MSW 兜底)
+//
+// 从 CICD src/modules/adaptive-practice 迁移。
+// 替代 CICD 的 Server Actions + practice-service.tsx 模式,
+// 改为 portal-shell 的 GraphQL hooks 模式。
+//
+// schema 无对应根字段,全部通过 MSW handlers 返回 mock 数据。
+// 后端补齐契约后切换真实 fetcher,组件无需改动。
+// ════════════════════════════════════════════════════════════════
+
+// ── 类型定义 ──────────────────────────────────────────────────
+
+/** 自适应练习类型 */
+export type AdaptivePracticeType =
+ "error_variant" | "knowledge_point" | "weak_chapter" | "ai_recommended";
+
+/** 自适应练习会话状态(与教师域 PracticeStatus 区分) */
+export type AdaptivePracticeStatus = "in_progress" | "completed" | "abandoned";
+
+/** 答题状态 */
+export type AdaptivePracticeAnswerStatus = "pending" | "answered" | "skipped";
+
+/** AI 推荐理由 */
+export type AdaptiveAiRecommendedReason =
+ "student_initiated" | "teacher_assigned" | "parent_suggested";
+
+/** 自适应练习会话摘要(列表视图) */
+export interface AdaptivePracticeSessionSummary {
+ id: string;
+ studentId: string;
+ subjectId: string | null;
+ practiceType: AdaptivePracticeType;
+ status: AdaptivePracticeStatus;
+ totalQuestions: number;
+ answeredQuestions: number;
+ correctCount: number;
+ /** 正确率 0-1 */
+ accuracy: number;
+ startedAt: string;
+ completedAt: string | null;
+ createdAt: string;
+}
+
+/** 答题记录 */
+export interface AdaptivePracticeAnswerRecord {
+ id: string;
+ sessionId: string;
+ questionId: string;
+ variantContent: unknown | null;
+ isVariant: boolean;
+ orderIndex: number;
+ status: AdaptivePracticeAnswerStatus;
+ studentAnswer: unknown | null;
+ isCorrect: boolean | null;
+ score: number | null;
+ maxScore: number;
+ answeredAt: string | null;
+ question: {
+ id: string;
+ content: unknown;
+ type: string;
+ difficulty: number | null;
+ } | null;
+}
+
+/** 自适应练习会话详情(含答题记录) */
+export interface AdaptivePracticeSessionDetail extends AdaptivePracticeSessionSummary {
+ sourceMeta: Record | null;
+ answers: AdaptivePracticeAnswerRecord[];
+}
+
+/** 自适应练习统计 */
+export interface AdaptivePracticeStats {
+ totalSessions: number;
+ completedSessions: number;
+ totalQuestionsAnswered: number;
+ totalCorrect: number;
+ overallAccuracy: number;
+ byType: Array<{
+ practiceType: AdaptivePracticeType;
+ sessionCount: number;
+ totalQuestions: number;
+ correctCount: number;
+ accuracy: number;
+ }>;
+}
+
+/** 创建练习会话的输入 */
+export interface CreateAdaptivePracticeSessionInput {
+ practiceType: AdaptivePracticeType;
+ subjectId?: string;
+ sourceMeta: Record;
+ questionCount?: number;
+}
+
+/** 提交答案的输入 */
+export interface SubmitAdaptivePracticeAnswerInput {
+ sessionId: string;
+ answerId: string;
+ answer: unknown;
+ skip: boolean;
+}
+
+// ── 查询 Hooks ────────────────────────────────────────────────
+
+interface AdaptivePracticeSessionsResponse {
+ adaptivePracticeSessions: {
+ items: AdaptivePracticeSessionSummary[];
+ total: number;
+ };
+}
+
+interface AdaptivePracticeSessionDetailResponse {
+ adaptivePracticeSession: AdaptivePracticeSessionDetail | null;
+}
+
+interface AdaptivePracticeStatsResponse {
+ adaptivePracticeStats: AdaptivePracticeStats | null;
+}
+
+/**
+ * 查询自适应练习会话列表(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useAdaptivePracticeSessions(
+ studentId?: string,
+ options?: PracticeQueryOptions,
+): UseQueryResult<{ items: AdaptivePracticeSessionSummary[]; total: number }> {
+ const result = useWidgetQuery<
+ AdaptivePracticeSessionsResponse,
+ { studentId?: string; limit?: number; offset?: number }
+ >(
+ GET_ADAPTIVE_PRACTICE_SESSIONS_DOC,
+ { studentId, limit: 50, offset: 0 },
+ {
+ enabled: options?.enabled ?? true,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.adaptivePracticeSessions,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 查询单个自适应练习会话详情(含答题记录)(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useAdaptivePracticeSessionDetail(
+ sessionId: string,
+ studentId?: string,
+ options?: PracticeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ AdaptivePracticeSessionDetailResponse,
+ { sessionId: string; studentId?: string }
+ >(
+ GET_ADAPTIVE_PRACTICE_SESSION_DETAIL_DOC,
+ { sessionId, studentId },
+ {
+ enabled: options?.enabled ?? !!sessionId,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.adaptivePracticeSession ?? undefined,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+/**
+ * 查询自适应练习统计(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useAdaptivePracticeStats(
+ studentId?: string,
+ options?: PracticeQueryOptions,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ AdaptivePracticeStatsResponse,
+ { studentId?: string }
+ >(
+ GET_ADAPTIVE_PRACTICE_STATS_DOC,
+ { studentId },
+ {
+ enabled: options?.enabled ?? true,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.adaptivePracticeStats ?? undefined,
+ loading: result.loading,
+ error: result.error,
+ refetch: result.refetch,
+ };
+}
+
+// ── 变更 Hooks ────────────────────────────────────────────────
+
+interface CreateAdaptivePracticeSessionResponse {
+ createAdaptivePracticeSession: {
+ sessionId: string;
+ selectedCount: number;
+ } | null;
+}
+
+interface SubmitAdaptivePracticeAnswerResponse {
+ submitAdaptivePracticeAnswer: {
+ isCorrect: boolean | null;
+ score: number | null;
+ } | null;
+}
+
+interface CompleteAdaptivePracticeSessionResponse {
+ completeAdaptivePracticeSession: boolean;
+}
+
+interface AbandonAdaptivePracticeSessionResponse {
+ abandonAdaptivePracticeSession: boolean;
+}
+
+/**
+ * 创建自适应练习会话 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 后端补齐 mutation 后切换真实 fetcher,组件无需改动。
+ */
+export function useCreateAdaptivePracticeSession(): {
+ run: (
+ input: CreateAdaptivePracticeSessionInput,
+ ) => Promise<{ sessionId: string; selectedCount: number }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ CreateAdaptivePracticeSessionResponse,
+ { input: CreateAdaptivePracticeSessionInput }
+ >(CREATE_ADAPTIVE_PRACTICE_SESSION_DOC);
+
+ const run = async (
+ input: CreateAdaptivePracticeSessionInput,
+ ): Promise<{ sessionId: string; selectedCount: number }> => {
+ const data = await rawRun({ input });
+ if (!data?.createAdaptivePracticeSession) {
+ throw new ApiError(
+ "Failed to create adaptive practice session",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.createAdaptivePracticeSession;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 提交单题答案 mutation(@contract-pending,MSW 兜底)。
+ */
+export function useSubmitAdaptivePracticeAnswer(): {
+ run: (
+ input: SubmitAdaptivePracticeAnswerInput,
+ ) => Promise<{ isCorrect: boolean | null; score: number | null }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ SubmitAdaptivePracticeAnswerResponse,
+ { input: SubmitAdaptivePracticeAnswerInput }
+ >(SUBMIT_ADAPTIVE_PRACTICE_ANSWER_DOC);
+
+ const run = async (
+ input: SubmitAdaptivePracticeAnswerInput,
+ ): Promise<{ isCorrect: boolean | null; score: number | null }> => {
+ const data = await rawRun({ input });
+ if (!data?.submitAdaptivePracticeAnswer) {
+ throw new ApiError(
+ "Failed to submit adaptive practice answer",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.submitAdaptivePracticeAnswer;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 完成自适应练习会话 mutation(@contract-pending,MSW 兜底)。
+ */
+export function useCompleteAdaptivePracticeSession(): {
+ run: (sessionId: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ CompleteAdaptivePracticeSessionResponse,
+ { sessionId: string }
+ >(COMPLETE_ADAPTIVE_PRACTICE_SESSION_DOC);
+
+ const run = async (sessionId: string): Promise => {
+ await rawRun({ sessionId });
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 放弃自适应练习会话 mutation(@contract-pending,MSW 兜底)。
+ */
+export function useAbandonAdaptivePracticeSession(): {
+ run: (sessionId: string) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ AbandonAdaptivePracticeSessionResponse,
+ { sessionId: string }
+ >(ABANDON_ADAPTIVE_PRACTICE_SESSION_DOC);
+
+ const run = async (sessionId: string): Promise => {
+ await rawRun({ sessionId });
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/questions.ts b/apps/portal-shell/src/lib/api/questions.ts
index 25125db..a2b609a 100644
--- a/apps/portal-shell/src/lib/api/questions.ts
+++ b/apps/portal-shell/src/lib/api/questions.ts
@@ -3,10 +3,11 @@
/**
* Questions domain API(ARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域题库模块)
*
- * 三类操作:
+ * 操作:
* 1. useQuestion(按 id 单查):✅ 真实查询 question(id: ID!),schema 已就绪
* 2. useQuestions(列表查询):❌ schema 无 questions(...) 根字段 → MSW 兜底(@contract-pending)
* 3. useCreate/useUpdate/useDeleteQuestion(mutation):❌ schema 无 Mutation → MSW 兜底(@contract-pending)
+ * 4. useImportQuestions / useExportQuestions / useBatchDeleteQuestions(批量操作):❌ schema 无对应根字段 → MSW 兜底
*
* 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#questions
* 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock
@@ -19,10 +20,13 @@ import { useWidgetMutation } from "../useWidgetMutation";
import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
+ BATCH_DELETE_QUESTIONS_DOC,
CREATE_QUESTION_DOC,
DELETE_QUESTION_DOC,
+ EXPORT_QUESTIONS_DOC,
GET_QUESTION_DOC,
GET_QUESTIONS_DOC,
+ IMPORT_QUESTIONS_DOC,
UPDATE_QUESTION_DOC,
} from "./operations/questions.graphql";
import type { UseQueryResult } from "./types";
@@ -133,6 +137,66 @@ interface DeleteQuestionResponse {
deleteQuestion: { id: string } | null;
}
+// ===== 批量操作类型(@contract-pending)=====
+
+/**
+ * 批量导入题目输入。
+ *
+ * payload 为文件原文(CSV/JSON 字符串);format 区分解析方式。
+ * 后端补齐 mutation 后切换 fetcher,调用方无需改动。
+ */
+export interface ImportQuestionsInput {
+ payload: string;
+ format: "csv" | "json";
+}
+
+/** 批量导入题目返回结果 */
+export interface ImportQuestionsResult {
+ imported: number;
+ skipped: number;
+ errors: string[];
+}
+
+/** 导入题目 mutation 响应(@contract-pending) */
+interface ImportQuestionsResponse {
+ importQuestions: ImportQuestionsResult | null;
+}
+
+/**
+ * 导出题目列表项(包含 answer / explanation 等完整字段)。
+ *
+ * 调用方拿到 items 后自行转为 CSV / JSON 下载。
+ */
+export interface ExportQuestionItem {
+ id: string;
+ type: string;
+ content: string;
+ difficulty: string;
+ answer: string;
+ explanation: string | null;
+ knowledgePointId: string;
+ subjectId: string;
+ source: string;
+ status: string;
+ createdAt: string;
+}
+
+/** 导出题目查询响应(@contract-pending) */
+interface ExportQuestionsResponse {
+ exportQuestions: { items: ExportQuestionItem[]; total: number } | null;
+}
+
+/** 批量删除题目返回结果 */
+export interface BatchDeleteQuestionsResult {
+ deleted: number;
+ failed: number;
+}
+
+/** 批量删除题目 mutation 响应(@contract-pending) */
+interface BatchDeleteQuestionsResponse {
+ batchDeleteQuestions: BatchDeleteQuestionsResult | null;
+}
+
// ===== 查询选项 =====
export interface QuestionQueryOptions {
@@ -313,3 +377,126 @@ export function useDeleteQuestion(): {
return { run, loading, error };
}
+
+/**
+ * 批量导入题目 mutation(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
+ * payload 由调用方读取文件后传入字符串原文;format 区分 JSON/CSV。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useImportQuestions(): {
+ run: (input: ImportQuestionsInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ ImportQuestionsResponse,
+ { input: ImportQuestionsInput }
+ >(IMPORT_QUESTIONS_DOC);
+
+ const run = async (
+ input: ImportQuestionsInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.importQuestions) {
+ throw new ApiError("Failed to import questions", "INTERNAL_ERROR");
+ }
+ return data.importQuestions;
+ };
+
+ return { run, loading, error };
+}
+
+/**
+ * 导出题目查询(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 exportQuestions(...) 根字段,由 MSW handlers 返回 mock 数据。
+ * 调用方拿到 items 后自行转换为 CSV / JSON 下载。
+ *
+ * 默认禁用查询(enabled=false),调用方点击导出按钮时通过 refetch 触发;
+ * 也可通过 options.enabled 主动启用。
+ *
+ * refetch 返回已解包的 `{ items, total }`,调用方无需再做 `as` 断言。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useExportQuestions(
+ filter: QuestionsListFilter,
+ options?: QuestionQueryOptions,
+): Omit<
+ UseQueryResult<{ items: ExportQuestionItem[]; total: number }>,
+ "refetch"
+> & {
+ refetch: () => Promise<
+ { items: ExportQuestionItem[]; total: number } | undefined
+ >;
+} {
+ const result = useWidgetQuery<
+ ExportQuestionsResponse,
+ {
+ type?: string;
+ difficulty?: string;
+ subjectId?: string;
+ q?: string;
+ }
+ >(
+ EXPORT_QUESTIONS_DOC,
+ {
+ type: filter.type,
+ difficulty: filter.difficulty,
+ subjectId: filter.subjectId,
+ q: filter.q,
+ },
+ {
+ enabled: options?.enabled ?? false,
+ fetchPolicy: options?.fetchPolicy,
+ pollInterval: options?.pollInterval,
+ },
+ );
+ return {
+ data: result.data?.exportQuestions ?? undefined,
+ loading: result.loading,
+ error: result.error,
+ refetch: async () => {
+ const r = await result.refetch();
+ return r.data?.exportQuestions ?? undefined;
+ },
+ };
+}
+
+/**
+ * 批量删除题目 mutation(@contract-pending,MSW 兜底)。
+ *
+ * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
+ */
+export function useBatchDeleteQuestions(): {
+ run: (ids: string[]) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ BATCH_DELETE_QUESTIONS_DOC,
+ );
+
+ const run = async (ids: string[]): Promise => {
+ const data = await rawRun({ ids });
+ if (!data?.batchDeleteQuestions) {
+ throw new ApiError("Failed to batch delete questions", "INTERNAL_ERROR");
+ }
+ return data.batchDeleteQuestions;
+ };
+
+ return { run, loading, error };
+}
diff --git a/apps/portal-shell/src/lib/api/student-portal.ts b/apps/portal-shell/src/lib/api/student-portal.ts
index d80b4b6..ccddecf 100644
--- a/apps/portal-shell/src/lib/api/student-portal.ts
+++ b/apps/portal-shell/src/lib/api/student-portal.ts
@@ -38,26 +38,34 @@ import {
GET_STUDENT_ATTENDANCE_DOC,
GET_STUDENT_CLASSES_DOC,
GET_STUDENT_COURSES_DOC,
+ JOIN_CLASS_BY_INVITATION_CODE_DOC,
GET_STUDENT_COURSE_DETAIL_DOC,
+ GET_STUDENT_ELECTIVE_DETAIL_DOC,
GET_STUDENT_COURSE_PLANS_DOC,
GET_STUDENT_COURSE_PLAN_DETAIL_DOC,
GET_STUDENT_LESSON_PLANS_DOC,
GET_STUDENT_LESSON_PLAN_VIEW_DOC,
+ GET_SUBJECT_OPTIONS_DOC,
GET_STUDENT_TEXTBOOKS_DOC,
GET_STUDENT_TEXTBOOK_CHAPTERS_DOC,
GET_STUDENT_ERROR_BOOK_V2_DOC,
GET_STUDENT_LEARNING_CENTER_DOC,
GET_STUDENT_TREND_DOC,
GET_STUDENT_PORTAL_WEAKNESS_DOC,
+ GET_STUDENT_SELF_DIAGNOSTIC_DOC,
GET_STUDENT_PRACTICE_DOC,
GET_STUDENT_PRACTICE_SESSION_DOC,
GET_STUDENT_LEAVE_DOC,
GET_STUDENT_MESSAGES_DOC,
GET_STUDENT_ANNOUNCEMENTS_DOC,
GET_STUDENT_ANNOUNCEMENT_DETAIL_DOC,
+ GET_STUDENT_MESSAGE_DETAIL_DOC,
+ GET_MESSAGE_RECIPIENTS_DOC,
+ SEND_STUDENT_MESSAGE_DOC,
SUBMIT_STUDENT_EXAM_DOC,
SUBMIT_STUDENT_HOMEWORK_DOC,
START_PRACTICE_SESSION_DOC,
+ GET_STUDENT_KNOWLEDGE_POINTS_DOC,
SUBMIT_PRACTICE_ANSWER_DOC,
SUBMIT_LEAVE_REQUEST_DOC,
// elective / ai-tutor 操作文档由 ./student.ts 使用,此处不再引入
@@ -78,6 +86,72 @@ export interface StudentGrade {
date: string;
}
+/** 排名趋势数据点(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentRankingTrendPoint {
+ /** 考试周期标识(如 ISO 日期或考试名) */
+ period: string;
+ /** 班级排名(1 起,越小越好) */
+ rank: number;
+ /** 班级总人数 */
+ totalStudents: number;
+}
+
+/** 分数段分布区间(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentScoreDistributionRange {
+ /** 分数段标签(如 "90-100" / "<60") */
+ range: string;
+ /** 该区间人数 */
+ count: number;
+ /** 当前学生是否落在该区间 */
+ isStudentIn: boolean;
+}
+
+/** 班级分数段分布(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentScoreDistribution {
+ ranges: StudentScoreDistributionRange[];
+ /** 当前学生在班级的排名 */
+ studentRank: number;
+ /** 班级总人数 */
+ studentTotal: number;
+}
+
+/** 成长档案单学科得分(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentGrowthArchiveSubjectScore {
+ subject: string;
+ score: number;
+}
+
+/** 成长档案数据点(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentGrowthArchivePoint {
+ /** 学年-学期标签(如 "2024-2025 上") */
+ period: string;
+ /** 该学期各学科得分 */
+ subjectScores: StudentGrowthArchiveSubjectScore[];
+}
+
+/**
+ * 学生成绩聚合结果(@contract-pending,core-edu 接入后由后端返回)。
+ *
+ * - items:成绩明细列表(已有契约)
+ * - rankingTrend:排名趋势(可选,未传时前端降级为 EmptyState)
+ * - distribution:班级分布(可选,未传时前端降级为 EmptyState)
+ * - growthArchive:成长档案(可选,未传时前端降级为 EmptyState)
+ */
+export interface StudentGradesResult {
+ items: StudentGrade[];
+ rankingTrend?: StudentRankingTrendPoint[];
+ distribution?: StudentScoreDistribution;
+ growthArchive?: StudentGrowthArchivePoint[];
+}
+
+export interface StudentReportCardRecord {
+ type: string;
+ title?: string;
+ score: number;
+ totalScore: number;
+ remark?: string;
+}
+
export interface StudentReportCardSubject {
subject: string;
score: number;
@@ -85,23 +159,33 @@ export interface StudentReportCardSubject {
scoreRate: number;
level: string;
teacherComment: string;
+ rankInSubject?: number;
+ totalStudentsInSubject?: number;
+ subjectAvg?: number;
+ records?: StudentReportCardRecord[];
}
export interface StudentReportCard {
academicYear: string;
semester: string;
subjects: StudentReportCardSubject[];
+ studentName?: string;
+ className?: string;
+ classTeacherName?: string;
+ generatedAt?: string;
+ overallRank?: number;
+ classTotalStudents?: number;
}
-/** 学生成绩列表(@contract-pending,MSW 兜底) */
+/** 学生成绩列表(@contract-pending,MSW 兜底;含排名趋势/班级分布/成长档案扩展字段) */
export function useStudentGrades(
options: { subject?: string; type?: string; q?: string } = {},
-): UseQueryResult {
+): UseQueryResult {
const result = useWidgetQuery<
- { studentGrades: { items: StudentGrade[] } },
+ { studentGrades: StudentGradesResult },
{ subject?: string; type?: string; q?: string }
>(GET_STUDENT_GRADES_DOC, options);
- return { ...result, data: result.data?.studentGrades?.items };
+ return { ...result, data: result.data?.studentGrades };
}
/** 成绩报告卡(@contract-pending,MSW 兜底) */
@@ -237,6 +321,12 @@ export interface StudentHomework {
dueDate: string;
status: string;
score: number | null;
+ /** 已用尝试次数(@contract-pending,core-edu 接入后由后端返回) */
+ attemptsUsed: number;
+ /** 最大尝试次数(@contract-pending,core-edu 接入后由后端返回) */
+ maxAttempts: number;
+ /** 最新一次尝试成绩(@contract-pending,null 表示尚未作答) */
+ latestScore: number | null;
}
export interface StudentHomeworkSubmitQuestion {
@@ -247,10 +337,26 @@ export interface StudentHomeworkSubmitQuestion {
answer: string;
}
+/**
+ * 学生作业作答数据。
+ *
+ * `submissionStatus` 用于分流渲染:
+ * - `not_started` / `in_progress` → 作答工作台(TakeView)
+ * - `submitted` / `graded` → 只读复盘视图(ReviewView)
+ *
+ * `maxAttempts` / `attemptsUsed` 用于侧栏尝试次数展示。
+ * `description` 用于侧栏作业描述展示。
+ */
export interface StudentHomeworkSubmit {
title: string;
dueDate: string;
questions: StudentHomeworkSubmitQuestion[];
+ submissionStatus?: string;
+ maxAttempts?: number;
+ attemptsUsed?: number;
+ description?: string;
+ submittedAt?: string;
+ feedback?: string;
}
export interface StudentHomeworkAnalysisQuestion {
@@ -341,20 +447,36 @@ export interface StudentScheduleItem {
classroom: string;
time: string;
weekday: number;
+ /** 关联班级 id(@contract-pending,用于班级筛选与课程跳转) */
+ classId: string;
+ /** 节次开始时间(HH:mm,@contract-pending,用于排序) */
+ startTime: string;
}
export interface StudentAttendanceRecord {
date: string;
status: string;
remark: string;
+ /** className */
+ className?: string;
}
export interface StudentAttendance {
+ /** studentName */
+ studentName?: string;
rate: number;
lateCount: number;
earlyLeaveCount: number;
leaveCount: number;
absentCount: number;
+ /** totalRecords */
+ totalRecords?: number;
+ /** presentCount */
+ presentCount?: number;
+ /** schoolActivityCount */
+ schoolActivityCount?: number;
+ /** lateRate */
+ lateRate?: number;
records: StudentAttendanceRecord[];
}
@@ -374,6 +496,30 @@ export interface StudentCourse {
teacher: string;
school: string;
headTeacher: string;
+ /** 教师邮箱(用于 mailto 链接,@contract-pending) */
+ teacherEmail?: string;
+ /** 教室(@contract-pending) */
+ room?: string;
+ /** 年级(@contract-pending) */
+ grade?: string;
+ /** 是否活跃(@contract-pending) */
+ isActive?: boolean;
+}
+
+/** 加入班级 mutation 输入(@contract-pending,MSW 兜底) */
+export interface JoinClassByInvitationCodeInput {
+ /** 6 位数字邀请码 */
+ code: string;
+}
+
+/** 加入班级 mutation 返回(@contract-pending,MSW 兜底) */
+export interface JoinClassResult {
+ /** 是否加入成功 */
+ success: boolean;
+ /** 加入的班级 id(成功时) */
+ classId?: string;
+ /** 失败原因(如邀请码无效、已加入该班级) */
+ message?: string;
}
export interface StudentCourseTeacher {
@@ -388,6 +534,12 @@ export interface StudentCourseScheduleItem {
teacher: string;
classroom: string;
time: string;
+ /** 开始时间 HH:mm(@contract-pending) */
+ startTime: string;
+ /** 结束时间 HH:mm(@contract-pending) */
+ endTime: string;
+ /** 课程名称(@contract-pending) */
+ course: string;
}
export interface StudentCourseDetail {
@@ -440,6 +592,35 @@ export function useStudentCourses(
return { ...result, data: result.data?.studentCourses?.items };
}
+/** 加入班级(@contract-pending,MSW 兜底) */
+export function useJoinClassByInvitationCode(): {
+ run: (input: JoinClassByInvitationCodeInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { joinClassByInvitationCode: JoinClassResult },
+ { input: JoinClassByInvitationCodeInput }
+ >(JOIN_CLASS_BY_INVITATION_CODE_DOC);
+ const run = async (
+ input: JoinClassByInvitationCodeInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.joinClassByInvitationCode?.success) {
+ throw new ApiError(
+ data?.joinClassByInvitationCode?.message ?? "Failed to join class",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.joinClassByInvitationCode;
+ };
+ return { run, loading, error };
+}
+
/** 课程详情(@contract-pending,MSW 兜底) */
export function useStudentCourseDetail(
id: string,
@@ -451,6 +632,67 @@ export function useStudentCourseDetail(
return { ...result, data: result.data?.studentCourseDetail ?? undefined };
}
+// =====================================================================
+// elective detail:学生选课详情(@contract-pending,MSW 兜底)
+// =====================================================================
+
+/** 选修课状态枚举(学生视角,@contract-pending,与 i18n status* key 对齐) */
+export type StudentElectiveStatus = "draft" | "open" | "closed" | "cancelled";
+
+/** 选课模式(@contract-pending) */
+export type ElectiveSelectionMode = "fcfs" | "lottery";
+
+/**
+ * 选修课详情实体(@contract-pending,schema 无 studentElectiveDetail 根字段)。
+ *
+ * 字段形状由 MSW mock 定义,后端补齐后对齐真实 schema。
+ * 学生视角不展示选课名单(隐私保护)。
+ */
+export interface ElectiveCourse {
+ id: string;
+ name: string;
+ teacher: string;
+ capacity: number;
+ enrolled: number;
+ schedule: string;
+ credits: number;
+ category: string;
+ /** 学科(@contract-pending) */
+ subject: string;
+ /** 年级(@contract-pending) */
+ grade: string;
+ /** 教室(@contract-pending) */
+ classroom: string;
+ /** 选课模式(@contract-pending) */
+ selectionMode: ElectiveSelectionMode;
+ /** 课程开始日期 ISO(@contract-pending) */
+ startDate: string;
+ /** 课程结束日期 ISO(@contract-pending) */
+ endDate: string;
+ /** 选课开始时间 ISO(@contract-pending) */
+ selectionStartAt: string;
+ /** 选课结束时间 ISO(@contract-pending) */
+ selectionEndAt: string;
+ /** 课程描述(@contract-pending) */
+ description: string;
+ /** 课程状态(@contract-pending) */
+ status: StudentElectiveStatus;
+}
+
+/** 学生选课详情(@contract-pending,MSW 兜底) */
+export function useStudentElectiveDetail(
+ id: string,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ { studentElectiveDetail: ElectiveCourse | null },
+ { id: string }
+ >(GET_STUDENT_ELECTIVE_DETAIL_DOC, { id }, { enabled: id.length > 0 });
+ return {
+ ...result,
+ data: result.data?.studentElectiveDetail ?? undefined,
+ };
+}
+
// =====================================================================
// course-plans / lesson-plans / textbooks
// =====================================================================
@@ -461,6 +703,32 @@ export interface StudentCoursePlan {
subject: string;
grade: string;
status: string;
+ /** className */
+ className?: string;
+ /** semester */
+ semester?: string;
+ /** teacherName */
+ teacherName?: string;
+ /** completedHours */
+ completedHours?: number;
+ /** totalHours */
+ totalHours?: number;
+ /** createdAt */
+ createdAt?: string;
+}
+
+/** 学生课程计划周计划项状态(@contract-pending,core-edu 接入后由后端返回) */
+export type StudentCoursePlanItemStatus =
+ "planned" | "in_progress" | "completed" | "skipped";
+
+/** 学生课程计划周计划项(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentCoursePlanItem {
+ id: string;
+ week: number;
+ topic: string;
+ hours: number;
+ chapter?: string;
+ status: StudentCoursePlanItemStatus;
}
export interface StudentCoursePlanDetail {
@@ -469,6 +737,34 @@ export interface StudentCoursePlanDetail {
grade: string;
description: string;
textbooks: string[];
+ /** 班级名称(@contract-pending,core-edu 接入后由后端返回) */
+ className?: string;
+ /** 教师姓名(@contract-pending,core-edu 接入后由后端返回) */
+ teacherName?: string;
+ /** 创建时间 ISO(@contract-pending,core-edu 接入后由后端返回) */
+ createdAt?: string;
+ /** 计划开始日期 ISO(@contract-pending,core-edu 接入后由后端返回) */
+ startDate?: string;
+ /** 计划结束日期 ISO(@contract-pending,core-edu 接入后由后端返回) */
+ endDate?: string;
+ /** 已完成学时(@contract-pending,core-edu 接入后由后端返回) */
+ completedHours?: number;
+ /** 总学时(@contract-pending,core-edu 接入后由后端返回) */
+ totalHours?: number;
+ /** 已完成周计划数(@contract-pending,core-edu 接入后由后端返回) */
+ completedItems?: number;
+ /** 周计划总数(@contract-pending,core-edu 接入后由后端返回) */
+ totalItems?: number;
+ /** 教学大纲(@contract-pending,core-edu 接入后由后端返回) */
+ syllabus?: string;
+ /** 教学目标(@contract-pending,core-edu 接入后由后端返回) */
+ objectives?: string;
+ /** 学期(@contract-pending,core-edu 接入后由后端返回) */
+ semester?: string;
+ /** 状态(@contract-pending,core-edu 接入后由后端返回) */
+ status?: string;
+ /** 周计划列表(@contract-pending,core-edu 接入后由后端返回) */
+ items?: StudentCoursePlanItem[];
}
export interface StudentLessonPlan {
@@ -477,6 +773,14 @@ export interface StudentLessonPlan {
subject: string;
grade: string;
updatedAt: string;
+ /** 教案状态(@contract-pending,core-edu 接入后由后端返回;前端按 "published" 过滤) */
+ status?: string;
+}
+
+/** 学科选项(@contract-pending,MSW 兜底;用于教案等列表的学科筛选器) */
+export interface SubjectOption {
+ id: string;
+ name: string;
}
export interface StudentLessonPlanView {
@@ -484,6 +788,12 @@ export interface StudentLessonPlanView {
subject: string;
grade: string;
content: string;
+ /** 教案状态(@contract-pending,core-edu 接入后由后端返回) */
+ status?: string;
+ /** 教材标题(@contract-pending,core-edu 接入后由后端返回) */
+ textbookTitle?: string;
+ /** 章节标题(@contract-pending,core-edu 接入后由后端返回) */
+ chapterTitle?: string;
}
export interface StudentTextbook {
@@ -493,10 +803,32 @@ export interface StudentTextbook {
grade: string;
}
+export interface StudentTextbookChapterSection {
+ /** 段落小标题 */
+ heading?: string;
+ /** 段落正文(每项一段) */
+ paragraphs?: string[];
+ /** 列表项 */
+ list?: string[];
+}
+
+/** 章节结构化内容(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentTextbookChapterContent {
+ /** 结构化分节内容 */
+ sections: StudentTextbookChapterSection[];
+}
+
export interface StudentTextbookChapter {
id: string;
title: string;
+ /** 章节纯文本内容(兜底渲染) */
content: string;
+ /** 学科(@contract-pending,与教材学科一致) */
+ subject: string;
+ /** 年级(@contract-pending,与教材年级一致) */
+ grade: string;
+ /** 结构化分节内容(@contract-pending,优先于 content 渲染) */
+ structuredContent?: StudentTextbookChapterContent;
}
export interface StudentTextbookChapters {
@@ -538,6 +870,15 @@ export function useStudentLessonPlans(): UseQueryResult {
return { ...result, data: result.data?.studentLessonPlans?.items };
}
+/** 学科选项列表(@contract-pending,MSW 兜底;用于教案等列表的学科筛选器) */
+export function useSubjectOptions(): UseQueryResult {
+ const result = useWidgetQuery<
+ { subjectOptions: SubjectOption[] },
+ Record
+ >(GET_SUBJECT_OPTIONS_DOC, {});
+ return { ...result, data: result.data?.subjectOptions };
+}
+
/** 教案只读视图(@contract-pending,MSW 兜底) */
export function useStudentLessonPlanView(
planId: string,
@@ -593,6 +934,18 @@ export interface StudentErrorBookStats {
toReview: number;
}
+/** 错题难度枚举(@contract-pending,与 i18n difficulty* key 对齐) */
+export type StudentErrorBookDifficulty = "easy" | "medium" | "hard";
+
+/** 错题关联题目(@contract-pending,core-edu 接入后由后端返回) */
+export interface StudentErrorBookQuestion {
+ id: string;
+ content: string;
+ type: string;
+ options?: string[];
+ answer?: string;
+}
+
export interface StudentErrorBookItem {
id: string;
question: string;
@@ -601,6 +954,22 @@ export interface StudentErrorBookItem {
lastErrorAt: string;
status: string;
source: string;
+ /** 难度(@contract-pending) */
+ difficulty?: StudentErrorBookDifficulty;
+ /** 错误标签(@contract-pending) */
+ errorTags?: string[];
+ /** 学生笔记(@contract-pending) */
+ note?: string;
+ /** 掌握度等级 0-5(@contract-pending) */
+ masteryLevel?: number;
+ /** 复习次数(@contract-pending) */
+ reviewCount?: number;
+ /** 下次复习时间 ISO(@contract-pending,逾期则前端高亮) */
+ nextReviewAt?: string;
+ /** 创建时间 ISO(@contract-pending) */
+ createdAt?: string;
+ /** 关联题目完整信息(@contract-pending) */
+ questionDetail?: StudentErrorBookQuestion;
}
export interface StudentErrorBookV2 {
@@ -716,10 +1085,84 @@ export function useStudentPortalWeakness(): UseQueryResult<
return { ...result, data: result.data?.studentWeakness ?? undefined };
}
+// =====================================================================
+// diagnostic:学生自我诊断(概览 + 知识点掌握度 + 报告)
+// 注意:与 ./diagnostic.ts 的 StudentDiagnostic(教师端,按 studentId 查询,snake_case)
+// 是两个不同契约,此处为学生自我诊断视图(无参数,camelCase),重命名以避免冲突
+// =====================================================================
+
+export interface StudentDiagnosticSummary {
+ studentName: string;
+ overallMastery: number;
+ strengthCount: number;
+ weaknessCount: number;
+}
+
+export interface StudentMasteryPoint {
+ name: string;
+ masteryRate: number;
+ isStrength: boolean;
+}
+
+export interface StudentSelfDiagnosticReport {
+ id: string;
+ status: string;
+ confidence: number;
+ period: string;
+ score: number;
+ summary: string;
+ recommendations: string[];
+}
+
+export interface StudentSelfDiagnostic {
+ summary: StudentDiagnosticSummary;
+ masteryPoints: StudentMasteryPoint[];
+ reports: StudentSelfDiagnosticReport[];
+}
+
+/** 学生自我诊断(@contract-pending,MSW 兜底) */
+export function useStudentSelfDiagnostic(): UseQueryResult<
+ StudentSelfDiagnostic | undefined
+> {
+ const result = useWidgetQuery<
+ { studentDiagnostic: StudentSelfDiagnostic | null },
+ Record
+ >(GET_STUDENT_SELF_DIAGNOSTIC_DOC, {});
+ return { ...result, data: result.data?.studentDiagnostic ?? undefined };
+}
+
// =====================================================================
// practice / leave / messages / announcements
// =====================================================================
+/** 知识点难度等级 */
+export type KnowledgePointDifficulty = "easy" | "medium" | "hard";
+
+/** 学生学科枚举(与 i18n subject.* 对应) */
+export type StudentSubject =
+ "math" | "chinese" | "english" | "physics" | "chemistry";
+
+/** 知识点(练习选择用,@contract-pending,MSW 兜底) */
+export interface StudentKnowledgePoint {
+ id: string;
+ name: string;
+ subject: StudentSubject;
+ grade: string;
+ difficulty: KnowledgePointDifficulty;
+ description?: string;
+}
+
+/** 知识点列表(@contract-pending,MSW 兜底),可选 subject 过滤 */
+export function useStudentKnowledgePoints(
+ options: { subject?: StudentSubject } = {},
+): UseQueryResult {
+ const result = useWidgetQuery<
+ { studentKnowledgePoints: StudentKnowledgePoint[] },
+ { subject?: StudentSubject }
+ >(GET_STUDENT_KNOWLEDGE_POINTS_DOC, options);
+ return { ...result, data: result.data?.studentKnowledgePoints };
+}
+
export interface StudentPracticeStats {
totalSessions: number;
completedSessions: number;
@@ -765,6 +1208,20 @@ export interface StudentLeaveItem {
status: string;
}
+/** Student leave paginated result (@contract-pending, MSW fallback) */
+export interface StudentLeaveResult {
+ items: StudentLeaveItem[];
+ total: number;
+ page: number;
+ pageSize: number;
+}
+
+/** Student leave query options (@contract-pending, MSW fallback) */
+export interface StudentLeaveQueryOptions {
+ page?: number;
+ pageSize?: number;
+}
+
export interface LeaveRequestInput {
classId: string;
startDate: string;
@@ -828,7 +1285,7 @@ export function useStudentPracticeSession(
/** 开始练习会话(@contract-pending,MSW 兜底),返回 session id */
export function useStartPracticeSession(): {
- run: (knowledgePointId: string) => Promise;
+ run: (knowledgePointIds: string[]) => Promise;
loading: boolean;
error: unknown;
} {
@@ -838,10 +1295,10 @@ export function useStartPracticeSession(): {
error,
} = useWidgetMutation<
{ startPracticeSession: string },
- { knowledgePointId: string }
+ { knowledgePointIds: string[] }
>(START_PRACTICE_SESSION_DOC);
- const run = async (knowledgePointId: string): Promise => {
- const data = await rawRun({ knowledgePointId });
+ const run = async (knowledgePointIds: string[]): Promise => {
+ const data = await rawRun({ knowledgePointIds });
if (!data?.startPracticeSession) {
throw new ApiError("Failed to start practice session", "INTERNAL_ERROR");
}
@@ -882,13 +1339,18 @@ export function useSubmitPracticeAnswer(): {
return { run, loading, error };
}
-/** 学生请假记录(@contract-pending,MSW 兜底) */
-export function useStudentLeave(): UseQueryResult {
+/** 学生请假记录(@contract-pending,MSW 兜底,支持分页) */
+export function useStudentLeave(
+ options: StudentLeaveQueryOptions = {},
+): UseQueryResult {
const result = useWidgetQuery<
- { studentLeave: { items: StudentLeaveItem[] } },
- Record
- >(GET_STUDENT_LEAVE_DOC, {});
- return { ...result, data: result.data?.studentLeave?.items };
+ { studentLeave: StudentLeaveResult | null },
+ { page?: number; pageSize?: number }
+ >(GET_STUDENT_LEAVE_DOC, {
+ page: options.page,
+ pageSize: options.pageSize,
+ });
+ return { ...result, data: result.data?.studentLeave ?? undefined };
}
/** 提交请假申请(@contract-pending,MSW 兜底) */
@@ -955,6 +1417,99 @@ export function useStudentAnnouncementDetail(
}
// =====================================================================
+// =====================================================================
+// messages:消息详情 / 收件人 / 发送消息
+// =====================================================================
+
+/** 消息详情(扩展 StudentMessage,增加 body / from_id / to / to_id / is_starred) */
+export interface StudentMessageDetail {
+ id: string;
+ from: string;
+ from_id: string;
+ to: string;
+ to_id: string;
+ subject: string;
+ body: string;
+ date: string;
+ status: string;
+ is_starred: boolean;
+}
+
+/** 收件人列表项(撰写消息页选择收件人) */
+export interface MessageRecipient {
+ id: string;
+ name: string;
+ role: string;
+ avatar_url: string | null;
+}
+
+/** 发送消息结果 */
+export interface SendMessageResult {
+ id: string;
+ success: boolean;
+}
+
+/** 发送消息输入 */
+export interface SendMessageInput {
+ receiver_id: string;
+ subject: string;
+ body: string;
+ parent_message_id?: string;
+}
+
+/** 消息详情(@contract-pending,MSW 兜底) */
+export function useStudentMessageDetail(
+ id: string,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ { studentMessageDetail: StudentMessageDetail | null },
+ { id: string }
+ >(
+ GET_STUDENT_MESSAGE_DETAIL_DOC,
+ { id },
+ {
+ enabled: id.length > 0,
+ },
+ );
+ return {
+ ...result,
+ data: result.data?.studentMessageDetail ?? undefined,
+ };
+}
+
+/** 收件人列表(@contract-pending,MSW 兜底) */
+export function useMessageRecipients(): UseQueryResult {
+ const result = useWidgetQuery<
+ { messageRecipients: MessageRecipient[] },
+ Record
+ >(GET_MESSAGE_RECIPIENTS_DOC, {});
+ return { ...result, data: result.data?.messageRecipients };
+}
+
+/** 发送消息(@contract-pending,MSW 兜底) */
+export function useSendMessage(): {
+ run: (input: SendMessageInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { sendMessage: SendMessageResult },
+ { input: SendMessageInput }
+ >(SEND_STUDENT_MESSAGE_DOC);
+ const run = async (input: SendMessageInput): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.sendMessage?.success) {
+ throw new ApiError("Failed to send message", "INTERNAL_ERROR");
+ }
+ return data.sendMessage;
+ };
+ return { run, loading, error };
+}
+
// elective / ai-tutor hooks 已在 ./student.ts 中定义并导出:
// - useElectiveCourses(termId, options) → Course[]
// - useEnrollCourse() / useDropCourse()
diff --git a/apps/portal-shell/src/lib/api/student.ts b/apps/portal-shell/src/lib/api/student.ts
index 917864c..2470821 100644
--- a/apps/portal-shell/src/lib/api/student.ts
+++ b/apps/portal-shell/src/lib/api/student.ts
@@ -23,6 +23,7 @@ import {
GET_ELECTIVE_COURSES_DOC,
ENROLL_COURSE_DOC,
DROP_COURSE_DOC,
+ GET_STUDENT_SELECTIONS_DOC,
GET_AI_SESSIONS_DOC,
SEND_MESSAGE_DOC,
} from "./operations/student.graphql";
@@ -75,6 +76,10 @@ export interface Course {
schedule: string;
credits: number;
category: CourseCategory;
+ subjectName?: string;
+ description?: string;
+ selectionMode?: "fcfs" | "lottery";
+ status?: "open" | "closed" | "in_progress" | "completed";
}
/** AI 辅导会话 */
@@ -214,12 +219,30 @@ export function useEnrollCourse(): {
return { run, loading, error };
}
+/**
+ * 查询当前学生已选课程列表。
+ * 归一化:返回 Course[](从 studentSelections 字段提取)。
+ */
+export function useStudentSelections(
+ termId: string,
+ options: { enabled?: boolean } = {},
+): UseQueryResult {
+ const result = useWidgetQuery<
+ { studentSelections: Course[] },
+ { termId: string }
+ >(GET_STUDENT_SELECTIONS_DOC, { termId }, { enabled: options.enabled });
+ return {
+ ...result,
+ data: result.data?.studentSelections,
+ };
+}
+
/**
* 退课。
* run 抛 ApiError 表示退课失败。
*/
export function useDropCourse(): {
- run: (courseId: string) => Promise;
+ run: (courseId: string, reason?: string) => Promise;
loading: boolean;
error: unknown;
} {
@@ -227,12 +250,13 @@ export function useDropCourse(): {
run: rawRun,
loading,
error,
- } = useWidgetMutation<{ dropCourse: boolean }, { courseId: string }>(
- DROP_COURSE_DOC,
- );
+ } = useWidgetMutation<
+ { dropCourse: boolean },
+ { courseId: string; reason?: string }
+ >(DROP_COURSE_DOC);
- const run = async (courseId: string): Promise => {
- const data = await rawRun({ courseId });
+ const run = async (courseId: string, reason?: string): Promise => {
+ const data = await rawRun({ courseId, reason });
if (!data?.dropCourse) {
throw new ApiError("Failed to drop course", "INTERNAL_ERROR");
}
diff --git a/apps/portal-shell/src/messages/en.json b/apps/portal-shell/src/messages/en.json
index e75d522..f1c7a0a 100644
--- a/apps/portal-shell/src/messages/en.json
+++ b/apps/portal-shell/src/messages/en.json
@@ -5206,6 +5206,37 @@
"adminWarningTitle": "Administrator role",
"adminWarning": "Administrators have the highest system privileges. Assigning this role grants the user full management capabilities. Proceed with caution."
},
+ "detail": {
+ "title": "User detail",
+ "notFound": "User not found",
+ "sectionBasic": "Basic information",
+ "sectionRoles": "Role assignment",
+ "fieldName": "Name",
+ "fieldEmail": "Email",
+ "fieldPhone": "Phone",
+ "fieldRole": "Primary role",
+ "fieldRoles": "Roles",
+ "fieldStatus": "Status",
+ "fieldUserType": "User type",
+ "fieldCreatedAt": "Created at",
+ "fieldUpdatedAt": "Updated at",
+ "editButton": "Edit",
+ "assignRolesButton": "Assign roles",
+ "editDialogTitle": "Edit user",
+ "editDialogDescription": "Update user basic information. Changes take effect immediately after saving.",
+ "labelName": "Name",
+ "labelEmail": "Email",
+ "labelPhone": "Phone",
+ "labelRole": "Primary role",
+ "labelStatus": "Status",
+ "cancel": "Cancel",
+ "submitting": "Saving...",
+ "submit": "Save",
+ "success": "User updated",
+ "adminWarningTitle": "Administrator user",
+ "adminWarning": "This user has administrator privileges and can operate all system features. Edit with caution.",
+ "mswNotice": "User detail query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ },
"error": {
"title": "User module error",
"unknown": "Unknown error in user module",
@@ -5347,7 +5378,8 @@
"countLabel": "{count} items",
"emptyTitle": "No permission points",
"emptyDescription": "Permission catalog will be displayed after syncing with IAM service",
- "mswNotice": "Permission catalog query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Permission catalog query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "total": "{count} total"
},
"error": {
"title": "Permission module error",
@@ -5558,7 +5590,9 @@
"saveSuccess": "Retention config saved",
"purgeConfirm": "Purge expired logs? Cannot undo.",
"purgeSuccess": "Purged: {auditLogsDeleted} audit, {loginLogsDeleted} login, {dataChangeLogsDeleted} data change logs",
- "loadFailed": "Failed to load retention config"
+ "loadFailed": "Failed to load retention config",
+ "purgeConfirmTitle": "Confirm Purge",
+ "purgeConfirmAction": "Purge Now"
},
"error": {
"title": "Audit log module error",
@@ -6393,7 +6427,20 @@
"saveError": "Failed to save system settings",
"resetConfirm": "Are you sure you want to reset all unsaved changes?",
"emptyTitle": "No configurable items",
- "mswNotice": "System settings query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "System settings query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "language": {
+ "zh-CN": "Simplified Chinese",
+ "en": "English"
+ },
+ "storageProviderOptions": {
+ "s3": "AWS S3",
+ "oss": "Aliyun OSS",
+ "cos": "Tencent COS",
+ "minio": "MinIO"
+ },
+ "storageProviderNone": "--",
+ "resetConfirmTitle": "Confirm Reset",
+ "resetConfirmAction": "Confirm Reset"
},
"error": {
"title": "System settings module error",
@@ -6434,7 +6481,9 @@
"editIsActive": "Enable",
"saveSuccess": "Viewport configuration saved",
"saveError": "Failed to save viewport configuration",
- "mswNotice": "Viewport configuration query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Viewport configuration query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "roleFilterPlaceholder": "Role filter (e.g. teacher / student)",
+ "roleFilterAriaLabel": "Role filter"
},
"error": {
"title": "Viewport configuration module error",
@@ -6482,7 +6531,9 @@
"emptyTitle": "No plugins",
"emptyDescription": "Plugin registry is empty, please ensure config-service is ready",
"emptyAction": "Refresh",
- "mswNotice": "Plugin registry is provided by config-service contract."
+ "mswNotice": "Plugin registry is provided by config-service contract.",
+ "categoryFilterPlaceholder": "Category filter (e.g. widget / chart)",
+ "categoryFilterAriaLabel": "Category filter"
},
"error": {
"title": "Plugin management module error",
@@ -6512,6 +6563,19 @@
"title": "Student management module error",
"unknown": "Unknown error in student management module",
"retry": "Retry"
+ },
+ "detail": {
+ "title": "Student Details",
+ "description": "View the student's basic and academic information",
+ "sectionBasic": "Basic Information",
+ "sectionAcademic": "Academic Information",
+ "fieldName": "Name",
+ "fieldStudentNo": "Student No.",
+ "fieldStatus": "Status",
+ "fieldGrade": "Grade",
+ "fieldClass": "Class",
+ "fieldEnrolledAt": "Enrolled At",
+ "fieldId": "ID"
}
},
"teachers": {
@@ -6537,6 +6601,21 @@
"title": "Teacher management module error",
"unknown": "Unknown error in teacher management module",
"retry": "Retry"
+ },
+ "detail": {
+ "title": "Teacher Details",
+ "description": "View the teacher's basic and teaching information",
+ "sectionBasic": "Basic Information",
+ "sectionTeaching": "Teaching Information",
+ "fieldName": "Name",
+ "fieldStaffNo": "Staff No.",
+ "fieldEmail": "Email",
+ "fieldStatus": "Status",
+ "fieldDepartment": "Department",
+ "fieldTitle": "Title",
+ "fieldClassCount": "Class Count",
+ "fieldSubjectCount": "Subject Count",
+ "fieldId": "ID"
}
},
"organization": {
@@ -6558,6 +6637,18 @@
"title": "Organization management module error",
"unknown": "Unknown error in organization management module",
"retry": "Retry"
+ },
+ "detail": {
+ "title": "Organization Node Details",
+ "description": "View the organization node's basic and structural information",
+ "sectionBasic": "Basic Information",
+ "sectionStructure": "Structural Information",
+ "fieldName": "Name",
+ "fieldType": "Type",
+ "fieldId": "ID",
+ "fieldParentId": "Parent Node ID",
+ "fieldMemberCount": "Member Count",
+ "fieldChildrenCount": "Children Count"
}
},
"coursePlans": {
@@ -7024,6 +7115,24 @@
"title": "Lesson plan module error",
"unknown": "Unknown error in lesson plan module",
"retry": "Retry"
+ },
+ "export": {
+ "button": "Export CSV",
+ "filename": "lesson-plans",
+ "colTitle": "Title",
+ "colSubject": "Subject",
+ "colGrade": "Grade",
+ "colClass": "Class",
+ "colStatus": "Status",
+ "colTeacher": "Teacher",
+ "colCreatedAt": "Created at",
+ "statusDraft": "Draft",
+ "statusPublished": "Published",
+ "statusArchived": "Archived",
+ "statusSubmitted": "Submitted",
+ "success": "Export succeeded",
+ "error": "Export failed: {message}",
+ "errorEmpty": "No data to export"
}
},
"errorBook": {
@@ -7129,7 +7238,10 @@
"toggle": "Enable/Disable",
"save": "Save",
"emptyTitle": "No rules",
- "mswNotice": "Scheduling rule query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Scheduling rule query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "resetConfirm": "Are you sure you want to reset to the last saved rule state?",
+ "resetConfirmTitle": "Confirm Reset",
+ "resetConfirmAction": "Confirm Reset"
},
"error": {
"title": "Scheduling module error",
@@ -7235,6 +7347,23 @@
"title": "Attendance management module error",
"unknown": "Unknown error in attendance management module",
"retry": "Retry"
+ },
+ "export": {
+ "button": "Export CSV",
+ "filename": "attendance-records",
+ "colStudent": "Student",
+ "colClass": "Class",
+ "colDate": "Date",
+ "colStatus": "Status",
+ "colNote": "Note",
+ "colRecorder": "Recorder",
+ "statusPresent": "Present",
+ "statusAbsent": "Absent",
+ "statusLate": "Late",
+ "statusLeave": "Leave",
+ "success": "Export succeeded",
+ "error": "Export failed: {message}",
+ "errorEmpty": "No data to export"
}
},
"elective": {
diff --git a/apps/portal-shell/src/messages/zh-CN.json b/apps/portal-shell/src/messages/zh-CN.json
index 9d10ad3..aa28f3d 100644
--- a/apps/portal-shell/src/messages/zh-CN.json
+++ b/apps/portal-shell/src/messages/zh-CN.json
@@ -5166,6 +5166,37 @@
"adminWarningTitle": "管理员角色",
"adminWarning": "管理员拥有系统最高权限,分配后该用户将获得全部管理能力,请谨慎操作。"
},
+ "detail": {
+ "title": "用户详情",
+ "notFound": "未找到该用户",
+ "sectionBasic": "基本信息",
+ "sectionRoles": "角色分配",
+ "fieldName": "姓名",
+ "fieldEmail": "邮箱",
+ "fieldPhone": "手机号",
+ "fieldRole": "主角色",
+ "fieldRoles": "角色列表",
+ "fieldStatus": "状态",
+ "fieldUserType": "用户类型",
+ "fieldCreatedAt": "创建时间",
+ "fieldUpdatedAt": "更新时间",
+ "editButton": "编辑",
+ "assignRolesButton": "分配角色",
+ "editDialogTitle": "编辑用户",
+ "editDialogDescription": "修改用户基本信息,保存后立即生效。",
+ "labelName": "姓名",
+ "labelEmail": "邮箱",
+ "labelPhone": "手机号",
+ "labelRole": "主角色",
+ "labelStatus": "状态",
+ "cancel": "取消",
+ "submitting": "保存中…",
+ "submit": "保存",
+ "success": "用户信息已更新",
+ "adminWarningTitle": "管理员用户",
+ "adminWarning": "该用户拥有管理员权限,可操作系统全部功能,请谨慎修改。",
+ "mswNotice": "用户详情查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ },
"error": {
"title": "用户模块出错了",
"unknown": "用户模块发生未知错误",
@@ -5307,7 +5338,8 @@
"countLabel": "共 {count} 项",
"emptyTitle": "暂无权限点",
"emptyDescription": "权限目录将由 IAM 服务同步后展示",
- "mswNotice": "权限目录查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "权限目录查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "total": "共 {count} 条"
},
"error": {
"title": "权限模块出错了",
@@ -5518,7 +5550,9 @@
"saveSuccess": "保留期配置已保存",
"purgeConfirm": "确定要立即清理过期日志吗?此操作不可撤销。",
"purgeSuccess": "已清理:审计日志 {auditLogsDeleted} 条,登录日志 {loginLogsDeleted} 条,数据变更日志 {dataChangeLogsDeleted} 条",
- "loadFailed": "加载保留期配置失败"
+ "loadFailed": "加载保留期配置失败",
+ "purgeConfirmTitle": "确认清理",
+ "purgeConfirmAction": "立即清理"
},
"error": {
"title": "审计日志模块出错了",
@@ -6386,7 +6420,20 @@
"saveError": "保存系统设置失败",
"resetConfirm": "确认重置所有未保存的修改?",
"emptyTitle": "暂无可配置项",
- "mswNotice": "系统设置查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "系统设置查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "language": {
+ "zh-CN": "简体中文",
+ "en": "English"
+ },
+ "storageProviderOptions": {
+ "s3": "AWS S3",
+ "oss": "Aliyun OSS",
+ "cos": "Tencent COS",
+ "minio": "MinIO"
+ },
+ "storageProviderNone": "--",
+ "resetConfirmTitle": "确认重置",
+ "resetConfirmAction": "确认重置"
},
"error": {
"title": "系统设置模块出错了",
@@ -6427,7 +6474,9 @@
"editIsActive": "启用",
"saveSuccess": "视口配置已保存",
"saveError": "保存视口配置失败",
- "mswNotice": "视口配置查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "视口配置查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "roleFilterPlaceholder": "按角色筛选(如 teacher / student)",
+ "roleFilterAriaLabel": "角色筛选"
},
"error": {
"title": "视口配置模块出错了",
@@ -6475,7 +6524,9 @@
"emptyTitle": "暂无插件",
"emptyDescription": "插件注册表为空,请确认 config-service 已就绪",
"emptyAction": "刷新",
- "mswNotice": "插件注册表通过 config-service 提供契约。"
+ "mswNotice": "插件注册表通过 config-service 提供契约。",
+ "categoryFilterPlaceholder": "按分类筛选(如 widget / chart)",
+ "categoryFilterAriaLabel": "分类筛选"
},
"error": {
"title": "插件管理模块出错了",
@@ -6514,6 +6565,19 @@
"title": "学生管理模块出错了",
"unknown": "学生管理模块发生未知错误",
"retry": "重试"
+ },
+ "detail": {
+ "title": "学生详情",
+ "description": "查看学生的基本信息与学籍信息",
+ "sectionBasic": "基本信息",
+ "sectionAcademic": "学籍信息",
+ "fieldName": "姓名",
+ "fieldStudentNo": "学号",
+ "fieldStatus": "状态",
+ "fieldGrade": "年级",
+ "fieldClass": "班级",
+ "fieldEnrolledAt": "入学日期",
+ "fieldId": "ID"
}
},
"teachers": {
@@ -6550,6 +6614,21 @@
"title": "教师管理模块出错了",
"unknown": "教师管理模块发生未知错误",
"retry": "重试"
+ },
+ "detail": {
+ "title": "教师详情",
+ "description": "查看教师的基本信息与教学信息",
+ "sectionBasic": "基本信息",
+ "sectionTeaching": "教学信息",
+ "fieldName": "姓名",
+ "fieldStaffNo": "工号",
+ "fieldEmail": "邮箱",
+ "fieldStatus": "状态",
+ "fieldDepartment": "部门",
+ "fieldTitle": "职称",
+ "fieldClassCount": "班级数",
+ "fieldSubjectCount": "科目数",
+ "fieldId": "ID"
}
},
"organization": {
@@ -6582,6 +6661,18 @@
"title": "组织管理模块出错了",
"unknown": "组织管理模块发生未知错误",
"retry": "重试"
+ },
+ "detail": {
+ "title": "组织节点详情",
+ "description": "查看组织节点的基本信息与结构信息",
+ "sectionBasic": "基本信息",
+ "sectionStructure": "结构信息",
+ "fieldName": "名称",
+ "fieldType": "类型",
+ "fieldId": "ID",
+ "fieldParentId": "上级节点 ID",
+ "fieldMemberCount": "成员数",
+ "fieldChildrenCount": "子节点数"
}
},
"coursePlans": {
@@ -7269,6 +7360,24 @@
"title": "教案模块出错了",
"unknown": "教案模块发生未知错误",
"retry": "重试"
+ },
+ "export": {
+ "button": "导出 CSV",
+ "filename": "教案",
+ "colTitle": "标题",
+ "colSubject": "学科",
+ "colGrade": "年级",
+ "colClass": "班级",
+ "colStatus": "状态",
+ "colTeacher": "教师",
+ "colCreatedAt": "创建时间",
+ "statusDraft": "草稿",
+ "statusPublished": "已发布",
+ "statusArchived": "已归档",
+ "statusSubmitted": "已提交",
+ "success": "导出成功",
+ "error": "导出失败:{message}",
+ "errorEmpty": "没有可导出的数据"
}
},
"errorBook": {
@@ -7397,7 +7506,9 @@
"saveSuccess": "排课规则已保存",
"saveError": "保存排课规则失败",
"resetConfirm": "确认重置为最近一次保存的规则状态吗?",
- "mswNotice": "排课规则查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "排课规则查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "resetConfirmTitle": "确认重置",
+ "resetConfirmAction": "确认重置"
},
"error": {
"title": "排课模块出错了",
@@ -7502,6 +7613,23 @@
"title": "考勤管理模块出错了",
"unknown": "考勤管理模块发生未知错误",
"retry": "重试"
+ },
+ "export": {
+ "button": "导出 CSV",
+ "filename": "考勤记录",
+ "colStudent": "学生姓名",
+ "colClass": "班级",
+ "colDate": "日期",
+ "colStatus": "状态",
+ "colNote": "请假原因",
+ "colRecorder": "记录人",
+ "statusPresent": "出勤",
+ "statusAbsent": "缺勤",
+ "statusLate": "迟到",
+ "statusLeave": "请假",
+ "success": "导出成功",
+ "error": "导出失败:{message}",
+ "errorEmpty": "没有可导出的数据"
}
}
}
diff --git a/apps/portal-shell/src/mocks/graphql-data.ts b/apps/portal-shell/src/mocks/graphql-data.ts
index 40aea20..b3f1f67 100644
--- a/apps/portal-shell/src/mocks/graphql-data.ts
+++ b/apps/portal-shell/src/mocks/graphql-data.ts
@@ -5611,6 +5611,39 @@ export function graphqlResponse(
// ── Users 域 ──
case "GetUsers":
return { data: { users: mockUsers } };
+ // GetUser($id):按 id 单查用户详情(dev 兜底,命中 mockUsers 则返回真实条目,否则兜底)
+ case "GetUser": {
+ const userId = (variables?.id as string | undefined) ?? "";
+ const found = mockUsers.items.find((u) => u.id === userId);
+ return {
+ data: {
+ user: found ?? mockUsers.items[0] ?? null,
+ },
+ };
+ }
+ // UpdateUser($id, $input):mutation 兜底,合并 input 后返回
+ case "UpdateUser": {
+ const userId = (variables?.id as string | undefined) ?? "usr-001";
+ const input = (variables?.input as Record) ?? {};
+ const base =
+ mockUsers.items.find((u) => u.id === userId) ?? mockUsers.items[0];
+ return {
+ data: {
+ updateUser: {
+ id: userId,
+ name: (input.name as string) ?? base?.name ?? "",
+ email: (input.email as string) ?? base?.email ?? "",
+ role: (input.role as string) ?? base?.role ?? "teacher",
+ roles: base?.roles ?? [(input.role as string) ?? "teacher"],
+ phone: (input.phone as string) ?? base?.phone ?? "",
+ status: (input.status as string) ?? base?.status ?? "active",
+ userType: "internal",
+ createdAt: base?.createdAt ?? "2026-06-01T00:00:00Z",
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ };
+ }
case "UpdateUserStatus":
return {
data: { updateUserStatus: { id: "usr-005", status: "active" } },
@@ -5798,12 +5831,12 @@ export function graphqlResponse(
}
// UpdateQuestion($id, $input):mutation 兜底
case "UpdateQuestion": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateQuestion: { id } } };
}
// DeleteQuestion($id):mutation 兜底
case "DeleteQuestion": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteQuestion: { id } } };
}
@@ -5867,7 +5900,7 @@ export function graphqlResponse(
}
// UpdateTextbook($id, $input):mutation 兜底
case "UpdateTextbook": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateTextbook: { id } } };
}
// ── Legacy widget 兜底(P2 迁移后重命名,保留旧 widget 契约)──
@@ -6009,7 +6042,7 @@ export function graphqlResponse(
classId: (input.classId as string) ?? "cls-001",
subjectId: (input.subjectId as string) ?? "sub-math",
title: (input.title as string) ?? "未命名考试",
- description: (input.description as string | null) ?? null,
+ description: (input.description as string | null) ?? "",
examDate: (input.examDate as string) ?? new Date().toISOString(),
duration: (input.duration as number) ?? 120,
totalScore: String((input.totalScore as number) ?? 100),
@@ -6134,7 +6167,7 @@ export function graphqlResponse(
classId: (input.classId as string) ?? "cls-001",
subjectId: (input.subjectId as string) ?? "sub-math",
title: (input.title as string) ?? "未命名作业",
- description: (input.description as string | null) ?? null,
+ description: (input.description as string | null) ?? "",
dueDate: (input.dueDate as string) ?? new Date().toISOString(),
gracePeriod: (input.gracePeriod as number) ?? 24,
status: "DRAFT",
@@ -7409,7 +7442,7 @@ export function graphqlResponse(
}
// MarkNotificationRead($id):标记单条已读 mutation 兜底
case "MarkNotificationRead": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
const target = mockNotifications.find((n) => n.id === id);
if (target) target.isRead = true;
return {
@@ -7529,17 +7562,17 @@ export function graphqlResponse(
return { data: { createRole: { id: "role-new", name: "newrole" } } };
// UpdateRole($id, $input):mutation 兜底
case "UpdateRole": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateRole: { id, name: "updated" } } };
}
// DeleteRole($id):mutation 兜底
case "DeleteRole": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteRole: { id } } };
}
// ToggleRoleEnabled($id, $isEnabled):mutation 兜底
case "ToggleRoleEnabled": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
const isEnabled = (variables?.isEnabled as boolean) ?? true;
return { data: { toggleRoleEnabled: { id, isEnabled } } };
}
@@ -7588,6 +7621,128 @@ export function graphqlResponse(
},
};
+ // GetRolePermissions:RBAC 权限矩阵(按模块分组的 CRUD 权限点)
+ // @contract-pending MSW 兜底,返回 roleId 对应角色的权限矩阵
+ case "GetRolePermissions": {
+ const roleId = (variables?.roleId as string | undefined) ?? "role-001";
+ const isFull = roleId === "role-001";
+ return {
+ data: {
+ rolePermissions: [
+ {
+ permissionId: "perm-001",
+ label: "用户管理",
+ module: "user",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-002",
+ label: "角色管理",
+ module: "role",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-003",
+ label: "权限目录",
+ module: "permission",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-004",
+ label: "班级管理",
+ module: "class",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-005",
+ label: "课程计划",
+ module: "course-plan",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-006",
+ label: "考试管理",
+ module: "exam",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-007",
+ label: "作业管理",
+ module: "homework",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-008",
+ label: "公告管理",
+ module: "announcement",
+ actions: {
+ read: true,
+ create: isFull,
+ update: isFull,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-009",
+ label: "审计日志",
+ module: "audit-log",
+ actions: {
+ read: isFull,
+ create: false,
+ update: false,
+ delete: isFull,
+ },
+ },
+ {
+ permissionId: "perm-010",
+ label: "系统设置",
+ module: "system",
+ actions: {
+ read: isFull,
+ create: isFull,
+ update: isFull,
+ delete: false,
+ },
+ },
+ ],
+ },
+ };
+ }
// GetAuditOverviewStats:审计总览统计
case "GetAuditOverviewStats":
return {
@@ -7734,6 +7889,194 @@ export function graphqlResponse(
},
};
+ // GetAuditLogs:审计日志列表(@contract-pending MSW 兜底)
+ // 支持 filter(userId/action/resource)+ limit/offset 分页
+ case "GetAuditLogs": {
+ const filter =
+ (variables?.filter as {
+ userId?: string | null;
+ action?: string | null;
+ resource?: string | null;
+ }) ?? {};
+ const limit = (variables?.limit as number | undefined) ?? 20;
+ const offset = (variables?.offset as number | undefined) ?? 0;
+ const allItems = [
+ {
+ id: "al-001",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "login",
+ resource: "auth",
+ resourceId: "usr-001",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-30T08:30:00Z",
+ details: "登录成功",
+ },
+ {
+ id: "al-002",
+ userId: "usr-002",
+ userName: "李老师",
+ action: "create",
+ resource: "class",
+ resourceId: "cls-001",
+ ip: "192.168.1.20",
+ timestamp: "2026-07-30T09:15:00Z",
+ details: "创建班级",
+ },
+ {
+ id: "al-003",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "update",
+ resource: "user",
+ resourceId: "usr-003",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-30T10:00:00Z",
+ details: "修改用户角色",
+ },
+ {
+ id: "al-004",
+ userId: "usr-003",
+ userName: "管理员",
+ action: "delete",
+ resource: "role",
+ resourceId: "role-005",
+ ip: "192.168.1.30",
+ timestamp: "2026-07-30T11:30:00Z",
+ details: "删除自定义角色",
+ },
+ {
+ id: "al-005",
+ userId: "usr-002",
+ userName: "李老师",
+ action: "read",
+ resource: "student",
+ resourceId: "stu-001",
+ ip: "192.168.1.20",
+ timestamp: "2026-07-30T14:00:00Z",
+ details: "查看学生信息",
+ },
+ {
+ id: "al-006",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "export",
+ resource: "grade",
+ resourceId: "cls-001",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-30T15:45:00Z",
+ details: "导出成绩",
+ },
+ {
+ id: "al-007",
+ userId: "usr-003",
+ userName: "管理员",
+ action: "create",
+ resource: "announcement",
+ resourceId: "ann-001",
+ ip: "192.168.1.30",
+ timestamp: "2026-07-29T09:00:00Z",
+ details: "发布公告",
+ },
+ {
+ id: "al-008",
+ userId: "usr-002",
+ userName: "李老师",
+ action: "update",
+ resource: "course-plan",
+ resourceId: "cp-001",
+ ip: "192.168.1.20",
+ timestamp: "2026-07-29T10:30:00Z",
+ details: "更新课程计划",
+ },
+ {
+ id: "al-009",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "login",
+ resource: "auth",
+ resourceId: "usr-001",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-29T08:00:00Z",
+ details: "登录成功",
+ },
+ {
+ id: "al-010",
+ userId: "usr-003",
+ userName: "管理员",
+ action: "update",
+ resource: "system",
+ resourceId: "sys-001",
+ ip: "192.168.1.30",
+ timestamp: "2026-07-29T16:00:00Z",
+ details: "修改系统设置",
+ },
+ {
+ id: "al-011",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "create",
+ resource: "exam",
+ resourceId: "exam-001",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-28T09:30:00Z",
+ details: "创建考试",
+ },
+ {
+ id: "al-012",
+ userId: "usr-002",
+ userName: "李老师",
+ action: "update",
+ resource: "homework",
+ resourceId: "hw-001",
+ ip: "192.168.1.20",
+ timestamp: "2026-07-28T11:00:00Z",
+ details: "更新作业",
+ },
+ {
+ id: "al-013",
+ userId: "usr-003",
+ userName: "管理员",
+ action: "delete",
+ resource: "user",
+ resourceId: "usr-005",
+ ip: "192.168.1.30",
+ timestamp: "2026-07-28T14:15:00Z",
+ details: "删除用户",
+ },
+ {
+ id: "al-014",
+ userId: "usr-001",
+ userName: "张老师",
+ action: "read",
+ resource: "audit-log",
+ resourceId: "al-001",
+ ip: "192.168.1.10",
+ timestamp: "2026-07-28T16:30:00Z",
+ details: "查看审计日志",
+ },
+ {
+ id: "al-015",
+ userId: "usr-002",
+ userName: "李老师",
+ action: "import",
+ resource: "student",
+ resourceId: "cls-002",
+ ip: "192.168.1.20",
+ timestamp: "2026-07-27T09:00:00Z",
+ details: "批量导入学生",
+ },
+ ];
+ const filtered = allItems.filter((item) => {
+ if (filter.userId && item.userId !== filter.userId) return false;
+ if (filter.action && item.action !== filter.action) return false;
+ if (filter.resource && item.resource !== filter.resource) return false;
+ return true;
+ });
+ const total = filtered.length;
+ const items = filtered.slice(offset, offset + limit);
+ return { data: { auditLogs: { items, total } } };
+ }
// ── 课程计划周计划项 CRUD(@contract-pending,MSW 兜底)──
// CreateCoursePlanItem:创建周计划项
@@ -7776,7 +8119,7 @@ export function graphqlResponse(
// UpdateCoursePlanItem:更新周计划项
case "UpdateCoursePlanItem": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
const input = (variables?.input ?? {}) as {
week?: number;
topic?: string;
@@ -7833,7 +8176,7 @@ export function graphqlResponse(
// DeleteCoursePlanItem:删除周计划项
case "DeleteCoursePlanItem": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
for (const planId of Object.keys(mockAdminCoursePlanItems)) {
const arr = mockAdminCoursePlanItems[planId];
if (!arr) continue;
@@ -7848,7 +8191,7 @@ export function graphqlResponse(
// ToggleCoursePlanItemCompleted:切换周计划项完成状态
case "ToggleCoursePlanItemCompleted": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
const isCompleted = (variables?.isCompleted as boolean) ?? false;
const now = new Date().toISOString();
for (const planId of Object.keys(mockAdminCoursePlanItems)) {
@@ -7949,12 +8292,12 @@ export function graphqlResponse(
return { data: { createSchool: { id: "sch-new", name: "新学校" } } };
// AdminUpdateSchool($id, $input):mutation 兜底(admin-p5 useAdminUpdateSchool)
case "AdminUpdateSchool": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateSchool: { id, name: "updated" } } };
}
// DeleteSchool($id):mutation 兜底
case "DeleteSchool": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteSchool: { id } } };
}
@@ -7968,12 +8311,12 @@ export function graphqlResponse(
};
// UpdateDepartment($id, $input):mutation 兜底
case "UpdateDepartment": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateDepartment: { id, name: "updated" } } };
}
// DeleteDepartment($id):mutation 兜底
case "DeleteDepartment": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteDepartment: { id } } };
}
@@ -7987,12 +8330,12 @@ export function graphqlResponse(
};
// UpdateAcademicYear($id, $input):mutation 兜底
case "UpdateAcademicYear": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateAcademicYear: { id, name: "updated" } } };
}
// DeleteAcademicYear($id):mutation 兜底
case "DeleteAcademicYear": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteAcademicYear: { id } } };
}
@@ -8036,12 +8379,12 @@ export function graphqlResponse(
return { data: { createGrade: { id: "grade-new", name: "新年级" } } };
// UpdateGrade($id, $input):mutation 兜底
case "UpdateGrade": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateGrade: { id, name: "updated" } } };
}
// DeleteGrade($id):mutation 兜底
case "DeleteGrade": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteGrade: { id } } };
}
@@ -8155,12 +8498,12 @@ export function graphqlResponse(
return { data: { createAdminClass: { id: "cls-new", name: "新班级" } } };
// UpdateAdminClass($id, $input):mutation 兜底(admin-p5 useUpdateAdminClass)
case "UpdateAdminClass": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateAdminClass: { id, name: "updated" } } };
}
// DeleteAdminClass($id):mutation 兜底(admin-p5 useDeleteAdminClass)
case "DeleteAdminClass": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteAdminClass: { id } } };
}
@@ -8219,29 +8562,29 @@ export function graphqlResponse(
};
// UpdateAnnouncement($id, $input):mutation 兜底
case "UpdateAnnouncement": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateAnnouncement: { id, title: "updated" } } };
}
// DeleteAnnouncement($id):mutation 兜底
case "DeleteAnnouncement": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteAnnouncement: { id } } };
}
// ArchiveAnnouncement($id):归档公告 mutation 兜底
case "ArchiveAnnouncement": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { archiveAnnouncement: { id, status: "archived" } } };
}
// PinAnnouncement($id):置顶公告 mutation 兜底
case "PinAnnouncement": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return {
data: { pinAnnouncement: { id, pinnedAt: "2026-07-22T10:00:00Z" } },
};
}
// PublishAnnouncement($id): 发布公告 mutation 兜底
case "PublishAnnouncement": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return {
data: {
publishAnnouncement: {
@@ -8284,7 +8627,7 @@ export function graphqlResponse(
};
// RevokeInvitationCode($id): mutation 兜底
case "RevokeInvitationCode": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { revokeInvitationCode: { id, status: "revoked" } } };
}
// GenerateInvitationCodes($input): 批量生成邀请码 mutation 兜底
@@ -8343,17 +8686,16 @@ export function graphqlResponse(
};
// UpdateAiProvider($id, $input):mutation 兜底
case "UpdateAiProvider": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateAiProvider: { id, name: "updated" } } };
}
// DeleteAiProvider($id):mutation 兜底
case "DeleteAiProvider": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { deleteAiProvider: { id } } };
}
// TestAiProvider($id): 测试 AI Provider 连通性 mutation 兜底
case "TestAiProvider": {
- const _id = (variables?.id as string) ?? "";
return {
data: {
testAiProvider: {
@@ -8416,7 +8758,7 @@ export function graphqlResponse(
return { data: { viewports: mockViewports } };
// UpdateViewport($id, $input):mutation 兜底
case "UpdateViewport": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { updateViewport: { id, name: "updated" } } };
}
@@ -9115,12 +9457,12 @@ export function graphqlResponse(
}
// ApproveScheduleChange($id):审批通过 mutation 兜底
case "ApproveScheduleChange": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { approveScheduleChange: { id, status: "approved" } } };
}
// RejectScheduleChange($id):审批驳回 mutation 兜底
case "RejectScheduleChange": {
- const _id = (variables?.id as string) ?? "";
+ const id = (variables?.id as string) ?? "";
return { data: { rejectScheduleChange: { id, status: "rejected" } } };
}
// AutoSchedule($classId):自动排课 mutation 兜底
diff --git a/apps/portal-shell/src/shared/components/ui/skeleton.tsx b/apps/portal-shell/src/shared/components/ui/skeleton.tsx
index bc82fa6..6079b29 100644
--- a/apps/portal-shell/src/shared/components/ui/skeleton.tsx
+++ b/apps/portal-shell/src/shared/components/ui/skeleton.tsx
@@ -15,4 +15,31 @@ function Skeleton({
);
}
-export { Skeleton };
+function SkeletonCard({
+ className,
+ variant = "default",
+ ...props
+}: React.HTMLAttributes & {
+ variant?: "default" | "chart" | "text" | "list";
+}): React.ReactNode {
+ const variantClasses: Record = {
+ default: "h-32",
+ chart: "h-64",
+ text: "h-4",
+ list: "h-48",
+ };
+ return (
+
+
+
+ );
+}
+
+export { Skeleton, SkeletonCard };
diff --git a/apps/portal-shell/src/shared/lib/route-permissions.ts b/apps/portal-shell/src/shared/lib/route-permissions.ts
index 92636f7..5d240f0 100644
--- a/apps/portal-shell/src/shared/lib/route-permissions.ts
+++ b/apps/portal-shell/src/shared/lib/route-permissions.ts
@@ -126,6 +126,10 @@ export const EXACT_ROUTE_PERMISSIONS: Record = {
requiredRoles: ["admin"],
anyOfPermissions: ["GRADE_READ", "GRADE_MANAGE", "SCHOOL_MANAGE"],
},
+ "/shell/admin/school/grades/insights": {
+ requiredRoles: ["admin"],
+ anyOfPermissions: ["GRADE_READ", "GRADE_MANAGE", "SCHOOL_MANAGE"],
+ },
"/shell/admin/plugins": {
requiredRoles: ["admin"],
requiredPermissions: ["PLUGIN_REGISTRY_MANAGE"],
@@ -253,10 +257,28 @@ export const EXACT_ROUTE_PERMISSIONS: Record = {
requiredRoles: ["teacher", "admin"],
anyOfPermissions: ["EXAM_READ", "EXAM_CREATE", "EXAM_UPDATE", "EXAM_GRADE"],
},
+ // 教师域补齐:考试子路由 EXACT 登记,收紧至单一操作权限点
+ "/shell/teacher/exams/all": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["EXAM_READ"],
+ },
+ "/shell/teacher/exams/create": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["EXAM_CREATE"],
+ },
+ "/shell/teacher/exams/grading": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["EXAM_GRADE"],
+ },
"/shell/teacher/homework": {
requiredRoles: ["teacher", "admin"],
anyOfPermissions: ["HOMEWORK_READ", "HOMEWORK_CREATE", "HOMEWORK_GRADE"],
},
+ // 教师域补齐:作业布置子路由 EXACT 登记,收紧至 HOMEWORK_CREATE
+ "/shell/teacher/homework/assignments/create": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["HOMEWORK_CREATE"],
+ },
"/shell/teacher/grades": {
requiredRoles: ["teacher", "admin"],
anyOfPermissions: ["GRADE_RECORD_MANAGE", "GRADE_RECORD_READ"],
@@ -271,6 +293,15 @@ export const EXACT_ROUTE_PERMISSIONS: Record = {
requiredRoles: ["teacher", "admin"],
anyOfPermissions: ["CLASS_READ", "CLASS_MANAGE"],
},
+ // 教师域补齐:班级子路由 EXACT 登记,收紧至 CLASS_READ
+ "/shell/teacher/classes/my": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["CLASS_READ"],
+ },
+ "/shell/teacher/classes/students": {
+ requiredRoles: ["teacher", "admin"],
+ requiredPermissions: ["CLASS_READ"],
+ },
// P2 迁移(B2 教师域):学生管理列表页根路由(无尾斜杠)
// 无 STUDENT_* 权限点,复用 CLASS_READ/CLASS_MANAGE(学生隶属班级域)
"/shell/teacher/students": {
@@ -448,6 +479,10 @@ export const EXACT_ROUTE_PERMISSIONS: Record = {
"/shell/settings": {
requiredRoles: ["teacher", "student", "parent", "admin"],
},
+ // B1 共享页:个人资料(所有角色可访问,仅需登录身份)
+ "/shell/profile": {
+ requiredRoles: ["teacher", "student", "parent", "admin"],
+ },
};
/**
diff --git a/apps/portal-shell/src/shared/lib/utils.ts b/apps/portal-shell/src/shared/lib/utils.ts
index 3f58ba5..dea73e1 100644
--- a/apps/portal-shell/src/shared/lib/utils.ts
+++ b/apps/portal-shell/src/shared/lib/utils.ts
@@ -31,6 +31,23 @@ export function formatNumber(v: number | null | undefined, digits = 1): string {
return v.toFixed(digits);
}
+/**
+ * 格式化 ISO 日期字符串为本地化展示(zh-CN,含年月日时分)。
+ * 输入无效(null/undefined/非法日期)返回占位符 "--"。
+ */
+export function formatDueDate(isoDate: string | null | undefined): string {
+ if (!isoDate) return "--";
+ const d = new Date(isoDate);
+ if (Number.isNaN(d.getTime())) return "--";
+ return d.toLocaleString("zh-CN", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
/**
* 从姓名生成头像占位用的首字母(最多 2 个字符)。
* 用于 AvatarFallback 组件。
diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md
index 4fa3d09..9cb19ce 100644
--- a/docs/troubleshooting/known-issues.md
+++ b/docs/troubleshooting/known-issues.md
@@ -727,52 +727,74 @@
> v2.1 架构:单 Next.js App Router 容器 + Micro-kernel 插件系统,替代旧 4 端微前端。关联 spec `2026-07-14-portal-shell-widget-dashboard-design.md`。
-| 场景 | 技术/规则 |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType` |
-| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
-| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
-| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
-| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
-| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
-| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
-| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
-| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
-| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
-| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit` |
-| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
-| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
-| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/.ts` 的语义化函数(ADR-042) |
-| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
-| graphql-codegen skipDocumentsValidation | 全局 `skipDocumentsValidation: true` 保留(子图未启动时校验 operations 失败);分域关闭:data-ana 域 dashboard-types.ts 输出已关 `skipDocumentsValidation: false`(P1-7),config 域待 `LayoutTemplateGql.availableSlots` schema 落地后关闭;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
-| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
-| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
-| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
-| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
-| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
-| CI 结构性检查三脚本(P1-8) | `scripts/check-route-table.ts`(路由表一致性:实际 `/shell/*` 路由必须登记到 route-permissions.ts)+ `scripts/check-page-count.ts`(页面计数 baseline=13)+ `scripts/check-codegen.ts`(codegen 契约校验:skipDocumentsValidation:false 输出严格校验);CI 接线 `.github/workflows/ci.yml` quality-ts job |
-| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
-| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
-| TS interface 不满足 Record 约束 | `useWidgetQuery>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
-| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
-| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
-| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
-| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
-| Turbopack 不支持 .js 后缀 import | Next 16 默认 Turbopack 无法像 webpack 那样通过 `resolve.extensionAlias` 将 `.js` 映射到 `.ts/.tsx`;`packages/ui-components` 和 `packages/hooks` 源码内部 import 必须去掉 `.js` 后缀(shared-ts 是 NestJS ESM 模式按规则 §3.4 保留 `.js` 后缀,不修改) |
-| 旧纸感令牌批量迁移 shadcn 标准 | 31 个 widget 全量替换:`bg-paper→bg-background` / `bg-surface→bg-card` / `text-ink→text-foreground` / `border-rule→border` 等 19 项映射;保留 button.tsx 中 `bg-accent`(shadcn 标准 hover 语义令牌,通过 `--accent` CSS 变量定义,非旧纸感 `bg-accent`) |
-| PERMISSION_BITMAP_ORDER 重复权限点 | `GRADE_READ` 在数组 bit 14 和 bit 53 重复出现,全量编解码 round-trip 测试需用 `[...new Set(PERMISSION_BITMAP_ORDER)]` 去重后再比较 |
-| jsdom Blob 不支持 .text() | jsdom 的 Blob 实现不提供 `.text()` 方法,`new Response(blob).text()` 也返回 `[object Blob]`;测试 sendBeacon 时用 `vi.stubGlobal("Blob", vi.fn(...))` mock 构造函数,捕获 `parts[0]` 字符串 |
-| vi.mock 提升导致变量未初始化 | `vi.mock()` 被提升到文件顶部,引用外部变量报 `Cannot access 'X' before initialization`;用 `vi.hoisted()` 将 mock 变量初始化提升到 vi.mock 之前 |
-| sonner toast 双重性质 mock | toast 既是可调用函数 `toast(msg, opts)` 又有方法 `toast.success/.error/.warning/.info`;mock 时需创建 `vi.fn()` 可调用对象后手动挂载 `.success/.error` 等方法 |
-| React 19 use() + Suspense jsdom 测试 | `use(promise)` 在 jsdom 中 promise resolve 后不自动触发重新渲染;需用 `await act(async () => { render(...); await Promise.resolve(); })` 包裹 render,并设置 `globalThis.IS_REACT_ACT_ENVIRONMENT = true`(setup 文件) |
-| 前端错误上报生产端点 | api-gateway 在 `/api/v1/log` 直接处理(不代理到下游),slog 结构化 JSON 日志,64KB body 限制,返回 204;`useErrorReport` 按 `process.env.NODE_ENV` 切换:production→`/api/v1/log`,development→`/api/log`(Next.js API route mock) |
-| vitest setup 文件配置 | `vitest.config.ts` 的 `setupFiles: ["src/__tests__/setup.ts"]` 注册 `@testing-library/jest-dom/vitest` matchers + 设置 `IS_REACT_ACT_ENVIRONMENT`;`declare global { var IS_REACT_ACT_ENVIRONMENT: boolean \| undefined }` 补类型签名 |
-| 管理域 GraphQL operation 命名冲突 | admin.graphql.ts 与 grades.graphql.ts/school-settings.graphql.ts 同名 mutation(CreateGrade/UpdateSchool 等)导致 codegen 重复声明;前缀化命名 `AdminCreateGrade`/`AdminUpdateSchool`/`AdminGetSchedulingRules` 区分 |
-| 管理域 invitation-codes 纯函数时间漂移 | 纯函数 `isInvitationExpired`/`getEffectiveStatus`/`isInvitationRevocable` 调用 `Date.now()` 会造成 SSR/CSR hydration mismatch;参数化注入 `now: number` 由 server page 传入 |
-| 管理域 StatusBadge 虚假推断状态 | `audit-logs` StatusBadge 用 `log.details ? "success" : ""` 推断 status 违反数据真实性原则;改用 schema 字段 `log.status`,未就绪时显示 "--" 占位符 |
-| 管理域未实现功能降级模式 | 未实现的 CRUD(如 questions 创建/导入导出、students/teachers/organization 详情)改为按钮 + `notify.info(t("mswNotice"))` 占位,避免死链;不创建实际路由 |
-| 管理域 @contract-pending 契约标注规则 | schema 未就绪字段在文件头注释 `@contract-pending: <工单>` + `§11.4 登记`;禁止在代码注释中声称 schema 已就绪而实际走 MSW 兜底(plugins 模块原标注造假已修正) |
-| 管理域 announcements grades 关联编辑 | AnnouncementInput.grades 字段已有 schema 定义;edit 表单用逗号分隔文本输入(简化版),避免下拉多选依赖 useGrades hook;提交时 `split(",").map(trim).filter(Boolean)` 解析 |
-| 管理域 audit-logs 分页 | ListPageShell pagination slot 支持 prev/next 按钮 + 当前页/总页数显示;URL 状态 `?page=N`,由 `updateQuery("page", ...)` 控制;总页数 `Math.ceil(total / pageSize)` |
-| 管理域 ai-settings 双权限注释 | AI_CHAT(普通用户访问 private provider)+ AI_CONFIGURE(管理员访问 public/他人 provider);admin 路由仅放行 ["admin"],管理员隐含 AI_CONFIGURE;普通用户 AI_CHAT 走 `/shell/ai-settings`(非 admin 域) |
-| 管理域 viewports DataScope 注释 | 视口配置属管理员全局视角(DataScope = "all"),不随班级/年级范围收窄;route-permissions.ts 仅放行 ["admin"];无需 ctx.dataScope 上下文 |
+| 场景 | 技术/规则 |
+| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType` |
+| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
+| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
+| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
+| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
+| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
+| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
+| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
+| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
+| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
+| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit` |
+| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
+| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
+| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/.ts` 的语义化函数(ADR-042) |
+| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
+| graphql-codegen skipDocumentsValidation | 全局 `skipDocumentsValidation: true` 保留(子图未启动时校验 operations 失败);分域关闭:data-ana 域 dashboard-types.ts 输出已关 `skipDocumentsValidation: false`(P1-7),config 域待 `LayoutTemplateGql.availableSlots` schema 落地后关闭;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
+| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
+| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
+| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
+| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
+| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
+| CI 结构性检查三脚本(P1-8) | `scripts/check-route-table.ts`(路由表一致性:实际 `/shell/*` 路由必须登记到 route-permissions.ts)+ `scripts/check-page-count.ts`(页面计数 baseline=13)+ `scripts/check-codegen.ts`(codegen 契约校验:skipDocumentsValidation:false 输出严格校验);CI 接线 `.github/workflows/ci.yml` quality-ts job |
+| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
+| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
+| TS interface 不满足 Record 约束 | `useWidgetQuery>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
+| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
+| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
+| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
+| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
+| Turbopack 不支持 .js 后缀 import | Next 16 默认 Turbopack 无法像 webpack 那样通过 `resolve.extensionAlias` 将 `.js` 映射到 `.ts/.tsx`;`packages/ui-components` 和 `packages/hooks` 源码内部 import 必须去掉 `.js` 后缀(shared-ts 是 NestJS ESM 模式按规则 §3.4 保留 `.js` 后缀,不修改) |
+| 旧纸感令牌批量迁移 shadcn 标准 | 31 个 widget 全量替换:`bg-paper→bg-background` / `bg-surface→bg-card` / `text-ink→text-foreground` / `border-rule→border` 等 19 项映射;保留 button.tsx 中 `bg-accent`(shadcn 标准 hover 语义令牌,通过 `--accent` CSS 变量定义,非旧纸感 `bg-accent`) |
+| PERMISSION_BITMAP_ORDER 重复权限点 | `GRADE_READ` 在数组 bit 14 和 bit 53 重复出现,全量编解码 round-trip 测试需用 `[...new Set(PERMISSION_BITMAP_ORDER)]` 去重后再比较 |
+| jsdom Blob 不支持 .text() | jsdom 的 Blob 实现不提供 `.text()` 方法,`new Response(blob).text()` 也返回 `[object Blob]`;测试 sendBeacon 时用 `vi.stubGlobal("Blob", vi.fn(...))` mock 构造函数,捕获 `parts[0]` 字符串 |
+| vi.mock 提升导致变量未初始化 | `vi.mock()` 被提升到文件顶部,引用外部变量报 `Cannot access 'X' before initialization`;用 `vi.hoisted()` 将 mock 变量初始化提升到 vi.mock 之前 |
+| sonner toast 双重性质 mock | toast 既是可调用函数 `toast(msg, opts)` 又有方法 `toast.success/.error/.warning/.info`;mock 时需创建 `vi.fn()` 可调用对象后手动挂载 `.success/.error` 等方法 |
+| React 19 use() + Suspense jsdom 测试 | `use(promise)` 在 jsdom 中 promise resolve 后不自动触发重新渲染;需用 `await act(async () => { render(...); await Promise.resolve(); })` 包裹 render,并设置 `globalThis.IS_REACT_ACT_ENVIRONMENT = true`(setup 文件) |
+| 前端错误上报生产端点 | api-gateway 在 `/api/v1/log` 直接处理(不代理到下游),slog 结构化 JSON 日志,64KB body 限制,返回 204;`useErrorReport` 按 `process.env.NODE_ENV` 切换:production→`/api/v1/log`,development→`/api/log`(Next.js API route mock) |
+| vitest setup 文件配置 | `vitest.config.ts` 的 `setupFiles: ["src/__tests__/setup.ts"]` 注册 `@testing-library/jest-dom/vitest` matchers + 设置 `IS_REACT_ACT_ENVIRONMENT`;`declare global { var IS_REACT_ACT_ENVIRONMENT: boolean \| undefined }` 补类型签名 |
+| 管理域 GraphQL operation 命名冲突 | admin.graphql.ts 与 grades.graphql.ts/school-settings.graphql.ts 同名 mutation(CreateGrade/UpdateSchool 等)导致 codegen 重复声明;前缀化命名 `AdminCreateGrade`/`AdminUpdateSchool`/`AdminGetSchedulingRules` 区分 |
+| 管理域 invitation-codes 纯函数时间漂移 | 纯函数 `isInvitationExpired`/`getEffectiveStatus`/`isInvitationRevocable` 调用 `Date.now()` 会造成 SSR/CSR hydration mismatch;参数化注入 `now: number` 由 server page 传入 |
+| 管理域 StatusBadge 虚假推断状态 | `audit-logs` StatusBadge 用 `log.details ? "success" : ""` 推断 status 违反数据真实性原则;改用 schema 字段 `log.status`,未就绪时显示 "--" 占位符 |
+| 管理域未实现功能降级模式 | 未实现的 CRUD(如 questions 创建/导入导出、students/teachers/organization 详情)改为按钮 + `notify.info(t("mswNotice"))` 占位,避免死链;不创建实际路由 |
+| 管理域 @contract-pending 契约标注规则 | schema 未就绪字段在文件头注释 `@contract-pending: <工单>` + `§11.4 登记`;禁止在代码注释中声称 schema 已就绪而实际走 MSW 兜底(plugins 模块原标注造假已修正) |
+| 管理域 announcements grades 关联编辑 | AnnouncementInput.grades 字段已有 schema 定义;edit 表单用逗号分隔文本输入(简化版),避免下拉多选依赖 useGrades hook;提交时 `split(",").map(trim).filter(Boolean)` 解析 |
+| 管理域 audit-logs 分页 | ListPageShell pagination slot 支持 prev/next 按钮 + 当前页/总页数显示;URL 状态 `?page=N`,由 `updateQuery("page", ...)` 控制;总页数 `Math.ceil(total / pageSize)` |
+| 管理域 ai-settings 双权限注释 | AI_CHAT(普通用户访问 private provider)+ AI_CONFIGURE(管理员访问 public/他人 provider);admin 路由仅放行 ["admin"],管理员隐含 AI_CONFIGURE;普通用户 AI_CHAT 走 `/shell/ai-settings`(非 admin 域) |
+| 管理域 viewports DataScope 注释 | 视口配置属管理员全局视角(DataScope = "all"),不随班级/年级范围收窄;route-permissions.ts 仅放行 ["admin"];无需 ctx.dataScope 上下文 |
+| 学生域 CICD 迁移整体策略 | 列表页统一改 Card 网格(md:grid-cols-2 lg:grid-cols-3)替代 CICD 表格;详情页用 DetailPageShell + DetailField;富文档自研 markdown 解析(标题/列表/段落/加粗),不引入 react-markdown;图表用纯 SVG 自研(折线/柱状/雷达),不引入 recharts |
+| 学生域 Server Action → Mutation 适配 | CICD 用 Next.js Server Actions(selectCourseAction/dropCourseAction/joinClassByInvitationCodeAction 等);portal-shell 改写为 Apollo mutation hook(useEnrollCourse/useDropCourse/useJoinClassByInvitationCode),MSW 兜底 |
+| 学生域 @contract-pending 数据契约清单 | studentDashboard/studentGrades/studentReportCard/studentAttendance/studentCoursePlans/studentCoursePlanDetail/studentSelfDiagnostic/studentErrorBook/studentHomework/studentHomeworkSubmit/studentHomeworkAnalysis/studentLessonPlans/studentLessonPlanView/studentPractice/studentPracticeSession/studentSchedule/studentLeave/electiveCourses/studentElectiveDetail/studentTextbooks/studentTextbookChapters/studentMessageDetail/messageRecipients/sendMessage/userProfile 均待 core-edu/iam/content/ai 服务补齐 |
+| 学生域角色自适应空态 | 学生未登录或无学生身份时显示 EmptyState + UserX 图标(schedule/learning-center 等);通过 useStudentClasses/useUserProfile hook 获取学生身份 |
+| 学生域状态分支避免逻辑错误 | homework/[id]/submit 页需根据 submission.status 分流:submitted/graded → ReviewView 复习视图,not_started/in_progress → TakeView 作答视图;始终显示 TakeView 会导致已批改作业仍可"作答"逻辑错误 |
+| 学生域范围校验契约占位 | lesson-plans/[id]/view 的 assertPlanInScope、textbooks 的学生年级强制过滤等 CICD 服务端校验在 portal-shell 改为前端兜底 + `@contract-pending scope-check` 注释,待 BFF resolver 补齐 |
+| 学生域 ReviewView 复习视图字段映射 | 复用 homework-analysis-client 的 QuestionAnalysisTable;状态归一化函数 normalizeSubmissionStatus 兼容 MSW 中文与英文枚举;maxAttempts/attemptsUsed 在 QuestionNavPanel 顶部显示 |
+| 学生域 A4 打印布局 | report-card 用 `.report-card-a4` 容器(width:210mm min-height:297mm padding:15mm)+ `report-card-print.css` 的 @media print 规则;打印按钮 loading 状态 + afterprint 事件重置;rowSpan 合并同学科 Subject 单元格展示多条 records |
+| 学生域 CoursePlanProgress 进度条 | 用纯 Tailwind div + ARIA role="progressbar" 实现双进度条(完成学时 completedHours/totalHours + 完成项数 completedItems/totalItems),三段色阶(100% 绿、>0 主色、0 灰) |
+| 学生域 KnowledgePoint 命名冲突 | student-portal.ts 的 `KnowledgePoint`/`useKnowledgePoints` 与 knowledge-graph.ts 同名冲突(TS2308);重命名为 `StudentKnowledgePoint`/`useStudentKnowledgePoints` 避免 barrel index.ts 重复导出 |
+| 学生域 StudentSelfDiagnostic 命名冲突 | 学生自我诊断(无参数 camelCase)与教师按 studentId 查询(snake_case)同名 `useStudentDiagnostic` 冲突;学生版重命名为 `StudentSelfDiagnostic`/`useStudentSelfDiagnostic`/`GetStudentSelfDiagnostic` |
+| 学生域 ElectiveStatus 命名冲突 | student-portal.ts 的 `ElectiveStatus` 与其他模块同名冲突(TS2308);重命名为 `StudentElectiveStatus` |
+| 学生域 UserProfile 命名冲突 | profile.ts 的 `UserProfile` 与 settings.ts 同名冲突(TS2308);profile 版重命名为 `UserProfileDetail`/`useUserProfile` |
+| 学生域 Card 视图 vs 表格视图切换 | homework-list/error-book-list 等列表页提供 viewMode 切换(card/table),默认 card;Card 视图展示更多字段(attemptsUsed/latestScore/difficulty/reviewCount 等),表格视图作为紧凑模式 |
+| 学生域 退课确认 AlertDialog | elective 用 AlertDialog 确认退课,含退课理由 textarea(maxLength 255);cancel/confirm 按钮 + loading 状态;成功后 refetch 已选列表 |
+| 学生域 加入班级邀请码表单 | courses-list 顶部 "加入班级" 按钮 + JoinClassDialog,6 位数字邀请码输入(pattern=[0-9]{6});MSW 兜底(123456 成功/000000 已加入/其他无效);成功后 refetch 课程列表 |
+| 学生域 messages 共享路由组件创建 | message-detail-client/message-compose-client/message-group-compose-client 3 个组件在 `features/shared/messages/` 下创建;学生/教师/管理员均可访问;group-compose 用原生 `` 多选收件人(项目无 Checkbox 组件,避免引入新依赖) |
+| 学生域 error-book 变式练习 hook 签名 | `useStartPracticeSession().run` 签名为 `(knowledgePointIds: string[])`;error-book-detail-dialog 调用时需包装为 `[kpId]` 数组,否则报 TS2345 |
+| 学生域 profile 共享路由 | `/shell/profile` 是所有角色可访问的共享路由(route-permissions.ts 不限角色);ProfileClient 显示双卡片布局(个人信息 + 账户信息)+ 角色专属概览面板(学生显示班级/年级,教师显示教授课程) |
+| 教师域 CICD 迁移数据层适配 | CICD 用 Server Actions + data-access.ts;portal-shell 改为 useWidgetQuery/useWidgetMutation + MSW 兜底,缺失 hook 用 `@contract-pending` 注释 + 本地 mock 占位 |
+| 教师域 CICD 迁移 Select/AlertDialog 适配 | CICD 用 Radix Select 复合组件(Trigger/Content/Item)+ AlertDialogTrigger;portal-shell 用原生 Select 的 `options` 数组 API + `useState` 手动管理 AlertDialog open 状态 |
+| 教师域 CICD 迁移 service-context 改造 | CICD 的 `services/xxx-service-context.tsx` 通过 Server Actions 暴露 service;portal-shell 改为 Context Provider + useWidgetQuery/useWidgetMutation 包装,默认 service 实现为 no-op + notify.info 提示契约未就绪 |
diff --git a/packages/ui-tokens/src/semantic-dark.css b/packages/ui-tokens/src/semantic-dark.css
index 3e4a6bc..bec01cb 100644
--- a/packages/ui-tokens/src/semantic-dark.css
+++ b/packages/ui-tokens/src/semantic-dark.css
@@ -64,6 +64,16 @@
--diff-remove: 0 84% 70%;
--diff-remove-bg: 0 84% 70% / 0.15;
+ /* ============ 业务状态语义令牌 ============ */
+ --success: 142 71% 55%;
+ --success-foreground: 0 0% 100%;
+ --warning: 38 92% 55%;
+ --warning-foreground: 0 0% 100%;
+ --danger: 0 84% 70%;
+ --danger-foreground: 0 0% 100%;
+ --info: 217 91% 65%;
+ --info-foreground: 0 0% 100%;
+
--graph-node-1: 220 70% 50%;
--graph-node-2: 160 60% 45%;
--graph-node-3: 30 80% 55%;
diff --git a/packages/ui-tokens/src/semantic-light.css b/packages/ui-tokens/src/semantic-light.css
index bfb472f..09ee64b 100644
--- a/packages/ui-tokens/src/semantic-light.css
+++ b/packages/ui-tokens/src/semantic-light.css
@@ -66,6 +66,16 @@
--diff-remove: 0 84% 60%;
--diff-remove-bg: 0 84% 60% / 0.1;
+ /* ============ 业务状态语义令牌 ============ */
+ --success: 142 71% 45%;
+ --success-foreground: 0 0% 100%;
+ --warning: 38 92% 50%;
+ --warning-foreground: 0 0% 100%;
+ --danger: 0 84% 60%;
+ --danger-foreground: 0 0% 100%;
+ --info: 217 91% 60%;
+ --info-foreground: 0 0% 100%;
+
/* 知识图谱节点色 */
--graph-node-1: 12 76% 61%; /* = --chart-1 */
--graph-node-2: 173 58% 39%; /* = --chart-2 */
diff --git a/packages/ui-tokens/src/tailwind-theme.css b/packages/ui-tokens/src/tailwind-theme.css
index 2b634d4..cecb443 100644
--- a/packages/ui-tokens/src/tailwind-theme.css
+++ b/packages/ui-tokens/src/tailwind-theme.css
@@ -55,6 +55,15 @@
--color-text-tertiary: hsl(var(--text-tertiary));
--color-border-strong: hsl(var(--border-strong));
--color-border-subtle: hsl(var(--border-subtle));
+ --color-success: hsl(var(--success));
+ --color-success-foreground: hsl(var(--success-foreground));
+ --color-warning: hsl(var(--warning));
+ --color-warning-foreground: hsl(var(--warning-foreground));
+ --color-danger: hsl(var(--danger));
+ --color-danger-foreground: hsl(var(--danger-foreground));
+ --color-info: hsl(var(--info));
+ --color-info-foreground: hsl(var(--info-foreground));
+
--color-diff-add: hsl(var(--diff-add));
--color-diff-add-bg: hsl(var(--diff-add-bg));
--color-diff-remove: hsl(var(--diff-remove));
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a62163b..d06eacb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -186,7 +186,7 @@ importers:
version: 5.101.2(react@18.3.0)
'@urql/next':
specifier: ^1.1.0
- version: 1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))
+ version: 1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))
clsx:
specifier: ^2.1.0
version: 2.1.1
@@ -222,7 +222,7 @@ importers:
version: 3.23.0
zustand:
specifier: ^4.5.0
- version: 4.5.0(@types/react@19.2.17)(react@18.3.0)
+ version: 4.5.0(@types/react@19.2.17)(immer@11.1.15)(react@18.3.0)
devDependencies:
'@axe-core/playwright':
specifier: ^4.10.0
@@ -351,6 +351,27 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.20(tailwindcss@4.3.2)
+ '@tiptap/core':
+ specifier: ^3.29.0
+ version: 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/extension-image':
+ specifier: ^3.29.0
+ version: 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-placeholder':
+ specifier: ^3.29.0
+ version: 3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-underline':
+ specifier: ^3.29.0
+ version: 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/pm':
+ specifier: ^3.29.0
+ version: 3.29.0
+ '@tiptap/react':
+ specifier: ^3.29.0
+ version: 3.29.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tiptap/starter-kit':
+ specifier: ^3.29.0
+ version: 3.29.0
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -387,6 +408,15 @@ importers:
react-dom:
specifier: ^19.2.1
version: 19.2.7(react@19.2.7)
+ react-markdown:
+ specifier: ^10.1.0
+ version: 10.1.0(@types/react@19.2.17)(react@19.2.7)
+ recharts:
+ specifier: ^3.6.0
+ version: 3.10.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@17.0.1)(react@19.2.7)(redux@5.0.1)
+ remark-gfm:
+ specifier: ^4.0.1
+ version: 4.0.1
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -404,7 +434,7 @@ importers:
version: 3.25.76
zustand:
specifier: ^5.0.9
- version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
+ version: 5.0.14(@types/react@19.2.17)(immer@11.1.15)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
devDependencies:
'@graphql-codegen/cli':
specifier: ^5.0.0
@@ -522,7 +552,7 @@ importers:
version: 3.23.0
zustand:
specifier: ^4.5.0
- version: 4.5.0(@types/react@19.2.17)(react@18.3.0)
+ version: 4.5.0(@types/react@19.2.17)(immer@11.1.15)(react@18.3.0)
devDependencies:
'@axe-core/playwright':
specifier: ^4.10.0
@@ -5761,6 +5791,17 @@ packages:
peerDependencies:
'@redis/client': ^1.0.0
+ '@reduxjs/toolkit@2.12.0':
+ resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@repeaterjs/repeater@3.0.4':
resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==}
@@ -6128,6 +6169,12 @@ packages:
'@sqltools/formatter@1.2.5':
resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@swc/core-darwin-arm64@1.15.46':
resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==}
engines: {node: '>=10'}
@@ -6364,6 +6411,166 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
+ '@tiptap/core@3.29.0':
+ resolution: {integrity: sha512-A/lrhKpOYtl0V5pmPS00Zps8pgBe1qDOoD9fzsumDSZ3HP8W398C959Jgru75PNFokAya9COPD3iaKP3NWF25g==}
+ peerDependencies:
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-blockquote@3.29.0':
+ resolution: {integrity: sha512-k8NKHLEYOqre7guObZBeFM04CaTwmEceCmdIrIjd7H9KoHSbRODaBNved6j5G33x/FWMwIF/RQbE2BjWaFSgzA==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-bold@3.29.0':
+ resolution: {integrity: sha512-BPUJvJ9sCsU3fxao5UJfDJrDuHhMn2hczZwZ9Qs4w1vkAeLGyOYbSejUTJdBYHLoexGiEwgnEeVeZN3C8GaI5g==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-bubble-menu@3.29.0':
+ resolution: {integrity: sha512-aBNI8ebX76ObderRmu/PJ9Q2AGVEuhpg0TBvv2SNbHtmU5kNUMHQI8HxrxKFblJwETAmG6EJ3Ed/5jRxpJmrsg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-bullet-list@3.29.0':
+ resolution: {integrity: sha512-1045sC5CRn7KD0wluxZeksBwpm2k3LKBUOM3kNwExHAPSV4in4gdarYJ2/WCmIKsmyj2FzUcF4rbpLLSB/dBSg==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.29.0
+
+ '@tiptap/extension-code-block@3.29.0':
+ resolution: {integrity: sha512-nKm1YSEiPVosjm4qeg6m+6SAKq82rKukAzE/g3M5eVs2OgEb4QIKQvQOmJ+zgGAnpb9Q/JIvcKsGGU1rn9Vnyw==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-code@3.29.0':
+ resolution: {integrity: sha512-Um0BlyunRJ8Fal288Jn7I2n15FRwQcTy3NnKBBrPz3ikwx+yZZ0n9xTozpLMSxXrTq6cRlxLkASp/9FFx9sX9A==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-document@3.29.0':
+ resolution: {integrity: sha512-folTlrwmUL+WSnojvfjZ951Gb25Pw8BxjXwhvXFuZXJ92qMfOcezA480kwFuu5nG8pPc/76ezQlz9a86Y5uQJw==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-dropcursor@3.29.0':
+ resolution: {integrity: sha512-IAY21q7KSXyCXN3u13kfpq0E8D2i3r9nSbsNYlLzkEv0Bpn+pWShZi05A0ubuothDNjdaMa96xaRY0f1y9gDZw==}
+ peerDependencies:
+ '@tiptap/extensions': 3.29.0
+
+ '@tiptap/extension-floating-menu@3.29.0':
+ resolution: {integrity: sha512-/rFlN6T70HuT34kFvftKm56UX0IPff7vlII7Sqgs7DprKVYkItiU3lM7+AoeDRzCsWMg4z1yNHj70BPx7+EJ6Q==}
+ peerDependencies:
+ '@floating-ui/dom': ^1.0.0
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-gapcursor@3.29.0':
+ resolution: {integrity: sha512-Jz0zwriGxPMuRjKo6x7WhPa/7pZn0dovyJu/GLarN4T+mMseGNXWwEBsLEKkuumsJVPDEPPFjX+rzEeaFKInPQ==}
+ peerDependencies:
+ '@tiptap/extensions': 3.29.0
+
+ '@tiptap/extension-hard-break@3.29.0':
+ resolution: {integrity: sha512-8qKj0oeuU7IYoG73Lj3W4EC/JS/dFjN5iHmc4yodqppP6qs0o2zsF/2ipceGN/REZq5megPtxRgXu/nxQZ1NYg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-heading@3.29.0':
+ resolution: {integrity: sha512-xWb3QEKo7cp9u6Vzi6oYKODra6jPwziHBv3fJAnxWpRldxtJqfyOVMikWJJFuDjmr+K0qyegzf206pMk+y0bDg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-horizontal-rule@3.29.0':
+ resolution: {integrity: sha512-u1OgncXkokIuUJQIh4jVQfYHL/6I8VnrokUcCud4eErWvgeLnCbNmVdih4kxIdfmvEyAxXjDwcqf4AB79TxuNg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-image@3.29.0':
+ resolution: {integrity: sha512-A+9Oxsobh4IRWpYj8+y4c0ujhZPY0z9PqvzyuV5GCqiUTRcTRnFI4a4PZDXO/GcCCQXfznfu8gbMtaEyEQW5+w==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-italic@3.29.0':
+ resolution: {integrity: sha512-c8gF+zM7yXB9iGxtVCioYecjQ+A2fLlNa1XYBYbTobNhbUo8a11akbTkGNcCIgttwnzwEoI4mB6/PK4yjaQvVg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-link@3.29.0':
+ resolution: {integrity: sha512-FuLDgE0k1dB4WCBTelw3re9BLmiurgXVSj3d3J4l3sunljyOs941RUU62b7pWP+ePNdGF7RgUMmMAB+58VZCIA==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-list-item@3.29.0':
+ resolution: {integrity: sha512-JSz27OIDHWDL7uw28E4W3eaN3H1u+NzJQKZkNXo5Qsvvsx8m/YIQWO9XlhiL3V4TZMjfvlc94lUp5tKW5KwETw==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.29.0
+
+ '@tiptap/extension-list-keymap@3.29.0':
+ resolution: {integrity: sha512-g14QLZR9fmfJIL+X2R/cVTvQS0Mp7CoenA9PO+8adTjK7hCeB8hA3UAakMB4ekkElOXNOtb16e0wqulDcoqANg==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.29.0
+
+ '@tiptap/extension-list@3.29.0':
+ resolution: {integrity: sha512-k0B/+nIkn4VvHSQ0kP+AzzAmgeOVxKMAdqG4a6qwxp/lR12aJGHlOP92KCjXV2RNPtuwDksJ1RIXrgxdf9WmJg==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-ordered-list@3.29.0':
+ resolution: {integrity: sha512-9TXUDFagkGeu6Bo8L0b2SAVAYrY2Xzd28MXwiDxYvDKMjEGWkkCUpMiEI5Qav3GnfFWLmS3jI8PgUjxIw9wLLQ==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.29.0
+
+ '@tiptap/extension-paragraph@3.29.0':
+ resolution: {integrity: sha512-OtGrkwzqlW+ehW+d2qrQ7VVcP9PbDeQmuT4Ec4yFjf3vR4QO8OVdVfeOcUq52PtkT3kYRxz9fzEiEvjqRm72yA==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-placeholder@3.29.0':
+ resolution: {integrity: sha512-V2jb0kL/k3rygAtKmSPYxoyBMCQJtJikPYnxiCYuY7FI1ohmCrmhyhkVqJv0b0EPMNW8BMIqf6DgpKJ/ODmXgA==}
+ peerDependencies:
+ '@tiptap/extensions': 3.29.0
+
+ '@tiptap/extension-strike@3.29.0':
+ resolution: {integrity: sha512-VzasZkckrEXzmFQT9z3O95rUWy1b0RLFHAC6GUSQHxKk4TpP+Ujfn5+rqmPukMn0eScJyw1Y36jwfJc0VuNR+Q==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-text@3.29.0':
+ resolution: {integrity: sha512-AMsf2V7IiGvzbl+LqLLfyWzufazZxHuid1gOnEt3KwolQ9AO7p8WJgHld0PZLxNyEh4SaSqK7b3d3SM9sqrlTw==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extension-underline@3.29.0':
+ resolution: {integrity: sha512-DSPTogdvxmoX/L0U5KgBC9jWffRvYkGiyh1tp28gn+QaARh4iJn79pAZpi1m1bEhal/0Mq5m4ukbiO7FWInwaA==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+
+ '@tiptap/extensions@3.29.0':
+ resolution: {integrity: sha512-ltlrm8dDHIgeNj3cOLEdLFMPPVy3TYvWA8ftrrJ44C/L01MBmgFB1f/vkPFYYnAasb2BYyVG6HxAGcTQHo5jHw==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/pm@3.29.0':
+ resolution: {integrity: sha512-4rr3HiZ8kbSNINuWXqKQJLv9fFMypCBN5gOwxoU+D4lEYVblkoM60fA1SlrI2BZVlZQmw2U38XKl5XMtUr55XQ==}
+
+ '@tiptap/react@3.29.0':
+ resolution: {integrity: sha512-HmZf9q5/HXg5M2t09WGEbk16vEBDUGPlkF3eXcQh6yG6PkzMZHq1SP+uA8lWku5H/Ggfmv4XKjI6dEQa5V8bwA==}
+ peerDependencies:
+ '@tiptap/core': 3.29.0
+ '@tiptap/pm': 3.29.0
+ '@types/react': ^19.0.0
+ '@types/react-dom': ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tiptap/starter-kit@3.29.0':
+ resolution: {integrity: sha512-J3jTp3/WXnnL58TCtdMsfeQ2BeycFYrAib6Nbpjd9G0OLUBE7AILiH2HrobRQpvRbAUgD5SW+sBZYOonbWWqNg==}
+
'@tokenizer/inflate@0.2.6':
resolution: {integrity: sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==}
engines: {node: '>=16'}
@@ -6449,6 +6656,9 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
'@types/docker-modem@3.0.6':
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
@@ -6461,6 +6671,9 @@ packages:
'@types/eslint@9.6.1':
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
@@ -6473,6 +6686,9 @@ packages:
'@types/google-protobuf@3.15.12':
resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==}
+ '@types/hast@3.0.5':
+ resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
+
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
@@ -6503,6 +6719,9 @@ packages:
'@types/long@4.0.2':
resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
'@types/memcached@2.2.10':
resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==}
@@ -6595,6 +6814,15 @@ packages:
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
@@ -6669,6 +6897,9 @@ packages:
resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@ungap/structured-clone@1.3.3':
+ resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
+
'@urql/core@2.4.3':
resolution: {integrity: sha512-FpapxUKF0nLdzRLoB1QsudDjeLXJhBwzkzl8bSOJ6Cnj7LRRKJ+dYdqHfqGykswB/ILrkZks2Isp4a4BhqyUow==}
peerDependencies:
@@ -7140,6 +7371,9 @@ packages:
backo2@1.0.2:
resolution: {integrity: sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==}
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
balanced-match@1.0.0:
resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==}
@@ -7325,6 +7559,9 @@ packages:
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
chai@5.1.1:
resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
engines: {node: '>=12'}
@@ -7355,6 +7592,18 @@ packages:
change-case@4.1.2:
resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
@@ -7470,6 +7719,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
command-line-args@6.0.2:
resolution: {integrity: sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==}
engines: {node: '>=12.20'}
@@ -7784,6 +8036,9 @@ packages:
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -7860,6 +8115,9 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -8086,6 +8344,9 @@ packages:
resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.50.0:
+ resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==}
+
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
peerDependencies:
@@ -8121,6 +8382,10 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
+ escape-string-regexp@5.0.0:
+ resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+ engines: {node: '>=12'}
+
eslint-config-prettier@9.1.2:
resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==}
hasBin: true
@@ -8188,6 +8453,9 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -8712,6 +8980,12 @@ packages:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
header-case@2.0.4:
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
@@ -8746,6 +9020,9 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ html-url-attributes@3.0.1:
+ resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -8816,6 +9093,9 @@ packages:
resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
engines: {node: '>= 4'}
+ immer@11.1.15:
+ resolution: {integrity: sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==}
+
immutable@5.1.9:
resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==}
@@ -8877,6 +9157,9 @@ packages:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ inline-style-parser@0.2.7:
+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
+
inquirer@8.0.0:
resolution: {integrity: sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==}
engines: {node: '>=8.0.0'}
@@ -8918,6 +9201,12 @@ packages:
resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==}
engines: {node: '>=0.10.0'}
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -8957,6 +9246,9 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
is-document.all@1.0.0:
resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
engines: {node: '>= 0.4'}
@@ -8985,6 +9277,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
@@ -9019,6 +9314,10 @@ packages:
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
engines: {node: '>=0.10.0'}
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
@@ -9380,6 +9679,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ linkifyjs@4.3.3:
+ resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
+
lint-staged@15.0.0:
resolution: {integrity: sha512-OdrAd/qZhO4MnPyNRehI1Fh6j8kcfQoGpjQ1Vw8dSkDkNONdqe/HpKXqtKpFIPCAYeF4/tfnE11MnjLYuLKjqA==}
engines: {node: '>=18.12.0'}
@@ -9503,6 +9805,9 @@ packages:
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -9586,10 +9891,58 @@ packages:
resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
engines: {node: '>=8'}
+ markdown-table@3.0.4:
+ resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mdast-util-find-and-replace@3.0.2:
+ resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
+
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
+
+ mdast-util-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
+
+ mdast-util-gfm-table@2.0.0:
+ resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
+
+ mdast-util-gfm@3.1.0:
+ resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
+
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -9625,6 +9978,90 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
+
+ micromark-extension-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
+
+ micromark-extension-gfm-table@2.1.1:
+ resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
+
+ micromark-extension-gfm@3.0.0:
+ resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
@@ -10044,6 +10481,9 @@ packages:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
+ orderedmap@2.1.1:
+ resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
+
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
@@ -10093,6 +10533,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
parse-filepath@1.0.2:
resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==}
engines: {node: '>=0.8'}
@@ -10381,6 +10824,48 @@ packages:
resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==}
engines: {node: '>=14'}
+ property-information@7.2.0:
+ resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+
+ prosemirror-changeset@2.4.1:
+ resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
+
+ prosemirror-commands@1.7.1:
+ resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
+
+ prosemirror-dropcursor@1.8.3:
+ resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==}
+
+ prosemirror-gapcursor@1.4.1:
+ resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
+
+ prosemirror-history@1.5.0:
+ resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
+
+ prosemirror-inputrules@1.5.1:
+ resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
+
+ prosemirror-keymap@1.2.3:
+ resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
+
+ prosemirror-model@1.25.11:
+ resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==}
+
+ prosemirror-schema-list@1.5.1:
+ resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
+
+ prosemirror-state@1.4.4:
+ resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
+
+ prosemirror-tables@1.8.5:
+ resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==}
+
+ prosemirror-transform@1.12.0:
+ resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
+
+ prosemirror-view@1.42.2:
+ resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==}
+
protobufjs@7.6.5:
resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==}
engines: {node: '>=12.0.0'}
@@ -10473,6 +10958,24 @@ packages:
react-is@17.0.1:
resolution: {integrity: sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==}
+ react-markdown@10.1.0:
+ resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+ react: '>=18'
+
+ react-redux@9.3.0:
+ resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -10571,6 +11074,14 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+ recharts@3.10.1:
+ resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -10590,6 +11101,14 @@ packages:
redis@4.7.0:
resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==}
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -10612,6 +11131,18 @@ packages:
react:
optional: true
+ remark-gfm@4.0.1:
+ resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
+
+ remark-parse@11.0.0:
+ resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
+
+ remark-rehype@11.1.2:
+ resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+
+ remark-stringify@11.0.0:
+ resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+
remedial@1.0.8:
resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==}
@@ -10644,6 +11175,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ reselect@5.2.0:
+ resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -10714,6 +11248,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rope-sequence@1.3.4:
+ resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
+
rrweb-cssom@0.6.0:
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
@@ -10962,6 +11499,9 @@ packages:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
@@ -11099,6 +11639,9 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
strip-ansi@5.0.0:
resolution: {integrity: sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==}
engines: {node: '>=6'}
@@ -11147,6 +11690,12 @@ packages:
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
engines: {node: '>=18'}
+ style-to-js@1.1.21:
+ resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
+
+ style-to-object@1.0.14:
+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+
styled-jsx@5.1.1:
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@@ -11445,10 +11994,16 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
trim-newlines@5.0.0:
resolution: {integrity: sha512-kstfs+hgwmdsOadN3KgA+C68wPJwnZq4DN6WMDCvZapDWEF34W2TyPKN2v2+BJnZgIz5QOfxFeldLyYvdgRAwg==}
engines: {node: '>=14.16'}
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
@@ -11673,6 +12228,24 @@ packages:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@@ -11812,9 +12385,18 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
vite-node@2.1.0:
resolution: {integrity: sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -11873,6 +12455,9 @@ packages:
jsdom:
optional: true
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
@@ -12191,6 +12776,9 @@ packages:
use-sync-external-store:
optional: true
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
snapshots:
'@0no-co/graphql.web@1.3.2(graphql@16.14.2)':
@@ -14170,7 +14758,7 @@ snapshots:
next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
react: 18.3.0
react-dom: 18.3.0(react@18.3.0)
- styled-jsx: 5.1.6(@babel/core@7.29.7)(react@18.3.0)
+ styled-jsx: 5.1.6(react@18.3.0)
webpack: 5.92.1(lightningcss@1.32.0)(postcss@8.5.18)
transitivePeerDependencies:
- '@rspack/core'
@@ -16998,6 +17586,18 @@ snapshots:
dependencies:
'@redis/client': 1.6.0
+ '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.15
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.2.0
+ optionalDependencies:
+ react: 19.2.7
+ react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
+
'@repeaterjs/repeater@3.0.4': {}
'@repeaterjs/repeater@3.1.0': {}
@@ -17388,6 +17988,10 @@ snapshots:
'@sqltools/formatter@1.2.5': {}
+ '@standard-schema/spec@1.1.0': {}
+
+ '@standard-schema/utils@0.3.0': {}
+
'@swc/core-darwin-arm64@1.15.46':
optional: true
@@ -17587,6 +18191,187 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.1
+ '@tiptap/core@3.29.0(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-blockquote@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-bold@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-bubble-menu@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+ optional: true
+
+ '@tiptap/extension-bullet-list@3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-code-block@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-code@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-document@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-dropcursor@3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extensions': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-floating-menu@3.29.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+ optional: true
+
+ '@tiptap/extension-gapcursor@3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extensions': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-hard-break@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-heading@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-horizontal-rule@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-image@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-italic@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-link@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+ linkifyjs: 4.3.3
+
+ '@tiptap/extension-list-item@3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-list-keymap@3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/extension-ordered-list@3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-paragraph@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-placeholder@3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/extensions': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-strike@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-text@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extension-underline@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+
+ '@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
+ '@tiptap/pm@3.29.0':
+ dependencies:
+ prosemirror-changeset: 2.4.1
+ prosemirror-commands: 1.7.1
+ prosemirror-dropcursor: 1.8.3
+ prosemirror-gapcursor: 1.4.1
+ prosemirror-history: 1.5.0
+ prosemirror-inputrules: 1.5.1
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-schema-list: 1.5.1
+ prosemirror-state: 1.4.4
+ prosemirror-tables: 1.8.5
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.2
+
+ '@tiptap/react@3.29.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+ '@types/use-sync-external-store': 0.0.6
+ fast-equals: 5.4.1
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ '@tiptap/extension-bubble-menu': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-floating-menu': 3.29.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ transitivePeerDependencies:
+ - '@floating-ui/dom'
+
+ '@tiptap/starter-kit@3.29.0':
+ dependencies:
+ '@tiptap/core': 3.29.0(@tiptap/pm@3.29.0)
+ '@tiptap/extension-blockquote': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-bold': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-bullet-list': 3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-code': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-code-block': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-document': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-dropcursor': 3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-gapcursor': 3.29.0(@tiptap/extensions@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-hard-break': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-heading': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-horizontal-rule': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-italic': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-link': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-list': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/extension-list-item': 3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-list-keymap': 3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-ordered-list': 3.29.0(@tiptap/extension-list@3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0))
+ '@tiptap/extension-paragraph': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-strike': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-text': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extension-underline': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))
+ '@tiptap/extensions': 3.29.0(@tiptap/core@3.29.0(@tiptap/pm@3.29.0))(@tiptap/pm@3.29.0)
+ '@tiptap/pm': 3.29.0
+
'@tokenizer/inflate@0.2.6':
dependencies:
debug: 4.4.3
@@ -17688,6 +18473,10 @@ snapshots:
'@types/d3-timer@3.0.2': {}
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
'@types/docker-modem@3.0.6':
dependencies:
'@types/node': 22.20.1
@@ -17709,6 +18498,10 @@ snapshots:
'@types/estree': 1.0.9
'@types/json-schema': 7.0.15
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.9
+
'@types/estree@1.0.9': {}
'@types/express-serve-static-core@4.19.9':
@@ -17727,6 +18520,10 @@ snapshots:
'@types/google-protobuf@3.15.12': {}
+ '@types/hast@3.0.5':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/http-errors@2.0.5': {}
'@types/istanbul-lib-coverage@2.0.6': {}
@@ -17756,6 +18553,10 @@ snapshots:
'@types/long@4.0.2': {}
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/memcached@2.2.10':
dependencies:
'@types/node': 22.20.1
@@ -17863,6 +18664,12 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@types/use-sync-external-store@0.0.6': {}
+
'@types/uuid@10.0.0': {}
'@types/uuid@9.0.8': {}
@@ -17968,6 +18775,8 @@ snapshots:
'@typescript-eslint/types': 8.63.0
eslint-visitor-keys: 5.0.1
+ '@ungap/structured-clone@1.3.3': {}
+
'@urql/core@2.4.3(graphql@16.14.2)':
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2)
@@ -17981,7 +18790,7 @@ snapshots:
transitivePeerDependencies:
- graphql
- '@urql/next@1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))':
+ '@urql/next@1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))':
dependencies:
next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
react: 18.3.0
@@ -18535,6 +19344,8 @@ snapshots:
backo2@1.0.2: {}
+ bail@2.0.2: {}
+
balanced-match@1.0.0: {}
balanced-match@4.0.4: {}
@@ -18743,6 +19554,8 @@ snapshots:
tslib: 2.8.1
upper-case-first: 2.0.2
+ ccount@2.0.1: {}
+
chai@5.1.1:
dependencies:
assertion-error: 2.0.1
@@ -18797,6 +19610,14 @@ snapshots:
snake-case: 3.0.4
tslib: 2.8.1
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
chardet@0.7.0: {}
check-error@2.1.3: {}
@@ -18897,6 +19718,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
+ comma-separated-tokens@2.0.3: {}
+
command-line-args@6.0.2:
dependencies:
array-back: 6.2.3
@@ -19187,6 +20010,10 @@ snapshots:
decimal.js@10.6.0: {}
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -19237,6 +20064,10 @@ snapshots:
detect-node-es@1.1.0: {}
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
didyoumean@1.2.2: {}
dir-glob@3.0.1:
@@ -19455,6 +20286,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es-toolkit@1.50.0: {}
+
esbuild-register@3.6.0(esbuild@0.19.7):
dependencies:
debug: 4.4.3
@@ -19549,6 +20382,8 @@ snapshots:
escape-string-regexp@4.0.0: {}
+ escape-string-regexp@5.0.0: {}
+
eslint-config-prettier@9.1.2(eslint@9.39.5(jiti@1.21.7)):
dependencies:
eslint: 9.39.5(jiti@1.21.7)
@@ -19713,6 +20548,8 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-is-identifier-name@3.0.0: {}
+
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -20325,6 +21162,30 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
header-case@2.0.4:
dependencies:
capital-case: 1.0.4
@@ -20359,6 +21220,8 @@ snapshots:
html-escaper@2.0.2: {}
+ html-url-attributes@3.0.1: {}
+
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -20434,6 +21297,8 @@ snapshots:
ignore@7.0.6: {}
+ immer@11.1.15: {}
+
immutable@5.1.9: {}
import-fresh@3.0.0:
@@ -20500,6 +21365,8 @@ snapshots:
ini@4.1.1: {}
+ inline-style-parser@0.2.7: {}
+
inquirer@8.0.0:
dependencies:
ansi-escapes: 4.2.1
@@ -20595,6 +21462,13 @@ snapshots:
is-relative: 1.0.0
is-windows: 1.0.2
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.9
@@ -20641,6 +21515,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
+ is-decimal@2.0.1: {}
+
is-document.all@1.0.0:
dependencies:
call-bound: 1.0.4
@@ -20667,6 +21543,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-hexadecimal@2.0.1: {}
+
is-interactive@1.0.0: {}
is-lower-case@2.0.2:
@@ -20690,6 +21568,8 @@ snapshots:
is-plain-obj@1.1.0: {}
+ is-plain-obj@4.1.0: {}
+
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {}
@@ -21020,6 +21900,8 @@ snapshots:
lines-and-columns@1.2.4: {}
+ linkifyjs@4.3.3: {}
+
lint-staged@15.0.0:
dependencies:
chalk: 5.3.0
@@ -21137,6 +22019,8 @@ snapshots:
long@5.3.2: {}
+ longest-streak@3.1.0: {}
+
loose-envify@1.4.0:
dependencies:
js-tokens: 3.0.0
@@ -21205,8 +22089,163 @@ snapshots:
map-obj@4.3.0: {}
+ markdown-table@3.0.4: {}
+
math-intrinsics@1.1.0: {}
+ mdast-util-find-and-replace@3.0.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ escape-string-regexp: 5.0.0
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-find-and-replace: 3.0.2
+ micromark-util-character: 2.1.1
+
+ mdast-util-gfm-footnote@2.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ micromark-util-normalize-identifier: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-table@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ markdown-table: 3.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm@3.1.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-gfm-autolink-literal: 2.0.1
+ mdast-util-gfm-footnote: 2.1.0
+ mdast-util-gfm-strikethrough: 2.0.0
+ mdast-util-gfm-table: 2.0.0
+ mdast-util-gfm-task-list-item: 2.0.0
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.3
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
media-typer@0.3.0: {}
memfs@3.4.1:
@@ -21240,6 +22279,197 @@ snapshots:
methods@1.1.2: {}
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-footnote@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-table@2.1.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm@3.0.0:
+ dependencies:
+ micromark-extension-gfm-autolink-literal: 2.1.0
+ micromark-extension-gfm-footnote: 2.1.0
+ micromark-extension-gfm-strikethrough: 2.1.0
+ micromark-extension-gfm-table: 2.1.1
+ micromark-extension-gfm-tagfilter: 2.0.0
+ micromark-extension-gfm-task-list-item: 2.1.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
micromatch@4.0.5:
dependencies:
braces: 3.0.3
@@ -21703,6 +22933,8 @@ snapshots:
strip-ansi: 6.0.0
wcwidth: 1.0.1
+ orderedmap@2.1.1: {}
+
os-tmpdir@1.0.2: {}
outvariant@1.4.3: {}
@@ -21750,6 +22982,16 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
parse-filepath@1.0.2:
dependencies:
is-absolute: 1.0.0
@@ -22029,6 +23271,82 @@ snapshots:
dependencies:
mkdirp: 1.0.4
+ property-information@7.2.0: {}
+
+ prosemirror-changeset@2.4.1:
+ dependencies:
+ prosemirror-transform: 1.12.0
+
+ prosemirror-commands@1.7.1:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-dropcursor@1.8.3:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.2
+
+ prosemirror-gapcursor@1.4.1:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-view: 1.42.2
+
+ prosemirror-history@1.5.0:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.2
+ rope-sequence: 1.3.4
+
+ prosemirror-inputrules@1.5.1:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-keymap@1.2.3:
+ dependencies:
+ prosemirror-state: 1.4.4
+ w3c-keyname: 2.2.8
+
+ prosemirror-model@1.25.11:
+ dependencies:
+ orderedmap: 2.1.1
+
+ prosemirror-schema-list@1.5.1:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-state@1.4.4:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.2
+
+ prosemirror-tables@1.8.5:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.2
+
+ prosemirror-transform@1.12.0:
+ dependencies:
+ prosemirror-model: 1.25.11
+
+ prosemirror-view@1.42.2:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
protobufjs@7.6.5:
dependencies:
'@protobufjs/aspromise': 1.1.2
@@ -22125,6 +23443,33 @@ snapshots:
react-is@17.0.1: {}
+ react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@types/react': 19.2.17
+ devlop: 1.1.0
+ hast-util-to-jsx-runtime: 2.3.6
+ html-url-attributes: 3.0.1
+ mdast-util-to-hast: 13.2.1
+ react: 19.2.7
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ redux: 5.0.1
+
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7):
@@ -22245,6 +23590,26 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
+ recharts@3.10.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@17.0.1)(react@19.2.7)(redux@5.0.1):
+ dependencies:
+ '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)
+ clsx: 2.1.1
+ decimal.js-light: 2.5.1
+ es-toolkit: 1.50.0
+ eventemitter3: 5.0.4
+ immer: 11.1.15
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-is: 17.0.1
+ react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
+ reselect: 5.2.0
+ tiny-invariant: 1.3.3
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ victory-vendor: 37.3.6
+ transitivePeerDependencies:
+ - '@types/react'
+ - redux
+
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -22270,6 +23635,12 @@ snapshots:
'@redis/search': 1.2.0(@redis/client@1.6.0)
'@redis/time-series': 1.1.0(@redis/client@1.6.0)
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.10:
@@ -22297,6 +23668,40 @@ snapshots:
'@types/react': 19.2.17
react: 19.2.7
+ remark-gfm@4.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-gfm: 3.1.0
+ micromark-extension-gfm: 3.0.0
+ remark-parse: 11.0.0
+ remark-stringify: 11.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-parse@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ micromark-util-types: 2.0.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-rehype@11.1.2:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ mdast-util-to-hast: 13.2.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ remark-stringify@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-to-markdown: 2.1.2
+ unified: 11.0.5
+
remedial@1.0.8: {}
remove-trailing-separator@1.1.0: {}
@@ -22326,6 +23731,8 @@ snapshots:
requires-port@1.0.0: {}
+ reselect@5.2.0: {}
+
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -22412,6 +23819,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.2
fsevents: 2.3.3
+ rope-sequence@1.3.4: {}
+
rrweb-cssom@0.6.0: {}
rrweb-cssom@0.7.1: {}
@@ -22722,6 +24131,8 @@ snapshots:
source-map@0.7.4: {}
+ space-separated-tokens@2.0.2: {}
+
spdx-correct@3.2.0:
dependencies:
spdx-expression-parse: 3.0.1
@@ -22887,6 +24298,11 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
strip-ansi@5.0.0:
dependencies:
ansi-regex: 4.1.1
@@ -22925,6 +24341,14 @@ snapshots:
dependencies:
'@tokenizer/token': 0.3.0
+ style-to-js@1.1.21:
+ dependencies:
+ style-to-object: 1.0.14
+
+ style-to-object@1.0.14:
+ dependencies:
+ inline-style-parser: 0.2.7
+
styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.0):
dependencies:
client-only: 0.0.1
@@ -22932,14 +24356,6 @@ snapshots:
optionalDependencies:
'@babel/core': 7.29.7
- styled-jsx@5.1.6(@babel/core@7.29.7)(react@18.3.0):
- dependencies:
- client-only: 0.0.1
- react: 18.3.0
- optionalDependencies:
- '@babel/core': 7.29.7
- optional: true
-
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7):
dependencies:
client-only: 0.0.1
@@ -22947,6 +24363,12 @@ snapshots:
optionalDependencies:
'@babel/core': 7.29.7
+ styled-jsx@5.1.6(react@18.3.0):
+ dependencies:
+ client-only: 0.0.1
+ react: 18.3.0
+ optional: true
+
subscriptions-transport-ws@0.11.0(graphql@16.14.2):
dependencies:
backo2: 1.0.2
@@ -23289,8 +24711,12 @@ snapshots:
tree-kill@1.2.2: {}
+ trim-lines@3.0.1: {}
+
trim-newlines@5.0.0: {}
+ trough@2.2.0: {}
+
ts-api-utils@2.5.0(typescript@5.6.2):
dependencies:
typescript: 5.6.2
@@ -23469,6 +24895,39 @@ snapshots:
unicorn-magic@0.1.0: {}
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
universalify@0.2.0: {}
universalify@2.0.1: {}
@@ -23595,6 +25054,16 @@ snapshots:
vary@1.1.2: {}
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
victory-vendor@36.9.2:
dependencies:
'@types/d3-array': 3.2.2
@@ -23612,6 +25081,23 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
vite-node@2.1.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0):
dependencies:
cac: 6.7.14
@@ -23707,6 +25193,8 @@ snapshots:
- supports-color
- terser
+ w3c-keyname@2.2.8: {}
+
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
@@ -24081,15 +25569,19 @@ snapshots:
dependencies:
tslib: 2.8.1
- zustand@4.5.0(@types/react@19.2.17)(react@18.3.0):
+ zustand@4.5.0(@types/react@19.2.17)(immer@11.1.15)(react@18.3.0):
dependencies:
use-sync-external-store: 1.2.0(react@18.3.0)
optionalDependencies:
'@types/react': 19.2.17
+ immer: 11.1.15
react: 18.3.0
- zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)):
+ zustand@5.0.14(@types/react@19.2.17)(immer@11.1.15)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)):
optionalDependencies:
'@types/react': 19.2.17
+ immer: 11.1.15
react: 19.2.7
use-sync-external-store: 1.6.0(react@19.2.7)
+
+ zwitch@2.0.4: {}