registry/public/js/tree.js

164 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 树形组件
* 将配置项按“路径映射维度前缀 + 嵌套键”构建成注册表风格的树并渲染
* 说明:仅映射为 path 的维度进入路径label 维度由维度栏筛选,不进入树层级;
* 配置键 key 自身按 / 拆分为多级目录,叶子节点为真正的配置项。
*/
const Tree = {
/**
* 将配置项列表构建为树形数据结构
* 每个节点结构: { name, path, type:"dir"|"leaf", children:{}, config }
* @param {Object} project - 项目对象
* @param {Array} configs - 已按 label 维度筛选后的配置项列表(含 path 字段)
* @returns {Object} 树根节点
*/
buildTree(project, configs) {
const root = { name: project.name, path: "", type: "dir", children: {}, config: null };
configs.forEach((config) => {
// path 已由后端按映射规则生成,直接按 / 拆分为层级
const segments = String(config.path || config.key).split("/").filter((s) => s !== "");
let node = root;
let acc = "";
segments.forEach((seg, idx) => {
acc = acc ? acc + "/" + seg : seg;
const isLast = idx === segments.length - 1;
if (!node.children[seg]) {
node.children[seg] = { name: seg, path: acc, type: "dir", children: {}, config: null };
}
node = node.children[seg];
if (isLast) {
// 叶子节点绑定真实配置项
node.type = "leaf";
node.config = config;
}
});
});
return root;
},
/**
* 渲染树到指定容器
* @param {HTMLElement} container - 容器元素
* @param {Object} project - 项目对象
* @param {Array} configs - 配置项列表
* @param {Function} onSelect - 节点选中回调,参数为(配置列表, 路径, 节点)
*/
render(container, project, configs, onSelect) {
container.innerHTML = "";
if (!project) {
container.innerHTML = '<div class="tree-empty">请先选择一个项目</div>';
return;
}
const tree = this.buildTree(project, configs);
const rootEl = this._renderNode(tree, onSelect, true);
container.appendChild(rootEl);
},
/**
* 收集某节点及其子孙下的全部叶子配置项
* @param {Object} node - 树节点
* @returns {Array} 配置项列表
*/
collectConfigs(node) {
let list = [];
if (node.type === "leaf" && node.config) {
list.push(node.config);
}
Object.values(node.children).forEach((child) => {
list = list.concat(this.collectConfigs(child));
});
return list;
},
/**
* 递归渲染单个树节点
* @param {Object} node - 树节点
* @param {Function} onSelect - 选中回调
* @param {boolean} isRoot - 是否为根节点
* @returns {HTMLElement} 节点元素
*/
_renderNode(node, onSelect, isRoot) {
const nodeEl = document.createElement("div");
nodeEl.className = "tree-node";
const childKeys = Object.keys(node.children);
const hasChildren = childKeys.length > 0;
const isLeaf = node.type === "leaf" && !hasChildren;
const label = document.createElement("div");
label.className = "tree-node-label" + (isLeaf ? " is-leaf" : "");
label.dataset.path = node.path;
const toggle = document.createElement("span");
toggle.className = "tree-toggle";
toggle.textContent = hasChildren ? "▼" : "";
const icon = document.createElement("span");
icon.className = "tree-icon";
icon.textContent = isRoot ? "📦" : isLeaf ? "🔑" : "📁";
const text = document.createElement("span");
text.textContent = node.name;
label.appendChild(toggle);
label.appendChild(icon);
label.appendChild(text);
nodeEl.appendChild(label);
label.addEventListener("click", (e) => {
e.stopPropagation();
document.querySelectorAll(".tree-node-label.selected").forEach((el) => el.classList.remove("selected"));
label.classList.add("selected");
onSelect(this.collectConfigs(node), node.path || "/", node);
});
if (hasChildren) {
const childrenEl = document.createElement("div");
childrenEl.className = "tree-children";
childKeys.sort().forEach((key) => {
childrenEl.appendChild(this._renderNode(node.children[key], onSelect, false));
});
nodeEl.appendChild(childrenEl);
toggle.addEventListener("click", (e) => {
e.stopPropagation();
childrenEl.classList.toggle("collapsed");
toggle.textContent = childrenEl.classList.contains("collapsed") ? "▶" : "▼";
});
}
return nodeEl;
},
/**
* 根据路径展开并选中对应节点(供地址栏跳转使用)
* @param {HTMLElement} container - 树容器
* @param {string} targetPath - 目标路径
* @returns {boolean} 是否命中并选中节点
*/
selectByPath(container, targetPath) {
const normalized = String(targetPath || "").split("/").map((s) => s.trim()).filter((s) => s !== "").join("/");
const labels = container.querySelectorAll(".tree-node-label");
for (const label of labels) {
if (label.dataset.path === normalized) {
// 展开所有祖先节点
let parent = label.parentElement;
while (parent && parent !== container) {
if (parent.classList.contains("tree-children")) {
parent.classList.remove("collapsed");
const pToggle = parent.previousElementSibling?.querySelector(".tree-toggle");
if (pToggle && pToggle.textContent === "▶") pToggle.textContent = "▼";
}
parent = parent.parentElement;
}
label.click();
label.scrollIntoView({ block: "center" });
return true;
}
}
return false;
},
};