399 lines
28 KiB
JavaScript
399 lines
28 KiB
JavaScript
// --- 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) {
|
||
if (isFilter) {
|
||
contentDiv.innerHTML = buildRealFilterTree(cache.data, currentPath, cache.hiddenKeys, isCommandGroup, mode);
|
||
} else {
|
||
contentDiv.innerHTML = buildTree(cache, currentPath, isCommandGroup, mode);
|
||
}
|
||
detailsEl.dataset.loaded = 'true'; // 標記為已載入
|
||
}
|
||
};
|
||
|
||
// ==========================================
|
||
// 🌟 輔助函數:全部展開與全部收合 (支援延遲渲染)
|
||
// ==========================================
|
||
export function expandAll(elementId) {
|
||
const details = document.getElementById(`details-${elementId}`);
|
||
if (details) {
|
||
// 如果還沒載入,先強制載入它
|
||
if (!details.dataset.loaded) {
|
||
const isFilter = details.id.includes('filter-');
|
||
window.lazyLoadFolder(elementId, isFilter);
|
||
}
|
||
details.open = true;
|
||
|
||
// 找出剛剛渲染出來的第一層子資料夾,遞迴展開
|
||
const contentDiv = details.querySelector(':scope > div');
|
||
if (contentDiv) {
|
||
const childDetails = contentDiv.querySelectorAll(':scope > details');
|
||
childDetails.forEach(d => {
|
||
const subId = d.id.replace('details-', '');
|
||
expandAll(subId); // 遞迴展開
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
export function collapseAll(elementId) {
|
||
const details = document.getElementById(`details-${elementId}`);
|
||
if (details) {
|
||
const subDetails = details.querySelectorAll('details');
|
||
subDetails.forEach(d => d.open = false);
|
||
details.open = false;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版)
|
||
// ==========================================
|
||
export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running') {
|
||
const htmlParts = [];
|
||
if (!node || typeof node !== 'object') return htmlParts.join('');
|
||
|
||
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 ? '<span style="color: #95a5a6; font-size: 0.85em; font-weight: normal; margin-left: 5px;">(指令群組)</span>' : '';
|
||
|
||
// 🌟 將資料存入快取,供延遲渲染使用
|
||
folderDataCache[elementId] = value;
|
||
|
||
const svgFolderClosed = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#f1c40f"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
|
||
const svgFolderOpen = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#f1c40f"><path d="M19 8H8.99C8.04 8 7.19 8.59 6.81 9.46L2.81 18.55C2.62 18.98 2.94 19.5 3.41 19.5H16.99C17.94 19.5 18.79 18.91 19.17 18.04L23.17 8.95C23.36 8.52 23.04 8 22.57 8H19zM4 4c-1.1 0-2 .9-2 2v10.59l3.09-7.04C5.58 8.36 6.27 8 7.01 8H19v-2c0-1.1-.9-2-2-2h-7l-2-2H4c-1.1 0-2 .9-2 2v12z"/></svg>`;
|
||
const svgGroupClosed = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#95a5a6"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`;
|
||
const svgGroupOpen = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#3498db"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`;
|
||
|
||
const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed;
|
||
const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen;
|
||
|
||
const expandAllIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>`;
|
||
const collapseAllIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>`;
|
||
|
||
const expandCollapseBtns = `
|
||
<span style="margin-left: 8px; display: inline-flex; gap: 6px; align-items: center;">
|
||
<span onclick="event.stopPropagation(); event.preventDefault(); expandAll('${elementId}')"
|
||
style="cursor: pointer; color: #95a5a6; transition: color 0.2s; display: flex; align-items: center;"
|
||
onmouseover="this.style.color='#3498db'" onmouseout="this.style.color='#95a5a6'" title="展開全部子項目">
|
||
${expandAllIcon}
|
||
</span>
|
||
<span onclick="event.stopPropagation(); event.preventDefault(); collapseAll('${elementId}')"
|
||
style="cursor: pointer; color: #95a5a6; transition: color 0.2s; display: flex; align-items: center;"
|
||
onmouseover="this.style.color='#e67e22'" onmouseout="this.style.color='#95a5a6'" title="收合全部子項目">
|
||
${collapseAllIcon}
|
||
</span>
|
||
</span>
|
||
`;
|
||
|
||
// 🌟 延遲渲染防護:取得當前設備 IP,並檢查此節點是否已被鎖定 (跨設備隔離)
|
||
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] && window.globalActiveLocks[currentHost][currentPath]) {
|
||
const lockInfo = window.globalActiveLocks[currentHost][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}] 編輯中`
|
||
: `🔒 您正在另一個視圖編輯此項目`;
|
||
}
|
||
}
|
||
|
||
htmlParts.push(`
|
||
<details id="details-${elementId}" class="tree-folder" style="margin-top: 4px; margin-left: 20px;"
|
||
data-path="${currentPath}" data-is-group="${isCommandGroup}" data-mode="${mode}"
|
||
ontoggle="this.querySelector('.icon-closed').style.display = this.open ? 'none' : 'inline-block'; this.querySelector('.icon-open').style.display = this.open ? 'inline-block' : 'none'; if(this.open) window.lazyLoadFolder('${elementId}', false);">
|
||
<summary class="tree-folder-header"
|
||
title="${cliCommand}"
|
||
onmouseover="this.style.backgroundColor='#f1f2f6'"
|
||
onmouseout="this.style.backgroundColor='transparent'"
|
||
style="cursor: pointer; padding: 4px 8px; display: flex; align-items: center; outline: none; border-radius: 4px; transition: background-color 0.2s;">
|
||
|
||
<span style="margin-right: 6px; display: inline-flex; align-items: center; width: 18px; height: 18px;">
|
||
<span class="icon-closed" style="display: inline-block;">${iconClosed}</span>
|
||
<span class="icon-open" style="display: none;">${iconOpen}</span>
|
||
</span>
|
||
<b style="color: #2c3e50;">${key}</b>${folderSuffix}
|
||
${expandCollapseBtns}
|
||
|
||
<div style="margin-left: auto; display: flex; align-items: center;">
|
||
<span class="debug-preview-btn admin-only"
|
||
onclick="event.stopPropagation(); event.preventDefault(); previewNodeCLI(this, '${currentPath}')"
|
||
style="display: none; cursor: pointer; margin-right: 8px; font-size: 14px; transition: transform 0.2s;"
|
||
title="[開發者] 預覽此節點下所有指令"
|
||
onmouseover="this.style.transform='scale(1.2)'"
|
||
onmouseout="this.style.transform='scale(1)'">
|
||
🔍
|
||
</span>
|
||
<span id="edit-btn-${elementId}" class="edit-btn" data-path="${currentPath}" onclick="event.stopPropagation(); event.preventDefault(); startEditFolder('${currentPath}', '${elementId}')"
|
||
style="cursor: pointer; font-size: 14px; opacity: ${lockOpacity}; pointer-events: ${lockPointer}; transition: opacity 0.2s;"
|
||
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=${lockOpacity}" title="${lockTitle}">
|
||
${lockText}
|
||
</span>
|
||
<span id="actions-${elementId}" style="display:none; margin-left: 10px;">
|
||
<button onclick="event.stopPropagation(); event.preventDefault(); previewFolderCLI('${currentPath}', '${elementId}')" style="background-color: #27ae60; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;">✅ 預覽指令</button>
|
||
<button onclick="event.stopPropagation(); event.preventDefault(); cancelEditFolder('${currentPath}', '${elementId}')" style="background-color: #e74c3c; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 5px;">❌ 取消</button>
|
||
</span>
|
||
</div>
|
||
</summary>
|
||
<div id="content-${elementId}" class="tree-folder-content" style="margin-left: 12px; border-left: 1px dashed #bdc3c7; padding-left: 12px;">
|
||
<!-- 🌟 延遲渲染:這裡一開始是空的,展開時才會填入 -->
|
||
</div>
|
||
</details>
|
||
`);
|
||
} 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 svgLeaf = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#bdc3c7"><path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/></svg>`;
|
||
const svgDisabled = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#e74c3c"><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-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"/></svg>`;
|
||
|
||
const iconHtml = (icon) => `<span style="margin-right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px;">${icon}</span>`;
|
||
const valueStyle = `color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);`;
|
||
|
||
// 🌟 延遲渲染防護:取得當前設備 IP,並檢查此節點是否已被鎖定 (跨設備隔離)
|
||
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] && window.globalActiveLocks[currentHost][currentPath]) {
|
||
const lockInfo = window.globalActiveLocks[currentHost][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}] 編輯中`
|
||
: `🔒 您正在另一個視圖編輯此項目`;
|
||
}
|
||
}
|
||
|
||
let displayContent = '';
|
||
|
||
if (isVirtualIndex) {
|
||
displayContent = `
|
||
${iconHtml(svgLeaf)}
|
||
<span class="leaf-container" id="container-${elementId}" data-path="${currentPath}" data-original="${safeValue}" style="display: inline-flex; align-items: center; min-height: 24px;">
|
||
<span class="leaf-value" style="${valueStyle}">${safeValue}</span>
|
||
</span>
|
||
`;
|
||
} else if (isNoCommand) {
|
||
displayContent = `
|
||
<span id="display-wrapper-${elementId}" style="display: inline-flex; align-items: center;">
|
||
${iconHtml(svgDisabled)}
|
||
<b style="color: #e74c3c;">no:</b>
|
||
<span class="leaf-container" id="container-${elementId}" data-path="${currentPath}" data-original="${safeValue}" style="display: inline-flex; align-items: center; min-height: 24px;">
|
||
<span class="leaf-value" style="color: #7f8c8d; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeValue}</span>
|
||
</span>
|
||
</span>
|
||
`;
|
||
} else {
|
||
displayContent = `
|
||
${iconHtml(svgLeaf)} <b>${key}</b>:
|
||
<span class="leaf-container" id="container-${elementId}" data-path="${currentPath}" data-original="${safeValue}" style="display: inline-flex; align-items: center; min-height: 24px;">
|
||
<span class="leaf-value" style="${valueStyle}">${safeValue}</span>
|
||
</span>
|
||
`;
|
||
}
|
||
|
||
htmlParts.push(`
|
||
<div class="tree-leaf-node"
|
||
title="${cliCommand}"
|
||
onmouseover="this.style.backgroundColor='#f1f2f6'"
|
||
onmouseout="this.style.backgroundColor='transparent'"
|
||
style="padding: 4px 8px; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; display: flex; align-items: center; border-radius: 4px; transition: background-color 0.2s;">
|
||
${displayContent}
|
||
|
||
<div style="margin-left: auto; display: flex; align-items: center;">
|
||
<span class="debug-preview-btn admin-only"
|
||
onclick="event.stopPropagation(); event.preventDefault(); previewNodeCLI(this, '${currentPath}')"
|
||
style="display: none; cursor: pointer; margin-right: 8px; font-size: 14px; transition: transform 0.2s;"
|
||
title="[開發者] 預覽此節點下所有指令"
|
||
onmouseover="this.style.transform='scale(1.2)'"
|
||
onmouseout="this.style.transform='scale(1)'">
|
||
🔍
|
||
</span>
|
||
<span id="edit-btn-${elementId}" class="edit-btn" data-path="${currentPath}" onclick="event.stopPropagation(); event.preventDefault(); startEditLeaf('${currentPath}', '${elementId}')"
|
||
style="cursor: pointer; font-size: 14px; opacity: ${lockOpacity}; pointer-events: ${lockPointer}; transition: opacity 0.2s;"
|
||
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=${lockOpacity}" title="${lockTitle}">
|
||
${lockText}
|
||
</span>
|
||
<span id="actions-${elementId}" style="display:none; margin-left: 10px;">
|
||
<button onclick="event.stopPropagation(); event.preventDefault(); previewLeafCLI('${currentPath}', '${elementId}')" style="background-color: #27ae60; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;">✅ 預覽指令</button>
|
||
<button onclick="event.stopPropagation(); event.preventDefault(); cancelEditLeaf('${currentPath}', '${elementId}')" style="background-color: #e74c3c; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 5px;">❌ 取消</button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
`);
|
||
}
|
||
}
|
||
return htmlParts.join('');
|
||
}
|
||
|
||
// ==========================================
|
||
// 🌟 核心函數:生成系統設定的過濾樹狀圖 (Lazy 版)
|
||
// ==========================================
|
||
export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running') {
|
||
if (typeof data !== 'object' || data === null) return '';
|
||
|
||
const htmlParts = [];
|
||
htmlParts.push(`<div style="margin-left: ${parentPath ? '24px' : '0'};">`);
|
||
|
||
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 svgFolderClosed = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#f1c40f"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
|
||
const svgFolderOpen = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#f1c40f"><path d="M19 8H8.99C8.04 8 7.19 8.59 6.81 9.46L2.81 18.55C2.62 18.98 2.94 19.5 3.41 19.5H16.99C17.94 19.5 18.79 18.91 19.17 18.04L23.17 8.95C23.36 8.52 23.04 8 22.57 8H19zM4 4c-1.1 0-2 .9-2 2v10.59l3.09-7.04C5.58 8.36 6.27 8 7.01 8H19v-2c0-1.1-.9-2-2-2h-7l-2-2H4c-1.1 0-2 .9-2 2v12z"/></svg>`;
|
||
const svgGroupClosed = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#95a5a6"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`;
|
||
const svgGroupOpen = `<svg width="18" height="18" viewBox="0 0 24 24" fill="#3498db"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`;
|
||
const svgLeaf = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#bdc3c7"><path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/></svg>`;
|
||
|
||
const isFolder = typeof value === 'object' && value !== null && Object.keys(value).length > 0;
|
||
const onChangeEvent = isFolder ? 'toggleChildCheckboxes(this)' : 'updateParentCheckboxState(this)';
|
||
const checkboxHtml = `<input type="checkbox" value="${currentPath}" class="filter-checkbox" ${isChecked} onclick="event.stopPropagation();" onchange="${onChangeEvent}" style="margin-right: 8px; cursor: pointer; width: 14px; height: 14px; flex-shrink: 0;">`;
|
||
|
||
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 ? `<span style="color: #95a5a6; font-size: 0.85em; font-weight: normal; margin-left: 5px;">(指令群組)</span>` : '';
|
||
|
||
const expandAllIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>`;
|
||
const collapseAllIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>`;
|
||
|
||
const expandCollapseBtns = `
|
||
<span style="margin-left: 8px; display: inline-flex; gap: 6px; align-items: center;">
|
||
<span onclick="event.stopPropagation(); event.preventDefault(); expandAll('${elementId}')"
|
||
style="cursor: pointer; color: #95a5a6; transition: color 0.2s; display: flex; align-items: center;"
|
||
onmouseover="this.style.color='#3498db'" onmouseout="this.style.color='#95a5a6'" title="展開全部子項目">
|
||
${expandAllIcon}
|
||
</span>
|
||
<span onclick="event.stopPropagation(); event.preventDefault(); collapseAll('${elementId}')"
|
||
style="cursor: pointer; color: #95a5a6; transition: color 0.2s; display: flex; align-items: center;"
|
||
onmouseover="this.style.color='#e67e22'" onmouseout="this.style.color='#95a5a6'" title="收合全部子項目">
|
||
${collapseAllIcon}
|
||
</span>
|
||
</span>
|
||
`;
|
||
|
||
// 🌟 加入 lazyLoadFolder 觸發
|
||
htmlParts.push(`
|
||
<details id="details-${elementId}" class="tree-folder"
|
||
data-path="${currentPath}" data-is-group="${isCommandGroup}" data-mode="${mode}"
|
||
ontoggle="this.querySelector('.icon-closed').style.display = this.open ? 'none' : 'inline-block'; this.querySelector('.icon-open').style.display = this.open ? 'inline-block' : 'none'; if(this.open) window.lazyLoadFolder('${elementId}', true);">
|
||
<summary class="tree-node-header" title="${cliCommand}" style="font-weight: bold; color: #2c3e50; margin-top: 4px; list-style: none; display: flex; align-items: center; cursor: pointer; outline: none; padding: 4px 0;">
|
||
<style>#details-${elementId} > summary::-webkit-details-marker { display: none; }</style>
|
||
${checkboxHtml}
|
||
<span style="margin-right: 6px; display: inline-flex; align-items: center; width: 18px; height: 18px;">
|
||
<span class="icon-closed" style="display: inline-block;">${iconClosed}</span>
|
||
<span class="icon-open" style="display: none;">${iconOpen}</span>
|
||
</span>
|
||
<span>${key}${folderSuffix}</span>
|
||
${expandCollapseBtns}
|
||
</summary>
|
||
<div id="filter-content-${elementId}" class="filter-children-container" style="border-left: 1px dashed #bdc3c7; margin-left: 7px; padding-left: 16px;">
|
||
<!-- 🌟 延遲渲染 -->
|
||
</div>
|
||
</details>
|
||
`);
|
||
} else {
|
||
const isVirtualIndex = /^\[\d+\]$/.test(key);
|
||
let displayName = isVirtualIndex ? `<span style="color: #95a5a6; font-weight: normal;">項目 ${key}</span>` : `<b>${key}</b>`;
|
||
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
|
||
|
||
const isNoCommand = (key === 'no');
|
||
|
||
let valueHtml = `<span style="color: #16a085; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeValue}</span>`;
|
||
let colonHtml = `:`;
|
||
let currentIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#bdc3c7"><path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/></svg>`;
|
||
|
||
if (isNoCommand) {
|
||
let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim();
|
||
cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`;
|
||
|
||
displayName = `<b style="color: #e74c3c;">no</b>`;
|
||
colonHtml = `:`;
|
||
valueHtml = `<span style="color: #7f8c8d; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeValue}</span>`;
|
||
currentIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#e74c3c"><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-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"/></svg>`;
|
||
}
|
||
|
||
htmlParts.push(`
|
||
<div class="tree-leaf-node"
|
||
onmouseover="this.style.backgroundColor='#f1f2f6'"
|
||
onmouseout="this.style.backgroundColor='transparent'"
|
||
style="padding: 2px 0; color: #34495e; display: flex; align-items: center; margin-top: 2px; border-radius: 4px; transition: background-color 0.2s;"
|
||
title="${cliCommand}">
|
||
${checkboxHtml}
|
||
<span style="margin-right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex-shrink: 0;">
|
||
${currentIcon}
|
||
</span>
|
||
<span style="margin-right: 5px;">${displayName}${colonHtml}</span>
|
||
${valueHtml}
|
||
</div>
|
||
`);
|
||
}
|
||
}
|
||
htmlParts.push('</div>');
|
||
return htmlParts.join('');
|
||
} |