Files
NextEdu/src/modules/lesson-preparation/lib/node-summary.ts
SpecialX 20023e13fd 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
2026-07-03 10:25:21 +08:00

152 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isRecord } from "@/shared/lib/type-guards";
import type { LessonPlanNode, TextbookContentNode } from "../types";
/**
* 节点摘要翻译函数接口。
* 调用方传入 next-intl 的 t 函数,避免纯函数直接耦合 i18n 实现。
* values 类型对齐 next-intl 的 TranslationValuesstring | number | Date
*/
export interface NodeSummaryT {
(
key:
| "editor.questionCount"
| "editor.charCount"
| "editor.nodeSummaryEmpty"
| "editor.itemCount"
| "editor.pointCount"
| "editor.assignmentCount"
| "editor.durationMin",
values?: Record<string, string | number | Date>,
): 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 {
// node.data 是 BlockData 联合类型,这里安全地作为 unknown 读取可选字段
const data: unknown = node.data;
// 按类型优先级提取摘要(使用安全字段提取,避免 as 断言)
const itemsLen = isRecord(data) ? getArrayLength(data.items) : undefined;
if (itemsLen !== undefined) {
return t("editor.questionCount", { count: itemsLen });
}
const objectivesLen = isRecord(data) ? getArrayLength(data.objectives) : undefined;
if (objectivesLen !== undefined) {
return t("editor.itemCount", { count: objectivesLen });
}
const keyPointsLen = isRecord(data) ? getArrayLength(data.keyPoints) : undefined;
if (keyPointsLen !== undefined) {
return t("editor.itemCount", { count: keyPointsLen });
}
const teachingPointsLen = isRecord(data) ? getArrayLength(data.teachingPoints) : undefined;
if (teachingPointsLen !== undefined) {
return t("editor.pointCount", { count: teachingPointsLen });
}
const summaryPointsLen = isRecord(data) ? getArrayLength(data.summaryPoints) : undefined;
if (summaryPointsLen !== undefined) {
return t("editor.itemCount", { count: summaryPointsLen });
}
const assignmentsLen = isRecord(data) ? getArrayLength(data.assignments) : undefined;
if (assignmentsLen !== undefined) {
return t("editor.assignmentCount", { count: assignmentsLen });
}
const reflectionLen = isRecord(data) ? getArrayLength(data.reflection) : undefined;
if (reflectionLen !== undefined) {
return t("editor.itemCount", { count: reflectionLen });
}
const durationMin = isRecord(data) ? getNumber(data.durationMin) : undefined;
if (durationMin !== undefined) {
return t("editor.durationMin", { count: durationMin });
}
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 (sourceText) {
return t("editor.charCount", { count: sourceText.length });
}
const content = isRecord(data) ? getString(data.content) : undefined;
if (content) {
const text = content.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
const html = isRecord(data) ? getString(data.html) : undefined;
if (html) {
// 去标签后取前 40 字
const text = html.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
return t("editor.nodeSummaryEmpty");
}
/**
* 纯函数:获取正文节点摘要。
*/
export function getTextbookContentSummary(
node: TextbookContentNode,
t: NodeSummaryT,
): string {
if (!node.data.content) return t("editor.nodeSummaryEmpty");
return t("editor.charCount", { count: node.data.content.length });
}
/**
* 节点类型 → CSS 变量名V4 P1-4 修复:从硬编码 hex 提取到 globals.css 设计令牌)。
* 供 lesson-node 和 minimap 复用。
*/
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_COLOR_VARS[type] ?? NODE_DEFAULT_COLOR_VAR
}
export function getNodeColorVar(type: string): string {
return NODE_COLOR_VARS[type] ?? NODE_DEFAULT_COLOR_VAR
}