// --- static/tree-ui.js --- import { SESSION_USER_ID, currentEditElementId } from './edit-mode.js'; // ========================================== // 🌟 效能終極優化:全域快取與延遲渲染 (Lazy Rendering) // ========================================== export const folderDataCache = {}; export const filterFolderCache = {}; // 🧹 [修復 Leak] 徹底清空樹狀圖快取,釋放記憶體 export function clearTreeCache() { for (let key in folderDataCache) delete folderDataCache[key]; for (let key in filterFolderCache) delete filterFolderCache[key]; } // 魔法函數:當資料夾被點擊展開時,才將 HTML 渲染進去 window.lazyLoadFolder = function(elementId, isFilter = false) { const detailsEl = document.getElementById(`details-${elementId}`); // 如果已經載入過,就不重複渲染 if (!detailsEl || detailsEl.dataset.loaded === 'true') return; // 從 dataset 讀取當初存下來的屬性 const currentPath = detailsEl.dataset.path; const isCommandGroup = detailsEl.dataset.isGroup === 'true'; const mode = detailsEl.dataset.mode; const contentDiv = document.getElementById(isFilter ? `filter-content-${elementId}` : `content-${elementId}`); const cache = isFilter ? filterFolderCache[elementId] : folderDataCache[elementId]; if (contentDiv && cache) { // 🌟 統一渲染作法:單點展開也套用非同步渲染保護 contentDiv.innerHTML = `⏳ 載入中...`; document.body.style.cursor = 'wait'; setTimeout(() => { if (isFilter) { contentDiv.innerHTML = buildRealFilterTree(cache.data, currentPath, cache.hiddenKeys, isCommandGroup, mode); } else { contentDiv.innerHTML = buildTree(cache, currentPath, isCommandGroup, mode); } detailsEl.dataset.loaded = 'true'; // 標記為已載入 document.body.style.cursor = 'default'; }, 10); } }; // ========================================== // 🌟 輔助函數:全部展開與全部收合 (支援延遲渲染) // ========================================== export function expandAll(elementId) { const details = document.getElementById(`details-${elementId}`); if (!details) return; const isFilter = details.id.includes('filter-'); const cache = isFilter ? filterFolderCache[elementId] : folderDataCache[elementId]; if (!cache) return; // 🌟 讓滑鼠變成讀取狀態,給予使用者即時回饋 document.body.style.cursor = 'wait'; const contentDiv = document.getElementById(isFilter ? `filter-content-${elementId}` : `content-${elementId}`); if (contentDiv) { contentDiv.innerHTML = `⏳ 正在極速展開中,請稍候...`; } // 🌟 使用 setTimeout 讓出一個 Frame,讓瀏覽器能渲染出「讀取中」的文字與滑鼠狀態 setTimeout(() => { const currentPath = details.dataset.path; const isCommandGroup = details.dataset.isGroup === 'true'; const mode = details.dataset.mode; // 🚀 核心魔法:在記憶體中一次性遞迴生成所有子節點的 HTML 字串 (傳入 forceExpand = true) let fullHtml = ''; if (isFilter) { fullHtml = buildRealFilterTree(cache.data, currentPath, cache.hiddenKeys, isCommandGroup, mode, true); } else { fullHtml = buildTree(cache, currentPath, isCommandGroup, mode, true); } // 🚀 終極效能:只執行 1 次 DOM 寫入! if (contentDiv) contentDiv.innerHTML = fullHtml; details.dataset.loaded = 'true'; details.open = true; const iconClosed = details.querySelector('.icon-closed'); const iconOpen = details.querySelector('.icon-open'); if (iconClosed) iconClosed.style.display = 'none'; if (iconOpen) iconOpen.style.display = 'inline-block'; document.body.style.cursor = 'default'; }, 20); } export function collapseAll(elementId) { const details = document.getElementById(`details-${elementId}`); if (details) { // 🌟 效能優化:只尋找目前是 open 狀態的 details 進行關閉,減少 DOM 操作 const openSubDetails = details.querySelectorAll('details[open]'); openSubDetails.forEach(d => { d.open = false; const iconClosed = d.querySelector('.icon-closed'); const iconOpen = d.querySelector('.icon-open'); if (iconClosed) iconClosed.style.display = 'inline-block'; if (iconOpen) iconOpen.style.display = 'none'; }); details.open = false; const mainIconClosed = details.querySelector('.icon-closed'); const mainIconOpen = details.querySelector('.icon-open'); if (mainIconClosed) mainIconClosed.style.display = 'inline-block'; if (mainIconOpen) mainIconOpen.style.display = 'none'; } } // ========================================== // 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版) // ========================================== // 🌟 修改函數宣告,加入 renderAll 參數 export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running', forceExpand = false, renderAll = false) { const htmlParts = []; if (!node || typeof node !== 'object') return htmlParts.join(''); const svgFolderClosed = ``; const svgFolderOpen = ``; const svgGroupClosed = ``; const svgGroupOpen = ``; const expandAllIcon = ``; const collapseAllIcon = ``; const svgLeaf = ``; const svgDisabled = ``; for (const [key, value] of Object.entries(node)) { const currentPath = path ? `${path}::${key}` : key; let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); if (typeof value === 'object' && value !== null) { const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null); const isCommandGroup = isParentCommandGroup || !hasNestedObject; const elementId = `${mode}-folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; const folderSuffix = isCommandGroup ? '(指令群組)' : ''; folderDataCache[elementId] = value; const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed; const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen; const expandCollapseBtns = ` ${expandAllIcon} ${collapseAllIcon} `; const currentHost = document.getElementById('cmtsHost')?.value.trim(); let isLocked = false; let lockTitle = "鎖定並編輯此群組"; let lockText = "✏️"; let lockOpacity = "0.3"; let lockPointer = "auto"; if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) { const hostLocks = window.globalActiveLocks[currentHost]; if (hostLocks[currentPath]) { const lockInfo = hostLocks[currentPath]; if (!(lockInfo.user_id === SESSION_USER_ID && elementId === currentEditElementId)) { isLocked = true; lockText = "🔒"; lockOpacity = "0.2"; lockPointer = "none"; lockTitle = lockInfo.user_id !== SESSION_USER_ID ? `🔒 此區塊正由 [${lockInfo.username}] 編輯中` : `🔒 您正在另一個視圖編輯此項目`; } } else { for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) { if (currentPath.startsWith(lockedPath + "::")) { isLocked = true; lockText = "🔒"; lockOpacity = "0.2"; lockPointer = "none"; lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`; break; } } } } // 🚀 核心魔法:如果 forceExpand 為 true,直接在記憶體中遞迴生成子節點 HTML const isOpenAttr = forceExpand ? 'open' : ''; const isLoadedAttr = (forceExpand || renderAll) ? 'true' : 'false'; const displayClosed = forceExpand ? 'none' : 'inline-block'; const displayOpen = forceExpand ? 'inline-block' : 'none'; let innerContent = ''; if (forceExpand || renderAll) { // 🌟 遞迴呼叫時,把 renderAll 傳遞下去 innerContent = buildTree(value, currentPath, isCommandGroup, mode, forceExpand, renderAll); } htmlParts.push(`
${iconClosed} ${iconOpen} ${key}${folderSuffix} ${expandCollapseBtns}
${lockText}
${innerContent}
`); } else { const isVirtualIndex = /^\[\d+\]$/.test(key); const isNoCommand = (key === 'no'); const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); const elementId = `${mode}-leaf-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; if (isNoCommand) { let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim(); cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`; } const iconHtml = (icon) => `${icon}`; const valueStyle = `color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);`; const currentHost = document.getElementById('cmtsHost')?.value.trim(); let isLocked = false; let lockTitle = "鎖定並編輯此項目"; let lockText = "✏️"; let lockOpacity = "0.3"; let lockPointer = "auto"; if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) { const hostLocks = window.globalActiveLocks[currentHost]; if (hostLocks[currentPath]) { const lockInfo = hostLocks[currentPath]; if (!(lockInfo.user_id === SESSION_USER_ID && elementId === currentEditElementId)) { isLocked = true; lockText = "🔒"; lockOpacity = "0.2"; lockPointer = "none"; lockTitle = lockInfo.user_id !== SESSION_USER_ID ? `🔒 此區塊正由 [${lockInfo.username}] 編輯中` : `🔒 您正在另一個視圖編輯此項目`; } } else { for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) { if (currentPath.startsWith(lockedPath + "::")) { isLocked = true; lockText = "🔒"; lockOpacity = "0.2"; lockPointer = "none"; lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`; break; } } } } let displayContent = ''; if (isVirtualIndex) { displayContent = ` ${iconHtml(svgLeaf)} ${safeValue} `; } else if (isNoCommand) { displayContent = ` ${iconHtml(svgDisabled)} no: ${safeValue} `; } else { displayContent = ` ${iconHtml(svgLeaf)} ${key}: ${safeValue} `; } htmlParts.push(`
${displayContent}
${lockText}
`); } } return htmlParts.join(''); } // ========================================== // 🌟 核心函數:生成系統設定的過濾樹狀圖 (Lazy 版) // ========================================== // 🌟 新增 forceExpand 參數,預設為 false export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running', forceExpand = false) { if (typeof data !== 'object' || data === null) return ''; const htmlParts = []; htmlParts.push(`
`); const svgFolderClosed = ``; const svgFolderOpen = ``; const svgGroupClosed = ``; const svgGroupOpen = ``; const svgLeaf = ``; const expandAllIcon = ``; const collapseAllIcon = ``; for (const key in data) { const value = data[key]; const currentPath = parentPath ? `${parentPath}::${key}` : key; const isChecked = hiddenKeys.includes(currentPath) ? "checked" : ""; let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); const isFolder = typeof value === 'object' && value !== null && Object.keys(value).length > 0; const onChangeEvent = isFolder ? 'toggleChildCheckboxes(this)' : 'updateParentCheckboxState(this)'; const checkboxHtml = ``; if (isFolder) { const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null); const isCommandGroup = isParentCommandGroup || !hasNestedObject; const elementId = `filter-${mode}-folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; filterFolderCache[elementId] = { data: value, hiddenKeys: hiddenKeys }; const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed; const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen; const folderSuffix = isCommandGroup ? `(指令群組)` : ''; const expandCollapseBtns = ` ${expandAllIcon} ${collapseAllIcon} `; // 🚀 核心魔法:如果 forceExpand 為 true,直接在記憶體中遞迴生成子節點 HTML const isOpenAttr = forceExpand ? 'open' : ''; const isLoadedAttr = forceExpand ? 'true' : 'false'; const displayClosed = forceExpand ? 'none' : 'inline-block'; const displayOpen = forceExpand ? 'inline-block' : 'none'; let innerContent = ''; if (forceExpand) { innerContent = buildRealFilterTree(value, currentPath, hiddenKeys, isCommandGroup, mode, true); } htmlParts.push(`
${checkboxHtml} ${iconClosed} ${iconOpen} ${key}${folderSuffix} ${expandCollapseBtns}
${innerContent}
`); } else { const isVirtualIndex = /^\[\d+\]$/.test(key); let displayName = isVirtualIndex ? `項目 ${key}` : `${key}`; const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); const isNoCommand = (key === 'no'); let valueHtml = `${safeValue}`; let colonHtml = `:`; let currentIcon = ``; if (isNoCommand) { let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim(); cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`; displayName = `no`; colonHtml = `:`; valueHtml = `${safeValue}`; currentIcon = ``; } htmlParts.push(`
${checkboxHtml} ${currentIcon} ${displayName}${colonHtml} ${valueHtml}
`); } } htmlParts.push('
'); return htmlParts.join(''); } // ========================================== // 🌟 輔助函數:強制預先渲染資料夾 HTML (供編輯模式使用) // ========================================== window.forceRenderFolderHTML = function(elementId) { const detailsEl = document.getElementById(`details-${elementId}`); const contentDiv = document.getElementById(`content-${elementId}`); const cache = folderDataCache[elementId]; if (detailsEl && contentDiv && cache && detailsEl.dataset.loaded !== 'true') { const currentPath = detailsEl.dataset.path; const isCommandGroup = detailsEl.dataset.isGroup === 'true'; const mode = detailsEl.dataset.mode; // 重新渲染,傳入 renderAll = true (生成 HTML 但不展開) contentDiv.innerHTML = buildTree(cache, currentPath, isCommandGroup, mode, false, true); detailsEl.dataset.loaded = 'true'; } };