import { isRecord } from "@/shared/lib/type-guards"; import type { LessonPlanNode, TextbookContentNode } from "../types"; /** * 节点摘要翻译函数接口。 * 调用方传入 next-intl 的 t 函数,避免纯函数直接耦合 i18n 实现。 * values 类型对齐 next-intl 的 TranslationValues(string | number | Date)。 */ export interface NodeSummaryT { ( key: | "editor.questionCount" | "editor.charCount" | "editor.nodeSummaryEmpty" | "editor.itemCount" | "editor.pointCount" | "editor.assignmentCount" | "editor.durationMin", values?: Record, ): 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 = { 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 = 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 }