aifastdb-devplan 1.5.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autopilot.d.ts +58 -0
- package/dist/autopilot.d.ts.map +1 -0
- package/dist/autopilot.js +250 -0
- package/dist/autopilot.js.map +1 -0
- package/dist/dev-plan-document-store.d.ts +15 -1
- package/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +122 -0
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-factory.d.ts +69 -3
- package/dist/dev-plan-factory.d.ts.map +1 -1
- package/dist/dev-plan-factory.js +113 -19
- package/dist/dev-plan-factory.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +79 -1
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +420 -3
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/dev-plan-interface.d.ts +24 -1
- package/dist/dev-plan-interface.d.ts.map +1 -1
- package/dist/dev-plan-migrate.d.ts +1 -0
- package/dist/dev-plan-migrate.d.ts.map +1 -1
- package/dist/dev-plan-migrate.js +28 -2
- package/dist/dev-plan-migrate.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +397 -4
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/types.d.ts +160 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
- package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/api-compat.js +344 -0
- package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
- package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
- package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/clusterer.js +460 -0
- package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
- package/dist/visualize/graph-canvas/core.d.ts +11 -0
- package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/core.js +1136 -0
- package/dist/visualize/graph-canvas/core.js.map +1 -0
- package/dist/visualize/graph-canvas/index.d.ts +22 -0
- package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/index.js +69 -0
- package/dist/visualize/graph-canvas/index.js.map +1 -0
- package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
- package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/interaction.js +457 -0
- package/dist/visualize/graph-canvas/interaction.js.map +1 -0
- package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
- package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/layout-worker.js +577 -0
- package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
- package/dist/visualize/graph-canvas/lod.d.ts +10 -0
- package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/lod.js +111 -0
- package/dist/visualize/graph-canvas/lod.js.map +1 -0
- package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
- package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/renderer.js +813 -0
- package/dist/visualize/graph-canvas/renderer.js.map +1 -0
- package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
- package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/spatial-index.js +482 -0
- package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
- package/dist/visualize/graph-canvas/styles.d.ts +11 -0
- package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/styles.js +152 -0
- package/dist/visualize/graph-canvas/styles.js.map +1 -0
- package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
- package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/viewport.js +385 -0
- package/dist/visualize/graph-canvas/viewport.js.map +1 -0
- package/dist/visualize/server.js +737 -7
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template-core.d.ts +9 -0
- package/dist/visualize/template-core.d.ts.map +1 -0
- package/dist/visualize/template-core.js +714 -0
- package/dist/visualize/template-core.js.map +1 -0
- package/dist/visualize/template-data-loading.d.ts +7 -0
- package/dist/visualize/template-data-loading.d.ts.map +1 -0
- package/dist/visualize/template-data-loading.js +677 -0
- package/dist/visualize/template-data-loading.js.map +1 -0
- package/dist/visualize/template-detail-panel.d.ts +14 -0
- package/dist/visualize/template-detail-panel.d.ts.map +1 -0
- package/dist/visualize/template-detail-panel.js +553 -0
- package/dist/visualize/template-detail-panel.js.map +1 -0
- package/dist/visualize/template-graph-3d.d.ts +7 -0
- package/dist/visualize/template-graph-3d.d.ts.map +1 -0
- package/dist/visualize/template-graph-3d.js +1112 -0
- package/dist/visualize/template-graph-3d.js.map +1 -0
- package/dist/visualize/template-graph-vis.d.ts +8 -0
- package/dist/visualize/template-graph-vis.d.ts.map +1 -0
- package/dist/visualize/template-graph-vis.js +1204 -0
- package/dist/visualize/template-graph-vis.js.map +1 -0
- package/dist/visualize/template-html.d.ts +9 -0
- package/dist/visualize/template-html.d.ts.map +1 -0
- package/dist/visualize/template-html.js +484 -0
- package/dist/visualize/template-html.js.map +1 -0
- package/dist/visualize/template-pages.d.ts +7 -0
- package/dist/visualize/template-pages.d.ts.map +1 -0
- package/dist/visualize/template-pages.js +806 -0
- package/dist/visualize/template-pages.js.map +1 -0
- package/dist/visualize/template-stats-modal.d.ts +7 -0
- package/dist/visualize/template-stats-modal.d.ts.map +1 -0
- package/dist/visualize/template-stats-modal.js +406 -0
- package/dist/visualize/template-stats-modal.js.map +1 -0
- package/dist/visualize/template-styles.d.ts +9 -0
- package/dist/visualize/template-styles.d.ts.map +1 -0
- package/dist/visualize/template-styles.js +487 -0
- package/dist/visualize/template-styles.js.map +1 -0
- package/dist/visualize/template.d.ts +14 -3
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +38 -2889
- package/dist/visualize/template.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevPlan 图可视化 — vis-network 渲染模块
|
|
4
|
+
*
|
|
5
|
+
* 包含: 边高亮、文档节点展开/收起、呼吸灯动画、节点样式、
|
|
6
|
+
* 节点动态大小、vis-network 图渲染、节点类型筛选。
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getGraphVisScript = getGraphVisScript;
|
|
10
|
+
function getGraphVisScript() {
|
|
11
|
+
return `
|
|
12
|
+
// ========== 边高亮:选中节点时关联边变色,取消选中时恢复灰色 ==========
|
|
13
|
+
function highlightConnectedEdges(nodeId) {
|
|
14
|
+
if (!edgesDataSet || !network || typeof network.getConnectedEdges !== 'function') return;
|
|
15
|
+
var connectedEdgeIds = network.getConnectedEdges(nodeId);
|
|
16
|
+
var connectedSet = {};
|
|
17
|
+
for (var i = 0; i < connectedEdgeIds.length; i++) connectedSet[connectedEdgeIds[i]] = true;
|
|
18
|
+
var updates = [];
|
|
19
|
+
edgesDataSet.forEach(function(edge) {
|
|
20
|
+
if (connectedSet[edge.id]) {
|
|
21
|
+
// 关联边 → 使用高亮色
|
|
22
|
+
updates.push({ id: edge.id, color: { color: edge._highlightColor || '#9ca3af', highlight: edge._highlightColor || '#9ca3af', hover: edge._highlightColor || '#9ca3af' }, width: (edge._origWidth || 1) < 2 ? 2 : (edge._origWidth || edge.width || 1) });
|
|
23
|
+
} else {
|
|
24
|
+
// 非关联边 → 变淡
|
|
25
|
+
updates.push({ id: edge.id, color: { color: 'rgba(75,85,99,0.15)', highlight: edge._highlightColor || '#9ca3af', hover: edge._highlightColor || '#9ca3af' }, width: edge._origWidth || edge.width || 1 });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
edgesDataSet.update(updates);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resetAllEdgeColors() {
|
|
32
|
+
if (!edgesDataSet) return;
|
|
33
|
+
var updates = [];
|
|
34
|
+
edgesDataSet.forEach(function(edge) {
|
|
35
|
+
updates.push({ id: edge.id, color: { color: EDGE_GRAY, highlight: edge._highlightColor || '#9ca3af', hover: edge._highlightColor || '#9ca3af' }, width: edge._origWidth || edge.width || 1 });
|
|
36
|
+
});
|
|
37
|
+
edgesDataSet.update(updates);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// ========== 文档节点展开/收起 ==========
|
|
42
|
+
/** 记录哪些父文档节点处于收起状态(nodeId → true 表示收起) */
|
|
43
|
+
var collapsedDocNodes = {};
|
|
44
|
+
/** 收起时被重定向的边信息: { edgeId: { origFrom, origTo } } */
|
|
45
|
+
var redirectedEdges = {};
|
|
46
|
+
/** 记录各父文档 +/- 按钮在 canvas 坐标系中的位置,用于点击检测 */
|
|
47
|
+
var docToggleBtnPositions = {};
|
|
48
|
+
/** 收起前保存子文档节点的位置: { nodeId: { x, y } } */
|
|
49
|
+
var savedChildPositions = {};
|
|
50
|
+
|
|
51
|
+
/** 获取节点 ID 对应的子文档节点 ID 列表(仅直接子文档) */
|
|
52
|
+
function getChildDocNodeIds(parentNodeId) {
|
|
53
|
+
var childIds = [];
|
|
54
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
55
|
+
if (allEdges[i].from === parentNodeId && allEdges[i].label === 'doc_has_child') {
|
|
56
|
+
childIds.push(allEdges[i].to);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return childIds;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 递归获取所有后代文档节点 ID(含多层子文档) */
|
|
63
|
+
function getAllDescendantDocNodeIds(parentNodeId) {
|
|
64
|
+
var result = [];
|
|
65
|
+
var queue = [parentNodeId];
|
|
66
|
+
while (queue.length > 0) {
|
|
67
|
+
var current = queue.shift();
|
|
68
|
+
var children = getChildDocNodeIds(current);
|
|
69
|
+
for (var i = 0; i < children.length; i++) {
|
|
70
|
+
result.push(children[i]);
|
|
71
|
+
queue.push(children[i]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 检查节点是否为父文档(有子文档的文档节点) */
|
|
78
|
+
function isParentDocNode(node) {
|
|
79
|
+
if (node.type !== 'document') return false;
|
|
80
|
+
var props = node.properties || {};
|
|
81
|
+
var childDocs = props.childDocs || [];
|
|
82
|
+
if (childDocs.length > 0) return true;
|
|
83
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
84
|
+
if (allEdges[i].from === node.id && allEdges[i].label === 'doc_has_child') return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 通过 nodeId 在 allNodes 中查找节点数据 */
|
|
90
|
+
function findAllNode(nodeId) {
|
|
91
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
92
|
+
if (allNodes[i].id === nodeId) return allNodes[i];
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 检查节点是否应被隐藏(因为其祖先父文档处于收起状态) */
|
|
98
|
+
function isNodeCollapsedByParent(nodeId) {
|
|
99
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
100
|
+
var e = allEdges[i];
|
|
101
|
+
if (e.to === nodeId && e.label === 'doc_has_child') {
|
|
102
|
+
if (collapsedDocNodes[e.from]) return true;
|
|
103
|
+
if (isNodeCollapsedByParent(e.from)) return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 切换父文档节点的展开/收起状态 */
|
|
110
|
+
function toggleDocNodeExpand(nodeId) {
|
|
111
|
+
collapsedDocNodes[nodeId] = !collapsedDocNodes[nodeId];
|
|
112
|
+
var childIds = getAllDescendantDocNodeIds(nodeId);
|
|
113
|
+
var isCollapsed = collapsedDocNodes[nodeId];
|
|
114
|
+
|
|
115
|
+
if (isCollapsed) {
|
|
116
|
+
// ---- 收起 ----
|
|
117
|
+
var removeNodeIds = {};
|
|
118
|
+
for (var i = 0; i < childIds.length; i++) removeNodeIds[childIds[i]] = true;
|
|
119
|
+
|
|
120
|
+
// 0) 保存子文档节点当前位置
|
|
121
|
+
var childPositions = network.getPositions(childIds);
|
|
122
|
+
for (var i = 0; i < childIds.length; i++) {
|
|
123
|
+
if (childPositions[childIds[i]]) {
|
|
124
|
+
savedChildPositions[childIds[i]] = { x: childPositions[childIds[i]].x, y: childPositions[childIds[i]].y };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 1) 将连接到子文档的非 doc_has_child 边重定向到父文档
|
|
129
|
+
var edgesToRedirect = [];
|
|
130
|
+
var edgesToRemove = [];
|
|
131
|
+
edgesDataSet.forEach(function(edge) {
|
|
132
|
+
var touchesChild = removeNodeIds[edge.from] || removeNodeIds[edge.to];
|
|
133
|
+
if (!touchesChild) return;
|
|
134
|
+
if (edge._label === 'doc_has_child') {
|
|
135
|
+
// doc_has_child 边直接移除
|
|
136
|
+
edgesToRemove.push(edge.id);
|
|
137
|
+
} else {
|
|
138
|
+
// 其他边(如 task_has_doc)重定向到父文档
|
|
139
|
+
edgesToRedirect.push(edge);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 移除 doc_has_child 边
|
|
144
|
+
if (edgesToRemove.length > 0) edgesDataSet.remove(edgesToRemove);
|
|
145
|
+
|
|
146
|
+
// 重定向其他边到父文档
|
|
147
|
+
for (var i = 0; i < edgesToRedirect.length; i++) {
|
|
148
|
+
var edge = edgesToRedirect[i];
|
|
149
|
+
var newFrom = removeNodeIds[edge.from] ? nodeId : edge.from;
|
|
150
|
+
var newTo = removeNodeIds[edge.to] ? nodeId : edge.to;
|
|
151
|
+
// 检查是否已存在相同的重定向边(避免重复)
|
|
152
|
+
var duplicate = false;
|
|
153
|
+
edgesDataSet.forEach(function(existing) {
|
|
154
|
+
if (existing.from === newFrom && existing.to === newTo && existing._label === edge._label) duplicate = true;
|
|
155
|
+
});
|
|
156
|
+
if (newFrom === newTo) { duplicate = true; } // 不自连
|
|
157
|
+
if (!duplicate) {
|
|
158
|
+
redirectedEdges[edge.id] = { origFrom: edge.from, origTo: edge.to };
|
|
159
|
+
edgesDataSet.update({ id: edge.id, from: newFrom, to: newTo });
|
|
160
|
+
} else {
|
|
161
|
+
// 重复则移除
|
|
162
|
+
redirectedEdges[edge.id] = { origFrom: edge.from, origTo: edge.to };
|
|
163
|
+
edgesDataSet.remove([edge.id]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2) 移除子文档节点
|
|
168
|
+
nodesDataSet.remove(childIds);
|
|
169
|
+
|
|
170
|
+
// 3) 更新父节点标签(加左侧留白和收起数量提示)
|
|
171
|
+
var parentNode = nodesDataSet.get(nodeId);
|
|
172
|
+
if (parentNode) {
|
|
173
|
+
var origLabel = parentNode._origLabel || parentNode.label;
|
|
174
|
+
var pad = ' ';
|
|
175
|
+
nodesDataSet.update({ id: nodeId, label: pad + origLabel + ' [' + childIds.length + ']', _origLabel: origLabel });
|
|
176
|
+
}
|
|
177
|
+
log('收起文档: 隐藏 ' + childIds.length + ' 个子文档, 重定向 ' + edgesToRedirect.length + ' 条边', true);
|
|
178
|
+
|
|
179
|
+
} else {
|
|
180
|
+
// ---- 展开 ----
|
|
181
|
+
// 1) 恢复被重定向的边
|
|
182
|
+
var restoreEdgeIds = [];
|
|
183
|
+
for (var eid in redirectedEdges) {
|
|
184
|
+
var info = redirectedEdges[eid];
|
|
185
|
+
// 检查 origFrom 或 origTo 是否属于此父文档的子孙
|
|
186
|
+
var isRelated = false;
|
|
187
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
188
|
+
if (info.origFrom === childIds[ci] || info.origTo === childIds[ci]) { isRelated = true; break; }
|
|
189
|
+
}
|
|
190
|
+
if (!isRelated) continue;
|
|
191
|
+
restoreEdgeIds.push(eid);
|
|
192
|
+
// 恢复原始 from/to 或重新添加
|
|
193
|
+
var existing = edgesDataSet.get(eid);
|
|
194
|
+
if (existing) {
|
|
195
|
+
edgesDataSet.update({ id: eid, from: info.origFrom, to: info.origTo });
|
|
196
|
+
} else {
|
|
197
|
+
// 边已被移除(因重复),需重新添加
|
|
198
|
+
// 在 allEdges 中找到此边原始数据
|
|
199
|
+
for (var ai = 0; ai < allEdges.length; ai++) {
|
|
200
|
+
var ae = allEdges[ai];
|
|
201
|
+
if (ae.from === info.origFrom && ae.to === info.origTo) {
|
|
202
|
+
var es = edgeStyle(ae);
|
|
203
|
+
edgesDataSet.add({ id: eid, from: ae.from, to: ae.to, width: es.width, _origWidth: es.width, color: es.color, dashes: es.dashes, arrows: es.arrows, _label: ae.label, _highlightColor: es._highlightColor || '#9ca3af' });
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (var ri = 0; ri < restoreEdgeIds.length; ri++) {
|
|
210
|
+
delete redirectedEdges[restoreEdgeIds[ri]];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 2) 重新添加子文档节点(使用保存的位置或思维导图排列)
|
|
214
|
+
var parentPos = network.getPositions([nodeId])[nodeId];
|
|
215
|
+
var addNodes = [];
|
|
216
|
+
var visibleChildIds = [];
|
|
217
|
+
for (var ni = 0; ni < allNodes.length; ni++) {
|
|
218
|
+
var n = allNodes[ni];
|
|
219
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
220
|
+
if (n.id === childIds[ci] && !isNodeCollapsedByParent(n.id)) {
|
|
221
|
+
var deg = getNodeDegree(n);
|
|
222
|
+
var s = nodeStyle(n, deg);
|
|
223
|
+
var nodeData = { id: n.id, label: n.label, _origLabel: n.label, title: n.label + ' (连接: ' + deg + ')', shape: s.shape, size: s.size, color: s.color, font: s.font, borderWidth: s.borderWidth, _type: n.type, _props: n.properties || {} };
|
|
224
|
+
// 使用保存的位置
|
|
225
|
+
if (savedChildPositions[n.id]) {
|
|
226
|
+
nodeData.x = savedChildPositions[n.id].x;
|
|
227
|
+
nodeData.y = savedChildPositions[n.id].y;
|
|
228
|
+
}
|
|
229
|
+
addNodes.push(nodeData);
|
|
230
|
+
visibleChildIds.push(n.id);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (addNodes.length > 0) {
|
|
236
|
+
nodesDataSet.add(addNodes);
|
|
237
|
+
// 如果没有保存位置,按思维导图方式排列
|
|
238
|
+
var needArrange = false;
|
|
239
|
+
for (var i = 0; i < visibleChildIds.length; i++) {
|
|
240
|
+
if (!savedChildPositions[visibleChildIds[i]]) { needArrange = true; break; }
|
|
241
|
+
}
|
|
242
|
+
if (needArrange && parentPos) {
|
|
243
|
+
arrangeDocMindMap(nodeId, visibleChildIds);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 3) 重新添加 doc_has_child 边
|
|
248
|
+
var addedNodeIds = {};
|
|
249
|
+
nodesDataSet.forEach(function(n) { addedNodeIds[n.id] = true; });
|
|
250
|
+
var addEdges = [];
|
|
251
|
+
for (var ei = 0; ei < allEdges.length; ei++) {
|
|
252
|
+
var e = allEdges[ei];
|
|
253
|
+
if (!addedNodeIds[e.from] || !addedNodeIds[e.to]) continue;
|
|
254
|
+
if (e.label !== 'doc_has_child') continue;
|
|
255
|
+
var exists = false;
|
|
256
|
+
edgesDataSet.forEach(function(existing) {
|
|
257
|
+
if (existing.from === e.from && existing.to === e.to && existing._label === e.label) exists = true;
|
|
258
|
+
});
|
|
259
|
+
if (!exists) {
|
|
260
|
+
var es = edgeStyle(e);
|
|
261
|
+
addEdges.push({ id: 'e_expand_' + ei, from: e.from, to: e.to, width: es.width, _origWidth: es.width, color: es.color, dashes: es.dashes, arrows: es.arrows, _label: e.label, _highlightColor: es._highlightColor || '#9ca3af' });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (addEdges.length > 0) edgesDataSet.add(addEdges);
|
|
265
|
+
|
|
266
|
+
// 4) 恢复父节点标签(保留左侧留白)
|
|
267
|
+
var parentNode = nodesDataSet.get(nodeId);
|
|
268
|
+
if (parentNode && parentNode._origLabel) {
|
|
269
|
+
var pad = ' ';
|
|
270
|
+
nodesDataSet.update({ id: nodeId, label: pad + parentNode._origLabel });
|
|
271
|
+
}
|
|
272
|
+
log('展开文档: 显示 ' + addNodes.length + ' 个子文档', true);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 在 afterDrawing 中绘制父文档节点的 +/- 按钮 */
|
|
277
|
+
function drawDocToggleButtons(ctx) {
|
|
278
|
+
docToggleBtnPositions = {};
|
|
279
|
+
nodesDataSet.forEach(function(node) {
|
|
280
|
+
if (node._type !== 'document') return;
|
|
281
|
+
var allNode = findAllNode(node.id);
|
|
282
|
+
if (!allNode || !isParentDocNode(allNode)) return;
|
|
283
|
+
var pos = network.getPositions([node.id])[node.id];
|
|
284
|
+
if (!pos) return;
|
|
285
|
+
var isCollapsed = !!collapsedDocNodes[node.id];
|
|
286
|
+
var btnRadius = 9;
|
|
287
|
+
|
|
288
|
+
// 使用 getBoundingBox 获取节点精确边界,按钮放在节点内左侧留白区域中心
|
|
289
|
+
var bbox = network.getBoundingBox(node.id);
|
|
290
|
+
var btnX, btnY;
|
|
291
|
+
if (bbox) {
|
|
292
|
+
btnX = bbox.left + btnRadius + 1; // 按钮完全在节点内,左侧留白区域居中
|
|
293
|
+
btnY = (bbox.top + bbox.bottom) / 2; // 垂直居中
|
|
294
|
+
} else {
|
|
295
|
+
btnX = pos.x;
|
|
296
|
+
btnY = pos.y;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 记录位置(canvas 坐标)
|
|
300
|
+
docToggleBtnPositions[node.id] = { x: btnX, y: btnY, r: btnRadius };
|
|
301
|
+
|
|
302
|
+
// 绘制圆形按钮背景(蓝色系配色)
|
|
303
|
+
ctx.beginPath();
|
|
304
|
+
ctx.arc(btnX, btnY, btnRadius, 0, Math.PI * 2);
|
|
305
|
+
ctx.fillStyle = isCollapsed ? '#3b82f6' : '#1e40af'; // 收起:亮蓝 展开:深蓝
|
|
306
|
+
ctx.fill();
|
|
307
|
+
ctx.strokeStyle = '#ffffff'; // 白色描边
|
|
308
|
+
ctx.lineWidth = 1.5;
|
|
309
|
+
ctx.stroke();
|
|
310
|
+
ctx.closePath();
|
|
311
|
+
|
|
312
|
+
// 绘制 + 或 - 符号
|
|
313
|
+
ctx.fillStyle = '#ffffff';
|
|
314
|
+
ctx.font = 'bold 13px sans-serif';
|
|
315
|
+
ctx.textAlign = 'center';
|
|
316
|
+
ctx.textBaseline = 'middle';
|
|
317
|
+
ctx.fillText(isCollapsed ? '+' : '−', btnX, btnY + 0.5);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 检查 canvas 坐标是否点击了某个 +/- 按钮,返回 nodeId 或 null */
|
|
322
|
+
function hitTestDocToggleBtn(canvasX, canvasY) {
|
|
323
|
+
for (var nodeId in docToggleBtnPositions) {
|
|
324
|
+
var btn = docToggleBtnPositions[nodeId];
|
|
325
|
+
var dx = canvasX - btn.x;
|
|
326
|
+
var dy = canvasY - btn.y;
|
|
327
|
+
if (dx * dx + dy * dy <= (btn.r + 4) * (btn.r + 4)) {
|
|
328
|
+
return nodeId;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 将父文档及其子文档按思维导图方式排列:
|
|
336
|
+
* 父文档在左,子文档在右侧垂直等距、左边缘对齐
|
|
337
|
+
*/
|
|
338
|
+
function arrangeDocMindMap(parentNodeId, childNodeIds) {
|
|
339
|
+
if (!network || childNodeIds.length === 0) return;
|
|
340
|
+
var parentPos = network.getPositions([parentNodeId])[parentNodeId];
|
|
341
|
+
if (!parentPos) return;
|
|
342
|
+
|
|
343
|
+
var parentBbox = network.getBoundingBox(parentNodeId);
|
|
344
|
+
var parentRight = parentBbox ? parentBbox.right : (parentPos.x + 80);
|
|
345
|
+
var leftEdgeX = parentRight + 40; // 子节点左边缘的目标 X
|
|
346
|
+
var vGap = 45;
|
|
347
|
+
var count = childNodeIds.length;
|
|
348
|
+
var totalHeight = (count - 1) * vGap;
|
|
349
|
+
var startY = parentPos.y - totalHeight / 2;
|
|
350
|
+
|
|
351
|
+
// 先读取每个子节点当前的宽度(移动前 bbox 有效)
|
|
352
|
+
var halfLefts = [];
|
|
353
|
+
for (var i = 0; i < count; i++) {
|
|
354
|
+
var cid = childNodeIds[i];
|
|
355
|
+
var bbox = network.getBoundingBox(cid);
|
|
356
|
+
var cpos = network.getPositions([cid])[cid];
|
|
357
|
+
if (bbox && cpos) {
|
|
358
|
+
halfLefts.push(cpos.x - bbox.left); // 节点中心到左边缘的距离(即半宽)
|
|
359
|
+
} else {
|
|
360
|
+
halfLefts.push(100); // 默认估算
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 一次性移动所有子节点:左边缘对齐到 leftEdgeX
|
|
365
|
+
for (var i = 0; i < count; i++) {
|
|
366
|
+
var cx = leftEdgeX + halfLefts[i];
|
|
367
|
+
var cy = startY + i * vGap;
|
|
368
|
+
network.moveNode(childNodeIds[i], cx, cy);
|
|
369
|
+
savedChildPositions[childNodeIds[i]] = { x: cx, y: cy };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 初始化时将所有父文档-子文档按思维导图方式排列 */
|
|
374
|
+
function arrangeAllDocMindMaps() {
|
|
375
|
+
// 找到所有父文档节点
|
|
376
|
+
var parentDocIds = [];
|
|
377
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
378
|
+
var n = allNodes[i];
|
|
379
|
+
if (isParentDocNode(n)) {
|
|
380
|
+
// 检查该节点在当前可见节点集中
|
|
381
|
+
var visible = nodesDataSet.get(n.id);
|
|
382
|
+
if (visible) parentDocIds.push(n.id);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (var pi = 0; pi < parentDocIds.length; pi++) {
|
|
386
|
+
var pid = parentDocIds[pi];
|
|
387
|
+
var childIds = getChildDocNodeIds(pid);
|
|
388
|
+
// 只排列当前可见的子节点
|
|
389
|
+
var visibleChildIds = [];
|
|
390
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
391
|
+
if (nodesDataSet.get(childIds[ci])) visibleChildIds.push(childIds[ci]);
|
|
392
|
+
}
|
|
393
|
+
if (visibleChildIds.length > 0) {
|
|
394
|
+
arrangeDocMindMap(pid, visibleChildIds);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
log('思维导图排列: ' + parentDocIds.length + ' 个父文档已排列', true);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
// ========== 呼吸灯动画 (in_progress 主任务) ==========
|
|
402
|
+
var breathAnimId = null; // requestAnimationFrame ID
|
|
403
|
+
var breathPhase = 0; // 动画相位 [0, 2π)
|
|
404
|
+
|
|
405
|
+
/** 启动呼吸灯动画循环 */
|
|
406
|
+
function startBreathAnimation() {
|
|
407
|
+
if (breathAnimId) return; // 已在运行
|
|
408
|
+
function tick() {
|
|
409
|
+
breathPhase += 0.03; // 控制呼吸速度
|
|
410
|
+
if (breathPhase > Math.PI * 2) breathPhase -= Math.PI * 2;
|
|
411
|
+
if (network) network.redraw();
|
|
412
|
+
breathAnimId = requestAnimationFrame(tick);
|
|
413
|
+
}
|
|
414
|
+
breathAnimId = requestAnimationFrame(tick);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 停止呼吸灯动画循环 */
|
|
418
|
+
function stopBreathAnimation() {
|
|
419
|
+
if (breathAnimId) {
|
|
420
|
+
cancelAnimationFrame(breathAnimId);
|
|
421
|
+
breathAnimId = null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** 获取所有 in_progress 的主任务节点 ID 列表 */
|
|
426
|
+
function getInProgressMainTaskIds() {
|
|
427
|
+
var ids = [];
|
|
428
|
+
if (!nodesDataSet) return ids;
|
|
429
|
+
var all = nodesDataSet.get();
|
|
430
|
+
for (var i = 0; i < all.length; i++) {
|
|
431
|
+
var n = all[i];
|
|
432
|
+
if (n._type === 'main-task' && n._props && n._props.status === 'in_progress') {
|
|
433
|
+
ids.push(n.id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return ids;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 监听 Ctrl 按键状态
|
|
440
|
+
document.addEventListener('keydown', function(e) { if (e.key === 'Control') ctrlPressed = true; });
|
|
441
|
+
document.addEventListener('keyup', function(e) { if (e.key === 'Control') ctrlPressed = false; });
|
|
442
|
+
window.addEventListener('blur', function() { ctrlPressed = false; });
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
// ========== Node Styles (统一颜色配置) ==========
|
|
446
|
+
var _visUniStyle = getUnifiedNodeStyle();
|
|
447
|
+
var STATUS_COLORS = _visUniStyle.statusGeneric;
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
// ========== 节点动态大小规则 ==========
|
|
451
|
+
// 根据节点的连接数(度数)动态调整大小,连接越多节点越大
|
|
452
|
+
// min: 最小尺寸, max: 最大尺寸, baseFont: 基础字号, maxFont: 最大字号
|
|
453
|
+
// scale: 缩放系数 (越大增长越快)
|
|
454
|
+
var NODE_SIZE_RULES = {
|
|
455
|
+
'project': { min: 35, max: 65, baseFont: 16, maxFont: 22, scale: 3.5 },
|
|
456
|
+
'module': { min: 20, max: 45, baseFont: 12, maxFont: 16, scale: 2.8 },
|
|
457
|
+
'main-task': { min: 14, max: 38, baseFont: 11, maxFont: 15, scale: 2.2 },
|
|
458
|
+
'sub-task': { min: 7, max: 18, baseFont: 8, maxFont: 11, scale: 1.5 },
|
|
459
|
+
'document': { min: 12, max: 30, baseFont: 9, maxFont: 13, scale: 1.8 }
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/** 获取节点度数:纯后端下发,缺失视为 0 */
|
|
463
|
+
function getNodeDegree(node) {
|
|
464
|
+
if (typeof node.degree === 'number' && !isNaN(node.degree)) return node.degree;
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** 根据类型和度数计算节点尺寸与字号 */
|
|
469
|
+
function calcNodeSize(type, degree) {
|
|
470
|
+
var rule = NODE_SIZE_RULES[type] || { min: 10, max: 22, baseFont: 10, maxFont: 13, scale: 1.0 };
|
|
471
|
+
// 使用 sqrt 曲线:低度数时增长快,高度数时增长变缓
|
|
472
|
+
var size = rule.min + rule.scale * Math.sqrt(degree);
|
|
473
|
+
size = Math.max(rule.min, Math.min(size, rule.max));
|
|
474
|
+
// 字号随尺寸线性插值
|
|
475
|
+
var sizeRatio = (size - rule.min) / (rule.max - rule.min || 1);
|
|
476
|
+
var fontSize = Math.round(rule.baseFont + sizeRatio * (rule.maxFont - rule.baseFont));
|
|
477
|
+
return { size: Math.round(size), fontSize: fontSize };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function nodeStyle(node, degree) {
|
|
481
|
+
var t = node.type;
|
|
482
|
+
var p = node.properties || {};
|
|
483
|
+
var status = p.status || 'pending';
|
|
484
|
+
var sc = STATUS_COLORS[status] || STATUS_COLORS.pending;
|
|
485
|
+
var ns = calcNodeSize(t, degree || 0);
|
|
486
|
+
|
|
487
|
+
if (t === 'project') {
|
|
488
|
+
var _pc = _visUniStyle.project;
|
|
489
|
+
return { shape: 'star', size: ns.size, color: { background: _pc.bg, border: _pc.border, highlight: { background: _pc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: _pc.font }, borderWidth: 3 };
|
|
490
|
+
}
|
|
491
|
+
if (t === 'module') {
|
|
492
|
+
var _mc = _visUniStyle.module;
|
|
493
|
+
return { shape: 'diamond', size: ns.size, color: { background: _mc.bg, border: _mc.border, highlight: { background: _mc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: _mc.font }, borderWidth: 2 };
|
|
494
|
+
}
|
|
495
|
+
if (t === 'main-task') {
|
|
496
|
+
// 主任务: 从统一配置读取状态颜色 (深绿色系)
|
|
497
|
+
var mtc = _visUniStyle.mainTask[status] || _visUniStyle.mainTask.pending;
|
|
498
|
+
return { shape: 'dot', size: ns.size, color: { background: mtc.bg, border: mtc.border, highlight: { background: mtc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: mtc.font }, borderWidth: 2 };
|
|
499
|
+
}
|
|
500
|
+
if (t === 'sub-task') {
|
|
501
|
+
// 子任务: 从统一配置读取状态颜色 (pending=暖肤色, completed=亮绿)
|
|
502
|
+
var stc = _visUniStyle.subTask[status] || _visUniStyle.subTask.pending;
|
|
503
|
+
return { shape: 'dot', size: ns.size, color: { background: stc.bg, border: stc.border, highlight: { background: stc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: stc.font }, borderWidth: 1 };
|
|
504
|
+
}
|
|
505
|
+
if (t === 'document') {
|
|
506
|
+
var _dc = _visUniStyle.document;
|
|
507
|
+
return { shape: 'box', size: ns.size, color: { background: _dc.bg, border: _dc.border, highlight: { background: _dc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: _dc.font }, borderWidth: 1 };
|
|
508
|
+
}
|
|
509
|
+
return { shape: 'dot', size: ns.size, color: { background: '#6b7280', border: '#4b5563' }, font: { size: ns.fontSize, color: '#9ca3af' } };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 默认灰色 + 选中时高亮色(per-type)
|
|
513
|
+
var EDGE_GRAY = '#4b5563';
|
|
514
|
+
|
|
515
|
+
function edgeStyle(edge) {
|
|
516
|
+
var label = edge.label || '';
|
|
517
|
+
if (label === 'has_main_task') return { width: 2, color: { color: EDGE_GRAY, highlight: '#93c5fd', hover: '#93c5fd' }, dashes: false, arrows: { to: { enabled: true, scaleFactor: 0.6 } }, _highlightColor: '#93c5fd' };
|
|
518
|
+
if (label === 'has_sub_task') return { width: 1, color: { color: EDGE_GRAY, highlight: '#818cf8', hover: '#818cf8' }, dashes: false, arrows: { to: { enabled: true, scaleFactor: 0.4 } }, _highlightColor: '#818cf8' };
|
|
519
|
+
if (label === 'has_document') return { width: 1, color: { color: EDGE_GRAY, highlight: '#60a5fa', hover: '#60a5fa' }, dashes: [5, 5], arrows: { to: { enabled: true, scaleFactor: 0.4 } }, _highlightColor: '#60a5fa' };
|
|
520
|
+
if (label === 'has_module') return { width: 1.5, color: { color: EDGE_GRAY, highlight: '#ff8533', hover: '#ff8533' }, dashes: [3, 3], arrows: { to: { enabled: true, scaleFactor: 0.5 } }, _highlightColor: '#ff8533' };
|
|
521
|
+
if (label === 'module_has_task') return { width: 1.5, color: { color: EDGE_GRAY, highlight: '#ff8533', hover: '#ff8533' }, dashes: [2, 4], arrows: { to: { enabled: true, scaleFactor: 0.5 } }, _highlightColor: '#ff8533' };
|
|
522
|
+
if (label === 'task_has_doc') return { width: 1.5, color: { color: EDGE_GRAY, highlight: '#f59e0b', hover: '#f59e0b' }, dashes: [4, 3], arrows: { to: { enabled: true, scaleFactor: 0.5 } }, _highlightColor: '#f59e0b' };
|
|
523
|
+
if (label === 'doc_has_child') return { width: 1.5, color: { color: EDGE_GRAY, highlight: '#c084fc', hover: '#c084fc' }, dashes: [6, 3], arrows: { to: { enabled: true, scaleFactor: 0.5 } }, _highlightColor: '#c084fc' };
|
|
524
|
+
return { width: 1, color: { color: EDGE_GRAY, highlight: '#9ca3af', hover: '#9ca3af' }, dashes: false, _highlightColor: '#9ca3af' };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
// ========== Graph Rendering ==========
|
|
529
|
+
function renderGraph() {
|
|
530
|
+
try {
|
|
531
|
+
var container = document.getElementById('graph');
|
|
532
|
+
var rect = container.getBoundingClientRect();
|
|
533
|
+
log('容器尺寸: ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + ', 渲染中...', true);
|
|
534
|
+
|
|
535
|
+
if (rect.height < 50) {
|
|
536
|
+
container.style.height = (window.innerHeight - 140) + 'px';
|
|
537
|
+
rect = container.getBoundingClientRect();
|
|
538
|
+
log('容器高度修正为: ' + Math.round(rect.height) + 'px', true);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
var visibleNodes = [];
|
|
542
|
+
var DOC_BTN_PAD = ' '; // 父文档标签左侧留白,为 +/- 按钮腾出空间
|
|
543
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
544
|
+
var n = allNodes[i];
|
|
545
|
+
if (hiddenTypes[n.type]) continue;
|
|
546
|
+
// 跳过被收起的子文档节点
|
|
547
|
+
if (isNodeCollapsedByParent(n.id)) continue;
|
|
548
|
+
var deg = getNodeDegree(n);
|
|
549
|
+
var s = nodeStyle(n, deg);
|
|
550
|
+
var label = n.label;
|
|
551
|
+
var isParentDoc = isParentDocNode(n);
|
|
552
|
+
if (isParentDoc) {
|
|
553
|
+
// 父文档标签左侧加空格,为按钮腾位
|
|
554
|
+
if (collapsedDocNodes[n.id]) {
|
|
555
|
+
var childCount = getAllDescendantDocNodeIds(n.id).length;
|
|
556
|
+
label = DOC_BTN_PAD + label + ' [' + childCount + ']';
|
|
557
|
+
} else {
|
|
558
|
+
label = DOC_BTN_PAD + label;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Phase-10 T10.5: Add double-click hint for main-task nodes in tiered mode
|
|
562
|
+
var tooltip = n.label + ' (连接: ' + deg + ')';
|
|
563
|
+
if (n.type === 'main-task' && !USE_3D && tieredLoadState.l0l1Loaded && !tieredLoadState.l2Loaded) {
|
|
564
|
+
var phaseId = (n.properties || {}).taskId || n.id;
|
|
565
|
+
tooltip += tieredLoadState.expandedPhases[phaseId] ? '\\n双击收起子任务' : '\\n双击展开子任务';
|
|
566
|
+
}
|
|
567
|
+
visibleNodes.push({ id: n.id, label: label, _origLabel: n.label, title: tooltip, shape: s.shape, size: s.size, color: s.color, font: s.font, borderWidth: s.borderWidth, _type: n.type, _props: n.properties || {}, _isParentDoc: isParentDoc });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
var visibleIds = {};
|
|
571
|
+
var _projectNodeIds = {}; // 收集所有 project 类型节点 ID
|
|
572
|
+
for (var i = 0; i < visibleNodes.length; i++) {
|
|
573
|
+
visibleIds[visibleNodes[i].id] = true;
|
|
574
|
+
if (visibleNodes[i]._type === 'project') _projectNodeIds[visibleNodes[i].id] = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 读取"主节点连线"设置
|
|
578
|
+
var _graphSettings = getGraphSettings();
|
|
579
|
+
var _hideProjectEdges = !_graphSettings.showProjectEdges;
|
|
580
|
+
|
|
581
|
+
var visibleEdges = [];
|
|
582
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
583
|
+
var e = allEdges[i];
|
|
584
|
+
if (!visibleIds[e.from] || !visibleIds[e.to]) continue;
|
|
585
|
+
// 主节点连线: 标记为隐藏但保留在数据中(3D 力模拟仍需要这些边)
|
|
586
|
+
var isProjectEdge = _hideProjectEdges && (_projectNodeIds[e.from] || _projectNodeIds[e.to]);
|
|
587
|
+
var es = edgeStyle(e);
|
|
588
|
+
visibleEdges.push({ id: 'e' + i, from: e.from, to: e.to, width: es.width, _origWidth: es.width, color: es.color, dashes: es.dashes, arrows: es.arrows, _label: e.label, _highlightColor: es._highlightColor || '#9ca3af', _projectEdgeHidden: !!isProjectEdge, hidden: !!isProjectEdge });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
log('可见节点: ' + visibleNodes.length + ', 可见边: ' + visibleEdges.length, true);
|
|
592
|
+
|
|
593
|
+
if (network) {
|
|
594
|
+
network.destroy();
|
|
595
|
+
network = null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── 3D Force Graph 渲染路径 ──
|
|
599
|
+
if (USE_3D) {
|
|
600
|
+
nodesDataSet = new SimpleDataSet(visibleNodes);
|
|
601
|
+
edgesDataSet = new SimpleDataSet(visibleEdges);
|
|
602
|
+
render3DGraph(container, visibleNodes, visibleEdges);
|
|
603
|
+
return; // 3D 有独立的事件绑定和生命周期
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ── vis-network 渲染路径 ──
|
|
607
|
+
nodesDataSet = new vis.DataSet(visibleNodes);
|
|
608
|
+
edgesDataSet = new vis.DataSet(visibleEdges);
|
|
609
|
+
|
|
610
|
+
// ── Phase-10 T10.4: Adaptive physics config based on node count ──
|
|
611
|
+
var nodeCount = visibleNodes.length;
|
|
612
|
+
var physicsConfig;
|
|
613
|
+
if (nodeCount > 2000) {
|
|
614
|
+
physicsConfig = {
|
|
615
|
+
enabled: false,
|
|
616
|
+
stabilization: { enabled: false }
|
|
617
|
+
};
|
|
618
|
+
log('物理引擎: 已禁用 (节点 ' + nodeCount + ' > 2000)', true);
|
|
619
|
+
} else if (nodeCount > 800) {
|
|
620
|
+
physicsConfig = {
|
|
621
|
+
enabled: true,
|
|
622
|
+
solver: 'forceAtlas2Based',
|
|
623
|
+
forceAtlas2Based: {
|
|
624
|
+
gravitationalConstant: -60,
|
|
625
|
+
centralGravity: 0.02,
|
|
626
|
+
springLength: 120,
|
|
627
|
+
springConstant: 0.06,
|
|
628
|
+
damping: 0.5,
|
|
629
|
+
avoidOverlap: 0.8
|
|
630
|
+
},
|
|
631
|
+
stabilization: { enabled: true, iterations: 80, updateInterval: 20 }
|
|
632
|
+
};
|
|
633
|
+
log('物理引擎: 大图模式 iterations=80 (节点 ' + nodeCount + ')', true);
|
|
634
|
+
} else if (nodeCount > 200) {
|
|
635
|
+
physicsConfig = {
|
|
636
|
+
enabled: true,
|
|
637
|
+
solver: 'forceAtlas2Based',
|
|
638
|
+
forceAtlas2Based: {
|
|
639
|
+
gravitationalConstant: -80,
|
|
640
|
+
centralGravity: 0.015,
|
|
641
|
+
springLength: 150,
|
|
642
|
+
springConstant: 0.05,
|
|
643
|
+
damping: 0.4,
|
|
644
|
+
avoidOverlap: 0.8
|
|
645
|
+
},
|
|
646
|
+
stabilization: { enabled: true, iterations: 120, updateInterval: 25 }
|
|
647
|
+
};
|
|
648
|
+
log('物理引擎: 中等模式 iterations=120 (节点 ' + nodeCount + ')', true);
|
|
649
|
+
} else {
|
|
650
|
+
physicsConfig = {
|
|
651
|
+
enabled: true,
|
|
652
|
+
solver: 'forceAtlas2Based',
|
|
653
|
+
forceAtlas2Based: {
|
|
654
|
+
gravitationalConstant: -80,
|
|
655
|
+
centralGravity: 0.015,
|
|
656
|
+
springLength: 150,
|
|
657
|
+
springConstant: 0.05,
|
|
658
|
+
damping: 0.4,
|
|
659
|
+
avoidOverlap: 0.8
|
|
660
|
+
},
|
|
661
|
+
stabilization: { enabled: true, iterations: 150, updateInterval: 25 }
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ── 性能优化: 根据节点数量自适应渲染配置 ──
|
|
666
|
+
var isLargeGraph = nodeCount > 300;
|
|
667
|
+
var isVeryLargeGraph = nodeCount > 800;
|
|
668
|
+
|
|
669
|
+
var networkOptions = {
|
|
670
|
+
nodes: {
|
|
671
|
+
borderWidth: 2,
|
|
672
|
+
shadow: isLargeGraph
|
|
673
|
+
? false
|
|
674
|
+
: { enabled: true, color: 'rgba(0,0,0,0.3)', size: 5, x: 0, y: 2 }
|
|
675
|
+
},
|
|
676
|
+
edges: {
|
|
677
|
+
smooth: isLargeGraph
|
|
678
|
+
? false
|
|
679
|
+
: { enabled: true, type: 'continuous', roundness: 0.5 },
|
|
680
|
+
shadow: false
|
|
681
|
+
},
|
|
682
|
+
physics: physicsConfig,
|
|
683
|
+
interaction: {
|
|
684
|
+
hover: !isVeryLargeGraph,
|
|
685
|
+
tooltipDelay: 200,
|
|
686
|
+
navigationButtons: false,
|
|
687
|
+
keyboard: false,
|
|
688
|
+
zoomView: true,
|
|
689
|
+
dragView: true,
|
|
690
|
+
hideEdgesOnDrag: isLargeGraph,
|
|
691
|
+
hideEdgesOnZoom: isLargeGraph,
|
|
692
|
+
zoomSpeed: isVeryLargeGraph ? 0.8 : 1,
|
|
693
|
+
},
|
|
694
|
+
layout: {
|
|
695
|
+
improvedLayout: (nodeCount > 200 && nodeCount <= 800),
|
|
696
|
+
hierarchical: false
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
network = new vis.Network(container,
|
|
701
|
+
{ nodes: nodesDataSet, edges: edgesDataSet },
|
|
702
|
+
networkOptions
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
// Phase-10 T10.3: Mark network as reusable for incremental updates
|
|
706
|
+
networkReusable = true;
|
|
707
|
+
|
|
708
|
+
// Phase-10 T10.4: When physics is disabled (large graph), immediately show result
|
|
709
|
+
if (!physicsConfig.enabled) {
|
|
710
|
+
document.getElementById('loading').style.display = 'none';
|
|
711
|
+
log('图谱渲染完成 (无物理引擎)! ' + visibleNodes.length + ' 节点, ' + visibleEdges.length + ' 边', true);
|
|
712
|
+
network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
|
|
713
|
+
} else {
|
|
714
|
+
log('Network 实例已创建, 等待物理稳定化...', true);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
network.on('stabilizationIterationsDone', function() {
|
|
718
|
+
network.setOptions({ physics: { enabled: false } });
|
|
719
|
+
document.getElementById('loading').style.display = 'none';
|
|
720
|
+
log('图谱渲染完成! ' + visibleNodes.length + ' 节点, ' + visibleEdges.length + ' 边', true);
|
|
721
|
+
// 稳定后将父文档-子文档按思维导图方式整齐排列
|
|
722
|
+
arrangeAllDocMindMaps();
|
|
723
|
+
network.fit({ animation: { duration: 800, easingFunction: 'easeInOutQuad' } });
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ── 性能优化: 缩放时根据 zoom level 自适应标签可见性 ──
|
|
727
|
+
// 缩小到一定程度时隐藏子任务/文档标签(反正看不清),减少 canvas 文本绘制开销
|
|
728
|
+
if (isLargeGraph) {
|
|
729
|
+
var labelHidden = false;
|
|
730
|
+
network.on('zoom', function() {
|
|
731
|
+
var scale = network.getScale();
|
|
732
|
+
if (scale < 0.4 && !labelHidden) {
|
|
733
|
+
// 缩小时: 隐藏子任务和文档的标签
|
|
734
|
+
labelHidden = true;
|
|
735
|
+
var updates = [];
|
|
736
|
+
nodesDataSet.forEach(function(n) {
|
|
737
|
+
if (n._type === 'sub-task' || n._type === 'document') {
|
|
738
|
+
updates.push({ id: n.id, font: { size: 0 } });
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
if (updates.length > 0) nodesDataSet.update(updates);
|
|
742
|
+
} else if (scale >= 0.4 && labelHidden) {
|
|
743
|
+
// 放大时: 恢复标签
|
|
744
|
+
labelHidden = false;
|
|
745
|
+
var updates = [];
|
|
746
|
+
nodesDataSet.forEach(function(n) {
|
|
747
|
+
if (n._type === 'sub-task') {
|
|
748
|
+
updates.push({ id: n.id, font: { size: 9, color: n.font ? n.font.color : '#9ca3af' } });
|
|
749
|
+
} else if (n._type === 'document') {
|
|
750
|
+
updates.push({ id: n.id, font: { size: 10, color: n.font ? n.font.color : '#dbeafe' } });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
if (updates.length > 0) nodesDataSet.update(updates);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── Phase-10 T10.5: Double-click to expand/collapse sub-tasks ──
|
|
759
|
+
network.on('doubleClick', function(params) {
|
|
760
|
+
if (params.nodes.length > 0) {
|
|
761
|
+
var clickedId = params.nodes[0];
|
|
762
|
+
var clickedNode = nodesDataSet.get(clickedId);
|
|
763
|
+
if (clickedNode && clickedNode._type === 'main-task') {
|
|
764
|
+
var taskId = (clickedNode._props || {}).taskId || clickedId;
|
|
765
|
+
loadSubTasksForPhase(taskId);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
network.on('click', function(params) {
|
|
771
|
+
// 先检查是否点击了 +/- 按钮
|
|
772
|
+
if (params.pointer && params.pointer.canvas) {
|
|
773
|
+
var hitNodeId = hitTestDocToggleBtn(params.pointer.canvas.x, params.pointer.canvas.y);
|
|
774
|
+
if (hitNodeId) {
|
|
775
|
+
toggleDocNodeExpand(hitNodeId);
|
|
776
|
+
return; // 消费此次点击,不触发节点选择
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (params.nodes.length > 0) {
|
|
780
|
+
// 直接点击图谱节点 → 清空历史栈,重新开始导航
|
|
781
|
+
panelHistory = [];
|
|
782
|
+
currentPanelNodeId = null;
|
|
783
|
+
highlightConnectedEdges(params.nodes[0]);
|
|
784
|
+
showPanel(params.nodes[0]);
|
|
785
|
+
} else {
|
|
786
|
+
resetAllEdgeColors();
|
|
787
|
+
closePanel();
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ========== Ctrl+拖拽整体移动关联节点 ==========
|
|
792
|
+
var groupDrag = { active: false, nodeId: null, connectedIds: [], startPositions: {} };
|
|
793
|
+
|
|
794
|
+
network.on('dragStart', function(params) {
|
|
795
|
+
if (!ctrlPressed || params.nodes.length === 0) {
|
|
796
|
+
groupDrag.active = false;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
var draggedId = params.nodes[0];
|
|
800
|
+
// 获取所有直接关联的节点
|
|
801
|
+
var connected = network.getConnectedNodes(draggedId);
|
|
802
|
+
groupDrag.active = true;
|
|
803
|
+
groupDrag.nodeId = draggedId;
|
|
804
|
+
groupDrag.connectedIds = connected;
|
|
805
|
+
// 记录所有关联节点的初始位置
|
|
806
|
+
groupDrag.startPositions = {};
|
|
807
|
+
var positions = network.getPositions([draggedId].concat(connected));
|
|
808
|
+
groupDrag.startPositions = positions;
|
|
809
|
+
groupDrag.dragStartPos = positions[draggedId];
|
|
810
|
+
log('Ctrl+拖拽: 整体移动 ' + (connected.length + 1) + ' 个节点', true);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
network.on('dragging', function(params) {
|
|
814
|
+
if (!groupDrag.active || params.nodes.length === 0) return;
|
|
815
|
+
var draggedId = groupDrag.nodeId;
|
|
816
|
+
// 获取当前被拖拽节点的位置
|
|
817
|
+
var currentPos = network.getPositions([draggedId])[draggedId];
|
|
818
|
+
if (!currentPos || !groupDrag.dragStartPos) return;
|
|
819
|
+
// 计算位移差
|
|
820
|
+
var dx = currentPos.x - groupDrag.dragStartPos.x;
|
|
821
|
+
var dy = currentPos.y - groupDrag.dragStartPos.y;
|
|
822
|
+
// 移动所有关联节点
|
|
823
|
+
for (var i = 0; i < groupDrag.connectedIds.length; i++) {
|
|
824
|
+
var cid = groupDrag.connectedIds[i];
|
|
825
|
+
var startPos = groupDrag.startPositions[cid];
|
|
826
|
+
if (startPos) {
|
|
827
|
+
network.moveNode(cid, startPos.x + dx, startPos.y + dy);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
network.on('dragEnd', function(params) {
|
|
833
|
+
if (groupDrag.active) {
|
|
834
|
+
log('整体移动完成', true);
|
|
835
|
+
groupDrag.active = false;
|
|
836
|
+
groupDrag.nodeId = null;
|
|
837
|
+
groupDrag.connectedIds = [];
|
|
838
|
+
groupDrag.startPositions = {};
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// ========== afterDrawing: 呼吸灯 + 文档展开/收起按钮 ==========
|
|
843
|
+
network.on('afterDrawing', function(ctx) {
|
|
844
|
+
// 绘制父文档的 +/- 按钮
|
|
845
|
+
drawDocToggleButtons(ctx);
|
|
846
|
+
|
|
847
|
+
var ids = getInProgressMainTaskIds();
|
|
848
|
+
if (ids.length === 0) return;
|
|
849
|
+
|
|
850
|
+
// 呼吸因子: 0 → 1 → 0 平滑循环
|
|
851
|
+
var breath = (Math.sin(breathPhase) + 1) / 2; // [0, 1]
|
|
852
|
+
|
|
853
|
+
for (var i = 0; i < ids.length; i++) {
|
|
854
|
+
var pos = network.getPositions([ids[i]])[ids[i]];
|
|
855
|
+
if (!pos) continue;
|
|
856
|
+
var nodeData = nodesDataSet.get(ids[i]);
|
|
857
|
+
var baseSize = (nodeData && nodeData.size) || 14;
|
|
858
|
+
|
|
859
|
+
// 将网络坐标转换为 canvas 坐标
|
|
860
|
+
var canvasPos = network.canvasToDOM(pos);
|
|
861
|
+
// 再通过 DOMtoCanvas 获取正确的 canvas 上下文坐标
|
|
862
|
+
// vis-network 的 afterDrawing ctx 已经在正确的坐标系中,直接用 pos 即可
|
|
863
|
+
|
|
864
|
+
// 外层大范围弥散光晕(营造醒目的辉光感)
|
|
865
|
+
var outerGlowRadius = baseSize + 20 + breath * baseSize * 2.5;
|
|
866
|
+
var outerGrad = ctx.createRadialGradient(pos.x, pos.y, baseSize, pos.x, pos.y, outerGlowRadius);
|
|
867
|
+
outerGrad.addColorStop(0, 'rgba(124, 58, 237, ' + (0.18 + breath * 0.12) + ')');
|
|
868
|
+
outerGrad.addColorStop(0.5, 'rgba(139, 92, 246, ' + (0.08 + breath * 0.06) + ')');
|
|
869
|
+
outerGrad.addColorStop(1, 'rgba(139, 92, 246, 0)');
|
|
870
|
+
ctx.beginPath();
|
|
871
|
+
ctx.arc(pos.x, pos.y, outerGlowRadius, 0, Math.PI * 2);
|
|
872
|
+
ctx.fillStyle = outerGrad;
|
|
873
|
+
ctx.fill();
|
|
874
|
+
ctx.closePath();
|
|
875
|
+
|
|
876
|
+
// 外圈脉冲光环(更粗、扩展范围更大)
|
|
877
|
+
var maxExpand = baseSize * 2.2;
|
|
878
|
+
var ringRadius = baseSize + 8 + breath * maxExpand;
|
|
879
|
+
var ringAlpha = 0.55 * (1 - breath * 0.5);
|
|
880
|
+
|
|
881
|
+
ctx.beginPath();
|
|
882
|
+
ctx.arc(pos.x, pos.y, ringRadius, 0, Math.PI * 2);
|
|
883
|
+
ctx.strokeStyle = 'rgba(139, 92, 246, ' + ringAlpha + ')';
|
|
884
|
+
ctx.lineWidth = 3.5 + breath * 3;
|
|
885
|
+
ctx.stroke();
|
|
886
|
+
ctx.closePath();
|
|
887
|
+
|
|
888
|
+
// 中圈脉冲光环(第二道更紧凑的环)
|
|
889
|
+
var midRingRadius = baseSize + 4 + breath * baseSize * 1.2;
|
|
890
|
+
var midRingAlpha = 0.4 * (1 - breath * 0.4);
|
|
891
|
+
ctx.beginPath();
|
|
892
|
+
ctx.arc(pos.x, pos.y, midRingRadius, 0, Math.PI * 2);
|
|
893
|
+
ctx.strokeStyle = 'rgba(167, 139, 250, ' + midRingAlpha + ')';
|
|
894
|
+
ctx.lineWidth = 2.5 + breath * 2;
|
|
895
|
+
ctx.stroke();
|
|
896
|
+
ctx.closePath();
|
|
897
|
+
|
|
898
|
+
// 内圈柔光(更大范围的径向渐变)
|
|
899
|
+
var glowRadius = baseSize + 10 + breath * 16;
|
|
900
|
+
var gradient = ctx.createRadialGradient(pos.x, pos.y, baseSize * 0.3, pos.x, pos.y, glowRadius);
|
|
901
|
+
gradient.addColorStop(0, 'rgba(124, 58, 237, ' + (0.25 + breath * 0.15) + ')');
|
|
902
|
+
gradient.addColorStop(0.6, 'rgba(139, 92, 246, ' + (0.10 + breath * 0.08) + ')');
|
|
903
|
+
gradient.addColorStop(1, 'rgba(139, 92, 246, 0)');
|
|
904
|
+
ctx.beginPath();
|
|
905
|
+
ctx.arc(pos.x, pos.y, glowRadius, 0, Math.PI * 2);
|
|
906
|
+
ctx.fillStyle = gradient;
|
|
907
|
+
ctx.fill();
|
|
908
|
+
ctx.closePath();
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// 检查是否有 in_progress 主任务,有则启动动画
|
|
913
|
+
stopBreathAnimation();
|
|
914
|
+
var inProgIds = getInProgressMainTaskIds();
|
|
915
|
+
if (inProgIds.length > 0) {
|
|
916
|
+
startBreathAnimation();
|
|
917
|
+
log('呼吸灯: 检测到 ' + inProgIds.length + ' 个进行中主任务', true);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// 超时回退
|
|
921
|
+
setTimeout(function() {
|
|
922
|
+
if (document.getElementById('loading').style.display !== 'none') {
|
|
923
|
+
document.getElementById('loading').style.display = 'none';
|
|
924
|
+
log('稳定化超时, 强制显示图谱', true);
|
|
925
|
+
if (network) network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
|
|
926
|
+
}
|
|
927
|
+
}, 8000);
|
|
928
|
+
|
|
929
|
+
} catch (err) {
|
|
930
|
+
log('渲染错误: ' + err.message, false);
|
|
931
|
+
console.error('[DevPlan] renderGraph error:', err);
|
|
932
|
+
document.getElementById('loading').innerHTML = '<div style="text-align:center"><div style="font-size:48px;margin-bottom:16px;">⚠️</div><p style="color:#f87171;">渲染失败: ' + err.message + '</p><button class="refresh-btn" onclick="loadData()" style="margin-top:12px;">重试</button></div>';
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
// ========== Filters ==========
|
|
938
|
+
/** 保存筛选隐藏时各节点的 (x, y) 位置,重新显示时恢复 */
|
|
939
|
+
var savedFilterPositions = {};
|
|
940
|
+
|
|
941
|
+
function toggleFilter(type) {
|
|
942
|
+
var el = document.querySelector('.legend-item.toggle[data-type="' + type + '"]');
|
|
943
|
+
var cb = document.getElementById('cb-' + type);
|
|
944
|
+
if (!el) return;
|
|
945
|
+
|
|
946
|
+
var isCurrentlyActive = el.classList.contains('active');
|
|
947
|
+
|
|
948
|
+
if (isCurrentlyActive) {
|
|
949
|
+
// ── 分层模式: 该类型尚未加载 → 首次点击触发按需加载 ──
|
|
950
|
+
if (!USE_3D && tieredLoadState.l0l1Loaded) {
|
|
951
|
+
if (type === 'sub-task' && !tieredLoadState.l2Loaded) {
|
|
952
|
+
loadTierDataByType('sub-task');
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (type === 'document' && !tieredLoadState.l3Loaded) {
|
|
956
|
+
loadTierDataByType('document');
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// ── 正常隐藏此类型 ──
|
|
961
|
+
el.classList.remove('active');
|
|
962
|
+
if (cb) cb.checked = false;
|
|
963
|
+
hiddenTypes[type] = true;
|
|
964
|
+
} else {
|
|
965
|
+
// ── 显示此类型 ──
|
|
966
|
+
el.classList.add('active');
|
|
967
|
+
if (cb) cb.checked = true;
|
|
968
|
+
delete hiddenTypes[type];
|
|
969
|
+
|
|
970
|
+
// 分层模式: 如果该类型数据尚未加载,触发按需加载
|
|
971
|
+
if (!USE_3D && tieredLoadState.l0l1Loaded) {
|
|
972
|
+
if (type === 'sub-task' && !tieredLoadState.l2Loaded) {
|
|
973
|
+
loadTierDataByType('sub-task');
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (type === 'document' && !tieredLoadState.l3Loaded) {
|
|
977
|
+
loadTierDataByType('document');
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Phase-10 T10.3: Incremental filter toggle — add/remove from DataSet
|
|
984
|
+
if (networkReusable && nodesDataSet && edgesDataSet && network && !USE_3D) {
|
|
985
|
+
if (isCurrentlyActive) {
|
|
986
|
+
// ── 隐藏: 保存位置 → 移除节点 ──
|
|
987
|
+
var removeNodeIds = [];
|
|
988
|
+
nodesDataSet.forEach(function(n) {
|
|
989
|
+
if (n._type === type) removeNodeIds.push(n.id);
|
|
990
|
+
});
|
|
991
|
+
if (removeNodeIds.length > 0) {
|
|
992
|
+
// 保存当前位置,以便重新勾选时恢复
|
|
993
|
+
var positions = network.getPositions(removeNodeIds);
|
|
994
|
+
for (var k in positions) {
|
|
995
|
+
savedFilterPositions[k] = positions[k];
|
|
996
|
+
}
|
|
997
|
+
// Remove edges connected to these nodes first
|
|
998
|
+
var removeEdgeIds = [];
|
|
999
|
+
var removeSet = {};
|
|
1000
|
+
for (var i = 0; i < removeNodeIds.length; i++) removeSet[removeNodeIds[i]] = true;
|
|
1001
|
+
edgesDataSet.forEach(function(edge) {
|
|
1002
|
+
if (removeSet[edge.from] || removeSet[edge.to]) removeEdgeIds.push(edge.id);
|
|
1003
|
+
});
|
|
1004
|
+
if (removeEdgeIds.length > 0) edgesDataSet.remove(removeEdgeIds);
|
|
1005
|
+
nodesDataSet.remove(removeNodeIds);
|
|
1006
|
+
log('类型筛选: 隐藏 ' + type + ' (-' + removeNodeIds.length + ' 节点, 位置已保存)', true);
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
// ── 显示: 恢复节点到之前保存的位置 ──
|
|
1010
|
+
var addNodes = [];
|
|
1011
|
+
var addEdges = [];
|
|
1012
|
+
var currentIds = {};
|
|
1013
|
+
nodesDataSet.forEach(function(n) { currentIds[n.id] = true; });
|
|
1014
|
+
|
|
1015
|
+
var restoredCount = 0;
|
|
1016
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
1017
|
+
var n = allNodes[i];
|
|
1018
|
+
if (n.type === type && !currentIds[n.id]) {
|
|
1019
|
+
var deg = getNodeDegree(n);
|
|
1020
|
+
var s = nodeStyle(n, deg);
|
|
1021
|
+
var nodeData = {
|
|
1022
|
+
id: n.id, label: n.label, _origLabel: n.label,
|
|
1023
|
+
title: n.label + ' (连接: ' + deg + ')',
|
|
1024
|
+
shape: s.shape, size: s.size, color: s.color, font: s.font,
|
|
1025
|
+
borderWidth: s.borderWidth, _type: n.type,
|
|
1026
|
+
_props: n.properties || {},
|
|
1027
|
+
};
|
|
1028
|
+
// 恢复之前保存的位置
|
|
1029
|
+
if (savedFilterPositions[n.id]) {
|
|
1030
|
+
nodeData.x = savedFilterPositions[n.id].x;
|
|
1031
|
+
nodeData.y = savedFilterPositions[n.id].y;
|
|
1032
|
+
delete savedFilterPositions[n.id];
|
|
1033
|
+
restoredCount++;
|
|
1034
|
+
}
|
|
1035
|
+
addNodes.push(nodeData);
|
|
1036
|
+
currentIds[n.id] = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
// Re-add edges for newly visible nodes
|
|
1040
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
1041
|
+
var e = allEdges[i];
|
|
1042
|
+
if (currentIds[e.from] && currentIds[e.to]) {
|
|
1043
|
+
var existing = edgesDataSet.get('e' + i);
|
|
1044
|
+
if (!existing) {
|
|
1045
|
+
var es = edgeStyle(e);
|
|
1046
|
+
addEdges.push({
|
|
1047
|
+
id: 'e' + i, from: e.from, to: e.to,
|
|
1048
|
+
width: es.width, _origWidth: es.width,
|
|
1049
|
+
color: es.color, dashes: es.dashes, arrows: es.arrows,
|
|
1050
|
+
_label: e.label, _highlightColor: es._highlightColor || '#9ca3af',
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (addNodes.length > 0) nodesDataSet.add(addNodes);
|
|
1056
|
+
if (addEdges.length > 0) edgesDataSet.add(addEdges);
|
|
1057
|
+
log('类型筛选: 显示 ' + type + ' (+' + addNodes.length + ' 节点, ' + restoredCount + ' 位置已恢复)', true);
|
|
1058
|
+
|
|
1059
|
+
// 仅对没有保存位置的新节点短暂开启物理引擎
|
|
1060
|
+
var unpositioned = addNodes.length - restoredCount;
|
|
1061
|
+
if (unpositioned > 0 && unpositioned < 200) {
|
|
1062
|
+
network.setOptions({ physics: { enabled: true, stabilization: { enabled: false } } });
|
|
1063
|
+
setTimeout(function() {
|
|
1064
|
+
if (network) network.setOptions({ physics: { enabled: false } });
|
|
1065
|
+
}, 1500);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Fallback: full rebuild
|
|
1072
|
+
renderGraph();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* 分层模式: 按类型按需加载数据(子任务 / 文档)。
|
|
1077
|
+
* 当用户点击底部图例激活某类型时,如果该类型尚未加载,触发此函数。
|
|
1078
|
+
*/
|
|
1079
|
+
function loadTierDataByType(type) {
|
|
1080
|
+
var entityTypeMap = { 'sub-task': 'devplan-sub-task', 'document': 'devplan-document' };
|
|
1081
|
+
var entityType = entityTypeMap[type];
|
|
1082
|
+
if (!entityType) return;
|
|
1083
|
+
|
|
1084
|
+
var el = document.querySelector('.legend-item.toggle[data-type="' + type + '"]');
|
|
1085
|
+
if (el) el.classList.add('loading');
|
|
1086
|
+
|
|
1087
|
+
log('按需加载: ' + type + '...', true);
|
|
1088
|
+
var url = '/api/graph/paged?offset=0&limit=5000&entityTypes=' + entityType +
|
|
1089
|
+
'&includeDocuments=' + (type === 'document' ? 'true' : 'false') +
|
|
1090
|
+
'&includeModules=false';
|
|
1091
|
+
|
|
1092
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(result) {
|
|
1093
|
+
var newNodes = result.nodes || [];
|
|
1094
|
+
var newEdges = result.edges || [];
|
|
1095
|
+
|
|
1096
|
+
// 合并到 allNodes/allEdges(去重)
|
|
1097
|
+
var existingIds = {};
|
|
1098
|
+
for (var i = 0; i < allNodes.length; i++) existingIds[allNodes[i].id] = true;
|
|
1099
|
+
|
|
1100
|
+
var addedNodes = [];
|
|
1101
|
+
var addedEdges = [];
|
|
1102
|
+
for (var i = 0; i < newNodes.length; i++) {
|
|
1103
|
+
if (!existingIds[newNodes[i].id]) {
|
|
1104
|
+
allNodes.push(newNodes[i]);
|
|
1105
|
+
addedNodes.push(newNodes[i]);
|
|
1106
|
+
existingIds[newNodes[i].id] = true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// 也检查服务器返回的新边
|
|
1110
|
+
var existingEdgeSet = {};
|
|
1111
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
1112
|
+
existingEdgeSet[allEdges[i].from + '->' + allEdges[i].to] = true;
|
|
1113
|
+
}
|
|
1114
|
+
for (var i = 0; i < newEdges.length; i++) {
|
|
1115
|
+
var e = newEdges[i];
|
|
1116
|
+
var edgeKey = e.from + '->' + e.to;
|
|
1117
|
+
if (!existingEdgeSet[edgeKey] && existingIds[e.from] && existingIds[e.to]) {
|
|
1118
|
+
allEdges.push(e);
|
|
1119
|
+
addedEdges.push(e);
|
|
1120
|
+
existingEdgeSet[edgeKey] = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (type === 'sub-task') tieredLoadState.l2Loaded = true;
|
|
1125
|
+
if (type === 'document') tieredLoadState.l3Loaded = true;
|
|
1126
|
+
|
|
1127
|
+
if (el) {
|
|
1128
|
+
el.classList.remove('loading');
|
|
1129
|
+
el.classList.remove('not-loaded');
|
|
1130
|
+
el.title = '点击切换' + type + '显隐';
|
|
1131
|
+
}
|
|
1132
|
+
log('按需加载完成: +' + addedNodes.length + ' ' + type + ' 节点, +' + addedEdges.length + ' 边', true);
|
|
1133
|
+
|
|
1134
|
+
// 增量添加到图谱
|
|
1135
|
+
if (networkReusable && nodesDataSet && edgesDataSet && network) {
|
|
1136
|
+
incrementalAddNodes(addedNodes, addedEdges);
|
|
1137
|
+
} else {
|
|
1138
|
+
renderGraph();
|
|
1139
|
+
}
|
|
1140
|
+
updateTieredIndicator();
|
|
1141
|
+
}).catch(function(err) {
|
|
1142
|
+
if (el) el.classList.remove('loading');
|
|
1143
|
+
log('按需加载失败: ' + err.message, false);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* 同步图例 toggle 状态与当前 hiddenTypes(页面初始化 / 分层加载后调用)。
|
|
1149
|
+
*/
|
|
1150
|
+
function syncLegendToggleState() {
|
|
1151
|
+
var types = ['module', 'main-task', 'sub-task', 'document'];
|
|
1152
|
+
for (var i = 0; i < types.length; i++) {
|
|
1153
|
+
var el = document.querySelector('.legend-item.toggle[data-type="' + types[i] + '"]');
|
|
1154
|
+
var cb = document.getElementById('cb-' + types[i]);
|
|
1155
|
+
if (!el) continue;
|
|
1156
|
+
if (hiddenTypes[types[i]]) {
|
|
1157
|
+
el.classList.remove('active');
|
|
1158
|
+
if (cb) cb.checked = false;
|
|
1159
|
+
} else {
|
|
1160
|
+
el.classList.add('active');
|
|
1161
|
+
if (cb) cb.checked = true;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* 分层模式: 标记尚未加载的节点类型(子任务 / 文档)在图例中显示"未加载"视觉提示。
|
|
1168
|
+
* 用户点击后会触发按需加载。
|
|
1169
|
+
*/
|
|
1170
|
+
function markUnloadedTypeLegends() {
|
|
1171
|
+
var subEl = document.querySelector('.legend-item.toggle[data-type="sub-task"]');
|
|
1172
|
+
var docEl = document.querySelector('.legend-item.toggle[data-type="document"]');
|
|
1173
|
+
if (subEl && !tieredLoadState.l2Loaded) {
|
|
1174
|
+
subEl.classList.add('not-loaded');
|
|
1175
|
+
subEl.title = '点击加载子任务';
|
|
1176
|
+
}
|
|
1177
|
+
if (docEl && !tieredLoadState.l3Loaded) {
|
|
1178
|
+
docEl.classList.add('not-loaded');
|
|
1179
|
+
docEl.title = '点击加载文档';
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* 全量加载后: 清除所有"未加载"标记,确保所有 toggle 为 active 状态。
|
|
1185
|
+
*/
|
|
1186
|
+
function clearUnloadedTypeLegends() {
|
|
1187
|
+
var els = document.querySelectorAll('.legend-item.toggle.not-loaded');
|
|
1188
|
+
for (var i = 0; i < els.length; i++) {
|
|
1189
|
+
els[i].classList.remove('not-loaded');
|
|
1190
|
+
}
|
|
1191
|
+
// 确保所有 toggle 为 active
|
|
1192
|
+
var toggles = document.querySelectorAll('.legend-item.toggle');
|
|
1193
|
+
for (var i = 0; i < toggles.length; i++) {
|
|
1194
|
+
if (!toggles[i].classList.contains('active')) {
|
|
1195
|
+
toggles[i].classList.add('active');
|
|
1196
|
+
}
|
|
1197
|
+
var tp = toggles[i].getAttribute('data-type');
|
|
1198
|
+
if (tp) toggles[i].title = '点击切换' + tp + '显隐';
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
`;
|
|
1203
|
+
}
|
|
1204
|
+
//# sourceMappingURL=template-graph-vis.js.map
|