/**
* 树形菜单组件
*/
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;
}
}