|
|
@@ -129,6 +129,59 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}}
|
|
|
+ .match-list-panel {{
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border-bottom: 1px solid #0f3460;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ }}
|
|
|
+ .match-list-panel h3 {{
|
|
|
+ font-size: 13px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ color: #e94560;
|
|
|
+ }}
|
|
|
+ .match-list {{
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }}
|
|
|
+ .match-item {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 6px 8px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ background: rgba(233, 69, 96, 0.1);
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 11px;
|
|
|
+ gap: 8px;
|
|
|
+ }}
|
|
|
+ .match-item:hover {{
|
|
|
+ background: rgba(233, 69, 96, 0.2);
|
|
|
+ }}
|
|
|
+ .match-item.active {{
|
|
|
+ background: rgba(233, 69, 96, 0.3);
|
|
|
+ border: 1px solid #e94560;
|
|
|
+ }}
|
|
|
+ .match-item .score {{
|
|
|
+ background: #e94560;
|
|
|
+ color: #fff;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-weight: bold;
|
|
|
+ min-width: 40px;
|
|
|
+ text-align: center;
|
|
|
+ }}
|
|
|
+ .match-item .score.high {{
|
|
|
+ background: #27ae60;
|
|
|
+ }}
|
|
|
+ .match-item .score.medium {{
|
|
|
+ background: #f39c12;
|
|
|
+ }}
|
|
|
+ .match-item .names {{
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }}
|
|
|
#persona-tree-panel {{
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
@@ -538,6 +591,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
<div id="sidebar">
|
|
|
<h1>匹配图谱</h1>
|
|
|
|
|
|
+ <div class="match-list-panel">
|
|
|
+ <h3>匹配关系 <span id="matchCount">(0)</span></h3>
|
|
|
+ <div id="matchList" class="match-list"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="detail-panel active" id="detailPanel">
|
|
|
<h3 id="detailTitle">点击节点或边查看详情</h3>
|
|
|
<div id="detailContent">
|
|
|
@@ -842,6 +900,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
const expandedNodes = nodes.filter(n => n.source === "人设" && n.是否扩展);
|
|
|
const matchLinks = links.filter(l => l.type === "匹配");
|
|
|
|
|
|
+ // 更新匹配列表(按分数降序)
|
|
|
+ updateMatchList(matchLinks, nodes);
|
|
|
+
|
|
|
// 构建帖子节点到人设节点的映射
|
|
|
const postToPersona = {{}};
|
|
|
const personaToPost = {{}};
|
|
|
@@ -1634,7 +1695,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 绘制可见的边
|
|
|
const link = linkG.append("line")
|
|
|
.attr("class", d => "link " + getEdgeClass(d.type))
|
|
|
- .attr("stroke-width", d => d.type === "匹配" ? 2.5 : 1.5);
|
|
|
+ .attr("stroke-width", d => d.type === "匹配" ? 2.5 : 1.5)
|
|
|
+ .attr("stroke-dasharray", d => {{
|
|
|
+ // 匹配边根据相似度设置虚实线
|
|
|
+ if (d.type === "匹配" && d.边详情 && d.边详情.相似度 !== undefined) {{
|
|
|
+ const score = d.边详情.相似度;
|
|
|
+ if (score >= 0.8) return null; // >= 0.8 实线
|
|
|
+ if (score >= 0.5) return "6,4"; // 0.5-0.8 虚线
|
|
|
+ }}
|
|
|
+ return null; // 默认实线
|
|
|
+ }});
|
|
|
|
|
|
// 判断是否为跨层边(根据源和目标节点的层级)- 赋值给全局变量
|
|
|
isCrossLayerEdge = function(d) {{
|
|
|
@@ -1644,9 +1714,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return getNodeLayer(sourceNode) !== getNodeLayer(targetNode);
|
|
|
}};
|
|
|
|
|
|
- // 设置跨层边的初始可见性(默认隐藏)
|
|
|
+ // 设置跨层边的初始可见性(匹配边始终显示,其他跨层边默认隐藏)
|
|
|
linkG.each(function(d) {{
|
|
|
- if (isCrossLayerEdge(d) && !showCrossLayerEdges) {{
|
|
|
+ if (d.type === "匹配") {{
|
|
|
+ d3.select(this).style("display", "block"); // 匹配边始终显示
|
|
|
+ }} else if (isCrossLayerEdge(d) && !showCrossLayerEdges) {{
|
|
|
d3.select(this).style("display", "none");
|
|
|
}}
|
|
|
}});
|
|
|
@@ -2203,6 +2275,89 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
document.getElementById("detailPanel").classList.remove("active");
|
|
|
}}
|
|
|
|
|
|
+ // 更新匹配列表
|
|
|
+ function updateMatchList(matchLinks, nodes) {{
|
|
|
+ const listEl = document.getElementById("matchList");
|
|
|
+ const countEl = document.getElementById("matchCount");
|
|
|
+
|
|
|
+ // 构建节点ID到名称的映射
|
|
|
+ const nodeNames = {{}};
|
|
|
+ nodes.forEach(n => {{
|
|
|
+ nodeNames[n.id] = n.节点名称 || n.id;
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 按分数降序排序
|
|
|
+ const sortedLinks = [...matchLinks].sort((a, b) => {{
|
|
|
+ const scoreA = a.边详情?.相似度 ?? 0;
|
|
|
+ const scoreB = b.边详情?.相似度 ?? 0;
|
|
|
+ return scoreB - scoreA;
|
|
|
+ }});
|
|
|
+
|
|
|
+ countEl.textContent = `(${{sortedLinks.length}})`;
|
|
|
+
|
|
|
+ // 生成列表HTML
|
|
|
+ let html = "";
|
|
|
+ sortedLinks.forEach((link, index) => {{
|
|
|
+ const score = link.边详情?.相似度;
|
|
|
+ const scoreText = score !== undefined ? score.toFixed(2) : "N/A";
|
|
|
+ const scoreClass = score >= 0.8 ? "high" : (score >= 0.5 ? "medium" : "");
|
|
|
+
|
|
|
+ const srcId = typeof link.source === "object" ? link.source.id : link.source;
|
|
|
+ const tgtId = typeof link.target === "object" ? link.target.id : link.target;
|
|
|
+ const srcName = nodeNames[srcId] || srcId;
|
|
|
+ const tgtName = nodeNames[tgtId] || tgtId;
|
|
|
+
|
|
|
+ html += `
|
|
|
+ <div class="match-item" data-index="${{index}}" data-src="${{srcId}}" data-tgt="${{tgtId}}">
|
|
|
+ <span class="score ${{scoreClass}}">${{scoreText}}</span>
|
|
|
+ <span class="names" title="${{srcName}} → ${{tgtName}}">${{srcName}} → ${{tgtName}}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }});
|
|
|
+
|
|
|
+ listEl.innerHTML = html;
|
|
|
+
|
|
|
+ // 添加点击事件
|
|
|
+ listEl.querySelectorAll(".match-item").forEach(item => {{
|
|
|
+ item.addEventListener("click", () => {{
|
|
|
+ const srcId = item.dataset.src;
|
|
|
+ const tgtId = item.dataset.tgt;
|
|
|
+ const index = parseInt(item.dataset.index);
|
|
|
+
|
|
|
+ // 高亮对应的边
|
|
|
+ highlightMatchEdge(srcId, tgtId);
|
|
|
+
|
|
|
+ // 更新列表项样式
|
|
|
+ listEl.querySelectorAll(".match-item").forEach(el => el.classList.remove("active"));
|
|
|
+ item.classList.add("active");
|
|
|
+ }});
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 高亮匹配边
|
|
|
+ function highlightMatchEdge(srcId, tgtId) {{
|
|
|
+ if (!g) return;
|
|
|
+
|
|
|
+ // 重置所有
|
|
|
+ g.selectAll(".node").classed("dimmed", true).classed("highlighted", false);
|
|
|
+ g.selectAll(".link-group").classed("dimmed", true).classed("highlighted", false);
|
|
|
+
|
|
|
+ // 高亮对应的边和节点
|
|
|
+ g.selectAll(".link-group").each(function(d) {{
|
|
|
+ const dSrc = typeof d.source === "object" ? d.source.id : d.source;
|
|
|
+ const dTgt = typeof d.target === "object" ? d.target.id : d.target;
|
|
|
+ if ((dSrc === srcId && dTgt === tgtId) || (dSrc === tgtId && dTgt === srcId)) {{
|
|
|
+ d3.select(this).classed("dimmed", false).classed("highlighted", true);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+
|
|
|
+ g.selectAll(".node").each(function(d) {{
|
|
|
+ if (d.id === srcId || d.id === tgtId) {{
|
|
|
+ d3.select(this).classed("dimmed", false).classed("highlighted", true);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
// 获取节点颜色(全局版本,根据节点数据判断维度)
|
|
|
function getTreeNodeColor(d) {{
|
|
|
const dimColors = {{ "灵感点": "#f39c12", "目的点": "#3498db", "关键点": "#9b59b6" }};
|