UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

712 lines (668 loc) 28.1 kB
// 顶部消息条函数,需在所有用到它的函数之前定义 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(); })();