- 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
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
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, 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
|
||
}
|