/** * 树形菜单组件 */ class TreeView { constructor(container, data) { this.container = container; this.rawData = data; this.treeData = []; this.expandedNodes = new Set(); this.selectedNode = null; this.onNodeClick = null; this.buildTree(); this.render(); } /** * 构建树形数据结构 */ buildTree() { this.treeData = this.rawData.map((result, resultIdx) => { // 第1层:结果项 const matchInfo = result.最高匹配信息 || {}; const similarity = matchInfo.相似度 || matchInfo.Jaccard相似度 || 0; const similarityScore = (similarity * 100).toFixed(1); const personaName = matchInfo.人设特征名称 || ''; const resultNode = { id: `result-${resultIdx}`, level: 1, type: 'result', label: personaName ? `${result.原始特征名称} → ${personaName} | 相似度: ${similarityScore}%` : result.原始特征名称, data: result, children: [] }; // 第2层:关联分类 (result.找到的关联 || []).forEach((assoc, assocIdx) => { // 对特征列表去重:同名特征只保留有搜索结果的 const featureMap = new Map(); (assoc.特征列表 || []).forEach(feature => { const name = feature.特征名称; const hasResult = !!feature.search_result; if (!featureMap.has(name)) { // 首次出现,直接添加 featureMap.set(name, feature); } else { // 已存在同名特征 const existing = featureMap.get(name); const existingHasResult = !!existing.search_result; // 优先保留有搜索结果的 if (hasResult && !existingHasResult) { featureMap.set(name, feature); } // 如果当前和已存在的都有结果,保留第一个 // 如果都没有结果,也保留第一个 } }); const uniqueFeatures = Array.from(featureMap.values()); const jaccardScore = (assoc['Jaccard相似度'] * 100).toFixed(1); const assocNode = { id: `assoc-${resultIdx}-${assocIdx}`, level: 2, type: 'association', label: assoc.目标分类路径, badge: `${uniqueFeatures.length}个特征 | Jaccard: ${jaccardScore}%`, data: assoc, parent: resultNode, children: [] }; // 第3层:特征列表(使用去重后的) uniqueFeatures.forEach((feature, featureIdx) => { const hasResult = !!feature.search_result; const status = feature.search_metadata?.status || 'pending'; const featureNode = { id: `feature-${resultIdx}-${assocIdx}-${featureIdx}`, level: 3, type: 'feature', label: feature.特征名称, searchWord: feature.search_word, badge: hasResult ? `${this.getNoteCount(feature)}条` : '待搜索', badgeClass: `status-${status}`, data: feature, parent: assocNode, children: [] }; // 第4层:显示所有帖子 if (hasResult && feature.search_result) { const notes = this.getNotes(feature); notes.forEach((note, noteIdx) => { const noteCard = note.note_card || note; const noteNode = { id: `note-${resultIdx}-${assocIdx}-${featureIdx}-${noteIdx}`, level: 4, type: 'note', label: noteCard.display_title || '无标题', data: note, parent: featureNode }; featureNode.children.push(noteNode); }); } assocNode.children.push(featureNode); }); resultNode.children.push(assocNode); }); return resultNode; }); } /** * 渲染树形菜单 */ render() { if (this.treeData.length === 0) { this.container.innerHTML = '
暂无数据
'; return; } const html = this.treeData.map(node => this.renderNode(node)).join(''); this.container.innerHTML = html; this.bindEvents(); } /** * 渲染单个节点 * @param {Object} node - 节点数据 * @returns {string} HTML字符串 */ renderNode(node) { const hasChildren = node.children && node.children.length > 0; const isExpanded = this.expandedNodes.has(node.id); const isSelected = this.selectedNode === node.id; let iconClass = 'tree-node-icon'; if (hasChildren) { iconClass += isExpanded ? ' expanded' : ' collapsed'; } else { iconClass += ' no-children'; } return `
${this.getNodeIcon(node.type)} ${this.escapeHtml(node.label)} ${node.badge ? ` ${node.badge} ` : ''}
${hasChildren ? `
${node.children.map(child => this.renderNode(child)).join('')}
` : ''}
`; } /** * 绑定事件 */ bindEvents() { // 展开/折叠 this.container.querySelectorAll('[data-node-toggle]').forEach(toggle => { toggle.addEventListener('click', (e) => { e.stopPropagation(); const nodeId = toggle.getAttribute('data-node-toggle'); this.toggleNode(nodeId); }); }); // 节点点击 this.container.querySelectorAll('[data-node-content]').forEach(content => { content.addEventListener('click', () => { const nodeId = content.getAttribute('data-node-content'); this.selectNode(nodeId); }); }); } /** * 展开/折叠节点 * @param {string} nodeId - 节点ID */ toggleNode(nodeId) { if (this.expandedNodes.has(nodeId)) { this.expandedNodes.delete(nodeId); } else { this.expandedNodes.add(nodeId); } this.render(); } /** * 选中节点 * @param {string} nodeId - 节点ID */ selectNode(nodeId) { this.selectedNode = nodeId; const node = this.findNodeById(nodeId); // 更新UI this.container.querySelectorAll('.tree-node-content').forEach(content => { content.classList.remove('active'); }); const selectedContent = this.container.querySelector(`[data-node-content="${nodeId}"]`); if (selectedContent) { selectedContent.classList.add('active'); } // 触发回调 if (this.onNodeClick && node) { this.onNodeClick(node); } } /** * 通过ID查找节点 * @param {string} nodeId - 节点ID * @returns {Object|null} 节点对象 */ findNodeById(nodeId) { const search = (nodes) => { for (const node of nodes) { if (node.id === nodeId) return node; if (node.children) { const found = search(node.children); if (found) return found; } } return null; }; return search(this.treeData); } /** * 全部折叠 */ collapseAll() { this.expandedNodes.clear(); this.render(); } /** * 展开所有第一层节点 */ expandFirstLevel() { this.expandedNodes.clear(); this.treeData.forEach(node => { this.expandedNodes.add(node.id); }); this.render(); } /** * 搜索节点 * @param {string} keyword - 搜索关键词 */ search(keyword) { if (!keyword) { this.render(); return; } const lowerKeyword = keyword.toLowerCase(); const matchedNodes = new Set(); const searchNodes = (nodes) => { for (const node of nodes) { if (node.label.toLowerCase().includes(lowerKeyword) || (node.searchWord && node.searchWord.toLowerCase().includes(lowerKeyword))) { matchedNodes.add(node.id); // 展开父节点 let parent = node.parent; while (parent) { this.expandedNodes.add(parent.id); matchedNodes.add(parent.id); parent = parent.parent; } } if (node.children) { searchNodes(node.children); } } }; searchNodes(this.treeData); // 过滤显示 this.filterNodes(matchedNodes); } /** * 过滤节点显示 * @param {Set} matchedNodes - 匹配的节点ID集合 */ filterNodes(matchedNodes) { this.container.querySelectorAll('.tree-node').forEach(nodeEl => { const nodeId = nodeEl.getAttribute('data-node-id'); nodeEl.style.display = matchedNodes.has(nodeId) ? '' : 'none'; }); } /** * 获取节点图标 * @param {string} type - 节点类型 * @returns {string} 图标 */ getNodeIcon(type) { const icons = { result: '📂', association: '📁', feature: '🔍', note: '📄' }; return icons[type] || '•'; } /** * 获取帖子数量 * @param {Object} feature - 特征数据 * @returns {number} 帖子数量 */ getNoteCount(feature) { if (feature.search_metadata) { return feature.search_metadata.note_count || 0; } return this.getNotes(feature).length; } /** * 获取帖子列表 * @param {Object} feature - 特征数据 * @returns {Array} 帖子数组 */ getNotes(feature) { if (!feature.search_result) return []; return feature.search_result.data?.data || []; } /** * HTML 转义 * @param {string} text - 原始文本 * @returns {string} 转义后的文本 */ escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }