refactor: isNoCommand 讀取寫入邏輯重整
This commit is contained in:
parent
523ebc706f
commit
8f4faa8956
|
|
@ -99028,5 +99028,71 @@
|
|||
"type": "string_or_number",
|
||||
"current_value": null,
|
||||
"updated_at": 1778489033
|
||||
},
|
||||
"cable mac-domain 13:0/0.0 shared-secret": {
|
||||
"hint": "Description: shared-secret {0 | 7}\nPossible completions:\n<shared-secret {0 | 7}>[7] <cr>",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "7",
|
||||
"updated_at": 1778495587
|
||||
},
|
||||
"network link-holdoff-time-ms": {
|
||||
"hint": "Description: Link bring-up delay in milliseconds\nPossible completions:\n<unsignedInt>[0]",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "0",
|
||||
"updated_at": 1778566706
|
||||
},
|
||||
"network l3-toward-access": {
|
||||
"hint": "Description: Enables/disables L3 mode\nPossible completions:\n[disabled] disabled enabled",
|
||||
"options": [
|
||||
"disabled",
|
||||
"enabled"
|
||||
],
|
||||
"type": "list",
|
||||
"current_value": "disabled",
|
||||
"updated_at": 1778566706
|
||||
},
|
||||
"logging buffered": {
|
||||
"hint": "Possible completions:\nseverity size-mb",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": null,
|
||||
"updated_at": 1778570349
|
||||
},
|
||||
"cable rip": {
|
||||
"hint": "Possible completions:\nallow-subnet Allowed subnets for RIP routes\nauthentication",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": null,
|
||||
"updated_at": 1778570650
|
||||
},
|
||||
"cable bundle 1 nsi start-ipv6-address": {
|
||||
"hint": "Description: Starting address within ipv6 subnet on the CRE for the bundle nsi subnet\nPossible completions:\n<IPv6 address>[2001:50:5:11::7]",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "2001:50:5:11::7",
|
||||
"updated_at": 1778582997
|
||||
},
|
||||
"cable bundle 1 nsi cre-ipv4-address": {
|
||||
"hint": "Description: CRE gateway IP address for the bundle nsi subnet\nPossible completions:\n<string>[10.14.11.1/24]",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "10.14.11.1/24",
|
||||
"updated_at": 1778582997
|
||||
},
|
||||
"cable bundle 1 nsi cre-ipv6-address": {
|
||||
"hint": "Description: CRE gateway IPv6 address for the bundle nsi subnet\nPossible completions:\n<string>[2001:50:5:11::1/64]",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "2001:50:5:11::1/64",
|
||||
"updated_at": 1778582997
|
||||
},
|
||||
"cable bundle 1 nsi pool-count": {
|
||||
"hint": "Description: Number of IP addresses starting from \"start-ipv4-addr/start-ipv6-addr\" used as nsi\nPossible completions:\n<unsignedInt>[10]",
|
||||
"options": [],
|
||||
"type": "string_or_number",
|
||||
"current_value": "10",
|
||||
"updated_at": 1778582997
|
||||
}
|
||||
}
|
||||
58
shared.py
58
shared.py
|
|
@ -26,10 +26,45 @@ def parse_cli_to_tree(cli_text: str) -> dict:
|
|||
"""
|
||||
將 CMTS 的 CLI 配置文字轉換為完美的邏輯巢狀 JSON (兩階段解析法)
|
||||
"""
|
||||
lines = [line.rstrip() for line in cli_text.splitlines() if line.strip()]
|
||||
# ==========================================
|
||||
# 🌟 階段 0:預處理 (修復終端機 200 字元自動折行問題)
|
||||
# ==========================================
|
||||
raw_lines = cli_text.split('\n')
|
||||
fixed_lines = []
|
||||
|
||||
# 定義常見的 Root 指令關鍵字,避免把正常的指令誤判為斷行
|
||||
root_keywords = (
|
||||
'alias', 'ssh', 'hostname', 'logging', 'cable', 'ipdr',
|
||||
'snmp-server', 'aaa', 'radius-server', 'privilege', 'cli', 'packetcable',
|
||||
'system', 'network', 'clock', 'no'
|
||||
)
|
||||
|
||||
for line in raw_lines:
|
||||
line = line.replace('\r', '')
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# 💡 判斷是否為被截斷的行:
|
||||
# 1. 前一行長度非常長 (大於 190 字元,根據您的截圖,極限是 199)
|
||||
# 2. 當前行「沒有縮排」(終端機自動折行會把字元推到最左邊第 0 格)
|
||||
# 3. 當前行不是註解 '!'
|
||||
if fixed_lines and len(fixed_lines[-1]) > 190 and not line.startswith(' ') and not line.startswith('!'):
|
||||
# 取得當前行的第一個單字
|
||||
first_word = line.strip().split()[0] if line.strip() else ""
|
||||
|
||||
# 確保它不是一個正常的 Root 指令
|
||||
if first_word not in root_keywords:
|
||||
# ✅ 觸發黏合邏輯:將這行直接接在上一行後面
|
||||
fixed_lines[-1] = fixed_lines[-1] + line
|
||||
continue
|
||||
|
||||
fixed_lines.append(line)
|
||||
|
||||
# 統一去除右側多餘空白,準備進入階段 1
|
||||
lines = [line.rstrip() for line in fixed_lines if line.strip()]
|
||||
|
||||
# ==========================================
|
||||
# 階段 1:建立實體樹 (依據縮排與 '!')
|
||||
# 階段 1:建立實體樹 (嚴格比對縮排與 '!' 的對應關係)
|
||||
# ==========================================
|
||||
def build_raw_tree(start_idx, current_indent):
|
||||
nodes = []
|
||||
|
|
@ -37,26 +72,25 @@ def parse_cli_to_tree(cli_text: str) -> dict:
|
|||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
# 遇到 ! 的處理邏輯
|
||||
if stripped == '!':
|
||||
if current_indent > 0:
|
||||
return nodes, i + 1 # 子區塊遇到 !,結束並消耗該行
|
||||
else:
|
||||
i += 1 # Root 層級的 ! 只是分隔符號,直接跳過
|
||||
continue
|
||||
|
||||
indent = len(line) - len(line.lstrip())
|
||||
|
||||
# 縮排退回,隱含結束當前區塊 (不消耗該行)
|
||||
# 🌟 嚴格判斷 1:如果遇到縮排退回 (包含縮排退回的 !),代表當前深度的區塊已結束
|
||||
if indent < current_indent:
|
||||
return nodes, i
|
||||
|
||||
# 🌟 嚴格判斷 2:處理同層級的 !
|
||||
if stripped == '!':
|
||||
# 能走到這裡,代表 indent >= current_indent
|
||||
# 這是對應當前層級的結束符號或分隔符,我們將其消耗掉並繼續,絕對不提早 return
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 探測是否有子節點
|
||||
has_children = False
|
||||
if i + 1 < len(lines):
|
||||
next_line = lines[i+1]
|
||||
next_stripped = next_line.strip()
|
||||
# 只有當下一行不是 ! 且縮排大於當前行時,才判定為有子節點
|
||||
if next_stripped != '!':
|
||||
next_indent = len(next_line) - len(next_line.lstrip())
|
||||
if next_indent > indent:
|
||||
|
|
|
|||
271
static/app.js
271
static/app.js
|
|
@ -178,7 +178,12 @@ async function startEditFolder(path, elementId) {
|
|||
const origVal = container.getAttribute('data-original');
|
||||
const rawPath = container.getAttribute('data-path');
|
||||
// 🌟 轉換為空白格式後,全域拔除路徑中所有的虛擬索引 [0], [1]
|
||||
const cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
|
||||
// 💡 攔截 no 指令:將查詢路徑改為實際指令
|
||||
if (rawPath.endsWith('::no')) {
|
||||
cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
|
||||
}
|
||||
|
||||
let leafCache = optData[cacheKey];
|
||||
|
||||
|
|
@ -265,7 +270,13 @@ async function startEditLeaf(path, elementId) {
|
|||
let optData = await optRes.json();
|
||||
|
||||
// 🌟 轉換為空白格式後,全域拔除路徑中所有的虛擬索引 [0], [1]
|
||||
const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
let cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
|
||||
// 💡 攔截 no 指令:將查詢路徑改為實際指令
|
||||
if (path.endsWith('::no')) {
|
||||
cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
|
||||
}
|
||||
|
||||
let leafCache = optData[cacheKey];
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
|
@ -545,17 +556,41 @@ function showCliPreviewModal(cliScript) {
|
|||
}
|
||||
|
||||
// ❌ 隱藏側邊預覽區塊 (恢復左側 100% 寬度)
|
||||
function hideSideCLI() {
|
||||
async function hideSideCLI() {
|
||||
document.getElementById('tree-container').style.width = '100%';
|
||||
document.getElementById('drag-resizer').style.display = 'none';
|
||||
document.getElementById('side-cli-preview').style.display = 'none';
|
||||
|
||||
// 💡 根據編輯類型,呼叫對應的套用函數
|
||||
if (currentEditIsSuccess && currentEditElementId && currentEditPath) {
|
||||
|
||||
// 🔍 檢查這個節點是不是 no 指令 (透過尋找我們剛加的 display-wrapper)
|
||||
const wrapper = document.getElementById(`display-wrapper-${currentEditElementId}`);
|
||||
|
||||
if (wrapper) {
|
||||
// 🌟 這是 no 指令的變身邏輯!
|
||||
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') : '');
|
||||
|
||||
// 1. 先正常解除後端鎖定 (這會還原原本的 HTML 結構)
|
||||
if (currentEditType === 'folder') {
|
||||
applyEditFolder(currentEditPath, currentEditElementId);
|
||||
await cancelEditFolder(currentEditPath, currentEditElementId);
|
||||
} else {
|
||||
await cancelEditLeaf(currentEditPath, currentEditElementId);
|
||||
}
|
||||
|
||||
// 2. 觸發原地變身!(這會把整個 wrapper 換成綠色打勾的樣式)
|
||||
transformNoToActive(currentEditElementId, newCommand);
|
||||
|
||||
} else {
|
||||
// 🌟 一般正常指令的套用邏輯 (維持您原本的寫法)
|
||||
if (currentEditType === 'folder') {
|
||||
await applyEditFolder(currentEditPath, currentEditElementId);
|
||||
} else if (currentEditType === 'leaf') {
|
||||
applyEditLeaf(currentEditPath, currentEditElementId);
|
||||
await applyEditLeaf(currentEditPath, currentEditElementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1538,30 +1573,34 @@ async function executeBondingConfig() {
|
|||
// ==========================================
|
||||
// 💡 6. 完整設備配置樹狀圖邏輯 (支援資料夾層級鎖定與虛擬群組視覺優化)
|
||||
// ==========================================
|
||||
function buildTree(node, path = '') {
|
||||
// 🌟 1. 在參數中加入 isParentCommandGroup,用來實現「向下繼承」
|
||||
// 🌟 加入 isParentCommandGroup 參數,實現向下繼承
|
||||
function buildTree(node, path = '', isParentCommandGroup = false) {
|
||||
let html = '';
|
||||
if (!node || typeof node !== 'object') return html;
|
||||
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
const currentPath = path ? `${path}::${key}` : key;
|
||||
|
||||
// 🌟 產生純淨的 CLI 指令路徑 (替換 :: 為空白,並移除虛擬索引)
|
||||
const cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// 🌟 處理資料夾或指令群組
|
||||
const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||||
|
||||
const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null);
|
||||
const folderIcon = hasNestedObject ? '📁' : '📑';
|
||||
const folderSuffix = hasNestedObject ? '' : '<span style="color: #95a5a6; font-size: 0.85em; font-weight: normal; margin-left: 5px;">(指令群組)</span>';
|
||||
const isCommandGroup = isParentCommandGroup || !hasNestedObject;
|
||||
const elementId = `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>' : '';
|
||||
|
||||
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;
|
||||
|
||||
// 🌟 定義專業的 SVG 向量圖示
|
||||
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>`;
|
||||
|
||||
// 🌟 全部展開/收合按鈕 (使用 SVG 並加入懸停變色效果)
|
||||
const expandCollapseBtns = hasNestedObject ? `
|
||||
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;"
|
||||
|
|
@ -1574,17 +1613,21 @@ function buildTree(node, path = '') {
|
|||
${collapseAllIcon}
|
||||
</span>
|
||||
</span>
|
||||
` : '';
|
||||
`;
|
||||
|
||||
// ✅ 加入 title="${cliCommand}" 提供懸停提示
|
||||
html += `
|
||||
<details id="details-${elementId}" class="tree-folder" style="margin-top: 4px; margin-left: 20px;">
|
||||
<details id="details-${elementId}" class="tree-folder" style="margin-top: 4px; margin-left: 20px;"
|
||||
ontoggle="this.querySelector('.icon-closed').style.display = this.open ? 'none' : 'inline-block'; this.querySelector('.icon-open').style.display = this.open ? 'inline-block' : 'none';">
|
||||
<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: 5px;">${folderIcon}</span>
|
||||
|
||||
<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}
|
||||
|
||||
|
|
@ -1601,35 +1644,63 @@ function buildTree(node, path = '') {
|
|||
</div>
|
||||
</summary>
|
||||
<div id="content-${elementId}" class="tree-folder-content" style="margin-left: 12px; border-left: 1px dashed #bdc3c7; padding-left: 12px;">
|
||||
${buildTree(value, currentPath)}
|
||||
${buildTree(value, currentPath, isCommandGroup)}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
} else {
|
||||
// 🌟 處理單一設定項目 (Leaf)
|
||||
const isVirtualIndex = /^\[\d+\]$/.test(key);
|
||||
const isNoCommand = (key === 'no');
|
||||
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
|
||||
const elementId = `leaf-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||||
|
||||
// 💡 終極修正:完美兼容根目錄與子目錄的 no 指令
|
||||
if (isNoCommand) {
|
||||
// 1. 移除結尾的 " no" (子目錄) 或是整句剛好是 "no" (根目錄)
|
||||
let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim();
|
||||
|
||||
// 2. 如果 baseCmd 有東西,代表是子目錄;如果為空,代表是根目錄
|
||||
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>`;
|
||||
|
||||
// 💡 關鍵修正:加上 display: inline-block; 與 transform: translateY(1.5px); 強制下移
|
||||
const valueStyle = `color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);`;
|
||||
|
||||
let displayContent = '';
|
||||
|
||||
if (isVirtualIndex) {
|
||||
displayContent = `
|
||||
<span style="margin-right: 5px;">📄</span>
|
||||
${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="color: #16a085; font-weight: normal; font-family: Courier New, monospace;">${safeValue}</span>
|
||||
<span class="leaf-value" style="${valueStyle}">${safeValue}</span>
|
||||
</span>
|
||||
`;
|
||||
} else if (isNoCommand) {
|
||||
// 為了方便後續的「原地變身」,我們加上了 id="display-wrapper-..."
|
||||
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 = `
|
||||
<span style="margin-right: 5px;">📄</span> <b>${key}</b>:
|
||||
${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="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeValue}</span>
|
||||
<span class="leaf-value" style="${valueStyle}">${safeValue}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// ✅ 加入 title="${cliCommand}" 提供懸停提示
|
||||
html += `
|
||||
<div class="tree-leaf-node"
|
||||
title="${cliCommand}"
|
||||
|
|
@ -1744,56 +1815,110 @@ async function loadRealConfigForFilters() {
|
|||
}
|
||||
}
|
||||
|
||||
function buildRealFilterTree(data, parentPath, hiddenKeys) {
|
||||
// 🌟 加入 isParentCommandGroup 參數,讓系統設定也能完美繼承群組狀態
|
||||
function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false) {
|
||||
if (typeof data !== 'object' || data === null) return '';
|
||||
|
||||
let html = `<div style="margin-left: ${parentPath ? '20px' : '0'};">`;
|
||||
let html = `<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" : "";
|
||||
|
||||
if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
|
||||
// 🌟 系統設定區塊也套用相同的視覺優化
|
||||
const isVirtualGroup = Object.keys(value).every(k => /^\[\d+\]$/.test(k));
|
||||
const isVirtualIndex = /^\[\d+\]$/.test(key);
|
||||
// 💡 統一在這裡解析 cliCommand,讓資料夾與 Leaf 都能擁有 Hint,並拔除虛擬索引 [0]
|
||||
let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
||||
|
||||
let displayName = key;
|
||||
let iconHtml = `<span class="tree-folder-icon"></span>`;
|
||||
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>`;
|
||||
|
||||
if (isVirtualGroup) {
|
||||
iconHtml = `<span style="margin-right: 5px; font-size: 16px;">📑</span>`;
|
||||
displayName = `${key} <span style="font-size: 12px; color: #7f8c8d; font-weight: normal;">(指令群組)</span>`;
|
||||
} else if (isVirtualIndex) {
|
||||
iconHtml = `<span style="margin-right: 5px; font-size: 14px;">📦</span>`;
|
||||
displayName = `<span style="color: #95a5a6; font-weight: normal;">項目 ${key}</span>`;
|
||||
}
|
||||
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-folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
html += `
|
||||
<details ontoggle="this.querySelector('summary').classList.toggle('is-open', this.open)">
|
||||
<summary class="tree-node-header" style="font-weight: bold; color: #2c3e50; margin-top: 5px; list-style: none; display: flex; align-items: center;">
|
||||
<input type="checkbox" value="${currentPath}" class="filter-checkbox" ${isChecked}
|
||||
onclick="event.stopPropagation();" onchange="toggleChildCheckboxes(this)" style="margin-right: 8px;">
|
||||
<span class="tree-chevron"></span>
|
||||
${iconHtml}
|
||||
<span style="flex-grow: 1;">${displayName}</span>
|
||||
<details id="details-${elementId}" class="tree-folder" ontoggle="this.querySelector('.icon-closed').style.display = this.open ? 'none' : 'inline-block'; this.querySelector('.icon-open').style.display = this.open ? 'inline-block' : 'none';">
|
||||
<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 class="filter-children-container">
|
||||
${buildRealFilterTree(value, currentPath, hiddenKeys)}
|
||||
<div class="filter-children-container" style="border-left: 1px dashed #bdc3c7; margin-left: 7px; padding-left: 16px;">
|
||||
${buildRealFilterTree(value, currentPath, hiddenKeys, isCommandGroup)}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
} else {
|
||||
// 🌟 處理單一設定項目 (Leaf)
|
||||
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>`;
|
||||
|
||||
// 💡 統一 UI 顯示:紅色禁止符號 + 紅色 no: + 灰色數值
|
||||
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>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div style="padding: 4px 0; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; padding-left: 10px; display: flex; align-items: center;">
|
||||
<input type="checkbox" value="${currentPath}" class="filter-checkbox" ${isChecked}
|
||||
onchange="updateParentCheckboxState(this)" style="margin-right: 8px;">
|
||||
<span style="margin-right: 5px;">📄</span> ${displayName}:
|
||||
<span style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeValue}</span>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
|
@ -1984,3 +2109,37 @@ async function clearVisibleCache() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 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 = `<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>`;
|
||||
|
||||
// 3. 執行原地替換 (In-place Update)
|
||||
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);">${restCommand}</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>
|
||||
`;
|
||||
|
||||
// 4. 隱藏該行的編輯按鈕,避免使用者在歸檔前重複編輯產生邏輯錯誤
|
||||
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';
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"hidden_keys": []
|
||||
"hidden_keys": [
|
||||
"ssh",
|
||||
"clock"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue