|
|
@@ -508,6 +508,251 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
margin-top: 4px;
|
|
|
}}
|
|
|
|
|
|
+ /* P值显示样式 */
|
|
|
+ .p-score-container {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 6px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
+ border-radius: 6px;
|
|
|
+ border-left: 3px solid #0ea5e9;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-label {{
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0369a1;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-value {{
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #0284c7;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-info-icon {{
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #0ea5e9;
|
|
|
+ transition: all 0.2s;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: white;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-info-icon:hover {{
|
|
|
+ background: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+ transform: scale(1.1);
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-arrow {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-top: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-score-arrow-symbol {{
|
|
|
+ color: #0ea5e9;
|
|
|
+ font-weight: bold;
|
|
|
+ }}
|
|
|
+
|
|
|
+ /* P值详情模态窗口 */
|
|
|
+ .p-detail-modal {{
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ z-index: 10000;
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-modal.active {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-window {{
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 800px;
|
|
|
+ max-height: 85vh;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-header {{
|
|
|
+ padding: 20px 25px;
|
|
|
+ background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
|
|
+ color: white;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-title {{
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-close-btn {{
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border: none;
|
|
|
+ color: white;
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-close-btn:hover {{
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
+ transform: scale(1.1);
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-detail-body {{
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 25px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-formula-section {{
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-formula-title {{
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0f172a;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-formula {{
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #0284c7;
|
|
|
+ background: white;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-summary {{
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-summary-item {{
|
|
|
+ background: white;
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ text-align: center;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-summary-label {{
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-summary-value {{
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #0f172a;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-summary-value.highlight {{
|
|
|
+ color: #0284c7;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-matches-section {{
|
|
|
+ margin-top: 20px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-matches-title {{
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0f172a;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-item {{
|
|
|
+ background: white;
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-item:hover {{
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.1);
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-header {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-title {{
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0f172a;
|
|
|
+ flex: 1;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-contribution {{
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #0284c7;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-details {{
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-detail-item {{
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .p-match-detail-label {{
|
|
|
+ font-weight: 600;
|
|
|
+ }}
|
|
|
+
|
|
|
/* 右侧结果区 */
|
|
|
.right-content {{
|
|
|
flex: 1;
|
|
|
@@ -1301,6 +1546,17 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- P值详情模态窗口 -->
|
|
|
+ <div class="p-detail-modal" id="pDetailModal">
|
|
|
+ <div class="p-detail-window">
|
|
|
+ <div class="p-detail-header">
|
|
|
+ <div class="p-detail-title">📊 综合得分P 计算详情</div>
|
|
|
+ <button class="p-detail-close-btn" onclick="closePDetailModal()">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="p-detail-body" id="pDetailContent"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
// 数据
|
|
|
const data = {data_json};
|
|
|
@@ -1386,6 +1642,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
const score = sw.score || 0;
|
|
|
const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
|
|
|
const sourceWord = sw.source_word || '';
|
|
|
+ const searchWord = sw.search_word || '';
|
|
|
|
|
|
const evaluation = sw['evaluation_with_filter'];
|
|
|
let evalBadges = '';
|
|
|
@@ -1404,6 +1661,28 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
|
|
|
}}
|
|
|
|
|
|
+ // P值显示
|
|
|
+ let pScoreHtml = '';
|
|
|
+ const pScore = sw.comprehensive_score;
|
|
|
+ const pDetail = sw.comprehensive_score_detail;
|
|
|
+ if (pScore !== undefined && pScore !== null) {{
|
|
|
+ const originalFeature = feature['原始特征名称'] || '';
|
|
|
+ pScoreHtml = `
|
|
|
+ <div class="p-score-container" onclick="event.stopPropagation();">
|
|
|
+ <div class="p-score-arrow">
|
|
|
+ <span>${{sourceWord}}</span>
|
|
|
+ <span class="p-score-arrow-symbol">→</span>
|
|
|
+ <span>${{originalFeature}}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="p-score-container" onclick="event.stopPropagation(); openPDetailModal('${{searchWord}}', ${{featureIdx}}, ${{groupIdx}}, ${{swIdx}});">
|
|
|
+ <span class="p-score-label">综合得分P:</span>
|
|
|
+ <span class="p-score-value">${{pScore.toFixed(3)}}</span>
|
|
|
+ <span class="p-score-info-icon" title="点击查看计算详情">ℹ️</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }}
|
|
|
+
|
|
|
html += `
|
|
|
<div class="search-word-item" onclick="scrollToBlock('${{blockId}}')"
|
|
|
id="sw-${{featureIdx}}-${{groupIdx}}-${{swIdx}}"
|
|
|
@@ -1415,6 +1694,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
来源: ${{sourceWord}}
|
|
|
</div>
|
|
|
<div class="search-word-eval">${{evalBadges}}</div>
|
|
|
+ ${{pScoreHtml}}
|
|
|
</div>
|
|
|
`;
|
|
|
}});
|
|
|
@@ -1698,8 +1978,162 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
if (e.target === modal) {{
|
|
|
closeModal();
|
|
|
}}
|
|
|
+
|
|
|
+ const pModal = document.getElementById('pDetailModal');
|
|
|
+ if (e.target === pModal) {{
|
|
|
+ closePDetailModal();
|
|
|
+ }}
|
|
|
}});
|
|
|
|
|
|
+ // 打开P值详情模态窗口
|
|
|
+ function openPDetailModal(searchWord, featureIdx, groupIdx, swIdx) {{
|
|
|
+ console.log('🔧 [调试] openPDetailModal被调用, searchWord:', searchWord);
|
|
|
+
|
|
|
+ const modal = document.getElementById('pDetailModal');
|
|
|
+ const modalContent = document.getElementById('pDetailContent');
|
|
|
+
|
|
|
+ if (!modal || !modalContent) {{
|
|
|
+ console.error('❌ [错误] 无法找到P值详情模态窗口元素');
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 获取对应的搜索词数据
|
|
|
+ const feature = data[featureIdx];
|
|
|
+ const group = feature['组合评估结果_分组'][groupIdx];
|
|
|
+ const sw = group['top10_searches'][swIdx];
|
|
|
+
|
|
|
+ const pScore = sw.comprehensive_score;
|
|
|
+ const pDetail = sw.comprehensive_score_detail;
|
|
|
+
|
|
|
+ if (!pDetail) {{
|
|
|
+ console.warn('⚠️ [警告] 未找到P值详情数据');
|
|
|
+ modalContent.innerHTML = '<div style="padding: 30px; text-align: center; color: #6b7280;">暂无P值详情数据</div>';
|
|
|
+ }} else {{
|
|
|
+ try {{
|
|
|
+ modalContent.innerHTML = renderPDetailContent(searchWord, feature['原始特征名称'], sw.source_word, pScore, pDetail);
|
|
|
+ console.log('✅ [调试] P值详情内容渲染成功');
|
|
|
+ }} catch (error) {{
|
|
|
+ console.error('❌ [错误] 渲染P值详情内容失败:', error);
|
|
|
+ modalContent.innerHTML = `<div style="padding: 30px; text-align: center; color: red;">渲染错误: ${{error.message}}</div>`;
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 显示模态窗口
|
|
|
+ modal.classList.add('active');
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
+ console.log('✅ [调试] P值详情模态窗口已显示');
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 关闭P值详情模态窗口
|
|
|
+ function closePDetailModal() {{
|
|
|
+ console.log('🔧 [调试] closePDetailModal被调用');
|
|
|
+ const modal = document.getElementById('pDetailModal');
|
|
|
+ if (modal) {{
|
|
|
+ modal.classList.remove('active');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ console.log('✅ [调试] P值详情模态窗口已关闭');
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 渲染P值详情内容
|
|
|
+ function renderPDetailContent(searchWord, targetFeature, sourceWord, pScore, pDetail) {{
|
|
|
+ const N = pDetail.N || 0;
|
|
|
+ const M = pDetail.M || 0;
|
|
|
+ const totalContribution = pDetail.total_contribution || 0;
|
|
|
+ const matches = pDetail.complete_matches || [];
|
|
|
+
|
|
|
+ let html = `
|
|
|
+ <div class="p-formula-section">
|
|
|
+ <div class="p-formula-title">📐 计算公式</div>
|
|
|
+ <div class="p-formula">P = Σ(a × b) / N = ${{totalContribution.toFixed(3)}} / ${{N}} = ${{pScore.toFixed(3)}}</div>
|
|
|
+ <div style="font-size: 12px; color: #64748b; margin-top: 8px;">
|
|
|
+ <div>• a: 评估得分(综合得分)</div>
|
|
|
+ <div>• b: 最高相似度(解构特征与目标特征的最高相似度)</div>
|
|
|
+ <div>• N: 总帖子数</div>
|
|
|
+ <div>• M: 完全匹配数(得分 ≥ 0.8)</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="p-summary">
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">搜索词</div>
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{searchWord}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">来源</div>
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{sourceWord}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">目标特征</div>
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{targetFeature}}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="p-summary">
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">总帖子数 (N)</div>
|
|
|
+ <div class="p-summary-value">${{N}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">完全匹配数 (M)</div>
|
|
|
+ <div class="p-summary-value highlight">${{M}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">总贡献值</div>
|
|
|
+ <div class="p-summary-value highlight">${{totalContribution.toFixed(3)}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-summary-item">
|
|
|
+ <div class="p-summary-label">综合得分 (P)</div>
|
|
|
+ <div class="p-summary-value highlight">${{pScore.toFixed(3)}}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ if (matches.length > 0) {{
|
|
|
+ html += `
|
|
|
+ <div class="p-matches-section">
|
|
|
+ <div class="p-matches-title">🎯 完全匹配帖子详情 (${{M}}个)</div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ matches.forEach((match, idx) => {{
|
|
|
+ html += `
|
|
|
+ <div class="p-match-item">
|
|
|
+ <div class="p-match-header">
|
|
|
+ <div class="p-match-title">${{idx + 1}}. ${{match.note_title || '未知标题'}}</div>
|
|
|
+ <div class="p-match-contribution">+${{match.contribution.toFixed(3)}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="p-match-details">
|
|
|
+ <div class="p-match-detail-item">
|
|
|
+ <span class="p-match-detail-label">评估得分 (a):</span>
|
|
|
+ <span>${{match.evaluation_score.toFixed(3)}}</span>
|
|
|
+ </div>
|
|
|
+ <div class="p-match-detail-item">
|
|
|
+ <span class="p-match-detail-label">最高相似度 (b):</span>
|
|
|
+ <span>${{match.max_similarity.toFixed(3)}}</span>
|
|
|
+ </div>
|
|
|
+ <div class="p-match-detail-item">
|
|
|
+ <span class="p-match-detail-label">贡献值 (a×b):</span>
|
|
|
+ <span>${{match.contribution.toFixed(3)}}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }});
|
|
|
+
|
|
|
+ html += `</div>`;
|
|
|
+ }} else {{
|
|
|
+ html += `
|
|
|
+ <div class="p-matches-section">
|
|
|
+ <div style="text-align: center; padding: 30px; color: #6b7280;">
|
|
|
+ 没有完全匹配的帖子(得分 ≥ 0.8)
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }}
|
|
|
+
|
|
|
+ return html;
|
|
|
+ }}
|
|
|
+
|
|
|
// 渲染解构内容
|
|
|
function renderDeconstructionContent(noteId) {{
|
|
|
const stage8Info = stage8Data[noteId];
|