|
@@ -754,6 +754,186 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
stroke-opacity: 0.6;
|
|
stroke-opacity: 0.6;
|
|
|
stroke-width: 1.5;
|
|
stroke-width: 1.5;
|
|
|
}}
|
|
}}
|
|
|
|
|
+
|
|
|
|
|
+ /* 帖子详情模态框 */
|
|
|
|
|
+ .post-detail-modal {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ z-index: 4000;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-modal.active {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 40px 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-content {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ max-width: 900px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-height: 90vh;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-close {{
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ right: 20px;
|
|
|
|
|
+ float: right;
|
|
|
|
|
+ font-size: 36px;
|
|
|
|
|
+ font-weight: 300;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-close:hover {{
|
|
|
|
|
+ color: #ef4444;
|
|
|
|
|
+ background: #fee2e2;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-header {{
|
|
|
|
|
+ padding: 40px 40px 20px 40px;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-title {{
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-meta {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-stats {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-body {{
|
|
|
|
|
+ padding: 30px 40px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-desc {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ line-height: 1.8;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-bottom: 25px;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-images {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-image {{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-footer {{
|
|
|
|
|
+ padding: 20px 40px 30px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-detail-link {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 12px 30px;
|
|
|
|
|
+ background: linear-gradient(135deg, #ff2442 0%, #ff6b81 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ border-radius: 25px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+ /* 帖子树根节点卡片 */
|
|
|
|
|
+ .post-card {{
|
|
|
|
|
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
|
|
|
+ border: 2px solid #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card:hover {{
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 6px 25px rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ border-color: #fff;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-thumbnail-wrapper {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-thumbnail {{
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-image-count {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 2px;
|
|
|
|
|
+ right: 2px;
|
|
|
|
|
+ background: rgba(0,0,0,0.7);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 1px 4px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-thumbnail-placeholder {{
|
|
|
|
|
+ background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #e94560;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-info {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-title {{
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.3;
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .post-card-stats {{
|
|
|
|
|
+ color: #8892b0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
@@ -1118,6 +1298,156 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}
|
|
}}
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
|
|
+ // 构建帖子树数据(从上到下:帖子 -> 维度 -> 点 -> 标签)
|
|
|
|
|
+ function buildPostTreeData(graphData) {{
|
|
|
|
|
+ const dimensions = ["灵感点", "目的点", "关键点"];
|
|
|
|
|
+ const dimColors = {{ "灵感点": "#f39c12", "目的点": "#3498db", "关键点": "#9b59b6" }};
|
|
|
|
|
+ const dimensionChildren = [];
|
|
|
|
|
+ let totalNodeCount = 0;
|
|
|
|
|
+ const postDetail = graphData.postDetail || {{}};
|
|
|
|
|
+
|
|
|
|
|
+ // 获取帖子点和帖子标签
|
|
|
|
|
+ const postPointNodes = graphData.帖子点节点列表 || [];
|
|
|
|
|
+ const postTagNodes = graphData.帖子标签节点列表 || [];
|
|
|
|
|
+ const belongEdges = graphData.帖子属于边列表 || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 构建节点映射
|
|
|
|
|
+ const pointMap = {{}};
|
|
|
|
|
+ postPointNodes.forEach(n => {{
|
|
|
|
|
+ pointMap[n.节点ID] = {{
|
|
|
|
|
+ ...n,
|
|
|
|
|
+ id: n.节点ID,
|
|
|
|
|
+ name: n.节点名称,
|
|
|
|
|
+ nodeType: "点",
|
|
|
|
|
+ dimColor: dimColors[n.节点层级],
|
|
|
|
|
+ children: []
|
|
|
|
|
+ }};
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ const tagMap = {{}};
|
|
|
|
|
+ postTagNodes.forEach(n => {{
|
|
|
|
|
+ tagMap[n.节点ID] = {{
|
|
|
|
|
+ ...n,
|
|
|
|
|
+ id: n.节点ID,
|
|
|
|
|
+ name: n.节点名称,
|
|
|
|
|
+ nodeType: "标签",
|
|
|
|
|
+ dimColor: dimColors[n.节点层级],
|
|
|
|
|
+ children: []
|
|
|
|
|
+ }};
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 根据属于边,把标签挂到点下面
|
|
|
|
|
+ belongEdges.forEach(e => {{
|
|
|
|
|
+ const tagNode = tagMap[e.源节点ID];
|
|
|
|
|
+ const pointNode = pointMap[e.目标节点ID];
|
|
|
|
|
+ if (tagNode && pointNode) {{
|
|
|
|
|
+ pointNode.children.push(tagNode);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 按维度分组点节点
|
|
|
|
|
+ dimensions.forEach(dim => {{
|
|
|
|
|
+ const dimPoints = postPointNodes
|
|
|
|
|
+ .filter(n => n.节点层级 === dim)
|
|
|
|
|
+ .map(n => pointMap[n.节点ID]);
|
|
|
|
|
+
|
|
|
|
|
+ if (dimPoints.length > 0) {{
|
|
|
|
|
+ const dimNode = {{
|
|
|
|
|
+ name: dim,
|
|
|
|
|
+ 节点名称: dim,
|
|
|
|
|
+ isDimension: true,
|
|
|
|
|
+ dimColor: dimColors[dim],
|
|
|
|
|
+ children: dimPoints
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ dimensionChildren.push(dimNode);
|
|
|
|
|
+ totalNodeCount += dimPoints.length;
|
|
|
|
|
+ dimPoints.forEach(p => totalNodeCount += p.children.length);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 根节点"帖子"
|
|
|
|
|
+ const title = postDetail.title || "帖子";
|
|
|
|
|
+ const rootNode = {{
|
|
|
|
|
+ name: title.length > 15 ? title.substring(0, 15) + "..." : title,
|
|
|
|
|
+ 节点名称: title,
|
|
|
|
|
+ isRoot: true,
|
|
|
|
|
+ postDetail: postDetail,
|
|
|
|
|
+ children: dimensionChildren
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ return {{ root: rootNode, nodeCount: totalNodeCount, tagMap: tagMap }};
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 显示帖子详情模态框
|
|
|
|
|
+ function showPostDetail(postData) {{
|
|
|
|
|
+ if (!postData) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 生成图片HTML
|
|
|
|
|
+ let imagesHtml = '';
|
|
|
|
|
+ if (postData.images && postData.images.length > 0) {{
|
|
|
|
|
+ imagesHtml = postData.images.map(img =>
|
|
|
|
|
+ `<img src="${{img}}" class="post-detail-image" alt="图片" loading="lazy">`
|
|
|
|
|
+ ).join('');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const modalHtml = `
|
|
|
|
|
+ <div class="post-detail-content" onclick="event.stopPropagation()">
|
|
|
|
|
+ <button class="post-detail-close" onclick="closePostDetail()">×</button>
|
|
|
|
|
+ <div class="post-detail-header">
|
|
|
|
|
+ <div class="post-detail-title">${{postData.title || '无标题'}}</div>
|
|
|
|
|
+ <div class="post-detail-meta">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>👤 ${{postData.author || '未知作者'}}</span>
|
|
|
|
|
+ <span> · 📅 ${{postData.publish_time || ''}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="post-detail-stats">
|
|
|
|
|
+ <span>👍 ${{postData.like_count || 0}}</span>
|
|
|
|
|
+ <span>💬 ${{postData.comment_count || 0}}</span>
|
|
|
|
|
+ <span>⭐ ${{postData.collect_count || 0}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="post-detail-body">
|
|
|
|
|
+ ${{postData.body_text ? `<div class="post-detail-desc">${{postData.body_text}}</div>` : '<div style="color: #9ca3af;">暂无正文</div>'}}
|
|
|
|
|
+ <div class="post-detail-images">
|
|
|
|
|
+ ${{imagesHtml}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="post-detail-footer">
|
|
|
|
|
+ <a href="${{postData.link || '#'}}" target="_blank" class="post-detail-link">
|
|
|
|
|
+ 在小红书查看完整内容 →
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ let modal = document.getElementById('postDetailModal');
|
|
|
|
|
+ if (!modal) {{
|
|
|
|
|
+ modal = document.createElement('div');
|
|
|
|
|
+ modal.id = 'postDetailModal';
|
|
|
|
|
+ modal.className = 'post-detail-modal';
|
|
|
|
|
+ modal.onclick = closePostDetail;
|
|
|
|
|
+ document.body.appendChild(modal);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ modal.innerHTML = modalHtml;
|
|
|
|
|
+ modal.classList.add('active');
|
|
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function closePostDetail(event) {{
|
|
|
|
|
+ if (event && event.target !== event.currentTarget) return;
|
|
|
|
|
+
|
|
|
|
|
+ const modal = document.getElementById('postDetailModal');
|
|
|
|
|
+ if (modal) {{
|
|
|
|
|
+ modal.classList.remove('active');
|
|
|
|
|
+ document.body.style.overflow = '';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
// 切换Tab
|
|
// 切换Tab
|
|
|
function switchTab(index) {{
|
|
function switchTab(index) {{
|
|
|
currentIndex = index;
|
|
currentIndex = index;
|
|
@@ -1148,20 +1478,30 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
const circleAreaWidth = width - 40;
|
|
const circleAreaWidth = width - 40;
|
|
|
const circleAreaCenterX = circleAreaWidth / 2;
|
|
const circleAreaCenterX = circleAreaWidth / 2;
|
|
|
|
|
|
|
|
- // 准备数据
|
|
|
|
|
- const nodes = data.nodes.map(n => ({{
|
|
|
|
|
- ...n,
|
|
|
|
|
- id: n.节点ID,
|
|
|
|
|
- source: n.节点ID.startsWith("帖子_") ? "帖子" : "人设",
|
|
|
|
|
- level: n.节点层级
|
|
|
|
|
- }}));
|
|
|
|
|
|
|
+ // 准备数据(过滤掉帖子点,只保留帖子标签和人设节点)
|
|
|
|
|
+ const nodes = data.nodes
|
|
|
|
|
+ .filter(n => !(n.节点ID.startsWith("帖子_") && n.节点类型 === "点"))
|
|
|
|
|
+ .map(n => ({{
|
|
|
|
|
+ ...n,
|
|
|
|
|
+ id: n.节点ID,
|
|
|
|
|
+ source: n.节点ID.startsWith("帖子_") ? "帖子" : "人设",
|
|
|
|
|
+ level: n.节点层级
|
|
|
|
|
+ }}));
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否是帖子点节点ID
|
|
|
|
|
+ function isPostPointId(id) {{
|
|
|
|
|
+ return id.startsWith("帖子_") && id.includes("_点_");
|
|
|
|
|
+ }}
|
|
|
|
|
|
|
|
- const links = data.edges.map(e => ({{
|
|
|
|
|
- ...e,
|
|
|
|
|
- source: e.源节点ID,
|
|
|
|
|
- target: e.目标节点ID,
|
|
|
|
|
- type: e.边类型
|
|
|
|
|
- }}));
|
|
|
|
|
|
|
+ // 过滤掉引用帖子点节点的边
|
|
|
|
|
+ const links = data.edges
|
|
|
|
|
+ .filter(e => !isPostPointId(e.源节点ID) && !isPostPointId(e.目标节点ID))
|
|
|
|
|
+ .map(e => ({{
|
|
|
|
|
+ ...e,
|
|
|
|
|
+ source: e.源节点ID,
|
|
|
|
|
+ target: e.目标节点ID,
|
|
|
|
|
+ type: e.边类型
|
|
|
|
|
+ }}));
|
|
|
|
|
|
|
|
// 分离节点类型
|
|
// 分离节点类型
|
|
|
const postNodes = nodes.filter(n => n.source === "帖子");
|
|
const postNodes = nodes.filter(n => n.source === "帖子");
|
|
@@ -1373,17 +1713,17 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
"关键点": "#9b59b6"
|
|
"关键点": "#9b59b6"
|
|
|
}};
|
|
}};
|
|
|
|
|
|
|
|
- // 获取节点的层级编号(四层结构)
|
|
|
|
|
|
|
+ // 获取节点的层级编号(三层结构:帖子标签、人设匹配1层、人设匹配2层)
|
|
|
function getNodeLayer(d) {{
|
|
function getNodeLayer(d) {{
|
|
|
if (d.source === "帖子") {{
|
|
if (d.source === "帖子") {{
|
|
|
- return d.节点类型 === "点" ? 0 : 1; // 点=0, 标签=1
|
|
|
|
|
|
|
+ return 0; // 帖子标签=0
|
|
|
}}
|
|
}}
|
|
|
// 人设节点:根据是否扩展分成两层
|
|
// 人设节点:根据是否扩展分成两层
|
|
|
- return d.是否扩展 ? 3 : 2; // 直接匹配=2, 扩展=3
|
|
|
|
|
|
|
+ return d.是否扩展 ? 2 : 1; // 直接匹配=1, 扩展=2
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // 统计每层节点数量(四层结构)
|
|
|
|
|
- const layerCounts = {{0: 0, 1: 0, 2: 0, 3: 0}};
|
|
|
|
|
|
|
+ // 统计每层节点数量(三层结构)
|
|
|
|
|
+ const layerCounts = {{0: 0, 1: 0, 2: 0}};
|
|
|
nodes.forEach(n => {{
|
|
nodes.forEach(n => {{
|
|
|
const layer = getNodeLayer(n);
|
|
const layer = getNodeLayer(n);
|
|
|
layerCounts[layer]++;
|
|
layerCounts[layer]++;
|
|
@@ -1391,8 +1731,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
console.log("每层节点数:", layerCounts);
|
|
console.log("每层节点数:", layerCounts);
|
|
|
|
|
|
|
|
// 根据节点数量计算每层圆的半径
|
|
// 根据节点数量计算每层圆的半径
|
|
|
- // 左列:帖子标签(上) + 人设匹配(下),右列:帖子点(同一行开始)
|
|
|
|
|
- // 左列是主要展示区域,视图中心对准左列
|
|
|
|
|
|
|
+ // 三层垂直排列:帖子标签(上)+ 人设匹配1层(中)+ 人设匹配2层(下)
|
|
|
const minRadius = 80;
|
|
const minRadius = 80;
|
|
|
const maxRadius = Math.min(circleAreaWidth / 3 - 20, 180);
|
|
const maxRadius = Math.min(circleAreaWidth / 3 - 20, 180);
|
|
|
const nodeSpacing = 60;
|
|
const nodeSpacing = 60;
|
|
@@ -1404,17 +1743,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return Math.max(minRadius, Math.min(maxRadius, r));
|
|
return Math.max(minRadius, Math.min(maxRadius, r));
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // 左列3层统一大小(取最大值)
|
|
|
|
|
- const leftColRadius = Math.max(
|
|
|
|
|
|
|
+ // 三层统一大小(取最大值)
|
|
|
|
|
+ const unifiedRadius = Math.max(
|
|
|
|
|
+ calcRadius(layerCounts[0]),
|
|
|
calcRadius(layerCounts[1]),
|
|
calcRadius(layerCounts[1]),
|
|
|
- calcRadius(layerCounts[2]),
|
|
|
|
|
- calcRadius(layerCounts[3])
|
|
|
|
|
|
|
+ calcRadius(layerCounts[2])
|
|
|
);
|
|
);
|
|
|
const layerRadius = {{
|
|
const layerRadius = {{
|
|
|
- 0: calcRadius(layerCounts[0]), // 帖子点(右列)
|
|
|
|
|
- 1: leftColRadius, // 帖子标签(左上)
|
|
|
|
|
- 2: leftColRadius, // 人设匹配1层(左中)
|
|
|
|
|
- 3: leftColRadius // 人设匹配2层(左下)
|
|
|
|
|
|
|
+ 0: unifiedRadius, // 帖子标签(上)
|
|
|
|
|
+ 1: unifiedRadius, // 人设匹配1层(中)
|
|
|
|
|
+ 2: unifiedRadius // 人设匹配2层(下)
|
|
|
}};
|
|
}};
|
|
|
console.log("每层半径:", layerRadius);
|
|
console.log("每层半径:", layerRadius);
|
|
|
|
|
|
|
@@ -1427,31 +1765,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
const egoRadius = 180;
|
|
const egoRadius = 180;
|
|
|
const egoAreaHeight = egoRadius * 2 + 80;
|
|
const egoAreaHeight = egoRadius * 2 + 80;
|
|
|
|
|
|
|
|
- // 计算布局:左列(帖子标签+人设匹配1层+人设匹配2层),右列(帖子点)
|
|
|
|
|
|
|
+ // 计算布局:三层垂直排列(帖子标签+人设匹配1层+人设匹配2层)
|
|
|
const layerPadding = 30;
|
|
const layerPadding = 30;
|
|
|
- const leftColHeight = layerRadius[1] * 2 + layerPadding + layerRadius[2] * 2 + layerPadding + layerRadius[3] * 2 + layerPadding;
|
|
|
|
|
- const circleHeight = leftColHeight;
|
|
|
|
|
|
|
+ const circleHeight = layerRadius[0] * 2 + layerPadding + layerRadius[1] * 2 + layerPadding + layerRadius[2] * 2 + layerPadding;
|
|
|
const height = Math.max(circleHeight + 80, treeHeight + 80, container.clientHeight);
|
|
const height = Math.max(circleHeight + 80, treeHeight + 80, container.clientHeight);
|
|
|
|
|
|
|
|
- // 计算每层圆心坐标
|
|
|
|
|
|
|
+ // 计算每层圆心坐标(垂直居中排列)
|
|
|
const layerCenterX = {{}};
|
|
const layerCenterX = {{}};
|
|
|
const layerCenterY = {{}};
|
|
const layerCenterY = {{}};
|
|
|
|
|
|
|
|
- // 左列居中显示,右列在旁边
|
|
|
|
|
- const leftColX = circleAreaCenterX; // 左列居中
|
|
|
|
|
- const rightColX = leftColX + Math.max(layerRadius[1], layerRadius[2], layerRadius[3]) + layerPadding + layerRadius[0] + 30; // 右列在右边
|
|
|
|
|
|
|
+ // 三层垂直排列,X坐标相同
|
|
|
|
|
+ const colX = circleAreaCenterX;
|
|
|
|
|
|
|
|
- // 左列:帖子标签(上)+ 人设匹配1层(中)+ 人设匹配2层(下)
|
|
|
|
|
- layerCenterX[1] = leftColX;
|
|
|
|
|
- layerCenterY[1] = layerRadius[1] + layerPadding / 2 + 20;
|
|
|
|
|
- layerCenterX[2] = leftColX;
|
|
|
|
|
|
|
+ // 帖子标签(上)+ 人设匹配1层(中)+ 人设匹配2层(下)
|
|
|
|
|
+ layerCenterX[0] = colX;
|
|
|
|
|
+ layerCenterY[0] = layerRadius[0] + layerPadding / 2 + 20;
|
|
|
|
|
+ layerCenterX[1] = colX;
|
|
|
|
|
+ layerCenterY[1] = layerCenterY[0] + layerRadius[0] + layerPadding + layerRadius[1];
|
|
|
|
|
+ layerCenterX[2] = colX;
|
|
|
layerCenterY[2] = layerCenterY[1] + layerRadius[1] + layerPadding + layerRadius[2];
|
|
layerCenterY[2] = layerCenterY[1] + layerRadius[1] + layerPadding + layerRadius[2];
|
|
|
- layerCenterX[3] = leftColX;
|
|
|
|
|
- layerCenterY[3] = layerCenterY[2] + layerRadius[2] + layerPadding + layerRadius[3];
|
|
|
|
|
-
|
|
|
|
|
- // 右列:帖子点(与帖子标签同一行开始)
|
|
|
|
|
- layerCenterX[0] = rightColX;
|
|
|
|
|
- layerCenterY[0] = layerCenterY[1]; // 与帖子标签同一行
|
|
|
|
|
|
|
|
|
|
console.log("每层圆心X:", layerCenterX);
|
|
console.log("每层圆心X:", layerCenterX);
|
|
|
console.log("每层圆心Y:", layerCenterY);
|
|
console.log("每层圆心Y:", layerCenterY);
|
|
@@ -1568,10 +1900,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
|
|
|
|
|
function force(alpha) {{
|
|
function force(alpha) {{
|
|
|
// 按层分组(三层)
|
|
// 按层分组(三层)
|
|
|
- const layerNodes = {{0: [], 1: [], 2: [], 3: []}};
|
|
|
|
|
|
|
+ const layerNodes = {{0: [], 1: [], 2: []}};
|
|
|
nodes.forEach(node => {{
|
|
nodes.forEach(node => {{
|
|
|
const layer = getNodeLayer(node);
|
|
const layer = getNodeLayer(node);
|
|
|
- layerNodes[layer].push(node);
|
|
|
|
|
|
|
+ if (layerNodes[layer]) layerNodes[layer].push(node);
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
// 对每层计算布局
|
|
// 对每层计算布局
|
|
@@ -1679,15 +2011,195 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return "match";
|
|
return "match";
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
|
|
+ // ===== 在上方绘制帖子树(只是展示用)=====
|
|
|
|
|
+ const postDetail = data.postDetail || {{}};
|
|
|
|
|
+ window.currentPostDetail = postDetail;
|
|
|
|
|
+
|
|
|
|
|
+ // 使用预构建的帖子树数据
|
|
|
|
|
+ const postTree = data.postTree || {{ root: null }};
|
|
|
|
|
+
|
|
|
|
|
+ // 帖子树D3布局(只在有帖子树数据时绘制)
|
|
|
|
|
+ const postTreeGroup = g.append("g").attr("class", "post-tree");
|
|
|
|
|
+ let postTreeActualHeight = 0;
|
|
|
|
|
+ let postTreeOffsetY = 60;
|
|
|
|
|
+ let postRoot = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (postTree && postTree.root) {{
|
|
|
|
|
+ const postTreeLayout = d3.tree()
|
|
|
|
|
+ .nodeSize([70, 80])
|
|
|
|
|
+ .separation((a, b) => a.parent === b.parent ? 1.2 : 1.5);
|
|
|
|
|
+
|
|
|
|
|
+ postRoot = d3.hierarchy(postTree.root);
|
|
|
|
|
+ postTreeLayout(postRoot);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算树的实际边界
|
|
|
|
|
+ let treeMinX = Infinity, treeMaxX = -Infinity, treeMinY = Infinity, treeMaxY = -Infinity;
|
|
|
|
|
+ postRoot.descendants().forEach(d => {{
|
|
|
|
|
+ treeMinX = Math.min(treeMinX, d.x);
|
|
|
|
|
+ treeMaxX = Math.max(treeMaxX, d.x);
|
|
|
|
|
+ treeMinY = Math.min(treeMinY, d.y);
|
|
|
|
|
+ treeMaxY = Math.max(treeMaxY, d.y);
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 帖子树居中,放在圆的上方
|
|
|
|
|
+ const postTreeOffsetX = circleAreaCenterX - (treeMinX + treeMaxX) / 2;
|
|
|
|
|
+ postTreeGroup.attr("transform", `translate(${{postTreeOffsetX}}, ${{postTreeOffsetY}})`);
|
|
|
|
|
+
|
|
|
|
|
+ // 卡片尺寸(提前定义,边绘制时需要用)
|
|
|
|
|
+ const cardWidth = 240;
|
|
|
|
|
+ const cardHeight = 90;
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制帖子树边(属于边,紫色,粗细根据权重)
|
|
|
|
|
+ const postTreeLinks = postTreeGroup.selectAll(".post-tree-link")
|
|
|
|
|
+ .data(postRoot.links())
|
|
|
|
|
+ .enter()
|
|
|
|
|
+ .append("g")
|
|
|
|
|
+ .attr("class", "post-tree-link-group");
|
|
|
|
|
+
|
|
|
|
|
+ postTreeLinks.append("path")
|
|
|
|
|
+ .attr("class", "post-tree-link")
|
|
|
|
|
+ .attr("d", d => {{
|
|
|
|
|
+ // 如果源节点是根节点(帖子卡片),从卡片下边框中心出发
|
|
|
|
|
+ const sourceY = d.source.data.isRoot ? d.source.y + cardHeight/2 : d.source.y;
|
|
|
|
|
+ return `M${{d.source.x}},${{sourceY}}
|
|
|
|
|
+ C${{d.source.x}},${{(sourceY + d.target.y) / 2}}
|
|
|
|
|
+ ${{d.target.x}},${{(sourceY + d.target.y) / 2}}
|
|
|
|
|
+ ${{d.target.x}},${{d.target.y}}`;
|
|
|
|
|
+ }})
|
|
|
|
|
+ .attr("fill", "none")
|
|
|
|
|
+ .attr("stroke", "#9b59b6")
|
|
|
|
|
+ .attr("stroke-width", 1.5)
|
|
|
|
|
+ .attr("stroke-opacity", 0.6);
|
|
|
|
|
+
|
|
|
|
|
+ // 在点->标签的边上显示权重(浮点数)
|
|
|
|
|
+ postTreeLinks.filter(d => d.target.data.nodeType === "标签" && d.target.data.weight > 0)
|
|
|
|
|
+ .append("text")
|
|
|
|
|
+ .attr("x", d => (d.source.x + d.target.x) / 2)
|
|
|
|
|
+ .attr("y", d => (d.source.y + d.target.y) / 2)
|
|
|
|
|
+ .attr("text-anchor", "middle")
|
|
|
|
|
+ .attr("fill", "#9b59b6")
|
|
|
|
|
+ .attr("font-size", "9px")
|
|
|
|
|
+ .attr("font-weight", "bold")
|
|
|
|
|
+ .text(d => d.target.data.weight.toFixed(1));
|
|
|
|
|
+
|
|
|
|
|
+ // 六边形路径生成函数
|
|
|
|
|
+ function hexagonPath(r) {{
|
|
|
|
|
+ const a = Math.PI / 3;
|
|
|
|
|
+ let path = "";
|
|
|
|
|
+ for (let i = 0; i < 6; i++) {{
|
|
|
|
|
+ const angle = a * i - Math.PI / 2;
|
|
|
|
|
+ const x = r * Math.cos(angle);
|
|
|
|
|
+ const y = r * Math.sin(angle);
|
|
|
|
|
+ path += (i === 0 ? "M" : "L") + x + "," + y;
|
|
|
|
|
+ }}
|
|
|
|
|
+ return path + "Z";
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制帖子树节点(非根节点)
|
|
|
|
|
+ const postTreeNodes = postTreeGroup.selectAll(".post-tree-node")
|
|
|
|
|
+ .data(postRoot.descendants().filter(d => !d.data.isRoot))
|
|
|
|
|
+ .enter()
|
|
|
|
|
+ .append("g")
|
|
|
|
|
+ .attr("class", "post-tree-node")
|
|
|
|
|
+ .attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
|
|
|
|
+
|
|
|
|
|
+ // 节点形状(维度=大圆,点=六边形,标签=小圆)
|
|
|
|
|
+ postTreeNodes.each(function(d) {{
|
|
|
|
|
+ const node = d3.select(this);
|
|
|
|
|
+ if (d.data.nodeType === "点") {{
|
|
|
|
|
+ // 点节点用六边形
|
|
|
|
|
+ node.append("path")
|
|
|
|
|
+ .attr("d", hexagonPath(6))
|
|
|
|
|
+ .attr("fill", d.data.dimColor || "#888")
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 1);
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ // 维度和标签用圆形
|
|
|
|
|
+ node.append("circle")
|
|
|
|
|
+ .attr("r", d.data.isDimension ? 7 : 3)
|
|
|
|
|
+ .attr("fill", d.data.dimColor || "#888")
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 1);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 节点标签
|
|
|
|
|
+ postTreeNodes.append("text")
|
|
|
|
|
+ .attr("dy", d => d.children ? -10 : 12)
|
|
|
|
|
+ .attr("text-anchor", "middle")
|
|
|
|
|
+ .attr("fill", d => d.data.dimColor || "#ccc")
|
|
|
|
|
+ .attr("font-size", d => {{
|
|
|
|
|
+ if (d.data.isDimension) return "11px";
|
|
|
|
|
+ if (d.data.nodeType === "点") return "10px";
|
|
|
|
|
+ return "8px";
|
|
|
|
|
+ }})
|
|
|
|
|
+ .attr("font-weight", d => d.data.isDimension ? "bold" : "normal")
|
|
|
|
|
+ .text(d => d.data.name || "");
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制根节点卡片(帖子详情卡片)
|
|
|
|
|
+ const postRootNode = postRoot;
|
|
|
|
|
+ const cardX = postRootNode.x - cardWidth/2;
|
|
|
|
|
+ const cardY = postRootNode.y - cardHeight/2;
|
|
|
|
|
+
|
|
|
|
|
+ const rootCard = postTreeGroup.append("foreignObject")
|
|
|
|
|
+ .attr("x", cardX)
|
|
|
|
|
+ .attr("y", cardY)
|
|
|
|
|
+ .attr("width", cardWidth)
|
|
|
|
|
+ .attr("height", cardHeight);
|
|
|
|
|
+
|
|
|
|
|
+ const images = postDetail.images || [];
|
|
|
|
|
+ const thumbnail = images.length > 0 ? images[0] : "";
|
|
|
|
|
+ const title = postDetail.title || "未知标题";
|
|
|
|
|
+ const likeCount = postDetail.like_count || 0;
|
|
|
|
|
+ const collectCount = postDetail.collect_count || 0;
|
|
|
|
|
+ const imageCount = images.length || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const thumbnailHtml = thumbnail
|
|
|
|
|
+ ? `<div class="post-card-thumbnail-wrapper" style="width:60px;height:60px;">
|
|
|
|
|
+ <img class="post-card-thumbnail" style="width:60px;height:60px;" src="${{thumbnail}}" alt="封面" onerror="this.style.display='none'"/>
|
|
|
|
|
+ ${{imageCount > 1 ? `<span class="post-card-image-count">${{imageCount}}图</span>` : ''}}
|
|
|
|
|
+ </div>`
|
|
|
|
|
+ : `<div class="post-card-thumbnail-placeholder" style="width:60px;height:60px;font-size:20px;">📝</div>`;
|
|
|
|
|
+
|
|
|
|
|
+ const cardHtml = `
|
|
|
|
|
+ <div class="post-card" xmlns="http://www.w3.org/1999/xhtml" style="padding:10px;" onclick="showPostDetail(window.currentPostDetail)">
|
|
|
|
|
+ <div class="post-card-header">
|
|
|
|
|
+ ${{thumbnailHtml}}
|
|
|
|
|
+ <div class="post-card-info">
|
|
|
|
|
+ <div class="post-card-title" style="font-size:12px;-webkit-line-clamp:2;" title="${{title}}">${{title}}</div>
|
|
|
|
|
+ <div class="post-card-stats" style="font-size:10px;">
|
|
|
|
|
+ <span>❤️ ${{likeCount}}</span>
|
|
|
|
|
+ <span>⭐ ${{collectCount}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ rootCard.append("xhtml:div").html(cardHtml);
|
|
|
|
|
+
|
|
|
|
|
+ postTreeActualHeight = treeMaxY - treeMinY;
|
|
|
|
|
+ }} // end if (postTree && postTree.root)
|
|
|
|
|
+
|
|
|
|
|
+ // 根据帖子树高度,调整圆的Y位置
|
|
|
|
|
+ const postTreeTotalHeight = postTreeOffsetY + postTreeActualHeight + 80;
|
|
|
|
|
+ const circleYOffset = postTreeTotalHeight;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新圆心Y坐标(三层)
|
|
|
|
|
+ layerCenterY[0] += circleYOffset;
|
|
|
|
|
+ layerCenterY[1] += circleYOffset;
|
|
|
|
|
+ layerCenterY[2] += circleYOffset;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新SVG高度
|
|
|
|
|
+ svg.attr("height", height + circleYOffset);
|
|
|
|
|
+
|
|
|
// 绘制层背景(圆形,使用动态大小)
|
|
// 绘制层背景(圆形,使用动态大小)
|
|
|
const layerBg = g.append("g").attr("class", "layer-backgrounds");
|
|
const layerBg = g.append("g").attr("class", "layer-backgrounds");
|
|
|
|
|
|
|
|
- // 层配置:名称、颜色(三层圆,关系图在左侧)
|
|
|
|
|
|
|
+ // 层配置:名称、颜色(三层圆)
|
|
|
const layerConfig = [
|
|
const layerConfig = [
|
|
|
- {{ name: "帖子点", layer: 0, color: "rgba(243, 156, 18, 0.08)", stroke: "rgba(243, 156, 18, 0.3)" }},
|
|
|
|
|
- {{ name: "帖子标签", layer: 1, color: "rgba(52, 152, 219, 0.08)", stroke: "rgba(52, 152, 219, 0.3)" }},
|
|
|
|
|
- {{ name: "人设匹配_1层", layer: 2, color: "rgba(155, 89, 182, 0.08)", stroke: "rgba(155, 89, 182, 0.3)" }},
|
|
|
|
|
- {{ name: "人设匹配_2层", layer: 3, color: "rgba(46, 204, 113, 0.08)", stroke: "rgba(46, 204, 113, 0.3)" }}
|
|
|
|
|
|
|
+ {{ name: "帖子标签", layer: 0, color: "rgba(52, 152, 219, 0.08)", stroke: "rgba(52, 152, 219, 0.3)" }},
|
|
|
|
|
+ {{ name: "人设匹配_1层", layer: 1, color: "rgba(155, 89, 182, 0.08)", stroke: "rgba(155, 89, 182, 0.3)" }},
|
|
|
|
|
+ {{ name: "人设匹配_2层", layer: 2, color: "rgba(46, 204, 113, 0.08)", stroke: "rgba(46, 204, 113, 0.3)" }}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
// 绘制三层圆形背景
|
|
// 绘制三层圆形背景
|
|
@@ -3803,10 +4315,30 @@ def main():
|
|
|
|
|
|
|
|
print()
|
|
print()
|
|
|
|
|
|
|
|
|
|
+ # 读取帖子树数据
|
|
|
|
|
+ post_trees_file = match_graph_dir / "post_trees.json"
|
|
|
|
|
+ post_trees_data = {} # postId -> postTree
|
|
|
|
|
+
|
|
|
|
|
+ if post_trees_file.exists():
|
|
|
|
|
+ print(f"读取帖子树数据: {post_trees_file.name}")
|
|
|
|
|
+ with open(post_trees_file, "r", encoding="utf-8") as f:
|
|
|
|
|
+ trees_data = json.load(f)
|
|
|
|
|
+ for tree in trees_data.get("postTrees", []):
|
|
|
|
|
+ post_trees_data[tree["postId"]] = tree
|
|
|
|
|
+ print(f" 帖子树数量: {len(post_trees_data)}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"警告: 帖子树数据文件不存在: {post_trees_file}")
|
|
|
|
|
+ print(" 请先运行 build_post_tree.py 生成帖子树数据")
|
|
|
|
|
+
|
|
|
|
|
+ print()
|
|
|
|
|
+
|
|
|
# 读取所有匹配图谱文件
|
|
# 读取所有匹配图谱文件
|
|
|
graph_files = sorted(match_graph_dir.glob("*_match_graph.json"))
|
|
graph_files = sorted(match_graph_dir.glob("*_match_graph.json"))
|
|
|
print(f"找到 {len(graph_files)} 个匹配图谱文件")
|
|
print(f"找到 {len(graph_files)} 个匹配图谱文件")
|
|
|
|
|
|
|
|
|
|
+ # results目录(存放完整帖子详情)
|
|
|
|
|
+ results_dir = config.intermediate_dir.parent / "results"
|
|
|
|
|
+
|
|
|
all_graph_data = []
|
|
all_graph_data = []
|
|
|
for i, graph_file in enumerate(graph_files, 1):
|
|
for i, graph_file in enumerate(graph_files, 1):
|
|
|
print(f" [{i}/{len(graph_files)}] 读取: {graph_file.name}")
|
|
print(f" [{i}/{len(graph_files)}] 读取: {graph_file.name}")
|
|
@@ -3814,14 +4346,35 @@ def main():
|
|
|
with open(graph_file, "r", encoding="utf-8") as f:
|
|
with open(graph_file, "r", encoding="utf-8") as f:
|
|
|
match_graph_data = json.load(f)
|
|
match_graph_data = json.load(f)
|
|
|
|
|
|
|
|
|
|
+ post_id = match_graph_data["说明"]["帖子ID"]
|
|
|
|
|
+
|
|
|
|
|
+ # 尝试读取完整帖子详情
|
|
|
|
|
+ post_detail = {
|
|
|
|
|
+ "title": match_graph_data["说明"].get("帖子标题", ""),
|
|
|
|
|
+ "post_id": post_id
|
|
|
|
|
+ }
|
|
|
|
|
+ how_file = results_dir / f"{post_id}_how.json"
|
|
|
|
|
+ if how_file.exists():
|
|
|
|
|
+ with open(how_file, "r", encoding="utf-8") as f:
|
|
|
|
|
+ how_data = json.load(f)
|
|
|
|
|
+ if "帖子详情" in how_data:
|
|
|
|
|
+ post_detail = how_data["帖子详情"]
|
|
|
|
|
+ post_detail["post_id"] = post_id
|
|
|
|
|
+
|
|
|
|
|
+ # 获取预构建的帖子树数据
|
|
|
|
|
+ post_tree = post_trees_data.get(post_id, {})
|
|
|
|
|
+
|
|
|
# 提取需要的数据
|
|
# 提取需要的数据
|
|
|
graph_data = {
|
|
graph_data = {
|
|
|
- "postId": match_graph_data["说明"]["帖子ID"],
|
|
|
|
|
|
|
+ "postId": post_id,
|
|
|
"postTitle": match_graph_data["说明"].get("帖子标题", ""),
|
|
"postTitle": match_graph_data["说明"].get("帖子标题", ""),
|
|
|
"stats": match_graph_data["说明"]["统计"],
|
|
"stats": match_graph_data["说明"]["统计"],
|
|
|
"nodes": match_graph_data["节点列表"],
|
|
"nodes": match_graph_data["节点列表"],
|
|
|
"edges": match_graph_data["边列表"],
|
|
"edges": match_graph_data["边列表"],
|
|
|
- "personaEdgeToMirrorEdges": match_graph_data.get("人设边到镜像边映射", {})
|
|
|
|
|
|
|
+ "personaEdgeToMirrorEdges": match_graph_data.get("人设边到镜像边映射", {}),
|
|
|
|
|
+ # 预构建的帖子树数据
|
|
|
|
|
+ "postTree": post_tree,
|
|
|
|
|
+ "postDetail": post_tree.get("postDetail", post_detail)
|
|
|
}
|
|
}
|
|
|
all_graph_data.append(graph_data)
|
|
all_graph_data.append(graph_data)
|
|
|
|
|
|