|
|
@@ -802,6 +802,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
overflow-y: auto;
|
|
|
height: fit-content;
|
|
|
max-height: calc(100vh - 280px);
|
|
|
+ /* 隐藏滚动条 */
|
|
|
+ scrollbar-width: none; /* Firefox */
|
|
|
+ -ms-overflow-style: none; /* IE/Edge */
|
|
|
+ }}
|
|
|
+
|
|
|
+ .left-sidebar::-webkit-scrollbar {{
|
|
|
+ display: none; /* Chrome/Safari */
|
|
|
}}
|
|
|
|
|
|
/* 中间栏 - base_word列表 */
|
|
|
@@ -815,6 +822,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
height: fit-content;
|
|
|
max-height: calc(100vh - 280px);
|
|
|
display: none;
|
|
|
+ /* 隐藏滚动条 */
|
|
|
+ scrollbar-width: none; /* Firefox */
|
|
|
+ -ms-overflow-style: none; /* IE/Edge */
|
|
|
+ }}
|
|
|
+
|
|
|
+ .middle-sidebar::-webkit-scrollbar {{
|
|
|
+ display: none; /* Chrome/Safari */
|
|
|
}}
|
|
|
|
|
|
.middle-sidebar.active {{
|
|
|
@@ -832,6 +846,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
height: fit-content;
|
|
|
max-height: calc(100vh - 280px);
|
|
|
display: none;
|
|
|
+ /* 隐藏滚动条 */
|
|
|
+ scrollbar-width: none; /* Firefox */
|
|
|
+ -ms-overflow-style: none; /* IE/Edge */
|
|
|
+ }}
|
|
|
+
|
|
|
+ .right-sidebar::-webkit-scrollbar {{
|
|
|
+ display: none; /* Chrome/Safari */
|
|
|
}}
|
|
|
|
|
|
.right-sidebar.active {{
|
|
|
@@ -846,6 +867,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
overflow-y: auto;
|
|
|
max-height: calc(100vh - 280px);
|
|
|
+ /* 隐藏滚动条 */
|
|
|
+ scrollbar-width: none; /* Firefox */
|
|
|
+ -ms-overflow-style: none; /* IE/Edge */
|
|
|
+ }}
|
|
|
+
|
|
|
+ .detail-area::-webkit-scrollbar {{
|
|
|
+ display: none; /* Chrome/Safari */
|
|
|
}}
|
|
|
|
|
|
/* 侧边栏标题 */
|
|
|
@@ -2856,6 +2884,23 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
margin-top: 4px;
|
|
|
}}
|
|
|
|
|
|
+ .partial-feature-score {{
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ca8a04;
|
|
|
+ background: #fef9c3;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-right: 6px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .partial-feature-meta {{
|
|
|
+ font-size: 11px;
|
|
|
+ color: #ca8a04;
|
|
|
+ margin-top: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
/* ========== 部分匹配特征样式(橘黄色)========== */
|
|
|
.partial-similarity-section {{
|
|
|
margin-top: 20px;
|
|
|
@@ -3122,6 +3167,28 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
|
|
|
<!-- 主容器 - 级联布局 -->
|
|
|
<div class="main-container">
|
|
|
+ <!-- SVG连接线层 -->
|
|
|
+ <svg id="connectionLines" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;">
|
|
|
+ <!-- 选题点到人设的连接线 -->
|
|
|
+ <path id="line-feature-to-base" d=""
|
|
|
+ stroke="#3b82f6"
|
|
|
+ stroke-width="1.5"
|
|
|
+ fill="none"
|
|
|
+ opacity="0.6"/>
|
|
|
+ <!-- 人设到搜索词的连接线 -->
|
|
|
+ <path id="line-base-to-search" d=""
|
|
|
+ stroke="#3b82f6"
|
|
|
+ stroke-width="1.5"
|
|
|
+ fill="none"
|
|
|
+ opacity="0.6"/>
|
|
|
+ <!-- 搜索词到搜索结果的连接线 -->
|
|
|
+ <path id="line-search-to-detail" d=""
|
|
|
+ stroke="#3b82f6"
|
|
|
+ stroke-width="1.5"
|
|
|
+ fill="none"
|
|
|
+ opacity="0.6"/>
|
|
|
+ </svg>
|
|
|
+
|
|
|
<!-- 左侧栏:原始特征列表 -->
|
|
|
<div class="left-sidebar" id="leftSidebar">
|
|
|
<div class="sidebar-header">选题点</div>
|
|
|
@@ -3357,7 +3424,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
let html = `
|
|
|
<div class="candidate-library">
|
|
|
<div class="candidate-header" onclick="toggleCandidateLibrary()">
|
|
|
- <div class="candidate-title">📋 候选词库(区分人设/帖子来源)</div>
|
|
|
+ <div class="candidate-title">📋 候选分类、标签(用于组合生成搜索query)</div>
|
|
|
<div class="candidate-toggle" id="candidate-toggle">▼</div>
|
|
|
</div>
|
|
|
<div class="candidate-content" id="candidate-content">
|
|
|
@@ -3367,7 +3434,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
if (personaCount > 0) {{
|
|
|
html += `
|
|
|
<div class="candidate-section">
|
|
|
- <div class="candidate-section-title">【人设候选词】(${{personaCount}}个)</div>
|
|
|
+ <div class="candidate-section-title">【来源人设】(${{personaCount}}个) 与帖子相似度≥0.8</div>
|
|
|
<div class="candidate-list">
|
|
|
`;
|
|
|
|
|
|
@@ -3392,7 +3459,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
if (postCount > 0) {{
|
|
|
html += `
|
|
|
<div class="candidate-section">
|
|
|
- <div class="candidate-section-title">【帖子候选词】(${{postCount}}个)</div>
|
|
|
+ <div class="candidate-section-title">【来源帖子】(${{postCount}}个) 与人设相似度≥0.8</div>
|
|
|
<div class="candidate-list">
|
|
|
`;
|
|
|
|
|
|
@@ -3490,13 +3557,34 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
sourcePath = top3Matches[0]['所属分类路径'] || '';
|
|
|
}}
|
|
|
|
|
|
+ // 检查是否有完全匹配的帖子
|
|
|
+ let hasCompleteMatch = false;
|
|
|
+ groups.forEach(group => {{
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
+ searches.forEach(search => {{
|
|
|
+ const evaluation = search['evaluation_with_filter'];
|
|
|
+ if (evaluation && evaluation.notes_evaluation) {{
|
|
|
+ evaluation.notes_evaluation.forEach(noteEval => {{
|
|
|
+ if (noteEval['匹配类型'] === '完全匹配') {{
|
|
|
+ hasCompleteMatch = true;
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ }});
|
|
|
+
|
|
|
const isActive = selectedFeatureIdx === featureIdx;
|
|
|
+ const postIcon = hasCompleteMatch ? ' 📝' : '';
|
|
|
|
|
|
html += `
|
|
|
<div class="feature-item-left ${{isActive ? 'active' : ''}}"
|
|
|
onclick="selectFeature(${{featureIdx}})"
|
|
|
id="feature-left-${{featureIdx}}">
|
|
|
- <div class="feature-name">🎯 ${{featureName}}</div>
|
|
|
+ <div class="feature-name">🎯 ${{featureName}}${{postIcon}}</div>
|
|
|
+ <div class="cascade-item-meta">
|
|
|
+ <span class="high-feature-score">相似度: ${{similarity.toFixed(2)}}</span>
|
|
|
+ <span class="high-feature-meta">${{dimension}}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
`;
|
|
|
}});
|
|
|
@@ -3536,6 +3624,117 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
leftContent.innerHTML = html;
|
|
|
}}
|
|
|
|
|
|
+ // ========== 连接线绘制函数 ==========
|
|
|
+
|
|
|
+ // 绘制两个元素之间的连接线
|
|
|
+ function drawConnectionLine(fromId, toId, lineId) {{
|
|
|
+ const fromEl = document.getElementById(fromId);
|
|
|
+ const toEl = document.getElementById(toId);
|
|
|
+ const lineEl = document.getElementById(lineId);
|
|
|
+
|
|
|
+ if (!fromEl || !toEl || !lineEl) {{
|
|
|
+ // 如果任一元素不存在,隐藏连接线
|
|
|
+ if (lineEl) lineEl.setAttribute('d', '');
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 获取容器位置
|
|
|
+ const container = document.querySelector('.main-container');
|
|
|
+ const containerRect = container.getBoundingClientRect();
|
|
|
+
|
|
|
+ // 获取元素位置(相对于容器)
|
|
|
+ const fromRect = fromEl.getBoundingClientRect();
|
|
|
+ const toRect = toEl.getBoundingClientRect();
|
|
|
+
|
|
|
+ // 计算起点(从元素右侧中心点出发)
|
|
|
+ const startX = fromRect.right - containerRect.left;
|
|
|
+ const startY = fromRect.top + fromRect.height / 2 - containerRect.top;
|
|
|
+
|
|
|
+ // 计算终点
|
|
|
+ let endX, endY;
|
|
|
+ if (toId === 'detailArea') {{
|
|
|
+ // 如果目标是detailArea,连接到其左上角区域
|
|
|
+ endX = toRect.left - containerRect.left;
|
|
|
+ endY = toRect.top + 80 - containerRect.top; // 80px是header高度
|
|
|
+ }} else {{
|
|
|
+ // 其他情况连接到左侧中心点
|
|
|
+ endX = toRect.left - containerRect.left;
|
|
|
+ endY = toRect.top + toRect.height / 2 - containerRect.top;
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 使用贝塞尔曲线绘制连接线
|
|
|
+ const controlPoint1X = startX + (endX - startX) * 0.5;
|
|
|
+ const controlPoint1Y = startY;
|
|
|
+ const controlPoint2X = startX + (endX - startX) * 0.5;
|
|
|
+ const controlPoint2Y = endY;
|
|
|
+
|
|
|
+ const path = `M ${{startX}} ${{startY}} C ${{controlPoint1X}} ${{controlPoint1Y}}, ${{controlPoint2X}} ${{controlPoint2Y}}, ${{endX}} ${{endY}}`;
|
|
|
+
|
|
|
+ lineEl.setAttribute('d', path);
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 更新所有连接线
|
|
|
+ function updateConnectionLines() {{
|
|
|
+ // 1. 选题点 → 人设
|
|
|
+ if (selectedFeatureIdx !== null && selectedBaseWordIdx !== null) {{
|
|
|
+ const fromId = `feature-left-${{selectedFeatureIdx}}`;
|
|
|
+ const toId = `baseword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}`;
|
|
|
+ drawConnectionLine(fromId, toId, 'line-feature-to-base');
|
|
|
+ }} else {{
|
|
|
+ // 清空线条
|
|
|
+ document.getElementById('line-feature-to-base').setAttribute('d', '');
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 2. 人设 → 搜索词
|
|
|
+ if (selectedBaseWordIdx !== null && selectedSearchWordIdx !== null) {{
|
|
|
+ const fromId = `baseword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}`;
|
|
|
+ const toId = `searchword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}-${{selectedSearchWordIdx}}`;
|
|
|
+ drawConnectionLine(fromId, toId, 'line-base-to-search');
|
|
|
+ }} else {{
|
|
|
+ // 清空线条
|
|
|
+ document.getElementById('line-base-to-search').setAttribute('d', '');
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 3. 搜索词 → 搜索结果
|
|
|
+ if (selectedSearchWordIdx !== null) {{
|
|
|
+ const fromId = `searchword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}-${{selectedSearchWordIdx}}`;
|
|
|
+ const toId = 'detailArea';
|
|
|
+ drawConnectionLine(fromId, toId, 'line-search-to-detail');
|
|
|
+ }} else {{
|
|
|
+ // 清空线条
|
|
|
+ document.getElementById('line-search-to-detail').setAttribute('d', '');
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 监听滚动和resize事件,更新连接线位置
|
|
|
+ function setupScrollListeners() {{
|
|
|
+ // 监听所有可能滚动的容器
|
|
|
+ const scrollContainers = [
|
|
|
+ document.getElementById('leftSidebar'),
|
|
|
+ document.getElementById('middleSidebar'),
|
|
|
+ document.getElementById('rightSidebar'),
|
|
|
+ document.getElementById('detailArea')
|
|
|
+ ];
|
|
|
+
|
|
|
+ scrollContainers.forEach(container => {{
|
|
|
+ if (container) {{
|
|
|
+ container.addEventListener('scroll', () => {{
|
|
|
+ updateConnectionLines();
|
|
|
+ }}, {{ passive: true }});
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', () => {{
|
|
|
+ updateConnectionLines();
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 监听窗口滚动
|
|
|
+ window.addEventListener('scroll', () => {{
|
|
|
+ updateConnectionLines();
|
|
|
+ }}, {{ passive: true }});
|
|
|
+ }}
|
|
|
+
|
|
|
// ========== 级联交互函数 ==========
|
|
|
|
|
|
// 选择特征 - 显示middle sidebar
|
|
|
@@ -3560,6 +3759,18 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
document.getElementById('rightSidebar').classList.remove('active');
|
|
|
document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
|
|
|
document.getElementById('detailContent').innerHTML = '';
|
|
|
+
|
|
|
+ // 自动选择第一个base_word
|
|
|
+ const feature = data[featureIdx];
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
+ if (groups.length > 0) {{
|
|
|
+ // 延迟执行,确保DOM渲染完成
|
|
|
+ setTimeout(() => {{
|
|
|
+ selectBaseWord(featureIdx, 0);
|
|
|
+ }}, 50);
|
|
|
+ }} else {{
|
|
|
+ updateConnectionLines();
|
|
|
+ }}
|
|
|
}}
|
|
|
|
|
|
// 渲染middle sidebar - 显示base_words
|
|
|
@@ -3618,6 +3829,21 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
// 隐藏detail content
|
|
|
document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
|
|
|
document.getElementById('detailContent').innerHTML = '';
|
|
|
+
|
|
|
+ // 更新连接线:选题点 → 人设
|
|
|
+ updateConnectionLines();
|
|
|
+
|
|
|
+ // 自动选择第一个搜索词
|
|
|
+ const feature = data[featureIdx];
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
+ const group = groups[baseWordIdx];
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
+ if (searches.length > 0) {{
|
|
|
+ // 延迟执行,确保DOM渲染完成
|
|
|
+ setTimeout(() => {{
|
|
|
+ selectSearchWord(featureIdx, baseWordIdx, 0);
|
|
|
+ }}, 50);
|
|
|
+ }}
|
|
|
}}
|
|
|
|
|
|
// 渲染right sidebar - 显示search words
|
|
|
@@ -3734,6 +3960,11 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
|
|
|
// 渲染detail area
|
|
|
renderDetailArea(featureIdx, baseWordIdx, swIdx);
|
|
|
+
|
|
|
+ // 更新连接线:人设 → 搜索词
|
|
|
+ setTimeout(() => {{
|
|
|
+ updateConnectionLines();
|
|
|
+ }}, 50);
|
|
|
}}
|
|
|
|
|
|
// 渲染detail area - 显示搜索结果
|
|
|
@@ -4801,22 +5032,14 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
renderLeftSidebar();
|
|
|
console.log('✅ [系统] 左侧导航(级联模式)渲染完成');
|
|
|
|
|
|
- // 级联模式:自动选择第一个特征
|
|
|
+ // 设置滚动监听器
|
|
|
+ setupScrollListeners();
|
|
|
+ console.log('✅ [系统] 滚动监听器已设置');
|
|
|
+
|
|
|
+ // 级联模式:自动选择第一个特征(会自动级联选择base_word和search_word)
|
|
|
if (data.length > 0) {{
|
|
|
selectFeature(0);
|
|
|
- console.log('✅ [系统] 自动选择第一个特征');
|
|
|
-
|
|
|
- const firstGroups = data[0]['组合评估结果_分组'];
|
|
|
- if (firstGroups && firstGroups.length > 0) {{
|
|
|
- selectBaseWord(0, 0);
|
|
|
- console.log('✅ [系统] 自动选择第一个base_word');
|
|
|
-
|
|
|
- const firstSearches = firstGroups[0]['top10_searches'];
|
|
|
- if (firstSearches && firstSearches.length > 0) {{
|
|
|
- selectSearchWord(0, 0, 0);
|
|
|
- console.log('✅ [系统] 自动选择第一个搜索词');
|
|
|
- }}
|
|
|
- }}
|
|
|
+ console.log('✅ [系统] 自动选择第一个特征(级联选择)');
|
|
|
}}
|
|
|
|
|
|
console.log('✅ [系统] 页面初始化完成');
|