refactor: 混合式架構 (Hybrid Approach) 的設定載入訊息設計。
-前端模擬動態流 (Simulated Streaming) -真實動態流 (True Streaming)
This commit is contained in:
parent
ecedee49d2
commit
3cd7ce253e
303
all_code.txt
303
all_code.txt
|
|
@ -2454,7 +2454,7 @@ function resetAllManagerData() {
|
|||
// 🌟 3. 設備查詢與全域配置載入 (Query & Full Config)
|
||||
// ============================================================================
|
||||
|
||||
// 執行綜合查詢 (CM / RPD)
|
||||
// 執行綜合查詢 (CM / RPD) - 🌟 導入前端模擬動態訊息流
|
||||
async function executeQuery(mode) {
|
||||
if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!");
|
||||
|
||||
|
|
@ -2467,10 +2467,28 @@ async function executeQuery(mode) {
|
|||
|
||||
openModal(connInfo.host);
|
||||
const outputEl = document.getElementById('modalOutput');
|
||||
outputEl.innerHTML = `<span style="color: #f39c12;">正在向 ${connInfo.host} 查詢中,請稍候...</span>`;
|
||||
|
||||
// 🌟 模擬動態訊息流邏輯
|
||||
const states = [
|
||||
`🔌 正在建立 SSH 連線至 ${connInfo.host}...`,
|
||||
`📥 正在向設備發送查詢指令...`,
|
||||
`🧩 正在等待設備回傳資料...`
|
||||
];
|
||||
let progressIdx = 0;
|
||||
outputEl.innerHTML = `<span id="query-progress-text" style="color: #f39c12; font-weight: bold;">${states[0]}</span>`;
|
||||
|
||||
const progressInterval = setInterval(() => {
|
||||
progressIdx++;
|
||||
if (progressIdx < states.length) {
|
||||
const textEl = document.getElementById('query-progress-text');
|
||||
if (textEl) textEl.innerText = states[progressIdx];
|
||||
}
|
||||
}, 1500); // 每 1.5 秒切換一次狀態
|
||||
|
||||
try {
|
||||
const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass);
|
||||
clearInterval(progressInterval); // 收到結果,停止輪播
|
||||
|
||||
if (result.status === 'success') {
|
||||
outputEl.style.color = "#e0e0e0";
|
||||
outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data;
|
||||
|
|
@ -2479,6 +2497,7 @@ async function executeQuery(mode) {
|
|||
outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`;
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval); // 發生錯誤,停止輪播
|
||||
outputEl.style.color = "#e74c3c";
|
||||
outputEl.textContent = `連線失敗: ${error}`;
|
||||
}
|
||||
|
|
@ -2494,6 +2513,7 @@ async function fetchFullConfig(configType = 'running') {
|
|||
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.display = 'inline-block';
|
||||
loadingMsg.style.color = '#f39c12'; // 🌟 確保初始狀態為橘色
|
||||
loadingMsg.innerHTML = '⏳ 準備連線至設備...';
|
||||
}
|
||||
if (treeContainer) treeContainer.innerHTML = '';
|
||||
|
|
@ -2502,7 +2522,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
let progressInterval = null;
|
||||
|
||||
try {
|
||||
// --- 1. 版本守門員 (維持原樣) ---
|
||||
try {
|
||||
const versionRes = await apiGetCmtsVersion(connInfo.host, connInfo.user, connInfo.pass);
|
||||
if (versionRes.status === 'success') {
|
||||
|
|
@ -2524,7 +2543,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
console.warn("版本檢查失敗,略過", vErr);
|
||||
}
|
||||
|
||||
// --- 2. 載入完整配置 (ReadableStream 串流接收) ---
|
||||
const response = await fetch(`/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&config_type=${configType}`);
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
|
@ -2559,26 +2577,20 @@ async function fetchFullConfig(configType = 'running') {
|
|||
}
|
||||
}, 500);
|
||||
} else {
|
||||
// 🌟 魔法核心:當收到下一個狀態時,先平順收尾動畫
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
|
||||
// 1. 強制填滿 100% 並給予成功提示
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`;
|
||||
loadingMsg.style.color = "#27ae60"; // 瞬間變綠色增加爽感
|
||||
loadingMsg.style.color = "#27ae60";
|
||||
}
|
||||
|
||||
// 2. 刻意停頓 0.6 秒,讓大腦接收視覺回饋
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
|
||||
// 3. 恢復原本的顏色
|
||||
if (loadingMsg) loadingMsg.style.color = "";
|
||||
if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 🌟 恢復橘色
|
||||
}
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.color = "#f39c12"; // 🌟 確保橘色
|
||||
loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
|
||||
// 停頓結束後,才顯示新的狀態 (例如:🧩 正在解析配置...)
|
||||
if (loadingMsg) loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
else if (data.status === 'success') {
|
||||
|
|
@ -2597,7 +2609,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
|
||||
window.currentTreeData = finalTreeData;
|
||||
|
||||
// --- 3. 防呆攔截與畫面渲染 ---
|
||||
const currentSelectedTask = document.getElementById('configTask').value;
|
||||
const expectedTask = configType === 'running' ? 'form-running-config' : 'form-full-config';
|
||||
|
||||
|
|
@ -2625,7 +2636,7 @@ async function fetchFullConfig(configType = 'running') {
|
|||
if (progressInterval) clearInterval(progressInterval);
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.display = 'none';
|
||||
loadingMsg.style.color = ""; // 確保重置顏色
|
||||
loadingMsg.style.color = "#f39c12"; // 🌟 確保重置為橘色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2888,12 +2899,11 @@ async function openSystemSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
// 載入真實設備配置以供過濾器勾選
|
||||
// 載入真實設備配置以供過濾器勾選 - 🌟 升級為真實動態訊息流 (重用現有串流 API)
|
||||
async function loadRealConfigForFilters() {
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!");
|
||||
|
||||
// 🌟 動態抓取過濾器模式
|
||||
const modeSelect = document.getElementById('filter-mode-select');
|
||||
const mode = modeSelect ? modeSelect.value : 'running';
|
||||
|
||||
|
|
@ -2901,26 +2911,89 @@ async function loadRealConfigForFilters() {
|
|||
const container = document.getElementById('filter-checkboxes');
|
||||
|
||||
loadingMsg.style.display = 'inline-block';
|
||||
loadingMsg.style.color = '#f39c12'; // 🌟 確保橘色
|
||||
loadingMsg.innerHTML = '⏳ 準備連線至設備...';
|
||||
container.style.display = 'none';
|
||||
|
||||
try {
|
||||
// 🌟 根據模式載入對應的配置檔 (Running 或 Full)
|
||||
const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`;
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
let progressInterval = null; // 🌟 新增:進度條計時器
|
||||
|
||||
if (result.status === 'success') {
|
||||
container.style.display = 'block';
|
||||
// 🌟 傳遞 mode 給 buildRealFilterTree
|
||||
container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys, false, mode);
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
|
||||
try {
|
||||
const url = `/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
let finalTreeData = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
for (let line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
|
||||
if (data.status === 'progress') {
|
||||
// 🌟 導入百分比動畫邏輯
|
||||
if (data.message.includes('正在下載')) {
|
||||
let progress = 0;
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
|
||||
progressInterval = setInterval(() => {
|
||||
progress += (95 - progress) * 0.06;
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (${Math.round(progress)}%)...`;
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (100%) - 下載完成!`;
|
||||
loadingMsg.style.color = "#27ae60"; // 瞬間變綠色
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 恢復橘色
|
||||
}
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.color = '#f39c12';
|
||||
loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (data.status === 'success') {
|
||||
finalTreeData = data.data;
|
||||
}
|
||||
else if (data.status === 'error') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("解析串流 JSON 失敗:", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (finalTreeData) {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = buildRealFilterTree(finalTreeData, "", currentHiddenKeys, false, mode);
|
||||
} else {
|
||||
throw new Error("未收到完整的配置資料");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線或解析失敗: ${error.message}</div>`;
|
||||
} finally {
|
||||
if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
|
||||
loadingMsg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
|
@ -2980,9 +3053,8 @@ async function saveSystemSettings() {
|
|||
// 🌟 新增:全域變數用來暫存所有快照資料,實現前端秒速過濾
|
||||
let allBackupsData = [];
|
||||
|
||||
// 1. 建立新快照
|
||||
// 1. 建立新快照 - 🌟 升級為真實動態訊息流 (含百分比動畫)
|
||||
async function createSnapshot() {
|
||||
// 🛡️ 防線一:必須先連線才能備份
|
||||
if (!isConnected) {
|
||||
return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後,再執行備份任務!\n(確保您清楚目前正在操作哪一台設備)");
|
||||
}
|
||||
|
|
@ -2993,7 +3065,6 @@ async function createSnapshot() {
|
|||
const snapshotName = document.getElementById('snapshotName').value.trim();
|
||||
if (!snapshotName) return alert("請輸入快照名稱!");
|
||||
|
||||
// 🟢 抓取描述輸入框的值
|
||||
const snapshotDescription = document.getElementById('snapshotDescription')?.value.trim() || "";
|
||||
const configType = document.getElementById('backupConfigType').value;
|
||||
const btn = document.getElementById('btnCreateSnapshot');
|
||||
|
|
@ -3001,6 +3072,10 @@ async function createSnapshot() {
|
|||
|
||||
btn.disabled = true;
|
||||
msg.style.display = 'inline-block';
|
||||
msg.style.color = '#f39c12'; // 🌟 確保橘色
|
||||
msg.innerHTML = '⏳ 準備連線至設備...';
|
||||
|
||||
let progressInterval = null; // 🌟 新增:進度條計時器
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/backups/snapshot', {
|
||||
|
|
@ -3011,25 +3086,84 @@ async function createSnapshot() {
|
|||
username: connInfo.user,
|
||||
password: connInfo.pass,
|
||||
snapshot_name: snapshotName,
|
||||
description: snapshotDescription, // 🟢 將描述傳給後端
|
||||
description: snapshotDescription,
|
||||
config_type: configType
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
alert(`✅ ${result.message}`);
|
||||
document.getElementById('snapshotName').value = ''; // 清空名稱輸入框
|
||||
if (document.getElementById('snapshotDescription')) {
|
||||
document.getElementById('snapshotDescription').value = ''; // 🟢 清空描述輸入框
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
let isSuccess = false;
|
||||
let finalMessage = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
for (let line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
if (data.status === 'progress') {
|
||||
// 🌟 導入百分比動畫邏輯
|
||||
if (data.message.includes('正在下載')) {
|
||||
let progress = 0;
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
|
||||
progressInterval = setInterval(() => {
|
||||
progress += (95 - progress) * 0.06;
|
||||
if (msg) {
|
||||
msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`;
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
if (msg) {
|
||||
msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`;
|
||||
msg.style.color = "#27ae60"; // 瞬間變綠色
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
if (msg) msg.style.color = "#f39c12"; // 恢復橘色
|
||||
}
|
||||
if (msg) {
|
||||
msg.style.color = '#f39c12';
|
||||
msg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
} else if (data.status === 'success') {
|
||||
isSuccess = true;
|
||||
finalMessage = data.message;
|
||||
} else if (data.status === 'error') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message) throw e;
|
||||
console.warn("解析串流 JSON 失敗:", line);
|
||||
}
|
||||
}
|
||||
loadBackupHistory(); // 重新載入歷史紀錄
|
||||
} else {
|
||||
alert(`❌ 備份失敗: ${result.message}`);
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
alert(`✅ ${finalMessage}`);
|
||||
document.getElementById('snapshotName').value = '';
|
||||
if (document.getElementById('snapshotDescription')) {
|
||||
document.getElementById('snapshotDescription').value = '';
|
||||
}
|
||||
loadBackupHistory();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`❌ 發生錯誤: ${error.message}`);
|
||||
} finally {
|
||||
if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
|
||||
btn.disabled = false;
|
||||
msg.style.display = 'none';
|
||||
}
|
||||
|
|
@ -3044,7 +3178,8 @@ async function loadBackupHistory() {
|
|||
const tbody = document.getElementById('backup-history-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #7f8c8d;">⏳ 正在載入歷史紀錄...</td></tr>';
|
||||
// 🌟 修正:將原本的灰色改為橘色並加粗
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #f39c12; font-weight: bold;">⏳ 正在載入歷史紀錄...</td></tr>';
|
||||
|
||||
try {
|
||||
// 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來
|
||||
|
|
@ -3203,8 +3338,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) {
|
|||
|
||||
// 1. 移除進度條,改為純文字百分比動態顯示
|
||||
modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`;
|
||||
// 🌟 修正:將原本的藍色改為橘色
|
||||
modalOutput.innerHTML = `
|
||||
<div id="diff-progress-text" style="color: #3498db; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
|
||||
<div id="diff-progress-text" style="color: #f39c12; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
|
||||
`;
|
||||
modal.classList.add('active');
|
||||
|
||||
|
|
@ -4338,7 +4474,8 @@ export async function startEditLeaf(path, elementId) {
|
|||
const origVal = container.getAttribute('data-original');
|
||||
const safeOrigVal = escapeHTML(origVal);
|
||||
|
||||
container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
|
||||
// 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22)
|
||||
container.innerHTML = `<span style="font-size: 12px; color: #f39c12; margin-left: 5px; font-weight: bold;">⏳ 設備連線與載入選項中...</span>`;
|
||||
|
||||
try {
|
||||
let optData = await apiGetLeafOptions(host, currentMode);
|
||||
|
|
@ -4770,7 +4907,7 @@ async function applyEditLeaf(path, elementId) {
|
|||
export function executeSideCLI() {
|
||||
const finalScript = document.getElementById('side-cli-textarea').value;
|
||||
document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...';
|
||||
document.getElementById('side-pane-title').style.color = '#3498db';
|
||||
document.getElementById('side-pane-title').style.color = '#f39c12'; // 🌟 統一載入中狀態為橘色 (原為 #3498db)
|
||||
document.getElementById('btn-side-cancel').style.display = 'none';
|
||||
document.getElementById('btn-side-confirm').style.display = 'none';
|
||||
document.getElementById('btn-side-close').style.display = 'none';
|
||||
|
|
@ -6044,35 +6181,51 @@ async def delete_backup(backup_id: str):
|
|||
return {"status": "error", "message": "刪除失敗或找不到該筆資料"}
|
||||
return {"status": "success", "message": "快照已成功刪除"}
|
||||
|
||||
@router.post("/snapshot", summary="手動建立設備快照")
|
||||
@router.post("/snapshot", summary="手動建立設備快照 (串流版)")
|
||||
async def create_snapshot(req: SnapshotRequest):
|
||||
try:
|
||||
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
|
||||
parsed_tree = parse_cli_to_tree(raw_cli)
|
||||
async def backup_streamer():
|
||||
try:
|
||||
# 1. 廣播:正在連線
|
||||
yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n"
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
backup_id = await insert_config_backup(
|
||||
host=req.host,
|
||||
config_type=req.config_type,
|
||||
raw_cli=raw_cli,
|
||||
parsed_tree=parsed_tree,
|
||||
snapshot_name=req.snapshot_name,
|
||||
description=req.description, # 🟢 將描述傳遞給資料庫函數
|
||||
is_auto=False
|
||||
)
|
||||
# 2. 廣播:正在下載
|
||||
yield json.dumps({"status": "progress", "message": f"📥 正在下載 {req.config_type} 配置檔 (可能需要 30~60 秒)..."}) + "\n"
|
||||
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
|
||||
|
||||
if not backup_id:
|
||||
return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}
|
||||
# 3. 廣播:正在解析
|
||||
yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n"
|
||||
parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"快照 '{req.snapshot_name}' 建立成功!",
|
||||
"backup_id": backup_id,
|
||||
"host": req.host
|
||||
}
|
||||
# 4. 廣播:寫入資料庫
|
||||
yield json.dumps({"status": "progress", "message": "💾 正在將快照寫入資料庫..."}) + "\n"
|
||||
backup_id = await insert_config_backup(
|
||||
host=req.host,
|
||||
config_type=req.config_type,
|
||||
raw_cli=raw_cli,
|
||||
parsed_tree=parsed_tree,
|
||||
snapshot_name=req.snapshot_name,
|
||||
description=req.description,
|
||||
is_auto=False
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 建立快照失敗: {e}")
|
||||
return {"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}
|
||||
if not backup_id:
|
||||
yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n"
|
||||
return
|
||||
|
||||
# 5. 廣播:成功
|
||||
yield json.dumps({
|
||||
"status": "success",
|
||||
"message": f"快照 '{req.snapshot_name}' 建立成功!",
|
||||
"backup_id": backup_id,
|
||||
"host": req.host
|
||||
}) + "\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 建立快照失敗: {e}")
|
||||
yield json.dumps({"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}) + "\n"
|
||||
|
||||
return StreamingResponse(backup_streamer(), media_type="application/x-ndjson")
|
||||
|
||||
@router.post("/{backup_id}/diff", summary="分析設備當前配置與快照的差異")
|
||||
async def analyze_backup_diff(backup_id: str, req: RestoreRequest):
|
||||
|
|
|
|||
|
|
@ -149,35 +149,51 @@ async def delete_backup(backup_id: str):
|
|||
return {"status": "error", "message": "刪除失敗或找不到該筆資料"}
|
||||
return {"status": "success", "message": "快照已成功刪除"}
|
||||
|
||||
@router.post("/snapshot", summary="手動建立設備快照")
|
||||
@router.post("/snapshot", summary="手動建立設備快照 (串流版)")
|
||||
async def create_snapshot(req: SnapshotRequest):
|
||||
try:
|
||||
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
|
||||
parsed_tree = parse_cli_to_tree(raw_cli)
|
||||
async def backup_streamer():
|
||||
try:
|
||||
# 1. 廣播:正在連線
|
||||
yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n"
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
backup_id = await insert_config_backup(
|
||||
host=req.host,
|
||||
config_type=req.config_type,
|
||||
raw_cli=raw_cli,
|
||||
parsed_tree=parsed_tree,
|
||||
snapshot_name=req.snapshot_name,
|
||||
description=req.description, # 🟢 將描述傳遞給資料庫函數
|
||||
is_auto=False
|
||||
)
|
||||
# 2. 廣播:正在下載
|
||||
yield json.dumps({"status": "progress", "message": f"📥 正在下載 {req.config_type} 配置檔 (可能需要 30~60 秒)..."}) + "\n"
|
||||
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
|
||||
|
||||
if not backup_id:
|
||||
return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}
|
||||
# 3. 廣播:正在解析
|
||||
yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n"
|
||||
parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"快照 '{req.snapshot_name}' 建立成功!",
|
||||
"backup_id": backup_id,
|
||||
"host": req.host
|
||||
}
|
||||
# 4. 廣播:寫入資料庫
|
||||
yield json.dumps({"status": "progress", "message": "💾 正在將快照寫入資料庫..."}) + "\n"
|
||||
backup_id = await insert_config_backup(
|
||||
host=req.host,
|
||||
config_type=req.config_type,
|
||||
raw_cli=raw_cli,
|
||||
parsed_tree=parsed_tree,
|
||||
snapshot_name=req.snapshot_name,
|
||||
description=req.description,
|
||||
is_auto=False
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 建立快照失敗: {e}")
|
||||
return {"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}
|
||||
if not backup_id:
|
||||
yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n"
|
||||
return
|
||||
|
||||
# 5. 廣播:成功
|
||||
yield json.dumps({
|
||||
"status": "success",
|
||||
"message": f"快照 '{req.snapshot_name}' 建立成功!",
|
||||
"backup_id": backup_id,
|
||||
"host": req.host
|
||||
}) + "\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 建立快照失敗: {e}")
|
||||
yield json.dumps({"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}) + "\n"
|
||||
|
||||
return StreamingResponse(backup_streamer(), media_type="application/x-ndjson")
|
||||
|
||||
@router.post("/{backup_id}/diff", summary="分析設備當前配置與快照的差異")
|
||||
async def analyze_backup_diff(backup_id: str, req: RestoreRequest):
|
||||
|
|
|
|||
234
static/app.js
234
static/app.js
|
|
@ -425,7 +425,7 @@ function resetAllManagerData() {
|
|||
// 🌟 3. 設備查詢與全域配置載入 (Query & Full Config)
|
||||
// ============================================================================
|
||||
|
||||
// 執行綜合查詢 (CM / RPD)
|
||||
// 執行綜合查詢 (CM / RPD) - 🌟 導入前端模擬動態訊息流
|
||||
async function executeQuery(mode) {
|
||||
if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!");
|
||||
|
||||
|
|
@ -438,10 +438,28 @@ async function executeQuery(mode) {
|
|||
|
||||
openModal(connInfo.host);
|
||||
const outputEl = document.getElementById('modalOutput');
|
||||
outputEl.innerHTML = `<span style="color: #f39c12;">正在向 ${connInfo.host} 查詢中,請稍候...</span>`;
|
||||
|
||||
// 🌟 模擬動態訊息流邏輯
|
||||
const states = [
|
||||
`🔌 正在建立 SSH 連線至 ${connInfo.host}...`,
|
||||
`📥 正在向設備發送查詢指令...`,
|
||||
`🧩 正在等待設備回傳資料...`
|
||||
];
|
||||
let progressIdx = 0;
|
||||
outputEl.innerHTML = `<span id="query-progress-text" style="color: #f39c12; font-weight: bold;">${states[0]}</span>`;
|
||||
|
||||
const progressInterval = setInterval(() => {
|
||||
progressIdx++;
|
||||
if (progressIdx < states.length) {
|
||||
const textEl = document.getElementById('query-progress-text');
|
||||
if (textEl) textEl.innerText = states[progressIdx];
|
||||
}
|
||||
}, 1500); // 每 1.5 秒切換一次狀態
|
||||
|
||||
try {
|
||||
const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass);
|
||||
clearInterval(progressInterval); // 收到結果,停止輪播
|
||||
|
||||
if (result.status === 'success') {
|
||||
outputEl.style.color = "#e0e0e0";
|
||||
outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data;
|
||||
|
|
@ -450,6 +468,7 @@ async function executeQuery(mode) {
|
|||
outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`;
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval); // 發生錯誤,停止輪播
|
||||
outputEl.style.color = "#e74c3c";
|
||||
outputEl.textContent = `連線失敗: ${error}`;
|
||||
}
|
||||
|
|
@ -465,6 +484,7 @@ async function fetchFullConfig(configType = 'running') {
|
|||
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.display = 'inline-block';
|
||||
loadingMsg.style.color = '#f39c12'; // 🌟 確保初始狀態為橘色
|
||||
loadingMsg.innerHTML = '⏳ 準備連線至設備...';
|
||||
}
|
||||
if (treeContainer) treeContainer.innerHTML = '';
|
||||
|
|
@ -473,7 +493,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
let progressInterval = null;
|
||||
|
||||
try {
|
||||
// --- 1. 版本守門員 (維持原樣) ---
|
||||
try {
|
||||
const versionRes = await apiGetCmtsVersion(connInfo.host, connInfo.user, connInfo.pass);
|
||||
if (versionRes.status === 'success') {
|
||||
|
|
@ -495,7 +514,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
console.warn("版本檢查失敗,略過", vErr);
|
||||
}
|
||||
|
||||
// --- 2. 載入完整配置 (ReadableStream 串流接收) ---
|
||||
const response = await fetch(`/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&config_type=${configType}`);
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
|
@ -530,26 +548,20 @@ async function fetchFullConfig(configType = 'running') {
|
|||
}
|
||||
}, 500);
|
||||
} else {
|
||||
// 🌟 魔法核心:當收到下一個狀態時,先平順收尾動畫
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
|
||||
// 1. 強制填滿 100% 並給予成功提示
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`;
|
||||
loadingMsg.style.color = "#27ae60"; // 瞬間變綠色增加爽感
|
||||
loadingMsg.style.color = "#27ae60";
|
||||
}
|
||||
|
||||
// 2. 刻意停頓 0.6 秒,讓大腦接收視覺回饋
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
|
||||
// 3. 恢復原本的顏色
|
||||
if (loadingMsg) loadingMsg.style.color = "";
|
||||
if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 🌟 恢復橘色
|
||||
}
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.color = "#f39c12"; // 🌟 確保橘色
|
||||
loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
|
||||
// 停頓結束後,才顯示新的狀態 (例如:🧩 正在解析配置...)
|
||||
if (loadingMsg) loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
else if (data.status === 'success') {
|
||||
|
|
@ -568,7 +580,6 @@ async function fetchFullConfig(configType = 'running') {
|
|||
|
||||
window.currentTreeData = finalTreeData;
|
||||
|
||||
// --- 3. 防呆攔截與畫面渲染 ---
|
||||
const currentSelectedTask = document.getElementById('configTask').value;
|
||||
const expectedTask = configType === 'running' ? 'form-running-config' : 'form-full-config';
|
||||
|
||||
|
|
@ -596,7 +607,7 @@ async function fetchFullConfig(configType = 'running') {
|
|||
if (progressInterval) clearInterval(progressInterval);
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.display = 'none';
|
||||
loadingMsg.style.color = ""; // 確保重置顏色
|
||||
loadingMsg.style.color = "#f39c12"; // 🌟 確保重置為橘色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -859,12 +870,11 @@ async function openSystemSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
// 載入真實設備配置以供過濾器勾選
|
||||
// 載入真實設備配置以供過濾器勾選 - 🌟 升級為真實動態訊息流 (重用現有串流 API)
|
||||
async function loadRealConfigForFilters() {
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!");
|
||||
|
||||
// 🌟 動態抓取過濾器模式
|
||||
const modeSelect = document.getElementById('filter-mode-select');
|
||||
const mode = modeSelect ? modeSelect.value : 'running';
|
||||
|
||||
|
|
@ -872,26 +882,89 @@ async function loadRealConfigForFilters() {
|
|||
const container = document.getElementById('filter-checkboxes');
|
||||
|
||||
loadingMsg.style.display = 'inline-block';
|
||||
loadingMsg.style.color = '#f39c12'; // 🌟 確保橘色
|
||||
loadingMsg.innerHTML = '⏳ 準備連線至設備...';
|
||||
container.style.display = 'none';
|
||||
|
||||
try {
|
||||
// 🌟 根據模式載入對應的配置檔 (Running 或 Full)
|
||||
const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`;
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
let progressInterval = null; // 🌟 新增:進度條計時器
|
||||
|
||||
if (result.status === 'success') {
|
||||
container.style.display = 'block';
|
||||
// 🌟 傳遞 mode 給 buildRealFilterTree
|
||||
container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys, false, mode);
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
|
||||
try {
|
||||
const url = `/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
let finalTreeData = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
for (let line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
|
||||
if (data.status === 'progress') {
|
||||
// 🌟 導入百分比動畫邏輯
|
||||
if (data.message.includes('正在下載')) {
|
||||
let progress = 0;
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
|
||||
progressInterval = setInterval(() => {
|
||||
progress += (95 - progress) * 0.06;
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (${Math.round(progress)}%)...`;
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
if (loadingMsg) {
|
||||
loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (100%) - 下載完成!`;
|
||||
loadingMsg.style.color = "#27ae60"; // 瞬間變綠色
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 恢復橘色
|
||||
}
|
||||
if (loadingMsg) {
|
||||
loadingMsg.style.color = '#f39c12';
|
||||
loadingMsg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (data.status === 'success') {
|
||||
finalTreeData = data.data;
|
||||
}
|
||||
else if (data.status === 'error') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("解析串流 JSON 失敗:", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (finalTreeData) {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = buildRealFilterTree(finalTreeData, "", currentHiddenKeys, false, mode);
|
||||
} else {
|
||||
throw new Error("未收到完整的配置資料");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
|
||||
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線或解析失敗: ${error.message}</div>`;
|
||||
} finally {
|
||||
if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
|
||||
loadingMsg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
|
@ -951,9 +1024,8 @@ async function saveSystemSettings() {
|
|||
// 🌟 新增:全域變數用來暫存所有快照資料,實現前端秒速過濾
|
||||
let allBackupsData = [];
|
||||
|
||||
// 1. 建立新快照
|
||||
// 1. 建立新快照 - 🌟 升級為真實動態訊息流 (含百分比動畫)
|
||||
async function createSnapshot() {
|
||||
// 🛡️ 防線一:必須先連線才能備份
|
||||
if (!isConnected) {
|
||||
return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後,再執行備份任務!\n(確保您清楚目前正在操作哪一台設備)");
|
||||
}
|
||||
|
|
@ -964,7 +1036,6 @@ async function createSnapshot() {
|
|||
const snapshotName = document.getElementById('snapshotName').value.trim();
|
||||
if (!snapshotName) return alert("請輸入快照名稱!");
|
||||
|
||||
// 🟢 抓取描述輸入框的值
|
||||
const snapshotDescription = document.getElementById('snapshotDescription')?.value.trim() || "";
|
||||
const configType = document.getElementById('backupConfigType').value;
|
||||
const btn = document.getElementById('btnCreateSnapshot');
|
||||
|
|
@ -972,6 +1043,10 @@ async function createSnapshot() {
|
|||
|
||||
btn.disabled = true;
|
||||
msg.style.display = 'inline-block';
|
||||
msg.style.color = '#f39c12'; // 🌟 確保橘色
|
||||
msg.innerHTML = '⏳ 準備連線至設備...';
|
||||
|
||||
let progressInterval = null; // 🌟 新增:進度條計時器
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/backups/snapshot', {
|
||||
|
|
@ -982,25 +1057,84 @@ async function createSnapshot() {
|
|||
username: connInfo.user,
|
||||
password: connInfo.pass,
|
||||
snapshot_name: snapshotName,
|
||||
description: snapshotDescription, // 🟢 將描述傳給後端
|
||||
description: snapshotDescription,
|
||||
config_type: configType
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
alert(`✅ ${result.message}`);
|
||||
document.getElementById('snapshotName').value = ''; // 清空名稱輸入框
|
||||
if (document.getElementById('snapshotDescription')) {
|
||||
document.getElementById('snapshotDescription').value = ''; // 🟢 清空描述輸入框
|
||||
|
||||
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
let isSuccess = false;
|
||||
let finalMessage = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
for (let line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
if (data.status === 'progress') {
|
||||
// 🌟 導入百分比動畫邏輯
|
||||
if (data.message.includes('正在下載')) {
|
||||
let progress = 0;
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
|
||||
progressInterval = setInterval(() => {
|
||||
progress += (95 - progress) * 0.06;
|
||||
if (msg) {
|
||||
msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`;
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
if (msg) {
|
||||
msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`;
|
||||
msg.style.color = "#27ae60"; // 瞬間變綠色
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
if (msg) msg.style.color = "#f39c12"; // 恢復橘色
|
||||
}
|
||||
if (msg) {
|
||||
msg.style.color = '#f39c12';
|
||||
msg.innerHTML = data.message;
|
||||
}
|
||||
}
|
||||
} else if (data.status === 'success') {
|
||||
isSuccess = true;
|
||||
finalMessage = data.message;
|
||||
} else if (data.status === 'error') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message) throw e;
|
||||
console.warn("解析串流 JSON 失敗:", line);
|
||||
}
|
||||
}
|
||||
loadBackupHistory(); // 重新載入歷史紀錄
|
||||
} else {
|
||||
alert(`❌ 備份失敗: ${result.message}`);
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
alert(`✅ ${finalMessage}`);
|
||||
document.getElementById('snapshotName').value = '';
|
||||
if (document.getElementById('snapshotDescription')) {
|
||||
document.getElementById('snapshotDescription').value = '';
|
||||
}
|
||||
loadBackupHistory();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`❌ 發生錯誤: ${error.message}`);
|
||||
} finally {
|
||||
if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
|
||||
btn.disabled = false;
|
||||
msg.style.display = 'none';
|
||||
}
|
||||
|
|
@ -1015,7 +1149,8 @@ async function loadBackupHistory() {
|
|||
const tbody = document.getElementById('backup-history-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #7f8c8d;">⏳ 正在載入歷史紀錄...</td></tr>';
|
||||
// 🌟 修正:將原本的灰色改為橘色並加粗
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #f39c12; font-weight: bold;">⏳ 正在載入歷史紀錄...</td></tr>';
|
||||
|
||||
try {
|
||||
// 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來
|
||||
|
|
@ -1174,8 +1309,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) {
|
|||
|
||||
// 1. 移除進度條,改為純文字百分比動態顯示
|
||||
modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`;
|
||||
// 🌟 修正:將原本的藍色改為橘色
|
||||
modalOutput.innerHTML = `
|
||||
<div id="diff-progress-text" style="color: #3498db; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
|
||||
<div id="diff-progress-text" style="color: #f39c12; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
|
||||
`;
|
||||
modal.classList.add('active');
|
||||
|
||||
|
|
|
|||
|
|
@ -314,7 +314,8 @@ export async function startEditLeaf(path, elementId) {
|
|||
const origVal = container.getAttribute('data-original');
|
||||
const safeOrigVal = escapeHTML(origVal);
|
||||
|
||||
container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
|
||||
// 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22)
|
||||
container.innerHTML = `<span style="font-size: 12px; color: #f39c12; margin-left: 5px; font-weight: bold;">⏳ 設備連線與載入選項中...</span>`;
|
||||
|
||||
try {
|
||||
let optData = await apiGetLeafOptions(host, currentMode);
|
||||
|
|
@ -746,7 +747,7 @@ async function applyEditLeaf(path, elementId) {
|
|||
export function executeSideCLI() {
|
||||
const finalScript = document.getElementById('side-cli-textarea').value;
|
||||
document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...';
|
||||
document.getElementById('side-pane-title').style.color = '#3498db';
|
||||
document.getElementById('side-pane-title').style.color = '#f39c12'; // 🌟 統一載入中狀態為橘色 (原為 #3498db)
|
||||
document.getElementById('btn-side-cancel').style.display = 'none';
|
||||
document.getElementById('btn-side-confirm').style.display = 'none';
|
||||
document.getElementById('btn-side-close').style.display = 'none';
|
||||
|
|
|
|||
Loading…
Reference in New Issue