基于Ace Editor+Pyodide的Python编辑器:实现目录展示与标签页
实现Pyodide编辑器的文件目录侧边栏和顶部标签页功能
一、左侧展示当前目录文件列表
1. 修改HTML结构
添加侧边栏容器,调整整体布局适配新结构:
<!DOCTYPE html> <html lang="en"> <head> <title>Pyodide in Action</title> <link rel="stylesheet" href="styles.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ace.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ext-static_highlight.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ext-modelist.js"></script> <script src="https://unpkg.com/file-saver"></script> <script src="pyodide.js"></script> </head> <body> <!-- 侧边栏:目录文件列表 --> <div id="file-sidebar"> <h3>文件目录</h3> <ul id="file-list"></ul> </div> <!-- 顶部标签栏 --> <div id="tab-bar"> <div id="tab-container"></div> </div> <div class="container"> <div class="editor-wrapper"> <pre id="editor"></pre> </div> </div> <button id="run_code">运行代码</button><br> <br> <textarea rows=10 cols=30 id=output style="margin: 0px; width: calc(100% - 220px); height: 461px;">Loading and compiling...</textarea><br> <script type="text/javascript" src="challenge-editor.js"></script> </body> </html>
2. 调整CSS样式
添加侧边栏、标签栏样式,修改原有布局适配新结构:
/* 侧边栏样式 */ #file-sidebar { position: fixed; left: 0; top: 0; width: 200px; height: 100vh; background-color: #f5f5f5; border-right: 1px solid #ddd; padding: 15px; overflow-y: auto; } #file-sidebar h3 { margin-top: 0; font-size: 16px; border-bottom: 1px solid #ddd; padding-bottom: 8px; } #file-list { list-style: none; padding: 0; margin: 0; } #file-list li { padding: 8px 5px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; } #file-list li:hover { background-color: #e0e0e0; } /* 顶部标签栏样式 */ #tab-bar { position: fixed; left: 200px; top: 0; right: 0; height: 40px; background-color: #f5f5f5; border-bottom: 1px solid #ddd; display: flex; align-items: center; padding: 0 10px; overflow-x: auto; } #tab-container { display: flex; gap: 4px; } .tab-item { background-color: #e0e0e0; padding: 8px 15px; border-radius: 4px 4px 0 0; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 14px; } .tab-item.active { background-color: #fff; border-bottom: 1px solid #fff; } .tab-close { font-size: 12px; color: #666; } .tab-close:hover { color: #000; } /* 主内容区调整 */ .container { margin-left: 200px; margin-top: 40px; width: calc(100% - 220px); height: calc(100vh - 120px); padding: 10px; } .editor-wrapper { width: 100%; height: 100%; position: relative; } #editor { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; } #run_code { margin-left: 200px; border-radius: 4px; padding: 10px 20px; background-color: #408ddf; color: white; font-size: 16px; border: none; cursor: pointer; } #output { margin-left: 200px; }
3. 扩展JavaScript功能
添加目录加载、标签页管理逻辑:
const fileSaver = require('file-saver'); var editor = ace.edit("editor"); var output_pane; // 维护当前打开的文件状态 var openFiles = {}; var activeFilePath = '/_static/test_files/main.py'; languagePluginLoader.then(() => { appendOutput('Python ready.\n'); }); function appendOutput(msg) { output_pane.value = output_pane.value + '\n' + msg; output_pane.scrollTop = output_pane.scrollHeight; } function configEditor(){ editor = ace.edit("editor"); editor.setTheme("ace/theme/xcode"); editor.session.setMode("ace/mode/python"); editor.setShowPrintMargin(false); editor.setBehavioursEnabled(true); editor.setFontSize(13); // 先加载目录,再打开初始文件 loadDirectory(getDirPath(activeFilePath)); openCode(activeFilePath); } // 解析文件路径的目录部分 function getDirPath(filePath) { return filePath.substring(0, filePath.lastIndexOf('/')); } // 加载指定目录下的文件列表 async function loadDirectory(dirPath) { try { // 若使用Pyodide虚拟文件系统,可替换为pyodide.FS.readdir(dirPath) const response = await fetch(`${dirPath}/?list`); const files = await response.json(); const fileListEl = document.getElementById('file-list'); fileListEl.innerHTML = ''; files.forEach(file => { if (file.endsWith('.py')) { const li = document.createElement('li'); li.textContent = file; li.addEventListener('click', () => { openCode(`${dirPath}/${file}`); }); fileListEl.appendChild(li); } }); } catch (error) { console.error('加载目录失败:', error); appendOutput('加载目录失败,请检查目录路径配置'); } } function openCode(filePath) { // 如果文件已打开,直接切换标签 if (openFiles[filePath]) { switchToTab(filePath); return; } fetch(filePath) .then(response => response.text()) .then(code => { var modelist = ace.require("ace/ext/modelist"); var modeName = modelist.getModeForPath(filePath).mode; // 保存文件状态 openFiles[filePath] = { code: code, mode: modeName, fileName: filePath.substring(filePath.lastIndexOf('/') + 1) }; // 创建标签页 createTab(filePath); // 切换到该标签 switchToTab(filePath); }) .catch(error => { console.error('打开文件失败:', error); appendOutput(`打开文件${filePath}失败`); }); } // 创建标签页元素 function createTab(filePath) { const tabContainer = document.getElementById('tab-container'); const tabItem = document.createElement('div'); tabItem.className = 'tab-item'; tabItem.dataset.path = filePath; const fileNameEl = document.createElement('span'); fileNameEl.textContent = openFiles[filePath].fileName; const closeBtn = document.createElement('span'); closeBtn.className = 'tab-close'; closeBtn.textContent = '×'; closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeTab(filePath); }); tabItem.appendChild(fileNameEl); tabItem.appendChild(closeBtn); tabItem.addEventListener('click', () => { switchToTab(filePath); }); tabContainer.appendChild(tabItem); } // 切换到指定标签页 function switchToTab(filePath) { // 更新标签激活状态 document.querySelectorAll('.tab-item').forEach(tab => { tab.classList.remove('active'); if (tab.dataset.path === filePath) { tab.classList.add('active'); } }); // 更新编辑器内容 const file = openFiles[filePath]; editor.session.setMode(file.mode); editor.session.setValue(file.code); activeFilePath = filePath; // 监听编辑器内容变化,实时更新文件状态 editor.session.on('change', () => { openFiles[filePath].code = editor.getValue(); }); } // 关闭标签页 function closeTab(filePath) { // 如果是最后一个标签,不允许关闭 if (Object.keys(openFiles).length <= 1) { appendOutput('至少保留一个打开的文件'); return; } // 移除标签元素 document.querySelector(`.tab-item[data-path="${filePath}"]`).remove(); // 删除文件状态 delete openFiles[filePath]; // 切换到第一个打开的文件 const newActivePath = Object.keys(openFiles)[0]; switchToTab(newActivePath); } async function runCode(code_to_run) { console.logs = []; try { await window.pyodide.runPython(code_to_run); appendOutput(console.logs.join('\n')); save_code(code_to_run); } catch (err) { alert("Error: " + err); } } function save_code(code) { var blob = new Blob([code], { type: "text/plain;charset=utf-8" }); window.saveAs(blob, activeFilePath); } document.addEventListener('DOMContentLoaded', (event) => { output_pane = document.getElementById("output"); document.getElementById("run_code").addEventListener('click', function () { runCode(editor.getValue()); }); console.stdlog = console.log.bind(console); console.logs = []; console.log = function(){ console.logs.push(Array.from(arguments).join(' ')); console.stdlog.apply(console, arguments); } configEditor(); });
关键说明
- 目录加载:若使用Pyodide虚拟文件系统,可将
loadDirectory中的fetch逻辑替换为pyodide.FS.readdir(dirPath),无需后端接口支持。 - 内容同步:编辑器内容变化时会实时更新
openFiles中的存储,切换标签时自动同步最新内容。
二、顶部文件标签页功能说明
上述代码已完整实现VS Code风格的标签页功能,核心逻辑:
- 通过
openFiles对象存储所有打开文件的内容、语法模式和文件名 - 打开新文件时自动创建对应标签,点击标签切换编辑器内容
- 点击标签关闭按钮可移除标签,自动切换到其他打开的文件
- 激活标签会高亮显示,区分当前编辑文件
内容的提问来源于stack exchange,提问作者Cr3
相关产品推荐
相关产品推荐

