feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff

- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes

- Add corresponding data-access layers for each new action module

- Add calendar-view, curriculum-map-view, version-diff-viewer components

- Add editor-slice, selection-slice, version-slice hooks for state management

- Add document-diff and scope-check lib utilities

- Add default-question-service and external-questions-bridge services
This commit is contained in:
SpecialX
2026-07-03 10:25:21 +08:00
parent a16f09d3c3
commit 20023e13fd
75 changed files with 5131 additions and 1186 deletions

View File

@@ -1,3 +1,4 @@
import { isRecord } from "@/shared/lib/type-guards";
import type { LessonPlanNode, TextbookContentNode } from "../types";
/**
@@ -19,79 +20,81 @@ export interface NodeSummaryT {
): string;
}
// ---- 安全字段提取辅助(替代 as 断言,从 unknown 收窄)----
function getArrayLength(v: unknown): number | undefined {
return Array.isArray(v) ? v.length : undefined;
}
function getString(v: unknown): string | undefined {
return typeof v === "string" ? v : undefined;
}
function getNumber(v: unknown): number | undefined {
return typeof v === "number" ? v : undefined;
}
/**
* 纯函数:获取节点摘要文本(用于节点卡片显示)。
* 从 lesson-node.tsx 抽取,便于单元测试。
* 翻译文本由调用方通过 t 函数注入,保证纯函数可测性。
*
* P1 修复:使用类型守卫从 unknown 安全收窄 node.data 字段,
* 替代原先的 `as { html?: string; ... }` 断言。
*/
export function getNodeSummary(node: LessonPlanNode, t: NodeSummaryT): string {
const data = node.data as {
// 富文本类
html?: string;
// 文本研习
sourceText?: string;
annotations?: unknown[];
// 练习
items?: unknown[];
// 教学目标
objectives?: unknown[];
// 重难点
keyPoints?: unknown[];
// 导入
durationMin?: number;
prompt?: string;
// 新授
teachingPoints?: unknown[];
// 小结
summaryPoints?: unknown[];
// 作业
assignments?: unknown[];
// 板书
content?: string;
// 反思
reflection?: unknown[];
// 知识点
knowledgePointIds?: string[];
};
// node.data 是 BlockData 联合类型,这里安全地作为 unknown 读取可选字段
const data: unknown = node.data;
// 按类型优先级提取摘要
if (data.items !== undefined) {
return t("editor.questionCount", { count: data.items.length });
// 按类型优先级提取摘要(使用安全字段提取,避免 as 断言)
const itemsLen = isRecord(data) ? getArrayLength(data.items) : undefined;
if (itemsLen !== undefined) {
return t("editor.questionCount", { count: itemsLen });
}
if (data.objectives !== undefined) {
return t("editor.itemCount", { count: data.objectives.length });
const objectivesLen = isRecord(data) ? getArrayLength(data.objectives) : undefined;
if (objectivesLen !== undefined) {
return t("editor.itemCount", { count: objectivesLen });
}
if (data.keyPoints !== undefined) {
return t("editor.itemCount", { count: data.keyPoints.length });
const keyPointsLen = isRecord(data) ? getArrayLength(data.keyPoints) : undefined;
if (keyPointsLen !== undefined) {
return t("editor.itemCount", { count: keyPointsLen });
}
if (data.teachingPoints !== undefined) {
return t("editor.pointCount", { count: data.teachingPoints.length });
const teachingPointsLen = isRecord(data) ? getArrayLength(data.teachingPoints) : undefined;
if (teachingPointsLen !== undefined) {
return t("editor.pointCount", { count: teachingPointsLen });
}
if (data.summaryPoints !== undefined) {
return t("editor.itemCount", { count: data.summaryPoints.length });
const summaryPointsLen = isRecord(data) ? getArrayLength(data.summaryPoints) : undefined;
if (summaryPointsLen !== undefined) {
return t("editor.itemCount", { count: summaryPointsLen });
}
if (data.assignments !== undefined) {
return t("editor.assignmentCount", { count: data.assignments.length });
const assignmentsLen = isRecord(data) ? getArrayLength(data.assignments) : undefined;
if (assignmentsLen !== undefined) {
return t("editor.assignmentCount", { count: assignmentsLen });
}
if (data.reflection !== undefined) {
return t("editor.itemCount", { count: data.reflection.length });
const reflectionLen = isRecord(data) ? getArrayLength(data.reflection) : undefined;
if (reflectionLen !== undefined) {
return t("editor.itemCount", { count: reflectionLen });
}
if (data.durationMin !== undefined) {
return t("editor.durationMin", { count: data.durationMin });
const durationMin = isRecord(data) ? getNumber(data.durationMin) : undefined;
if (durationMin !== undefined) {
return t("editor.durationMin", { count: durationMin });
}
if (data.annotations !== undefined && data.sourceText !== undefined) {
return t("editor.charCount", { count: data.sourceText.length });
const sourceText = isRecord(data) ? getString(data.sourceText) : undefined;
const hasAnnotations = isRecord(data) ? Array.isArray(data.annotations) : false;
if (hasAnnotations && sourceText !== undefined) {
return t("editor.charCount", { count: sourceText.length });
}
if (data.sourceText !== undefined && data.sourceText) {
return t("editor.charCount", { count: data.sourceText.length });
if (sourceText) {
return t("editor.charCount", { count: sourceText.length });
}
if (data.content !== undefined && data.content) {
const text = data.content.replace(/<[^>]+>/g, "").trim();
const content = isRecord(data) ? getString(data.content) : undefined;
if (content) {
const text = content.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
if (data.html) {
const html = isRecord(data) ? getString(data.html) : undefined;
if (html) {
// 去标签后取前 40 字
const text = data.html.replace(/<[^>]+>/g, "").trim();
const text = html.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
return t("editor.nodeSummaryEmpty");
@@ -109,25 +112,40 @@ export function getTextbookContentSummary(
}
/**
* 节点类型 → 图标颜色Material Design 色板)。
* 节点类型 → CSS 变量名V4 P1-4 修复:从硬编码 hex 提取到 globals.css 设计令牌)。
* 供 lesson-node 和 minimap 复用。
*/
export const NODE_COLORS: Record<string, string> = {
objective: "#4caf50",
key_point: "#f44336",
import: "#2196f3",
new_teaching: "#9c27b0",
consolidation: "#ff9800",
summary: "#607d8b",
homework: "#795548",
blackboard: "#009688",
text_study: "#3f51b5",
exercise: "#e91e63",
rich_text: "#9e9e9e",
reflection: "#cddc39",
textbook_content: "#455a64",
};
export const NODE_COLOR_VARS: Record<string, string> = {
objective: "var(--lesson-node-objective)",
key_point: "var(--lesson-node-key-point)",
import: "var(--lesson-node-import)",
new_teaching: "var(--lesson-node-new-teaching)",
consolidation: "var(--lesson-node-consolidation)",
summary: "var(--lesson-node-summary)",
homework: "var(--lesson-node-homework)",
blackboard: "var(--lesson-node-blackboard)",
text_study: "var(--lesson-node-text-study)",
exercise: "var(--lesson-node-exercise)",
rich_text: "var(--lesson-node-rich-text)",
reflection: "var(--lesson-node-reflection)",
textbook_content: "var(--lesson-node-textbook-content)",
}
/** 选中态颜色变量 */
export const NODE_SELECTED_COLOR_VAR = "var(--lesson-node-selected)"
/** 默认颜色变量(未匹配类型时使用) */
export const NODE_DEFAULT_COLOR_VAR = "var(--lesson-node-default)"
/**
* @deprecated 使用 `getNodeColorVar` 替代。保留是为了向后兼容旧代码引用。
*/
export const NODE_COLORS: Record<string, string> = NODE_COLOR_VARS
export function getNodeColor(type: string): string {
return NODE_COLORS[type] ?? "#9e9e9e";
return NODE_COLOR_VARS[type] ?? NODE_DEFAULT_COLOR_VAR
}
export function getNodeColorVar(type: string): string {
return NODE_COLOR_VARS[type] ?? NODE_DEFAULT_COLOR_VAR
}