Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CodeSketch</title> | |
| <!-- Split.js for Resizable Layout --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.0/split.min.js"></script> | |
| <!-- JSON Formatter for Console --> | |
| <script src="https://cdn.jsdelivr.net/npm/json-formatter-js@2.3.4/dist/json-formatter.umd.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #1e1e1e; | |
| --sidebar-bg: #252526; | |
| --sidebar-hover: #2a2d2e; | |
| --sidebar-active: #37373d; | |
| --border-color: #333; | |
| --text-color: #cccccc; | |
| --accent-color: #007acc; | |
| --console-bg: #1e1e1e; | |
| --console-text: #e0e0e0; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| header { | |
| height: 50px; | |
| background-color: #333333; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| justify-content: space-between; | |
| flex-shrink: 0; /* Prevent shrinking */ | |
| } | |
| h1 { | |
| font-size: 18px; | |
| font-weight: 500; | |
| color: #fff; | |
| } | |
| .run-btn { | |
| background-color: #0e639c; | |
| color: white; | |
| border: none; | |
| padding: 6px 16px; | |
| border-radius: 2px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .run-btn:hover { | |
| background-color: #1177bb; | |
| } | |
| /* Split.js Layout */ | |
| .main-container { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| height: calc(100vh - 50px); | |
| } | |
| /* Gutter Styles for Split.js */ | |
| .gutter { | |
| background-color: var(--bg-color); | |
| background-repeat: no-repeat; | |
| background-position: 50%; | |
| cursor: col-resize; | |
| } | |
| .gutter:hover { | |
| background-color: var(--accent-color); | |
| } | |
| /* Left Sidebar: Problem List */ | |
| .sidebar { | |
| background-color: var(--sidebar-bg); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| /* width is handled by Split.js */ | |
| } | |
| .sidebar-header { | |
| padding: 10px 15px; | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| font-weight: 600; | |
| color: #888; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .add-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| cursor: pointer; | |
| font-size: 18px; | |
| line-height: 1; | |
| } | |
| .add-btn:hover { color: #fff; } | |
| .problem-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| list-style: none; | |
| } | |
| .problem-item { | |
| padding: 8px 15px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| border-left: 3px solid transparent; | |
| display: flex; | |
| align-items: center; | |
| position: relative; /* For absolute positioning of actions */ | |
| height: 32px; /* Fixed height for consistency */ | |
| } | |
| .problem-title { | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| flex: 1; | |
| margin-right: 5px; /* Spacing from potential actions */ | |
| } | |
| .item-actions { | |
| position: absolute; | |
| right: 5px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| display: flex; | |
| gap: 4px; | |
| opacity: 0; /* Hidden by default */ | |
| transition: opacity 0.2s ease; | |
| background-color: var(--sidebar-bg); /* Cover text behind */ | |
| padding-left: 8px; /* Fade effect spacer if using gradient, here just solid bg */ | |
| box-shadow: -10px 0 10px -5px var(--sidebar-bg); /* Soft edge */ | |
| } | |
| .problem-item:hover .item-actions { | |
| opacity: 1; | |
| } | |
| .problem-item.active .item-actions { | |
| background-color: var(--sidebar-active); /* Match active bg */ | |
| box-shadow: -10px 0 10px -5px var(--sidebar-active); | |
| } | |
| .problem-item:hover .item-actions { | |
| background-color: var(--sidebar-hover); | |
| box-shadow: -10px 0 10px -5px var(--sidebar-hover); | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| cursor: pointer; | |
| padding: 4px; | |
| border-radius: 3px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 24px; | |
| height: 24px; | |
| } | |
| .action-btn:hover { | |
| color: #e0e0e0; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .action-btn svg { | |
| width: 14px; | |
| height: 14px; | |
| fill: currentColor; | |
| } | |
| .problem-item.active { | |
| background-color: var(--sidebar-active); | |
| border-left-color: var(--accent-color); | |
| color: #fff; | |
| } | |
| /* Center: Editor */ | |
| .editor-container { | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #monaco-editor { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Right: Console Output */ | |
| .output-panel { | |
| background-color: var(--console-bg); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .output-header { | |
| padding: 10px 15px; | |
| background-color: #252526; | |
| border-bottom: 1px solid var(--border-color); | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| font-weight: 600; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .clear-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .clear-btn:hover { color: #fff; } | |
| #console-output { | |
| flex: 1; | |
| padding: 10px; | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| font-size: 13px; | |
| overflow-y: auto; | |
| /* white-space: pre-wrap; Removed to allow JSONFormatter to handle layout */ | |
| color: var(--console-text); | |
| } | |
| .log-line { | |
| margin-bottom: 4px; | |
| border-bottom: 1px solid #2a2a2a; | |
| padding-bottom: 2px; | |
| display: flex; /* For alignment */ | |
| align-items: flex-start; | |
| } | |
| .log-error { color: #f48771; background-color: rgba(255, 0, 0, 0.1); } | |
| .log-warn { color: #cca700; background-color: rgba(255, 255, 0, 0.1); } | |
| .log-info { color: #e0e0e0; } | |
| /* JSON Formatter Tweaks for Dark Mode */ | |
| .json-formatter-row { | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| } | |
| .json-formatter-row .json-formatter-key { | |
| color: #9cdcfe ; | |
| } | |
| .json-formatter-row .json-formatter-string { | |
| color: #ce9178 ; | |
| } | |
| .json-formatter-row .json-formatter-number { | |
| color: #b5cea8 ; | |
| } | |
| .json-formatter-row .json-formatter-boolean { | |
| color: #569cd6 ; | |
| } | |
| .json-formatter-row .json-formatter-null { | |
| color: #569cd6 ; | |
| } | |
| .log-content { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| width: 100%; | |
| } | |
| /* Loading Overlay */ | |
| .loading { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100%; | |
| color: #888; | |
| } | |
| /* Category Group Styles */ | |
| .category-group { | |
| margin-bottom: 5px; | |
| } | |
| .category-header { | |
| padding: 0 15px; /* Taller */ | |
| font-size: 12px; | |
| font-weight: bold; | |
| color: #aaa; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; /* Push add button to right */ | |
| user-select: none; | |
| text-transform: uppercase; | |
| border-bottom: 1px solid rgba(255,255,255,0.05); /* Subtle separator */ | |
| } | |
| .category-header:hover { | |
| color: #fff; | |
| background-color: rgba(255,255,255,0.05); | |
| } | |
| .category-header svg { | |
| width: 14px; /* Slightly larger */ | |
| height: 14px; | |
| margin-right: 8px; | |
| transition: transform 0.2s; | |
| fill: currentColor; | |
| } | |
| .category-header .add-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| cursor: pointer; | |
| font-size: 20px; /* Larger */ | |
| width: 24px; | |
| height: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| line-height: 1; | |
| padding: 0; | |
| margin-left: 10px; | |
| } | |
| .category-header .add-btn:hover { | |
| color: #fff; | |
| background-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .category-header.collapsed svg { | |
| transform: rotate(-90deg); | |
| } | |
| .category-list { | |
| list-style: none; | |
| padding-left: 0; | |
| display: block; | |
| } | |
| .category-list.collapsed { | |
| display: none; | |
| } | |
| /* Pinned Item Styles */ | |
| .pinned-icon { | |
| color: #d7ba7d; /* Gold-ish for pinned */ | |
| margin-right: 5px; | |
| width: 12px; | |
| height: 12px; | |
| fill: currentColor; | |
| display: inline-block; | |
| vertical-align: middle; | |
| } | |
| .action-btn.pinned { | |
| color: #d7ba7d; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>CodeSketch</h1> | |
| <button class="run-btn" onclick="runCode()"> | |
| <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M4 2.5L13.5 8L4 13.5V2.5Z"/> | |
| </svg> | |
| 运行代码 (Run) | |
| </button> | |
| </header> | |
| <div class="main-container"> | |
| <div id="split-0" class="sidebar"> | |
| <div class="sidebar-header"> | |
| 题目列表 | |
| </div> | |
| <ul class="problem-list" id="problem-list"> | |
| <!-- Items will be injected here --> | |
| <div class="loading">Loading...</div> | |
| </ul> | |
| </div> | |
| <div id="split-1" class="editor-container"> | |
| <div id="monaco-editor"></div> | |
| </div> | |
| <div id="split-2" class="output-panel"> | |
| <div class="output-header"> | |
| <span>Console</span> | |
| <button class="clear-btn" onclick="clearConsole()">Clear</button> | |
| </div> | |
| <div id="console-output"> | |
| <div class="log-info"> | |
| <span class="log-content">Click 'Run' to see output...</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monaco Editor Loader --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script> | |
| <script> | |
| let editor; | |
| let currentProblemId = null; | |
| let autoSaveTimer = null; | |
| let isInternalUpdate = false; | |
| // Initialize Split.js | |
| Split(['#split-0', '#split-1', '#split-2'], { | |
| sizes: [20, 50, 30], // Initial percentages | |
| minSize: [150, 200, 200], | |
| gutterSize: 6, | |
| cursor: 'col-resize', | |
| onDragEnd: function() { | |
| if (editor) editor.layout(); // Resize editor when layout changes | |
| } | |
| }); | |
| require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }}); | |
| require(['vs/editor/editor.main'], function() { | |
| // Enhance JS/TS IntelliSense | |
| monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ | |
| target: monaco.languages.typescript.ScriptTarget.ES2020, | |
| allowNonTsExtensions: true, | |
| moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, | |
| module: monaco.languages.typescript.ModuleKind.CommonJS, | |
| noEmit: true, | |
| // Enable type checking for better suggestions | |
| checkJs: true | |
| }); | |
| // Add some basic lib definitions if needed (optional) | |
| // monaco.languages.typescript.javascriptDefaults.addExtraLib(...) | |
| editor = monaco.editor.create(document.getElementById('monaco-editor'), { | |
| value: '// Select a problem to start coding...', | |
| language: 'javascript', | |
| theme: 'vs-dark', | |
| automaticLayout: true, // Auto resize with container | |
| minimap: { enabled: false }, | |
| fontSize: 14, | |
| scrollBeyondLastLine: false, | |
| // IntelliSense features | |
| suggest: { | |
| showWords: false // Prefer intelligent suggestions over text matches | |
| }, | |
| quickSuggestions: true, | |
| parameterHints: { enabled: true } | |
| }); | |
| // Initialize | |
| fetchProblems(); | |
| // Auto Save Listener | |
| editor.onDidChangeModelContent(() => { | |
| if (currentProblemId && !isInternalUpdate) { | |
| clearTimeout(autoSaveTimer); | |
| autoSaveTimer = setTimeout(saveProblem, 1000); | |
| } | |
| }); | |
| }); | |
| async function fetchProblems(selectId = null) { | |
| try { | |
| const res = await fetch('/api/problems'); | |
| const problems = await res.json(); | |
| renderProblemList(problems); | |
| if (problems.length > 0) { | |
| const target = selectId | |
| ? problems.find(p => p.id === selectId) | |
| : (!currentProblemId ? problems[0] : problems.find(p => p.id === currentProblemId)); | |
| if (target) loadProblem(target); | |
| else if (problems.length > 0) loadProblem(problems[0]); | |
| } | |
| } catch (e) { | |
| console.error("Failed to fetch problems:", e); | |
| } | |
| } | |
| const CATEGORIES = ['练习题', '面试题', '手撕题', '算法题']; | |
| function renderProblemList(problems) { | |
| const listEl = document.getElementById('problem-list'); | |
| listEl.innerHTML = ''; | |
| // Group problems | |
| const grouped = {}; | |
| CATEGORIES.forEach(c => grouped[c] = []); | |
| problems.forEach(p => { | |
| const targetCat = CATEGORIES.includes(p.category) ? p.category : '练习题'; | |
| grouped[targetCat].push(p); | |
| }); | |
| CATEGORIES.forEach(category => { | |
| const items = grouped[category]; | |
| // Sort: Pinned first, then by ctime desc (backend sent ctime desc) | |
| items.sort((a, b) => { | |
| if (!!a.pinned === !!b.pinned) return 0; // Stable sort for same pin status | |
| return a.pinned ? -1 : 1; | |
| }); | |
| // Create Category Group | |
| const groupDiv = document.createElement('div'); | |
| groupDiv.className = 'category-group'; | |
| // Header | |
| const header = document.createElement('div'); | |
| header.className = 'category-header'; | |
| // Header Content | |
| const headerContent = document.createElement('div'); | |
| headerContent.style.display = 'flex'; | |
| headerContent.style.alignItems = 'center'; | |
| headerContent.innerHTML = ` | |
| <svg viewBox="0 0 16 16"><path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/></svg> | |
| ${category} (${items.length}) | |
| `; | |
| header.appendChild(headerContent); | |
| // Add Button in Header | |
| const addBtn = document.createElement('button'); | |
| addBtn.className = 'add-btn'; | |
| addBtn.innerHTML = '+'; | |
| addBtn.title = '新建题目'; | |
| // Removed inline styles to let CSS take over | |
| addBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| createProblem(category); | |
| }; | |
| header.appendChild(addBtn); | |
| // List | |
| const ul = document.createElement('ul'); | |
| ul.className = 'category-list'; | |
| // Toggle Handler | |
| header.onclick = (e) => { | |
| if (e.target === addBtn) return; | |
| header.classList.toggle('collapsed'); | |
| ul.classList.toggle('collapsed'); | |
| }; | |
| groupDiv.appendChild(header); | |
| items.forEach((p, index) => { | |
| const li = document.createElement('li'); | |
| li.className = 'problem-item'; | |
| li.dataset.id = p.id; | |
| li.onclick = () => loadProblem(p); | |
| // Title Span | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.className = 'problem-title'; | |
| // Clean title: remove existing leading numbering (e.g. "1. ", "01 ", "1、") | |
| const cleanTitle = p.title.replace(/^\d+[\.\s、]+/, ''); | |
| const displayTitle = `${index + 1}. ${cleanTitle}`; | |
| // Pinned Icon in Title | |
| if (p.pinned) { | |
| titleSpan.innerHTML = `<svg class="pinned-icon" viewBox="0 0 16 16"><path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354z"/></svg>`; | |
| titleSpan.appendChild(document.createTextNode(displayTitle)); | |
| } else { | |
| titleSpan.textContent = displayTitle; | |
| } | |
| li.appendChild(titleSpan); | |
| // Actions Div | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'item-actions'; | |
| // Pin Button | |
| const pinBtn = document.createElement('button'); | |
| pinBtn.className = `action-btn ${p.pinned ? 'pinned' : ''}`; | |
| pinBtn.title = p.pinned ? '取消置顶' : '置顶'; | |
| pinBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354z"/></svg>'; | |
| pinBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| togglePin(p); | |
| }; | |
| actionsDiv.appendChild(pinBtn); | |
| // Rename Button | |
| const renameBtn = document.createElement('button'); | |
| renameBtn.className = 'action-btn'; | |
| renameBtn.title = '重命名'; | |
| // Edit Icon (Pencil) | |
| renameBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>'; | |
| renameBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| renameProblem(p.id); | |
| }; | |
| actionsDiv.appendChild(renameBtn); | |
| // Delete Button | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'action-btn'; | |
| deleteBtn.title = '删除'; | |
| // Trash Icon | |
| deleteBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>'; | |
| deleteBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| deleteProblem(p.id); | |
| }; | |
| actionsDiv.appendChild(deleteBtn); | |
| li.appendChild(actionsDiv); | |
| ul.appendChild(li); | |
| }); | |
| groupDiv.appendChild(ul); | |
| listEl.appendChild(groupDiv); | |
| }); | |
| if (currentProblemId) { | |
| const activeEl = listEl.querySelector(`.problem-item[data-id="${currentProblemId}"]`); | |
| if (activeEl) activeEl.classList.add('active'); | |
| } | |
| } | |
| async function togglePin(problem) { | |
| try { | |
| const res = await fetch(`/api/problems/${problem.id}`); | |
| const content = await res.text(); | |
| let newContent; | |
| // Check if @pinned exists | |
| const hasPinned = /@pinned\s+(true|false)/i.test(content); | |
| const isPinned = problem.pinned; | |
| if (hasPinned) { | |
| // Replace existing | |
| newContent = content.replace(/@pinned\s+(true|false)/i, `@pinned ${!isPinned}`); | |
| } else { | |
| // Add after @title or @category | |
| if (content.includes('@category')) { | |
| newContent = content.replace(/(@category\s+.+)/, `$1\n * @pinned ${!isPinned}`); | |
| } else if (content.includes('@title')) { | |
| newContent = content.replace(/(@title\s+.+)/, `$1\n * @pinned ${!isPinned}`); | |
| } else { | |
| // Just prepend | |
| newContent = `/**\n * @pinned ${!isPinned}\n */\n` + content; | |
| } | |
| } | |
| // Save | |
| const saveRes = await fetch(`/api/problems/${problem.id}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ content: newContent }) | |
| }); | |
| if (saveRes.ok) { | |
| // Refresh list | |
| await fetchProblems(currentProblemId); | |
| } | |
| } catch (e) { | |
| console.error("Toggle pin failed", e); | |
| } | |
| } | |
| async function renameProblem(id) { | |
| // Strip extension for display if you want, but keep it simple for now | |
| const currentName = id.replace(/\.(js|ts)$/, ''); | |
| const newName = prompt("请输入新文件名 (不含后缀):", currentName); | |
| if (!newName || newName === currentName) return; | |
| try { | |
| const res = await fetch(`/api/problems/${id}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ new_filename: newName }) | |
| }); | |
| if (res.ok) { | |
| const newMeta = await res.json(); | |
| // If we renamed the currently open problem, update the ID | |
| if (currentProblemId === id) { | |
| currentProblemId = newMeta.id; | |
| } | |
| await fetchProblems(currentProblemId); | |
| logToConsole(['Renamed to:', newMeta.title], 'info'); | |
| } else { | |
| const err = await res.json(); | |
| alert(`Error: ${err.error}`); | |
| } | |
| } catch (e) { | |
| alert(`Error: ${e.message}`); | |
| } | |
| } | |
| async function deleteProblem(id) { | |
| if (!confirm(`确定要删除 "${id}" 吗? 此操作无法撤销。`)) return; | |
| try { | |
| const res = await fetch(`/api/problems/${id}`, { | |
| method: 'DELETE' | |
| }); | |
| if (res.ok) { | |
| logToConsole(['Deleted:', id], 'warn'); | |
| if (currentProblemId === id) { | |
| currentProblemId = null; | |
| if (editor) editor.setValue('// Select a problem to start coding...'); | |
| } | |
| await fetchProblems(); | |
| } else { | |
| const err = await res.json(); | |
| alert(`Error: ${err.error}`); | |
| } | |
| } catch (e) { | |
| alert(`Error: ${e.message}`); | |
| } | |
| } | |
| async function loadProblem(problem) { | |
| if (currentProblemId === problem.id && editor.getValue() === problem.content) return; | |
| currentProblemId = problem.id; | |
| document.querySelectorAll('.problem-item').forEach(el => { | |
| el.classList.toggle('active', el.dataset.id === problem.id); | |
| }); | |
| if (editor) { | |
| isInternalUpdate = true; | |
| editor.setValue(problem.content); | |
| isInternalUpdate = false; | |
| } | |
| clearConsole(); | |
| logToConsole(['Loaded:', problem.title], 'info'); | |
| } | |
| async function createProblem(category = '练习题') { | |
| const filename = prompt(`[${category}] 请输入新题目文件名 (例如: 03_new_problem):`, "new_problem"); | |
| if (!filename) return; | |
| try { | |
| const res = await fetch('/api/problems', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ filename, category }) | |
| }); | |
| if (res.ok) { | |
| const newProblem = await res.json(); | |
| await fetchProblems(newProblem.id); | |
| logToConsole(['Created:', newProblem.title], 'info'); | |
| } else { | |
| const err = await res.json(); | |
| alert(`Error: ${err.error}`); | |
| } | |
| } catch (e) { | |
| alert(`Error: ${e.message}`); | |
| } | |
| } | |
| async function saveProblem() { | |
| if (!currentProblemId || !editor) return; | |
| const content = editor.getValue(); | |
| try { | |
| const res = await fetch(`/api/problems/${currentProblemId}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ content }) | |
| }); | |
| if (res.ok) { | |
| const updatedMeta = await res.json(); | |
| const listItem = document.querySelector(`.problem-item[data-id="${currentProblemId}"]`); | |
| if (listItem && listItem.textContent !== updatedMeta.title) { | |
| listItem.textContent = updatedMeta.title; | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Save failed:", e); | |
| logToConsole(["Auto-save failed"], 'error'); | |
| } | |
| } | |
| function runCode() { | |
| if (!editor) return; | |
| const code = editor.getValue(); | |
| clearConsole(); | |
| logToConsole(["Running..."], 'info'); | |
| const originalLog = console.log; | |
| const originalError = console.error; | |
| const originalWarn = console.warn; | |
| try { | |
| console.log = (...args) => { | |
| logToConsole(args, 'info'); | |
| }; | |
| console.error = (...args) => { | |
| logToConsole(args, 'error'); | |
| }; | |
| console.warn = (...args) => { | |
| logToConsole(args, 'warn'); | |
| }; | |
| // Using new Function is safer than eval for scope isolation, but we want quick results | |
| // Note: Objects printed will be live references if we don't clone them, | |
| // but for a console that's usually desired or acceptable. | |
| // If async is needed: | |
| const wrappedCode = `(async () => { \n${code}\n })();`; | |
| eval(code); | |
| } catch (e) { | |
| logToConsole([e], 'error'); | |
| } finally { | |
| console.log = originalLog; | |
| console.error = originalError; | |
| console.warn = originalWarn; | |
| } | |
| } | |
| // New Log Function supporting Objects | |
| function logToConsole(args, type = 'info') { | |
| const outputEl = document.getElementById('console-output'); | |
| const lineDiv = document.createElement('div'); | |
| lineDiv.className = `log-line log-${type}`; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'log-content'; | |
| args.forEach(arg => { | |
| if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean' || arg === null || arg === undefined) { | |
| const span = document.createElement('span'); | |
| span.textContent = String(arg); | |
| span.style.marginRight = '5px'; | |
| contentDiv.appendChild(span); | |
| } else { | |
| // Use JSONFormatter for objects/arrays | |
| try { | |
| const formatter = new JSONFormatter(arg, 1, { // 1 = open depth | |
| theme: 'dark', | |
| hoverPreviewEnabled: true | |
| }); | |
| contentDiv.appendChild(formatter.render()); | |
| } catch (e) { | |
| const span = document.createElement('span'); | |
| span.textContent = String(arg); | |
| contentDiv.appendChild(span); | |
| } | |
| } | |
| }); | |
| lineDiv.appendChild(contentDiv); | |
| outputEl.appendChild(lineDiv); | |
| outputEl.scrollTop = outputEl.scrollHeight; | |
| } | |
| function clearConsole() { | |
| const outputEl = document.getElementById('console-output'); | |
| outputEl.innerHTML = ''; | |
| } | |
| </script> | |
| </body> | |
| </html> | |