feat(lesson-preparation): 左栏结构树容器

This commit is contained in:
SpecialX
2026-07-04 11:31:03 +08:00
parent c7cbc86fd4
commit 05f9b4fd9d

View File

@@ -0,0 +1,126 @@
"use client";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor";
import { TreeNodeRow } from "./tree-node-row";
import type { Block, InteractionBlockData, LessonPlanNode, TextbookContentNode } from "../../types";
export function StructureTree() {
const t = useTranslations("lessonPreparation");
const doc = useLessonPlanEditor((s) => s.doc);
const expandedNodeIds = useLessonPlanEditor((s) => s.expandedNodeIds);
const addNode = useLessonPlanEditor((s) => s.addNode);
const [expandedTreeNodes, setExpandedTreeNodes] = useState<Set<string>>(new Set());
// V1默认添加 rich_text 节点V2 接入节点类型选择对话框)
const onAddNode = () => {
const id = addNode("rich_text");
// 自动选中新节点addNode 内部已设置 selectedNodeId
void id;
};
const textbookNode = doc.nodes.find((n): n is TextbookContentNode => n.type === "textbook_content");
const teachingNodes = useMemo(
() =>
doc.nodes
.filter((n): n is LessonPlanNode => n.type !== "textbook_content")
.sort((a, b) => a.order - b.order),
[doc.nodes],
);
const toggleTreeNode = (id: string) => {
setExpandedTreeNodes((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<aside
style={{
borderRight: "1px solid var(--border)",
background: "var(--background)",
padding: "16px 12px",
overflowY: "auto",
}}
>
<div
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--muted-foreground)",
padding: "0 8px 12px",
display: "flex",
justifyContent: "space-between",
}}
>
<span>{t("v4.tree.title")}</span>
<span style={{ fontWeight: 500, letterSpacing: 0 }}>
{teachingNodes.length} {t("v4.tree.countSuffix")}
</span>
</div>
<div style={{ fontSize: 13 }}>
{/* 正文节点(始终在最上) */}
{textbookNode && (
<TreeNodeRow
node={{ ...textbookNode, type: "textbook_content" } as unknown as Block}
isExpanded={false}
/>
)}
{/* 教学节点 */}
{teachingNodes.map((node) => {
const isExpanded = expandedNodeIds.includes(node.id);
const isTreeNodeExpanded = expandedTreeNodes.has(node.id);
// 师生交互节点:子节点为对话轮次
let childItems: { id: string; label: string; kind: string }[] = [];
let hasChildren = false;
if (node.type === "interaction") {
const data = node.data as InteractionBlockData;
hasChildren = (data.turns?.length ?? 0) > 0;
childItems = (data.turns ?? []).map((turn, idx) => ({
id: turn.id,
label: `${t("v4.interaction.round", { n: idx + 1 })} · ${turn.role === "teacher" ? t("v4.interaction.roleTeacher") : t("v4.interaction.roleStudent")}`,
kind: turn.role === "teacher" ? t("v4.interaction.roleTeacher") : t("v4.interaction.roleStudent"),
}));
}
return (
<TreeNodeRow
key={node.id}
node={node}
isExpanded={isExpanded}
hasChildren={hasChildren}
isChildrenExpanded={isTreeNodeExpanded}
onToggleChildren={() => toggleTreeNode(node.id)}
children={childItems}
/>
);
})}
</div>
<div
style={{
marginTop: 10,
padding: "7px 8px",
border: "1px dashed var(--border)",
borderRadius: 5,
textAlign: "center",
fontSize: 12,
color: "var(--muted-foreground)",
cursor: "pointer",
}}
onClick={onAddNode}
>
+ {t("v4.tree.addNode")}
</div>
</aside>
);
}