`;
for (const key in data) {
const value = data[key];
const currentPath = parentPath ? `${parentPath}::${key}` : key;
const isChecked = hiddenKeys.includes(currentPath) ? "checked" : "";
// 💡 統一在這裡解析 cliCommand,讓資料夾與 Leaf 都能擁有 Hint,並拔除虛擬索引 [0]
let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
const svgFolderClosed = `
`;
const svgFolderOpen = `
`;
const svgGroupClosed = `
`;
const svgGroupOpen = `
`;
const svgLeaf = `
`;
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-folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed;
const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen;
const folderSuffix = isCommandGroup ? `
(指令群組)` : '';
const expandAllIcon = `
`;
const collapseAllIcon = `
`;
const expandCollapseBtns = `
${expandAllIcon}
${collapseAllIcon}
`;
html += `
${buildRealFilterTree(value, currentPath, hiddenKeys, isCommandGroup)}
`;
} else {
// 🌟 處理單一設定項目 (Leaf)
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 = `
`;
// 💡 統一 UI 顯示:紅色禁止符號 + 紅色 no: + 灰色數值
if (isNoCommand) {
let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim();
cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`;
displayName = `
no`;
colonHtml = `:`;
valueHtml = `
${safeValue}`;
currentIcon = `
`;
}
html += `
${checkboxHtml}
${currentIcon}
${displayName}${colonHtml}
${valueHtml}
`;
}
}
html += '
';
return html;
}
// 🔽 當點擊「父節點」時:同步勾選/取消所有子節點
function toggleChildCheckboxes(parentCheckbox) {
const details = parentCheckbox.closest('details');
if (details) {
// 找到該資料夾內所有的子 Checkbox
const childCheckboxes = details.querySelectorAll('.filter-children-container .filter-checkbox');
childCheckboxes.forEach(cb => {
cb.checked = parentCheckbox.checked;
});
}
// 往上檢查是否需要更新更上層的父節點
updateParentCheckboxState(parentCheckbox);
}
// 🔼 當點擊「子節點」時:往上檢查父節點是否該被勾選
function updateParentCheckboxState(element) {
// 找到包住這個元素的上一層 details
const parentDetails = element.closest('.filter-children-container')?.closest('details');
if (!parentDetails) return;
const parentCheckbox = parentDetails.querySelector('summary .filter-checkbox');
const childCheckboxes = parentDetails.querySelectorAll('.filter-children-container .filter-checkbox');
if (parentCheckbox && childCheckboxes.length > 0) {
// 如果所有的子節點都被勾選了,父節點就自動打勾;只要有一個沒勾,父節點就取消打勾
const allChecked = Array.from(childCheckboxes).every(cb => cb.checked);
parentCheckbox.checked = allChecked;
// 遞迴往上檢查,確保多層級的資料夾也能正確連動
updateParentCheckboxState(parentCheckbox);
}
}
async function saveSystemSettings() {
const checkboxes = document.querySelectorAll('.filter-checkbox:checked');
const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value);
try {
const response = await fetch('/api/v1/settings/tree-filters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hidden_keys: selectedHiddenKeys })
});
const result = await response.json();
if (result.status === 'success') {
const statusText = document.getElementById('settings-status');
statusText.style.display = 'inline-block';
currentHiddenKeys = selectedHiddenKeys; // 更新本地狀態
setTimeout(() => { statusText.style.display = 'none'; }, 3000);
}
} catch (error) {
console.error("儲存設定失敗:", error);
alert("儲存設定時發生錯誤!");
}
}
// ==========================================
// 8. Modal 彈出視窗控制
// ==========================================
function openModal(targetInfo) {
document.getElementById('outputModal').classList.add('active');
document.getElementById('modalTargetInfo').textContent = targetInfo ? `(${targetInfo})` : '';
document.body.style.overflow = 'hidden';
}
function closeModal(event) {
if (event && event.target !== event.currentTarget && event.target.className !== 'modal-close') return;
document.getElementById('outputModal').classList.remove('active');
document.body.style.overflow = '';
}
// ==========================================
// 💡 掃描按鈕狀態管理 (支援跨重新整理鎖定)
// ==========================================
// 1. 網頁載入時,檢查是否還有背景任務在跑
document.addEventListener("DOMContentLoaded", () => {
checkScanButtonState();
});
function checkScanButtonState() {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
const scanLockUntil = localStorage.getItem('scanLockUntil');
const now = Date.now();
if (scanLockUntil && now < parseInt(scanLockUntil)) {
// 如果還在鎖定期間內,恢復鎖定狀態
lockScanButton(parseInt(scanLockUntil) - now);
}
}
// 2. 鎖定按鈕的 UI 變化
function lockScanButton(durationMs) {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
btn.disabled = true;
btn.style.backgroundColor = "#e67e22"; // 變成醒目的橘色
btn.style.cursor = "not-allowed";
btn.innerText = "⏳ 背景掃描執行中...";
// 設定計時器,時間到自動解鎖
setTimeout(() => {
// 只有當樹狀圖已經載入時,才恢復成可點擊的紫色
const treeContainer = document.getElementById('tree-container');
if (treeContainer && treeContainer.innerHTML.includes('leaf-container')) {
enableGlobalScanButton();
} else {
// 樹狀圖沒載入,退回初始灰階狀態
btn.disabled = true;
btn.style.backgroundColor = "#bdc3c7";
btn.innerText = "⏳ 請先載入樹狀圖";
}
localStorage.removeItem('scanLockUntil');
}, durationMs);
}
// 3. 提供給「樹狀圖載入完成」時呼叫的解鎖函數
function enableGlobalScanButton() {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
const scanLockUntil = localStorage.getItem('scanLockUntil');
// 如果目前沒有被鎖定,才把它變成可點擊的紫色
if (!scanLockUntil || Date.now() >= parseInt(scanLockUntil)) {
btn.disabled = false;
btn.style.backgroundColor = "#8e44ad"; // 恢復紫色
btn.style.cursor = "pointer";
btn.innerText = "🔍 掃描畫面缺失選項";
}
}
// ==========================================
// 🌟 新增:清除畫面選項快取功能 (精準可見度判定版)
// ==========================================
async function clearVisibleCache() {
const elements = document.querySelectorAll('.leaf-container');
const pathsToClear = [];
elements.forEach(el => {
// 檢查這個元素是否被包在「未展開」的資料夾裡面
const isHiddenByFolder = el.closest('details:not([open])') !== null;
if (!isHiddenByFolder) {
const path = el.getAttribute('data-path');
if (path) {
const cacheKey = path.replace(/::/g, ' ');
if (!pathsToClear.includes(cacheKey)) {
pathsToClear.push(cacheKey);
}
}
}
});
if (pathsToClear.length === 0) {
alert("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
return;
}
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「一鍵掃描」)`)) return;
try {
const response = await fetch('/api/v1/clear_cache', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(pathsToClear)
});
if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
const result = await response.json();
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!\n現在請點擊旁邊的「一鍵掃描缺失選項」來重新擷取。`);
} catch (error) {
console.error("清除快取失敗:", error);
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
}
}
/**
* 將 no 指令原地變身為啟用狀態
* @param {string} elementId - 該節點的 elementId (例如 leaf-no-cable-...)
* @param {string} newCommand - 解除 no 之後的完整指令 (例如 "cable dynamic-secret tftp...")
*/
function transformNoToActive(elementId, newCommand) {
const wrapper = document.getElementById(`display-wrapper-${elementId}`);
if (!wrapper) return;
// 1. 解析指令:切出第一個字 (如 cable) 與後續指令
const parts = newCommand.trim().split(' ');
const firstWord = parts[0]; // "cable"
const restCommand = parts.slice(1).join(' '); // "dynamic-secret..."
// 💡 2. 換成視覺平衡的「空心綠圈圈 + 打勾」圖示
const svgEnabled = `