scm-harmonic-cmts-admin/static/edit-mode.js

756 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- static/edit-mode.js ---
import {
apiAcquireLock, apiReleaseLock, apiSendHeartbeat,
apiGetLeafOptions, apiSyncLeafOptions, apiGenerateCli, apiExecuteConfig,
apiSyncLeafOptionsStream // 🌟 新增這一個
} from './api.js';
import { getGlobalConnectionInfo } from './utils.js';
// --- 安全跳脫 HTML 特殊字元,防止破版 ---
function escapeHTML(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ==========================================
// 1. 全域狀態與鎖定管理 (Lock Management)
// ==========================================
export const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9);
const ACTIVE_HEARTBEATS = {};
// 💡 追蹤當前編輯狀態
export let currentEditPath = null;
export let currentEditElementId = null;
export let currentEditIsSuccess = false;
export let currentEditType = null;
export let currentEditDiffs = []; // 🌟 新增:用來記錄這次修改了哪些路徑
export async function releaseAllLocks() {
const keysToRelease = Object.keys(ACTIVE_HEARTBEATS);
if (keysToRelease.length === 0) return;
const releasePromises = keysToRelease.map(key => {
// 🌟 解析出 host 與 path
const [host, path] = key.split('@@');
clearInterval(ACTIVE_HEARTBEATS[key]);
delete ACTIVE_HEARTBEATS[key];
return apiReleaseLock(path, SESSION_USER_ID, host)
.catch(err => console.error(`釋放鎖定 ${path} 失敗:`, err));
});
await Promise.all(releasePromises);
alert("您已閒置超過 5 分鐘,系統已自動釋放編輯鎖定並重新整理頁面。");
location.reload();
}
// 🌟 加入 host 與 lockKey 參數
async function sendHeartbeat(path, host, intervalId, lockKey) {
try {
const response = await apiSendHeartbeat(path, SESSION_USER_ID, host);
if (!response.ok) {
console.warn(`心跳發送失敗 (${path}): 伺服器回傳 ${response.status}`);
if (response.status === 404) {
clearInterval(intervalId);
if (ACTIVE_HEARTBEATS[lockKey] === intervalId) {
delete ACTIVE_HEARTBEATS[lockKey];
}
}
}
} catch (error) {
console.error(`心跳請求發生錯誤 (${path}):`, error);
}
}
// ==========================================
// 2. 編輯模式啟動與取消 (Folder & Leaf)
// ==========================================
export async function startEditFolder(path, elementId) {
const connInfo = getGlobalConnectionInfo();
const username = connInfo ? connInfo.user : 'admin';
const host = connInfo ? connInfo.host : ''; // 🌟 取得設備 IP
const lockKey = `${host}@@${path}`;
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
try {
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
if (result.data.status === 'success') {
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
let intervalId;
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
ACTIVE_HEARTBEATS[lockKey] = intervalId;
// 🌟 修正:加入安全檢查,避免找不到元素時發生 null 錯誤
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (editBtn) editBtn.style.display = 'none';
const actionBtn = document.getElementById(`actions-${elementId}`);
if (actionBtn) actionBtn.style.display = 'inline-block';
const detailsEl = document.getElementById(`details-${elementId}`);
if (detailsEl) detailsEl.open = true;
const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) {
console.warn(`[防呆警告] 找不到 content-${elementId} 的容器,請略過此資料夾的編輯。`);
return; // 找不到內容容器就提早結束,避免後續 querySelectorAll 報錯
}
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
let optData = {};
// 🌟 修改 1傳入 host 給 apiGetLeafOptions
try { optData = await apiGetLeafOptions(host, currentMode); } catch (e) {}
const pathsToSync = [];
// ==========================================
// 🌟 效能急救:分批渲染 (Chunking) 避免卡死主執行緒
// ==========================================
const containersArray = Array.from(leafContainers);
const CHUNK_SIZE = 50; // 每次處理 50 個,確保畫面不結凍
for (let i = 0; i < containersArray.length; i += CHUNK_SIZE) {
const chunk = containersArray.slice(i, i + CHUNK_SIZE);
chunk.forEach(container => {
const origVal = container.getAttribute('data-original');
const rawPath = container.getAttribute('data-path');
const safeOrigVal = escapeHTML(origVal);
let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (rawPath.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
let leafCache = optData[cacheKey];
if (!leafCache) pathsToSync.push(cacheKey);
let inputHtml = "";
let hintAttr = "";
let hintIcon = "";
if (leafCache && leafCache.hint) {
const safeHint = leafCache.hint.replace(/"/g, '&quot;');
hintAttr = `title="${safeHint}"`;
hintIcon = `<span style="cursor: help; margin-left: 6px; color: #e67e22; font-size: 14px;" title="${safeHint}">❓</span>`;
}
if (leafCache && leafCache.options && leafCache.options.length > 0) {
let options = leafCache.options;
if (origVal && !options.includes(origVal)) options = [origVal, ...options];
inputHtml = `<select class="edit-input" data-path="${rawPath}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => {
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
});
inputHtml += `</select>${hintIcon}`;
} else {
inputHtml = `<input type="text" class="edit-input" data-path="${rawPath}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
}
container.innerHTML = inputHtml;
});
// 🌟 關鍵魔法:每處理完 50 個,就暫停 0 毫秒,讓瀏覽器有空檔去畫畫面或回應滑鼠
await new Promise(resolve => setTimeout(resolve, 0));
}
// 🌟 傳入 host 給 apiSyncLeafOptions
if (pathsToSync.length > 0) apiSyncLeafOptions(host, pathsToSync, currentMode);
}
} catch (error) {
alert("❌ 無法連線到鎖定伺服器:" + error.message);
}
}
export async function startEditLeaf(path, elementId) {
const connInfo = getGlobalConnectionInfo();
const username = connInfo ? connInfo.user : 'admin';
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
try {
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
if (result.data.status === 'success') {
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
let intervalId;
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
ACTIVE_HEARTBEATS[lockKey] = intervalId;
document.getElementById(`edit-btn-${elementId}`).style.display = 'none';
document.getElementById(`actions-${elementId}`).style.display = 'inline-block';
const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
try {
// 🌟 修改 1傳入 host
let optData = await apiGetLeafOptions(host, currentMode);
let cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (path.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
let leafCache = optData[cacheKey];
if (!leafCache) {
// 🌟 修改 2傳入 host
apiSyncLeafOptions(host, [cacheKey], currentMode);
let found = false;
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
// 🌟 修改 3傳入 host
optData = await apiGetLeafOptions(host, currentMode);
leafCache = optData[cacheKey];
if (leafCache && leafCache.options && leafCache.options.length > 0) {
found = true; break;
}
}
}
let inputHtml = "";
let hintAttr = "";
let hintIcon = "";
if (leafCache && leafCache.hint) {
const safeHint = leafCache.hint.replace(/"/g, '&quot;');
hintAttr = `title="${safeHint}"`;
hintIcon = `<span style="cursor: help; margin-left: 6px; color: #e67e22; font-size: 14px;" title="${safeHint}">❓</span>`;
}
if (leafCache && leafCache.options && leafCache.options.length > 0) {
let options = leafCache.options;
if (origVal && !options.includes(origVal)) options = [origVal, ...options];
inputHtml = `<select class="edit-input" data-path="${path}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => {
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
});
inputHtml += `</select>${hintIcon}`;
} else {
inputHtml = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
}
container.innerHTML = inputHtml;
} catch (e) {
container.innerHTML = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">`;
}
}
} catch (error) {
alert("❌ 無法連線到鎖定伺服器:" + error.message);
}
}
export async function cancelEditFolder(path, elementId) {
const connInfo = getGlobalConnectionInfo();
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
if (ACTIVE_HEARTBEATS[lockKey]) {
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
delete ACTIVE_HEARTBEATS[lockKey];
}
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
document.getElementById(`actions-${elementId}`).style.display = 'none';
const contentDiv = document.getElementById(`content-${elementId}`);
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
leafContainers.forEach(container => {
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
});
}
export async function cancelEditLeaf(path, elementId) {
const connInfo = getGlobalConnectionInfo();
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
if (ACTIVE_HEARTBEATS[lockKey]) {
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
delete ACTIVE_HEARTBEATS[lockKey];
}
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
document.getElementById(`actions-${elementId}`).style.display = 'none';
const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
}
// ==========================================
// 3. CLI 生成與預覽 (CLI Generation)
// ==========================================
function getInterfacesWithAdminState() {
// 🌟 限制只從當前顯示的樹狀圖中抓取狀態
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
const activeContainer = document.getElementById(`tree-container-${currentMode}`);
if (!activeContainer) return [];
const nodes = activeContainer.querySelectorAll('.leaf-container');
const interfaces = new Set();
nodes.forEach(node => {
const path = node.getAttribute('data-path');
if (path && path.endsWith('::admin-state')) {
const interfacePath = path.replace('::admin-state', '').replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
interfaces.add(interfacePath);
}
});
return Array.from(interfaces);
}
export async function previewFolderCLI(path, elementId) {
currentEditPath = path;
currentEditElementId = elementId;
currentEditIsSuccess = false;
currentEditType = 'folder';
const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) return;
const inputs = contentDiv.querySelectorAll('.edit-input');
const diffs = [];
inputs.forEach(input => {
const container = input.closest('.leaf-container');
if (!container) return;
const originalVal = container.getAttribute('data-original') || "";
const newVal = input.value.trim();
const nodePath = container.getAttribute('data-path');
if (originalVal !== newVal && nodePath) {
diffs.push({ path: nodePath, old_val: originalVal, new_val: newVal });
}
});
if (diffs.length === 0) return alert("沒有偵測到任何修改,無需生成指令!");
currentEditDiffs = diffs; // 🌟 新增這行:記錄修改差異
try {
const result = await apiGenerateCli(diffs, getInterfacesWithAdminState());
if (result.status === 'success') {
showCliPreviewModal(result.data);
} else {
alert("CLI 生成失敗: " + result.message);
}
} catch (error) {
console.error("Error generating CLI:", error);
alert("網路連線錯誤,無法生成 CLI");
}
}
export async function previewLeafCLI(path, elementId) {
currentEditPath = path;
currentEditElementId = elementId;
currentEditIsSuccess = false;
currentEditType = 'leaf';
const container = document.getElementById(`container-${elementId}`);
if (!container) return;
const input = container.querySelector('.edit-input');
if (!input) return;
const originalVal = container.getAttribute('data-original') || "";
const newVal = input.value.trim();
if (originalVal === newVal) return alert("沒有偵測到任何修改,無需生成指令!");
const diffs = [{ path: path, old_val: originalVal, new_val: newVal }];
currentEditDiffs = diffs; // 🌟 新增這行:記錄修改差異
try {
const result = await apiGenerateCli(diffs, getInterfacesWithAdminState());
if (result.status === 'success') {
showCliPreviewModal(result.data);
} else {
alert("CLI 生成失敗: " + result.message);
}
} catch (error) {
alert("網路連線錯誤,無法生成 CLI");
}
}
function showCliPreviewModal(cliScript) {
// 🌟 修改:改用 class 統一控制所有樹狀圖容器
const leftPanes = document.querySelectorAll('.tree-view-instance');
const resizer = document.getElementById('drag-resizer');
const rightPane = document.getElementById('side-cli-preview');
document.getElementById('side-pane-title').innerHTML = '⚠️ 即將寫入的指令';
document.getElementById('side-pane-title').style.color = '#f1c40f';
document.getElementById('btn-side-cancel').style.display = 'inline-block';
document.getElementById('btn-side-confirm').style.display = 'inline-block';
document.getElementById('btn-side-close').style.display = 'none';
document.getElementById('btn-side-close').textContent = '關閉';
document.getElementById('side-cli-textarea').style.display = 'block';
document.getElementById('side-execution-result').style.display = 'none';
const textarea = document.getElementById('side-cli-textarea');
textarea.value = cliScript;
// 👇 確保正常模式可以編輯,並明確恢復深色主題
textarea.readOnly = false;
textarea.style.backgroundColor = '#1e1e1e'; // 恢復成標準的深色背景 (您可以依喜好微調顏色碼)
textarea.style.color = '#ecf0f1'; // 確保字體是清晰的淺灰色
// 🌟 修改:遍歷所有樹狀圖容器進行縮放
leftPanes.forEach(pane => pane.style.width = 'calc(50% - 11px)');
rightPane.style.width = 'calc(50% - 11px)';
resizer.style.display = 'flex';
rightPane.style.display = 'block';
if (!window.isResizerInitialized) {
initResizer();
window.isResizerInitialized = true;
}
}
// ==========================================
// 4. 側邊欄與執行邏輯
// ==========================================
export async function hideSideCLI() {
// 🌟 修改:改用 class 統一控制所有樹狀圖容器
document.querySelectorAll('.tree-view-instance').forEach(pane => pane.style.width = '100%');
document.getElementById('drag-resizer').style.display = 'none';
document.getElementById('side-cli-preview').style.display = 'none';
if (currentEditIsSuccess && currentEditElementId && currentEditPath) {
const wrapper = document.getElementById(`display-wrapper-${currentEditElementId}`);
if (wrapper) {
const container = document.getElementById(`container-${currentEditElementId}`);
const input = container ? container.querySelector('.edit-input') : null;
const newCommand = input ? input.value.trim() : (container ? container.getAttribute('data-original') : '');
if (currentEditType === 'folder') await cancelEditFolder(currentEditPath, currentEditElementId);
else await cancelEditLeaf(currentEditPath, currentEditElementId);
transformNoToActive(currentEditElementId, newCommand);
} else {
if (currentEditType === 'folder') await applyEditFolder(currentEditPath, currentEditElementId);
else if (currentEditType === 'leaf') await applyEditLeaf(currentEditPath, currentEditElementId);
}
}
currentEditPath = null;
currentEditElementId = null;
currentEditIsSuccess = false;
currentEditType = null;
}
async function applyEditFolder(path, elementId) {
const contentDiv = document.getElementById(`content-${elementId}`);
if (contentDiv) {
const inputs = contentDiv.querySelectorAll('.edit-input');
inputs.forEach(input => {
const container = input.closest('.leaf-container');
if (container) container.setAttribute('data-original', input.value.trim());
});
}
await cancelEditFolder(path, elementId);
}
async function applyEditLeaf(path, elementId) {
const container = document.getElementById(`container-${elementId}`);
if (container) {
const input = container.querySelector('.edit-input');
if (input) container.setAttribute('data-original', input.value.trim());
}
await cancelEditLeaf(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('btn-side-cancel').style.display = 'none';
document.getElementById('btn-side-confirm').style.display = 'none';
document.getElementById('btn-side-close').style.display = 'none';
document.getElementById('side-cli-textarea').style.display = 'none';
const resultBox = document.getElementById('side-execution-result');
resultBox.style.display = 'block';
resultBox.innerHTML = `<span style="color: #ecf0f1;">${finalScript}</span>`;
executeGeneratedCLI(finalScript);
}
async function executeGeneratedCLI(script) {
const connInfo = getGlobalConnectionInfo();
if (!connInfo) return;
const outputEl = document.getElementById('side-execution-result');
try {
const result = await apiExecuteConfig(script, connInfo.host, connInfo.user, connInfo.pass);
if (result.status === 'success') {
currentEditIsSuccess = true;
document.getElementById('side-pane-title').innerHTML = '✅ 寫入成功!正在從設備同步最新狀態...';
document.getElementById('side-pane-title').style.color = '#f39c12';
outputEl.innerHTML = `<span style="color: #ecf0f1;">${result.data}\n\n[系統] 正在背景重新抓取變更欄位的最新選項,請稍候...</span>`;
const pathsToSync = currentEditDiffs.map(d => {
let cacheKey = d.path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (d.path.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + d.old_val;
return cacheKey;
});
const uniquePaths = [...new Set(pathsToSync)];
try {
// 🌟 修改:取得當前模式,並將 connInfo.host 與 currentMode 傳給 apiSyncLeafOptionsStream
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
await apiSyncLeafOptionsStream(connInfo.host, uniquePaths, (data) => {
if (data.event === 'done') {
document.getElementById('side-pane-title').innerHTML = '✅ 執行與快取同步完美達成';
document.getElementById('side-pane-title').style.color = '#2ecc71';
document.getElementById('btn-side-close').style.display = 'inline-block';
outputEl.innerHTML += `<br><br><span style="color: #2ecc71;">[系統] 快取同步完成!您可以關閉此面板。</span>`;
} else if (data.event === 'error') {
document.getElementById('side-pane-title').innerHTML = '✅ 寫入成功 (但同步快取失敗)';
document.getElementById('side-pane-title').style.color = '#e74c3c';
document.getElementById('btn-side-close').style.display = 'inline-block';
outputEl.innerHTML += `<br><br><span style="color: #e74c3c;">[系統] 同步失敗: ${data.message}</span>`;
}
}, currentMode);
} catch (syncErr) {
document.getElementById('btn-side-close').style.display = 'inline-block';
}
} else {
currentEditIsSuccess = false;
document.getElementById('btn-side-close').style.display = 'inline-block';
document.getElementById('side-pane-title').innerHTML = '❌ 執行失敗';
document.getElementById('side-pane-title').style.color = '#ff6b6b';
const rawLog = result.detail || result.message || '';
outputEl.innerHTML = `<span style="color: #ecf0f1;">${highlightTerminalOutput(rawLog)}</span>`;
}
} catch (error) {
document.getElementById('btn-side-close').style.display = 'inline-block';
document.getElementById('side-pane-title').innerHTML = '❌ 連線錯誤';
document.getElementById('side-pane-title').style.color = '#ff6b6b';
outputEl.innerHTML = `<span style="color: #ff6b6b; font-weight: bold;">連線或伺服器錯誤: ${error}</span>`;
}
}
// ==========================================
// 5. 輔助 UI 函數
// ==========================================
function initResizer() {
const resizer = document.getElementById('drag-resizer');
// 🌟 修改:改用 class 統一控制所有樹狀圖容器
const leftPanes = document.querySelectorAll('.tree-view-instance');
const rightPane = document.getElementById('side-cli-preview');
const container = document.getElementById('split-container');
let isResizing = false;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const containerRect = container.getBoundingClientRect();
let newLeftWidth = e.clientX - containerRect.left;
const minWidth = containerRect.width * 0.2;
const maxWidth = containerRect.width * 0.8;
if (newLeftWidth < minWidth) newLeftWidth = minWidth;
if (newLeftWidth > maxWidth) newLeftWidth = maxWidth;
const leftPercentage = (newLeftWidth / containerRect.width) * 100;
const rightPercentage = 100 - leftPercentage;
// 🌟 修改:遍歷所有樹狀圖容器進行縮放
leftPanes.forEach(pane => pane.style.width = `calc(${leftPercentage}% - 11px)`);
rightPane.style.width = `calc(${rightPercentage}% - 11px)`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = 'default';
document.body.style.userSelect = 'auto';
}
});
}
function highlightTerminalOutput(text) {
if (!text) return '';
const lines = text.split('\n');
const formattedLines = lines.map(line => {
if (line.match(/(Aborted|Error|Warning|Failed|% Invalid|% Unknown|Bad command)/i)) {
return `<span style="color: #ff6b6b; font-weight: bold;">${line}</span>`;
}
return line;
});
return formattedLines.join('\n');
}
function transformNoToActive(elementId, newCommand) {
const wrapper = document.getElementById(`display-wrapper-${elementId}`);
if (!wrapper) return;
const parts = newCommand.trim().split(' ');
const firstWord = parts[0];
const restCommand = parts.slice(1).join(' ');
const safeRestCommand = escapeHTML(restCommand);
const svgEnabled = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#27ae60"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zM8.29 11.59L7 12.88l4 4 8-8-1.29-1.29L11 14.18z"/></svg>`;
const iconHtml = `<span style="margin-right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px;">${svgEnabled}</span>`;
wrapper.innerHTML = `
${iconHtml}
<b style="color: #2c3e50;">${firstWord}</b>
<span class="leaf-container" style="display: inline-flex; align-items: center; min-height: 24px;">
<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeRestCommand}</span>
<span style="font-size: 0.8em; color: #f39c12; margin-left: 10px; border: 1px solid #f39c12; padding: 2px 6px; border-radius: 4px; background-color: #fffdf7;">(已啟用,重整後歸檔)</span>
</span>
`;
const actionBtns = document.getElementById(`actions-${elementId}`);
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (actionBtns) actionBtns.style.display = 'none';
if (editBtn) editBtn.style.display = 'none';
}
// ============================================================================
// 💻 開發者工具:預覽節點完整指令 (唯讀 Debug 模式)
// ============================================================================
export async function previewNodeCLI(btnElement, rootPath) {
// 1. 定位當前點擊的容器
let container = btnElement.closest('details');
if (container) {
const elementId = container.id.replace('details-', '');
// 確保延遲渲染的內容已經載入 (Lazy Load)
if (container.dataset.loaded !== 'true') {
await window.lazyLoadFolder(elementId, false);
}
}
const targetNode = container || btnElement.closest('.tree-leaf-node');
if (!targetNode) return;
// 2. 收集所有子節點資料
const leafNodes = targetNode.querySelectorAll('.leaf-container');
const interfaceGroups = {};
leafNodes.forEach(node => {
const path = node.getAttribute('data-path') || "";
const val = node.getAttribute('data-original');
if (!path) return;
const parts = path.split('::');
const paramName = parts.pop();
// 移除虛擬陣列索引 [0], [1] 等,還原真實介面路徑
let interfacePath = parts.join(' ').replace(/\s*\[\d+\]/g, '');
if (!interfaceGroups[interfacePath]) {
interfaceGroups[interfacePath] = [];
}
interfaceGroups[interfacePath].push({ param: paramName, val: val });
});
// 補強邏輯:處理使用者直接點擊葉節點的情況
if (Object.keys(interfaceGroups).length === 0 && targetNode.classList.contains('tree-leaf-node')) {
const singleNode = targetNode.querySelector('.leaf-container');
if (singleNode) {
const path = singleNode.getAttribute('data-path');
const val = singleNode.getAttribute('data-original');
const parts = path.split('::');
const paramName = parts.pop();
let interfacePath = parts.join(' ').replace(/\s*\[\d+\]/g, '');
interfaceGroups[interfacePath] = [{ param: paramName, val: val }];
}
}
if (Object.keys(interfaceGroups).length === 0) {
return alert("此節點下沒有任何數值可供預覽!");
}
// 3. 在前端組裝平坦化 CLI (模擬真實設備指令)
let cliLines = [];
cliLines.push("! --- Read-Only Configuration Preview ---");
for (const [interfacePath, params] of Object.entries(interfaceGroups)) {
cliLines.push(`! Node: ${interfacePath || 'Global'}`);
params.forEach(({param, val}) => {
if (/^\[\d+\]$/.test(param)) {
// 陣列元素本身就是值
if (val) {
cliLines.push(interfacePath ? `${interfacePath} ${val}` : val);
}
} else if (param === 'no') {
// 處理 no 指令
cliLines.push(interfacePath ? `${interfacePath} no ${val}` : `no ${val}`);
} else {
// 一般鍵值對 (若 val 為空,代表是單純的 flag 指令)
let line = interfacePath ? `${interfacePath} ${param}` : param;
if (val) line += ` ${val}`;
cliLines.push(line);
}
});
cliLines.push("!");
}
const finalScript = cliLines.join('\n');
// 4. 顯示唯讀預覽面板
showReadOnlyCliPreviewModal(finalScript, rootPath);
}
// 專為 God-Mode 設計的唯讀面板 UI
function showReadOnlyCliPreviewModal(cliScript, rootPath) {
const leftPanes = document.querySelectorAll('.tree-view-instance');
const resizer = document.getElementById('drag-resizer');
const rightPane = document.getElementById('side-cli-preview');
document.getElementById('side-pane-title').innerHTML = `🔍 [唯讀預覽] ${rootPath}`;
document.getElementById('side-pane-title').style.color = '#3498db';
// 隱藏執行按鈕,只保留關閉
document.getElementById('btn-side-confirm').style.display = 'none';
document.getElementById('btn-side-cancel').style.display = 'none';
const closeBtn = document.getElementById('btn-side-close');
closeBtn.style.display = 'inline-block';
closeBtn.textContent = '關閉預覽';
const textarea = document.getElementById('side-cli-textarea');
textarea.style.display = 'block';
textarea.value = cliScript;
textarea.readOnly = true; // 強制唯讀
textarea.style.backgroundColor = '#2c3e50'; // 改變背景色提示唯讀狀態
document.getElementById('side-execution-result').style.display = 'none';
leftPanes.forEach(pane => pane.style.width = 'calc(50% - 11px)');
rightPane.style.width = 'calc(50% - 11px)';
if (resizer) resizer.style.display = 'flex';
if (rightPane) rightPane.style.display = 'block';
if (!window.isResizerInitialized && typeof initResizer === 'function') {
initResizer();
window.isResizerInitialized = true;
}
}
// --- EOF (檔案結束) ---