| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /**
- * 树形菜单组件
- */
- 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 = '<div class="empty-state">暂无数据</div>';
- 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 `
- <div class="tree-node level-${node.level}" data-node-id="${node.id}">
- <div class="tree-node-content ${isSelected ? 'active' : ''}"
- data-node-content="${node.id}">
- <span class="${iconClass}"
- data-node-toggle="${node.id}"
- ${!hasChildren ? 'style="cursor: default;"' : ''}></span>
- <span class="tree-node-label" data-node-label="${node.id}">
- ${this.getNodeIcon(node.type)} ${this.escapeHtml(node.label)}
- </span>
- ${node.badge ? `
- <span class="tree-node-badge ${node.badgeClass || ''}">${node.badge}</span>
- ` : ''}
- </div>
- ${hasChildren ? `
- <div class="tree-node-children ${isExpanded ? 'expanded' : ''}"
- data-node-children="${node.id}">
- ${node.children.map(child => this.renderNode(child)).join('')}
- </div>
- ` : ''}
- </div>
- `;
- }
- /**
- * 绑定事件
- */
- 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;
- }
- }
|