docnest 0.1.0
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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +79 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +814 -0
- package/package.json +60 -0
- package/server/favicon.ico +0 -0
- package/server/static/css/highlight.css +72 -0
- package/server/static/css/styles.css +5762 -0
- package/server/static/favicon.ico +0 -0
- package/server/static/img/404-deep-space-light.webp +0 -0
- package/server/static/img/404-deep-space.webp +0 -0
- package/server/static/js/core/doc-client-nav.js +260 -0
- package/server/static/js/core/doc-pdf-export.js +579 -0
- package/server/static/js/core/mermaid-init.js +36 -0
- package/server/static/js/core/sidebar.js +141 -0
- package/server/static/js/core/theme.js +138 -0
- package/server/static/js/diagram/diagram-controls.js +90 -0
- package/server/static/js/diagram/diagram-download.js +594 -0
- package/server/static/js/diagram/diagram-drag.js +157 -0
- package/server/static/js/diagram/diagram-viewer.js +405 -0
- package/server/static/js/diagram/diagram-zoom.js +67 -0
- package/server/static/js/utils/app.js +14 -0
- package/server/static/js/utils/modal-stack.js +33 -0
- package/server/views/dir.ejs +142 -0
- package/server/views/doc.ejs +483 -0
- package/server/views/index.ejs +77 -0
- package/server/views/not-found.ejs +57 -0
- package/server/views/partials/breadcrumbs.ejs +15 -0
- package/server/views/partials/diagram-viewer.ejs +16 -0
- package/server/views/partials/header.ejs +37 -0
- package/server/views/partials/sidebar.ejs +19 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// 图表拖拽功能
|
|
2
|
+
let isDragging = false;
|
|
3
|
+
let dragStartX = 0;
|
|
4
|
+
let dragStartY = 0;
|
|
5
|
+
let dragStartScrollX = 0;
|
|
6
|
+
let dragStartScrollY = 0;
|
|
7
|
+
let dragMouseDownHandler = null;
|
|
8
|
+
let dragMouseMoveHandler = null;
|
|
9
|
+
let dragMouseUpHandler = null;
|
|
10
|
+
let dragAnimationFrameId = null;
|
|
11
|
+
let lastMouseX = 0;
|
|
12
|
+
let lastMouseY = 0;
|
|
13
|
+
|
|
14
|
+
function setupDragPan() {
|
|
15
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
16
|
+
if (!container) return;
|
|
17
|
+
|
|
18
|
+
dragMouseDownHandler = function(e) {
|
|
19
|
+
// 只响应左键按下
|
|
20
|
+
if (e.button !== 0) return;
|
|
21
|
+
|
|
22
|
+
// 如果点击的是控制按钮,不启动拖拽
|
|
23
|
+
if (e.target.closest('.diagram-viewer-controls')) return;
|
|
24
|
+
|
|
25
|
+
isDragging = true;
|
|
26
|
+
dragStartX = e.clientX;
|
|
27
|
+
dragStartY = e.clientY;
|
|
28
|
+
lastMouseX = e.clientX;
|
|
29
|
+
lastMouseY = e.clientY;
|
|
30
|
+
dragStartScrollX = container.scrollLeft;
|
|
31
|
+
dragStartScrollY = container.scrollTop;
|
|
32
|
+
|
|
33
|
+
// 改变鼠标样式
|
|
34
|
+
container.style.cursor = 'grabbing';
|
|
35
|
+
container.style.userSelect = 'none';
|
|
36
|
+
|
|
37
|
+
// 阻止默认行为
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
dragMouseMoveHandler = function(e) {
|
|
42
|
+
if (!isDragging) return;
|
|
43
|
+
|
|
44
|
+
// 更新最新的鼠标位置
|
|
45
|
+
lastMouseX = e.clientX;
|
|
46
|
+
lastMouseY = e.clientY;
|
|
47
|
+
|
|
48
|
+
// 如果已经有待处理的动画帧,取消它
|
|
49
|
+
if (dragAnimationFrameId !== null) {
|
|
50
|
+
cancelAnimationFrame(dragAnimationFrameId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 使用 requestAnimationFrame 来优化性能
|
|
54
|
+
dragAnimationFrameId = requestAnimationFrame(function() {
|
|
55
|
+
if (!isDragging) return;
|
|
56
|
+
|
|
57
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
58
|
+
if (!container) return;
|
|
59
|
+
|
|
60
|
+
// 计算鼠标移动距离
|
|
61
|
+
const deltaX = lastMouseX - dragStartX;
|
|
62
|
+
const deltaY = lastMouseY - dragStartY;
|
|
63
|
+
|
|
64
|
+
// 计算新的滚动位置(反向移动,实现拖拽效果)
|
|
65
|
+
const newScrollX = dragStartScrollX - deltaX;
|
|
66
|
+
const newScrollY = dragStartScrollY - deltaY;
|
|
67
|
+
|
|
68
|
+
// 限制滚动范围
|
|
69
|
+
const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
|
|
70
|
+
if (wrapper) {
|
|
71
|
+
const containerRect = container.getBoundingClientRect();
|
|
72
|
+
// 使用包装元素的实际尺寸来计算最大滚动范围
|
|
73
|
+
// 优先使用 style.width/height(更准确),否则使用 offsetWidth/Height
|
|
74
|
+
const wrapperWidth = parseFloat(wrapper.style.width) || wrapper.offsetWidth || 0;
|
|
75
|
+
const wrapperHeight = parseFloat(wrapper.style.height) || wrapper.offsetHeight || 0;
|
|
76
|
+
|
|
77
|
+
// 计算最大滚动范围:包装元素尺寸 - 容器可视区域尺寸
|
|
78
|
+
// 这样可以确保能滚动到所有边界(包括 padding 区域)
|
|
79
|
+
const maxScrollX = Math.max(0, wrapperWidth - containerRect.width);
|
|
80
|
+
const maxScrollY = Math.max(0, wrapperHeight - containerRect.height);
|
|
81
|
+
|
|
82
|
+
// 确保滚动位置在有效范围内
|
|
83
|
+
container.scrollLeft = Math.max(0, Math.min(newScrollX, maxScrollX));
|
|
84
|
+
container.scrollTop = Math.max(0, Math.min(newScrollY, maxScrollY));
|
|
85
|
+
} else {
|
|
86
|
+
// 如果没有包装元素,使用原来的逻辑
|
|
87
|
+
container.scrollLeft = newScrollX;
|
|
88
|
+
container.scrollTop = newScrollY;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dragAnimationFrameId = null;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 阻止默认行为
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
dragMouseUpHandler = function(e) {
|
|
99
|
+
if (!isDragging) return;
|
|
100
|
+
|
|
101
|
+
// 取消待处理的动画帧
|
|
102
|
+
if (dragAnimationFrameId !== null) {
|
|
103
|
+
cancelAnimationFrame(dragAnimationFrameId);
|
|
104
|
+
dragAnimationFrameId = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isDragging = false;
|
|
108
|
+
|
|
109
|
+
// 恢复鼠标样式
|
|
110
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
111
|
+
if (container) {
|
|
112
|
+
container.style.cursor = '';
|
|
113
|
+
container.style.userSelect = '';
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// 添加事件监听器
|
|
118
|
+
container.addEventListener('mousedown', dragMouseDownHandler);
|
|
119
|
+
document.addEventListener('mousemove', dragMouseMoveHandler);
|
|
120
|
+
document.addEventListener('mouseup', dragMouseUpHandler);
|
|
121
|
+
|
|
122
|
+
// 鼠标离开窗口时也要释放拖拽
|
|
123
|
+
document.addEventListener('mouseleave', dragMouseUpHandler);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function removeDragPan() {
|
|
127
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
128
|
+
|
|
129
|
+
// 取消待处理的动画帧
|
|
130
|
+
if (dragAnimationFrameId !== null) {
|
|
131
|
+
cancelAnimationFrame(dragAnimationFrameId);
|
|
132
|
+
dragAnimationFrameId = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (container && dragMouseDownHandler) {
|
|
136
|
+
container.removeEventListener('mousedown', dragMouseDownHandler);
|
|
137
|
+
dragMouseDownHandler = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (dragMouseMoveHandler) {
|
|
141
|
+
document.removeEventListener('mousemove', dragMouseMoveHandler);
|
|
142
|
+
dragMouseMoveHandler = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (dragMouseUpHandler) {
|
|
146
|
+
document.removeEventListener('mouseup', dragMouseUpHandler);
|
|
147
|
+
document.removeEventListener('mouseleave', dragMouseUpHandler);
|
|
148
|
+
dragMouseUpHandler = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 重置状态
|
|
152
|
+
isDragging = false;
|
|
153
|
+
if (container) {
|
|
154
|
+
container.style.cursor = '';
|
|
155
|
+
container.style.userSelect = '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// 图表查看器核心功能
|
|
2
|
+
let currentZoom = 1;
|
|
3
|
+
let defaultZoom = 1;
|
|
4
|
+
let currentSvg = null;
|
|
5
|
+
let isFullscreen = false;
|
|
6
|
+
|
|
7
|
+
function setupDiagramViewer() {
|
|
8
|
+
const diagrams = document.querySelectorAll('.markdown-body .mermaid');
|
|
9
|
+
diagrams.forEach((diagram, index) => {
|
|
10
|
+
// 添加下载按钮
|
|
11
|
+
const downloadBtn = document.createElement('button');
|
|
12
|
+
downloadBtn.className = 'mermaid-download-btn';
|
|
13
|
+
downloadBtn.innerHTML = '⬇️ 下载';
|
|
14
|
+
downloadBtn.title = '下载图表';
|
|
15
|
+
downloadBtn.onclick = function(e) {
|
|
16
|
+
e.stopPropagation(); // 阻止触发点击查看大图
|
|
17
|
+
const svg = diagram.querySelector('svg');
|
|
18
|
+
if (svg) {
|
|
19
|
+
if (typeof showDownloadMenu === 'function') {
|
|
20
|
+
showDownloadMenu(svg.cloneNode(true), `diagram-${index + 1}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
diagram.appendChild(downloadBtn);
|
|
25
|
+
|
|
26
|
+
// 点击查看大图
|
|
27
|
+
diagram.addEventListener('click', function(e) {
|
|
28
|
+
// 如果点击的是下载按钮,不打开查看器
|
|
29
|
+
if (e.target === downloadBtn || downloadBtn.contains(e.target)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const svg = diagram.querySelector('svg');
|
|
33
|
+
if (svg) {
|
|
34
|
+
openDiagramViewer(svg.cloneNode(true));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function openDiagramViewer(svgElement) {
|
|
41
|
+
const viewer = document.getElementById('diagram-viewer');
|
|
42
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
43
|
+
|
|
44
|
+
if (!viewer || !container) return;
|
|
45
|
+
|
|
46
|
+
// 获取原始 SVG 的尺寸
|
|
47
|
+
let originalWidth = parseFloat(svgElement.getAttribute('width'));
|
|
48
|
+
let originalHeight = parseFloat(svgElement.getAttribute('height'));
|
|
49
|
+
|
|
50
|
+
// 如果没有 width/height,尝试从 viewBox 获取
|
|
51
|
+
if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
|
|
52
|
+
const viewBox = svgElement.getAttribute('viewBox');
|
|
53
|
+
if (viewBox) {
|
|
54
|
+
const parts = viewBox.split(/\s+/);
|
|
55
|
+
if (parts.length >= 4) {
|
|
56
|
+
originalWidth = parseFloat(parts[2]) || 800;
|
|
57
|
+
originalHeight = parseFloat(parts[3]) || 600;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 如果还是没有,使用默认值
|
|
63
|
+
if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
|
|
64
|
+
originalWidth = 800;
|
|
65
|
+
originalHeight = 600;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 设置 SVG 样式,确保充分利用空间
|
|
69
|
+
svgElement.setAttribute('class', 'diagram-viewer-svg');
|
|
70
|
+
// 清除之前的样式,但保留必要的属性
|
|
71
|
+
svgElement.removeAttribute('style');
|
|
72
|
+
// 确保 SVG 有正确的属性
|
|
73
|
+
if (!svgElement.getAttribute('width')) {
|
|
74
|
+
svgElement.setAttribute('width', originalWidth);
|
|
75
|
+
}
|
|
76
|
+
if (!svgElement.getAttribute('height')) {
|
|
77
|
+
svgElement.setAttribute('height', originalHeight);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 包装元素本身还包含边界 padding,因此适配比例必须把容器 padding 和图表边界一并算入。
|
|
81
|
+
const padding = 40;
|
|
82
|
+
const containerWidth = container.clientWidth - 40 - padding * 2;
|
|
83
|
+
const containerHeight = container.clientHeight - 40 - padding * 2;
|
|
84
|
+
|
|
85
|
+
// 确保容器尺寸有效
|
|
86
|
+
const validContainerWidth = containerWidth > 0 ? containerWidth : 800;
|
|
87
|
+
const validContainerHeight = containerHeight > 0 ? containerHeight : 600;
|
|
88
|
+
|
|
89
|
+
const scaleX = validContainerWidth / originalWidth;
|
|
90
|
+
const scaleY = validContainerHeight / originalHeight;
|
|
91
|
+
const initialScale = Math.min(scaleX, scaleY, 1.5);
|
|
92
|
+
|
|
93
|
+
// 横向或纵向长图默认适配到查看器可视区;放大图仍保留最多 150% 的默认比例。
|
|
94
|
+
const safeInitialScale = isFinite(initialScale) && initialScale > 0 ? initialScale : 1;
|
|
95
|
+
currentZoom = Math.min(Math.max(safeInitialScale, 0.25), 1.5);
|
|
96
|
+
defaultZoom = currentZoom;
|
|
97
|
+
|
|
98
|
+
// 创建包装元素,用于正确计算滚动范围
|
|
99
|
+
// 添加边界 padding,确保边缘内容可见
|
|
100
|
+
const wrapper = document.createElement('div');
|
|
101
|
+
wrapper.className = 'diagram-viewer-svg-wrapper';
|
|
102
|
+
wrapper.style.width = (originalWidth * currentZoom + padding * 2) + 'px';
|
|
103
|
+
wrapper.style.height = (originalHeight * currentZoom + padding * 2) + 'px';
|
|
104
|
+
wrapper.style.position = 'relative';
|
|
105
|
+
wrapper.style.flexShrink = '0';
|
|
106
|
+
wrapper.style.display = 'block';
|
|
107
|
+
wrapper.style.margin = '0 auto';
|
|
108
|
+
wrapper.style.padding = padding + 'px';
|
|
109
|
+
wrapper.style.boxSizing = 'border-box';
|
|
110
|
+
|
|
111
|
+
svgElement.style.transform = `scale(${currentZoom})`;
|
|
112
|
+
svgElement.style.transformOrigin = 'top left';
|
|
113
|
+
svgElement.style.width = originalWidth + 'px';
|
|
114
|
+
svgElement.style.height = originalHeight + 'px';
|
|
115
|
+
svgElement.style.position = 'absolute';
|
|
116
|
+
svgElement.style.top = padding + 'px';
|
|
117
|
+
svgElement.style.left = padding + 'px';
|
|
118
|
+
svgElement.style.display = 'block';
|
|
119
|
+
|
|
120
|
+
// 先清空容器
|
|
121
|
+
container.innerHTML = '';
|
|
122
|
+
// 添加包装元素
|
|
123
|
+
container.appendChild(wrapper);
|
|
124
|
+
wrapper.appendChild(svgElement);
|
|
125
|
+
|
|
126
|
+
currentSvg = svgElement;
|
|
127
|
+
|
|
128
|
+
// 确保在 DOM 更新后再更新缩放信息和滚动位置
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
if (typeof updateZoomInfo === 'function') {
|
|
131
|
+
updateZoomInfo();
|
|
132
|
+
}
|
|
133
|
+
// 重置滚动位置到 padding 位置,确保能看到图表顶部和左侧边缘
|
|
134
|
+
container.scrollTop = 0;
|
|
135
|
+
container.scrollLeft = 0;
|
|
136
|
+
|
|
137
|
+
// 超宽图从左侧边界开始,避免 flex 居中产生不可滚动的负溢出;小图由 wrapper 的 auto margin 居中。
|
|
138
|
+
container.scrollLeft = 0;
|
|
139
|
+
container.scrollTop = 0;
|
|
140
|
+
}, 0);
|
|
141
|
+
|
|
142
|
+
viewer.classList.add('show');
|
|
143
|
+
document.body.style.overflow = 'hidden';
|
|
144
|
+
|
|
145
|
+
// 重置全屏状态
|
|
146
|
+
isFullscreen = false;
|
|
147
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
148
|
+
updateFullscreenButton();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 初始化各个功能模块
|
|
152
|
+
if (typeof setupDragPan === 'function') {
|
|
153
|
+
setupDragPan();
|
|
154
|
+
}
|
|
155
|
+
if (typeof setupControlsAutoHide === 'function') {
|
|
156
|
+
setupControlsAutoHide();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 将大图查看器加入弹窗栈
|
|
160
|
+
if (typeof pushModal === 'function') {
|
|
161
|
+
pushModal('diagram-viewer', closeDiagramViewer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function closeDiagramViewer() {
|
|
166
|
+
const viewer = document.getElementById('diagram-viewer');
|
|
167
|
+
if (!viewer) return;
|
|
168
|
+
|
|
169
|
+
// 如果处于全屏模式,先退出全屏
|
|
170
|
+
if (isFullscreen && typeof exitFullscreen === 'function') {
|
|
171
|
+
exitFullscreen();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 移除事件监听器
|
|
175
|
+
if (typeof removeDragPan === 'function') {
|
|
176
|
+
removeDragPan();
|
|
177
|
+
}
|
|
178
|
+
if (typeof removeControlsAutoHide === 'function') {
|
|
179
|
+
removeControlsAutoHide();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
viewer.classList.remove('show');
|
|
183
|
+
document.body.style.overflow = '';
|
|
184
|
+
currentSvg = null;
|
|
185
|
+
currentZoom = 1;
|
|
186
|
+
isFullscreen = false;
|
|
187
|
+
|
|
188
|
+
// 从弹窗栈中移除
|
|
189
|
+
if (typeof removeModal === 'function') {
|
|
190
|
+
removeModal('diagram-viewer');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resetDiagramZoom() {
|
|
195
|
+
if (!currentSvg) return;
|
|
196
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
197
|
+
if (!container) return;
|
|
198
|
+
|
|
199
|
+
const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
|
|
200
|
+
if (!wrapper) return;
|
|
201
|
+
|
|
202
|
+
// 获取原始尺寸
|
|
203
|
+
const originalWidth = parseFloat(currentSvg.style.width) || parseFloat(currentSvg.getAttribute('width')) || 800;
|
|
204
|
+
const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
|
|
205
|
+
|
|
206
|
+
const padding = 40; // 边界 padding 大小(像素)
|
|
207
|
+
currentZoom = defaultZoom;
|
|
208
|
+
currentSvg.style.transform = `scale(${currentZoom})`;
|
|
209
|
+
currentSvg.style.transformOrigin = 'top left';
|
|
210
|
+
|
|
211
|
+
// 更新包装元素尺寸(包含 padding)
|
|
212
|
+
wrapper.style.width = (originalWidth * currentZoom + padding * 2) + 'px';
|
|
213
|
+
wrapper.style.height = (originalHeight * currentZoom + padding * 2) + 'px';
|
|
214
|
+
|
|
215
|
+
if (typeof updateZoomInfo === 'function') {
|
|
216
|
+
updateZoomInfo();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 重置滚动位置到 padding 位置
|
|
220
|
+
container.scrollTop = 0;
|
|
221
|
+
container.scrollLeft = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function toggleFullscreen() {
|
|
225
|
+
if (isFullscreen) {
|
|
226
|
+
if (typeof exitFullscreen === 'function') {
|
|
227
|
+
exitFullscreen();
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
if (typeof enterFullscreen === 'function') {
|
|
231
|
+
enterFullscreen();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function enterFullscreen() {
|
|
237
|
+
const viewer = document.getElementById('diagram-viewer');
|
|
238
|
+
if (!viewer) return;
|
|
239
|
+
|
|
240
|
+
if (viewer.requestFullscreen) {
|
|
241
|
+
viewer.requestFullscreen();
|
|
242
|
+
} else if (viewer.webkitRequestFullscreen) {
|
|
243
|
+
viewer.webkitRequestFullscreen();
|
|
244
|
+
} else if (viewer.mozRequestFullScreen) {
|
|
245
|
+
viewer.mozRequestFullScreen();
|
|
246
|
+
} else if (viewer.msRequestFullscreen) {
|
|
247
|
+
viewer.msRequestFullscreen();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isFullscreen = true;
|
|
251
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
252
|
+
updateFullscreenButton();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 更新控制栏显示状态
|
|
256
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
257
|
+
if (controls) {
|
|
258
|
+
controls.classList.add('controls-hidden');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function exitFullscreen() {
|
|
263
|
+
if (document.exitFullscreen) {
|
|
264
|
+
document.exitFullscreen();
|
|
265
|
+
} else if (document.webkitExitFullscreen) {
|
|
266
|
+
document.webkitExitFullscreen();
|
|
267
|
+
} else if (document.mozCancelFullScreen) {
|
|
268
|
+
document.mozCancelFullScreen();
|
|
269
|
+
} else if (document.msExitFullscreen) {
|
|
270
|
+
document.msExitFullscreen();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isFullscreen = false;
|
|
274
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
275
|
+
updateFullscreenButton();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 显示控制栏
|
|
279
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
280
|
+
if (controls) {
|
|
281
|
+
controls.classList.remove('controls-hidden');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function updateFullscreenButton() {
|
|
286
|
+
const btn = document.getElementById('fullscreen-btn');
|
|
287
|
+
if (btn) {
|
|
288
|
+
btn.textContent = isFullscreen ? '⛶ 退出全屏' : '⛶ 全屏';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 更新复制和下载按钮的显示状态
|
|
292
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
293
|
+
if (controls) {
|
|
294
|
+
const copyBtn = controls.querySelector('.copy-btn');
|
|
295
|
+
const downloadBtn = controls.querySelector('.download-btn');
|
|
296
|
+
if (copyBtn) copyBtn.style.display = isFullscreen ? 'none' : '';
|
|
297
|
+
if (downloadBtn) downloadBtn.style.display = isFullscreen ? 'none' : '';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function updateZoomInfo() {
|
|
302
|
+
const zoomInfo = document.getElementById('zoom-info');
|
|
303
|
+
if (zoomInfo) {
|
|
304
|
+
// 确保 currentZoom 是有效数字
|
|
305
|
+
const zoom = isNaN(currentZoom) || !isFinite(currentZoom) ? 1 : currentZoom;
|
|
306
|
+
zoomInfo.textContent = Math.round(zoom * 100) + '%';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 点击模态框外部关闭
|
|
311
|
+
document.getElementById('diagram-viewer')?.addEventListener('click', function(e) {
|
|
312
|
+
if (e.target === this) {
|
|
313
|
+
closeDiagramViewer();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 监听全屏状态变化
|
|
318
|
+
document.addEventListener('fullscreenchange', function() {
|
|
319
|
+
isFullscreen = !!document.fullscreenElement;
|
|
320
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
321
|
+
updateFullscreenButton();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 更新控制栏显示状态
|
|
325
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
326
|
+
if (controls) {
|
|
327
|
+
if (isFullscreen) {
|
|
328
|
+
controls.classList.add('controls-hidden');
|
|
329
|
+
} else {
|
|
330
|
+
controls.classList.remove('controls-hidden');
|
|
331
|
+
if (typeof removeControlsAutoHide === 'function') {
|
|
332
|
+
removeControlsAutoHide();
|
|
333
|
+
}
|
|
334
|
+
if (typeof setupControlsAutoHide === 'function') {
|
|
335
|
+
setupControlsAutoHide();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
document.addEventListener('webkitfullscreenchange', function() {
|
|
342
|
+
isFullscreen = !!document.webkitFullscreenElement;
|
|
343
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
344
|
+
updateFullscreenButton();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
348
|
+
if (controls) {
|
|
349
|
+
if (isFullscreen) {
|
|
350
|
+
controls.classList.add('controls-hidden');
|
|
351
|
+
} else {
|
|
352
|
+
controls.classList.remove('controls-hidden');
|
|
353
|
+
if (typeof removeControlsAutoHide === 'function') {
|
|
354
|
+
removeControlsAutoHide();
|
|
355
|
+
}
|
|
356
|
+
if (typeof setupControlsAutoHide === 'function') {
|
|
357
|
+
setupControlsAutoHide();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
document.addEventListener('mozfullscreenchange', function() {
|
|
364
|
+
isFullscreen = !!document.mozFullScreenElement;
|
|
365
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
366
|
+
updateFullscreenButton();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
370
|
+
if (controls) {
|
|
371
|
+
if (isFullscreen) {
|
|
372
|
+
controls.classList.add('controls-hidden');
|
|
373
|
+
} else {
|
|
374
|
+
controls.classList.remove('controls-hidden');
|
|
375
|
+
if (typeof removeControlsAutoHide === 'function') {
|
|
376
|
+
removeControlsAutoHide();
|
|
377
|
+
}
|
|
378
|
+
if (typeof setupControlsAutoHide === 'function') {
|
|
379
|
+
setupControlsAutoHide();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
document.addEventListener('msfullscreenchange', function() {
|
|
386
|
+
isFullscreen = !!document.msFullscreenElement;
|
|
387
|
+
if (typeof updateFullscreenButton === 'function') {
|
|
388
|
+
updateFullscreenButton();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const controls = document.querySelector('.diagram-viewer-controls');
|
|
392
|
+
if (controls) {
|
|
393
|
+
if (isFullscreen) {
|
|
394
|
+
controls.classList.add('controls-hidden');
|
|
395
|
+
} else {
|
|
396
|
+
controls.classList.remove('controls-hidden');
|
|
397
|
+
if (typeof removeControlsAutoHide === 'function') {
|
|
398
|
+
removeControlsAutoHide();
|
|
399
|
+
}
|
|
400
|
+
if (typeof setupControlsAutoHide === 'function') {
|
|
401
|
+
setupControlsAutoHide();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 图表缩放功能(按钮缩放)
|
|
2
|
+
function zoomDiagram(direction) {
|
|
3
|
+
if (!currentSvg) return;
|
|
4
|
+
|
|
5
|
+
const container = document.getElementById('diagram-viewer-svg-container');
|
|
6
|
+
if (!container) return;
|
|
7
|
+
|
|
8
|
+
const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
|
|
9
|
+
if (!wrapper) return;
|
|
10
|
+
|
|
11
|
+
// 获取原始尺寸
|
|
12
|
+
const originalWidth = parseFloat(currentSvg.style.width) || parseFloat(currentSvg.getAttribute('width')) || 800;
|
|
13
|
+
const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
|
|
14
|
+
|
|
15
|
+
// 边界 padding 大小(与 diagram-viewer.js 中保持一致)
|
|
16
|
+
const padding = 40;
|
|
17
|
+
|
|
18
|
+
// 记录缩放前的状态
|
|
19
|
+
const containerRect = container.getBoundingClientRect();
|
|
20
|
+
const currentZoomValue = isNaN(currentZoom) || !isFinite(currentZoom) ? 1 : currentZoom;
|
|
21
|
+
|
|
22
|
+
// 计算容器中心点相对于包装元素的位置(考虑滚动和 padding)
|
|
23
|
+
const centerXInWrapper = container.scrollLeft + containerRect.width / 2;
|
|
24
|
+
const centerYInWrapper = container.scrollTop + containerRect.height / 2;
|
|
25
|
+
|
|
26
|
+
// 计算中心点指向的内容在原始尺寸中的位置(减去 padding)
|
|
27
|
+
const contentX = (centerXInWrapper - padding) / currentZoomValue;
|
|
28
|
+
const contentY = (centerYInWrapper - padding) / currentZoomValue;
|
|
29
|
+
|
|
30
|
+
// 计算新的缩放值
|
|
31
|
+
if (direction === 'in') {
|
|
32
|
+
currentZoom = Math.min(currentZoomValue + 0.25, 3);
|
|
33
|
+
} else {
|
|
34
|
+
currentZoom = Math.max(currentZoomValue - 0.25, 0.25);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 更新 SVG 缩放
|
|
38
|
+
currentSvg.style.transform = `scale(${currentZoom})`;
|
|
39
|
+
currentSvg.style.transformOrigin = 'top left';
|
|
40
|
+
|
|
41
|
+
// 更新包装元素尺寸,确保滚动范围正确(包含 padding)
|
|
42
|
+
const newWrapperWidth = originalWidth * currentZoom + padding * 2;
|
|
43
|
+
const newWrapperHeight = originalHeight * currentZoom + padding * 2;
|
|
44
|
+
wrapper.style.width = newWrapperWidth + 'px';
|
|
45
|
+
wrapper.style.height = newWrapperHeight + 'px';
|
|
46
|
+
|
|
47
|
+
if (typeof updateZoomInfo === 'function') {
|
|
48
|
+
updateZoomInfo();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 计算缩放后中心点指向的内容的新位置(加上 padding)
|
|
52
|
+
const newCenterXInWrapper = contentX * currentZoom + padding;
|
|
53
|
+
const newCenterYInWrapper = contentY * currentZoom + padding;
|
|
54
|
+
|
|
55
|
+
// 计算新的滚动位置,使中心点指向的内容位置保持不变
|
|
56
|
+
const newScrollX = newCenterXInWrapper - containerRect.width / 2;
|
|
57
|
+
const newScrollY = newCenterYInWrapper - containerRect.height / 2;
|
|
58
|
+
|
|
59
|
+
// 限制滚动范围,确保不超出边界
|
|
60
|
+
const maxScrollX = Math.max(0, newWrapperWidth - containerRect.width);
|
|
61
|
+
const maxScrollY = Math.max(0, newWrapperHeight - containerRect.height);
|
|
62
|
+
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
container.scrollLeft = Math.max(0, Math.min(newScrollX, maxScrollX));
|
|
65
|
+
container.scrollTop = Math.max(0, Math.min(newScrollY, maxScrollY));
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 主入口文件 - 保留必要的初始化逻辑
|
|
2
|
+
// 注意:此文件在所有模块加载后执行
|
|
3
|
+
|
|
4
|
+
// 全局变量已在 diagram-viewer.js 中定义
|
|
5
|
+
// 这里只保留必要的初始化逻辑
|
|
6
|
+
|
|
7
|
+
// 确保所有模块都已加载后再执行初始化
|
|
8
|
+
if (document.readyState === 'loading') {
|
|
9
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
10
|
+
console.log('文档已加载,所有模块已初始化');
|
|
11
|
+
});
|
|
12
|
+
} else {
|
|
13
|
+
console.log('文档已就绪,所有模块已初始化');
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 弹窗栈管理(用于多级弹窗的 ESC 键退出)
|
|
2
|
+
const modalStack = [];
|
|
3
|
+
|
|
4
|
+
function pushModal(modalId, closeFunction) {
|
|
5
|
+
modalStack.push({
|
|
6
|
+
id: modalId,
|
|
7
|
+
close: closeFunction
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function removeModal(modalId) {
|
|
12
|
+
const index = modalStack.findIndex(modal => modal.id === modalId);
|
|
13
|
+
if (index !== -1) {
|
|
14
|
+
modalStack.splice(index, 1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function closeTopModal() {
|
|
19
|
+
if (modalStack.length > 0) {
|
|
20
|
+
const topModal = modalStack[modalStack.length - 1];
|
|
21
|
+
if (topModal && topModal.close) {
|
|
22
|
+
topModal.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ESC 键关闭(按栈的顺序关闭弹窗)
|
|
28
|
+
document.addEventListener('keydown', function(e) {
|
|
29
|
+
if (e.key === 'Escape') {
|
|
30
|
+
// 按照栈的顺序关闭最顶层的弹窗
|
|
31
|
+
closeTopModal();
|
|
32
|
+
}
|
|
33
|
+
});
|