oimp
Version:
A CLI tool for generating OI problem and packages
1,157 lines (1,019 loc) • 50.8 kB
JavaScript
// 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 = ``;
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);