aifastdb-devplan 1.2.6 → 1.3.8
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/dev-plan-document-store.d.ts +18 -0
- package/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +80 -3
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-factory.d.ts +4 -0
- package/dist/dev-plan-factory.d.ts.map +1 -1
- package/dist/dev-plan-factory.js +6 -0
- package/dist/dev-plan-factory.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +85 -1
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +506 -18
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/dev-plan-interface.d.ts +38 -1
- package/dist/dev-plan-interface.d.ts.map +1 -1
- package/dist/dev-plan-migrate.d.ts +1 -1
- package/dist/dev-plan-migrate.d.ts.map +1 -1
- package/dist/dev-plan-migrate.js +2 -1
- package/dist/dev-plan-migrate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.js +168 -3
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/server.js +3 -1
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +777 -38
- package/dist/visualize/template.js.map +1 -1
- package/package.json +2 -2
|
@@ -98,7 +98,7 @@ function getVisualizationHTML(projectName) {
|
|
|
98
98
|
.panel-header.module { background: linear-gradient(135deg, #059669, #10b981); }
|
|
99
99
|
.panel-header.main-task { background: linear-gradient(135deg, #4f46e5, #6366f1); }
|
|
100
100
|
.panel-header.sub-task { background: linear-gradient(135deg, #7c3aed, #8b5cf6); }
|
|
101
|
-
.panel-header.document { background: linear-gradient(135deg, #
|
|
101
|
+
.panel-header.document { background: linear-gradient(135deg, #1d4ed8, #3b82f6); }
|
|
102
102
|
.panel-title { font-weight: 600; font-size: 14px; color: #fff; pointer-events: none; }
|
|
103
103
|
.panel-close { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 28px; height: 28px; border-radius: 6px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
|
|
104
104
|
.panel-close:hover { background: rgba(255,255,255,0.3); }
|
|
@@ -156,13 +156,14 @@ function getVisualizationHTML(projectName) {
|
|
|
156
156
|
.legend-icon.diamond { background: #10b981; clip-path: polygon(50% 0%,100% 50%,50% 100%,0% 50%); }
|
|
157
157
|
.legend-icon.circle { background: #6366f1; border-radius: 50%; }
|
|
158
158
|
.legend-icon.dot { background: #8b5cf6; border-radius: 50%; width: 8px; height: 8px; }
|
|
159
|
-
.legend-icon.square { background: #
|
|
159
|
+
.legend-icon.square { background: #3b82f6; border-radius: 2px; width: 10px; height: 10px; }
|
|
160
160
|
.legend-line { width: 24px; height: 2px; }
|
|
161
161
|
.legend-line.solid { background: #6b7280; }
|
|
162
162
|
.legend-line.thin { background: #6b7280; height: 1px; }
|
|
163
163
|
.legend-line.dashed { border-top: 2px dashed #6b7280; background: none; height: 0; }
|
|
164
164
|
.legend-line.dotted { border-top: 2px dotted #10b981; background: none; height: 0; }
|
|
165
|
-
.legend-line.task-doc { border-top: 2px dashed #
|
|
165
|
+
.legend-line.task-doc { border-top: 2px dashed #6b7280; background: none; height: 0; }
|
|
166
|
+
.legend-line.doc-child { border-top: 2px dashed #6b7280; background: none; height: 0; }
|
|
166
167
|
|
|
167
168
|
/* Document Content in Panel */
|
|
168
169
|
.doc-section { margin-top: 12px; border-top: 1px solid #374151; padding-top: 10px; }
|
|
@@ -313,6 +314,14 @@ function getVisualizationHTML(projectName) {
|
|
|
313
314
|
.docs-item-icon { font-size: 14px; flex-shrink: 0; opacity: 0.7; }
|
|
314
315
|
.docs-item-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
315
316
|
.docs-item-sub { font-size: 10px; color: #6b7280; flex-shrink: 0; }
|
|
317
|
+
.docs-item-toggle { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 4px; background: rgba(99,102,241,0.15); border: 1px solid rgba(99,102,241,0.3); color: #818cf8; font-size: 12px; font-weight: 700; cursor: pointer; flex-shrink: 0; transition: all 0.15s; line-height: 1; }
|
|
318
|
+
.docs-item-toggle:hover { background: rgba(99,102,241,0.3); color: #a5b4fc; }
|
|
319
|
+
.docs-children { overflow: hidden; transition: max-height 0.25s ease; }
|
|
320
|
+
.docs-children.collapsed { max-height: 0 !important; }
|
|
321
|
+
.docs-children .docs-item { padding-left: 44px; font-size: 12px; opacity: 0.85; }
|
|
322
|
+
.docs-children .docs-children .docs-item { padding-left: 60px; font-size: 11px; opacity: 0.75; }
|
|
323
|
+
.docs-child-line { position: absolute; left: 35px; top: 0; bottom: 0; width: 1px; background: #374151; }
|
|
324
|
+
.docs-item-wrapper { position: relative; }
|
|
316
325
|
|
|
317
326
|
.docs-content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
|
|
318
327
|
.docs-content-header { padding: 16px 28px 12px; border-bottom: 1px solid #374151; flex-shrink: 0; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|
@@ -465,6 +474,7 @@ function getVisualizationHTML(projectName) {
|
|
|
465
474
|
<div class="legend-item"><div class="legend-line dashed"></div> 文档</div>
|
|
466
475
|
<div class="legend-item"><div class="legend-line dotted"></div> 模块关联</div>
|
|
467
476
|
<div class="legend-item"><div class="legend-line task-doc"></div> 任务-文档</div>
|
|
477
|
+
<div class="legend-item"><div class="legend-line doc-child"></div> 文档层级</div>
|
|
468
478
|
</div>
|
|
469
479
|
</div>
|
|
470
480
|
|
|
@@ -650,6 +660,393 @@ var ctrlPressed = false;
|
|
|
650
660
|
var INCLUDE_NODE_DEGREE = true;
|
|
651
661
|
var ENABLE_BACKEND_DEGREE_FALLBACK = true;
|
|
652
662
|
|
|
663
|
+
// ========== 边高亮:选中节点时关联边变色,取消选中时恢复灰色 ==========
|
|
664
|
+
function highlightConnectedEdges(nodeId) {
|
|
665
|
+
if (!edgesDataSet || !network) return;
|
|
666
|
+
var connectedEdgeIds = network.getConnectedEdges(nodeId);
|
|
667
|
+
var connectedSet = {};
|
|
668
|
+
for (var i = 0; i < connectedEdgeIds.length; i++) connectedSet[connectedEdgeIds[i]] = true;
|
|
669
|
+
var updates = [];
|
|
670
|
+
edgesDataSet.forEach(function(edge) {
|
|
671
|
+
if (connectedSet[edge.id]) {
|
|
672
|
+
// 关联边 → 使用高亮色
|
|
673
|
+
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) });
|
|
674
|
+
} else {
|
|
675
|
+
// 非关联边 → 变淡
|
|
676
|
+
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 });
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
edgesDataSet.update(updates);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function resetAllEdgeColors() {
|
|
683
|
+
if (!edgesDataSet) return;
|
|
684
|
+
var updates = [];
|
|
685
|
+
edgesDataSet.forEach(function(edge) {
|
|
686
|
+
updates.push({ id: edge.id, color: { color: EDGE_GRAY, highlight: edge._highlightColor || '#9ca3af', hover: edge._highlightColor || '#9ca3af' }, width: edge._origWidth || edge.width || 1 });
|
|
687
|
+
});
|
|
688
|
+
edgesDataSet.update(updates);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ========== 文档节点展开/收起 ==========
|
|
692
|
+
/** 记录哪些父文档节点处于收起状态(nodeId → true 表示收起) */
|
|
693
|
+
var collapsedDocNodes = {};
|
|
694
|
+
/** 收起时被重定向的边信息: { edgeId: { origFrom, origTo } } */
|
|
695
|
+
var redirectedEdges = {};
|
|
696
|
+
/** 记录各父文档 +/- 按钮在 canvas 坐标系中的位置,用于点击检测 */
|
|
697
|
+
var docToggleBtnPositions = {};
|
|
698
|
+
/** 收起前保存子文档节点的位置: { nodeId: { x, y } } */
|
|
699
|
+
var savedChildPositions = {};
|
|
700
|
+
|
|
701
|
+
/** 获取节点 ID 对应的子文档节点 ID 列表(仅直接子文档) */
|
|
702
|
+
function getChildDocNodeIds(parentNodeId) {
|
|
703
|
+
var childIds = [];
|
|
704
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
705
|
+
if (allEdges[i].from === parentNodeId && allEdges[i].label === 'doc_has_child') {
|
|
706
|
+
childIds.push(allEdges[i].to);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return childIds;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** 递归获取所有后代文档节点 ID(含多层子文档) */
|
|
713
|
+
function getAllDescendantDocNodeIds(parentNodeId) {
|
|
714
|
+
var result = [];
|
|
715
|
+
var queue = [parentNodeId];
|
|
716
|
+
while (queue.length > 0) {
|
|
717
|
+
var current = queue.shift();
|
|
718
|
+
var children = getChildDocNodeIds(current);
|
|
719
|
+
for (var i = 0; i < children.length; i++) {
|
|
720
|
+
result.push(children[i]);
|
|
721
|
+
queue.push(children[i]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** 检查节点是否为父文档(有子文档的文档节点) */
|
|
728
|
+
function isParentDocNode(node) {
|
|
729
|
+
if (node.type !== 'document') return false;
|
|
730
|
+
var props = node.properties || {};
|
|
731
|
+
var childDocs = props.childDocs || [];
|
|
732
|
+
if (childDocs.length > 0) return true;
|
|
733
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
734
|
+
if (allEdges[i].from === node.id && allEdges[i].label === 'doc_has_child') return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** 通过 nodeId 在 allNodes 中查找节点数据 */
|
|
740
|
+
function findAllNode(nodeId) {
|
|
741
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
742
|
+
if (allNodes[i].id === nodeId) return allNodes[i];
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** 检查节点是否应被隐藏(因为其祖先父文档处于收起状态) */
|
|
748
|
+
function isNodeCollapsedByParent(nodeId) {
|
|
749
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
750
|
+
var e = allEdges[i];
|
|
751
|
+
if (e.to === nodeId && e.label === 'doc_has_child') {
|
|
752
|
+
if (collapsedDocNodes[e.from]) return true;
|
|
753
|
+
if (isNodeCollapsedByParent(e.from)) return true;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** 切换父文档节点的展开/收起状态 */
|
|
760
|
+
function toggleDocNodeExpand(nodeId) {
|
|
761
|
+
collapsedDocNodes[nodeId] = !collapsedDocNodes[nodeId];
|
|
762
|
+
var childIds = getAllDescendantDocNodeIds(nodeId);
|
|
763
|
+
var isCollapsed = collapsedDocNodes[nodeId];
|
|
764
|
+
|
|
765
|
+
if (isCollapsed) {
|
|
766
|
+
// ---- 收起 ----
|
|
767
|
+
var removeNodeIds = {};
|
|
768
|
+
for (var i = 0; i < childIds.length; i++) removeNodeIds[childIds[i]] = true;
|
|
769
|
+
|
|
770
|
+
// 0) 保存子文档节点当前位置
|
|
771
|
+
var childPositions = network.getPositions(childIds);
|
|
772
|
+
for (var i = 0; i < childIds.length; i++) {
|
|
773
|
+
if (childPositions[childIds[i]]) {
|
|
774
|
+
savedChildPositions[childIds[i]] = { x: childPositions[childIds[i]].x, y: childPositions[childIds[i]].y };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 1) 将连接到子文档的非 doc_has_child 边重定向到父文档
|
|
779
|
+
var edgesToRedirect = [];
|
|
780
|
+
var edgesToRemove = [];
|
|
781
|
+
edgesDataSet.forEach(function(edge) {
|
|
782
|
+
var touchesChild = removeNodeIds[edge.from] || removeNodeIds[edge.to];
|
|
783
|
+
if (!touchesChild) return;
|
|
784
|
+
if (edge._label === 'doc_has_child') {
|
|
785
|
+
// doc_has_child 边直接移除
|
|
786
|
+
edgesToRemove.push(edge.id);
|
|
787
|
+
} else {
|
|
788
|
+
// 其他边(如 task_has_doc)重定向到父文档
|
|
789
|
+
edgesToRedirect.push(edge);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// 移除 doc_has_child 边
|
|
794
|
+
if (edgesToRemove.length > 0) edgesDataSet.remove(edgesToRemove);
|
|
795
|
+
|
|
796
|
+
// 重定向其他边到父文档
|
|
797
|
+
for (var i = 0; i < edgesToRedirect.length; i++) {
|
|
798
|
+
var edge = edgesToRedirect[i];
|
|
799
|
+
var newFrom = removeNodeIds[edge.from] ? nodeId : edge.from;
|
|
800
|
+
var newTo = removeNodeIds[edge.to] ? nodeId : edge.to;
|
|
801
|
+
// 检查是否已存在相同的重定向边(避免重复)
|
|
802
|
+
var duplicate = false;
|
|
803
|
+
edgesDataSet.forEach(function(existing) {
|
|
804
|
+
if (existing.from === newFrom && existing.to === newTo && existing._label === edge._label) duplicate = true;
|
|
805
|
+
});
|
|
806
|
+
if (newFrom === newTo) { duplicate = true; } // 不自连
|
|
807
|
+
if (!duplicate) {
|
|
808
|
+
redirectedEdges[edge.id] = { origFrom: edge.from, origTo: edge.to };
|
|
809
|
+
edgesDataSet.update({ id: edge.id, from: newFrom, to: newTo });
|
|
810
|
+
} else {
|
|
811
|
+
// 重复则移除
|
|
812
|
+
redirectedEdges[edge.id] = { origFrom: edge.from, origTo: edge.to };
|
|
813
|
+
edgesDataSet.remove([edge.id]);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 2) 移除子文档节点
|
|
818
|
+
nodesDataSet.remove(childIds);
|
|
819
|
+
|
|
820
|
+
// 3) 更新父节点标签(加左侧留白和收起数量提示)
|
|
821
|
+
var parentNode = nodesDataSet.get(nodeId);
|
|
822
|
+
if (parentNode) {
|
|
823
|
+
var origLabel = parentNode._origLabel || parentNode.label;
|
|
824
|
+
var pad = ' ';
|
|
825
|
+
nodesDataSet.update({ id: nodeId, label: pad + origLabel + ' [' + childIds.length + ']', _origLabel: origLabel });
|
|
826
|
+
}
|
|
827
|
+
log('收起文档: 隐藏 ' + childIds.length + ' 个子文档, 重定向 ' + edgesToRedirect.length + ' 条边', true);
|
|
828
|
+
|
|
829
|
+
} else {
|
|
830
|
+
// ---- 展开 ----
|
|
831
|
+
// 1) 恢复被重定向的边
|
|
832
|
+
var restoreEdgeIds = [];
|
|
833
|
+
for (var eid in redirectedEdges) {
|
|
834
|
+
var info = redirectedEdges[eid];
|
|
835
|
+
// 检查 origFrom 或 origTo 是否属于此父文档的子孙
|
|
836
|
+
var isRelated = false;
|
|
837
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
838
|
+
if (info.origFrom === childIds[ci] || info.origTo === childIds[ci]) { isRelated = true; break; }
|
|
839
|
+
}
|
|
840
|
+
if (!isRelated) continue;
|
|
841
|
+
restoreEdgeIds.push(eid);
|
|
842
|
+
// 恢复原始 from/to 或重新添加
|
|
843
|
+
var existing = edgesDataSet.get(eid);
|
|
844
|
+
if (existing) {
|
|
845
|
+
edgesDataSet.update({ id: eid, from: info.origFrom, to: info.origTo });
|
|
846
|
+
} else {
|
|
847
|
+
// 边已被移除(因重复),需重新添加
|
|
848
|
+
// 在 allEdges 中找到此边原始数据
|
|
849
|
+
for (var ai = 0; ai < allEdges.length; ai++) {
|
|
850
|
+
var ae = allEdges[ai];
|
|
851
|
+
if (ae.from === info.origFrom && ae.to === info.origTo) {
|
|
852
|
+
var es = edgeStyle(ae);
|
|
853
|
+
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' });
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
for (var ri = 0; ri < restoreEdgeIds.length; ri++) {
|
|
860
|
+
delete redirectedEdges[restoreEdgeIds[ri]];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// 2) 重新添加子文档节点(使用保存的位置或思维导图排列)
|
|
864
|
+
var parentPos = network.getPositions([nodeId])[nodeId];
|
|
865
|
+
var addNodes = [];
|
|
866
|
+
var visibleChildIds = [];
|
|
867
|
+
for (var ni = 0; ni < allNodes.length; ni++) {
|
|
868
|
+
var n = allNodes[ni];
|
|
869
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
870
|
+
if (n.id === childIds[ci] && !isNodeCollapsedByParent(n.id)) {
|
|
871
|
+
var deg = getNodeDegree(n);
|
|
872
|
+
var s = nodeStyle(n, deg);
|
|
873
|
+
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 || {} };
|
|
874
|
+
// 使用保存的位置
|
|
875
|
+
if (savedChildPositions[n.id]) {
|
|
876
|
+
nodeData.x = savedChildPositions[n.id].x;
|
|
877
|
+
nodeData.y = savedChildPositions[n.id].y;
|
|
878
|
+
}
|
|
879
|
+
addNodes.push(nodeData);
|
|
880
|
+
visibleChildIds.push(n.id);
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (addNodes.length > 0) {
|
|
886
|
+
nodesDataSet.add(addNodes);
|
|
887
|
+
// 如果没有保存位置,按思维导图方式排列
|
|
888
|
+
var needArrange = false;
|
|
889
|
+
for (var i = 0; i < visibleChildIds.length; i++) {
|
|
890
|
+
if (!savedChildPositions[visibleChildIds[i]]) { needArrange = true; break; }
|
|
891
|
+
}
|
|
892
|
+
if (needArrange && parentPos) {
|
|
893
|
+
arrangeDocMindMap(nodeId, visibleChildIds);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 3) 重新添加 doc_has_child 边
|
|
898
|
+
var addedNodeIds = {};
|
|
899
|
+
nodesDataSet.forEach(function(n) { addedNodeIds[n.id] = true; });
|
|
900
|
+
var addEdges = [];
|
|
901
|
+
for (var ei = 0; ei < allEdges.length; ei++) {
|
|
902
|
+
var e = allEdges[ei];
|
|
903
|
+
if (!addedNodeIds[e.from] || !addedNodeIds[e.to]) continue;
|
|
904
|
+
if (e.label !== 'doc_has_child') continue;
|
|
905
|
+
var exists = false;
|
|
906
|
+
edgesDataSet.forEach(function(existing) {
|
|
907
|
+
if (existing.from === e.from && existing.to === e.to && existing._label === e.label) exists = true;
|
|
908
|
+
});
|
|
909
|
+
if (!exists) {
|
|
910
|
+
var es = edgeStyle(e);
|
|
911
|
+
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' });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (addEdges.length > 0) edgesDataSet.add(addEdges);
|
|
915
|
+
|
|
916
|
+
// 4) 恢复父节点标签(保留左侧留白)
|
|
917
|
+
var parentNode = nodesDataSet.get(nodeId);
|
|
918
|
+
if (parentNode && parentNode._origLabel) {
|
|
919
|
+
var pad = ' ';
|
|
920
|
+
nodesDataSet.update({ id: nodeId, label: pad + parentNode._origLabel });
|
|
921
|
+
}
|
|
922
|
+
log('展开文档: 显示 ' + addNodes.length + ' 个子文档', true);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** 在 afterDrawing 中绘制父文档节点的 +/- 按钮 */
|
|
927
|
+
function drawDocToggleButtons(ctx) {
|
|
928
|
+
docToggleBtnPositions = {};
|
|
929
|
+
nodesDataSet.forEach(function(node) {
|
|
930
|
+
if (node._type !== 'document') return;
|
|
931
|
+
var allNode = findAllNode(node.id);
|
|
932
|
+
if (!allNode || !isParentDocNode(allNode)) return;
|
|
933
|
+
var pos = network.getPositions([node.id])[node.id];
|
|
934
|
+
if (!pos) return;
|
|
935
|
+
var isCollapsed = !!collapsedDocNodes[node.id];
|
|
936
|
+
var btnRadius = 9;
|
|
937
|
+
|
|
938
|
+
// 使用 getBoundingBox 获取节点精确边界,按钮放在节点内左侧留白区域中心
|
|
939
|
+
var bbox = network.getBoundingBox(node.id);
|
|
940
|
+
var btnX, btnY;
|
|
941
|
+
if (bbox) {
|
|
942
|
+
btnX = bbox.left + btnRadius + 1; // 按钮完全在节点内,左侧留白区域居中
|
|
943
|
+
btnY = (bbox.top + bbox.bottom) / 2; // 垂直居中
|
|
944
|
+
} else {
|
|
945
|
+
btnX = pos.x;
|
|
946
|
+
btnY = pos.y;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 记录位置(canvas 坐标)
|
|
950
|
+
docToggleBtnPositions[node.id] = { x: btnX, y: btnY, r: btnRadius };
|
|
951
|
+
|
|
952
|
+
// 绘制圆形按钮背景(蓝色系配色)
|
|
953
|
+
ctx.beginPath();
|
|
954
|
+
ctx.arc(btnX, btnY, btnRadius, 0, Math.PI * 2);
|
|
955
|
+
ctx.fillStyle = isCollapsed ? '#3b82f6' : '#1e40af'; // 收起:亮蓝 展开:深蓝
|
|
956
|
+
ctx.fill();
|
|
957
|
+
ctx.strokeStyle = '#ffffff'; // 白色描边
|
|
958
|
+
ctx.lineWidth = 1.5;
|
|
959
|
+
ctx.stroke();
|
|
960
|
+
ctx.closePath();
|
|
961
|
+
|
|
962
|
+
// 绘制 + 或 - 符号
|
|
963
|
+
ctx.fillStyle = '#ffffff';
|
|
964
|
+
ctx.font = 'bold 13px sans-serif';
|
|
965
|
+
ctx.textAlign = 'center';
|
|
966
|
+
ctx.textBaseline = 'middle';
|
|
967
|
+
ctx.fillText(isCollapsed ? '+' : '−', btnX, btnY + 0.5);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** 检查 canvas 坐标是否点击了某个 +/- 按钮,返回 nodeId 或 null */
|
|
972
|
+
function hitTestDocToggleBtn(canvasX, canvasY) {
|
|
973
|
+
for (var nodeId in docToggleBtnPositions) {
|
|
974
|
+
var btn = docToggleBtnPositions[nodeId];
|
|
975
|
+
var dx = canvasX - btn.x;
|
|
976
|
+
var dy = canvasY - btn.y;
|
|
977
|
+
if (dx * dx + dy * dy <= (btn.r + 4) * (btn.r + 4)) {
|
|
978
|
+
return nodeId;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* 将父文档及其子文档按思维导图方式排列:
|
|
986
|
+
* 父文档在左,子文档在右侧垂直等距、左边缘对齐
|
|
987
|
+
*/
|
|
988
|
+
function arrangeDocMindMap(parentNodeId, childNodeIds) {
|
|
989
|
+
if (!network || childNodeIds.length === 0) return;
|
|
990
|
+
var parentPos = network.getPositions([parentNodeId])[parentNodeId];
|
|
991
|
+
if (!parentPos) return;
|
|
992
|
+
|
|
993
|
+
var parentBbox = network.getBoundingBox(parentNodeId);
|
|
994
|
+
var parentRight = parentBbox ? parentBbox.right : (parentPos.x + 80);
|
|
995
|
+
var leftEdgeX = parentRight + 40; // 子节点左边缘的目标 X
|
|
996
|
+
var vGap = 45;
|
|
997
|
+
var count = childNodeIds.length;
|
|
998
|
+
var totalHeight = (count - 1) * vGap;
|
|
999
|
+
var startY = parentPos.y - totalHeight / 2;
|
|
1000
|
+
|
|
1001
|
+
// 先读取每个子节点当前的宽度(移动前 bbox 有效)
|
|
1002
|
+
var halfLefts = [];
|
|
1003
|
+
for (var i = 0; i < count; i++) {
|
|
1004
|
+
var cid = childNodeIds[i];
|
|
1005
|
+
var bbox = network.getBoundingBox(cid);
|
|
1006
|
+
var cpos = network.getPositions([cid])[cid];
|
|
1007
|
+
if (bbox && cpos) {
|
|
1008
|
+
halfLefts.push(cpos.x - bbox.left); // 节点中心到左边缘的距离(即半宽)
|
|
1009
|
+
} else {
|
|
1010
|
+
halfLefts.push(100); // 默认估算
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 一次性移动所有子节点:左边缘对齐到 leftEdgeX
|
|
1015
|
+
for (var i = 0; i < count; i++) {
|
|
1016
|
+
var cx = leftEdgeX + halfLefts[i];
|
|
1017
|
+
var cy = startY + i * vGap;
|
|
1018
|
+
network.moveNode(childNodeIds[i], cx, cy);
|
|
1019
|
+
savedChildPositions[childNodeIds[i]] = { x: cx, y: cy };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** 初始化时将所有父文档-子文档按思维导图方式排列 */
|
|
1024
|
+
function arrangeAllDocMindMaps() {
|
|
1025
|
+
// 找到所有父文档节点
|
|
1026
|
+
var parentDocIds = [];
|
|
1027
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
1028
|
+
var n = allNodes[i];
|
|
1029
|
+
if (isParentDocNode(n)) {
|
|
1030
|
+
// 检查该节点在当前可见节点集中
|
|
1031
|
+
var visible = nodesDataSet.get(n.id);
|
|
1032
|
+
if (visible) parentDocIds.push(n.id);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
for (var pi = 0; pi < parentDocIds.length; pi++) {
|
|
1036
|
+
var pid = parentDocIds[pi];
|
|
1037
|
+
var childIds = getChildDocNodeIds(pid);
|
|
1038
|
+
// 只排列当前可见的子节点
|
|
1039
|
+
var visibleChildIds = [];
|
|
1040
|
+
for (var ci = 0; ci < childIds.length; ci++) {
|
|
1041
|
+
if (nodesDataSet.get(childIds[ci])) visibleChildIds.push(childIds[ci]);
|
|
1042
|
+
}
|
|
1043
|
+
if (visibleChildIds.length > 0) {
|
|
1044
|
+
arrangeDocMindMap(pid, visibleChildIds);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
log('思维导图排列: ' + parentDocIds.length + ' 个父文档已排列', true);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
653
1050
|
// ========== 呼吸灯动画 (in_progress 主任务) ==========
|
|
654
1051
|
var breathAnimId = null; // requestAnimationFrame ID
|
|
655
1052
|
var breathPhase = 0; // 动画相位 [0, 2π)
|
|
@@ -696,7 +1093,7 @@ window.addEventListener('blur', function() { ctrlPressed = false; });
|
|
|
696
1093
|
// ========== Node Styles ==========
|
|
697
1094
|
var STATUS_COLORS = {
|
|
698
1095
|
completed: { bg: '#059669', border: '#047857', font: '#d1fae5' },
|
|
699
|
-
in_progress: { bg: '#
|
|
1096
|
+
in_progress: { bg: '#7c3aed', border: '#6d28d9', font: '#ddd6fe' },
|
|
700
1097
|
pending: { bg: '#4b5563', border: '#374151', font: '#d1d5db' },
|
|
701
1098
|
cancelled: { bg: '#92400e', border: '#78350f', font: '#fde68a' }
|
|
702
1099
|
};
|
|
@@ -751,19 +1148,23 @@ function nodeStyle(node, degree) {
|
|
|
751
1148
|
return { shape: 'dot', size: ns.size, color: { background: sc.bg, border: sc.border, highlight: { background: sc.bg, border: '#fff' } }, font: { size: ns.fontSize, color: sc.font }, borderWidth: 1 };
|
|
752
1149
|
}
|
|
753
1150
|
if (t === 'document') {
|
|
754
|
-
return { shape: 'box', size: ns.size, color: { background: '#
|
|
1151
|
+
return { shape: 'box', size: ns.size, color: { background: '#2563eb', border: '#1d4ed8', highlight: { background: '#3b82f6', border: '#fff' } }, font: { size: ns.fontSize, color: '#dbeafe' }, borderWidth: 1 };
|
|
755
1152
|
}
|
|
756
1153
|
return { shape: 'dot', size: ns.size, color: { background: '#6b7280', border: '#4b5563' }, font: { size: ns.fontSize, color: '#9ca3af' } };
|
|
757
1154
|
}
|
|
758
1155
|
|
|
1156
|
+
// 默认灰色 + 选中时高亮色(per-type)
|
|
1157
|
+
var EDGE_GRAY = '#4b5563';
|
|
1158
|
+
|
|
759
1159
|
function edgeStyle(edge) {
|
|
760
1160
|
var label = edge.label || '';
|
|
761
|
-
if (label === 'has_main_task') return { width: 2, color: { color: '#
|
|
762
|
-
if (label === 'has_sub_task') return { width: 1, color: { color: '#
|
|
763
|
-
if (label === 'has_document') return { width: 1, color: { color: '#
|
|
764
|
-
if (label === 'module_has_task') return { width: 1.5, color: { color: '#
|
|
765
|
-
if (label === 'task_has_doc') return { width: 1.5, color: { color: '#
|
|
766
|
-
return { width: 1, color: { color: '#
|
|
1161
|
+
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' };
|
|
1162
|
+
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' };
|
|
1163
|
+
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' };
|
|
1164
|
+
if (label === 'module_has_task') return { width: 1.5, color: { color: EDGE_GRAY, highlight: '#34d399', hover: '#34d399' }, dashes: [2, 4], arrows: { to: { enabled: true, scaleFactor: 0.5 } }, _highlightColor: '#34d399' };
|
|
1165
|
+
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' };
|
|
1166
|
+
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' };
|
|
1167
|
+
return { width: 1, color: { color: EDGE_GRAY, highlight: '#9ca3af', hover: '#9ca3af' }, dashes: false, _highlightColor: '#9ca3af' };
|
|
767
1168
|
}
|
|
768
1169
|
|
|
769
1170
|
// ========== Data Loading ==========
|
|
@@ -794,13 +1195,16 @@ function renderStats(progress, graph) {
|
|
|
794
1195
|
var bar = document.getElementById('statsBar');
|
|
795
1196
|
var pct = progress.overallPercent || 0;
|
|
796
1197
|
var moduleCount = 0;
|
|
1198
|
+
var docCount = 0;
|
|
797
1199
|
for (var i = 0; i < graph.nodes.length; i++) {
|
|
798
1200
|
if (graph.nodes[i].type === 'module') moduleCount++;
|
|
1201
|
+
if (graph.nodes[i].type === 'document') docCount++;
|
|
799
1202
|
}
|
|
800
1203
|
bar.innerHTML =
|
|
801
1204
|
'<div class="stat clickable" onclick="showStatsModal(\\x27module\\x27)" title="查看所有模块"><span class="num amber">' + moduleCount + '</span> 模块</div>' +
|
|
802
1205
|
'<div class="stat clickable" onclick="showStatsModal(\\x27main-task\\x27)" title="查看所有主任务"><span class="num blue">' + progress.mainTaskCount + '</span> 主任务</div>' +
|
|
803
1206
|
'<div class="stat clickable" onclick="showStatsModal(\\x27sub-task\\x27)" title="查看所有子任务"><span class="num purple">' + progress.subTaskCount + '</span> 子任务</div>' +
|
|
1207
|
+
'<div class="stat clickable" onclick="showStatsModal(\\x27document\\x27)" title="查看所有文档"><span class="num" style="color:#3b82f6;">📄 ' + docCount + '</span> 文档</div>' +
|
|
804
1208
|
'<div class="stat"><span class="num green">' + progress.completedSubTasks + '/' + progress.subTaskCount + '</span> 已完成</div>' +
|
|
805
1209
|
'<div class="stat"><div class="progress-bar"><div class="progress-fill" style="width:' + pct + '%"></div></div><span>' + pct + '%</span></div>';
|
|
806
1210
|
}
|
|
@@ -819,12 +1223,26 @@ function renderGraph() {
|
|
|
819
1223
|
}
|
|
820
1224
|
|
|
821
1225
|
var visibleNodes = [];
|
|
1226
|
+
var DOC_BTN_PAD = ' '; // 父文档标签左侧留白,为 +/- 按钮腾出空间
|
|
822
1227
|
for (var i = 0; i < allNodes.length; i++) {
|
|
823
1228
|
var n = allNodes[i];
|
|
824
1229
|
if (hiddenTypes[n.type]) continue;
|
|
1230
|
+
// 跳过被收起的子文档节点
|
|
1231
|
+
if (isNodeCollapsedByParent(n.id)) continue;
|
|
825
1232
|
var deg = getNodeDegree(n);
|
|
826
1233
|
var s = nodeStyle(n, deg);
|
|
827
|
-
|
|
1234
|
+
var label = n.label;
|
|
1235
|
+
var isParentDoc = isParentDocNode(n);
|
|
1236
|
+
if (isParentDoc) {
|
|
1237
|
+
// 父文档标签左侧加空格,为按钮腾位
|
|
1238
|
+
if (collapsedDocNodes[n.id]) {
|
|
1239
|
+
var childCount = getAllDescendantDocNodeIds(n.id).length;
|
|
1240
|
+
label = DOC_BTN_PAD + label + ' [' + childCount + ']';
|
|
1241
|
+
} else {
|
|
1242
|
+
label = DOC_BTN_PAD + label;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
visibleNodes.push({ id: n.id, label: 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 || {}, _isParentDoc: isParentDoc });
|
|
828
1246
|
}
|
|
829
1247
|
|
|
830
1248
|
var visibleIds = {};
|
|
@@ -835,7 +1253,7 @@ function renderGraph() {
|
|
|
835
1253
|
var e = allEdges[i];
|
|
836
1254
|
if (!visibleIds[e.from] || !visibleIds[e.to]) continue;
|
|
837
1255
|
var es = edgeStyle(e);
|
|
838
|
-
visibleEdges.push({ id: 'e' + i, from: e.from, to: e.to, width: es.width, color: es.color, dashes: es.dashes, arrows: es.arrows, _label: e.label });
|
|
1256
|
+
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' });
|
|
839
1257
|
}
|
|
840
1258
|
|
|
841
1259
|
log('可见节点: ' + visibleNodes.length + ', 可见边: ' + visibleEdges.length, true);
|
|
@@ -890,16 +1308,28 @@ function renderGraph() {
|
|
|
890
1308
|
network.setOptions({ physics: { enabled: false } });
|
|
891
1309
|
document.getElementById('loading').style.display = 'none';
|
|
892
1310
|
log('图谱渲染完成! ' + visibleNodes.length + ' 节点, ' + visibleEdges.length + ' 边', true);
|
|
1311
|
+
// 稳定后将父文档-子文档按思维导图方式整齐排列
|
|
1312
|
+
arrangeAllDocMindMaps();
|
|
893
1313
|
network.fit({ animation: { duration: 800, easingFunction: 'easeInOutQuad' } });
|
|
894
1314
|
});
|
|
895
1315
|
|
|
896
1316
|
network.on('click', function(params) {
|
|
1317
|
+
// 先检查是否点击了 +/- 按钮
|
|
1318
|
+
if (params.pointer && params.pointer.canvas) {
|
|
1319
|
+
var hitNodeId = hitTestDocToggleBtn(params.pointer.canvas.x, params.pointer.canvas.y);
|
|
1320
|
+
if (hitNodeId) {
|
|
1321
|
+
toggleDocNodeExpand(hitNodeId);
|
|
1322
|
+
return; // 消费此次点击,不触发节点选择
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
897
1325
|
if (params.nodes.length > 0) {
|
|
898
1326
|
// 直接点击图谱节点 → 清空历史栈,重新开始导航
|
|
899
1327
|
panelHistory = [];
|
|
900
1328
|
currentPanelNodeId = null;
|
|
1329
|
+
highlightConnectedEdges(params.nodes[0]);
|
|
901
1330
|
showPanel(params.nodes[0]);
|
|
902
1331
|
} else {
|
|
1332
|
+
resetAllEdgeColors();
|
|
903
1333
|
closePanel();
|
|
904
1334
|
}
|
|
905
1335
|
});
|
|
@@ -955,8 +1385,11 @@ function renderGraph() {
|
|
|
955
1385
|
}
|
|
956
1386
|
});
|
|
957
1387
|
|
|
958
|
-
// ==========
|
|
1388
|
+
// ========== afterDrawing: 呼吸灯 + 文档展开/收起按钮 ==========
|
|
959
1389
|
network.on('afterDrawing', function(ctx) {
|
|
1390
|
+
// 绘制父文档的 +/- 按钮
|
|
1391
|
+
drawDocToggleButtons(ctx);
|
|
1392
|
+
|
|
960
1393
|
var ids = getInProgressMainTaskIds();
|
|
961
1394
|
if (ids.length === 0) return;
|
|
962
1395
|
|
|
@@ -974,23 +1407,46 @@ function renderGraph() {
|
|
|
974
1407
|
// 再通过 DOMtoCanvas 获取正确的 canvas 上下文坐标
|
|
975
1408
|
// vis-network 的 afterDrawing ctx 已经在正确的坐标系中,直接用 pos 即可
|
|
976
1409
|
|
|
977
|
-
//
|
|
978
|
-
var
|
|
979
|
-
var
|
|
980
|
-
|
|
1410
|
+
// 外层大范围弥散光晕(营造醒目的辉光感)
|
|
1411
|
+
var outerGlowRadius = baseSize + 20 + breath * baseSize * 2.5;
|
|
1412
|
+
var outerGrad = ctx.createRadialGradient(pos.x, pos.y, baseSize, pos.x, pos.y, outerGlowRadius);
|
|
1413
|
+
outerGrad.addColorStop(0, 'rgba(124, 58, 237, ' + (0.18 + breath * 0.12) + ')');
|
|
1414
|
+
outerGrad.addColorStop(0.5, 'rgba(139, 92, 246, ' + (0.08 + breath * 0.06) + ')');
|
|
1415
|
+
outerGrad.addColorStop(1, 'rgba(139, 92, 246, 0)');
|
|
1416
|
+
ctx.beginPath();
|
|
1417
|
+
ctx.arc(pos.x, pos.y, outerGlowRadius, 0, Math.PI * 2);
|
|
1418
|
+
ctx.fillStyle = outerGrad;
|
|
1419
|
+
ctx.fill();
|
|
1420
|
+
ctx.closePath();
|
|
1421
|
+
|
|
1422
|
+
// 外圈脉冲光环(更粗、扩展范围更大)
|
|
1423
|
+
var maxExpand = baseSize * 2.2;
|
|
1424
|
+
var ringRadius = baseSize + 8 + breath * maxExpand;
|
|
1425
|
+
var ringAlpha = 0.55 * (1 - breath * 0.5);
|
|
981
1426
|
|
|
982
1427
|
ctx.beginPath();
|
|
983
1428
|
ctx.arc(pos.x, pos.y, ringRadius, 0, Math.PI * 2);
|
|
984
|
-
ctx.strokeStyle = 'rgba(
|
|
985
|
-
ctx.lineWidth =
|
|
1429
|
+
ctx.strokeStyle = 'rgba(139, 92, 246, ' + ringAlpha + ')';
|
|
1430
|
+
ctx.lineWidth = 3.5 + breath * 3;
|
|
986
1431
|
ctx.stroke();
|
|
987
1432
|
ctx.closePath();
|
|
988
1433
|
|
|
989
|
-
//
|
|
990
|
-
var
|
|
991
|
-
var
|
|
992
|
-
|
|
993
|
-
|
|
1434
|
+
// 中圈脉冲光环(第二道更紧凑的环)
|
|
1435
|
+
var midRingRadius = baseSize + 4 + breath * baseSize * 1.2;
|
|
1436
|
+
var midRingAlpha = 0.4 * (1 - breath * 0.4);
|
|
1437
|
+
ctx.beginPath();
|
|
1438
|
+
ctx.arc(pos.x, pos.y, midRingRadius, 0, Math.PI * 2);
|
|
1439
|
+
ctx.strokeStyle = 'rgba(167, 139, 250, ' + midRingAlpha + ')';
|
|
1440
|
+
ctx.lineWidth = 2.5 + breath * 2;
|
|
1441
|
+
ctx.stroke();
|
|
1442
|
+
ctx.closePath();
|
|
1443
|
+
|
|
1444
|
+
// 内圈柔光(更大范围的径向渐变)
|
|
1445
|
+
var glowRadius = baseSize + 10 + breath * 16;
|
|
1446
|
+
var gradient = ctx.createRadialGradient(pos.x, pos.y, baseSize * 0.3, pos.x, pos.y, glowRadius);
|
|
1447
|
+
gradient.addColorStop(0, 'rgba(124, 58, 237, ' + (0.25 + breath * 0.15) + ')');
|
|
1448
|
+
gradient.addColorStop(0.6, 'rgba(139, 92, 246, ' + (0.10 + breath * 0.08) + ')');
|
|
1449
|
+
gradient.addColorStop(1, 'rgba(139, 92, 246, 0)');
|
|
994
1450
|
ctx.beginPath();
|
|
995
1451
|
ctx.arc(pos.x, pos.y, glowRadius, 0, Math.PI * 2);
|
|
996
1452
|
ctx.fillStyle = gradient;
|
|
@@ -1035,6 +1491,7 @@ function navigateToPanel(nodeId) {
|
|
|
1035
1491
|
panelHistory.push(currentPanelNodeId);
|
|
1036
1492
|
}
|
|
1037
1493
|
network.selectNodes([nodeId]);
|
|
1494
|
+
highlightConnectedEdges(nodeId);
|
|
1038
1495
|
showPanel(nodeId);
|
|
1039
1496
|
}
|
|
1040
1497
|
|
|
@@ -1043,6 +1500,7 @@ function panelGoBack() {
|
|
|
1043
1500
|
if (panelHistory.length === 0) return;
|
|
1044
1501
|
var prevNodeId = panelHistory.pop();
|
|
1045
1502
|
network.selectNodes([prevNodeId]);
|
|
1503
|
+
highlightConnectedEdges(prevNodeId);
|
|
1046
1504
|
showPanel(prevNodeId);
|
|
1047
1505
|
}
|
|
1048
1506
|
|
|
@@ -1251,6 +1709,7 @@ function closePanel() {
|
|
|
1251
1709
|
panelHistory = [];
|
|
1252
1710
|
currentPanelNodeId = null;
|
|
1253
1711
|
updateBackButton();
|
|
1712
|
+
resetAllEdgeColors();
|
|
1254
1713
|
}
|
|
1255
1714
|
|
|
1256
1715
|
// ========== Panel Resize ==========
|
|
@@ -1523,7 +1982,16 @@ function toggleFilter(type) {
|
|
|
1523
1982
|
}
|
|
1524
1983
|
|
|
1525
1984
|
// ========== Stats Modal ==========
|
|
1985
|
+
/** 记录文档弹层中各文档的折叠状态(docKey → true 表示已展开) */
|
|
1986
|
+
var docModalExpandedState = {};
|
|
1987
|
+
|
|
1526
1988
|
function showStatsModal(nodeType) {
|
|
1989
|
+
// 文档类型使用专用渲染
|
|
1990
|
+
if (nodeType === 'document') {
|
|
1991
|
+
showDocModal();
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1527
1995
|
var titleMap = { 'module': '功能模块', 'main-task': '主任务', 'sub-task': '子任务' };
|
|
1528
1996
|
var iconMap = { 'module': '◆', 'main-task': '●', 'sub-task': '·' };
|
|
1529
1997
|
var items = [];
|
|
@@ -1577,6 +2045,161 @@ function showStatsModal(nodeType) {
|
|
|
1577
2045
|
document.getElementById('statsModalOverlay').classList.add('active');
|
|
1578
2046
|
}
|
|
1579
2047
|
|
|
2048
|
+
/** 获取文档节点的 docKey (section|subSection) */
|
|
2049
|
+
function getDocNodeKey(node) {
|
|
2050
|
+
var p = node.properties || {};
|
|
2051
|
+
return p.section + (p.subSection ? '|' + p.subSection : '');
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/** 构建文档层级树:{ node, children: [...] } */
|
|
2055
|
+
function buildDocTree() {
|
|
2056
|
+
var docNodes = [];
|
|
2057
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
2058
|
+
if (allNodes[i].type === 'document') docNodes.push(allNodes[i]);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// 建立 parentDoc → children 映射
|
|
2062
|
+
var childrenMap = {}; // parentDocKey → [nodes]
|
|
2063
|
+
var childKeySet = {}; // 属于子文档的 nodeId 集合
|
|
2064
|
+
for (var i = 0; i < docNodes.length; i++) {
|
|
2065
|
+
var p = docNodes[i].properties || {};
|
|
2066
|
+
if (p.parentDoc) {
|
|
2067
|
+
if (!childrenMap[p.parentDoc]) childrenMap[p.parentDoc] = [];
|
|
2068
|
+
childrenMap[p.parentDoc].push(docNodes[i]);
|
|
2069
|
+
childKeySet[docNodes[i].id] = true;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// 按 section 分组顶级文档
|
|
2074
|
+
var groups = {};
|
|
2075
|
+
var groupOrder = [];
|
|
2076
|
+
for (var i = 0; i < docNodes.length; i++) {
|
|
2077
|
+
if (childKeySet[docNodes[i].id]) continue;
|
|
2078
|
+
var sec = (docNodes[i].properties || {}).section || 'custom';
|
|
2079
|
+
if (!groups[sec]) { groups[sec] = []; groupOrder.push(sec); }
|
|
2080
|
+
groups[sec].push(docNodes[i]);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
return { groups: groups, groupOrder: groupOrder, childrenMap: childrenMap };
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/** 显示文档弹层(左侧列表) */
|
|
2087
|
+
function showDocModal() {
|
|
2088
|
+
var docNodes = [];
|
|
2089
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
2090
|
+
if (allNodes[i].type === 'document') docNodes.push(allNodes[i]);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
document.getElementById('statsModalTitle').textContent = '📄 文档列表';
|
|
2094
|
+
document.getElementById('statsModalCount').textContent = '(' + docNodes.length + ')';
|
|
2095
|
+
|
|
2096
|
+
var tree = buildDocTree();
|
|
2097
|
+
var html = renderDocTreeHTML(tree);
|
|
2098
|
+
|
|
2099
|
+
if (docNodes.length === 0) {
|
|
2100
|
+
html = '<div style="text-align:center;padding:40px;color:#6b7280;">暂无文档</div>';
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
document.getElementById('statsModalBody').innerHTML = html;
|
|
2104
|
+
// 根据侧边栏状态调整弹层位置
|
|
2105
|
+
updateStatsModalPosition();
|
|
2106
|
+
document.getElementById('statsModalOverlay').classList.add('active');
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/** 渲染文档层级树 HTML */
|
|
2110
|
+
function renderDocTreeHTML(tree) {
|
|
2111
|
+
var SECTION_NAMES_MODAL = {
|
|
2112
|
+
overview: '概述', core_concepts: '核心概念', api_design: 'API 设计',
|
|
2113
|
+
file_structure: '文件结构', config: '配置', examples: '使用示例',
|
|
2114
|
+
technical_notes: '技术笔记', api_endpoints: 'API 端点',
|
|
2115
|
+
milestones: '里程碑', changelog: '变更记录', custom: '自定义'
|
|
2116
|
+
};
|
|
2117
|
+
var SECTION_ICONS_MODAL = {
|
|
2118
|
+
overview: '📋', core_concepts: '🧠', api_design: '🔌',
|
|
2119
|
+
file_structure: '📁', config: '⚙️', examples: '💡',
|
|
2120
|
+
technical_notes: '🔬', api_endpoints: '🌐',
|
|
2121
|
+
milestones: '🏁', changelog: '📝', custom: '📎'
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
var html = '';
|
|
2125
|
+
for (var gi = 0; gi < tree.groupOrder.length; gi++) {
|
|
2126
|
+
var sec = tree.groupOrder[gi];
|
|
2127
|
+
var items = tree.groups[sec];
|
|
2128
|
+
var secName = SECTION_NAMES_MODAL[sec] || sec;
|
|
2129
|
+
var secIcon = SECTION_ICONS_MODAL[sec] || '📄';
|
|
2130
|
+
|
|
2131
|
+
html += '<div style="margin-bottom:4px;">';
|
|
2132
|
+
html += '<div style="padding:8px 20px;font-size:11px;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:0.05em;display:flex;align-items:center;gap:6px;">';
|
|
2133
|
+
html += '<span>' + secIcon + '</span><span>' + secName + '</span>';
|
|
2134
|
+
html += '<span style="margin-left:auto;font-size:10px;color:#4b5563;">' + items.length + '</span>';
|
|
2135
|
+
html += '</div>';
|
|
2136
|
+
|
|
2137
|
+
for (var ii = 0; ii < items.length; ii++) {
|
|
2138
|
+
html += renderDocTreeItem(items[ii], tree.childrenMap, 0);
|
|
2139
|
+
}
|
|
2140
|
+
html += '</div>';
|
|
2141
|
+
}
|
|
2142
|
+
return html;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/** 递归渲染单个文档节点及其子文档 */
|
|
2146
|
+
function renderDocTreeItem(node, childrenMap, depth) {
|
|
2147
|
+
var docKey = getDocNodeKey(node);
|
|
2148
|
+
var p = node.properties || {};
|
|
2149
|
+
var children = childrenMap[docKey] || [];
|
|
2150
|
+
var hasChildren = children.length > 0;
|
|
2151
|
+
var isExpanded = docModalExpandedState[docKey] === true;
|
|
2152
|
+
var paddingLeft = 20 + depth * 20;
|
|
2153
|
+
|
|
2154
|
+
var html = '';
|
|
2155
|
+
|
|
2156
|
+
// 文档项
|
|
2157
|
+
html += '<div class="stats-modal-item" style="padding-left:' + paddingLeft + 'px;gap:6px;" onclick="docModalSelectDoc(\\x27' + escHtml(docKey).replace(/'/g, "\\\\'") + '\\x27,\\x27' + escHtml(node.id).replace(/'/g, "\\\\'") + '\\x27)">';
|
|
2158
|
+
|
|
2159
|
+
// 展开/折叠按钮
|
|
2160
|
+
if (hasChildren) {
|
|
2161
|
+
html += '<span class="doc-modal-toggle" onclick="event.stopPropagation();toggleDocModalExpand(\\x27' + escHtml(docKey).replace(/'/g, "\\\\'") + '\\x27)" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#818cf8;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0;transition:all 0.15s;line-height:1;">' + (isExpanded ? '−' : '+') + '</span>';
|
|
2162
|
+
} else {
|
|
2163
|
+
html += '<span style="width:18px;flex-shrink:0;"></span>';
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
html += '<span class="stats-modal-item-icon" style="font-size:13px;">📄</span>';
|
|
2167
|
+
html += '<span class="stats-modal-item-name" title="' + escHtml(node.label) + '" style="font-size:' + (depth > 0 ? '12' : '13') + 'px;' + (depth > 0 ? 'opacity:0.85;' : '') + '">' + escHtml(node.label) + '</span>';
|
|
2168
|
+
|
|
2169
|
+
if (hasChildren) {
|
|
2170
|
+
html += '<span style="font-size:10px;color:#818cf8;flex-shrink:0;">' + children.length + '</span>';
|
|
2171
|
+
}
|
|
2172
|
+
if (p.subSection) {
|
|
2173
|
+
html += '<span style="font-size:10px;color:#6b7280;flex-shrink:0;font-family:monospace;">' + escHtml(p.subSection) + '</span>';
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
html += '</div>';
|
|
2177
|
+
|
|
2178
|
+
// 子文档列表(仅展开时显示)
|
|
2179
|
+
if (hasChildren && isExpanded) {
|
|
2180
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
2181
|
+
html += renderDocTreeItem(children[ci], childrenMap, depth + 1);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
return html;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/** 展开/折叠文档弹层中的子文档 */
|
|
2189
|
+
function toggleDocModalExpand(docKey) {
|
|
2190
|
+
docModalExpandedState[docKey] = !docModalExpandedState[docKey];
|
|
2191
|
+
// 重新渲染文档列表
|
|
2192
|
+
var tree = buildDocTree();
|
|
2193
|
+
var html = renderDocTreeHTML(tree);
|
|
2194
|
+
document.getElementById('statsModalBody').innerHTML = html;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/** 在文档弹层中选中文档 — 复用右侧图谱详情面板显示内容 */
|
|
2198
|
+
function docModalSelectDoc(docKey, nodeId) {
|
|
2199
|
+
// 直接复用 statsModalGoToNode,聚焦图谱节点并打开已有的右侧详情面板
|
|
2200
|
+
statsModalGoToNode(nodeId);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1580
2203
|
function closeStatsModal() {
|
|
1581
2204
|
document.getElementById('statsModalOverlay').classList.remove('active');
|
|
1582
2205
|
}
|
|
@@ -1584,6 +2207,7 @@ function closeStatsModal() {
|
|
|
1584
2207
|
function statsModalGoToNode(nodeId) {
|
|
1585
2208
|
if (network && nodesDataSet && nodesDataSet.get(nodeId)) {
|
|
1586
2209
|
network.selectNodes([nodeId]);
|
|
2210
|
+
highlightConnectedEdges(nodeId);
|
|
1587
2211
|
network.focus(nodeId, { scale: 1.2, animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
|
|
1588
2212
|
panelHistory = [];
|
|
1589
2213
|
currentPanelNodeId = null;
|
|
@@ -1723,6 +2347,16 @@ var docsLoaded = false;
|
|
|
1723
2347
|
var docsData = []; // 全部文档列表
|
|
1724
2348
|
var currentDocKey = ''; // 当前选中文档的 key (section|subSection)
|
|
1725
2349
|
|
|
2350
|
+
/** 根据 docKey 从 docsData 中查找文档标题 */
|
|
2351
|
+
function findDocTitle(docKey) {
|
|
2352
|
+
for (var i = 0; i < docsData.length; i++) {
|
|
2353
|
+
var d = docsData[i];
|
|
2354
|
+
var key = d.section + (d.subSection ? '|' + d.subSection : '');
|
|
2355
|
+
if (key === docKey) return d.title;
|
|
2356
|
+
}
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
1726
2360
|
/** Section 类型的中文名称映射 */
|
|
1727
2361
|
var SECTION_NAMES = {
|
|
1728
2362
|
overview: '概述', core_concepts: '核心概念', api_design: 'API 设计',
|
|
@@ -1753,16 +2387,38 @@ function loadDocsPage() {
|
|
|
1753
2387
|
});
|
|
1754
2388
|
}
|
|
1755
2389
|
|
|
1756
|
-
/**
|
|
2390
|
+
/** 获取文档的 key(唯一标识) */
|
|
2391
|
+
function docItemKey(item) {
|
|
2392
|
+
return item.section + (item.subSection ? '|' + item.subSection : '');
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/** 记录哪些父文档处于折叠状态(key → true 表示折叠) */
|
|
2396
|
+
var docsCollapsedState = {};
|
|
2397
|
+
|
|
2398
|
+
/** 将文档列表按 section 分组渲染,支持 parentDoc 层级 */
|
|
1757
2399
|
function renderDocsList(docs) {
|
|
1758
2400
|
var list = document.getElementById('docsGroupList');
|
|
1759
2401
|
if (!list) return;
|
|
1760
2402
|
|
|
1761
|
-
//
|
|
2403
|
+
// 建立 parentDoc → children 映射,区分顶级和子文档
|
|
2404
|
+
var childrenMap = {}; // parentDocKey → [child items]
|
|
2405
|
+
var childKeySet = {}; // 属于子文档的 key 集合
|
|
2406
|
+
for (var i = 0; i < docs.length; i++) {
|
|
2407
|
+
var d = docs[i];
|
|
2408
|
+
if (d.parentDoc) {
|
|
2409
|
+
if (!childrenMap[d.parentDoc]) childrenMap[d.parentDoc] = [];
|
|
2410
|
+
childrenMap[d.parentDoc].push(d);
|
|
2411
|
+
childKeySet[docItemKey(d)] = true;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// 按 section 分组(只放顶级文档)
|
|
1762
2416
|
var groups = {};
|
|
1763
2417
|
var groupOrder = [];
|
|
1764
2418
|
for (var i = 0; i < docs.length; i++) {
|
|
1765
2419
|
var d = docs[i];
|
|
2420
|
+
var key = docItemKey(d);
|
|
2421
|
+
if (childKeySet[key]) continue; // 跳过子文档(由父文档渲染)
|
|
1766
2422
|
var sec = d.section;
|
|
1767
2423
|
if (!groups[sec]) {
|
|
1768
2424
|
groups[sec] = [];
|
|
@@ -1783,25 +2439,22 @@ function renderDocsList(docs) {
|
|
|
1783
2439
|
var secName = SECTION_NAMES[sec] || sec;
|
|
1784
2440
|
var secIcon = SECTION_ICONS[sec] || '📄';
|
|
1785
2441
|
|
|
2442
|
+
// 计算此分组下文档总数(含子文档)
|
|
2443
|
+
var totalCount = 0;
|
|
2444
|
+
for (var ci = 0; ci < docs.length; ci++) {
|
|
2445
|
+
if (docs[ci].section === sec) totalCount++;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
1786
2448
|
html += '<div class="docs-group" data-section="' + sec + '">';
|
|
1787
2449
|
html += '<div class="docs-group-title" onclick="toggleDocsGroup(this)">';
|
|
1788
2450
|
html += '<span class="docs-group-arrow">▼</span>';
|
|
1789
2451
|
html += '<span>' + secIcon + ' ' + secName + '</span>';
|
|
1790
|
-
html += '<span class="docs-group-count">' +
|
|
2452
|
+
html += '<span class="docs-group-count">' + totalCount + '</span>';
|
|
1791
2453
|
html += '</div>';
|
|
1792
2454
|
html += '<div class="docs-group-items">';
|
|
1793
2455
|
|
|
1794
2456
|
for (var ii = 0; ii < items.length; ii++) {
|
|
1795
|
-
|
|
1796
|
-
var docKey = item.section + (item.subSection ? '|' + item.subSection : '');
|
|
1797
|
-
var isActive = docKey === currentDocKey ? ' active' : '';
|
|
1798
|
-
html += '<div class="docs-item' + isActive + '" data-key="' + escHtml(docKey) + '" onclick="selectDoc(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
1799
|
-
html += '<span class="docs-item-icon">' + secIcon + '</span>';
|
|
1800
|
-
html += '<span class="docs-item-text" title="' + escHtml(item.title) + '">' + escHtml(item.title) + '</span>';
|
|
1801
|
-
if (item.subSection) {
|
|
1802
|
-
html += '<span class="docs-item-sub">' + escHtml(item.subSection) + '</span>';
|
|
1803
|
-
}
|
|
1804
|
-
html += '</div>';
|
|
2457
|
+
html += renderDocItemWithChildren(items[ii], childrenMap, secIcon);
|
|
1805
2458
|
}
|
|
1806
2459
|
|
|
1807
2460
|
html += '</div></div>';
|
|
@@ -1810,6 +2463,63 @@ function renderDocsList(docs) {
|
|
|
1810
2463
|
list.innerHTML = html;
|
|
1811
2464
|
}
|
|
1812
2465
|
|
|
2466
|
+
/** 递归渲染文档项及其子文档 */
|
|
2467
|
+
function renderDocItemWithChildren(item, childrenMap, secIcon) {
|
|
2468
|
+
var docKey = docItemKey(item);
|
|
2469
|
+
var isActive = docKey === currentDocKey ? ' active' : '';
|
|
2470
|
+
var children = childrenMap[docKey] || [];
|
|
2471
|
+
var hasChildren = children.length > 0;
|
|
2472
|
+
var isCollapsed = docsCollapsedState[docKey] === true;
|
|
2473
|
+
|
|
2474
|
+
var html = '<div class="docs-item-wrapper">';
|
|
2475
|
+
|
|
2476
|
+
// 文档项本身
|
|
2477
|
+
html += '<div class="docs-item' + isActive + '" data-key="' + escHtml(docKey) + '" onclick="selectDoc(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
2478
|
+
|
|
2479
|
+
if (hasChildren) {
|
|
2480
|
+
var toggleIcon = isCollapsed ? '+' : '−';
|
|
2481
|
+
html += '<span class="docs-item-toggle" onclick="event.stopPropagation();toggleDocChildren(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)" title="' + (isCollapsed ? '展开子文档' : '收起子文档') + '">' + toggleIcon + '</span>';
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
html += '<span class="docs-item-icon">' + secIcon + '</span>';
|
|
2485
|
+
html += '<span class="docs-item-text" title="' + escHtml(item.title) + '">' + escHtml(item.title) + '</span>';
|
|
2486
|
+
if (hasChildren) {
|
|
2487
|
+
html += '<span class="docs-item-sub" style="color:#818cf8;">' + children.length + ' 子文档</span>';
|
|
2488
|
+
} else if (item.subSection) {
|
|
2489
|
+
html += '<span class="docs-item-sub">' + escHtml(item.subSection) + '</span>';
|
|
2490
|
+
}
|
|
2491
|
+
html += '</div>';
|
|
2492
|
+
|
|
2493
|
+
// 子文档列表
|
|
2494
|
+
if (hasChildren) {
|
|
2495
|
+
html += '<div class="docs-children' + (isCollapsed ? ' collapsed' : '') + '" data-parent="' + escHtml(docKey) + '">';
|
|
2496
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
2497
|
+
html += renderDocItemWithChildren(children[ci], childrenMap, secIcon);
|
|
2498
|
+
}
|
|
2499
|
+
html += '</div>';
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
html += '</div>';
|
|
2503
|
+
return html;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** 展开/折叠子文档 */
|
|
2507
|
+
function toggleDocChildren(docKey) {
|
|
2508
|
+
docsCollapsedState[docKey] = !docsCollapsedState[docKey];
|
|
2509
|
+
var container = document.querySelector('.docs-children[data-parent="' + docKey + '"]');
|
|
2510
|
+
if (!container) return;
|
|
2511
|
+
container.classList.toggle('collapsed');
|
|
2512
|
+
// 更新切换按钮图标
|
|
2513
|
+
var wrapper = container.previousElementSibling;
|
|
2514
|
+
if (wrapper) {
|
|
2515
|
+
var toggle = wrapper.querySelector('.docs-item-toggle');
|
|
2516
|
+
if (toggle) {
|
|
2517
|
+
toggle.textContent = docsCollapsedState[docKey] ? '+' : '−';
|
|
2518
|
+
toggle.title = docsCollapsedState[docKey] ? '展开子文档' : '收起子文档';
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
1813
2523
|
/** 展开/折叠文档分组 */
|
|
1814
2524
|
function toggleDocsGroup(el) {
|
|
1815
2525
|
var group = el.closest('.docs-group');
|
|
@@ -1905,6 +2615,35 @@ function renderDocContent(doc, section, subSection) {
|
|
|
1905
2615
|
contentHtml = '<div style="text-align:center;padding:40px;color:#6b7280;">文档内容为空</div>';
|
|
1906
2616
|
}
|
|
1907
2617
|
|
|
2618
|
+
// 父文档链接
|
|
2619
|
+
if (doc.parentDoc) {
|
|
2620
|
+
var parentTitle = findDocTitle(doc.parentDoc);
|
|
2621
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
2622
|
+
contentHtml += '<div class="docs-related-title">⬆️ 父文档</div>';
|
|
2623
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + doc.parentDoc.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
2624
|
+
contentHtml += '<span class="rel-icon" style="background:#1e3a5f;color:#93c5fd;">📄</span>';
|
|
2625
|
+
contentHtml += '<span style="flex:1;color:#818cf8;">' + escHtml(parentTitle || doc.parentDoc) + '</span>';
|
|
2626
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(doc.parentDoc) + '</span>';
|
|
2627
|
+
contentHtml += '</div></div>';
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// 子文档列表
|
|
2631
|
+
var childDocs = doc.childDocs || [];
|
|
2632
|
+
if (childDocs.length > 0) {
|
|
2633
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
2634
|
+
contentHtml += '<div class="docs-related-title">⬇️ 子文档 (' + childDocs.length + ')</div>';
|
|
2635
|
+
for (var ci = 0; ci < childDocs.length; ci++) {
|
|
2636
|
+
var childKey = childDocs[ci];
|
|
2637
|
+
var childTitle = findDocTitle(childKey);
|
|
2638
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + childKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
2639
|
+
contentHtml += '<span class="rel-icon" style="background:#1e1b4b;color:#c084fc;">📄</span>';
|
|
2640
|
+
contentHtml += '<span style="flex:1;color:#c084fc;">' + escHtml(childTitle || childKey) + '</span>';
|
|
2641
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(childKey) + '</span>';
|
|
2642
|
+
contentHtml += '</div>';
|
|
2643
|
+
}
|
|
2644
|
+
contentHtml += '</div>';
|
|
2645
|
+
}
|
|
2646
|
+
|
|
1908
2647
|
// 关联任务
|
|
1909
2648
|
var relatedTasks = doc.relatedTasks || [];
|
|
1910
2649
|
if (relatedTasks.length > 0) {
|