355 lines
26 KiB
JavaScript
355 lines
26 KiB
JavaScript
// --- static/tree-ui.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>
|
|
`;
|
|
|
|
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: 0.3; transition: opacity 0.2s;"
|
|
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.3" title="鎖定並編輯此群組">
|
|
✏️
|
|
</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);`;
|
|
|
|
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: 0.3; transition: opacity 0.2s;"
|
|
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.3" title="鎖定並編輯此項目">
|
|
✏️
|
|
</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('');
|
|
} |