| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- /**
- * 主应用逻辑
- */
- class App {
- constructor() {
- this.data = null;
- this.treeView = null;
- this.currentFeature = null;
- this.init();
- }
- /**
- * 初始化应用
- */
- async init() {
- try {
- await this.loadData();
- this.initComponents();
- this.bindEvents();
- this.updateStats();
- } catch (error) {
- console.error('初始化失败:', error);
- this.showError('加载失败,请刷新页面重试');
- }
- }
- /**
- * 加载数据
- */
- async loadData() {
- try {
- const response = await fetch('data/data.json');
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- this.data = await response.json();
- } catch (error) {
- console.error('加载数据失败:', error);
- throw error;
- }
- }
- /**
- * 初始化组件
- */
- initComponents() {
- // 初始化树形菜单
- const treeContainer = document.getElementById('tree-container');
- this.treeView = new TreeView(treeContainer, this.data);
- // 设置节点点击回调
- this.treeView.onNodeClick = (node) => {
- this.handleNodeClick(node);
- };
- // 默认展开第一层
- this.treeView.expandFirstLevel();
- }
- /**
- * 绑定事件
- */
- bindEvents() {
- // 搜索
- const searchInput = document.getElementById('search-input');
- const searchBtn = document.getElementById('search-btn');
- let searchTimer = null;
- searchInput?.addEventListener('input', (e) => {
- clearTimeout(searchTimer);
- searchTimer = setTimeout(() => {
- this.treeView.search(e.target.value);
- }, 300);
- });
- searchBtn?.addEventListener('click', () => {
- this.treeView.search(searchInput.value);
- });
- // 全部折叠
- const collapseBtn = document.getElementById('collapse-all');
- collapseBtn?.addEventListener('click', () => {
- this.treeView.collapseAll();
- });
- // 状态筛选
- const filterStatus = document.getElementById('filter-status');
- filterStatus?.addEventListener('change', () => {
- this.filterByStatus(filterStatus.value);
- });
- // 排序
- const sortBy = document.getElementById('sort-by');
- sortBy?.addEventListener('change', () => {
- this.sortData(sortBy.value);
- });
- // 主题切换
- const themeToggle = document.getElementById('theme-toggle');
- themeToggle?.addEventListener('click', () => {
- this.toggleTheme();
- });
- }
- /**
- * 处理节点点击
- * @param {Object} node - 节点数据
- */
- handleNodeClick(node) {
- const panelTitle = document.getElementById('panel-title');
- const panelInfo = document.getElementById('panel-info');
- const cardsContainer = document.getElementById('cards-container');
- if (node.type === 'feature' && node.data) {
- // 点击特征节点,显示搜索结果
- this.currentFeature = node.data;
- // 更新标题
- panelTitle.textContent = `"${node.label}" 的搜索结果`;
- // 构建详细的搜索信息
- const metadata = node.data.search_metadata;
- if (metadata && metadata.status === 'success') {
- // 有搜索结果,显示完整参数
- const params = metadata.search_params || {};
- panelInfo.innerHTML = `
- <div style="display: flex; gap: 2rem; flex-wrap: wrap;">
- <span><strong>搜索词:</strong> ${params.keyword || '未知'}</span>
- <span><strong>内容类型:</strong> ${params.content_type || '未知'}</span>
- <span><strong>排序方式:</strong> ${params.sort_type || '未知'}</span>
- <span><strong>结果:</strong> <span style="color: var(--secondary-color); font-weight: 600;">${metadata.note_count || 0} 条帖子</span></span>
- </div>
- `;
- } else if (metadata && metadata.status === 'failed') {
- // 搜索失败
- panelInfo.innerHTML = `
- <span style="color: var(--danger-color);">搜索失败</span>
- `;
- } else if (node.searchWord === null) {
- // search_word 为 null(重复被过滤)
- panelInfo.innerHTML = `
- <span style="color: var(--warning-color);">该特征的搜索词与其他特征重复,已被过滤</span>
- `;
- } else {
- // 待搜索
- panelInfo.innerHTML = `
- <span style="color: var(--text-secondary);">待搜索</span>
- ${node.searchWord ? ` - 搜索词: ${node.searchWord}` : ''}
- `;
- }
- // 渲染帖子卡片
- if (node.data.search_result) {
- const notes = node.data.search_result.data?.data || [];
- // 存储到全局变量供 CardRenderer 使用
- window.__currentNotes = notes;
- CardRenderer.renderNotes(notes, cardsContainer);
- } else {
- cardsContainer.innerHTML = `
- <div class="empty-state">
- <div class="empty-icon">⏳</div>
- <p class="empty-text">该特征暂无搜索结果</p>
- </div>
- `;
- }
- } else if (node.type === 'association') {
- // 点击关联分类,显示特征列表
- const features = node.data.特征列表 || [];
- const jaccardScore = (node.data['Jaccard相似度'] * 100).toFixed(1);
- panelTitle.textContent = node.label;
- panelInfo.innerHTML = `
- <span>共 ${features.length} 个特征</span>
- <span style="margin-left: 2rem; color: var(--secondary-color); font-weight: 600;">
- Jaccard相似度: ${jaccardScore}%
- </span>
- `;
- this.renderFeatureList(features, cardsContainer);
- } else if (node.type === 'result') {
- // 点击结果项,显示概览
- panelTitle.textContent = node.label;
- panelInfo.textContent = `共 ${node.children?.length || 0} 个关联分类`;
- this.renderResultOverview(node.data, cardsContainer);
- } else if (node.type === 'note') {
- // 点击单个帖子,显示该帖子详情
- window.__currentNotes = [node.data];
- CardRenderer.renderNotes([node.data], cardsContainer);
- }
- }
- /**
- * 渲染特征列表
- * @param {Array} features - 特征数组
- * @param {HTMLElement} container - 容器元素
- */
- renderFeatureList(features, container) {
- const html = `
- <div style="padding: 1rem;">
- <table style="width: 100%; border-collapse: collapse;">
- <thead>
- <tr style="border-bottom: 2px solid var(--border-color);">
- <th style="padding: 0.75rem; text-align: left;">特征名称</th>
- <th style="padding: 0.75rem; text-align: left;">搜索词</th>
- <th style="padding: 0.75rem; text-align: center;">帖子数</th>
- <th style="padding: 0.75rem; text-align: center;">状态</th>
- </tr>
- </thead>
- <tbody>
- ${features.map(feature => {
- const noteCount = feature.search_metadata?.note_count || 0;
- const status = feature.search_metadata?.status || 'pending';
- const statusText = {
- success: '成功',
- failed: '失败',
- pending: '待搜索'
- }[status] || '未知';
- const statusColor = {
- success: 'var(--secondary-color)',
- failed: 'var(--danger-color)',
- pending: 'var(--warning-color)'
- }[status] || 'var(--text-secondary)';
- return `
- <tr style="border-bottom: 1px solid var(--border-color);">
- <td style="padding: 0.75rem;">${feature.特征名称}</td>
- <td style="padding: 0.75rem; color: var(--text-secondary);">
- ${feature.search_word || '-'}
- </td>
- <td style="padding: 0.75rem; text-align: center;">
- ${noteCount}
- </td>
- <td style="padding: 0.75rem; text-align: center;">
- <span style="color: ${statusColor}; font-weight: 600;">
- ${statusText}
- </span>
- </td>
- </tr>
- `;
- }).join('')}
- </tbody>
- </table>
- </div>
- `;
- container.innerHTML = html;
- }
- /**
- * 渲染结果概览
- * @param {Object} result - 结果数据
- * @param {HTMLElement} container - 容器元素
- */
- renderResultOverview(result, container) {
- const html = `
- <div style="padding: 2rem;">
- <div style="margin-bottom: 2rem;">
- <h3 style="margin-bottom: 1rem;">基本信息</h3>
- <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
- <div style="padding: 1rem; background: var(--bg-color); border-radius: 8px;">
- <div style="color: var(--text-secondary); font-size: 0.875rem;">原始特征名称</div>
- <div style="font-size: 1.25rem; font-weight: 600; margin-top: 0.5rem;">
- ${result.原始特征名称}
- </div>
- </div>
- <div style="padding: 1rem; background: var(--bg-color); border-radius: 8px;">
- <div style="color: var(--text-secondary); font-size: 0.875rem;">人设特征名称</div>
- <div style="font-size: 1.25rem; font-weight: 600; margin-top: 0.5rem;">
- ${result.最高匹配信息?.人设特征名称 || '-'}
- </div>
- </div>
- <div style="padding: 1rem; background: var(--bg-color); border-radius: 8px;">
- <div style="color: var(--text-secondary); font-size: 0.875rem;">来源层级</div>
- <div style="font-size: 1.25rem; font-weight: 600; margin-top: 0.5rem;">
- ${result.来源层级}
- </div>
- </div>
- <div style="padding: 1rem; background: var(--bg-color); border-radius: 8px;">
- <div style="color: var(--text-secondary); font-size: 0.875rem;">关联数量</div>
- <div style="font-size: 1.25rem; font-weight: 600; margin-top: 0.5rem;">
- ${result.找到的关联?.length || 0} 个
- </div>
- </div>
- </div>
- </div>
- <div>
- <h3 style="margin-bottom: 1rem;">关联分类</h3>
- <div style="display: flex; flex-direction: column; gap: 0.5rem;">
- ${(result.找到的关联 || []).map(assoc => `
- <div style="padding: 1rem; background: var(--bg-color); border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
- <div>
- <div style="font-weight: 600;">${assoc.目标分类路径}</div>
- <div style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 0.25rem;">
- ${assoc.特征列表?.length || 0} 个特征
- </div>
- </div>
- <div style="text-align: right; font-size: 0.875rem; color: var(--text-secondary);">
- Jaccard: ${(assoc['Jaccard相似度'] * 100).toFixed(1)}%
- </div>
- </div>
- `).join('')}
- </div>
- </div>
- </div>
- `;
- container.innerHTML = html;
- }
- /**
- * 按状态筛选
- * @param {string} status - 状态值
- */
- filterByStatus(status) {
- // TODO: 实现筛选逻辑
- console.log('筛选状态:', status);
- }
- /**
- * 排序数据
- * @param {string} sortBy - 排序方式
- */
- sortData(sortBy) {
- // TODO: 实现排序逻辑
- console.log('排序方式:', sortBy);
- }
- /**
- * 切换主题
- */
- toggleTheme() {
- const body = document.body;
- const themeToggle = document.getElementById('theme-toggle');
- if (body.classList.contains('dark-theme')) {
- body.classList.remove('dark-theme');
- themeToggle.textContent = '🌙';
- localStorage.setItem('theme', 'light');
- } else {
- body.classList.add('dark-theme');
- themeToggle.textContent = '☀️';
- localStorage.setItem('theme', 'dark');
- }
- }
- /**
- * 更新统计数据
- */
- updateStats() {
- const statResults = document.getElementById('stat-results');
- const statFeatures = document.getElementById('stat-features');
- const statNotes = document.getElementById('stat-notes');
- // 统计数据
- let totalFeatures = 0;
- let totalNotes = 0;
- this.data.forEach(result => {
- (result.找到的关联 || []).forEach(assoc => {
- (assoc.特征列表 || []).forEach(feature => {
- totalFeatures++;
- if (feature.search_metadata?.note_count) {
- totalNotes += feature.search_metadata.note_count;
- }
- });
- });
- });
- statResults.textContent = this.data.length;
- statFeatures.textContent = totalFeatures;
- statNotes.textContent = totalNotes;
- }
- /**
- * 显示错误
- * @param {string} message - 错误消息
- */
- showError(message) {
- const treeContainer = document.getElementById('tree-container');
- const cardsContainer = document.getElementById('cards-container');
- treeContainer.innerHTML = `<div class="empty-state"><div class="empty-icon">❌</div><p>${message}</p></div>`;
- cardsContainer.innerHTML = `<div class="empty-state"><div class="empty-icon">❌</div><p>${message}</p></div>`;
- }
- }
- // 初始化应用
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => {
- new App();
- });
- } else {
- new App();
- }
- // 恢复主题设置
- const savedTheme = localStorage.getItem('theme');
- if (savedTheme === 'dark') {
- document.body.classList.add('dark-theme');
- const themeToggle = document.getElementById('theme-toggle');
- if (themeToggle) themeToggle.textContent = '☀️';
- }
|