UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

1,157 lines (1,019 loc) 50.8 kB
// Monaco Editor 本地加载 require.config({ paths: { 'vs': '/static/monaco/min/vs' } }); window.MonacoEnvironment = { getWorkerUrl: function (moduleId, label) { // 指向本地 worker 文件 return '/static/monaco/min/vs/base/worker/workerMain.js'; } }; let editor, currentFile = ''; let isDirty = false; let isRestoringTreeSelection = false; let pendingTreeData = null; let isRefreshingTree = false; let debounceTreeChangedTimer = null; let problemFile = ''; let problemFileContent = ''; let isMdDirty = false; let lastSavedContent = ''; let currentIsMarkdown = false; document.addEventListener('DOMContentLoaded', async function () { // 刷新按钮事件绑定 var btn = document.getElementById('btn-refresh-file'); if (btn) btn.onclick = function () { reloadFile(); }; // 移除 btn-refresh-tree 相关逻辑 var btnTree = document.getElementById('btn-refresh-tree'); if (btnTree) { // 移除 btn-refresh-tree 相关逻辑 } var editorDiv = document.getElementById('editor'); if (!editorDiv) return; require(["vs/editor/editor.main"], function () { editor = monaco.editor.create(editorDiv, { value: '', language: 'cpp', theme: 'vs-dark', automaticLayout: true, fontSize: 14, }); window.editor = editor; // 实时预览 editor.onDidChangeModelContent(function () { if (!isDirty) { isDirty = true; updateCurrentFileDisplay(); } // 不再依赖 ws 推送,直接前端实时渲染 // updatePreviewFromEditor(); }); // 光标变动时预览区跟随 // editor.onDidChangeCursorPosition(function (e) { // scrollPreviewToEditorLine(e.position.lineNumber); // }); // 预览区滚动到对应行 function scrollPreviewToEditorLine(line) { const previewDiv = document.getElementById('preview-html'); if (previewDiv) { const el = previewDiv.querySelector(`[data-line='${line}']`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } } // 文件树懒加载 $('#file-tree').jstree({ 'core': { 'themes': { 'name': 'default-dark', 'dots': true, 'icons': true }, 'data': function (obj, cb) { if (obj.id === '#') { fetch('/api/tree').then(r => r.json()).then(data => cb(data)); } else { // 用真实目录名(data.path)作为 path 参数 const path = obj.data && typeof obj.data.path === 'string' ? obj.data.path : obj.id; fetch('/api/tree?path=' + encodeURIComponent(path)).then(r => r.json()).then(data => cb(data)); } }, 'check_callback': true }, 'plugins': ['wholerow'] }); // 自动展开题目ID目录并自动打开第一个md文件 $('#file-tree').on('ready.jstree', function (e, data) { const tree = $('#file-tree').jstree(true); const root = tree.get_node('#').children[0]; console.log('root', root); tree.open_node(root, function () { // 查找第一个md文件 const allNodes = tree.get_json(root, { flat: true }); const firstMd = allNodes.find(n => n.data && n.data.type === 'file' && n.text.toLowerCase().endsWith('.md')); if (firstMd) { setTimeout(function () { tree.deselect_all(); tree.select_node(firstMd.id); // 强制加载 loadFile(firstMd.data.path); }, 100); } }); }); // jstree节点渲染时,灰色不可编辑文件 $('#file-tree').on('after_open.jstree refresh.jstree', function (e, data) { const tree = $('#file-tree').jstree(true); tree.get_json(data.node || '#', { flat: true }).forEach(function (n) { if (n.data && n.data.type === 'file') { const ext = n.text.split('.').pop().toLowerCase(); // id 已为安全 id const anchorId = '#' + n.id + '_anchor'; if (['md', 'cpp', 'cc', 'in', 'out', 'ans', 'json', 'yaml'].indexOf(ext) === -1) { $(anchorId).css({ 'color': '#1f1f1f', 'pointer-events': 'none', 'cursor': 'not-allowed' }); } else { $(anchorId).css({ 'color': '', 'pointer-events': '', 'cursor': '' }); } } }); }); // 只在 editor 初始化后绑定 select_node 事件 $('#file-tree').off('select_node.jstree'); $('#file-tree').on('select_node.jstree', function (e, data) { if (isRestoringTreeSelection) { // console.log('跳过 select_node 事件(正在恢复选中)'); return; } if (data.node && data.node.data && data.node.data.type === 'file') { const path = data.node.data.path; const ext = path.split('.').pop().toLowerCase(); // 只允许特定后缀可编辑 if (["md", "in", "ans", "out", "txt", "cpp", "cc", "cxx", "json", "yaml", "yml", "js"].indexOf(ext) !== -1) { loadFile(path); } else { // 取消选中 setTimeout(() => $('#file-tree').jstree('deselect_node', data.node), 0); } } else if (data.node && data.node.data && data.node.data.type === 'dir') { $('#file-tree').jstree('toggle_node', data.node); } }); // 切换到markdown编辑器 function switchToMarkdownEditor(content) { // 隐藏 Monaco Editor domNode,仅显示 textarea if (window.editor && editor.getDomNode()) editor.getDomNode().style.display = 'none'; mdTextarea.style.display = 'block'; mdTextarea.value = content; currentIsMarkdown = true; isMdDirty = false; updateCurrentFileDisplay(); // 显示预览区 document.getElementById('preview').style.display = ''; // 重新绑定光标和滚动同步 mdTextarea.removeEventListener('keyup', syncPreviewToMdTextareaBlock); mdTextarea.removeEventListener('click', syncPreviewToMdTextareaBlock); mdTextarea.removeEventListener('scroll', window.syncPreviewToMdTextareaScroll); mdTextarea.addEventListener('keyup', syncPreviewToMdTextareaBlock); mdTextarea.addEventListener('click', syncPreviewToMdTextareaBlock); mdTextarea.addEventListener('scroll', window.syncPreviewToMdTextareaScroll); } // 切换到Monaco编辑器 async function switchToMonacoEditor(content, lang) { mdTextarea.style.display = 'none'; // 移除textarea的滚动同步 mdTextarea.removeEventListener('keyup', syncPreviewToMdTextareaBlock); mdTextarea.removeEventListener('click', syncPreviewToMdTextareaBlock); mdTextarea.removeEventListener('scroll', window.syncPreviewToMdTextareaScroll); if (window.editor && editor.getDomNode()) editor.getDomNode().style.display = ''; editor.setValue(content); if (lang) monaco.editor.setModelLanguage(editor.getModel(), lang); currentIsMarkdown = false; isDirty = false; updateCurrentFileDisplay(); // 不隐藏预览区,让其显示题目内容 // document.getElementById('preview').style.display = 'none'; // 为C++文件添加右侧功能按钮 if (lang === 'cpp') { addCppToolbar(); } else { removeCppToolbar(); } } // 添加C++工具栏 function addCppToolbar() { removeCppToolbar(); // 先移除已存在的 const toolbar = document.createElement('div'); toolbar.id = 'cpp-toolbar'; toolbar.style.cssText = 'position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:8px;z-index:100;'; // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存'; saveBtn.title = '保存当前文件 (Ctrl+S)'; saveBtn.style.cssText = 'padding:6px 12px;background:#333;color:#e0e0e0;border:1px solid #555;border-radius:5px;cursor:pointer;font-size:12px;transition:background 0.2s;'; saveBtn.onmouseenter = () => saveBtn.style.background = '#444'; saveBtn.onmouseleave = () => saveBtn.style.background = '#333'; saveBtn.onclick = saveFile; // 编译按钮 const compileBtn = document.createElement('button'); compileBtn.textContent = '编译'; compileBtn.title = '保存并编译当前文件'; compileBtn.style.cssText = saveBtn.style.cssText; compileBtn.onmouseenter = () => compileBtn.style.background = '#444'; compileBtn.onmouseleave = () => compileBtn.style.background = '#333'; compileBtn.onclick = compileCurrentFile; // 检查文件类型 - 使用当前正在编辑的文件路径 const fileName = currentFile ? currentFile.split('/').pop() : ''; console.log('addCppToolbar - currentFile:', currentFile, 'fileName:', fileName); const isValidator = fileName === 'validator.cpp'; const isChecker = fileName === 'checker.cpp'; const isGenerator = fileName === 'generator.cpp'; // 所有C++文件都显示保存和编译按钮 toolbar.appendChild(saveBtn); toolbar.appendChild(compileBtn); // 添加运行按钮 - 对于普通C++文件和generator.cpp if (!isValidator && !isChecker) { const runBtn = document.createElement('button'); runBtn.textContent = '运行'; runBtn.title = '编译并运行当前程序,传入参数"1"'; runBtn.style.cssText = saveBtn.style.cssText; runBtn.onmouseenter = () => runBtn.style.background = '#444'; runBtn.onmouseleave = () => runBtn.style.background = '#333'; runBtn.onclick = runCurrentFile; toolbar.appendChild(runBtn); } if (isValidator) { // validator文件:显示测试验证器按钮 const validatorBtn = document.createElement('button'); validatorBtn.textContent = '测试验证器'; validatorBtn.title = '编译并测试validator,使用sample中的in文件'; validatorBtn.style.cssText = saveBtn.style.cssText; validatorBtn.onmouseenter = () => validatorBtn.style.background = '#444'; validatorBtn.onmouseleave = () => validatorBtn.style.background = '#333'; validatorBtn.onclick = runValidatorTest; toolbar.appendChild(validatorBtn); } else if (!isChecker && !isGenerator) { // 普通C++文件(非validator、checker、generator):显示diff按钮 const diffBtn = document.createElement('button'); diffBtn.textContent = 'diff'; diffBtn.title = '编译并测试样例,显示与标准答案的差异'; diffBtn.style.cssText = saveBtn.style.cssText; diffBtn.onmouseenter = () => diffBtn.style.background = '#444'; diffBtn.onmouseleave = () => diffBtn.style.background = '#333'; diffBtn.onclick = runDiffTest; toolbar.appendChild(diffBtn); } // checker.cpp 只显示保存和编译按钮 document.getElementById('editor').appendChild(toolbar); } // 运行validator测试 async function runValidatorTest() { if (!currentFile) { showSaveMsg('错误:未选择文件', true); return; } // 先保存文件 await saveFile(); const ext = currentFile.split('.').pop().toLowerCase(); if (!['cpp', 'cc', 'cxx'].includes(ext)) { showSaveMsg('错误:不是C++文件', true); return; } // 检查是否是validator文件 const fileName = currentFile.split('/').pop(); if (fileName !== 'validator.cpp') { showSaveMsg('错误:只有validator.cpp文件支持此功能', true); return; } // 获取sample文件列表 try { const sampleRes = await fetch('/api/tree?path=sample'); if (!sampleRes.ok) { showSaveMsg('错误:无法获取sample目录', true); return; } const sampleFiles = await sampleRes.json(); const inFiles = sampleFiles.filter(f => f.text.endsWith('.in')).map(f => f.text); if (inFiles.length === 0) { showSaveMsg('错误:sample目录中没有.in文件', true); return; } // 构建命令序列 const commands = []; const problemId = getCurrentProblemId(); // 获取平台信息并选择跨平台命令 let rmCmd = 'rm -f'; // 默认使用rm let fileCheckCmd = 'if [ -f'; // 默认使用bash语法 try { const platformRes = await fetch('/api/platform'); if (platformRes.ok) { const platformData = await platformRes.json(); rmCmd = platformData.rmCommand || 'rm -f'; fileCheckCmd = platformData.fileCheckCommand || 'if [ -f'; console.log(`[VALIDATOR] 平台信息:`, platformData); } } catch (err) { console.warn('无法获取平台信息,使用默认命令'); } console.log(`[VALIDATOR] 最终使用的命令:`, { rmCmd, fileCheckCmd }); // 构建清理、编译和检查命令 const executablePath = `${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}`; const cleanCmd = `${rmCmd} "${executablePath}"`; const compileCmd = `g++ -O2 -std=c++14 -o "${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" "${problemId}/${currentFile}"`; // 先添加清理命令 commands.push(cleanCmd); console.log(`[VALIDATOR] 清理命令:`, cleanCmd); // 再添加编译命令 commands.push(compileCmd); console.log(`[VALIDATOR] 编译命令:`, compileCmd); // 添加编译结果检查命令 const checkCompileCmd = `${fileCheckCmd} "${executablePath}" ]; then echo "编译成功: ${executablePath} 已生成"; else echo "编译失败: ${executablePath} 不存在"; exit 1; fi`; commands.push(checkCompileCmd); console.log(`[VALIDATOR] 编译检查命令:`, checkCompileCmd); // 构建所有sample的validator测试命令 const testCommands = []; for (const inFile of inFiles) { // 单个sample的validator测试命令 const singleTestCmd = `"${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" < ${problemId}/sample/${inFile}`; testCommands.push(singleTestCmd); console.log(`[VALIDATOR] 构建测试命令 ${testCommands.length}:`, { inFile, singleTestCmd }); } // 添加所有测试命令 commands.push(...testCommands); console.log(`[VALIDATOR] 总共 ${commands.length} 个命令:`, commands); // 在终端中执行命令 if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) { // 清空终端显示并滚动到底部 if (window.term) { window.term.clear(); // 确保滚动到底部 setTimeout(() => { window.term.scrollToBottom(); }, 100); } // 逐个发送命令到终端 for (let i = 0; i < commands.length; i++) { const cmd = commands[i]; console.log(`[VALIDATOR] 发送第 ${i + 1}/${commands.length} 个命令:`, cmd); // 检查命令类型并显示相应提示 if (cmd.includes('rm -f')) { showSaveMsg('清理旧的可执行文件...'); } else if (cmd.includes('g++') && cmd.includes('-o')) { showSaveMsg(`正在编译 validator.cpp...`); } else if (cmd.includes('if [ -f') || (cmd.includes('if exist') && cmd.includes('echo.'))) { showSaveMsg('检查编译结果...'); } else { const sampleMatch = cmd.match(/sample(\d+)\.in/); if (sampleMatch) { showSaveMsg(`验证 sample${sampleMatch[1]}...`); } else { showSaveMsg(`执行命令 ${i + 1}/${commands.length}...`); } } window.terminalWs.send(cmd + '\r'); // 发送命令后立即滚动到底部 setTimeout(() => { scrollTerminalToBottom(); }, 50); // 等待一段时间再发送下一个命令 if (i < commands.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); // 增加等待时间 } } showSaveMsg('所有validator测试命令已发送到终端'); } else { showSaveMsg('错误:终端WebSocket连接不可用', true); } } catch (err) { showSaveMsg('validator测试失败: ' + err.message, true); } } // 自动滚动终端到底部 function scrollTerminalToBottom() { if (window.term) { // 滚动到最底部 window.term.scrollLines(window.term.buffer.active.viewportY); } } // 清除终端内容 function clearTerminal() { if (window.term) { window.term.clear(); } } // 清除终端内容并滚动到底部 function clearTerminalAndScroll() { clearTerminal(); // 延迟滚动确保清除完成 setTimeout(() => { scrollTerminalToBottom(); }, 50); } // 将函数挂载到window对象,方便控制台测试 window.scrollTerminalToBottom = scrollTerminalToBottom; window.clearTerminal = clearTerminal; window.clearTerminalAndScroll = clearTerminalAndScroll; // 移除C++工具栏 function removeCppToolbar() { const toolbar = document.getElementById('cpp-toolbar'); if (toolbar) toolbar.remove(); } // 编译当前文件 async function compileCurrentFile() { if (!currentFile) { showSaveMsg('错误:未选择文件', true); return; } // 先保存文件 await saveFile(); const ext = currentFile.split('.').pop().toLowerCase(); if (!['cpp', 'cc', 'cxx'].includes(ext)) { showSaveMsg('错误:不是C++文件', true); return; } // 获取平台信息 let rmCmd = 'rm -f'; // 默认使用rm try { const platformRes = await fetch('/api/platform'); if (platformRes.ok) { const platformData = await platformRes.json(); rmCmd = platformData.rmCommand || 'rm -f'; } } catch (err) { console.warn('无法获取平台信息,使用默认命令'); } // 构建清理和编译命令 const fileName = currentFile.split('/').pop(); const problemId = getCurrentProblemId(); const executablePath = `${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}`; const cleanCmd = `${rmCmd} "${executablePath}"`; const compileCmd = `g++ -O2 -std=c++14 -o "${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" "${problemId}/${currentFile}"`; // 在终端中执行编译命令 if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) { // 清空终端显示并滚动到底部 clearTerminalAndScroll(); // 发送清理命令 console.log(`[COMPILE] 发送清理命令:`, cleanCmd); showSaveMsg('清理旧的可执行文件...'); window.terminalWs.send(cleanCmd + '\r'); // 延迟发送编译命令 setTimeout(() => { console.log(`[COMPILE] 发送编译命令:`, compileCmd); showSaveMsg(`正在编译 ${currentFile}...`); window.terminalWs.send(compileCmd + '\r'); // 发送命令后立即滚动到底部 setTimeout(() => { scrollTerminalToBottom(); }, 50); }, 500); showSaveMsg('编译命令已发送到终端'); } else { showSaveMsg('错误:终端WebSocket连接不可用', true); } } // 运行当前文件 async function runCurrentFile() { if (!currentFile) { showSaveMsg('错误:未选择文件', true); return; } // 先保存文件 await saveFile(); const ext = currentFile.split('.').pop().toLowerCase(); if (!['cpp', 'cc', 'cxx'].includes(ext)) { showSaveMsg('错误:不是C++文件', true); return; } // 检查是否是特殊文件 const fileName = currentFile.split('/').pop(); if (['validator.cpp', 'checker.cpp'].includes(fileName)) { showSaveMsg('错误:特殊文件不支持直接运行', true); return; } // 获取平台信息 let rmCmd = 'rm -f'; // 默认使用rm let isWindows = false; // 默认不是Windows let fileCheckCommand = 'if [ -f'; // 默认使用Unix/Linux/macOS语法 try { const platformRes = await fetch('/api/platform'); if (platformRes.ok) { const platformData = await platformRes.json(); rmCmd = platformData.rmCommand || 'rm -f'; isWindows = platformData.isWindows || false; fileCheckCommand = platformData.fileCheckCommand || 'if [ -f'; } } catch (err) { console.warn('无法获取平台信息,使用默认命令'); } // 构建清理、编译和运行命令 const problemId = getCurrentProblemId(); const executablePath = `${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}`; const cleanCmd = `${rmCmd} "${executablePath}"`; const compileCmd = `g++ -O2 -std=c++14 -o "${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" "${problemId}/${currentFile}"`; // 构建跨平台的文件检查和运行命令 let fileCheckCmd; if (isWindows) { // Windows平台使用cmd语法 fileCheckCmd = `if exist "${executablePath}" ("${executablePath}" 1)`; } else { // Unix/Linux/macOS平台使用bash语法 fileCheckCmd = `if [ -f "${executablePath}" ]; then "${executablePath}" 1; fi`; } // 在终端中执行命令 if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) { // 清空终端显示并滚动到底部 clearTerminalAndScroll(); // 发送清理命令 console.log(`[RUN] 发送清理命令:`, cleanCmd); showSaveMsg('清理旧的可执行文件...'); window.terminalWs.send(cleanCmd + '\r'); // 延迟发送编译命令 setTimeout(() => { console.log(`[RUN] 发送编译命令:`, compileCmd); showSaveMsg(`正在编译 ${currentFile}...`); window.terminalWs.send(compileCmd + '\r'); // 编译完成后运行程序 setTimeout(() => { // 检查可执行文件是否存在并直接运行或不执行 console.log(`[RUN] 发送运行命令:`, fileCheckCmd); showSaveMsg(`正在运行 ${currentFile}...`); window.terminalWs.send(fileCheckCmd + '\r'); // 发送命令后立即滚动到底部 setTimeout(() => { scrollTerminalToBottom(); }, 50); }, 1000); // 发送命令后立即滚动到底部 setTimeout(() => { scrollTerminalToBottom(); }, 50); }, 500); showSaveMsg('运行命令已发送到终端'); } else { showSaveMsg('错误:终端WebSocket连接不可用', true); } } // 运行diff测试 async function runDiffTest() { if (!currentFile) { showSaveMsg('错误:未选择文件', true); return; } // 先保存文件 await saveFile(); const ext = currentFile.split('.').pop().toLowerCase(); if (!['cpp', 'cc', 'cxx'].includes(ext)) { showSaveMsg('错误:不是C++文件', true); return; } // 检查是否是特殊文件 const fileName = currentFile.split('/').pop(); if (['validator.cpp', 'check.cpp', 'generator.cpp'].includes(fileName)) { showSaveMsg('错误:validator.cpp、check.cpp、generator.cpp 不支持diff测试', true); return; } // 获取sample文件列表 try { const sampleRes = await fetch('/api/tree?path=sample'); if (!sampleRes.ok) { showSaveMsg('错误:无法获取sample目录', true); return; } const sampleFiles = await sampleRes.json(); const inFiles = sampleFiles.filter(f => f.text.endsWith('.in')).map(f => f.text); if (inFiles.length === 0) { showSaveMsg('错误:sample目录中没有.in文件', true); return; } // 构建命令序列 const commands = []; const problemId = getCurrentProblemId(); const baseName = fileName.replace(/\.(cpp|cc|cxx)$/, ''); // 获取平台信息并选择跨平台命令 let diffCmd = 'diff -B -w'; // 默认使用diff,忽略空行和空白字符 let rmCmd = 'rm -f'; // 默认使用rm let isWindows = false; // 默认不是Windows let fileCheckCommand = 'if [ -f'; // 默认使用bash语法 try { const platformRes = await fetch('/api/platform'); if (platformRes.ok) { const platformData = await platformRes.json(); diffCmd = platformData.diffCommand; // 已经包含了忽略参数 rmCmd = platformData.rmCommand || 'rm -f'; isWindows = platformData.isWindows || false; fileCheckCommand = platformData.fileCheckCommand || 'if [ -f'; console.log(`[DIFF] 平台信息:`, platformData); console.log(`[DIFF] 使用忽略空白和空行的diff命令:`, diffCmd); } } catch (err) { console.warn('无法获取平台信息,使用默认命令'); } console.log(`[DIFF] 最终使用的命令:`, { diffCmd, rmCmd, fileCheckCommand }); // 构建清理、编译和检查命令 const executablePath = `${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}`; const cleanCmd = `${rmCmd} "${executablePath}"`; const compileCmd = `g++ -O2 -std=c++14 -o "${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" "${problemId}/${currentFile}"`; const mkdirCmd = `mkdir -p ${problemId}/outputs/${baseName}`; // 先添加清理命令 commands.push(cleanCmd); console.log(`[DIFF] 清理命令:`, cleanCmd); // 再添加编译命令 commands.push(compileCmd); console.log(`[DIFF] 编译命令:`, compileCmd); // 添加跨平台的编译结果检查命令(无echo输出) let checkCompileCmd; if (isWindows) { // Windows平台使用cmd语法,无echo输出 checkCompileCmd = `if exist "${executablePath}" (echo.) else (echo.)`; } else { // Unix/Linux/macOS平台使用bash语法,无echo输出 checkCompileCmd = `if [ -f "${executablePath}" ]; then true; else true; fi`; } commands.push(checkCompileCmd); console.log(`[DIFF] 编译检查命令:`, checkCompileCmd); // 构建所有sample的测试命令 const testCommands = []; for (const inFile of inFiles) { const ansFile = inFile.replace('.in', '.ans'); const outputFile = inFile.replace('.in', '.out'); // 使用平台特定的比较命令,已包含忽略空白和空行参数 const diffCmdFinal = `${diffCmd} "${problemId}/outputs/${baseName}/${outputFile}" "${problemId}/sample/${ansFile}"`; // 单个sample的测试命令 const singleTestCmd = `"${problemId}/${currentFile.replace(/\.(cpp|cc|cxx)$/, '')}" < "${problemId}/sample/${inFile}" > "${problemId}/outputs/${baseName}/${outputFile}" && ${diffCmdFinal}`; testCommands.push(singleTestCmd); console.log(`[DIFF] 构建测试命令 ${testCommands.length}:`, { inFile, ansFile, outputFile, diffCmd, diffCmdFinal, singleTestCmd }); } // 添加mkdir命令 commands.push(mkdirCmd); console.log(`[DIFF] 创建目录命令:`, mkdirCmd); // 添加所有测试命令 commands.push(...testCommands); console.log(`[DIFF] 总共 ${commands.length} 个命令:`, commands); // 在终端中执行命令 if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) { // 清空终端显示并滚动到底部 if (window.term) { window.term.clear(); // 确保滚动到底部 setTimeout(() => { window.term.scrollToBottom(); }, 100); } // 逐个发送命令到终端 for (let i = 0; i < commands.length; i++) { const cmd = commands[i]; console.log(`[DIFF] 发送第 ${i + 1}/${commands.length} 个命令:`, cmd); // 检查命令类型并显示相应提示 if (cmd.includes('rm -f')) { showSaveMsg('清理旧的可执行文件...'); } else if (cmd.includes('g++') && cmd.includes('-o')) { showSaveMsg(`正在编译 ${currentFile}...`); } else if (cmd.includes('if [ -f')) { showSaveMsg('检查编译结果...'); } else if (cmd.includes('mkdir')) { showSaveMsg('创建输出目录...'); } else { const sampleMatch = cmd.match(/sample(\d+)\.in/); if (sampleMatch) { showSaveMsg(`测试 sample${sampleMatch[1]}...`); } else { showSaveMsg(`执行命令 ${i + 1}/${commands.length}...`); } } window.terminalWs.send(cmd + '\r'); // 发送命令后立即滚动到底部 setTimeout(() => { scrollTerminalToBottom(); }, 50); // 等待一段时间再发送下一个命令 if (i < commands.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); // 增加等待时间,让用户看到编译结果 } } showSaveMsg('所有diff测试命令已发送到终端'); } else { showSaveMsg('错误:终端WebSocket连接不可用', true); } } catch (err) { showSaveMsg('diff测试失败: ' + err.message, true); } } // 监听textarea内容变化 mdTextarea.addEventListener('input', function () { isMdDirty = mdTextarea.value !== lastSavedContent; updateCurrentFileDisplay(); updatePreviewFromMdTextarea(mdTextarea.value); }); // Ctrl+S保存 mdTextarea.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveFile(); } }); // 粘贴图片 mdTextarea.addEventListener('paste', async function (event) { const items = event.clipboardData && event.clipboardData.items; if (!items) return; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.kind === 'file' && item.type.startsWith('image/')) { event.preventDefault(); const file = item.getAsFile(); const ext = file.type.split('/')[1] || 'png'; const rand = Array.from({ length: 16 }, () => Math.random().toString(36)[2]).join(''); const filename = rand + '.' + ext; let problemId = getCurrentProblemId && getCurrentProblemId(); if (!problemId) { alert('无法识别题目ID,图片粘贴失败'); return; } const formData = new FormData(); formData.append('file', file, filename); formData.append('filename', filename); formData.append('problemId', problemId); try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); if (data && data.relPath) { // 在光标处插入图片语法 const start = mdTextarea.selectionStart; const end = mdTextarea.selectionEnd; const insertText = `![粘贴图片](file://additional_file/${filename})`; mdTextarea.value = mdTextarea.value.slice(0, start) + insertText + mdTextarea.value.slice(end); mdTextarea.selectionStart = mdTextarea.selectionEnd = start + insertText.length; isMdDirty = true; updateCurrentFileDisplay(); updatePreviewFromMdTextarea(mdTextarea.value); } else { alert('图片上传失败'); } } catch (e) { alert('图片上传异常: ' + e.message); } } } }); // 保存文件内容 async function saveFile() { if (!currentFile) return showSaveMsg('未选择文件', true); let content = ''; if (currentIsMarkdown) { content = mdTextarea.value; } else { content = editor.getValue(); } const res = await fetch('/api/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentFile, content }) }); if (res.ok) { lastSavedContent = content; isDirty = false; isMdDirty = false; updateCurrentFileDisplay(); showSaveMsg('已保存'); } else showSaveMsg('保存失败', true); } function updateCurrentFileDisplay() { const el = document.getElementById('current-file'); el.textContent = currentFile ? ((isDirty || isMdDirty) ? currentFile + ' *' : currentFile) : ''; } window.updateCurrentFileDisplay = updateCurrentFileDisplay; // 重载文件内容 function reloadFile() { if (currentFile) loadFile(currentFile); } // Ctrl+S 保存 window.addEventListener('keydown', function (e) { // 如果当前是Markdown编辑模式,不在此处处理保存,因为mdTextarea上已经有监听器 if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); // 避免在Markdown编辑模式下重复触发保存 if (!currentIsMarkdown) { saveFile(); } } }); }); }); // 自动获取题目ID(取文件树根节点或目录名) function getCurrentProblemId() { // 尝试从文件树根节点获取 const tree = document.querySelector('#file-tree .jstree-anchor'); if (tree) return tree.textContent.trim(); // 或从当前目录推断 if (window.location.pathname && window.location.pathname.length > 1) { return window.location.pathname.split('/')[1]; } return ''; } // 编辑器和预览区自适应高度,终端始终显示 function resizeLayout() { // 使用全局的syncMainHeight函数来确保所有区域都正确同步 if (window.syncMainHeight) { window.syncMainHeight(); } } window.addEventListener('resize', resizeLayout); window.addEventListener('DOMContentLoaded', resizeLayout); // 文件树显示/隐藏按钮逻辑 (function () { const btn = document.getElementById('toggle-tree-btn'); const fileTreeWrap = document.getElementById('file-tree-wrap'); const ideMain = document.getElementById('ide-main'); let treeVisible = true; function updateBtn() { btn.title = treeVisible ? '隐藏文件树' : '显示文件树'; btn.innerHTML = treeVisible ? '<svg class="tree-arrow-svg" width="12" height="12" viewBox="0 0 18 18" style="display:block;transition:transform 0.08s;"><polyline points="12,4 6,9 12,14" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' : '<svg class="tree-arrow-svg" width="12" height="12" viewBox="0 0 18 18" style="display:block;transition:transform 0.08s;"><polyline points="6,4 12,9 6,14" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; btn.style.background = treeVisible ? 'transparent' : '#e0e7ef'; } btn.onclick = function (e) { e.stopPropagation(); treeVisible = !treeVisible; if (treeVisible) { fileTreeWrap.classList.remove('hide-anim'); fileTreeWrap.classList.add('show-anim'); fileTreeWrap.style.display = ''; setTimeout(() => { fileTreeWrap.style.width = ''; ideMain.style.width = ''; }, 220); } else { fileTreeWrap.classList.remove('show-anim'); fileTreeWrap.classList.add('hide-anim'); setTimeout(() => { fileTreeWrap.style.display = 'none'; ideMain.style.width = '100%'; }, 220); } updateBtn(); if (window.editor && window.editor.layout) window.editor.layout(); }; updateBtn(); })(); // 文件树出现/隐藏动画 const treeAnimStyle = document.createElement('style'); treeAnimStyle.innerHTML = ` #file-tree-wrap { transition: width 0.22s cubic-bezier(.4,1.6,.4,1), opacity 0.18s; will-change: width, opacity; overflow: hidden; } #file-tree-wrap.hide-anim { width: 0 !important; min-width: 0 !important; opacity: 0; pointer-events: none; } #file-tree-wrap.show-anim { opacity: 1; } `; document.head.appendChild(treeAnimStyle); // 按钮动画和 hover 效果 const style = document.createElement('style'); style.innerHTML = ` #toggle-tree-btn { transition: background 0.08s, box-shadow 0.08s, border-radius 0.08s, border-color 0.08s; border-radius: 7px; border: 1.5px solid transparent; } #toggle-tree-btn:hover { background: #e0e7ef !important; box-shadow: 0 2px 8px #bae6fd; border-color: #60a5fa; } #toggle-tree-btn:hover .tree-arrow-svg { transition: transform 0.08s; transform: scale(1.25); filter: drop-shadow(0 0 2px #60a5fa); } #toggle-tree-btn:active .tree-arrow-svg { transition: transform 0.08s; transform: scale(0.95); } `; document.head.appendChild(style); // 新建文件/文件夹按钮 hover 效果和 tooltip style.innerHTML += ` #btn-new-file:hover, #btn-new-folder:hover { background: #e0e7ef !important; box-shadow: 0 2px 8px #bae6fd; border-color: #60a5fa; } #btn-new-file:active svg, #btn-new-folder:active svg { transform: scale(0.95); } `; addBtnTooltip(document.getElementById('btn-new-file'), '新建文件'); addBtnTooltip(document.getElementById('btn-new-folder'), '新建文件夹'); // 顶部保存/重载按钮 hover 效果和 tooltip style.innerHTML += ` #btn-save:hover, #btn-reload:hover { background: #e0e7ef !important; box-shadow: 0 2px 8px #bae6fd; border-color: #60a5fa; } #btn-save:active svg, #btn-reload:active svg { transform: scale(0.95); } pre code.language-text, pre code.language-plaintext, pre code.language-txt { background: #f8fafc !important; /* 你喜欢的背景色 */ color: #334155 !important; /* 你喜欢的字体颜色 */ border-radius: 8px; /* 圆角大小 */ font-size: 15px; /* 字号 */ padding: 0.7em 1em; /* 内边距 */ font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace; border: none; /* 去掉边框 */ box-shadow: 0 2px 8px rgba(0,0,0,0.04); /* 可选:阴影 */ line-height: 1.7; /* 你可以继续加其它自定义属性 */ } `; addBtnTooltip(document.getElementById('btn-save'), '保存 (Ctrl+S)'); addBtnTooltip(document.getElementById('btn-reload'), '重载'); document.addEventListener('DOMContentLoaded', function () { if (window.saveFile && window.reloadFile) { document.getElementById('btn-save').onclick = saveFile; document.getElementById('btn-reload').onclick = reloadFile; } }); // 文件树和editor之间的拖动条 (function () { const dragbar = document.getElementById('tree-dragbar'); const fileTree = document.getElementById('file-tree'); const editorDiv = document.getElementById('editor'); const ideMain = document.getElementById('ide-main'); let dragging = false; let lastUserSelect = ''; dragbar.addEventListener('mousedown', function (e) { dragging = true; document.body.style.cursor = 'col-resize'; lastUserSelect = document.body.style.userSelect; document.body.style.userSelect = 'none'; e.preventDefault(); }); window.addEventListener('mousemove', function (e) { if (!dragging) return; const mainRect = ideMain.getBoundingClientRect(); let leftWidth = e.clientX - mainRect.left; // 限制最小/最大宽度 leftWidth = Math.max(120, Math.min(leftWidth, mainRect.width - 120)); fileTree.style.width = leftWidth + 'px'; editorDiv.style.width = (mainRect.width - leftWidth - dragbar.offsetWidth) + 'px'; if (window.editor && window.editor.layout) window.editor.layout(); }); window.addEventListener('mouseup', function (e) { if (dragging) { dragging = false; document.body.style.cursor = ''; document.body.style.userSelect = lastUserSelect; } }); })(); // 终端窗口上方拖动条,调整主内容区和终端高度 (function () { const dragbar = document.getElementById('terminal-dragbar'); const terminal = document.getElementById('terminal'); let dragging = false; let lastUserSelect = ''; dragbar.addEventListener('mousedown', function (e) { dragging = true; document.body.style.cursor = 'row-resize'; lastUserSelect = document.body.style.userSelect; document.body.style.userSelect = 'none'; e.preventDefault(); }); window.addEventListener('mousemove', function (e) { if (!dragging) return; const topbar = document.getElementById('topbar'); const workflowBar = document.getElementById('workflow-bar'); const previewMain = document.getElementById('ide-preview-main'); const ideMain = document.getElementById('ide-main'); const fileTree = document.getElementById('file-tree'); const editorDiv = document.getElementById('editor'); const previewDiv = document.getElementById('preview'); const totalHeight = window.innerHeight - topbar.offsetHeight - (workflowBar ? workflowBar.offsetHeight : 0); const minTerm = 200, minMain = 80; let newTermHeight = totalHeight - (e.clientY - topbar.getBoundingClientRect().bottom); newTermHeight = Math.max(minTerm, Math.min(newTermHeight, totalHeight - minMain)); // 设置终端高度 terminal.style.height = newTermHeight + 'px'; // 计算主区域高度 const mainHeight = totalHeight - newTermHeight; // 直接设置所有主区域的高度,确保实时同步 if (previewMain) previewMain.style.height = mainHeight + 'px'; if (ideMain) ideMain.style.height = mainHeight + 'px'; if (fileTree) fileTree.style.height = mainHeight + 'px'; if (editorDiv) editorDiv.style.height = mainHeight + 'px'; if (previewDiv) previewDiv.style.height = mainHeight + 'px'; // 同步编辑器布局 if (window.editor && window.editor.layout) { window.editor.layout(); } if (window.fitAddon) window.fitAddon.fit(); }); window.addEventListener('mouseup', function (e) { if (dragging) { dragging = false; document.body.style.cursor = ''; document.body.style.userSelect = lastUserSelect; // 最终同步一次,确保所有区域都正确 if (window.syncMainHeight) { window.syncMainHeight(); } if (window.fitAddon) window.fitAddon.fit(); } }); })(); // 让文件树和editor随窗口大小自适应 function syncTreeEditorHeight() { // 使用全局的syncMainHeight函数来确保所有区域都正确同步 if (window.syncMainHeight) { window.syncMainHeight(); } } window.addEventListener('resize', syncTreeEditorHeight); document.addEventListener('DOMContentLoaded', syncTreeEditorHeight); // 其它地方如终端拖动、最大化/还原等也应调用 syncTreeEditorHeight // 在相关函数中已调用 syncMainHeight(),可在其中加 syncTreeEditorHeight(); // 这里补一份定时器兜底 setInterval(syncTreeEditorHeight, 1000);