feat(复习): 节点选择交互 + 移除 session 维度
- MindMapViewer 新增 selectable 模式,点击节点 emit node-select 事件 - ReviewRecall.vue 移除会话选择器,改为节点选择交互 - 支持 URL ?focusPath= 参数自动预设复习起点 - 节点选择弹窗确认后回忆导图根节点=选中节点标题 - ReviewDetail 和 Welcome 中调用 findNode API 匹配节点后跳转 - API: recallCompare 使用 focusPath 替代 sessionNum - API: 新增 findNode 端点
This commit is contained in:
@@ -25,6 +25,10 @@ const props = defineProps<{
|
||||
colorByCompare?: boolean;
|
||||
/** 画布高度,如 "520px"、"60vh",默认 480px */
|
||||
height?: string;
|
||||
/** 节点可选择模式(点击节点触发 node-select 事件) */
|
||||
selectable?: boolean;
|
||||
/** 当前选中的节点路径(用于高亮,仅在 selectable 模式下生效) */
|
||||
selectedPath?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -32,11 +36,27 @@ const emit = defineEmits<{
|
||||
(e: "change", outline: string): void;
|
||||
/** 点击带溯源信息的节点 */
|
||||
(e: "node-click", node: { sourceType: string; sourceId: number }): void;
|
||||
/** 选择模式下点击节点,返回标题+路径 */
|
||||
(e: "node-select", node: { title: string; path: string; childCount: number }): void;
|
||||
}>();
|
||||
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
let instance: MindElixirInstance | null = null;
|
||||
let nodeCounter = 0;
|
||||
/** 节点路径映射:elixir node id → { title, path, childCount } */
|
||||
let nodePathMap: Record<string, { title: string; path: string; childCount: number }> = {};
|
||||
|
||||
/** 递归构建节点路径映射 */
|
||||
const buildPathMap = (node: MindMapTreeNode, ancestors: string[]): { title: string; path: string; childCount: number } => {
|
||||
const title = node.title || "(未命名)";
|
||||
const path = [...ancestors, title].join(" / ");
|
||||
const childCount = (node.children || []).reduce((sum, c) => sum + 1 + countAllDescendants(c), 0);
|
||||
return { title, path, childCount };
|
||||
};
|
||||
|
||||
const countAllDescendants = (node: MindMapTreeNode): number => {
|
||||
return (node.children || []).reduce((sum, c) => sum + 1 + countAllDescendants(c), 0);
|
||||
};
|
||||
|
||||
const COLOR_MATCHED = { background: "#c8e6c9", color: "#1b5e20" };
|
||||
const COLOR_MISSED = { background: "#ffcdd2", color: "#b71c1c" };
|
||||
@@ -48,15 +68,20 @@ const compareStatus = (notes?: string): "MATCHED" | "MISSED" | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const toElixirNode = (node: MindMapTreeNode): any => {
|
||||
const toElixirNode = (node: MindMapTreeNode, ancestors: string[] = []): any => {
|
||||
nodeCounter += 1;
|
||||
const elixirId = `n${nodeCounter}`;
|
||||
const result: any = {
|
||||
id: `n${nodeCounter}`,
|
||||
id: elixirId,
|
||||
topic: node.title || "(未命名)",
|
||||
children: (node.children || []).map(toElixirNode),
|
||||
children: (node.children || []).map((c) => toElixirNode(c, [...ancestors, node.title || "(未命名)"])),
|
||||
};
|
||||
// 记录路径映射
|
||||
if (props.selectable) {
|
||||
nodePathMap[elixirId] = buildPathMap(node, ancestors);
|
||||
}
|
||||
if (node.sourceType && node.sourceId != null) {
|
||||
result.hyperLink = ""; // 占位,溯源通过 dangerouslySetInnerHTML 外的 tags 展示
|
||||
result.hyperLink = "";
|
||||
result.tags = [node.sourceType === "REPORT" ? "报告" : node.sourceType === "FRAGMENT" ? "残片" : "应用"];
|
||||
result.sourceType = node.sourceType;
|
||||
result.sourceId = node.sourceId;
|
||||
@@ -104,6 +129,7 @@ const render = () => {
|
||||
instance.destroy();
|
||||
instance = null;
|
||||
}
|
||||
nodePathMap = {};
|
||||
instance = new MindElixir({
|
||||
el: container.value,
|
||||
direction: MindElixir.SIDE,
|
||||
@@ -122,6 +148,13 @@ const render = () => {
|
||||
}
|
||||
|
||||
instance.bus.addListener("selectNewNode", (node: any) => {
|
||||
if (props.selectable && node?.id) {
|
||||
const info = nodePathMap[node.id];
|
||||
if (info) {
|
||||
emit("node-select", info);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (node?.sourceType && node?.sourceId != null) {
|
||||
emit("node-click", { sourceType: String(node.sourceType), sourceId: Number(node.sourceId) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user