oimp
Version:
A CLI tool for generating OI problem and packages
712 lines (668 loc) • 28.1 kB
JavaScript
// 顶部消息条函数,需在所有用到它的函数之前定义
function showSaveMsg(msg, error) {
// 创建消息容器(如果不存在)
let container = document.getElementById('save-msg-container');
if (!container) {
container = document.createElement('div');
container.id = 'save-msg-container';
container.style.cssText = 'position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 3000; display: flex; flex-direction: column; align-items: center; gap: 10px;';
document.body.appendChild(container);
}
// 创建消息元素
const bar = document.createElement('div');
bar.className = 'save-msg-item';
bar.textContent = msg;
bar.style.cssText = `
background: ${error ? '#ef4444' : '#22c55e'};
color: #fff;
font-size: 15px;
padding: 9px 32px;
border-radius: 9px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.13);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s, top 0.18s;
font-weight: bold;
letter-spacing: 1px;
position: relative;
`;
container.appendChild(bar);
// 显示消息
setTimeout(() => {
bar.style.opacity = '1';
bar.style.top = '20px';
}, 10);
// 2秒后隐藏并移除消息
setTimeout(() => {
bar.style.opacity = '0';
bar.style.top = '12px';
// 动画结束后移除元素
setTimeout(() => {
if (bar.parentNode) {
bar.parentNode.removeChild(bar);
// 如果没有更多消息,移除容器
if (container.children.length === 0) {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
}
}
}, 180);
}, 2000);
}
// 命令执行与日志弹窗
function runCommandInTerminal(cmd) {
// 自动带上题目ID作为最后一个参数
const problemId = window.problemName || getCurrentProblemId();
if (!problemId) {
alert('无法自动识别题目ID');
return;
}
// 构建完整命令
const fullCommand = `oimp ${cmd} ${problemId}`;
// 在终端中显示命令
if (window.term) {
window.term.focus();
window.term.write(`${fullCommand}\r`);
}
// 通过WebSocket发送命令到后端执行
if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) {
window.terminalWs.send(fullCommand + '\r');
} else {
console.error('WebSocket连接未建立,无法执行命令');
alert('终端连接未建立,请刷新页面重试');
}
}
//< !--终端窗口最大化 / 最小化按钮-- >
(function () {
const terminalDiv = document.getElementById('terminal');
const previewMain = document.getElementById('ide-preview-main');
if (!terminalDiv || !previewMain) return;
// 按钮容器
const btnBar = document.createElement('div');
btnBar.className = 'terminal-btn-bar';
btnBar.style.position = 'absolute';
btnBar.style.right = '12px';
btnBar.style.top = '6px';
btnBar.style.zIndex = '20';
btnBar.style.display = 'flex';
btnBar.style.gap = '6px';
// 最小化按钮
const minBtn = document.createElement('button');
minBtn.textContent = '_';
minBtn.setAttribute('data-tooltip', '最小化终端');
minBtn.className = 'min-btn';
minBtn.onclick = function () {
terminalDiv.style.height = '36px';
terminalDiv.style.minHeight = '0';
terminalDiv.style.maxHeight = '36px';
previewMain.style.height = `calc(100vh - 40px - 36px)`;
if (window.syncMainHeight) window.syncMainHeight();
if (window.fitAddon) window.fitAddon.fit();
};
// 最大化按钮
const maxBtn = document.createElement('button');
maxBtn.textContent = '^';
maxBtn.setAttribute('data-tooltip', '最大化终端');
maxBtn.className = 'max-btn';
maxBtn.onclick = function () {
terminalDiv.style.height = '90vh';
terminalDiv.style.minHeight = '200px';
terminalDiv.style.maxHeight = 'none';
previewMain.style.height = 'calc(1vh - 40px)';
if (window.syncMainHeight) window.syncMainHeight();
if (window.fitAddon) window.fitAddon.fit();
};
// 还原按钮
const restoreBtn = document.createElement('button');
restoreBtn.textContent = '=';
restoreBtn.setAttribute('data-tooltip', '还原终端');
restoreBtn.className = 'restore-btn';
restoreBtn.onclick = function () {
terminalDiv.style.height = '180px';
terminalDiv.style.minHeight = '';
terminalDiv.style.maxHeight = '';
previewMain.style.height = `calc(100vh - 40px - 180px)`;
if (window.syncMainHeight) window.syncMainHeight();
if (window.fitAddon) window.fitAddon.fit();
};
// 字体缩小按钮
const fontMinusBtn = document.createElement('button');
fontMinusBtn.textContent = 'A-';
fontMinusBtn.setAttribute('data-tooltip', '减小终端字体');
fontMinusBtn.className = 'font-btn';
fontMinusBtn.onclick = function () {
if (window.term) {
let size = getTerminalFontSize();
size = Math.max(8, size - 1);
setTerminalFontSize(size);
if (window.fitAddon) window.fitAddon.fit();
}
};
// 兼容 xterm.js 4.x/5.x 的字体大小读写
function getTerminalFontSize() {
if (window.term && window.term.getOption) {
return window.term.getOption('fontSize');
} else if (window.term && window.term.options && window.term.options.fontSize) {
return window.term.options.fontSize;
} else {
return 14;
}
}
function setTerminalFontSize(size) {
if (window.term && window.term.setOption) {
window.term.setOption('fontSize', size);
} else if (window.term && window.term.options) {
window.term.options.fontSize = size;
if (window.term.refresh && window.term.rows) {
window.term.refresh(0, window.term.rows - 1);
}
}
}
// 字体放大按钮
const fontPlusBtn = document.createElement('button');
fontPlusBtn.textContent = 'A+';
fontPlusBtn.setAttribute('data-tooltip', '增大终端字体');
fontPlusBtn.className = 'font-btn';
fontPlusBtn.onclick = function () {
if (window.term) {
let size = getTerminalFontSize();
size = Math.min(40, size + 1);
setTerminalFontSize(size);
if (window.fitAddon) window.fitAddon.fit();
}
};
btnBar.appendChild(minBtn);
btnBar.appendChild(maxBtn);
btnBar.appendChild(restoreBtn);
btnBar.appendChild(fontMinusBtn);
btnBar.appendChild(fontPlusBtn);
terminalDiv.style.position = 'relative';
terminalDiv.appendChild(btnBar);
// 自定义 tooltip 逻辑
let tooltip = document.getElementById('terminal-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'terminal-tooltip';
tooltip.className = 'terminal-tooltip';
document.body.appendChild(tooltip);
}
function showTooltip(e, text) {
tooltip.textContent = text;
tooltip.style.opacity = 1;
const rect = e.target.getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2 - tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = (rect.bottom + 6) + 'px';
}
function hideTooltip() {
tooltip.style.opacity = 0;
}
[minBtn, maxBtn, restoreBtn, fontMinusBtn, fontPlusBtn].forEach(btn => {
btn.addEventListener('mouseenter', e => showTooltip(e, btn.getAttribute('data-tooltip')));
btn.addEventListener('mouseleave', hideTooltip);
});
if (terminalDiv) {
terminalDiv.addEventListener('transitionend', function (e) {
if (window.fitAddon) window.fitAddon.fit();
});
}
})();
// 新建按钮自定义 tooltip
function addBtnTooltip(btn, text) {
let tooltip = null;
btn.removeAttribute('title');
btn.addEventListener('mouseenter', function (e) {
if (tooltip) return;
tooltip = document.createElement('div');
tooltip.textContent = text;
tooltip.style.position = 'fixed';
tooltip.style.left = (e.clientX + 12) + 'px';
tooltip.style.top = (e.clientY - 8) + 'px';
tooltip.style.background = '#222';
tooltip.style.color = '#fff';
tooltip.style.fontSize = '13px';
tooltip.style.padding = '3px 10px';
tooltip.style.borderRadius = '6px';
tooltip.style.boxShadow = '0 2px 8px rgba(0,0,0,0.18)';
tooltip.style.zIndex = 9999;
tooltip.style.pointerEvents = 'none';
document.body.appendChild(tooltip);
});
btn.addEventListener('mousemove', function (e) {
if (tooltip) {
tooltip.style.left = (e.clientX + 12) + 'px';
tooltip.style.top = (e.clientY - 8) + 'px';
}
});
btn.addEventListener('mouseleave', function () {
if (tooltip) { document.body.removeChild(tooltip); tooltip = null; }
});
}
// 辅助函数:根据 path 查找节点 id
function findNodeIdByPath(tree, path) {
const all = tree.get_json('#', { flat: true });
const found = all.find(n => n.data && n.data.path === path);
return found && found.id;
}
// 辅助函数:获取节点 path 列表
function getNodePaths(tree, ids) {
return ids.map(id => {
const node = tree.get_node(id, false);
return node && node.data && node.data.path;
}).filter(Boolean);
}
// 查找和替换功能实现
function openFindWidget() {
// 使用 Monaco 内置的查找功能
if (window.editor) {
window.editor.getAction('actions.find').run();
}
}
function openReplaceWidget() {
// 使用 Monaco 内置的替换功能
if (window.editor) {
window.editor.getAction('editor.action.startFindReplaceAction').run();
}
}
// 添加键盘快捷键监听
document.addEventListener('keydown', function (e) {
// Ctrl+F 或 Cmd+F 触发查找
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
openFindWidget();
}
// Ctrl+H 或 Cmd+H 触发替换
if ((e.ctrlKey || e.metaKey) && e.key === 'h') {
e.preventDefault();
openReplaceWidget();
}
});
// 全局同步主区高度函数,考虑workflow-bar实际高度
export function syncMainHeight() {
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 topbar = document.getElementById('topbar');
const workflowBar = document.getElementById('workflow-bar');
const terminal = document.getElementById('terminal');
// 计算总高度,减去topbar和workflow-bar的实际高度
const totalHeight = window.innerHeight - (topbar ? topbar.offsetHeight : 0) - (workflowBar ? workflowBar.offsetHeight : 0);
// 终端高度
const termH = terminal ? terminal.offsetHeight : 200;
// 主区高度
const mainH = Math.max(0, totalHeight - termH);
// 同步所有主区域的高度
if (previewMain) previewMain.style.height = mainH + 'px';
if (ideMain) ideMain.style.height = mainH + 'px';
if (fileTree) fileTree.style.height = mainH + 'px';
if (editorDiv) editorDiv.style.height = mainH + 'px';
if (previewDiv) previewDiv.style.height = mainH + 'px';
// 同步编辑器布局(如果有Monaco编辑器)
if (window.editor && window.editor.layout) {
window.editor.layout();
}
}
window.syncMainHeight = syncMainHeight;
// 在文件树中选中指定文件并模拟点击
function selectFileInTree(filePath) {
// 获取jsTree实例
const $tree = $('#file-tree');
if (!$tree.length) {
console.warn('文件树未找到');
return;
}
const tree = $tree.jstree(true);
if (!tree) {
console.warn('jsTree实例未找到');
return;
}
// 去掉路径中的题目ID部分,只保留相对路径
// 例如:mahadon/problem_zh.md -> problem_zh.md
// 例如:mahadon/src/std.cpp -> src/std.cpp
const relativePath = filePath.replace(/^[^\/]+\//, '');
// 查找文件节点
const allNodes = tree.get_json('#', { flat: true });
const targetNode = allNodes.find(node => {
return node.data && node.data.path === relativePath;
});
if (targetNode) {
// 确保节点可见(展开父节点)
console.log('找到目标节点:', targetNode);
// 递归展开所有父节点
function expandParents(nodeId) {
const node = tree.get_node(nodeId);
if (node && node.parent && node.parent !== '#') {
// 先展开父节点
expandParents(node.parent);
// 然后展开当前父节点
if (tree.is_closed(node.parent)) {
tree.open_node(node.parent, null, false);
}
}
}
// 展开目标节点的所有父节点
expandParents(targetNode.id);
// 选中目标节点
tree.deselect_all();
tree.select_node(targetNode.id, false, false);
// 模拟点击节点,触发相应的逻辑(如打开文件)
tree.trigger('select_node.jstree', [targetNode.id, { originalEvent: { type: 'click' } }]);
console.log(`已选中并模拟点击文件: ${relativePath}`);
} else {
console.warn(`未找到文件: ${relativePath} (原始路径: ${filePath})`);
}
}
// 实时预览渲染函数
function updatePreviewFromEditor() {
const content = editor.getValue();
// 替换 file://additional_file/xxx 为 /files/additional_file/xxx
const fixedContent = content.replace(/file:\/\/additional_file\//g, '/files/additional_file/');
// 修正:整体渲染 markdown,避免按行分割,支持多行公式
const html = marked.parse(fixedContent);
// console.log('【调试】updatePreviewFromEditor 渲染 html:', html);
const previewDiv = document.getElementById('preview-html');
if (previewDiv) {
previewDiv.innerHTML = html;
// 确保 KaTeX 渲染
if (window.renderMathInElement) {
try {
window.renderMathInElement(previewDiv, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false }
],
throwOnError: false
});
} catch (e) { console.error('KaTeX渲染失败', e); }
} else {
setTimeout(() => {
if (window.renderMathInElement) {
try {
window.renderMathInElement(previewDiv, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false }
],
throwOnError: false
});
} catch (e) { }
}
}, 200);
}
}
}
window.updatePreviewFromEditor = updatePreviewFromEditor;
// 判断是否是表格分隔行
function isTableSeparator(line) {
// 表格分隔行包含 | 和 - 字符,例如: | --- | --- |
return /^\s*\|.*-.*\|.*$/.test(line) || /^\s*[\|\-\s:]+$/.test(line);
}
document.addEventListener('DOMContentLoaded', async function () {
// 创建并显示保存确认模态对话框
function showSaveConfirmModal() {
return new Promise((resolve) => {
// 创建模态对话框元素
const modal = document.createElement('div');
modal.id = 'save-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
// 创建对话框内容
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: #2d2d2d;
padding: 20px;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 90%;
text-align: center;
`;
// 添加标题
const title = document.createElement('h3');
title.textContent = '保存确认';
title.style.cssText = `
margin-top: 0;
color: #e0e0e0;
`;
// 添加消息文本
const message = document.createElement('p');
message.textContent = '当前文件有未保存内容,是否保存?';
message.style.cssText = `
margin: 20px 0;
color: #ccc;
`;
// 创建按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: center;
gap: 10px;
`;
// 保存按钮
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存';
saveBtn.style.cssText = `
padding: 8px 16px;
background-color: #007bff;
color: white;
border: 1px solid #0069d9;
border-radius: 4px;
cursor: pointer;
`;
saveBtn.addEventListener('click', () => {
document.body.removeChild(modal);
resolve('save');
});
// 不保存按钮
const noSaveBtn = document.createElement('button');
noSaveBtn.textContent = '不保存';
noSaveBtn.style.cssText = `
padding: 8px 16px;
background-color: #333;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
`;
noSaveBtn.addEventListener('click', () => {
document.body.removeChild(modal);
resolve('nosave');
});
// 取消按钮
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.style.cssText = `
padding: 8px 16px;
background-color: #333;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
`;
cancelBtn.addEventListener('click', () => {
document.body.removeChild(modal);
resolve('cancel');
});
// 组装对话框
modalContent.appendChild(title);
modalContent.appendChild(message);
buttonContainer.appendChild(saveBtn);
buttonContainer.appendChild(noSaveBtn);
buttonContainer.appendChild(cancelBtn);
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
// 添加到页面
document.body.appendChild(modal);
// 点击模态框背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
resolve('cancel');
}
});
});
}
window.showSaveConfirmModal = showSaveConfirmModal;
});
// 拖动分割条实现左右分栏自定义宽度
(function () {
const dragbar = document.getElementById('dragbar');
const ideMain = document.getElementById('ide-main');
const preview = document.getElementById('preview');
const idePreviewMain = document.getElementById('ide-preview-main');
const previewHtml = document.getElementById('preview-html');
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';
if (previewHtml) previewHtml.style.pointerEvents = 'none';
e.preventDefault();
});
window.addEventListener('mousemove', function (e) {
if (!dragging) return;
const totalWidth = idePreviewMain.offsetWidth;
let leftWidth = e.clientX - idePreviewMain.getBoundingClientRect().left;
// 限制最小/最大宽度
leftWidth = Math.max(200, Math.min(leftWidth, totalWidth - 200));
ideMain.style.width = leftWidth + 'px';
preview.style.width = (totalWidth - leftWidth - dragbar.offsetWidth) + 'px';
});
window.addEventListener('mouseup', function (e) {
if (dragging) {
dragging = false;
document.body.style.cursor = '';
document.body.style.userSelect = lastUserSelect;
if (previewHtml) previewHtml.style.pointerEvents = '';
}
});
})();
// ===== WebSocket 自动刷新文件树和状态面板 =====
(function () {
let ws = null;
let reconnectTimer = null;
let reconnectCount = 0;
function connectWS() {
ws = new window.WebSocket('ws://' + window.location.host);
ws.onopen = function () {
reconnectCount = 0;
};
ws.onmessage = async function (event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'tree_changed') {
// console.log('WS tree_changed arrived', data);
// 拉取最新树数据
const doRefreshTree = async (treeData) => {
const $tree = $('#file-tree');
const tree = $tree.jstree(true);
// 记录当前展开和选中节点 path
function getOpenedNodePaths(tree) {
const all = tree.get_json('#', { flat: true });
return all.filter(n => n.state && n.state.opened).map(n => n.data && n.data.path).filter(Boolean);
}
function getSelectedNodePaths(tree) {
if (typeof tree.get_selected === 'function') {
return getNodePaths(tree, tree.get_selected());
}
const all = tree.get_json('#', { flat: true });
return all.filter(n => n.state && n.state.selected).map(n => n.data && n.data.path).filter(Boolean);
}
const openedPaths = getOpenedNodePaths(tree);
const selectedPaths = getSelectedNodePaths(tree);
tree.settings.core.data = treeData;
isRefreshingTree = true;
$tree.one('refresh.jstree', function () {
const tree2 = $tree.jstree(true);
isRestoringTreeSelection = true;
// 恢复展开
openedPaths.forEach(path => {
const id = findNodeIdByPath(tree2, path);
// console.log('恢复展开', path, '->', id);
if (id) tree2.open_node(id, null, false);
});
// 恢复选中
let found = false;
for (const path of selectedPaths) {
const id = findNodeIdByPath(tree2, path);
// console.log('恢复选中', path, '->', id);
if (id) {
tree2.deselect_all();
tree2.select_node(id, false, false);
found = true;
break;
}
}
if (!found) {
const all = tree2.get_json('#', { flat: true });
const firstMd = all.find(n => n.data && n.data.type === 'file' && n.text.toLowerCase().endsWith('.md'));
if (firstMd) {
tree2.deselect_all();
tree2.select_node(firstMd.id, false, false);
loadFile(firstMd.data.path);
// console.log('未能恢复选中,自动选中第一个md文件', firstMd.data.path);
} else {
// console.log('未能恢复选中,也未找到md文件');
}
}
isRestoringTreeSelection = false;
isRefreshingTree = false;
// 如果有新的 pendingTreeData,立即再次刷新
if (pendingTreeData) {
const nextData = pendingTreeData;
pendingTreeData = null;
setTimeout(() => doRefreshTree(nextData), 0);
}
});
tree.refresh(true, true);
};
// 防抖处理
if (isRefreshingTree) {
// 正在刷新,存下最新数据
clearTimeout(debounceTreeChangedTimer);
debounceTreeChangedTimer = setTimeout(async () => {
const res = await fetch('/api/tree');
const treeData = await res.json();
pendingTreeData = treeData;
}, 1000);
} else {
clearTimeout(debounceTreeChangedTimer);
debounceTreeChangedTimer = setTimeout(async () => {
const res = await fetch('/api/tree');
const treeData = await res.json();
pendingTreeData = null;
doRefreshTree(treeData);
}, 1000);
}
return;
} else if (data.type === 'status_changed') {
fetchStatus && fetchStatus();
}
} catch (e) {
console.error("error:", e.message, e);
}
};
ws.onclose = function () {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connectWS, Math.min(5000, 1000 + 1000 * reconnectCount++));
};
ws.onerror = function () {
ws.close();
};
}
connectWS();
})();