From c29ba7737e43f8d1f9a036cc64754f7480e9f242 Mon Sep 17 00:00:00 2001 From: swpa Date: Wed, 13 May 2026 11:09:19 +0800 Subject: [PATCH] =?UTF-8?q?Refactor:=20=E4=BD=BF=E7=94=A8=20ES6=20Modules?= =?UTF-8?q?=20=E6=8A=BD=E9=9B=A2=20Terminal=20=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- static/app.js | 153 +++------------------------------------------ static/terminal.js | 136 ++++++++++++++++++++++++++++++++++++++++ static/utils.js | 14 +++++ 4 files changed, 160 insertions(+), 145 deletions(-) create mode 100644 static/terminal.js create mode 100644 static/utils.js diff --git a/index.html b/index.html index 7cc17e7..9ce6df1 100644 --- a/index.html +++ b/index.html @@ -374,7 +374,7 @@ - + diff --git a/static/app.js b/static/app.js index 59d5c0a..030634b 100644 --- a/static/app.js +++ b/static/app.js @@ -1,11 +1,18 @@ /* --- static/app.js --- */ +// 1. 從 terminal.js 引入我們需要的功能 +import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, downloadTerminal } from './terminal.js'; + +// 2. 將這些功能綁定到 window 上,這樣 HTML 裡的 onclick 才找得到它們! +window.connectWebSocket = () => connectWebSocket(resetAllManagerData); +window.disconnectWebSocket = disconnectWebSocket; +window.injectCommand = injectCommand; +window.downloadTerminal = downloadTerminal; + // ========================================== // 1. 全域變數與初始化 // ========================================== -let term, fitAddon, ws; let currentMacDomainData = null; -let isConnected = false; // 💡 新增:追蹤當前編輯狀態 let currentEditPath = null; @@ -1145,148 +1152,6 @@ function resetAllManagerData() { document.getElementById('queryTargetRpd').value = ''; } -// ========================================== -// 3. 終端機 (Terminal) 與 WebSocket 邏輯 -// ========================================== -function colorizeTerminalStream(text) { - if (text.includes('\x1b[')) return text; - let res = text; - res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[1;34m$1\x1b[0m'); - res = res.replace(/\b([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\b/g, '\x1b[1;35m$1\x1b[0m'); - res = res.replace(/\b(online|w-online|p-online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[1;32m$1\x1b[0m'); - res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[1;31m$1\x1b[0m'); - res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[33m$1\x1b[0m'); - return res; -} - -function initTerminal() { - term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 }); - fitAddon = new FitAddon.FitAddon(); - term.loadAddon(fitAddon); - term.open(document.getElementById('terminal-container')); - fitAddon.fit(); - term.writeln('Welcome to Harmonic cOS Web Terminal.'); - term.writeln('Please confirm the target device above and click "連線至 CMTS"... \r\n'); - - term.onData((data) => { - if (ws && ws.readyState === WebSocket.OPEN) ws.send(data); - }); -} - -function getGlobalConnectionInfo() { - const host = document.getElementById('cmtsHost').value.trim(); - const user = document.getElementById('cmtsUser').value.trim(); - const pass = document.getElementById('cmtsPass').value.trim(); - if (!host || !user || !pass) { - alert("請在畫面上方完整輸入目標設備的 IP、帳號與密碼!"); - return null; - } - return { host, user, pass }; -} - -function connectWebSocket() { - if (ws && ws.readyState === WebSocket.OPEN) return; - const connInfo = getGlobalConnectionInfo(); - if (!connInfo) return; - - resetAllManagerData(); - - term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`); - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/terminal?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; - - ws = new WebSocket(wsUrl); - - ws.onopen = () => { - isConnected = true; - const btnLoadTask = document.getElementById('btn-load-task'); - if (btnLoadTask) { - btnLoadTask.disabled = false; - btnLoadTask.style.backgroundColor = '#c0392b'; - btnLoadTask.style.cursor = 'pointer'; - } - - document.getElementById('wsStatus').textContent = `狀態:已連線`; - document.getElementById('wsStatus').style.color = '#27ae60'; - document.getElementById('btnConnect').style.display = 'none'; - document.getElementById('btnDisconnect').style.display = 'inline-block'; - document.getElementById('cmtsHost').disabled = true; - document.getElementById('cmtsUser').disabled = true; - document.getElementById('cmtsPass').disabled = true; - term.focus(); - }; - - ws.onmessage = (event) => { - term.write(colorizeTerminalStream(event.data)); - }; - - ws.onclose = () => { - isConnected = false; - const btnLoadTask = document.getElementById('btn-load-task'); - if (btnLoadTask) { - btnLoadTask.disabled = true; - btnLoadTask.style.backgroundColor = '#95a5a6'; - btnLoadTask.style.cursor = 'not-allowed'; - } - - document.getElementById('wsStatus').textContent = '狀態:已斷線'; - document.getElementById('wsStatus').style.color = '#c0392b'; - document.getElementById('btnConnect').style.display = 'inline-block'; - document.getElementById('btnDisconnect').style.display = 'none'; - document.getElementById('cmtsHost').disabled = false; - document.getElementById('cmtsUser').disabled = false; - document.getElementById('cmtsPass').disabled = false; - term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n'); - - resetAllManagerData(); - }; -} - -function disconnectWebSocket() { if (ws) ws.close(); } - -function injectCommand() { - if (!ws || ws.readyState !== WebSocket.OPEN) return alert("請先點擊「連線至 CMTS」!"); - const cmd = document.getElementById('fixedCmd').value; - ws.send(cmd + '\r'); - term.focus(); -} - -function getTerminalText() { - if (!term) return ''; - try { - if (term.hasSelection()) return term.getSelection(); - const buffer = term.buffer.active; - let text = ''; - for (let i = 0; i < buffer.length; i++) { - const line = buffer.getLine(i); - if (line) text += line.translateToString(true) + '\n'; - } - return text; - } catch (err) { - console.error("讀取終端機文字失敗:", err); - return ''; - } -} - -function downloadTerminal() { - const text = getTerminalText(); - if (!text.trim()) return alert("沒有內容可下載!"); - try { - const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); - a.download = `cmts_terminal_log_${timestamp}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (err) { - console.error("下載失敗:", err); - } -} - // ========================================== // 4. 綜合查詢 API 呼叫 // ========================================== diff --git a/static/terminal.js b/static/terminal.js new file mode 100644 index 0000000..731a158 --- /dev/null +++ b/static/terminal.js @@ -0,0 +1,136 @@ +// --- static/terminal.js --- + +// 1. 從剛剛建立的 utils.js 引入工具 +import { getGlobalConnectionInfo } from './utils.js'; + +// 2. 宣告終端機專用的變數 +let term, fitAddon, ws; +export let isConnected = false; + +// 3. 終端機文字上色 (內部使用,不需要 export) +function colorizeTerminalStream(text) { + if (text.includes('\x1b[')) return text; + let res = text; + res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[1;34m$1\x1b[0m'); + res = res.replace(/\b([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\b/g, '\x1b[1;35m$1\x1b[0m'); + res = res.replace(/\b(online|w-online|p-online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[1;32m$1\x1b[0m'); + res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[1;31m$1\x1b[0m'); + res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[33m$1\x1b[0m'); + return res; +} + +// 4. 匯出 (export) 所有需要被外部呼叫的函數 +export function initTerminal() { + term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 }); + fitAddon = new FitAddon.FitAddon(); + term.loadAddon(fitAddon); + term.open(document.getElementById('terminal-container')); + fitAddon.fit(); + term.writeln('Welcome to Harmonic cOS Web Terminal.'); + term.writeln('Please confirm the target device above and click "連線至 CMTS"... \r\n'); + + term.onData((data) => { + if (ws && ws.readyState === WebSocket.OPEN) ws.send(data); + }); +} + +export function connectWebSocket(resetCallback) { + if (ws && ws.readyState === WebSocket.OPEN) return; + const connInfo = getGlobalConnectionInfo(); + if (!connInfo) return; + + if (resetCallback) resetCallback(); // 呼叫 app.js 傳進來的重置函數 + + term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`); + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/terminal?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; + + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + isConnected = true; + const btnLoadTask = document.getElementById('btn-load-task'); + if (btnLoadTask) { + btnLoadTask.disabled = false; + btnLoadTask.style.backgroundColor = '#c0392b'; + btnLoadTask.style.cursor = 'pointer'; + } + document.getElementById('wsStatus').textContent = `狀態:已連線`; + document.getElementById('wsStatus').style.color = '#27ae60'; + document.getElementById('btnConnect').style.display = 'none'; + document.getElementById('btnDisconnect').style.display = 'inline-block'; + document.getElementById('cmtsHost').disabled = true; + document.getElementById('cmtsUser').disabled = true; + document.getElementById('cmtsPass').disabled = true; + term.focus(); + }; + + ws.onmessage = (event) => { + term.write(colorizeTerminalStream(event.data)); + }; + + ws.onclose = () => { + isConnected = false; + const btnLoadTask = document.getElementById('btn-load-task'); + if (btnLoadTask) { + btnLoadTask.disabled = true; + btnLoadTask.style.backgroundColor = '#95a5a6'; + btnLoadTask.style.cursor = 'not-allowed'; + } + document.getElementById('wsStatus').textContent = '狀態:已斷線'; + document.getElementById('wsStatus').style.color = '#c0392b'; + document.getElementById('btnConnect').style.display = 'inline-block'; + document.getElementById('btnDisconnect').style.display = 'none'; + document.getElementById('cmtsHost').disabled = false; + document.getElementById('cmtsUser').disabled = false; + document.getElementById('cmtsPass').disabled = false; + term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n'); + + if (resetCallback) resetCallback(); + }; +} + +export function disconnectWebSocket() { if (ws) ws.close(); } + +export function injectCommand() { + if (!ws || ws.readyState !== WebSocket.OPEN) return alert("請先點擊「連線至 CMTS」!"); + const cmd = document.getElementById('fixedCmd').value; + ws.send(cmd + '\r'); + term.focus(); +} + +export function getTerminalText() { + if (!term) return ''; + try { + if (term.hasSelection()) return term.getSelection(); + const buffer = term.buffer.active; + let text = ''; + for (let i = 0; i < buffer.length; i++) { + const line = buffer.getLine(i); + if (line) text += line.translateToString(true) + '\n'; + } + return text; + } catch (err) { + console.error("讀取終端機文字失敗:", err); + return ''; + } +} + +export function downloadTerminal() { + const text = getTerminalText(); + if (!text.trim()) return alert("沒有內容可下載!"); + try { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); + a.download = `cmts_terminal_log_${timestamp}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (err) { + console.error("下載失敗:", err); + } +} diff --git a/static/utils.js b/static/utils.js new file mode 100644 index 0000000..7c07330 --- /dev/null +++ b/static/utils.js @@ -0,0 +1,14 @@ +// --- static/utils.js --- + +// 加上 export,讓其他檔案可以使用這個函數 +export function getGlobalConnectionInfo() { + const host = document.getElementById('cmtsHost').value.trim(); + const user = document.getElementById('cmtsUser').value.trim(); + const pass = document.getElementById('cmtsPass').value.trim(); + + if (!host || !user || !pass) { + alert("請在畫面上方完整輸入目標設備的 IP、帳號與密碼!"); + return null; + } + return { host, user, pass }; +}