podflow 20250429__py3-none-any.whl → 20250430__py3-none-any.whl
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.
- podflow/templates/js/index.js +690 -690
- {podflow-20250429.dist-info → podflow-20250430.dist-info}/METADATA +1 -1
- {podflow-20250429.dist-info → podflow-20250430.dist-info}/RECORD +6 -6
- {podflow-20250429.dist-info → podflow-20250430.dist-info}/WHEEL +0 -0
- {podflow-20250429.dist-info → podflow-20250430.dist-info}/entry_points.txt +0 -0
- {podflow-20250429.dist-info → podflow-20250430.dist-info}/top_level.txt +0 -0
podflow/templates/js/index.js
CHANGED
@@ -1,725 +1,725 @@
|
|
1
1
|
(function() {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}
|
65
|
-
|
66
|
-
// --- 菜单控制 ---
|
67
|
-
/**
|
68
|
-
* 切换侧边菜单的显示/隐藏状态
|
69
|
-
*/
|
70
|
-
function toggleMenu() {
|
71
|
-
menu.classList.toggle('hidden');
|
72
|
-
const isHidden = menu.classList.contains('hidden');
|
73
|
-
toggleMenuBtn.style.left = isHidden ? '0px' : 'var(--menu-width)';
|
74
|
-
toggleMenuBtn.textContent = isHidden ? '❯' : '❮';
|
75
|
-
}
|
76
|
-
|
77
|
-
/**
|
78
|
-
* 根据页面 ID 显示对应的页面内容,并管理 SSE 连接
|
79
|
-
* @param {string} pageId - 'pageChannel' 或 'pageMessage'
|
80
|
-
*/
|
81
|
-
function showPage(pageId) {
|
82
|
-
// 隐藏所有页面
|
83
|
-
Object.values(pages).forEach(page => page.style.display = 'none');
|
84
|
-
|
85
|
-
// 显示目标页面
|
86
|
-
if (pages[pageId]) {
|
87
|
-
pages[pageId].style.display = 'block';
|
88
|
-
|
89
|
-
// 手机模式下,切换页面时自动隐藏菜单
|
90
|
-
if (window.innerWidth <= 600 && !menu.classList.contains('hidden')) {
|
91
|
-
toggleMenu();
|
92
|
-
}
|
93
|
-
|
94
|
-
// --- SSE 连接管理 ---
|
95
|
-
if (pageId === 'pageMessage') {
|
96
|
-
startMessageStream(); // 显示消息页面时,启动 SSE 连接
|
97
|
-
} else {
|
98
|
-
stopMessageStream(); // 切换到其他页面时,关闭 SSE 连接
|
99
|
-
}
|
100
|
-
} else {
|
101
|
-
console.warn(`未找到 ID 为 "${pageId}" 的页面`);
|
102
|
-
}
|
103
|
-
}
|
104
|
-
|
105
|
-
// --- 滚动处理 ---
|
106
|
-
/**
|
107
|
-
* 监听滚动事件,判断用户是否手动滚动了消息区域
|
108
|
-
* @param {Event} event - 滚动事件对象
|
109
|
-
*/
|
110
|
-
function onUserScroll(event) {
|
111
|
-
const element = event.target;
|
112
|
-
const containerId = element.id; // 获取滚动容器的ID
|
113
|
-
if (userScrolled.hasOwnProperty(containerId)) { // 确保是我们关心的容器
|
114
|
-
// 判断是否滚动到接近底部 (增加 10px 容差)
|
115
|
-
const isNearBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 10;
|
116
|
-
userScrolled[containerId] = !isNearBottom; // 如果没在底部,则标记为用户已滚动
|
2
|
+
// --- 缓存常用 DOM 节点 ---
|
3
|
+
// 菜单相关
|
4
|
+
const menu = document.getElementById('menu');
|
5
|
+
const toggleMenuBtn = document.getElementById('toggleMenu');
|
6
|
+
// 页面容器
|
7
|
+
const pages = {
|
8
|
+
pageChannel: document.getElementById('pageChannel'),
|
9
|
+
pageMessage: document.getElementById('pageMessage')
|
10
|
+
};
|
11
|
+
// Channel ID 相关表单
|
12
|
+
const inputForm = document.getElementById('inputForm');
|
13
|
+
const inputOutput = document.getElementById('inputOutput');
|
14
|
+
const pasteBtn = document.getElementById('pasteBtn');
|
15
|
+
const copyBtn = document.getElementById('copyBtn');
|
16
|
+
const clearBtn = document.getElementById('clearBtn');
|
17
|
+
// 主进度条相关
|
18
|
+
const mainProgress = document.getElementById('mainProgress');
|
19
|
+
const progressStatus = document.getElementById('progressStatus');
|
20
|
+
const progressPercentage = document.getElementById('progressPercentage');
|
21
|
+
// 消息显示区域
|
22
|
+
const messageArea = document.getElementById('messageArea'); // Podflow 消息
|
23
|
+
const messageHttp = document.getElementById('messageHttp'); // HTTP 消息
|
24
|
+
const messageDownload = document.getElementById('messageDownload'); // 下载进度条容器
|
25
|
+
const downloadLabel = document.getElementById('downloadLabel'); // 下载进度标题 (假设存在)
|
26
|
+
|
27
|
+
// --- 状态变量 ---
|
28
|
+
let lastMessage = { schedule: [], podflow: [], http: [], download: [] }; // 缓存上一次的消息数据
|
29
|
+
let userScrolled = { // 分别跟踪每个滚动区域的用户滚动状态
|
30
|
+
messageArea: false,
|
31
|
+
messageHttp: false,
|
32
|
+
messageDownload: false
|
33
|
+
};
|
34
|
+
let eventSource = null; // 用于存储 EventSource 实例
|
35
|
+
|
36
|
+
// --- 二维码生成 ---
|
37
|
+
/**
|
38
|
+
* 为指定的 DOM 容器生成二维码
|
39
|
+
* @param {HTMLElement} container - 要生成二维码的容器元素,需要有 data-url 属性
|
40
|
+
*/
|
41
|
+
function generateQRCodeForNode(container) {
|
42
|
+
const rootStyles = getComputedStyle(document.documentElement);
|
43
|
+
const textColor = rootStyles.getPropertyValue('--text-color').trim();
|
44
|
+
const inputBg = rootStyles.getPropertyValue('--input-bg').trim();
|
45
|
+
const url = container.dataset.url;
|
46
|
+
container.innerHTML = ''; // 清空容器
|
47
|
+
if (url) {
|
48
|
+
try {
|
49
|
+
new QRCode(container, {
|
50
|
+
text: url,
|
51
|
+
width: 220,
|
52
|
+
height: 220,
|
53
|
+
colorDark: textColor,
|
54
|
+
colorLight: inputBg,
|
55
|
+
correctLevel: QRCode.CorrectLevel.L
|
56
|
+
});
|
57
|
+
} catch (e) {
|
58
|
+
console.error("生成二维码失败:", e);
|
59
|
+
container.textContent = '二维码生成失败';
|
60
|
+
}
|
61
|
+
} else {
|
62
|
+
container.textContent = 'URL 未提供';
|
63
|
+
}
|
117
64
|
}
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
const percentage = progress * 100;
|
129
|
-
|
130
|
-
// 确保 DOM 元素存在
|
131
|
-
if (!mainProgress || !progressStatus || !progressPercentage) {
|
132
|
-
console.warn("进度条相关 DOM 元素未找到");
|
133
|
-
return;
|
134
|
-
}
|
135
|
-
|
136
|
-
if (status === "准备中" || status === "构建中") {
|
137
|
-
mainProgress.style.width = `${percentage}%`;
|
138
|
-
progressStatus.textContent = status;
|
139
|
-
progressPercentage.textContent = `${percentage.toFixed(2)}%`;
|
140
|
-
} else if (status === "已完成") {
|
141
|
-
mainProgress.style.width = '100%';
|
142
|
-
progressStatus.textContent = '已完成';
|
143
|
-
progressPercentage.textContent = '100.00%'; // 保持格式一致
|
144
|
-
}
|
145
|
-
// 可以选择性地处理其他状态或进度为 null/undefined 的情况
|
146
|
-
} else {
|
147
|
-
// console.warn("接收到的进度数据格式不正确:", scheduleData);
|
65
|
+
|
66
|
+
// --- 菜单控制 ---
|
67
|
+
/**
|
68
|
+
* 切换侧边菜单的显示/隐藏状态
|
69
|
+
*/
|
70
|
+
function toggleMenu() {
|
71
|
+
menu.classList.toggle('hidden');
|
72
|
+
const isHidden = menu.classList.contains('hidden');
|
73
|
+
toggleMenuBtn.style.left = isHidden ? '0px' : 'var(--menu-width)';
|
74
|
+
toggleMenuBtn.textContent = isHidden ? '❯' : '❮';
|
148
75
|
}
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
container.textContent = '未提供二维码 URL';
|
176
|
-
}
|
177
|
-
});
|
178
|
-
}
|
179
|
-
|
180
|
-
/**
|
181
|
-
* 更新消息显示区域 (Podflow / HTTP),保留向上更新四行的逻辑
|
182
|
-
* @param {HTMLElement} container - 消息容器元素 (messageArea 或 messageHttp)
|
183
|
-
* @param {string[]} newMessages - 最新的消息数组
|
184
|
-
* @param {string[]} oldMessages - 上一次的消息数组
|
185
|
-
*/
|
186
|
-
function appendMessages(container, newMessages, oldMessages) {
|
187
|
-
if (!container) return; // 防御性编程
|
188
|
-
|
189
|
-
const containerId = container.id;
|
190
|
-
const wasAtBottom = !userScrolled[containerId] && (container.scrollHeight - container.scrollTop <= container.clientHeight + 10);
|
191
|
-
const newLength = newMessages.length;
|
192
|
-
const oldLength = oldMessages.length;
|
193
|
-
|
194
|
-
// --- 主要逻辑:比较新旧消息,更新 DOM ---
|
195
|
-
if (newLength === oldLength && newLength > 0) {
|
196
|
-
// --- 长度相同:检查最后几条消息是否有变化 ---
|
197
|
-
let replaceCount = 1; // 默认只检查最后一条
|
198
|
-
const lastMessageContent = newMessages[newLength - 1];
|
199
|
-
// 特殊逻辑:如果最后一条消息包含特定文本,则检查最后四条
|
200
|
-
if (lastMessageContent.includes("未扫描") || lastMessageContent.includes("二维码超时, 请重试")) {
|
201
|
-
replaceCount = Math.min(4, newLength); // 最多检查4条或数组长度
|
202
|
-
}
|
203
|
-
|
204
|
-
// 从后往前比较并替换变化的元素
|
205
|
-
for (let i = 0; i < replaceCount; i++) {
|
206
|
-
const index = newLength - 1 - i;
|
207
|
-
const newMessage = newMessages[index];
|
208
|
-
const oldMessage = oldMessages[index];
|
209
|
-
|
210
|
-
if (newMessage !== oldMessage) {
|
211
|
-
const div = createMessageElement(newMessage);
|
212
|
-
processQRCodeContainers(div); // 处理二维码
|
213
|
-
const childToReplace = container.children[index];
|
214
|
-
if (childToReplace) {
|
215
|
-
container.replaceChild(div, childToReplace);
|
216
|
-
} else {
|
217
|
-
// 如果预期的子元素不存在(理论上不应发生),则追加
|
218
|
-
console.warn(`试图替换索引 ${index} 的子元素,但未找到。将追加。`);
|
219
|
-
container.appendChild(div);
|
220
|
-
}
|
221
|
-
}
|
222
|
-
}
|
223
|
-
} else if (newLength > oldLength) {
|
224
|
-
// --- 新消息比旧消息多 ---
|
225
|
-
// 1. 如果旧消息存在,替换旧消息数组中最后一条对应的 DOM 元素
|
226
|
-
if (oldLength > 0) {
|
227
|
-
const replaceIndex = oldLength - 1;
|
228
|
-
const newMessageToReplace = newMessages[replaceIndex];
|
229
|
-
const oldMessageToCompare = oldMessages[replaceIndex];
|
230
|
-
|
231
|
-
// 仅当内容实际改变时才替换
|
232
|
-
if (newMessageToReplace !== oldMessageToCompare) {
|
233
|
-
const div = createMessageElement(newMessageToReplace);
|
234
|
-
processQRCodeContainers(div); // 处理二维码
|
235
|
-
const childToReplace = container.children[replaceIndex]; // 替换对应索引的元素
|
236
|
-
if (childToReplace) {
|
237
|
-
container.replaceChild(div, childToReplace);
|
238
|
-
} else {
|
239
|
-
console.warn(`试图替换索引 ${replaceIndex} 的子元素,但未找到。`);
|
240
|
-
// 此处可以选择追加或其他错误处理
|
241
|
-
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* 根据页面 ID 显示对应的页面内容,并管理 SSE 连接
|
79
|
+
* @param {string} pageId - 'pageChannel' 或 'pageMessage'
|
80
|
+
*/
|
81
|
+
function showPage(pageId) {
|
82
|
+
// 隐藏所有页面
|
83
|
+
Object.values(pages).forEach(page => page.style.display = 'none');
|
84
|
+
|
85
|
+
// 显示目标页面
|
86
|
+
if (pages[pageId]) {
|
87
|
+
pages[pageId].style.display = 'block';
|
88
|
+
|
89
|
+
// 手机模式下,切换页面时自动隐藏菜单
|
90
|
+
if (window.innerWidth <= 600 && !menu.classList.contains('hidden')) {
|
91
|
+
toggleMenu();
|
92
|
+
}
|
93
|
+
|
94
|
+
// --- SSE 连接管理 ---
|
95
|
+
if (pageId === 'pageMessage') {
|
96
|
+
startMessageStream(); // 显示消息页面时,启动 SSE 连接
|
97
|
+
} else {
|
98
|
+
stopMessageStream(); // 切换到其他页面时,关闭 SSE 连接
|
99
|
+
}
|
100
|
+
} else {
|
101
|
+
console.warn(`未找到 ID 为 "${pageId}" 的页面`);
|
242
102
|
}
|
243
|
-
}
|
244
|
-
|
245
|
-
// 2. *** 优化点:使用 DocumentFragment 批量追加新增的消息 ***
|
246
|
-
const fragment = document.createDocumentFragment();
|
247
|
-
for (let i = oldLength; i < newLength; i++) {
|
248
|
-
const div = createMessageElement(newMessages[i]);
|
249
|
-
processQRCodeContainers(div); // 处理二维码
|
250
|
-
fragment.appendChild(div);
|
251
|
-
}
|
252
|
-
container.appendChild(fragment); // 一次性追加所有新消息
|
253
|
-
|
254
|
-
} else if (newLength < oldLength) {
|
255
|
-
// --- 新消息比旧消息少(通常意味着列表被清空或重置)---
|
256
|
-
// 简单处理:清空容器,重新添加所有新消息
|
257
|
-
console.log("新消息数量少于旧消息,将重新渲染整个列表。");
|
258
|
-
container.innerHTML = ''; // 清空
|
259
|
-
const fragment = document.createDocumentFragment();
|
260
|
-
newMessages.forEach(msg => {
|
261
|
-
const div = createMessageElement(msg);
|
262
|
-
processQRCodeContainers(div);
|
263
|
-
fragment.appendChild(div);
|
264
|
-
});
|
265
|
-
container.appendChild(fragment);
|
266
|
-
// 重置滚动状态,因为内容完全变了
|
267
|
-
userScrolled[containerId] = false;
|
268
103
|
}
|
269
104
|
|
270
|
-
// ---
|
271
|
-
|
272
|
-
|
105
|
+
// --- 滚动处理 ---
|
106
|
+
/**
|
107
|
+
* 监听滚动事件,判断用户是否手动滚动了消息区域
|
108
|
+
* @param {Event} event - 滚动事件对象
|
109
|
+
*/
|
110
|
+
function onUserScroll(event) {
|
111
|
+
const element = event.target;
|
112
|
+
const containerId = element.id; // 获取滚动容器的ID
|
113
|
+
if (userScrolled.hasOwnProperty(containerId)) { // 确保是我们关心的容器
|
114
|
+
// 判断是否滚动到接近底部 (增加 10px 容差)
|
115
|
+
const isNearBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 10;
|
116
|
+
userScrolled[containerId] = !isNearBottom; // 如果没在底部,则标记为用户已滚动
|
117
|
+
}
|
273
118
|
}
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
const filesuffix = document.createElement('div');
|
302
|
-
filesuffix.className = 'scroll-suffix';
|
303
|
-
filesuffix.innerHTML = file;
|
304
|
-
// 创建滚动文本区域
|
305
|
-
const scroll = document.createElement('div');
|
306
|
-
scroll.className = 'scroll-container';
|
307
|
-
const namebar = document.createElement('div');
|
308
|
-
namebar.className = 'scroll-content';
|
309
|
-
const filename = document.createElement('div');
|
310
|
-
filename.className = 'scroll-text';
|
311
|
-
filename.innerHTML = nameText;
|
312
|
-
// 组合元素
|
313
|
-
namebar.appendChild(filename);
|
314
|
-
scroll.appendChild(namebar);
|
315
|
-
fileInfo.appendChild(scroll);
|
316
|
-
fileInfo.appendChild(filesuffix);
|
317
|
-
download.appendChild(idnameText);
|
318
|
-
download.appendChild(fileInfo);
|
319
|
-
// 延迟测量文本宽度,决定是否滚动 (!!! 原始逻辑 !!!)
|
320
|
-
setTimeout(() => {
|
321
|
-
const contentWidth = filename.scrollWidth; // 单份文本宽度
|
322
|
-
const containerWidth = scroll.clientWidth;
|
323
|
-
if (contentWidth > containerWidth) {
|
324
|
-
// 需要滚动,添加第二份文本实现无缝滚动
|
325
|
-
const filename1 = document.createElement('div');
|
326
|
-
filename1.className = 'scroll-text';
|
327
|
-
filename1.innerHTML = nameText;
|
328
|
-
namebar.appendChild(filename1);
|
329
|
-
// 重新计算宽度,这次是双倍宽度中的单份宽度用于计算时间
|
330
|
-
const singleContentWidth = namebar.scrollWidth / 2;
|
331
|
-
const scrollSpeed = 30; // 滚动速度 (像素/秒)
|
332
|
-
const duration = singleContentWidth / scrollSpeed;
|
333
|
-
namebar.style.animationDuration = duration + 's';
|
334
|
-
// 延迟添加滚动类
|
335
|
-
setTimeout(() => {
|
336
|
-
namebar.classList.add('scrolling');
|
337
|
-
}, 1500); // 1.5秒延迟
|
119
|
+
|
120
|
+
// --- 消息处理 ---
|
121
|
+
/**
|
122
|
+
* 更新主进度条的状态和百分比
|
123
|
+
* @param {Array} scheduleData - 包含状态和进度的数组,例如 ["状态", 0.5]
|
124
|
+
*/
|
125
|
+
function updateProgress(scheduleData) {
|
126
|
+
if (Array.isArray(scheduleData) && scheduleData.length === 2) {
|
127
|
+
const [status, progress] = scheduleData;
|
128
|
+
const percentage = progress * 100;
|
129
|
+
|
130
|
+
// 确保 DOM 元素存在
|
131
|
+
if (!mainProgress || !progressStatus || !progressPercentage) {
|
132
|
+
console.warn("进度条相关 DOM 元素未找到");
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
|
136
|
+
if (status === "准备中" || status === "构建中") {
|
137
|
+
mainProgress.style.width = `${percentage}%`;
|
138
|
+
progressStatus.textContent = status;
|
139
|
+
progressPercentage.textContent = `${percentage.toFixed(2)}%`;
|
140
|
+
} else if (status === "已完成") {
|
141
|
+
mainProgress.style.width = '100%';
|
142
|
+
progressStatus.textContent = '已完成';
|
143
|
+
progressPercentage.textContent = '100.00%'; // 保持格式一致
|
144
|
+
}
|
145
|
+
// 可以选择性地处理其他状态或进度为 null/undefined 的情况
|
338
146
|
} else {
|
339
|
-
|
340
|
-
namebar.classList.remove('scrolling');
|
341
|
-
namebar.style.animationDuration = '';
|
147
|
+
// console.warn("接收到的进度数据格式不正确:", scheduleData);
|
342
148
|
}
|
343
|
-
}
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* 创建单个消息元素的 DOM 结构
|
153
|
+
* @param {string} message - 包含 HTML 的消息字符串
|
154
|
+
* @returns {HTMLDivElement} - 创建的消息元素
|
155
|
+
*/
|
156
|
+
function createMessageElement(message) {
|
157
|
+
const div = document.createElement('div');
|
158
|
+
// 使用 innerHTML,因为消息内容可能包含 HTML 标签 (如二维码容器)
|
159
|
+
div.innerHTML = message;
|
160
|
+
div.className = 'message'; // 假设 'message' 是单个消息的样式类
|
161
|
+
return div;
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* 处理消息元素内部的二维码容器
|
166
|
+
* @param {HTMLElement} element - 包含 .qrcode-container 的父元素
|
167
|
+
*/
|
168
|
+
function processQRCodeContainers(element) {
|
169
|
+
const qrContainers = element.querySelectorAll('.qrcode-container');
|
170
|
+
qrContainers.forEach(container => {
|
171
|
+
if (container.dataset.url) {
|
172
|
+
generateQRCodeForNode(container);
|
173
|
+
} else {
|
174
|
+
console.log('容器中未提供 URL,跳过二维码生成:', container);
|
175
|
+
container.textContent = '未提供二维码 URL';
|
176
|
+
}
|
177
|
+
});
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* 更新消息显示区域 (Podflow / HTTP),保留向上更新四行的逻辑
|
182
|
+
* @param {HTMLElement} container - 消息容器元素 (messageArea 或 messageHttp)
|
183
|
+
* @param {string[]} newMessages - 最新的消息数组
|
184
|
+
* @param {string[]} oldMessages - 上一次的消息数组
|
185
|
+
*/
|
186
|
+
function appendMessages(container, newMessages, oldMessages) {
|
187
|
+
if (!container) return; // 防御性编程
|
188
|
+
|
189
|
+
const containerId = container.id;
|
190
|
+
const wasAtBottom = !userScrolled[containerId] && (container.scrollHeight - container.scrollTop <= container.clientHeight + 10);
|
191
|
+
const newLength = newMessages.length;
|
192
|
+
const oldLength = oldMessages.length;
|
193
|
+
|
194
|
+
// --- 主要逻辑:比较新旧消息,更新 DOM ---
|
195
|
+
if (newLength === oldLength && newLength > 0) {
|
196
|
+
// --- 长度相同:检查最后几条消息是否有变化 ---
|
197
|
+
let replaceCount = 1; // 默认只检查最后一条
|
198
|
+
const lastMessageContent = newMessages[newLength - 1];
|
199
|
+
// 特殊逻辑:如果最后一条消息包含特定文本,则检查最后四条
|
200
|
+
if (lastMessageContent.includes("未扫描") || lastMessageContent.includes("二维码超时, 请重试")) {
|
201
|
+
replaceCount = Math.min(4, newLength); // 最多检查4条或数组长度
|
202
|
+
}
|
203
|
+
|
204
|
+
// 从后往前比较并替换变化的元素
|
205
|
+
for (let i = 0; i < replaceCount; i++) {
|
206
|
+
const index = newLength - 1 - i;
|
207
|
+
const newMessage = newMessages[index];
|
208
|
+
const oldMessage = oldMessages[index];
|
209
|
+
|
210
|
+
if (newMessage !== oldMessage) {
|
211
|
+
const div = createMessageElement(newMessage);
|
212
|
+
processQRCodeContainers(div); // 处理二维码
|
213
|
+
const childToReplace = container.children[index];
|
214
|
+
if (childToReplace) {
|
215
|
+
container.replaceChild(div, childToReplace);
|
216
|
+
} else {
|
217
|
+
// 如果预期的子元素不存在(理论上不应发生),则追加
|
218
|
+
console.warn(`试图替换索引 ${index} 的子元素,但未找到。将追加。`);
|
219
|
+
container.appendChild(div);
|
220
|
+
}
|
221
|
+
}
|
222
|
+
}
|
223
|
+
} else if (newLength > oldLength) {
|
224
|
+
// --- 新消息比旧消息多 ---
|
225
|
+
// 1. 如果旧消息存在,替换旧消息数组中最后一条对应的 DOM 元素
|
226
|
+
if (oldLength > 0) {
|
227
|
+
const replaceIndex = oldLength - 1;
|
228
|
+
const newMessageToReplace = newMessages[replaceIndex];
|
229
|
+
const oldMessageToCompare = oldMessages[replaceIndex];
|
230
|
+
|
231
|
+
// 仅当内容实际改变时才替换
|
232
|
+
if (newMessageToReplace !== oldMessageToCompare) {
|
233
|
+
const div = createMessageElement(newMessageToReplace);
|
234
|
+
processQRCodeContainers(div); // 处理二维码
|
235
|
+
const childToReplace = container.children[replaceIndex]; // 替换对应索引的元素
|
236
|
+
if (childToReplace) {
|
237
|
+
container.replaceChild(div, childToReplace);
|
238
|
+
} else {
|
239
|
+
console.warn(`试图替换索引 ${replaceIndex} 的子元素,但未找到。`);
|
240
|
+
// 此处可以选择追加或其他错误处理
|
241
|
+
}
|
242
|
+
}
|
430
243
|
}
|
431
|
-
|
432
|
-
|
433
|
-
|
244
|
+
|
245
|
+
// 2. *** 优化点:使用 DocumentFragment 批量追加新增的消息 ***
|
246
|
+
const fragment = document.createDocumentFragment();
|
247
|
+
for (let i = oldLength; i < newLength; i++) {
|
248
|
+
const div = createMessageElement(newMessages[i]);
|
249
|
+
processQRCodeContainers(div); // 处理二维码
|
250
|
+
fragment.appendChild(div);
|
251
|
+
}
|
252
|
+
container.appendChild(fragment); // 一次性追加所有新消息
|
253
|
+
|
254
|
+
} else if (newLength < oldLength) {
|
255
|
+
// --- 新消息比旧消息少(通常意味着列表被清空或重置)---
|
256
|
+
// 简单处理:清空容器,重新添加所有新消息
|
257
|
+
console.log("新消息数量少于旧消息,将重新渲染整个列表。");
|
258
|
+
container.innerHTML = ''; // 清空
|
259
|
+
const fragment = document.createDocumentFragment();
|
260
|
+
newMessages.forEach(msg => {
|
261
|
+
const div = createMessageElement(msg);
|
262
|
+
processQRCodeContainers(div);
|
263
|
+
fragment.appendChild(div);
|
264
|
+
});
|
265
|
+
container.appendChild(fragment);
|
266
|
+
// 重置滚动状态,因为内容完全变了
|
267
|
+
userScrolled[containerId] = false;
|
434
268
|
}
|
435
269
|
|
436
|
-
// ---
|
437
|
-
if (
|
438
|
-
|
439
|
-
const fragment = document.createDocumentFragment();
|
440
|
-
for (let i = oldlength; i < newlength; i++) {
|
441
|
-
const messageContent = newMessages[i];
|
442
|
-
// 解构赋值,确保顺序正确
|
443
|
-
const [percentageText, time, speed, part, status, idname, nameText, file] = messageContent;
|
444
|
-
// 调用未修改的 addProgressBar 来创建元素
|
445
|
-
const downloadElement = addProgressBar(i, percentageText, time, speed, part, status, idname, nameText, file);
|
446
|
-
fragment.appendChild(downloadElement);
|
447
|
-
}
|
448
|
-
// 使用 insertBefore 将新的进度条批量插入到容器顶部
|
449
|
-
container.insertBefore(fragment, container.firstChild);
|
270
|
+
// --- 自动滚动到底部 ---
|
271
|
+
if (wasAtBottom && !userScrolled[containerId]) {
|
272
|
+
container.scrollTop = container.scrollHeight;
|
450
273
|
}
|
451
|
-
} else {
|
452
|
-
// 如果新消息为空,可以选择清空进度条区域或显示提示
|
453
|
-
if (downloadLabel) downloadLabel.textContent = '暂无下载任务';
|
454
|
-
// container.innerHTML = ''; // 清空旧的进度条
|
455
274
|
}
|
456
|
-
// --- 原始逻辑结束 ---
|
457
275
|
|
458
|
-
|
459
|
-
//
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
276
|
+
|
277
|
+
// --- 下载进度条处理 ---
|
278
|
+
/**
|
279
|
+
* 创建单个下载进度条的 DOM 结构 (!!! 内部逻辑未修改 !!!)
|
280
|
+
* @param {number} i - 索引
|
281
|
+
* @param {number} percentageText - 进度百分比 (0-1)
|
282
|
+
* @param {string} time - 剩余时间
|
283
|
+
* @param {string} speed - 下载速度
|
284
|
+
* @param {string} part - 分片信息
|
285
|
+
* @param {string} status - 状态文本
|
286
|
+
* @param {string} idname - 标识名称
|
287
|
+
* @param {string} nameText - 文件名 (可能需要滚动)
|
288
|
+
* @param {string} file - 文件后缀或类型
|
289
|
+
* @returns {HTMLDivElement} - 创建的进度条容器元素
|
290
|
+
*/
|
291
|
+
function addProgressBar(i, percentageText, time, speed, part, status, idname, nameText, file){
|
292
|
+
const download = document.createElement('div');
|
293
|
+
download.className = 'download-container';
|
294
|
+
// 创建 idname 文本节点(只创建一次)
|
295
|
+
const idnameText = document.createElement('div');
|
296
|
+
idnameText.className = 'scroll-text'; // 可能也需要滚动?根据 CSS 定义
|
297
|
+
idnameText.innerHTML = ' ' + idname; // 前导空格?
|
298
|
+
// 创建文件信息部分
|
299
|
+
const fileInfo = document.createElement('div');
|
300
|
+
fileInfo.className = 'scroll'; // 类名可能与滚动有关
|
301
|
+
const filesuffix = document.createElement('div');
|
302
|
+
filesuffix.className = 'scroll-suffix';
|
303
|
+
filesuffix.innerHTML = file;
|
304
|
+
// 创建滚动文本区域
|
305
|
+
const scroll = document.createElement('div');
|
306
|
+
scroll.className = 'scroll-container';
|
307
|
+
const namebar = document.createElement('div');
|
308
|
+
namebar.className = 'scroll-content';
|
309
|
+
const filename = document.createElement('div');
|
310
|
+
filename.className = 'scroll-text';
|
311
|
+
filename.innerHTML = nameText;
|
312
|
+
// 组合元素
|
313
|
+
namebar.appendChild(filename);
|
314
|
+
scroll.appendChild(namebar);
|
315
|
+
fileInfo.appendChild(scroll);
|
316
|
+
fileInfo.appendChild(filesuffix);
|
317
|
+
download.appendChild(idnameText);
|
318
|
+
download.appendChild(fileInfo);
|
319
|
+
// 延迟测量文本宽度,决定是否滚动 (!!! 原始逻辑 !!!)
|
320
|
+
setTimeout(() => {
|
321
|
+
const contentWidth = filename.scrollWidth; // 单份文本宽度
|
322
|
+
const containerWidth = scroll.clientWidth;
|
323
|
+
if (contentWidth > containerWidth) {
|
324
|
+
// 需要滚动,添加第二份文本实现无缝滚动
|
325
|
+
const filename1 = document.createElement('div');
|
326
|
+
filename1.className = 'scroll-text';
|
327
|
+
filename1.innerHTML = nameText;
|
328
|
+
namebar.appendChild(filename1);
|
329
|
+
// 重新计算宽度,这次是双倍宽度中的单份宽度用于计算时间
|
330
|
+
const singleContentWidth = namebar.scrollWidth / 2;
|
331
|
+
const scrollSpeed = 30; // 滚动速度 (像素/秒)
|
332
|
+
const duration = singleContentWidth / scrollSpeed;
|
333
|
+
namebar.style.animationDuration = duration + 's';
|
334
|
+
// 延迟添加滚动类
|
335
|
+
setTimeout(() => {
|
336
|
+
namebar.classList.add('scrolling');
|
337
|
+
}, 1500); // 1.5秒延迟
|
338
|
+
} else {
|
339
|
+
// 不需要滚动,确保移除动画类和样式
|
340
|
+
namebar.classList.remove('scrolling');
|
341
|
+
namebar.style.animationDuration = '';
|
342
|
+
}
|
343
|
+
}, 0); // 使用 setTimeout 0ms 延迟,等待浏览器渲染后测量
|
344
|
+
// 进度条部分
|
345
|
+
const pbBar = document.createElement('div');
|
346
|
+
pbBar.className = 'pb-bar';
|
347
|
+
const pbProgress = document.createElement('div');
|
348
|
+
pbProgress.className = 'pb-progress pb-animated'; // 假设有动画效果
|
349
|
+
pbProgress.style.width = `${percentageText * 100}%`;
|
350
|
+
pbProgress.id = 'pbProgress' + (i+1); // ID 基于索引
|
351
|
+
const pbStatusText = document.createElement('div');
|
352
|
+
pbStatusText.className = 'pb-status-text';
|
353
|
+
pbStatusText.innerHTML = status;
|
354
|
+
pbStatusText.id = 'pbStatusText' + (i+1);
|
355
|
+
const pbPercentageText = document.createElement('div');
|
356
|
+
pbPercentageText.className = 'pb-percentage-text';
|
357
|
+
pbPercentageText.innerHTML = `${(percentageText * 100).toFixed(2)}%`;
|
358
|
+
pbPercentageText.id = 'pbPercentageText' + (i+1);
|
359
|
+
pbBar.appendChild(pbProgress);
|
360
|
+
pbBar.appendChild(pbStatusText);
|
361
|
+
pbBar.appendChild(pbPercentageText);
|
362
|
+
download.appendChild(pbBar);
|
363
|
+
// 速度、分片、时间部分
|
364
|
+
const speedContainer = document.createElement('div'); // 使用 div 而非 table 可能更灵活
|
365
|
+
speedContainer.className = 'scroll';
|
366
|
+
const speedText = document.createElement('div');
|
367
|
+
speedText.className = 'speed-text';
|
368
|
+
speedText.innerHTML = speed;
|
369
|
+
speedText.id = 'speedText' + (i+1);
|
370
|
+
const partText = document.createElement('div');
|
371
|
+
partText.className = 'speed-text';
|
372
|
+
partText.innerHTML = part;
|
373
|
+
partText.id = 'partText' + (i+1);
|
374
|
+
const timeText = document.createElement('div');
|
375
|
+
timeText.className = 'time-text';
|
376
|
+
timeText.innerHTML = time;
|
377
|
+
timeText.id = 'timeText' + (i+1);
|
378
|
+
speedContainer.appendChild(speedText);
|
379
|
+
speedContainer.appendChild(partText);
|
380
|
+
speedContainer.appendChild(timeText);
|
381
|
+
download.appendChild(speedContainer);
|
382
|
+
return download;
|
481
383
|
}
|
482
384
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
385
|
+
/**
|
386
|
+
* 更新下载进度条区域 (!!! 内部逻辑未修改 !!!)
|
387
|
+
* @param {HTMLElement} container - 下载进度条容器 (messageDownload)
|
388
|
+
* @param {Array[]} newMessages - 最新的下载信息数组,每个元素是 [percentage, time, speed, part, status, idname, nameText, file]
|
389
|
+
* @param {Array[]} oldMessages - 上一次的下载信息数组
|
390
|
+
*/
|
391
|
+
function appendBar(container, newMessages, oldMessages) {
|
392
|
+
if (!container) return; // 防御
|
393
|
+
|
394
|
+
const containerId = container.id;
|
395
|
+
const wasAtBottom = !userScrolled[containerId] && (container.scrollHeight - container.scrollTop <= container.clientHeight + 10);
|
396
|
+
const newlength = newMessages.length;
|
397
|
+
const oldlength = oldMessages.length;
|
398
|
+
|
399
|
+
// --- 原始逻辑开始 ---
|
400
|
+
if (newlength > 0) {
|
401
|
+
if (downloadLabel) downloadLabel.textContent = '下载进度:'; // 更新标题
|
402
|
+
|
403
|
+
// --- 更新已存在的进度条 (对应旧消息列表中的最后一项) ---
|
404
|
+
if (oldlength !== 0) {
|
405
|
+
// 假设进度条是按顺序添加的,旧消息的最后一个对应容器中的第一个子元素(因为是 insertBefore(firstChild))
|
406
|
+
// 或者需要更可靠的方式定位?如果 ID 总是连续且从1开始,可以查找 #pbProgress{oldlength} 的父元素
|
407
|
+
// 这里我们假设原始逻辑依赖于 DOM 结构顺序
|
408
|
+
const childToUpdate = container.children[0]; // 找到对应旧列表最后一项的DOM元素
|
409
|
+
|
410
|
+
if(childToUpdate) {
|
411
|
+
const newMessage = newMessages[oldlength - 1];
|
412
|
+
const oldMessage = oldMessages[oldlength - 1];
|
413
|
+
|
414
|
+
// 比较整个数组是否相同可能更可靠,或者比较关键字段
|
415
|
+
if (JSON.stringify(newMessage) !== JSON.stringify(oldMessage)) {
|
416
|
+
const [percentageText, time, speed, part, status] = newMessage; // 只需要更新这些字段
|
417
|
+
const progressElement = childToUpdate.querySelector('#pbProgress' + oldlength);
|
418
|
+
const statusElement = childToUpdate.querySelector('#pbStatusText' + oldlength);
|
419
|
+
const percentageElement = childToUpdate.querySelector('#pbPercentageText' + oldlength);
|
420
|
+
const speedElement = childToUpdate.querySelector('#speedText' + oldlength);
|
421
|
+
const partElement = childToUpdate.querySelector('#partText' + oldlength);
|
422
|
+
const timeElement = childToUpdate.querySelector('#timeText' + oldlength);
|
423
|
+
|
424
|
+
if (progressElement) progressElement.style.width = `${percentageText * 100}%`;
|
425
|
+
if (statusElement) statusElement.innerHTML = status;
|
426
|
+
if (percentageElement) percentageElement.innerHTML = `${(percentageText * 100).toFixed(2)}%`;
|
427
|
+
if (speedElement) speedElement.innerHTML = speed;
|
428
|
+
if (partElement) partElement.innerHTML = part;
|
429
|
+
if (timeElement) timeElement.innerHTML = time;
|
430
|
+
}
|
431
|
+
} else {
|
432
|
+
console.warn(`appendBar: 尝试更新旧进度条 #${oldlength} 时未找到对应的 DOM 元素。`);
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
436
|
+
// --- 添加新的进度条 ---
|
437
|
+
if (newlength !== oldlength) {
|
438
|
+
// *** 优化点:同样可以使用 DocumentFragment 批量添加 ***
|
439
|
+
const fragment = document.createDocumentFragment();
|
440
|
+
for (let i = oldlength; i < newlength; i++) {
|
441
|
+
const messageContent = newMessages[i];
|
442
|
+
// 解构赋值,确保顺序正确
|
443
|
+
const [percentageText, time, speed, part, status, idname, nameText, file] = messageContent;
|
444
|
+
// 调用未修改的 addProgressBar 来创建元素
|
445
|
+
const downloadElement = addProgressBar(i, percentageText, time, speed, part, status, idname, nameText, file);
|
446
|
+
fragment.appendChild(downloadElement);
|
447
|
+
}
|
448
|
+
// 使用 insertBefore 将新的进度条批量插入到容器顶部
|
449
|
+
container.insertBefore(fragment, container.firstChild);
|
450
|
+
}
|
451
|
+
} else {
|
452
|
+
// 如果新消息为空,可以选择清空进度条区域或显示提示
|
453
|
+
if (downloadLabel) downloadLabel.textContent = '暂无下载任务';
|
454
|
+
// container.innerHTML = ''; // 清空旧的进度条
|
504
455
|
}
|
456
|
+
// --- 原始逻辑结束 ---
|
505
457
|
|
506
|
-
//
|
507
|
-
|
508
|
-
|
509
|
-
|
458
|
+
// --- 自动滚动到底部 (对于进度条区域,通常是希望看到最新的,即顶部,所以可能不需要自动滚动到底部) ---
|
459
|
+
// 如果确实需要滚动到底部(显示旧的在上面):
|
460
|
+
// if (wasAtBottom && !userScrolled[containerId]) {
|
461
|
+
// container.scrollTop = container.scrollHeight;
|
462
|
+
// }
|
463
|
+
// 如果希望保持在顶部看到新添加的:
|
464
|
+
if (newlength > oldlength && !userScrolled[containerId]) { // 如果是新增了进度条且用户没滚动
|
465
|
+
container.scrollTop = 0; // 滚动到顶部
|
466
|
+
} else if (wasAtBottom && !userScrolled[containerId]) { // 如果是更新现有条目且之前在底部
|
467
|
+
container.scrollTop = container.scrollHeight; // 保持在底部
|
510
468
|
}
|
469
|
+
}
|
511
470
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
471
|
+
|
472
|
+
// --- SSE (Server-Sent Events) 处理 ---
|
473
|
+
/**
|
474
|
+
* 启动 SSE 连接,接收服务器推送的消息
|
475
|
+
*/
|
476
|
+
function startMessageStream() {
|
477
|
+
// 如果已存在连接,先关闭
|
478
|
+
if (eventSource) {
|
479
|
+
console.log("SSE 连接已存在,将重新启动...");
|
480
|
+
eventSource.close();
|
516
481
|
}
|
517
482
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
483
|
+
console.log("正在启动 SSE 连接到 /stream ...");
|
484
|
+
eventSource = new EventSource('/stream');
|
485
|
+
|
486
|
+
// 监听 'message' 事件 (默认事件)
|
487
|
+
eventSource.onmessage = function(event) {
|
488
|
+
try {
|
489
|
+
const data = JSON.parse(event.data);
|
490
|
+
|
491
|
+
// --- 使用接收到的数据更新 UI ---
|
492
|
+
// 确保 lastMessage 和 data 结构完整,提供默认值防止错误
|
493
|
+
lastMessage = lastMessage || { schedule: [], podflow: [], http: [], download: [] };
|
494
|
+
data.schedule = data.schedule || [];
|
495
|
+
data.podflow = data.podflow || [];
|
496
|
+
data.http = data.http || [];
|
497
|
+
data.download = data.download || [];
|
498
|
+
|
499
|
+
// 1. 更新主进度条
|
500
|
+
// 使用 stringify 比较是为了简单深比较,对于小对象性能影响不大
|
501
|
+
if (JSON.stringify(data.schedule) !== JSON.stringify(lastMessage.schedule)) {
|
502
|
+
updateProgress(data.schedule);
|
503
|
+
lastMessage.schedule = data.schedule; // 更新缓存
|
504
|
+
}
|
505
|
+
|
506
|
+
// 2. 更新 Podflow 消息区域
|
507
|
+
if (JSON.stringify(data.podflow) !== JSON.stringify(lastMessage.podflow)) {
|
508
|
+
appendMessages(messageArea, data.podflow, lastMessage.podflow || []);
|
509
|
+
lastMessage.podflow = [...data.podflow]; // 更新缓存 (使用浅拷贝)
|
510
|
+
}
|
511
|
+
|
512
|
+
// 3. 更新 HTTP 消息区域
|
513
|
+
if (JSON.stringify(data.http) !== JSON.stringify(lastMessage.http)) {
|
514
|
+
appendMessages(messageHttp, data.http, lastMessage.http || []);
|
515
|
+
lastMessage.http = [...data.http]; // 更新缓存 (使用浅拷贝)
|
516
|
+
}
|
517
|
+
|
518
|
+
// 4. 更新下载进度条区域
|
519
|
+
if (JSON.stringify(data.download) !== JSON.stringify(lastMessage.download)) {
|
520
|
+
appendBar(messageDownload, data.download, lastMessage.download || []);
|
521
|
+
lastMessage.download = [...data.download]; // 更新缓存 (使用浅拷贝)
|
522
|
+
}
|
523
|
+
|
524
|
+
} catch (error) {
|
525
|
+
console.error('处理 SSE 消息失败:', error, '原始数据:', event.data);
|
526
|
+
}
|
527
|
+
};
|
528
|
+
|
529
|
+
// 监听错误事件
|
530
|
+
eventSource.onerror = function(error) {
|
531
|
+
console.error('SSE 连接发生错误:', error);
|
532
|
+
// EventSource 默认会自动尝试重连,通常不需要手动处理
|
533
|
+
// 如果需要彻底停止,可以在这里关闭
|
534
|
+
// eventSource.close();
|
535
|
+
// eventSource = null;
|
536
|
+
// 可以考虑在这里添加 UI 提示,告知用户连接中断
|
537
|
+
};
|
538
|
+
|
539
|
+
// 监听连接打开事件 (可选)
|
540
|
+
eventSource.onopen = function() {
|
541
|
+
console.log("SSE 连接已成功建立。");
|
542
|
+
// 连接成功后,可能需要立即获取一次全量数据?或者依赖于服务器推送
|
543
|
+
};
|
544
|
+
|
545
|
+
console.log("SSE 事件监听器已设置。");
|
546
|
+
}
|
547
|
+
|
548
|
+
/**
|
549
|
+
* 停止 SSE 连接
|
550
|
+
*/
|
551
|
+
function stopMessageStream() {
|
552
|
+
if (eventSource) {
|
553
|
+
eventSource.close(); // 关闭连接
|
554
|
+
eventSource = null; // 清除引用
|
555
|
+
console.log("SSE 连接已关闭。");
|
522
556
|
}
|
557
|
+
}
|
523
558
|
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
559
|
+
// --- 事件监听器绑定 ---
|
560
|
+
// 滚动事件 (确保所有相关容器都监听)
|
561
|
+
if (messageArea) messageArea.addEventListener('scroll', onUserScroll);
|
562
|
+
if (messageHttp) messageHttp.addEventListener('scroll', onUserScroll);
|
563
|
+
if (messageDownload) messageDownload.addEventListener('scroll', onUserScroll); // 添加 messageDownload 的监听
|
564
|
+
|
565
|
+
// Channel ID 表单异步提交
|
566
|
+
if (inputForm) {
|
567
|
+
inputForm.addEventListener('submit', function(event) {
|
568
|
+
event.preventDefault(); // 阻止表单默认提交行为
|
569
|
+
const content = inputOutput.value.trim(); // 获取并去除首尾空格
|
570
|
+
if (!content) {
|
571
|
+
alert('请输入内容!');
|
572
|
+
return;
|
573
|
+
}
|
574
|
+
// 可以添加一个加载状态提示
|
575
|
+
console.log("正在提交内容获取 Channel ID...");
|
576
|
+
fetch('getid', {
|
577
|
+
method: 'POST',
|
578
|
+
headers: {
|
579
|
+
'Content-Type': 'application/json',
|
580
|
+
'Accept': 'application/json' // 明确希望接收 JSON
|
581
|
+
},
|
582
|
+
body: JSON.stringify({ content: content }) // 发送 JSON 数据
|
583
|
+
})
|
584
|
+
.then(response => {
|
585
|
+
if (!response.ok) {
|
586
|
+
// 如果服务器返回错误状态码,尝试读取错误信息
|
587
|
+
return response.json().catch(() => {
|
588
|
+
// 如果无法解析 JSON 错误体,抛出通用错误
|
589
|
+
throw new Error(`网络响应错误: ${response.status} ${response.statusText}`);
|
590
|
+
}).then(errorData => {
|
591
|
+
// 如果解析成功,抛出包含服务器信息的错误
|
592
|
+
throw new Error(errorData.error || `请求失败: ${response.status}`);
|
593
|
+
});
|
594
|
+
}
|
595
|
+
return response.json(); // 解析成功的 JSON 响应
|
596
|
+
})
|
597
|
+
.then(data => {
|
598
|
+
if (data && data.response) {
|
599
|
+
inputOutput.value = data.response; // 更新输入框内容
|
600
|
+
console.log("成功获取 Channel ID:", data.response);
|
601
|
+
} else {
|
602
|
+
console.warn("服务器响应格式不正确:", data);
|
603
|
+
alert('服务器返回数据格式错误!');
|
604
|
+
}
|
605
|
+
})
|
606
|
+
.catch(error => {
|
607
|
+
console.error('获取 Channel ID 请求失败:', error);
|
608
|
+
alert(`请求失败:${error.message || '请检查网络或联系管理员'}`);
|
609
|
+
});
|
610
|
+
});
|
611
|
+
}
|
528
612
|
|
529
|
-
//
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
613
|
+
// 粘贴按钮
|
614
|
+
if (pasteBtn) {
|
615
|
+
pasteBtn.addEventListener('click', function() {
|
616
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
617
|
+
navigator.clipboard.readText()
|
618
|
+
.then(text => {
|
619
|
+
inputOutput.value = text;
|
620
|
+
inputOutput.focus(); // 粘贴后聚焦
|
621
|
+
})
|
622
|
+
.catch(err => {
|
623
|
+
console.warn("通过 navigator.clipboard 读取剪贴板失败:", err);
|
624
|
+
alert("无法自动读取剪贴板,请手动粘贴 (Ctrl+V)!");
|
625
|
+
inputOutput.focus(); // 提示后聚焦,方便手动粘贴
|
626
|
+
});
|
627
|
+
} else {
|
628
|
+
// 备选方案 (可能在非 HTTPS 或旧浏览器中需要)
|
629
|
+
try {
|
630
|
+
inputOutput.focus();
|
631
|
+
// execCommand 已不推荐使用,但作为后备
|
632
|
+
if (!document.execCommand('paste')) {
|
633
|
+
throw new Error('execCommand paste failed');
|
634
|
+
}
|
635
|
+
} catch (err) {
|
636
|
+
console.warn("execCommand 粘贴失败:", err);
|
637
|
+
alert("您的浏览器不支持自动粘贴,请手动操作 (Ctrl+V)!");
|
638
|
+
inputOutput.focus();
|
639
|
+
}
|
640
|
+
}
|
641
|
+
});
|
642
|
+
}
|
538
643
|
|
539
|
-
//
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
644
|
+
// 复制按钮
|
645
|
+
if (copyBtn) {
|
646
|
+
copyBtn.addEventListener('click', function() {
|
647
|
+
const textToCopy = inputOutput.value;
|
648
|
+
if (!textToCopy) return; // 没有内容则不复制
|
649
|
+
|
650
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
651
|
+
navigator.clipboard.writeText(textToCopy)
|
652
|
+
.then(() => {
|
653
|
+
// 可以给用户一个成功的视觉反馈,例如按钮变色或提示
|
654
|
+
console.log("内容已复制到剪贴板");
|
655
|
+
// alert("已复制!"); // 或者使用更友好的提示方式
|
656
|
+
})
|
657
|
+
.catch(err => {
|
658
|
+
console.warn("通过 navigator.clipboard 写入剪贴板失败:", err);
|
659
|
+
alert("自动复制失败,请手动选择文本后按 Ctrl+C 复制!");
|
660
|
+
inputOutput.select(); // 选中内容方便手动复制
|
661
|
+
});
|
662
|
+
} else {
|
663
|
+
// 备选方案
|
664
|
+
try {
|
665
|
+
inputOutput.select(); // 选中输入框中的文本
|
666
|
+
if (!document.execCommand('copy')) { // 执行复制命令
|
667
|
+
throw new Error('execCommand copy failed');
|
668
|
+
}
|
669
|
+
console.log("内容已通过 execCommand 复制");
|
670
|
+
// alert("已复制!");
|
671
|
+
} catch (err) {
|
672
|
+
console.warn("execCommand 复制失败:", err);
|
673
|
+
alert("您的浏览器不支持自动复制,请手动选择文本后按 Ctrl+C 复制!");
|
674
|
+
inputOutput.select();
|
675
|
+
}
|
676
|
+
}
|
677
|
+
});
|
678
|
+
}
|
544
679
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
function stopMessageStream() {
|
552
|
-
if (eventSource) {
|
553
|
-
eventSource.close(); // 关闭连接
|
554
|
-
eventSource = null; // 清除引用
|
555
|
-
console.log("SSE 连接已关闭。");
|
680
|
+
// 清空按钮
|
681
|
+
if (clearBtn) {
|
682
|
+
clearBtn.addEventListener('click', function() {
|
683
|
+
inputOutput.value = '';
|
684
|
+
inputOutput.focus(); // 清空后聚焦
|
685
|
+
});
|
556
686
|
}
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
// 如果服务器返回错误状态码,尝试读取错误信息
|
587
|
-
return response.json().catch(() => {
|
588
|
-
// 如果无法解析 JSON 错误体,抛出通用错误
|
589
|
-
throw new Error(`网络响应错误: ${response.status} ${response.statusText}`);
|
590
|
-
}).then(errorData => {
|
591
|
-
// 如果解析成功,抛出包含服务器信息的错误
|
592
|
-
throw new Error(errorData.error || `请求失败: ${response.status}`);
|
593
|
-
});
|
594
|
-
}
|
595
|
-
return response.json(); // 解析成功的 JSON 响应
|
596
|
-
})
|
597
|
-
.then(data => {
|
598
|
-
if (data && data.response) {
|
599
|
-
inputOutput.value = data.response; // 更新输入框内容
|
600
|
-
console.log("成功获取 Channel ID:", data.response);
|
687
|
+
|
688
|
+
// 菜单项点击事件 (使用事件委托)
|
689
|
+
if (menu) {
|
690
|
+
menu.addEventListener('click', function(event) {
|
691
|
+
// 确保点击的是 LI 元素且具有 data-page 属性
|
692
|
+
const target = event.target.closest('li[data-page]');
|
693
|
+
if (target) {
|
694
|
+
const pageId = target.dataset.page;
|
695
|
+
showPage(pageId); // 调用页面切换函数
|
696
|
+
}
|
697
|
+
});
|
698
|
+
}
|
699
|
+
|
700
|
+
// 菜单切换按钮
|
701
|
+
if (toggleMenuBtn) {
|
702
|
+
toggleMenuBtn.addEventListener('click', toggleMenu);
|
703
|
+
}
|
704
|
+
|
705
|
+
// --- 初始化 ---
|
706
|
+
// 根据屏幕宽度初始化菜单状态
|
707
|
+
if (window.innerWidth <= 600) {
|
708
|
+
menu.classList.add('hidden');
|
709
|
+
toggleMenuBtn.style.left = '0px';
|
710
|
+
toggleMenuBtn.textContent = '❯';
|
711
|
+
} else {
|
712
|
+
// 确保大屏幕下按钮初始状态正确 (如果默认不是展开的话)
|
713
|
+
if (menu.classList.contains('hidden')) {
|
714
|
+
toggleMenuBtn.style.left = '0px';
|
715
|
+
toggleMenuBtn.textContent = '❯';
|
601
716
|
} else {
|
602
|
-
|
603
|
-
|
604
|
-
}
|
605
|
-
})
|
606
|
-
.catch(error => {
|
607
|
-
console.error('获取 Channel ID 请求失败:', error);
|
608
|
-
alert(`请求失败:${error.message || '请检查网络或联系管理员'}`);
|
609
|
-
});
|
610
|
-
});
|
611
|
-
}
|
612
|
-
|
613
|
-
// 粘贴按钮
|
614
|
-
if (pasteBtn) {
|
615
|
-
pasteBtn.addEventListener('click', function() {
|
616
|
-
if (navigator.clipboard && navigator.clipboard.readText) {
|
617
|
-
navigator.clipboard.readText()
|
618
|
-
.then(text => {
|
619
|
-
inputOutput.value = text;
|
620
|
-
inputOutput.focus(); // 粘贴后聚焦
|
621
|
-
})
|
622
|
-
.catch(err => {
|
623
|
-
console.warn("通过 navigator.clipboard 读取剪贴板失败:", err);
|
624
|
-
alert("无法自动读取剪贴板,请手动粘贴 (Ctrl+V)!");
|
625
|
-
inputOutput.focus(); // 提示后聚焦,方便手动粘贴
|
626
|
-
});
|
627
|
-
} else {
|
628
|
-
// 备选方案 (可能在非 HTTPS 或旧浏览器中需要)
|
629
|
-
try {
|
630
|
-
inputOutput.focus();
|
631
|
-
// execCommand 已不推荐使用,但作为后备
|
632
|
-
if (!document.execCommand('paste')) {
|
633
|
-
throw new Error('execCommand paste failed');
|
634
|
-
}
|
635
|
-
} catch (err) {
|
636
|
-
console.warn("execCommand 粘贴失败:", err);
|
637
|
-
alert("您的浏览器不支持自动粘贴,请手动操作 (Ctrl+V)!");
|
638
|
-
inputOutput.focus();
|
717
|
+
toggleMenuBtn.style.left = 'var(--menu-width)';
|
718
|
+
toggleMenuBtn.textContent = '❮';
|
639
719
|
}
|
640
|
-
}
|
641
|
-
});
|
642
|
-
}
|
643
|
-
|
644
|
-
// 复制按钮
|
645
|
-
if (copyBtn) {
|
646
|
-
copyBtn.addEventListener('click', function() {
|
647
|
-
const textToCopy = inputOutput.value;
|
648
|
-
if (!textToCopy) return; // 没有内容则不复制
|
649
|
-
|
650
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
651
|
-
navigator.clipboard.writeText(textToCopy)
|
652
|
-
.then(() => {
|
653
|
-
// 可以给用户一个成功的视觉反馈,例如按钮变色或提示
|
654
|
-
console.log("内容已复制到剪贴板");
|
655
|
-
// alert("已复制!"); // 或者使用更友好的提示方式
|
656
|
-
})
|
657
|
-
.catch(err => {
|
658
|
-
console.warn("通过 navigator.clipboard 写入剪贴板失败:", err);
|
659
|
-
alert("自动复制失败,请手动选择文本后按 Ctrl+C 复制!");
|
660
|
-
inputOutput.select(); // 选中内容方便手动复制
|
661
|
-
});
|
662
|
-
} else {
|
663
|
-
// 备选方案
|
664
|
-
try {
|
665
|
-
inputOutput.select(); // 选中输入框中的文本
|
666
|
-
if (!document.execCommand('copy')) { // 执行复制命令
|
667
|
-
throw new Error('execCommand copy failed');
|
668
|
-
}
|
669
|
-
console.log("内容已通过 execCommand 复制");
|
670
|
-
// alert("已复制!");
|
671
|
-
} catch (err) {
|
672
|
-
console.warn("execCommand 复制失败:", err);
|
673
|
-
alert("您的浏览器不支持自动复制,请手动选择文本后按 Ctrl+C 复制!");
|
674
|
-
inputOutput.select();
|
675
|
-
}
|
676
|
-
}
|
677
|
-
});
|
678
|
-
}
|
679
|
-
|
680
|
-
// 清空按钮
|
681
|
-
if (clearBtn) {
|
682
|
-
clearBtn.addEventListener('click', function() {
|
683
|
-
inputOutput.value = '';
|
684
|
-
inputOutput.focus(); // 清空后聚焦
|
685
|
-
});
|
686
|
-
}
|
687
|
-
|
688
|
-
// 菜单项点击事件 (使用事件委托)
|
689
|
-
if (menu) {
|
690
|
-
menu.addEventListener('click', function(event) {
|
691
|
-
// 确保点击的是 LI 元素且具有 data-page 属性
|
692
|
-
const target = event.target.closest('li[data-page]');
|
693
|
-
if (target) {
|
694
|
-
const pageId = target.dataset.page;
|
695
|
-
showPage(pageId); // 调用页面切换函数
|
696
|
-
}
|
697
|
-
});
|
698
|
-
}
|
699
|
-
|
700
|
-
// 菜单切换按钮
|
701
|
-
if (toggleMenuBtn) {
|
702
|
-
toggleMenuBtn.addEventListener('click', toggleMenu);
|
703
|
-
}
|
704
|
-
|
705
|
-
// --- 初始化 ---
|
706
|
-
// 根据屏幕宽度初始化菜单状态
|
707
|
-
if (window.innerWidth <= 600) {
|
708
|
-
menu.classList.add('hidden');
|
709
|
-
toggleMenuBtn.style.left = '0px';
|
710
|
-
toggleMenuBtn.textContent = '❯';
|
711
|
-
} else {
|
712
|
-
// 确保大屏幕下按钮初始状态正确 (如果默认不是展开的话)
|
713
|
-
if (menu.classList.contains('hidden')) {
|
714
|
-
toggleMenuBtn.style.left = '0px';
|
715
|
-
toggleMenuBtn.textContent = '❯';
|
716
|
-
} else {
|
717
|
-
toggleMenuBtn.style.left = 'var(--menu-width)';
|
718
|
-
toggleMenuBtn.textContent = '❮';
|
719
720
|
}
|
720
|
-
}
|
721
721
|
|
722
|
-
|
723
|
-
|
722
|
+
// 初始化时显示默认页面 (例如消息页面)
|
723
|
+
showPage('pageMessage');
|
724
724
|
|
725
725
|
})(); // IIFE 结束
|