fix: 解決編輯模式下含有雙引號與特殊符號導致的 HTML 破版問題

This commit is contained in:
swpa 2026-05-13 15:43:42 +08:00
parent b7395a1763
commit 93eb15ac9c
1 changed files with 40 additions and 8 deletions

View File

@ -6,6 +6,17 @@ import {
} from './api.js'; } from './api.js';
import { getGlobalConnectionInfo } from './utils.js'; import { getGlobalConnectionInfo } from './utils.js';
// --- 安全跳脫 HTML 特殊字元,防止破版 ---
function escapeHTML(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ========================================== // ==========================================
// 1. 全域狀態與鎖定管理 (Lock Management) // 1. 全域狀態與鎖定管理 (Lock Management)
// ========================================== // ==========================================
@ -80,6 +91,10 @@ export async function startEditFolder(path, elementId) {
leafContainers.forEach(container => { leafContainers.forEach(container => {
const origVal = container.getAttribute('data-original'); const origVal = container.getAttribute('data-original');
const rawPath = container.getAttribute('data-path'); const rawPath = container.getAttribute('data-path');
// ✅ 修改這裡:新增 safeOrigVal
const safeOrigVal = escapeHTML(origVal);
let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (rawPath.endsWith('::no')) { if (rawPath.endsWith('::no')) {
@ -108,12 +123,15 @@ export async function startEditFolder(path, elementId) {
inputHtml = `<select class="edit-input" data-path="${rawPath}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`; inputHtml = `<select class="edit-input" data-path="${rawPath}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => { options.forEach(opt => {
// ✅ 修改這裡:新增 safeOpt並替換 value 與顯示文字
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : ''; const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${opt}" ${isSelected}>${opt}</option>`; inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
}); });
inputHtml += `</select>${hintIcon}`; inputHtml += `</select>${hintIcon}`;
} else { } else {
inputHtml = `<input type="text" class="edit-input" data-path="${rawPath}" value="${origVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`; // ✅ 修改這裡:把 value="${origVal}" 改成 value="${safeOrigVal}"
inputHtml = `<input type="text" class="edit-input" data-path="${rawPath}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
} }
container.innerHTML = inputHtml; container.innerHTML = inputHtml;
}); });
@ -144,6 +162,10 @@ export async function startEditLeaf(path, elementId) {
const container = document.getElementById(`container-${elementId}`); const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original'); const origVal = container.getAttribute('data-original');
// ✅ 修改這裡:新增 safeOrigVal
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`; container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
try { try {
@ -188,17 +210,21 @@ export async function startEditLeaf(path, elementId) {
inputHtml = `<select class="edit-input" data-path="${path}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`; inputHtml = `<select class="edit-input" data-path="${path}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => { options.forEach(opt => {
// ✅ 修改這裡:新增 safeOpt
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : ''; const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${opt}" ${isSelected}>${opt}</option>`; inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
}); });
inputHtml += `</select>${hintIcon}`; inputHtml += `</select>${hintIcon}`;
} else { } else {
inputHtml = `<input type="text" class="edit-input" data-path="${path}" value="${origVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`; // ✅ 修改這裡:使用 safeOrigVal
inputHtml = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
} }
container.innerHTML = inputHtml; container.innerHTML = inputHtml;
} catch (e) { } catch (e) {
console.warn("選項載入失敗", e); console.warn("選項載入失敗", e);
container.innerHTML = `<input type="text" class="edit-input" data-path="${path}" value="${origVal}" style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">`; // ✅ 修改這裡catch 區塊也要使用 safeOrigVal
container.innerHTML = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">`;
} }
} }
} catch (error) { } catch (error) {
@ -220,7 +246,9 @@ export async function cancelEditFolder(path, elementId) {
const leafContainers = contentDiv.querySelectorAll('.leaf-container'); const leafContainers = contentDiv.querySelectorAll('.leaf-container');
leafContainers.forEach(container => { leafContainers.forEach(container => {
const origVal = container.getAttribute('data-original'); const origVal = container.getAttribute('data-original');
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${origVal}</span>`; // ✅ 修改這裡:還原時也要跳脫
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
}); });
} }
@ -236,7 +264,9 @@ export async function cancelEditLeaf(path, elementId) {
const container = document.getElementById(`container-${elementId}`); const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original'); const origVal = container.getAttribute('data-original');
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${origVal}</span>`; // ✅ 修改這裡:還原時也要跳脫
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
} }
// ========================================== // ==========================================
@ -508,6 +538,8 @@ function transformNoToActive(elementId, newCommand) {
const firstWord = parts[0]; const firstWord = parts[0];
const restCommand = parts.slice(1).join(' '); const restCommand = parts.slice(1).join(' ');
const safeRestCommand = escapeHTML(restCommand);
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 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>`; const iconHtml = `<span style="margin-right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px;">${svgEnabled}</span>`;
@ -515,7 +547,7 @@ function transformNoToActive(elementId, newCommand) {
${iconHtml} ${iconHtml}
<b style="color: #2c3e50;">${firstWord}</b> <b style="color: #2c3e50;">${firstWord}</b>
<span class="leaf-container" style="display: inline-flex; align-items: center; min-height: 24px;"> <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 class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeRestCommand}</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 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> </span>
`; `;