"use client";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { Tag, Eye, Pencil } from "lucide-react";
import type { BlackboardBlockData } from "../../types";
import { isBlackboardLayout } from "../../lib/type-guards";
import { KnowledgePointPicker } from "../knowledge-point-picker";
import { LessonPlanErrorBoundary } from "../lesson-plan-error-boundary";
interface Props {
data: BlackboardBlockData;
textbookId?: string;
chapterId?: string;
onUpdate: (data: BlackboardBlockData) => void;
}
const LAYOUTS: BlackboardBlockData["layout"][] = ["structure", "mindmap", "text"];
/**
* V5-14 F4:板书可视化工具。
*
* 在原有纯文本编辑基础上增加轻量级可视化预览(不引入新库):
* - structure(结构式):按行解析缩进,渲染为带连接线的层级树
* - mindmap(思维导图):第一行为中心,其余为分支节点
* - text(文字式):等宽字体直接展示
*
* 编辑/预览模式切换,避免双栏占满侧边面板。
*/
export function BlackboardBlock({ data, textbookId, chapterId, onUpdate }: Props) {
const t = useTranslations("lessonPreparation");
const [showKpPicker, setShowKpPicker] = useState(false);
const [previewMode, setPreviewMode] = useState(false);
return (
{t("blackboard.hint")}
{/* 编辑/预览切换 */}
{/* 内容区:编辑模式 or 预览模式 */}
{previewMode ? (
) : (
{/* arbitrary-value: textarea min height */}
)}
{data.knowledgePointIds.length > 0 && (
{t("knowledgePoint.linked", { count: data.knowledgePointIds.length })}
)}
{showKpPicker && (
onUpdate({ ...data, knowledgePointIds: ids })}
onClose={() => setShowKpPicker(false)}
/>
)}
);
}
/**
* 板书可视化预览。纯 CSS + 文本解析,不引入新库。
*
* - structure:按行解析前导空格/Tab 缩进,渲染为带竖线的层级树
* - mindmap:第一行为中心节点,其余为放射分支
* - text:等宽字体直接展示
*/
function BlackboardPreview({
content,
layout,
t,
}: {
content: string;
layout: BlackboardBlockData["layout"];
t: ReturnType;
}) {
const parsed = useMemo(() => parseContent(content, layout), [content, layout]);
if (!content.trim()) {
return (
{t("blackboard.previewEmpty")}
);
}
if (layout === "text") {
return (
// arbitrary-value: content min height
{content}
);
}
if (layout === "mindmap") {
const center = parsed[0]?.text ?? "";
const branches = parsed.slice(1);
return (
// arbitrary-value: content min height
{/* 中心节点 */}
{center || t("blackboard.untitled")}
{/* 分支节点 */}
{branches.length > 0 && (
{branches.map((node, idx) => (
-
└
{node.text}
))}
)}
);
}
// structure:层级树
return (
// arbitrary-value: content min height
{parsed.map((node, idx) => (
-
{node.level === 0 ? "●" : "├"}
{node.text}
))}
);
}
/** 解析文本为节点列表(按缩进识别层级) */
interface ParsedNode {
level: number;
text: string;
}
function parseContent(
content: string,
_layout: BlackboardBlockData["layout"],
): ParsedNode[] {
const lines = content.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.trim());
return lines.map((line) => {
// 前导空格(每 2 空格或 1 Tab 算一级)
const leadingMatch = line.match(/^[\t ]*/);
const leading = leadingMatch ? leadingMatch[0] : "";
const tabs = (leading.match(/\t/g) ?? []).length;
const spaces = (leading.match(/ /g) ?? []).length;
const level = tabs + spaces;
return { level, text: line.trim() };
});
}