From 0f926a57ef34e876f735149a96650c8a11e42542 Mon Sep 17 00:00:00 2001 From: q Date: Tue, 6 Jan 2026 00:00:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0Playground=E5=92=8CJsHttpClie?= =?UTF-8?q?nt=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=95=B4?= =?UTF-8?q?=E7=90=86=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../cn/qaiu/parser/customjs/JsHttpClient.java | 30 +- .../parser/customjs/JsPlaygroundExecutor.java | 2 +- .../java/cn/qaiu/parser/JsHttpClientTest.java | 170 +++ web-front/{ => doc}/MONACO_EDITOR_NPM.md | 0 web-front/{ => doc}/PLAYGROUND_UI_UPGRADE.md | 0 web-front/{ => doc}/UI_FIXES.md | 0 web-front/src/components/MonacoEditor.vue | 62 ++ web-front/src/views/Playground.vue | 968 ++++++++++++++++-- web-service/src/main/resources/app-dev.yml | 4 +- 10 files changed, 1155 insertions(+), 83 deletions(-) rename web-front/{ => doc}/MONACO_EDITOR_NPM.md (100%) rename web-front/{ => doc}/PLAYGROUND_UI_UPGRADE.md (100%) rename web-front/{ => doc}/UI_FIXES.md (100%) diff --git a/.gitignore b/.gitignore index eca4391..e97cbff 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ target/ /src/logs/ *.zip sdkTest.log +app.yml +app-local.yml #some local files diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java index 449c746..24dbeef 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java @@ -244,19 +244,17 @@ public class JsHttpClient { if (data != null) { if (data instanceof String) { - request.sendBuffer(Buffer.buffer((String) data)); + return request.sendBuffer(Buffer.buffer((String) data)); } else if (data instanceof Map) { @SuppressWarnings("unchecked") Map mapData = (Map) data; - request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); + return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); } else { - request.sendJson(data); + return request.sendJson(data); } } else { - request.send(); + return request.send(); } - - return request.send(); }); } @@ -276,19 +274,17 @@ public class JsHttpClient { if (data != null) { if (data instanceof String) { - request.sendBuffer(Buffer.buffer((String) data)); + return request.sendBuffer(Buffer.buffer((String) data)); } else if (data instanceof Map) { @SuppressWarnings("unchecked") Map mapData = (Map) data; - request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); + return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); } else { - request.sendJson(data); + return request.sendJson(data); } } else { - request.send(); + return request.send(); } - - return request.send(); }); } @@ -322,19 +318,17 @@ public class JsHttpClient { if (data != null) { if (data instanceof String) { - request.sendBuffer(Buffer.buffer((String) data)); + return request.sendBuffer(Buffer.buffer((String) data)); } else if (data instanceof Map) { @SuppressWarnings("unchecked") Map mapData = (Map) data; - request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); + return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData)); } else { - request.sendJson(data); + return request.sendJson(data); } } else { - request.send(); + return request.send(); } - - return request.send(); }); } diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java index e0e0c38..e1a31cc 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java @@ -111,7 +111,7 @@ public class JsPlaygroundExecutor { playgroundLogger.infoJava("✅ Fetch API和Promise polyfill注入成功"); } - playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功(演练场)"); + playgroundLogger.infoJava("初始化成功"); // 执行JavaScript代码 engine.eval(jsCode); diff --git a/parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java b/parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java index 94f3508..61fcd55 100644 --- a/parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java +++ b/parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java @@ -549,6 +549,176 @@ public class JsHttpClientTest { } } + @Test + public void testPostWithJsonString() { + System.out.println("\n[测试16] POST请求(JSON字符串) - httpbin.org/post"); + System.out.println("测试修复:POST请求发送JSON字符串时请求体是否正确发送"); + + try { + String url = "https://httpbin.org/post"; + System.out.println("请求URL: " + url); + + // 模拟阿里云盘登录请求格式 + String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_token_123\"}"; + System.out.println("POST数据(JSON字符串): " + jsonData); + + // 设置Content-Type为application/json + httpClient.putHeader("Content-Type", "application/json"); + System.out.println("设置Content-Type: application/json"); + System.out.println("开始请求..."); + + long startTime = System.currentTimeMillis(); + JsHttpClient.JsHttpResponse response = httpClient.post(url, jsonData); + long endTime = System.currentTimeMillis(); + + System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms"); + System.out.println("状态码: " + response.statusCode()); + + String body = response.body(); + System.out.println("响应体(前500字符): " + (body != null && body.length() > 500 ? body.substring(0, 500) + "..." : body)); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.statusCode()); + assertNotNull("响应体不能为null", body); + // 验证请求体是否正确发送(httpbin会回显请求数据) + assertTrue("响应体应该包含发送的JSON数据", + body.contains("grant_type") || body.contains("refresh_token")); + + System.out.println("✓ 测试通过 - POST请求体已正确发送"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("POST JSON字符串请求测试失败: " + e.getMessage()); + } + } + + @Test + public void testAlipanTokenApi() { + System.out.println("\n[测试20] 阿里云盘Token接口测试 - auth.aliyundrive.com/v2/account/token"); + System.out.println("参考 alipan.js 中的登录逻辑,测试请求格式是否正确"); + + try { + String tokenUrl = "https://auth.aliyundrive.com/v2/account/token"; + System.out.println("请求URL: " + tokenUrl); + + // 参考 alipan.js 中的请求格式 + // setJsonHeaders(http) 设置 Content-Type: application/json 和 User-Agent + httpClient.putHeader("Content-Type", "application/json"); + httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + // 参考 alipan.js: JSON.stringify({grant_type: "refresh_token", refresh_token: REFRESH_TOKEN}) + String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"\"}"; + System.out.println("POST数据(JSON字符串): " + jsonData); + System.out.println("注意:使用无效token测试错误响应格式"); + System.out.println("开始请求..."); + + long startTime = System.currentTimeMillis(); + JsHttpClient.JsHttpResponse response = httpClient.post(tokenUrl, jsonData); + long endTime = System.currentTimeMillis(); + + System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms"); + System.out.println("状态码: " + response.statusCode()); + + String body = response.body(); + System.out.println("响应体: " + body); + + // 验证结果 + assertNotNull("响应不能为null", response); + // 使用无效token应该返回400或401等错误状态码,但请求格式应该是正确的 + assertTrue("状态码应该是4xx(无效token)或200(如果token有效)", + response.statusCode() >= 200 && response.statusCode() < 500); + assertNotNull("响应体不能为null", body); + + // 验证响应格式(阿里云盘API通常返回JSON) + try { + Object jsonResponse = response.json(); + System.out.println("响应JSON解析成功: " + jsonResponse); + assertNotNull("JSON响应不能为null", jsonResponse); + } catch (Exception e) { + System.out.println("警告:响应不是有效的JSON格式"); + } + + // 验证请求头是否正确设置 + System.out.println("验证请求头设置..."); + Map headers = httpClient.getHeaders(); + assertTrue("应该设置了Content-Type", headers.containsKey("Content-Type")); + assertEquals("Content-Type应该是application/json", + "application/json", headers.get("Content-Type")); + + System.out.println("✓ 测试通过 - 请求格式正确,已成功发送到阿里云盘API"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + // 如果是超时或其他网络错误,说明请求格式可能有问题 + if (e.getMessage() != null && e.getMessage().contains("超时")) { + fail("请求超时,可能是请求格式问题或网络问题: " + e.getMessage()); + } else { + fail("阿里云盘Token接口测试失败: " + e.getMessage()); + } + } + } + + @Test + public void testAlipanTokenApiWithValidFormat() { + System.out.println("\n[测试21] 阿里云盘Token接口格式验证 - 使用httpbin验证请求格式"); + System.out.println("通过httpbin回显验证请求格式是否与alipan.js中的格式一致"); + + try { + // 使用httpbin来验证请求格式 + String testUrl = "https://httpbin.org/post"; + System.out.println("测试URL: " + testUrl); + + // 参考 alipan.js 中的请求格式 + httpClient.clearHeaders(); // 清空之前的头 + httpClient.putHeader("Content-Type", "application/json"); + httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + // 完全模拟 alipan.js 中的请求体格式 + String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_refresh_token_12345\"}"; + System.out.println("POST数据(JSON字符串): " + jsonData); + System.out.println("开始请求..."); + + long startTime = System.currentTimeMillis(); + JsHttpClient.JsHttpResponse response = httpClient.post(testUrl, jsonData); + long endTime = System.currentTimeMillis(); + + System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms"); + System.out.println("状态码: " + response.statusCode()); + + String body = response.body(); + System.out.println("响应体(前800字符): " + (body != null && body.length() > 800 ? body.substring(0, 800) + "..." : body)); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.statusCode()); + assertNotNull("响应体不能为null", body); + + // httpbin会回显请求数据,验证请求体是否正确发送 + assertTrue("响应体应该包含grant_type字段", body.contains("grant_type")); + assertTrue("响应体应该包含refresh_token字段", body.contains("refresh_token")); + assertTrue("响应体应该包含发送的refresh_token值", body.contains("test_refresh_token_12345")); + + // 验证Content-Type是否正确 + assertTrue("响应体应该包含Content-Type信息", body.contains("application/json")); + + // 验证User-Agent是否正确 + assertTrue("响应体应该包含User-Agent信息", body.contains("Mozilla")); + + System.out.println("✓ 测试通过 - 请求格式与alipan.js中的格式完全一致"); + System.out.println(" - JSON请求体正确发送"); + System.out.println(" - Content-Type正确设置"); + System.out.println(" - User-Agent正确设置"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("阿里云盘Token接口格式验证失败: " + e.getMessage()); + } + } + @Test public void testSetTimeout() { System.out.println("\n[测试15] 设置超时时间 - setTimeout方法"); diff --git a/web-front/MONACO_EDITOR_NPM.md b/web-front/doc/MONACO_EDITOR_NPM.md similarity index 100% rename from web-front/MONACO_EDITOR_NPM.md rename to web-front/doc/MONACO_EDITOR_NPM.md diff --git a/web-front/PLAYGROUND_UI_UPGRADE.md b/web-front/doc/PLAYGROUND_UI_UPGRADE.md similarity index 100% rename from web-front/PLAYGROUND_UI_UPGRADE.md rename to web-front/doc/PLAYGROUND_UI_UPGRADE.md diff --git a/web-front/UI_FIXES.md b/web-front/doc/UI_FIXES.md similarity index 100% rename from web-front/UI_FIXES.md rename to web-front/doc/UI_FIXES.md diff --git a/web-front/src/components/MonacoEditor.vue b/web-front/src/components/MonacoEditor.vue index f226053..536a9c3 100644 --- a/web-front/src/components/MonacoEditor.vue +++ b/web-front/src/components/MonacoEditor.vue @@ -34,6 +34,7 @@ export default { const editorContainer = ref(null); let editor = null; let monaco = null; + let touchHandlers = { start: null, move: null }; const defaultOptions = { value: props.modelValue, @@ -136,6 +137,46 @@ export default { if (editorContainer.value) { editorContainer.value.style.height = props.height; } + + // 移动端:添加触摸缩放来调整字体大小 + if (window.innerWidth <= 768 && editorContainer.value) { + let initialDistance = 0; + let initialFontSize = defaultOptions.fontSize || 14; + const minFontSize = 8; + const maxFontSize = 24; + + const getTouchDistance = (touch1, touch2) => { + const dx = touch1.clientX - touch2.clientX; + const dy = touch1.clientY - touch2.clientY; + return Math.sqrt(dx * dx + dy * dy); + }; + + touchHandlers.start = (e) => { + if (e.touches.length === 2 && editor) { + initialDistance = getTouchDistance(e.touches[0], e.touches[1]); + initialFontSize = editor.getOption(monaco.editor.EditorOption.fontSize); + } + }; + + touchHandlers.move = (e) => { + if (e.touches.length === 2 && editor) { + e.preventDefault(); // 防止页面缩放 + const currentDistance = getTouchDistance(e.touches[0], e.touches[1]); + const scale = currentDistance / initialDistance; + const newFontSize = Math.round(initialFontSize * scale); + + // 限制字体大小范围 + const clampedFontSize = Math.max(minFontSize, Math.min(maxFontSize, newFontSize)); + + if (clampedFontSize !== editor.getOption(monaco.editor.EditorOption.fontSize)) { + editor.updateOptions({ fontSize: clampedFontSize }); + } + } + }; + + editorContainer.value.addEventListener('touchstart', touchHandlers.start, { passive: false }); + editorContainer.value.addEventListener('touchmove', touchHandlers.move, { passive: false }); + } } catch (error) { console.error('Monaco Editor初始化失败:', error); console.error('错误详情:', error.stack); @@ -179,6 +220,11 @@ export default { }); onBeforeUnmount(() => { + // 清理触摸事件监听器 + if (editorContainer.value && touchHandlers.start && touchHandlers.move) { + editorContainer.value.removeEventListener('touchstart', touchHandlers.start); + editorContainer.value.removeEventListener('touchmove', touchHandlers.move); + } if (editor) { editor.dispose(); } @@ -200,10 +246,26 @@ export default { border: 1px solid #dcdfe6; border-radius: 4px; overflow: hidden; + /* 允许用户选择文本 */ + -webkit-user-select: text; + user-select: text; } .monaco-editor-container :deep(.monaco-editor) { border-radius: 4px; } + +/* 移动端:禁用页面缩放,只允许编辑器字体缩放 */ +@media (max-width: 768px) { + .monaco-editor-container { + /* 禁用页面级别的缩放,只允许编辑器内部字体缩放 */ + touch-action: pan-x pan-y; + } + + .monaco-editor-container :deep(.monaco-editor) { + /* 禁用页面级别的缩放 */ + touch-action: pan-x pan-y; + } +} diff --git a/web-front/src/views/Playground.vue b/web-front/src/views/Playground.vue index 54ef926..4f35206 100644 --- a/web-front/src/views/Playground.vue +++ b/web-front/src/views/Playground.vue @@ -65,20 +65,30 @@ +
+
+ + + + + + 首页 + + + 脚本解析器演练场 + JavaScript (ES5) + + +
+
+ + + + + + +
必填, 将作为文件名和@name
+
+ + +
必填, 将作为@type类型标识
+
+ + +
可选, 默认为 yourname
+
+ + +
可选, 正则表达式, 用于匹配分享链接URL
+
+
+ +
+ { + return files.value.find(f => f.id === activeFileId.value) || files.value[0]; + }); + + // 当前编辑的代码(绑定到活动文件) + const isFileChanging = ref(false); // 标记是否正在切换文件 + const currentCode = computed({ + get: () => activeFile.value?.content || '', + set: (value) => { + if (activeFile.value && !isFileChanging.value) { + // 只有在不是切换文件时才标记为已修改 + const oldContent = activeFile.value.content; + activeFile.value.content = value; + // 只有当内容真正改变时才标记为已修改 + if (oldContent !== value) { + activeFile.value.modified = true; + } + } + } + }); + + // ===== 新建文件对话框 ===== + const newFileDialogVisible = ref(false); + const newFileForm = ref({ + name: '', + identifier: '', + author: '', + match: '' + }); + const newFileFormRules = { + name: [ + { required: true, message: '请输入解析器名称', trigger: 'blur' } + ], + identifier: [ + { required: true, message: '请输入标识', trigger: 'blur' } + ] + }; + const newFileFormRef = ref(null); + // ===== 加载和认证状态 ===== const loading = ref(true); const loadProgress = ref(0); @@ -776,6 +980,7 @@ export default { { name: '全屏模式', keys: ['F11'] }, { name: '清空控制台', keys: ['Ctrl+L', 'Cmd+L'] }, { name: '重置代码', keys: ['Ctrl+R', 'Cmd+R'] }, + { name: '编辑器缩放', keys: ['Ctrl+滚轮', 'Cmd+滚轮', 'Ctrl+Plus/Minus', 'Cmd+Plus/Minus'] }, { name: '快捷键帮助', keys: ['Ctrl+/', 'Cmd+/'] } ]; @@ -852,6 +1057,12 @@ function parseById(shareLinkInfo, http, logger) { // 编辑器主题 const editorTheme = computed(() => { + // 根据当前主题名称直接判断,而不是依赖 isDarkMode + const theme = themes.find(t => t.name === currentTheme.value); + if (theme) { + return theme.editor; + } + // 如果没有找到主题,回退到基于 isDarkMode 的判断 return isDarkMode.value ? 'vs-dark' : 'vs'; }); @@ -861,15 +1072,28 @@ function parseById(shareLinkInfo, http, logger) { }); // 编辑器配置 - const editorOptions = { - minimap: { enabled: true }, - scrollBeyondLastLine: false, - wordWrap: 'on', - lineNumbers: 'on', - formatOnPaste: true, - formatOnType: true, - tabSize: 2 - }; + const wordWrapEnabled = ref(true); + const editorOptions = computed(() => { + const baseOptions = { + minimap: { enabled: !isMobile.value }, // 移动端禁用 minimap + scrollBeyondLastLine: false, + wordWrap: wordWrapEnabled.value ? 'on' : 'off', + lineNumbers: 'on', + lineNumbersMinChars: isMobile.value ? 3 : 5, // 移动端行号最多显示3位 + formatOnPaste: true, + formatOnType: true, + tabSize: 2, + // 启用缩放功能 + mouseWheelZoom: true, // PC端:Ctrl/Cmd + 鼠标滚轮缩放 + fontSize: 14, // 默认字体大小 + quickSuggestions: true, + // 移动端支持触摸缩放 + ...(isMobile.value ? { + // 移动端特殊配置 + } : {}) + }; + return baseOptions; + }); // ===== 移动端检测 ===== const updateIsMobile = () => { @@ -951,6 +1175,25 @@ function parseById(shareLinkInfo, http, logger) { router.push('/'); }; + // 新窗口打开首页 + const goHomeInNewWindow = () => { + window.open('/', '_blank'); + }; + + // 检查是否有未保存的文件 + const hasUnsavedFiles = computed(() => { + return files.value.some(f => f.modified); + }); + + // 页面关闭/刷新前的提示 + const handleBeforeUnload = (e) => { + if (hasUnsavedFiles.value) { + e.preventDefault(); + e.returnValue = '您有未保存的文件,确定要离开吗?'; + return e.returnValue; + } + }; + const submitPassword = async () => { if (!inputPassword.value.trim()) { authError.value = '请输入密码'; @@ -989,29 +1232,45 @@ function parseById(shareLinkInfo, http, logger) { await nextTick(); setProgress(20, '加载配置和本地数据...'); - // 加载保存的代码 - const saved = localStorage.getItem('playground_code'); - if (saved) { - jsCode.value = saved; - } else { - // 默认加载示例代码和示例参数 - jsCode.value = exampleCode; - testParams.value.shareUrl = 'https://example.com/s/abc'; - testParams.value.pwd = ''; - testParams.value.method = 'parse'; + + // 加载保存的文件列表 + loadAllFilesFromStorage(); + + // 如果没有文件,加载默认代码 + if (files.value.length === 0 || !files.value[0].content) { + const saved = localStorage.getItem('playground_code'); + if (saved) { + if (files.value.length === 0) { + files.value.push({ id: 'file1', name: '文件1.js', content: saved, modified: false }); + } else { + files.value[0].content = saved; + } + } else { + if (files.value.length === 0) { + files.value.push({ id: 'file1', name: '文件1.js', content: exampleCode, modified: false }); + } else { + files.value[0].content = exampleCode; + } + testParams.value.shareUrl = 'https://example.com/s/abc'; + testParams.value.pwd = ''; + testParams.value.method = 'parse'; + } } - setProgress(50, '初始化Monaco Editor类型定义...'); - await initMonacoTypes(); + // 更新第一个文件的名称(从代码中提取) + if (files.value.length > 0 && files.value[0].id === 'file1') { + updateFileNameFromCode(files.value[0]); + } - setProgress(80, '加载完成...'); - - // 加载保存的主题 + // 先加载保存的主题(在编辑器初始化之前) const savedTheme = localStorage.getItem('playground_theme'); if (savedTheme) { currentTheme.value = savedTheme; const theme = themes.find(t => t.name === savedTheme); if (theme) { + // 同步更新 isDarkMode + isDarkMode.value = theme.page === 'dark'; + await nextTick(); const html = document.documentElement; const body = document.body; @@ -1029,6 +1288,12 @@ function parseById(shareLinkInfo, http, logger) { } } + + setProgress(50, '初始化Monaco Editor类型定义...'); + await initMonacoTypes(); + + setProgress(80, '加载完成...'); + // 加载保存的折叠状态 const savedCollapsed = localStorage.getItem('playground_collapsed_panels'); if (savedCollapsed) { @@ -1087,14 +1352,314 @@ function parseById(shareLinkInfo, http, logger) { // 代码变化处理 const onCodeChange = (value) => { - jsCode.value = value; - // 保存到localStorage - localStorage.setItem('playground_code', value); + currentCode.value = value; + // 更新第一个文件的名称(如果代码中包含@name) + if (activeFile.value && activeFile.value.id === 'file1') { + updateFileNameFromCode(activeFile.value); + } + // 保存到localStorage(保存所有文件) + saveAllFilesToStorage(); + }; + + // 保存所有文件到localStorage + const saveAllFilesToStorage = () => { + const filesData = files.value.map(f => ({ + id: f.id, + name: f.name, + content: f.content + })); + localStorage.setItem('playground_files', JSON.stringify(filesData)); + localStorage.setItem('playground_active_file', activeFileId.value); + }; + + // 从localStorage加载所有文件 + const loadAllFilesFromStorage = () => { + const savedFiles = localStorage.getItem('playground_files'); + if (savedFiles) { + try { + const filesData = JSON.parse(savedFiles); + files.value = filesData.map(f => ({ + ...f, + modified: false + })); + const savedActiveFile = localStorage.getItem('playground_active_file'); + if (savedActiveFile && files.value.find(f => f.id === savedActiveFile)) { + activeFileId.value = savedActiveFile; + } + } catch (e) { + console.warn('加载文件列表失败', e); + } + } + }; + + // 文件切换处理 + const handleFileChange = (fileId) => { + // 标记正在切换文件,防止触发修改标记 + isFileChanging.value = true; + activeFileId.value = fileId; + saveAllFilesToStorage(); + // 等待编辑器更新 + nextTick(() => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.focus(); + } + } + // 切换完成后,取消标记 + setTimeout(() => { + isFileChanging.value = false; + }, 100); + }); + }; + + // 删除文件 + const removeFile = (fileId) => { + if (files.value.length <= 1) { + ElMessage.warning('至少需要保留一个文件'); + return; + } + const index = files.value.findIndex(f => f.id === fileId); + if (index !== -1) { + files.value.splice(index, 1); + // 如果删除的是当前活动文件,切换到第一个文件 + if (activeFileId.value === fileId) { + activeFileId.value = files.value[0].id; + } + saveAllFilesToStorage(); + } + }; + + // 显示新建文件对话框 + const showNewFileDialog = () => { + newFileForm.value = { + name: '', + identifier: '', + author: '', + match: '' + }; + newFileDialogVisible.value = true; + }; + + // 生成模板代码 + const generateTemplate = (name, identifier, author, match) => { + const type = identifier.toLowerCase().replace(/[^a-z0-9]/g, '_'); + const displayName = name; + const description = `使用JavaScript实现的${name}解析器`; + + return `// ==UserScript== +// @name ${name} +// @type ${type} +// @displayName ${displayName} +// @description ${description} +// @match ${match || 'https?://example.com/s/(?\\w+)'} +// @author ${author || 'yourname'} +// @version 1.0.0 +// ==/UserScript== + +/** + * 解析单个文件下载链接 + * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息 + * @param {JsHttpClient} http - HTTP客户端 + * @param {JsLogger} logger - 日志对象 + * @returns {string} 下载链接 + */ +function parse(shareLinkInfo, http, logger) { + var url = shareLinkInfo.getShareUrl(); + logger.info("开始解析: " + url); + + var response = http.get(url); + if (!response.isSuccess()) { + throw new Error("请求失败: " + response.statusCode()); + } + + var html = response.body(); + // 这里添加你的解析逻辑 + // 例如:使用正则表达式提取下载链接 + + return "https://example.com/download/file.zip"; +} + +/** + * 解析文件列表(可选) + * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息 + * @param {JsHttpClient} http - HTTP客户端 + * @param {JsLogger} logger - 日志对象 + * @returns {Array} 文件信息数组 + */ +function parseFileList(shareLinkInfo, http, logger) { + var dirId = shareLinkInfo.getOtherParam("dirId") || "0"; + logger.info("解析文件列表,目录ID: " + dirId); + + // 这里添加你的文件列表解析逻辑 + var fileList = []; + + return fileList; +}`; + }; + + // 创建新文件 + const createNewFile = async () => { + if (!newFileFormRef.value) return; + + await newFileFormRef.value.validate((valid) => { + if (!valid) return; + + // 使用解析器名称作为文件名 + const fileName = newFileForm.value.name.endsWith('.js') + ? newFileForm.value.name + : newFileForm.value.name + '.js'; + + // 检查文件名是否已存在 + if (files.value.some(f => f.name === fileName)) { + ElMessage.warning('文件名已存在,请使用其他名称'); + return; + } + + // 生成模板代码 + const template = generateTemplate( + newFileForm.value.name, + newFileForm.value.identifier, + newFileForm.value.author, + newFileForm.value.match + ); + + // 创建新文件 + fileIdCounter.value++; + const newFile = { + id: 'file' + fileIdCounter.value, + name: fileName, + content: template, + modified: false + }; + + files.value.push(newFile); + activeFileId.value = newFile.id; + newFileDialogVisible.value = false; + saveAllFilesToStorage(); + + ElMessage.success('文件创建成功'); + + // 等待编辑器更新后聚焦 + nextTick(() => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.focus(); + } + } + }); + }); + }; + + // IDE功能:复制全部 + const copyAll = async () => { + try { + await navigator.clipboard.writeText(currentCode.value); + ElMessage.success('已复制全部内容到剪贴板'); + } catch (error) { + ElMessage.error('复制失败: ' + error.message); + } + }; + + // IDE功能:全选 + const selectAll = () => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.setSelection(editor.getModel().getFullModelRange()); + editor.focus(); + } + } + }; + + // IDE功能:切换自动换行 + const toggleWordWrap = () => { + wordWrapEnabled.value = !wordWrapEnabled.value; + // 更新编辑器选项 + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.updateOptions({ wordWrap: wordWrapEnabled.value ? 'on' : 'off' }); + } + } + ElMessage.success(wordWrapEnabled.value ? '已开启自动换行' : '已关闭自动换行'); + }; + + // IDE功能:导出当前文件 + const exportCurrentFile = () => { + if (!activeFile.value || !activeFile.value.content) { + ElMessage.warning('当前文件为空,无法导出'); + return; + } + + try { + const blob = new Blob([activeFile.value.content], { type: 'text/javascript;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = activeFile.value.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + ElMessage.success('文件导出成功'); + } catch (error) { + ElMessage.error('导出失败: ' + error.message); + } + }; + + // IDE功能:撤销 + const undo = () => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.trigger('keyboard', 'undo', null); + editor.focus(); + } + } + }; + + // IDE功能:重做 + const redo = () => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.trigger('keyboard', 'redo', null); + editor.focus(); + } + } + }; + + // 从代码中提取解析器名称 + const extractParserName = (code) => { + if (!code) return null; + const match = code.match(/@name\s+([^\r\n]+)/); + if (match && match[1]) { + return match[1].trim(); + } + return null; + }; + + // 更新文件名称(从代码中提取) + const updateFileNameFromCode = (file) => { + if (!file || file.id !== 'file1') return; // 只更新第一个文件 + const parserName = extractParserName(file.content); + if (parserName) { + const newName = parserName.endsWith('.js') ? parserName : parserName + '.js'; + if (file.name !== newName) { + file.name = newName; + saveAllFilesToStorage(); + } + } }; // 加载示例代码 const loadTemplate = () => { - jsCode.value = exampleCode; + if (activeFile.value) { + activeFile.value.content = exampleCode; + activeFile.value.modified = true; + } // 重置测试参数为示例链接 testParams.value.shareUrl = 'https://example.com/s/abc'; testParams.value.pwd = ''; @@ -1117,33 +1682,33 @@ function parseById(shareLinkInfo, http, logger) { // 保存代码 const saveCode = () => { - localStorage.setItem('playground_code', jsCode.value); - ElMessage.success('代码已保存'); + if (activeFile.value) { + activeFile.value.modified = false; + saveAllFilesToStorage(); + ElMessage.success('代码已保存'); + } }; - // 加载代码 + // 加载代码(已废弃,使用多文件管理) const loadCode = () => { - const saved = localStorage.getItem('playground_code'); - if (saved) { - jsCode.value = saved; - ElMessage.success('代码已加载'); - } else { - ElMessage.warning('没有保存的代码'); - } + loadAllFilesFromStorage(); + ElMessage.success('代码已加载'); }; // 清空代码 const clearCode = () => { - jsCode.value = ''; + if (activeFile.value) { + activeFile.value.content = ''; + activeFile.value.modified = true; + } testResult.value = null; - compiledES5Code.value = ''; - compileStatus.value = { success: true, errors: [] }; }; // 语言切换处理 // 执行测试 const executeTest = async () => { - if (!jsCode.value.trim()) { + const codeToTest = currentCode.value; + if (!codeToTest.trim()) { ElMessage.warning('请先输入JavaScript代码'); return; } @@ -1161,7 +1726,7 @@ function parseById(shareLinkInfo, http, logger) { ]; for (const { pattern, message } of dangerousPatterns) { - if (pattern.test(jsCode.value)) { + if (pattern.test(codeToTest)) { const confirmed = await ElMessageBox.confirm( `⚠️ ${message}\n\n这可能导致脚本无法停止并占用服务器资源。\n\n建议修改代码,添加合理的循环退出条件。\n\n确定要继续执行吗?`, '危险代码警告', @@ -1186,7 +1751,7 @@ function parseById(shareLinkInfo, http, logger) { try { const result = await playgroundApi.testScript( - jsCode.value, // 直接使用JavaScript代码 + codeToTest, // 使用当前活动文件的代码 testParams.value.shareUrl, testParams.value.pwd, testParams.value.method @@ -1276,11 +1841,12 @@ function parseById(shareLinkInfo, http, logger) { // 发布解析器 const publishParser = () => { - if (!jsCode.value.trim()) { + const codeToPublish = currentCode.value; + if (!codeToPublish.trim()) { ElMessage.warning('请先编写JavaScript代码'); return; } - publishForm.value.jsCode = jsCode.value; + publishForm.value.jsCode = codeToPublish; publishDialogVisible.value = true; }; @@ -1288,14 +1854,15 @@ function parseById(shareLinkInfo, http, logger) { const confirmPublish = async () => { publishing.value = true; try { - const result = await playgroundApi.saveParser(jsCode.value); + const codeToPublish = currentCode.value; + const result = await playgroundApi.saveParser(codeToPublish); console.log('保存解析器响应:', result); // 检查响应格式 if (result.code === 200 || result.success) { // 从响应或代码中提取type信息 let parserType = ''; try { - const typeMatch = jsCode.value.match(/@type\s+(\w+)/); + const typeMatch = codeToPublish.match(/@type\s+(\w+)/); parserType = typeMatch ? typeMatch[1] : ''; } catch (e) { console.warn('无法提取type', e); @@ -1363,14 +1930,56 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" } }; - // 加载解析器到编辑器 + // 加载解析器到编辑器(添加到新的文件tab标签) const loadParserToEditor = async (parser) => { try { const result = await playgroundApi.getParserById(parser.id); if (result.code === 200 && result.data) { - jsCode.value = result.data.jsCode; + // 从代码中提取文件名 + const code = result.data.jsCode; + let fileName = parser.name || '解析器.js'; + + // 尝试从@name提取文件名 + const nameMatch = code.match(/@name\s+([^\r\n]+)/); + if (nameMatch && nameMatch[1]) { + const parserName = nameMatch[1].trim(); + fileName = parserName.endsWith('.js') ? parserName : parserName + '.js'; + } + + // 检查文件名是否已存在,如果存在则添加序号 + let finalFileName = fileName; + let counter = 1; + while (files.value.some(f => f.name === finalFileName)) { + const nameWithoutExt = fileName.replace(/\.js$/, ''); + finalFileName = `${nameWithoutExt}_${counter}.js`; + counter++; + } + + // 创建新文件 + fileIdCounter.value++; + const newFile = { + id: 'file' + fileIdCounter.value, + name: finalFileName, + content: code, + modified: false + }; + + files.value.push(newFile); + activeFileId.value = newFile.id; activeTab.value = 'editor'; - ElMessage.success('已加载到编辑器'); + saveAllFilesToStorage(); + + ElMessage.success('已添加到新文件标签'); + + // 等待编辑器更新后聚焦 + nextTick(() => { + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.focus(); + } + } + }); } else { ElMessage.error('加载失败'); } @@ -1414,6 +2023,9 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" currentTheme.value = themeName; const theme = themes.find(t => t.name === themeName); if (theme) { + // 同步更新 isDarkMode + isDarkMode.value = theme.page === 'dark'; + // 切换页面主题 const html = document.documentElement; const body = document.body; @@ -1615,6 +2227,9 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" updateIsMobile(); window.addEventListener('resize', updateIsMobile); + // 添加页面关闭/刷新前的提示 + window.addEventListener('beforeunload', handleBeforeUnload); + // 检查认证状态 const isAuthed = await checkAuthStatus(); @@ -1651,12 +2266,15 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" onUnmounted(() => { window.removeEventListener('resize', updateIsMobile); + // 移除页面关闭/刷新前的提示 + window.removeEventListener('beforeunload', handleBeforeUnload); }); return { LANGUAGE, editorRef, jsCode, + currentCode, testParams, testResult, testing, @@ -1664,6 +2282,27 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" editorTheme, shouldShowAuthUI, editorOptions, + // 多文件管理 + files, + activeFileId, + activeFile, + handleFileChange, + removeFile, + // 新建文件 + newFileDialogVisible, + newFileForm, + newFileFormRef, + newFileFormRules, + showNewFileDialog, + createNewFile, + // IDE功能 + copyAll, + selectAll, + toggleWordWrap, + wordWrapEnabled, + exportCurrentFile, + undo, + redo, // 加载和认证 loading, loadProgress, @@ -1677,6 +2316,7 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" checkAuthStatus, submitPassword, goHome, + goHomeInNewWindow, // 移动端 isMobile, updateIsMobile, @@ -1939,6 +2579,12 @@ body.dark-theme .splitpanes__splitter:hover, transition: all 0.3s ease; } +/* 移动端:去掉边距,占满宽度 */ +.playground-container.is-mobile { + padding: 0; + min-height: 100vh; +} + .playground-container.dark-theme { background-color: #0a0a0a; } @@ -1964,6 +2610,18 @@ body.dark-theme .splitpanes__splitter:hover, transition: all 0.3s ease; } +/* 移动端:卡片占满宽度,去掉边距 */ +.playground-container.is-mobile .playground-card { + margin: 0; + border-radius: 0; + border-left: none; + border-right: none; +} + +.playground-container.is-mobile .playground-card :deep(.el-card__body) { + padding: 12px; +} + .dark-theme .playground-card { background: #1a1a1a; border-color: rgba(255, 255, 255, 0.1); @@ -1985,8 +2643,24 @@ body.dark-theme .splitpanes__splitter:hover, } .header-left { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + min-width: 0; + gap: 4px; +} + +.header-left-top { display: flex; align-items: center; + width: 100%; +} + +.header-left-bottom { + display: flex; + align-items: center; + width: 100%; } .title { @@ -1995,6 +2669,58 @@ body.dark-theme .splitpanes__splitter:hover, color: var(--el-text-color-primary); } +/* 面包屑导航样式 */ +.breadcrumb-nav { + display: flex; + align-items: center; + white-space: nowrap; + flex-shrink: 0; +} + +.breadcrumb-nav :deep(.el-breadcrumb) { + display: flex; + align-items: center; + white-space: nowrap; +} + +.breadcrumb-nav :deep(.el-breadcrumb__inner) { + font-size: 14px; + color: var(--el-text-color-regular); + white-space: nowrap; + display: inline-block; +} + +.breadcrumb-nav :deep(.el-breadcrumb__inner.is-link) { + color: var(--el-text-color-primary); + font-weight: 500; +} + +.breadcrumb-nav :deep(.el-breadcrumb__separator) { + margin: 0 8px; + white-space: nowrap; +} + +.breadcrumb-link { + display: inline-flex; + align-items: center; + color: var(--el-text-color-primary); + font-size: 14px; + font-weight: 500; + transition: color 0.2s; +} + +.breadcrumb-link:hover { + color: var(--el-color-primary); +} + +.dark-theme .breadcrumb-link { + color: rgba(255, 255, 255, 0.85); +} + +.dark-theme .breadcrumb-link:hover { + color: var(--el-color-primary); +} + .header-actions { display: flex; gap: 8px; @@ -2002,6 +2728,16 @@ body.dark-theme .splitpanes__splitter:hover, flex-wrap: wrap; } +/* 移动端:按钮左对齐 */ +.playground-container.is-mobile .header-actions { + justify-content: flex-start; + width: 100%; +} + +.playground-container.is-mobile .header-actions .el-button-group { + margin-right: 0; +} + /* ===== Splitpanes样式 ===== */ .splitpanes { height: calc(100vh - 280px); @@ -2217,6 +2953,15 @@ html.dark .default-theme.splitpanes { background: transparent !important; } +/* 确保编辑器面板和测试面板高度一致 */ +.editor-pane, +.test-pane { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + /* 暗色模式下确保所有分隔线都是深色 - 终极覆盖 */ .dark-theme .splitpanes__splitter, .dark-theme .splitpanes.default-theme > .splitpanes__splitter, @@ -2383,12 +3128,55 @@ html.dark .playground-container .splitpanes__splitter:hover { transform: translateY(-20px); } +/* ===== 文件标签页 ===== */ +.file-tabs-container { + margin-bottom: 12px; +} + +.file-tabs-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.file-tabs { + flex: 1; +} + +.file-tabs :deep(.el-tabs__header) { + margin: 0; +} + +.file-tabs :deep(.el-tabs__item) { + padding: 0 15px; + height: 32px; + line-height: 32px; + font-size: 13px; +} + +.file-tabs :deep(.el-tabs__item.is-active) { + background-color: var(--el-color-primary-light-9); + color: var(--el-color-primary); +} + +.dark-theme .file-tabs :deep(.el-tabs__item.is-active) { + background-color: rgba(64, 158, 255, 0.2); + color: var(--el-color-primary); +} + +.new-file-tab-btn { + flex-shrink: 0; +} + /* ===== 编辑器区域 ===== */ .editor-section { border-radius: 4px; overflow: hidden; border: 1px solid var(--el-border-color); background: var(--el-bg-color); + height: 100%; + display: flex; + flex-direction: column; } /* ===== 测试区域 ===== */ @@ -2661,6 +3449,14 @@ html.dark .playground-container .splitpanes__splitter:hover { color: rgba(255, 255, 255, 0.85); } +/* ===== 新建文件对话框样式 ===== */ +.new-file-dialog .form-tip { + font-size: 12px; + color: var(--el-text-color-secondary); + margin-top: 4px; + line-height: 1.4; +} + .empty-result { text-align: center; padding: 40px 0; @@ -2903,7 +3699,55 @@ html.dark .playground-container .splitpanes__splitter:hover { .mobile-layout .editor-section { width: 100%; + margin: 0; margin-bottom: 12px; + padding: 0; +} + +/* 移动端编辑器容器:去掉所有边距 */ +.playground-container.is-mobile .mobile-layout .editor-section { + margin: 0; + padding: 0; + position: relative; +} + +.playground-container.is-mobile .mobile-layout .editor-section :deep(.monaco-editor-container) { + border-radius: 0; + border-left: none; + border-right: none; +} + +/* 移动端编辑器悬浮操作按钮 */ +.mobile-editor-actions { + position: absolute; + bottom: 20px; + right: 20px; + z-index: 10; + display: flex; + gap: 8px; +} + +.mobile-editor-actions .editor-action-btn { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + background: var(--el-bg-color); + border: 1px solid var(--el-border-color); +} + +.mobile-editor-actions .editor-action-btn:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + transform: translateY(-2px); + transition: all 0.2s ease; +} + +.dark-theme .mobile-editor-actions .editor-action-btn { + background: rgba(30, 30, 30, 0.95); + border-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.85); +} + +.dark-theme .mobile-editor-actions .editor-action-btn:hover { + background: rgba(40, 40, 40, 0.95); + border-color: rgba(255, 255, 255, 0.2); } .mobile-test-section { diff --git a/web-service/src/main/resources/app-dev.yml b/web-service/src/main/resources/app-dev.yml index 3271a88..72d30fc 100644 --- a/web-service/src/main/resources/app-dev.yml +++ b/web-service/src/main/resources/app-dev.yml @@ -16,9 +16,9 @@ proxyConf: server-proxy # JS演练场配置 playground: # 是否启用演练场,默认false不启用 - enabled: false + enabled: true # 公开模式,默认false需要密码访问,设为true则无需密码 - public: false + public: true # 访问密码,建议修改默认密码! password: 'nfd_playground_2024'