fix: 修復系統中「鎖定狀態未向下繼承」的嚴重 UI/UX 漏洞。
This commit is contained in:
parent
6698e34292
commit
ecedee49d2
307
all_code.txt
307
all_code.txt
|
|
@ -2176,7 +2176,8 @@ function startLockStatusPolling() {
|
|||
newLockState.add(lockKey); // 記憶體加入 Host 標籤
|
||||
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`);
|
||||
// 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
|
||||
|
||||
targetBtns.forEach(btn => {
|
||||
// 略過目前正在編輯的那個實體按鈕
|
||||
|
|
@ -2185,11 +2186,18 @@ function startLockStatusPolling() {
|
|||
if (btn.innerText !== "🔒") {
|
||||
btn.style.pointerEvents = 'none';
|
||||
btn.style.opacity = '0.2';
|
||||
// 判斷是別人鎖的,還是自己在另一個視圖鎖的
|
||||
if (lockInfo.user_id !== SESSION_USER_ID) {
|
||||
btn.title = `🔒 此區塊正由 [${lockInfo.username}] 編輯中`;
|
||||
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
if (btnPath === path) {
|
||||
// 判斷是別人鎖的,還是自己在另一個視圖鎖的
|
||||
if (lockInfo.user_id !== SESSION_USER_ID) {
|
||||
btn.title = `🔒 此區塊正由 [${lockInfo.username}] 編輯中`;
|
||||
} else {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
// 子節點被父節點鎖定
|
||||
btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
}
|
||||
btn.innerText = "🔒";
|
||||
}
|
||||
|
|
@ -2203,14 +2211,33 @@ function startLockStatusPolling() {
|
|||
// 🛡️ 嚴格限制:只解除「當前設備」且「已不在新鎖定清單中」的節點
|
||||
if (oldHost === host && !newLockState.has(oldKey)) {
|
||||
const safePath = oldPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`);
|
||||
// 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
|
||||
|
||||
targetBtns.forEach(btn => {
|
||||
if (btn.innerText === "🔒") {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
let stillLockedByOther = false;
|
||||
|
||||
// 檢查自己是否還在鎖定清單中
|
||||
if (currentLocks[btnPath]) {
|
||||
stillLockedByOther = true;
|
||||
} else {
|
||||
// 檢查是否有其他父節點鎖定它
|
||||
for (const lockedPath of Object.keys(currentLocks)) {
|
||||
if (btnPath.startsWith(lockedPath + "::")) {
|
||||
stillLockedByOther = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillLockedByOther) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -4021,6 +4048,9 @@ function escapeHTML(str) {
|
|||
export const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9);
|
||||
const ACTIVE_HEARTBEATS = {};
|
||||
|
||||
// 🌟 新增:用來防禦「秒按取消」導致的非同步渲染競爭危害
|
||||
const activeEditSessions = new Set();
|
||||
|
||||
export let currentEditPath = null;
|
||||
export let currentEditElementId = null;
|
||||
export let currentEditIsSuccess = false;
|
||||
|
|
@ -4066,6 +4096,10 @@ async function sendHeartbeat(path, host, intervalId, lockKey) {
|
|||
// ==========================================
|
||||
|
||||
export async function startEditFolder(path, elementId) {
|
||||
// 🚨 防護 1:防止連點或重複執行
|
||||
if (activeEditSessions.has(elementId)) return;
|
||||
activeEditSessions.add(elementId); // 立即標記進入編輯狀態的意圖
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const username = connInfo ? connInfo.user : 'admin';
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
|
|
@ -4073,9 +4107,41 @@ export async function startEditFolder(path, elementId) {
|
|||
|
||||
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
|
||||
|
||||
// 立即讓按鈕呈現讀取中,防止使用者焦慮連點
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'none';
|
||||
editBtn.innerText = "⏳";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
|
||||
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
|
||||
|
||||
// 🚨 防護 2:如果在等待後端上鎖的期間,使用者已經按了取消
|
||||
if (!activeEditSessions.has(elementId)) {
|
||||
if (result.status === 200 || (result.data && result.data.status === 'success')) {
|
||||
// 🌟 補上防閃爍冷卻,防止輪詢抓到時間差
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
apiReleaseLock(path, SESSION_USER_ID, host); // 默默把剛拿到的鎖還回去
|
||||
}
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return; // 立即中斷,絕不觸碰 UI
|
||||
}
|
||||
|
||||
if (result.status === 409) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return alert(`⚠️ ${result.data.detail}`);
|
||||
}
|
||||
|
||||
if (result.data.status === 'success') {
|
||||
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
|
|
@ -4094,16 +4160,22 @@ export async function startEditFolder(path, elementId) {
|
|||
}
|
||||
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
|
||||
// 🌟 擴大選擇器:連同所有子節點一起秒級鎖定
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
|
||||
if (btn.id !== `edit-btn-${elementId}`) {
|
||||
btn.style.pointerEvents = 'none';
|
||||
btn.style.opacity = '0.2';
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
if (btnPath === path) {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
} else {
|
||||
btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
}
|
||||
btn.innerText = "🔒";
|
||||
}
|
||||
});
|
||||
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) editBtn.style.display = 'none';
|
||||
|
||||
const actionBtn = document.getElementById(`actions-${elementId}`);
|
||||
|
|
@ -4122,12 +4194,17 @@ export async function startEditFolder(path, elementId) {
|
|||
let optData = {};
|
||||
try { optData = await apiGetLeafOptions(host, currentMode); } catch (e) {}
|
||||
|
||||
const pathsToSync = [];
|
||||
// 🚨 防護 3:如果等待 API 期間使用者按了取消,立刻中斷後續渲染
|
||||
if (!activeEditSessions.has(elementId)) return;
|
||||
|
||||
const pathsToSync = [];
|
||||
const containersArray = Array.from(leafContainers);
|
||||
const CHUNK_SIZE = 50;
|
||||
|
||||
for (let i = 0; i < containersArray.length; i += CHUNK_SIZE) {
|
||||
// 🚨 防護 4:如果分批渲染期間使用者按了取消,立刻中斷
|
||||
if (!activeEditSessions.has(elementId)) return;
|
||||
|
||||
const chunk = containersArray.slice(i, i + CHUNK_SIZE);
|
||||
|
||||
chunk.forEach(container => {
|
||||
|
|
@ -4174,11 +4251,20 @@ export async function startEditFolder(path, elementId) {
|
|||
if (pathsToSync.length > 0) apiSyncLeafOptions(host, pathsToSync, currentMode, username, connInfo.pass);
|
||||
}
|
||||
} catch (error) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
alert("❌ 無法連線到鎖定伺服器:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startEditLeaf(path, elementId) {
|
||||
// 🚨 防護 1:防止連點或重複執行
|
||||
if (activeEditSessions.has(elementId)) return;
|
||||
activeEditSessions.add(elementId);
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const username = connInfo ? connInfo.user : 'admin';
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
|
|
@ -4186,9 +4272,40 @@ export async function startEditLeaf(path, elementId) {
|
|||
|
||||
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
|
||||
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'none';
|
||||
editBtn.innerText = "⏳";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
|
||||
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
|
||||
|
||||
// 🚨 防護 2:如果在等待後端上鎖的期間,使用者已經按了取消
|
||||
if (!activeEditSessions.has(elementId)) {
|
||||
if (result.status === 200 || (result.data && result.data.status === 'success')) {
|
||||
// 🌟 補上防閃爍冷卻,防止輪詢抓到時間差
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
apiReleaseLock(path, SESSION_USER_ID, host);
|
||||
}
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status === 409) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return alert(`⚠️ ${result.data.detail}`);
|
||||
}
|
||||
|
||||
if (result.data.status === 'success') {
|
||||
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
|
|
@ -4196,12 +4313,10 @@ export async function startEditLeaf(path, elementId) {
|
|||
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
|
||||
ACTIVE_HEARTBEATS[lockKey] = intervalId;
|
||||
|
||||
// 🌟 立即更新全域鎖定狀態 (加入 Host 隔離)
|
||||
if (!window.globalActiveLocks) window.globalActiveLocks = {};
|
||||
if (!window.globalActiveLocks[host]) window.globalActiveLocks[host] = {};
|
||||
window.globalActiveLocks[host][path] = { user_id: SESSION_USER_ID, username: username };
|
||||
|
||||
// 🌟 確保不在冷卻名單中 (防止手速過快)
|
||||
if (window.recentlyReleasedLocks) {
|
||||
delete window.recentlyReleasedLocks[`${host}@@${path}`];
|
||||
}
|
||||
|
|
@ -4216,7 +4331,7 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'none';
|
||||
if (editBtn) editBtn.style.display = 'none';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'inline-block';
|
||||
|
||||
const container = document.getElementById(`container-${elementId}`);
|
||||
|
|
@ -4235,6 +4350,7 @@ export async function startEditLeaf(path, elementId) {
|
|||
apiSyncLeafOptions(host, [cacheKey], currentMode, username, connInfo.pass);
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
optData = await apiGetLeafOptions(host, currentMode);
|
||||
leafCache = optData[cacheKey];
|
||||
|
|
@ -4244,6 +4360,8 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
|
||||
let inputHtml = "";
|
||||
let hintAttr = "";
|
||||
let hintIcon = "";
|
||||
|
|
@ -4270,15 +4388,23 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
container.innerHTML = inputHtml;
|
||||
} catch (e) {
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
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) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
alert("❌ 無法連線到鎖定伺服器:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelEditFolder(path, elementId) {
|
||||
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
const lockKey = `${host}@@${path}`;
|
||||
|
|
@ -4287,28 +4413,59 @@ export async function cancelEditFolder(path, elementId) {
|
|||
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
delete ACTIVE_HEARTBEATS[lockKey];
|
||||
}
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 寫入防閃爍冷卻表 (8秒內忽略後端的舊狀態)
|
||||
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
|
||||
delete window.globalActiveLocks[host][path];
|
||||
}
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
|
||||
// 🌟 擴大選擇器:連同所有子節點一起秒級解除鎖定
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
|
||||
if (btn.id !== `edit-btn-${elementId}`) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
let stillLockedByOther = false;
|
||||
|
||||
// 確保解除鎖定時,不會誤解開被其他父節點鎖定的子節點
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host]) {
|
||||
const hostLocks = window.globalActiveLocks[host];
|
||||
if (hostLocks[btnPath]) {
|
||||
stillLockedByOther = true;
|
||||
} else {
|
||||
for (const lockedPath of Object.keys(hostLocks)) {
|
||||
if (btnPath.startsWith(lockedPath + "::")) {
|
||||
stillLockedByOther = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillLockedByOther) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'none';
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.display = 'inline-block';
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
|
||||
}
|
||||
|
||||
const actionBtns = document.getElementById(`actions-${elementId}`);
|
||||
if (actionBtns) actionBtns.style.display = 'none';
|
||||
|
||||
const contentDiv = document.getElementById(`content-${elementId}`);
|
||||
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
|
||||
|
|
@ -4342,6 +4499,8 @@ export async function cancelEditFolder(path, elementId) {
|
|||
}
|
||||
|
||||
export async function cancelEditLeaf(path, elementId) {
|
||||
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
const lockKey = `${host}@@${path}`;
|
||||
|
|
@ -4350,12 +4509,13 @@ export async function cancelEditLeaf(path, elementId) {
|
|||
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
delete ACTIVE_HEARTBEATS[lockKey];
|
||||
}
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 寫入防閃爍冷卻表 (8秒內忽略後端的舊狀態)
|
||||
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
|
||||
delete window.globalActiveLocks[host][path];
|
||||
|
|
@ -4370,8 +4530,17 @@ export async function cancelEditLeaf(path, elementId) {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'none';
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.display = 'inline-block';
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
|
||||
}
|
||||
|
||||
const actionBtns = document.getElementById(`actions-${elementId}`);
|
||||
if (actionBtns) actionBtns.style.display = 'none';
|
||||
|
||||
const container = document.getElementById(`container-${elementId}`);
|
||||
const origVal = container.getAttribute('data-original');
|
||||
|
|
@ -5415,16 +5584,33 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
|
|||
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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) {
|
||||
const hostLocks = window.globalActiveLocks[currentHost];
|
||||
|
||||
// 1. 檢查自己是否被鎖定
|
||||
if (hostLocks[currentPath]) {
|
||||
const lockInfo = hostLocks[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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
// 2. 檢查父層級是否被鎖定 (Cascading Lock)
|
||||
for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) {
|
||||
if (currentPath.startsWith(lockedPath + "::")) {
|
||||
isLocked = true;
|
||||
lockText = "🔒";
|
||||
lockOpacity = "0.2";
|
||||
lockPointer = "none";
|
||||
lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5495,16 +5681,33 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
|
|||
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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) {
|
||||
const hostLocks = window.globalActiveLocks[currentHost];
|
||||
|
||||
// 1. 檢查自己是否被鎖定
|
||||
if (hostLocks[currentPath]) {
|
||||
const lockInfo = hostLocks[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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
// 2. 檢查父層級是否被鎖定 (Cascading Lock)
|
||||
for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) {
|
||||
if (currentPath.startsWith(lockedPath + "::")) {
|
||||
isLocked = true;
|
||||
lockText = "🔒";
|
||||
lockOpacity = "0.2";
|
||||
lockPointer = "none";
|
||||
lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ function startLockStatusPolling() {
|
|||
newLockState.add(lockKey); // 記憶體加入 Host 標籤
|
||||
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`);
|
||||
// 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
|
||||
|
||||
targetBtns.forEach(btn => {
|
||||
// 略過目前正在編輯的那個實體按鈕
|
||||
|
|
@ -156,11 +157,18 @@ function startLockStatusPolling() {
|
|||
if (btn.innerText !== "🔒") {
|
||||
btn.style.pointerEvents = 'none';
|
||||
btn.style.opacity = '0.2';
|
||||
// 判斷是別人鎖的,還是自己在另一個視圖鎖的
|
||||
if (lockInfo.user_id !== SESSION_USER_ID) {
|
||||
btn.title = `🔒 此區塊正由 [${lockInfo.username}] 編輯中`;
|
||||
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
if (btnPath === path) {
|
||||
// 判斷是別人鎖的,還是自己在另一個視圖鎖的
|
||||
if (lockInfo.user_id !== SESSION_USER_ID) {
|
||||
btn.title = `🔒 此區塊正由 [${lockInfo.username}] 編輯中`;
|
||||
} else {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
// 子節點被父節點鎖定
|
||||
btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
}
|
||||
btn.innerText = "🔒";
|
||||
}
|
||||
|
|
@ -174,14 +182,33 @@ function startLockStatusPolling() {
|
|||
// 🛡️ 嚴格限制:只解除「當前設備」且「已不在新鎖定清單中」的節點
|
||||
if (oldHost === host && !newLockState.has(oldKey)) {
|
||||
const safePath = oldPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`);
|
||||
// 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
|
||||
const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
|
||||
|
||||
targetBtns.forEach(btn => {
|
||||
if (btn.innerText === "🔒") {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
let stillLockedByOther = false;
|
||||
|
||||
// 檢查自己是否還在鎖定清單中
|
||||
if (currentLocks[btnPath]) {
|
||||
stillLockedByOther = true;
|
||||
} else {
|
||||
// 檢查是否有其他父節點鎖定它
|
||||
for (const lockedPath of Object.keys(currentLocks)) {
|
||||
if (btnPath.startsWith(lockedPath + "::")) {
|
||||
stillLockedByOther = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillLockedByOther) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ function escapeHTML(str) {
|
|||
export const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9);
|
||||
const ACTIVE_HEARTBEATS = {};
|
||||
|
||||
// 🌟 新增:用來防禦「秒按取消」導致的非同步渲染競爭危害
|
||||
const activeEditSessions = new Set();
|
||||
|
||||
export let currentEditPath = null;
|
||||
export let currentEditElementId = null;
|
||||
export let currentEditIsSuccess = false;
|
||||
|
|
@ -69,6 +72,10 @@ async function sendHeartbeat(path, host, intervalId, lockKey) {
|
|||
// ==========================================
|
||||
|
||||
export async function startEditFolder(path, elementId) {
|
||||
// 🚨 防護 1:防止連點或重複執行
|
||||
if (activeEditSessions.has(elementId)) return;
|
||||
activeEditSessions.add(elementId); // 立即標記進入編輯狀態的意圖
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const username = connInfo ? connInfo.user : 'admin';
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
|
|
@ -76,9 +83,41 @@ export async function startEditFolder(path, elementId) {
|
|||
|
||||
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
|
||||
|
||||
// 立即讓按鈕呈現讀取中,防止使用者焦慮連點
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'none';
|
||||
editBtn.innerText = "⏳";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
|
||||
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
|
||||
|
||||
// 🚨 防護 2:如果在等待後端上鎖的期間,使用者已經按了取消
|
||||
if (!activeEditSessions.has(elementId)) {
|
||||
if (result.status === 200 || (result.data && result.data.status === 'success')) {
|
||||
// 🌟 補上防閃爍冷卻,防止輪詢抓到時間差
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
apiReleaseLock(path, SESSION_USER_ID, host); // 默默把剛拿到的鎖還回去
|
||||
}
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return; // 立即中斷,絕不觸碰 UI
|
||||
}
|
||||
|
||||
if (result.status === 409) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return alert(`⚠️ ${result.data.detail}`);
|
||||
}
|
||||
|
||||
if (result.data.status === 'success') {
|
||||
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
|
|
@ -97,16 +136,22 @@ export async function startEditFolder(path, elementId) {
|
|||
}
|
||||
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
|
||||
// 🌟 擴大選擇器:連同所有子節點一起秒級鎖定
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
|
||||
if (btn.id !== `edit-btn-${elementId}`) {
|
||||
btn.style.pointerEvents = 'none';
|
||||
btn.style.opacity = '0.2';
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
if (btnPath === path) {
|
||||
btn.title = `🔒 您正在另一個視圖編輯此項目`;
|
||||
} else {
|
||||
btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
}
|
||||
btn.innerText = "🔒";
|
||||
}
|
||||
});
|
||||
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) editBtn.style.display = 'none';
|
||||
|
||||
const actionBtn = document.getElementById(`actions-${elementId}`);
|
||||
|
|
@ -125,12 +170,17 @@ export async function startEditFolder(path, elementId) {
|
|||
let optData = {};
|
||||
try { optData = await apiGetLeafOptions(host, currentMode); } catch (e) {}
|
||||
|
||||
const pathsToSync = [];
|
||||
// 🚨 防護 3:如果等待 API 期間使用者按了取消,立刻中斷後續渲染
|
||||
if (!activeEditSessions.has(elementId)) return;
|
||||
|
||||
const pathsToSync = [];
|
||||
const containersArray = Array.from(leafContainers);
|
||||
const CHUNK_SIZE = 50;
|
||||
|
||||
for (let i = 0; i < containersArray.length; i += CHUNK_SIZE) {
|
||||
// 🚨 防護 4:如果分批渲染期間使用者按了取消,立刻中斷
|
||||
if (!activeEditSessions.has(elementId)) return;
|
||||
|
||||
const chunk = containersArray.slice(i, i + CHUNK_SIZE);
|
||||
|
||||
chunk.forEach(container => {
|
||||
|
|
@ -177,11 +227,20 @@ export async function startEditFolder(path, elementId) {
|
|||
if (pathsToSync.length > 0) apiSyncLeafOptions(host, pathsToSync, currentMode, username, connInfo.pass);
|
||||
}
|
||||
} catch (error) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
alert("❌ 無法連線到鎖定伺服器:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startEditLeaf(path, elementId) {
|
||||
// 🚨 防護 1:防止連點或重複執行
|
||||
if (activeEditSessions.has(elementId)) return;
|
||||
activeEditSessions.add(elementId);
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const username = connInfo ? connInfo.user : 'admin';
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
|
|
@ -189,9 +248,40 @@ export async function startEditLeaf(path, elementId) {
|
|||
|
||||
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
|
||||
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'none';
|
||||
editBtn.innerText = "⏳";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
|
||||
if (result.status === 409) return alert(`⚠️ ${result.data.detail}`);
|
||||
|
||||
// 🚨 防護 2:如果在等待後端上鎖的期間,使用者已經按了取消
|
||||
if (!activeEditSessions.has(elementId)) {
|
||||
if (result.status === 200 || (result.data && result.data.status === 'success')) {
|
||||
// 🌟 補上防閃爍冷卻,防止輪詢抓到時間差
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
apiReleaseLock(path, SESSION_USER_ID, host);
|
||||
}
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status === 409) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
return alert(`⚠️ ${result.data.detail}`);
|
||||
}
|
||||
|
||||
if (result.data.status === 'success') {
|
||||
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
|
|
@ -199,12 +289,10 @@ export async function startEditLeaf(path, elementId) {
|
|||
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
|
||||
ACTIVE_HEARTBEATS[lockKey] = intervalId;
|
||||
|
||||
// 🌟 立即更新全域鎖定狀態 (加入 Host 隔離)
|
||||
if (!window.globalActiveLocks) window.globalActiveLocks = {};
|
||||
if (!window.globalActiveLocks[host]) window.globalActiveLocks[host] = {};
|
||||
window.globalActiveLocks[host][path] = { user_id: SESSION_USER_ID, username: username };
|
||||
|
||||
// 🌟 確保不在冷卻名單中 (防止手速過快)
|
||||
if (window.recentlyReleasedLocks) {
|
||||
delete window.recentlyReleasedLocks[`${host}@@${path}`];
|
||||
}
|
||||
|
|
@ -219,7 +307,7 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'none';
|
||||
if (editBtn) editBtn.style.display = 'none';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'inline-block';
|
||||
|
||||
const container = document.getElementById(`container-${elementId}`);
|
||||
|
|
@ -238,6 +326,7 @@ export async function startEditLeaf(path, elementId) {
|
|||
apiSyncLeafOptions(host, [cacheKey], currentMode, username, connInfo.pass);
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
optData = await apiGetLeafOptions(host, currentMode);
|
||||
leafCache = optData[cacheKey];
|
||||
|
|
@ -247,6 +336,8 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
|
||||
let inputHtml = "";
|
||||
let hintAttr = "";
|
||||
let hintIcon = "";
|
||||
|
|
@ -273,15 +364,23 @@ export async function startEditLeaf(path, elementId) {
|
|||
}
|
||||
container.innerHTML = inputHtml;
|
||||
} catch (e) {
|
||||
if (!activeEditSessions.has(elementId)) return; // 🚨 防護
|
||||
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) {
|
||||
activeEditSessions.delete(elementId);
|
||||
if (editBtn) {
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.innerText = "✏️";
|
||||
}
|
||||
alert("❌ 無法連線到鎖定伺服器:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelEditFolder(path, elementId) {
|
||||
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
const lockKey = `${host}@@${path}`;
|
||||
|
|
@ -290,28 +389,59 @@ export async function cancelEditFolder(path, elementId) {
|
|||
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
delete ACTIVE_HEARTBEATS[lockKey];
|
||||
}
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 寫入防閃爍冷卻表 (8秒內忽略後端的舊狀態)
|
||||
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
|
||||
delete window.globalActiveLocks[host][path];
|
||||
}
|
||||
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
|
||||
// 🌟 擴大選擇器:連同所有子節點一起秒級解除鎖定
|
||||
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
|
||||
if (btn.id !== `edit-btn-${elementId}`) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
const btnPath = btn.getAttribute('data-path');
|
||||
let stillLockedByOther = false;
|
||||
|
||||
// 確保解除鎖定時,不會誤解開被其他父節點鎖定的子節點
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host]) {
|
||||
const hostLocks = window.globalActiveLocks[host];
|
||||
if (hostLocks[btnPath]) {
|
||||
stillLockedByOther = true;
|
||||
} else {
|
||||
for (const lockedPath of Object.keys(hostLocks)) {
|
||||
if (btnPath.startsWith(lockedPath + "::")) {
|
||||
stillLockedByOther = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillLockedByOther) {
|
||||
btn.style.pointerEvents = 'auto';
|
||||
btn.style.opacity = '0.3';
|
||||
btn.title = "鎖定並編輯此項目";
|
||||
btn.innerText = "✏️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'none';
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.display = 'inline-block';
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
|
||||
}
|
||||
|
||||
const actionBtns = document.getElementById(`actions-${elementId}`);
|
||||
if (actionBtns) actionBtns.style.display = 'none';
|
||||
|
||||
const contentDiv = document.getElementById(`content-${elementId}`);
|
||||
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
|
||||
|
|
@ -345,6 +475,8 @@ export async function cancelEditFolder(path, elementId) {
|
|||
}
|
||||
|
||||
export async function cancelEditLeaf(path, elementId) {
|
||||
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
|
||||
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
const host = connInfo ? connInfo.host : '';
|
||||
const lockKey = `${host}@@${path}`;
|
||||
|
|
@ -353,12 +485,13 @@ export async function cancelEditLeaf(path, elementId) {
|
|||
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
|
||||
delete ACTIVE_HEARTBEATS[lockKey];
|
||||
}
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 寫入防閃爍冷卻表 (8秒內忽略後端的舊狀態)
|
||||
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
|
||||
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
|
||||
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
|
||||
|
||||
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
|
||||
|
||||
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
|
||||
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
|
||||
delete window.globalActiveLocks[host][path];
|
||||
|
|
@ -373,8 +506,17 @@ export async function cancelEditLeaf(path, elementId) {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block';
|
||||
document.getElementById(`actions-${elementId}`).style.display = 'none';
|
||||
const editBtn = document.getElementById(`edit-btn-${elementId}`);
|
||||
if (editBtn) {
|
||||
editBtn.style.display = 'inline-block';
|
||||
editBtn.style.pointerEvents = 'auto';
|
||||
editBtn.style.opacity = '0.3';
|
||||
editBtn.title = "鎖定並編輯此項目";
|
||||
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
|
||||
}
|
||||
|
||||
const actionBtns = document.getElementById(`actions-${elementId}`);
|
||||
if (actionBtns) actionBtns.style.display = 'none';
|
||||
|
||||
const container = document.getElementById(`container-${elementId}`);
|
||||
const origVal = container.getAttribute('data-original');
|
||||
|
|
|
|||
|
|
@ -126,16 +126,33 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
|
|||
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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) {
|
||||
const hostLocks = window.globalActiveLocks[currentHost];
|
||||
|
||||
// 1. 檢查自己是否被鎖定
|
||||
if (hostLocks[currentPath]) {
|
||||
const lockInfo = hostLocks[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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
// 2. 檢查父層級是否被鎖定 (Cascading Lock)
|
||||
for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) {
|
||||
if (currentPath.startsWith(lockedPath + "::")) {
|
||||
isLocked = true;
|
||||
lockText = "🔒";
|
||||
lockOpacity = "0.2";
|
||||
lockPointer = "none";
|
||||
lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,16 +223,33 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
|
|||
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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
if (currentHost && window.globalActiveLocks && window.globalActiveLocks[currentHost]) {
|
||||
const hostLocks = window.globalActiveLocks[currentHost];
|
||||
|
||||
// 1. 檢查自己是否被鎖定
|
||||
if (hostLocks[currentPath]) {
|
||||
const lockInfo = hostLocks[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}] 編輯中`
|
||||
: `🔒 您正在另一個視圖編輯此項目`;
|
||||
}
|
||||
} else {
|
||||
// 2. 檢查父層級是否被鎖定 (Cascading Lock)
|
||||
for (const [lockedPath, lockInfo] of Object.entries(hostLocks)) {
|
||||
if (currentPath.startsWith(lockedPath + "::")) {
|
||||
isLocked = true;
|
||||
lockText = "🔒";
|
||||
lockOpacity = "0.2";
|
||||
lockPointer = "none";
|
||||
lockTitle = `🔒 父層級已被鎖定,無法編輯此項目`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue