| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Monaco Editor - 多标签JS源码编辑器</title>
- <link rel="stylesheet" href="/font-awesome/css/all.min.css">
- <style>
- :root {
- --primary-color: #0078d7;
- --tab-bg: #252526;
- --tab-active-bg: #1e1e1e;
- --tab-hover-bg: #2d2d2d;
- --toolbar-bg: #333333;
- --border-color: #454545;
- --text-color: #d4d4d4;
- --danger-color: #f14c4c;
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background-color: #1e1e1e;
- color: var(--text-color);
- height: 100vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
-
- #toolbar {
- background-color: var(--toolbar-bg);
- padding: 10px 15px;
- display: flex;
- gap: 15px;
- border-bottom: 1px solid var(--border-color);
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
- z-index: 10;
- }
-
- .btn {
- background-color: #3c3c3c;
- color: var(--text-color);
- border: 1px solid var(--border-color);
- padding: 8px 15px;
- border-radius: 4px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 8px;
- transition: all 0.2s;
- }
-
- .btn:hover {
- background-color: #454545;
- }
-
- .btn-primary {
- background-color: var(--primary-color);
- border-color: var(--primary-color);
- }
-
- .btn-primary:hover {
- background-color: #0066b4;
- }
-
- .btn-danger {
- background-color: var(--danger-color);
- border-color: var(--danger-color);
- }
-
- .btn-danger:hover {
- background-color: #d93636;
- }
-
- #tabs-container {
- background-color: var(--tab-bg);
- display: flex;
- overflow-x: auto;
- padding: 0 10px;
- border-bottom: 1px solid var(--border-color);
- }
-
- .tab {
- padding: 10px 15px;
- background-color: var(--tab-bg);
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 10px;
- border-right: 1px solid var(--border-color);
- min-width: 180px;
- max-width: 250px;
- position: relative;
- }
-
- .tab:hover {
- background-color: var(--tab-hover-bg);
- }
-
- .tab.active {
- background-color: var(--tab-active-bg);
- }
-
- .tab.active::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
- background-color: var(--primary-color);
- }
-
- .tab-title {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 14px;
- }
-
- .tab-close {
- color: #888;
- padding: 2px;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .tab-close:hover {
- background-color: rgba(255, 255, 255, 0.1);
- color: #fff;
- }
-
- #editors-container {
- flex: 1;
- position: relative;
- overflow: hidden;
- }
-
- .editor-wrapper {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: none;
- }
-
- .editor-wrapper.active {
- display: block;
- }
-
- #status-bar {
- background-color: var(--toolbar-bg);
- padding: 5px 15px;
- font-size: 12px;
- display: flex;
- justify-content: space-between;
- border-top: 1px solid var(--border-color);
- }
-
- .status-item {
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .ws-status {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background-color: #888;
- }
-
- .ws-status.connected {
- background-color: #4caf50;
- }
-
- .ws-status.disconnected {
- background-color: #f44336;
- }
-
- #notification {
- position: fixed;
- bottom: 20px;
- right: 20px;
- background-color: #333;
- color: white;
- padding: 15px 20px;
- border-radius: 4px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- display: none;
- z-index: 1000;
- }
-
- #ws-console {
- position: fixed;
- bottom: 0;
- right: 0;
- width: 300px;
- height: 200px;
- background-color: rgba(0, 0, 0, 0.8);
- border-left: 1px solid #333;
- border-top: 1px solid #333;
- padding: 10px;
- font-size: 12px;
- overflow-y: auto;
- display: none;
- z-index: 100;
- }
-
- .console-title {
- margin-bottom: 8px;
- font-weight: bold;
- color: var(--primary-color);
- }
-
- .console-entry {
- margin-bottom: 5px;
- padding: 3px 5px;
- border-radius: 3px;
- background-color: rgba(255, 255, 255, 0.05);
- }
-
- .console-entry.incoming {
- color: #4caf50;
- }
-
- .console-entry.outgoing {
- color: #2196f3;
- }
-
- .console-entry.error {
- color: #f44336;
- }
- </style>
- </head>
- <body>
- <div id="toolbar">
- <button class="btn btn-primary" id="new-tab">
- <i class="fas fa-plus"></i> 新建标签页
- </button>
- <button class="btn" id="open-file">
- <i class="fas fa-folder-open"></i> 打开
- </button>
- <button class="btn" id="save-file">
- <i class="fas fa-save"></i> 保存
- </button>
- <button class="btn" id="run-code">
- <i class="fas fa-play"></i> 运行
- </button>
- <button class="btn" id="toggle-console">
- <i class="fas fa-terminal"></i> 调试控制台
- </button>
- <div style="flex: 1"></div>
- <button class="btn" id="theme-toggle">
- <i class="fas fa-moon"></i> 暗色模式
- </button>
- </div>
-
- <div id="tabs-container"></div>
-
- <div id="editors-container"></div>
-
- <div id="status-bar">
- <div class="status-item">
- <span>WebSocket:</span>
- <span class="ws-status" id="ws-status"></span>
- <span id="ws-status-text">未连接</span>
- </div>
- <div class="status-item">
- <span id="cursor-position">行: 1, 列: 1</span>
- </div>
- </div>
-
- <div id="notification"></div>
-
- <div id="ws-console">
- <div class="console-title">WebSocket 通信日志</div>
- <div id="console-content"></div>
- </div>
- <!-- Monaco Editor Loader -->
- <script src="/monaco-editor/min/vs/loader.js"></script>
- <script>
- const AppState = {
- tabs: {},
- activeTabId: null,
- nextTabId: 1,
- theme: 'vs-dark',
- wsConnected: false,
- wsConsoleVisible: false
- };
-
- // 初始化函数
- function initApp() {
- // 设置Monaco编辑器路径
- require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
-
- // 初始化Monaco环境
- require(['vs/editor/editor.main'], () => {
- // 添加初始标签页
- addNewTab('script.js', '// 欢迎使用JavaScript编辑器\nconsole.log("Hello, World!");\n\nfunction example() {\n return "This is an example function";\n}');
-
- // 初始化事件监听器
- initEventListeners();
-
- // 模拟WebSocket连接
- //simulateWebSocketConnection();
- // 真实WebSocket连接
- connectWebSocket(); // >>> ADDED
- });
- }
- let realWS = null; // 真正的 WebSocket 实例,全局变量,用于保持连接
- function connectWebSocket() {
- // 获取页面上的状态显示元素
- const wsStatus = document.getElementById('ws-status');
- const wsStatusText = document.getElementById('ws-status-text');
- try {
- // 建立 WebSocket 连接,连接到本地服务器的 3999 端口
- realWS = new WebSocket("ws://localhost:3999");
- // 连接成功时触发
- realWS.onopen = () => {
- AppState.wsConnected = true; // 更新应用状态为已连接
- wsStatus.className = 'ws-status connected'; // 修改状态图标样式
- wsStatusText.textContent = '已连接'; // 显示连接状态文本
- showNotification('WebSocket 连接成功'); // 弹出提示
- logWebSocketMessage('incoming', '连接已建立'); // 记录日志
- };
- // 接收到消息时触发
- realWS.onmessage = (event) => {
- logWebSocketMessage('incoming', event.data); // 日志记录原始消息内容
- let data;
- try {
- data = JSON.parse(event.data); // 尝试将消息解析为 JSON 对象
- } catch (e) {
- console.warn("WebSocket 消息解析失败", e); // JSON 解析失败时的警告
- return;
- }
- // 根据消息类型处理不同逻辑
- if (data.type === 'open_success') {
- const { filename, fullpath, content } = data;
- // 调用函数创建新的标签页并填充文件内容
- addNewTab(filename, content);
- // 提示用户文件已成功打开
- showNotification(`成功打开文件 "${filename}"`);
- }
- else if (data.type === 'save_success') {
- // 文件保存成功,弹出提示
- showNotification(`成功保存 "${data.filename}" 到 "${data.fullpath}"`);
- }
- };
- // 出现连接错误时触发
- realWS.onerror = (err) => {
- showNotification('WebSocket 错误', 'error'); // 弹出错误提示
- console.error(err); // 打印错误信息
- };
- // 连接关闭时触发
- realWS.onclose = () => {
- AppState.wsConnected = false; // 更新状态为未连接
- wsStatus.className = 'ws-status disconnected'; // 修改状态图标样式
- wsStatusText.textContent = '未连接'; // 显示未连接状态
- showNotification('WebSocket 已断开', 'error'); // 弹出断开通知
- };
- } catch (e) {
- // 整个初始化过程中发生异常
- console.error("WebSocket 初始化失败", e);
- }
- }
- // 创建新标签页
- function addNewTab(title = '未命名.js', content = '') {
- const tabId = `tab-${AppState.nextTabId++}`;
- const isActive = Object.keys(AppState.tabs).length === 0;
-
- // 创建标签页元素
- const tabEl = document.createElement('div');
- tabEl.className = `tab ${isActive ? 'active' : ''}`;
- tabEl.dataset.tabId = tabId;
- tabEl.innerHTML = `
- <span class="tab-title">${title}</span>
- <div class="tab-close"><i class="fas fa-times"></i></div>
- `;
- document.getElementById('tabs-container').appendChild(tabEl);
-
- // 创建编辑器容器
- const editorWrapper = document.createElement('div');
- editorWrapper.className = `editor-wrapper ${isActive ? 'active' : ''}`;
- editorWrapper.id = `editor-${tabId}`;
- document.getElementById('editors-container').appendChild(editorWrapper);
-
- // 创建Monaco编辑器实例
- const editor = monaco.editor.create(editorWrapper, {
- value: content,
- language: 'javascript',
- theme: AppState.theme,
- automaticLayout: true,
- minimap: { enabled: true },
- fontSize: 14,
- lineNumbers: 'on',
- scrollBeyondLastLine: false,
- roundedSelection: false,
- padding: { top: 10 },
- suggest: {
- showKeywords: true,
- snippetsPreventQuickSuggestions: false
- }
- });
-
- // 保存编辑器状态
- AppState.tabs[tabId] = {
- id: tabId,
- title,
- editor,
- container: editorWrapper,
- element: tabEl,
- modified: false,
- viewState: editor.saveViewState()
- };
-
- // 激活标签页
- if (isActive) {
- setActiveTab(tabId);
- }
-
- // 添加编辑器事件监听
- editor.onDidChangeModelContent(() => {
- setTabModified(tabId, true);
- });
-
- editor.onDidChangeCursorPosition(e => {
- if (AppState.activeTabId === tabId) {
- document.getElementById('cursor-position').textContent =
- `行: ${e.position.lineNumber}, 列: ${e.position.column}`;
- }
- });
-
- return tabId;
- }
-
- // 设置活动标签页
- function setActiveTab(tabId) {
- if (AppState.activeTabId === tabId) return;
-
- // 保存当前标签页状态
- if (AppState.activeTabId) {
- const prevTab = AppState.tabs[AppState.activeTabId];
- prevTab.viewState = prevTab.editor.saveViewState();
- prevTab.element.classList.remove('active');
- prevTab.container.classList.remove('active');
- }
-
- // 设置新活动标签页
- const tab = AppState.tabs[tabId];
- tab.element.classList.add('active');
- tab.container.classList.add('active');
- AppState.activeTabId = tabId;
-
- // 恢复编辑器状态
- if (tab.viewState) {
- tab.editor.restoreViewState(tab.viewState);
- }
- tab.editor.focus();
-
- // 更新光标位置
- const position = tab.editor.getPosition();
- document.getElementById('cursor-position').textContent =
- `行: ${position.lineNumber}, 列: ${position.column}`;
- }
-
- // 关闭标签页
- function closeTab(tabId) {
- const tab = AppState.tabs[tabId];
- if (!tab) return;
-
- // 如果是已修改的标签页,提示保存
- if (tab.modified) {
- if (!confirm(`"${tab.title}" 有未保存的更改。确定要关闭吗?`)) {
- return;
- }
- }
-
- // 销毁编辑器
- tab.editor.dispose();
-
- // 移除DOM元素
- tab.element.remove();
- tab.container.remove();
-
- // 从状态中移除
- delete AppState.tabs[tabId];
-
- // 如果关闭的是活动标签页,激活另一个标签页
- if (AppState.activeTabId === tabId) {
- const remainingTabs = Object.keys(AppState.tabs);
- if (remainingTabs.length > 0) {
- setActiveTab(remainingTabs[0]);
- } else {
- // 没有标签页时创建新标签页
- AppState.activeTabId = null;
- addNewTab();
- }
- }
- }
-
- // 设置标签页修改状态
- function setTabModified(tabId, modified) {
- const tab = AppState.tabs[tabId];
- if (!tab || tab.modified === modified) return;
-
- tab.modified = modified;
- const titleEl = tab.element.querySelector('.tab-title');
- if (modified) {
- titleEl.textContent = `* ${tab.title}`;
- titleEl.style.fontStyle = 'italic';
- } else {
- titleEl.textContent = tab.title;
- titleEl.style.fontStyle = 'normal';
- }
- }
-
- // 初始化事件监听器
- function initEventListeners() {
- // 新建标签页按钮
- document.getElementById('new-tab').addEventListener('click', () => {
- addNewTab();
- });
- // 打开按钮
- document.getElementById('open-file').addEventListener('click', () => {
- // 向后端发送打开文件的请求
- sendWebSocketMessage({
- type: 'open'
- });
- });
-
- // 保存按钮
- document.getElementById('save-file').addEventListener('click', () => {
- if (AppState.activeTabId) {
- const tab = AppState.tabs[AppState.activeTabId];
- const content = tab.editor.getValue();
- // 通过WebSocket发送保存请求
- sendWebSocketMessage({
- type: 'save',
- filename: tab.title,
- content: content
- });
- setTabModified(AppState.activeTabId, false);
- //showNotification(`"${tab.title}" 保存成功`);
- }
- });
-
- // 运行按钮
- document.getElementById('run-code').addEventListener('click', () => {
- if (AppState.activeTabId) {
- const tab = AppState.tabs[AppState.activeTabId];
- const content = tab.editor.getValue();
- // 通过WebSocket发送执行请求
- sendWebSocketMessage({
- type: 'execute',
- filename: tab.title,
- content: content
- });
- showNotification(`正在执行 "${tab.title}"...`);
- }
- });
-
- // 标签页点击事件委托
- document.getElementById('tabs-container').addEventListener('click', (e) => {
- const tabEl = e.target.closest('.tab');
- if (!tabEl) return;
-
- const tabId = tabEl.dataset.tabId;
-
- // 关闭按钮点击
- if (e.target.closest('.tab-close')) {
- closeTab(tabId);
- return;
- }
-
- // 标签页点击
- setActiveTab(tabId);
- });
-
- // 主题切换
- document.getElementById('theme-toggle').addEventListener('click', () => {
- const newTheme = AppState.theme === 'vs' ? 'vs-dark' : 'vs';
- AppState.theme = newTheme;
-
- // 更新所有编辑器的主题
- Object.values(AppState.tabs).forEach(tab => {
- monaco.editor.setTheme(newTheme);
- });
-
- // 更新按钮图标
- const icon = document.querySelector('#theme-toggle i');
- icon.className = newTheme === 'vs-dark' ? 'fas fa-sun' : 'fas fa-moon';
- document.getElementById('theme-toggle').innerHTML =
- `<i class="${icon.className}"></i> ${newTheme === 'vs-dark' ? '亮色模式' : '暗色模式'}`;
-
- // 更新根元素类以调整整体主题
- document.documentElement.style.setProperty('--primary-color', newTheme === 'vs-dark' ? '#0078d7' : '#0078d7');
- document.documentElement.style.setProperty('--tab-bg', newTheme === 'vs-dark' ? '#252526' : '#f3f3f3');
- document.documentElement.style.setProperty('--tab-active-bg', newTheme === 'vs-dark' ? '#1e1e1e' : '#ffffff');
- document.documentElement.style.setProperty('--tab-hover-bg', newTheme === 'vs-dark' ? '#2d2d2d' : '#eaeaea');
- document.documentElement.style.setProperty('--toolbar-bg', newTheme === 'vs-dark' ? '#333333' : '#e0e0e0');
- document.documentElement.style.setProperty('--border-color', newTheme === 'vs-dark' ? '#454545' : '#cccccc');
- document.documentElement.style.setProperty('--text-color', newTheme === 'vs-dark' ? '#d4d4d4' : '#333333');
-
- showNotification(`已切换至${newTheme === 'vs-dark' ? '暗色' : '亮色'}主题`);
- });
-
- // 调试控制台切换
- document.getElementById('toggle-console').addEventListener('click', () => {
- AppState.wsConsoleVisible = !AppState.wsConsoleVisible;
- document.getElementById('ws-console').style.display =
- AppState.wsConsoleVisible ? 'block' : 'none';
- });
- }
-
- // 模拟WebSocket连接
- function simulateWebSocketConnection() {
- const wsStatus = document.getElementById('ws-status');
- const wsStatusText = document.getElementById('ws-status-text');
-
- // 初始状态为未连接
- wsStatus.className = 'ws-status disconnected';
- wsStatusText.textContent = '未连接';
- // 模拟连接过程
- setTimeout(() => {
- AppState.wsConnected = true;
- wsStatus.className = 'ws-status connected';
- wsStatusText.textContent = '已连接';
- showNotification('WebSocket 连接成功');
- logWebSocketMessage('incoming', '连接已建立');
- }, 1500);
- }
- // 发送WebSocket消息
- function sendWebSocketMessage(message) {
- if (!AppState.wsConnected || !realWS || realWS.readyState !== WebSocket.OPEN) {
- showNotification('错误:WebSocket 未连接', 'error'); // 弹出错误提示
- return; // 中止发送
- }
- // 将传入的消息对象转换为 JSON 字符串
- const msg = JSON.stringify(message);
- // 通过 WebSocket 发送消息
- realWS.send(msg);
- // 记录已发送的消息内容,用于调试或日志展示
- logWebSocketMessage('outgoing', msg);
- }
- // 发送WebSocket消息(模拟)
- function sendWebSocketMessage_false(message) {
- if (!AppState.wsConnected) {
- showNotification('错误:WebSocket 未连接', 'error');
- return;
- }
-
- // 记录发送的消息
- logWebSocketMessage('outgoing', JSON.stringify(message));
-
- // 模拟服务器响应
- setTimeout(() => {
- let response;
- if (message.type === 'save') {
- response = { type: 'save_success', filename: message.filename };
- logWebSocketMessage('incoming', JSON.stringify(response));
- } else if (message.type === 'execute') {
- response = {
- type: 'execute_result',
- filename: message.filename,
- result: '执行完成(模拟结果)'
- };
- logWebSocketMessage('incoming', JSON.stringify(response));
- }
- }, 500);
- }
-
- // 记录WebSocket消息到控制台
- function logWebSocketMessage(direction, content) {
- if (!AppState.wsConsoleVisible) return;
-
- const consoleEl = document.getElementById('console-content');
- const entry = document.createElement('div');
- entry.className = `console-entry ${direction}`;
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${content}`;
- consoleEl.appendChild(entry);
- consoleEl.scrollTop = consoleEl.scrollHeight;
- }
-
- // 显示通知
- function showNotification(message, type = 'info') {
- const notification = document.getElementById('notification');
- notification.textContent = message;
- notification.style.display = 'block';
- notification.style.backgroundColor = type === 'error' ? '#f44336' : '#333';
-
- setTimeout(() => {
- notification.style.display = 'none';
- }, 3000);
- }
-
- // 启动应用
- window.addEventListener('DOMContentLoaded', initApp);
- </script>
- </body>
- </html>
|