index.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Monaco Editor - 多标签JS源码编辑器</title>
  7. <link rel="stylesheet" href="/font-awesome/css/all.min.css">
  8. <style>
  9. :root {
  10. --primary-color: #0078d7;
  11. --tab-bg: #252526;
  12. --tab-active-bg: #1e1e1e;
  13. --tab-hover-bg: #2d2d2d;
  14. --toolbar-bg: #333333;
  15. --border-color: #454545;
  16. --text-color: #d4d4d4;
  17. --danger-color: #f14c4c;
  18. }
  19. * {
  20. margin: 0;
  21. padding: 0;
  22. box-sizing: border-box;
  23. }
  24. body {
  25. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  26. background-color: #1e1e1e;
  27. color: var(--text-color);
  28. height: 100vh;
  29. overflow: hidden;
  30. display: flex;
  31. flex-direction: column;
  32. }
  33. #toolbar {
  34. background-color: var(--toolbar-bg);
  35. padding: 10px 15px;
  36. display: flex;
  37. gap: 15px;
  38. border-bottom: 1px solid var(--border-color);
  39. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  40. z-index: 10;
  41. }
  42. .btn {
  43. background-color: #3c3c3c;
  44. color: var(--text-color);
  45. border: 1px solid var(--border-color);
  46. padding: 8px 15px;
  47. border-radius: 4px;
  48. cursor: pointer;
  49. display: flex;
  50. align-items: center;
  51. gap: 8px;
  52. transition: all 0.2s;
  53. }
  54. .btn:hover {
  55. background-color: #454545;
  56. }
  57. .btn-primary {
  58. background-color: var(--primary-color);
  59. border-color: var(--primary-color);
  60. }
  61. .btn-primary:hover {
  62. background-color: #0066b4;
  63. }
  64. .btn-danger {
  65. background-color: var(--danger-color);
  66. border-color: var(--danger-color);
  67. }
  68. .btn-danger:hover {
  69. background-color: #d93636;
  70. }
  71. #tabs-container {
  72. background-color: var(--tab-bg);
  73. display: flex;
  74. overflow-x: auto;
  75. padding: 0 10px;
  76. border-bottom: 1px solid var(--border-color);
  77. }
  78. .tab {
  79. padding: 10px 15px;
  80. background-color: var(--tab-bg);
  81. cursor: pointer;
  82. display: flex;
  83. align-items: center;
  84. gap: 10px;
  85. border-right: 1px solid var(--border-color);
  86. min-width: 180px;
  87. max-width: 250px;
  88. position: relative;
  89. }
  90. .tab:hover {
  91. background-color: var(--tab-hover-bg);
  92. }
  93. .tab.active {
  94. background-color: var(--tab-active-bg);
  95. }
  96. .tab.active::before {
  97. content: '';
  98. position: absolute;
  99. top: 0;
  100. left: 0;
  101. right: 0;
  102. height: 3px;
  103. background-color: var(--primary-color);
  104. }
  105. .tab-title {
  106. flex: 1;
  107. overflow: hidden;
  108. text-overflow: ellipsis;
  109. white-space: nowrap;
  110. font-size: 14px;
  111. }
  112. .tab-close {
  113. color: #888;
  114. padding: 2px;
  115. border-radius: 50%;
  116. width: 20px;
  117. height: 20px;
  118. display: flex;
  119. align-items: center;
  120. justify-content: center;
  121. }
  122. .tab-close:hover {
  123. background-color: rgba(255, 255, 255, 0.1);
  124. color: #fff;
  125. }
  126. #editors-container {
  127. flex: 1;
  128. position: relative;
  129. overflow: hidden;
  130. }
  131. .editor-wrapper {
  132. position: absolute;
  133. top: 0;
  134. left: 0;
  135. width: 100%;
  136. height: 100%;
  137. display: none;
  138. }
  139. .editor-wrapper.active {
  140. display: block;
  141. }
  142. #status-bar {
  143. background-color: var(--toolbar-bg);
  144. padding: 5px 15px;
  145. font-size: 12px;
  146. display: flex;
  147. justify-content: space-between;
  148. border-top: 1px solid var(--border-color);
  149. }
  150. .status-item {
  151. display: flex;
  152. align-items: center;
  153. gap: 8px;
  154. }
  155. .ws-status {
  156. display: inline-block;
  157. width: 10px;
  158. height: 10px;
  159. border-radius: 50%;
  160. background-color: #888;
  161. }
  162. .ws-status.connected {
  163. background-color: #4caf50;
  164. }
  165. .ws-status.disconnected {
  166. background-color: #f44336;
  167. }
  168. #notification {
  169. position: fixed;
  170. bottom: 20px;
  171. right: 20px;
  172. background-color: #333;
  173. color: white;
  174. padding: 15px 20px;
  175. border-radius: 4px;
  176. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  177. display: none;
  178. z-index: 1000;
  179. }
  180. #ws-console {
  181. position: fixed;
  182. bottom: 0;
  183. right: 0;
  184. width: 300px;
  185. height: 200px;
  186. background-color: rgba(0, 0, 0, 0.8);
  187. border-left: 1px solid #333;
  188. border-top: 1px solid #333;
  189. padding: 10px;
  190. font-size: 12px;
  191. overflow-y: auto;
  192. display: none;
  193. z-index: 100;
  194. }
  195. .console-title {
  196. margin-bottom: 8px;
  197. font-weight: bold;
  198. color: var(--primary-color);
  199. }
  200. .console-entry {
  201. margin-bottom: 5px;
  202. padding: 3px 5px;
  203. border-radius: 3px;
  204. background-color: rgba(255, 255, 255, 0.05);
  205. }
  206. .console-entry.incoming {
  207. color: #4caf50;
  208. }
  209. .console-entry.outgoing {
  210. color: #2196f3;
  211. }
  212. .console-entry.error {
  213. color: #f44336;
  214. }
  215. </style>
  216. </head>
  217. <body>
  218. <div id="toolbar">
  219. <button class="btn btn-primary" id="new-tab">
  220. <i class="fas fa-plus"></i> 新建标签页
  221. </button>
  222. <button class="btn" id="open-file">
  223. <i class="fas fa-folder-open"></i> 打开
  224. </button>
  225. <button class="btn" id="save-file">
  226. <i class="fas fa-save"></i> 保存
  227. </button>
  228. <button class="btn" id="run-code">
  229. <i class="fas fa-play"></i> 运行
  230. </button>
  231. <button class="btn" id="toggle-console">
  232. <i class="fas fa-terminal"></i> 调试控制台
  233. </button>
  234. <div style="flex: 1"></div>
  235. <button class="btn" id="theme-toggle">
  236. <i class="fas fa-moon"></i> 暗色模式
  237. </button>
  238. </div>
  239. <div id="tabs-container"></div>
  240. <div id="editors-container"></div>
  241. <div id="status-bar">
  242. <div class="status-item">
  243. <span>WebSocket:</span>
  244. <span class="ws-status" id="ws-status"></span>
  245. <span id="ws-status-text">未连接</span>
  246. </div>
  247. <div class="status-item">
  248. <span id="cursor-position">行: 1, 列: 1</span>
  249. </div>
  250. </div>
  251. <div id="notification"></div>
  252. <div id="ws-console">
  253. <div class="console-title">WebSocket 通信日志</div>
  254. <div id="console-content"></div>
  255. </div>
  256. <!-- Monaco Editor Loader -->
  257. <script src="/monaco-editor/min/vs/loader.js"></script>
  258. <script>
  259. const AppState = {
  260. tabs: {},
  261. activeTabId: null,
  262. nextTabId: 1,
  263. theme: 'vs-dark',
  264. wsConnected: false,
  265. wsConsoleVisible: false
  266. };
  267. // 初始化函数
  268. function initApp() {
  269. // 设置Monaco编辑器路径
  270. require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
  271. // 初始化Monaco环境
  272. require(['vs/editor/editor.main'], () => {
  273. // 添加初始标签页
  274. addNewTab('script.js', '// 欢迎使用JavaScript编辑器\nconsole.log("Hello, World!");\n\nfunction example() {\n return "This is an example function";\n}');
  275. // 初始化事件监听器
  276. initEventListeners();
  277. // 模拟WebSocket连接
  278. //simulateWebSocketConnection();
  279. // 真实WebSocket连接
  280. connectWebSocket(); // >>> ADDED
  281. });
  282. }
  283. let realWS = null; // 真正的 WebSocket 实例,全局变量,用于保持连接
  284. function connectWebSocket() {
  285. // 获取页面上的状态显示元素
  286. const wsStatus = document.getElementById('ws-status');
  287. const wsStatusText = document.getElementById('ws-status-text');
  288. try {
  289. // 建立 WebSocket 连接,连接到本地服务器的 3999 端口
  290. realWS = new WebSocket("ws://localhost:3999");
  291. // 连接成功时触发
  292. realWS.onopen = () => {
  293. AppState.wsConnected = true; // 更新应用状态为已连接
  294. wsStatus.className = 'ws-status connected'; // 修改状态图标样式
  295. wsStatusText.textContent = '已连接'; // 显示连接状态文本
  296. showNotification('WebSocket 连接成功'); // 弹出提示
  297. logWebSocketMessage('incoming', '连接已建立'); // 记录日志
  298. };
  299. // 接收到消息时触发
  300. realWS.onmessage = (event) => {
  301. logWebSocketMessage('incoming', event.data); // 日志记录原始消息内容
  302. let data;
  303. try {
  304. data = JSON.parse(event.data); // 尝试将消息解析为 JSON 对象
  305. } catch (e) {
  306. console.warn("WebSocket 消息解析失败", e); // JSON 解析失败时的警告
  307. return;
  308. }
  309. // 根据消息类型处理不同逻辑
  310. if (data.type === 'open_success') {
  311. const { filename, fullpath, content } = data;
  312. // 调用函数创建新的标签页并填充文件内容
  313. addNewTab(filename, content);
  314. // 提示用户文件已成功打开
  315. showNotification(`成功打开文件 "${filename}"`);
  316. }
  317. else if (data.type === 'save_success') {
  318. // 文件保存成功,弹出提示
  319. showNotification(`成功保存 "${data.filename}" 到 "${data.fullpath}"`);
  320. }
  321. };
  322. // 出现连接错误时触发
  323. realWS.onerror = (err) => {
  324. showNotification('WebSocket 错误', 'error'); // 弹出错误提示
  325. console.error(err); // 打印错误信息
  326. };
  327. // 连接关闭时触发
  328. realWS.onclose = () => {
  329. AppState.wsConnected = false; // 更新状态为未连接
  330. wsStatus.className = 'ws-status disconnected'; // 修改状态图标样式
  331. wsStatusText.textContent = '未连接'; // 显示未连接状态
  332. showNotification('WebSocket 已断开', 'error'); // 弹出断开通知
  333. };
  334. } catch (e) {
  335. // 整个初始化过程中发生异常
  336. console.error("WebSocket 初始化失败", e);
  337. }
  338. }
  339. // 创建新标签页
  340. function addNewTab(title = '未命名.js', content = '') {
  341. const tabId = `tab-${AppState.nextTabId++}`;
  342. const isActive = Object.keys(AppState.tabs).length === 0;
  343. // 创建标签页元素
  344. const tabEl = document.createElement('div');
  345. tabEl.className = `tab ${isActive ? 'active' : ''}`;
  346. tabEl.dataset.tabId = tabId;
  347. tabEl.innerHTML = `
  348. <span class="tab-title">${title}</span>
  349. <div class="tab-close"><i class="fas fa-times"></i></div>
  350. `;
  351. document.getElementById('tabs-container').appendChild(tabEl);
  352. // 创建编辑器容器
  353. const editorWrapper = document.createElement('div');
  354. editorWrapper.className = `editor-wrapper ${isActive ? 'active' : ''}`;
  355. editorWrapper.id = `editor-${tabId}`;
  356. document.getElementById('editors-container').appendChild(editorWrapper);
  357. // 创建Monaco编辑器实例
  358. const editor = monaco.editor.create(editorWrapper, {
  359. value: content,
  360. language: 'javascript',
  361. theme: AppState.theme,
  362. automaticLayout: true,
  363. minimap: { enabled: true },
  364. fontSize: 14,
  365. lineNumbers: 'on',
  366. scrollBeyondLastLine: false,
  367. roundedSelection: false,
  368. padding: { top: 10 },
  369. suggest: {
  370. showKeywords: true,
  371. snippetsPreventQuickSuggestions: false
  372. }
  373. });
  374. // 保存编辑器状态
  375. AppState.tabs[tabId] = {
  376. id: tabId,
  377. title,
  378. editor,
  379. container: editorWrapper,
  380. element: tabEl,
  381. modified: false,
  382. viewState: editor.saveViewState()
  383. };
  384. // 激活标签页
  385. if (isActive) {
  386. setActiveTab(tabId);
  387. }
  388. // 添加编辑器事件监听
  389. editor.onDidChangeModelContent(() => {
  390. setTabModified(tabId, true);
  391. });
  392. editor.onDidChangeCursorPosition(e => {
  393. if (AppState.activeTabId === tabId) {
  394. document.getElementById('cursor-position').textContent =
  395. `行: ${e.position.lineNumber}, 列: ${e.position.column}`;
  396. }
  397. });
  398. return tabId;
  399. }
  400. // 设置活动标签页
  401. function setActiveTab(tabId) {
  402. if (AppState.activeTabId === tabId) return;
  403. // 保存当前标签页状态
  404. if (AppState.activeTabId) {
  405. const prevTab = AppState.tabs[AppState.activeTabId];
  406. prevTab.viewState = prevTab.editor.saveViewState();
  407. prevTab.element.classList.remove('active');
  408. prevTab.container.classList.remove('active');
  409. }
  410. // 设置新活动标签页
  411. const tab = AppState.tabs[tabId];
  412. tab.element.classList.add('active');
  413. tab.container.classList.add('active');
  414. AppState.activeTabId = tabId;
  415. // 恢复编辑器状态
  416. if (tab.viewState) {
  417. tab.editor.restoreViewState(tab.viewState);
  418. }
  419. tab.editor.focus();
  420. // 更新光标位置
  421. const position = tab.editor.getPosition();
  422. document.getElementById('cursor-position').textContent =
  423. `行: ${position.lineNumber}, 列: ${position.column}`;
  424. }
  425. // 关闭标签页
  426. function closeTab(tabId) {
  427. const tab = AppState.tabs[tabId];
  428. if (!tab) return;
  429. // 如果是已修改的标签页,提示保存
  430. if (tab.modified) {
  431. if (!confirm(`"${tab.title}" 有未保存的更改。确定要关闭吗?`)) {
  432. return;
  433. }
  434. }
  435. // 销毁编辑器
  436. tab.editor.dispose();
  437. // 移除DOM元素
  438. tab.element.remove();
  439. tab.container.remove();
  440. // 从状态中移除
  441. delete AppState.tabs[tabId];
  442. // 如果关闭的是活动标签页,激活另一个标签页
  443. if (AppState.activeTabId === tabId) {
  444. const remainingTabs = Object.keys(AppState.tabs);
  445. if (remainingTabs.length > 0) {
  446. setActiveTab(remainingTabs[0]);
  447. } else {
  448. // 没有标签页时创建新标签页
  449. AppState.activeTabId = null;
  450. addNewTab();
  451. }
  452. }
  453. }
  454. // 设置标签页修改状态
  455. function setTabModified(tabId, modified) {
  456. const tab = AppState.tabs[tabId];
  457. if (!tab || tab.modified === modified) return;
  458. tab.modified = modified;
  459. const titleEl = tab.element.querySelector('.tab-title');
  460. if (modified) {
  461. titleEl.textContent = `* ${tab.title}`;
  462. titleEl.style.fontStyle = 'italic';
  463. } else {
  464. titleEl.textContent = tab.title;
  465. titleEl.style.fontStyle = 'normal';
  466. }
  467. }
  468. // 初始化事件监听器
  469. function initEventListeners() {
  470. // 新建标签页按钮
  471. document.getElementById('new-tab').addEventListener('click', () => {
  472. addNewTab();
  473. });
  474. // 打开按钮
  475. document.getElementById('open-file').addEventListener('click', () => {
  476. // 向后端发送打开文件的请求
  477. sendWebSocketMessage({
  478. type: 'open'
  479. });
  480. });
  481. // 保存按钮
  482. document.getElementById('save-file').addEventListener('click', () => {
  483. if (AppState.activeTabId) {
  484. const tab = AppState.tabs[AppState.activeTabId];
  485. const content = tab.editor.getValue();
  486. // 通过WebSocket发送保存请求
  487. sendWebSocketMessage({
  488. type: 'save',
  489. filename: tab.title,
  490. content: content
  491. });
  492. setTabModified(AppState.activeTabId, false);
  493. //showNotification(`"${tab.title}" 保存成功`);
  494. }
  495. });
  496. // 运行按钮
  497. document.getElementById('run-code').addEventListener('click', () => {
  498. if (AppState.activeTabId) {
  499. const tab = AppState.tabs[AppState.activeTabId];
  500. const content = tab.editor.getValue();
  501. // 通过WebSocket发送执行请求
  502. sendWebSocketMessage({
  503. type: 'execute',
  504. filename: tab.title,
  505. content: content
  506. });
  507. showNotification(`正在执行 "${tab.title}"...`);
  508. }
  509. });
  510. // 标签页点击事件委托
  511. document.getElementById('tabs-container').addEventListener('click', (e) => {
  512. const tabEl = e.target.closest('.tab');
  513. if (!tabEl) return;
  514. const tabId = tabEl.dataset.tabId;
  515. // 关闭按钮点击
  516. if (e.target.closest('.tab-close')) {
  517. closeTab(tabId);
  518. return;
  519. }
  520. // 标签页点击
  521. setActiveTab(tabId);
  522. });
  523. // 主题切换
  524. document.getElementById('theme-toggle').addEventListener('click', () => {
  525. const newTheme = AppState.theme === 'vs' ? 'vs-dark' : 'vs';
  526. AppState.theme = newTheme;
  527. // 更新所有编辑器的主题
  528. Object.values(AppState.tabs).forEach(tab => {
  529. monaco.editor.setTheme(newTheme);
  530. });
  531. // 更新按钮图标
  532. const icon = document.querySelector('#theme-toggle i');
  533. icon.className = newTheme === 'vs-dark' ? 'fas fa-sun' : 'fas fa-moon';
  534. document.getElementById('theme-toggle').innerHTML =
  535. `<i class="${icon.className}"></i> ${newTheme === 'vs-dark' ? '亮色模式' : '暗色模式'}`;
  536. // 更新根元素类以调整整体主题
  537. document.documentElement.style.setProperty('--primary-color', newTheme === 'vs-dark' ? '#0078d7' : '#0078d7');
  538. document.documentElement.style.setProperty('--tab-bg', newTheme === 'vs-dark' ? '#252526' : '#f3f3f3');
  539. document.documentElement.style.setProperty('--tab-active-bg', newTheme === 'vs-dark' ? '#1e1e1e' : '#ffffff');
  540. document.documentElement.style.setProperty('--tab-hover-bg', newTheme === 'vs-dark' ? '#2d2d2d' : '#eaeaea');
  541. document.documentElement.style.setProperty('--toolbar-bg', newTheme === 'vs-dark' ? '#333333' : '#e0e0e0');
  542. document.documentElement.style.setProperty('--border-color', newTheme === 'vs-dark' ? '#454545' : '#cccccc');
  543. document.documentElement.style.setProperty('--text-color', newTheme === 'vs-dark' ? '#d4d4d4' : '#333333');
  544. showNotification(`已切换至${newTheme === 'vs-dark' ? '暗色' : '亮色'}主题`);
  545. });
  546. // 调试控制台切换
  547. document.getElementById('toggle-console').addEventListener('click', () => {
  548. AppState.wsConsoleVisible = !AppState.wsConsoleVisible;
  549. document.getElementById('ws-console').style.display =
  550. AppState.wsConsoleVisible ? 'block' : 'none';
  551. });
  552. }
  553. // 模拟WebSocket连接
  554. function simulateWebSocketConnection() {
  555. const wsStatus = document.getElementById('ws-status');
  556. const wsStatusText = document.getElementById('ws-status-text');
  557. // 初始状态为未连接
  558. wsStatus.className = 'ws-status disconnected';
  559. wsStatusText.textContent = '未连接';
  560. // 模拟连接过程
  561. setTimeout(() => {
  562. AppState.wsConnected = true;
  563. wsStatus.className = 'ws-status connected';
  564. wsStatusText.textContent = '已连接';
  565. showNotification('WebSocket 连接成功');
  566. logWebSocketMessage('incoming', '连接已建立');
  567. }, 1500);
  568. }
  569. // 发送WebSocket消息
  570. function sendWebSocketMessage(message) {
  571. if (!AppState.wsConnected || !realWS || realWS.readyState !== WebSocket.OPEN) {
  572. showNotification('错误:WebSocket 未连接', 'error'); // 弹出错误提示
  573. return; // 中止发送
  574. }
  575. // 将传入的消息对象转换为 JSON 字符串
  576. const msg = JSON.stringify(message);
  577. // 通过 WebSocket 发送消息
  578. realWS.send(msg);
  579. // 记录已发送的消息内容,用于调试或日志展示
  580. logWebSocketMessage('outgoing', msg);
  581. }
  582. // 发送WebSocket消息(模拟)
  583. function sendWebSocketMessage_false(message) {
  584. if (!AppState.wsConnected) {
  585. showNotification('错误:WebSocket 未连接', 'error');
  586. return;
  587. }
  588. // 记录发送的消息
  589. logWebSocketMessage('outgoing', JSON.stringify(message));
  590. // 模拟服务器响应
  591. setTimeout(() => {
  592. let response;
  593. if (message.type === 'save') {
  594. response = { type: 'save_success', filename: message.filename };
  595. logWebSocketMessage('incoming', JSON.stringify(response));
  596. } else if (message.type === 'execute') {
  597. response = {
  598. type: 'execute_result',
  599. filename: message.filename,
  600. result: '执行完成(模拟结果)'
  601. };
  602. logWebSocketMessage('incoming', JSON.stringify(response));
  603. }
  604. }, 500);
  605. }
  606. // 记录WebSocket消息到控制台
  607. function logWebSocketMessage(direction, content) {
  608. if (!AppState.wsConsoleVisible) return;
  609. const consoleEl = document.getElementById('console-content');
  610. const entry = document.createElement('div');
  611. entry.className = `console-entry ${direction}`;
  612. entry.textContent = `[${new Date().toLocaleTimeString()}] ${content}`;
  613. consoleEl.appendChild(entry);
  614. consoleEl.scrollTop = consoleEl.scrollHeight;
  615. }
  616. // 显示通知
  617. function showNotification(message, type = 'info') {
  618. const notification = document.getElementById('notification');
  619. notification.textContent = message;
  620. notification.style.display = 'block';
  621. notification.style.backgroundColor = type === 'error' ? '#f44336' : '#333';
  622. setTimeout(() => {
  623. notification.style.display = 'none';
  624. }, 3000);
  625. }
  626. // 启动应用
  627. window.addEventListener('DOMContentLoaded', initApp);
  628. </script>
  629. </body>
  630. </html>