127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
"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)}
|
||
childItems={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>
|
||
);
|
||
}
|