Merge branch 'enhancement/ux'
This commit is contained in:
commit
17ff31dcf7
|
|
@ -1,29 +1,67 @@
|
||||||
# --- routers/leaf_options.py ---
|
# --- routers/leaf_options.py ---
|
||||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Body
|
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Body
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List
|
from typing import List
|
||||||
import json, os, asyncio, time, re
|
import json, os, asyncio, re
|
||||||
|
|
||||||
# 🌟 引入新的 asyncssh 版本爬蟲
|
|
||||||
from cmts_scraper import sync_cmts_leaves_async
|
from cmts_scraper import sync_cmts_leaves_async
|
||||||
from shared import CMTS_DEVICE
|
from shared import CMTS_DEVICE
|
||||||
|
|
||||||
# 🌟 移除原本硬寫的 prefix="/api/v1",只留下 tags
|
|
||||||
router = APIRouter(tags=["Options"])
|
router = APIRouter(tags=["Options"])
|
||||||
CACHE_FILE = "cmts_leaf_options_cache.json"
|
CACHE_FILE = "cmts_leaf_options_cache.json"
|
||||||
|
|
||||||
# 🌟 1. 改用全域布林變數,狀態切換更即時,消滅非同步時間差
|
# ==========================================
|
||||||
|
# 🌟 全域廣播機制 (SSE)
|
||||||
|
# ==========================================
|
||||||
IS_SCANNING = False
|
IS_SCANNING = False
|
||||||
|
active_clients = set()
|
||||||
|
|
||||||
|
async def broadcast_message(message_dict: dict):
|
||||||
|
"""將訊息推播給所有連線中的前端"""
|
||||||
|
dead_clients = set()
|
||||||
|
|
||||||
|
# 🌟 關鍵修復 1:SSE 協定嚴格要求必須以 "data: " 開頭,並以 "\n\n" 結尾
|
||||||
|
message_str = f"data: {json.dumps(message_dict)}\n\n"
|
||||||
|
|
||||||
|
for client_queue in active_clients:
|
||||||
|
try:
|
||||||
|
await client_queue.put(message_str)
|
||||||
|
except Exception:
|
||||||
|
dead_clients.add(client_queue)
|
||||||
|
for dead in dead_clients:
|
||||||
|
active_clients.discard(dead)
|
||||||
|
|
||||||
|
@router.get("/cmts-leaf-options/stream")
|
||||||
|
async def sse_stream(request: Request):
|
||||||
|
"""前端一載入就會連上這個路由,持續監聽廣播"""
|
||||||
|
client_queue = asyncio.Queue()
|
||||||
|
active_clients.add(client_queue)
|
||||||
|
|
||||||
|
async def event_generator():
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
message = await asyncio.wait_for(client_queue.get(), timeout=2.0)
|
||||||
|
yield message
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if await request.is_disconnected():
|
||||||
|
break
|
||||||
|
# 🌟 關鍵修復 2:每 2 秒發送一個空註解,防止瀏覽器因閒置而自動斷線
|
||||||
|
yield ": keepalive\n\n"
|
||||||
|
finally:
|
||||||
|
active_clients.discard(client_queue)
|
||||||
|
|
||||||
|
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 🌟 新增:讓前端查詢目前是否有人正在掃描
|
|
||||||
# ==========================================
|
|
||||||
@router.get("/scan-status")
|
@router.get("/scan-status")
|
||||||
async def get_scan_status():
|
async def get_scan_status():
|
||||||
global IS_SCANNING
|
global IS_SCANNING
|
||||||
return {"is_scanning": IS_SCANNING}
|
return {"is_scanning": IS_SCANNING}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 🌟 快取讀取與背景掃描任務
|
||||||
|
# ==========================================
|
||||||
class SyncOptionsRequest(BaseModel):
|
class SyncOptionsRequest(BaseModel):
|
||||||
leaf_paths: List[str]
|
leaf_paths: List[str]
|
||||||
|
|
||||||
|
|
@ -33,25 +71,43 @@ async def get_leaf_options():
|
||||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
async def run_scan_task(paths: List[str]):
|
||||||
|
"""在背景執行的爬蟲任務,並將進度廣播出去"""
|
||||||
|
global IS_SCANNING
|
||||||
|
try:
|
||||||
|
await broadcast_message({"event": "scan_start"})
|
||||||
|
|
||||||
|
# 呼叫爬蟲 (帶入設備連線資訊)
|
||||||
|
async for chunk in sync_cmts_leaves_async(
|
||||||
|
host=CMTS_DEVICE.get("host"),
|
||||||
|
username=CMTS_DEVICE.get("username"),
|
||||||
|
password=CMTS_DEVICE.get("password"),
|
||||||
|
leaf_paths=paths
|
||||||
|
):
|
||||||
|
if isinstance(chunk, str):
|
||||||
|
await broadcast_message(json.loads(chunk))
|
||||||
|
else:
|
||||||
|
await broadcast_message(chunk)
|
||||||
|
|
||||||
|
await broadcast_message({"event": "done"})
|
||||||
|
except Exception as e:
|
||||||
|
await broadcast_message({"event": "error", "message": str(e)})
|
||||||
|
finally:
|
||||||
|
IS_SCANNING = False
|
||||||
|
|
||||||
@router.post("/cmts-leaf-options/sync")
|
@router.post("/cmts-leaf-options/sync")
|
||||||
async def sync_leaf_options(request: SyncOptionsRequest):
|
async def sync_leaf_options(request: SyncOptionsRequest, background_tasks: BackgroundTasks):
|
||||||
global IS_SCANNING # 宣告我們要修改全域變數
|
"""前端點擊掃描時,只負責觸發背景任務並立即回傳 JSON"""
|
||||||
|
global IS_SCANNING
|
||||||
|
|
||||||
if not request.leaf_paths:
|
if not request.leaf_paths:
|
||||||
raise HTTPException(status_code=400)
|
raise HTTPException(status_code=400)
|
||||||
|
|
||||||
# 🌟 2. 第一時間檢查並「立即」阻擋
|
|
||||||
if IS_SCANNING:
|
if IS_SCANNING:
|
||||||
async def busy_stream():
|
return {"status": "busy", "message": "⚠️ 掃描任務正在執行中,請勿重複點擊!"}
|
||||||
yield json.dumps({"event": "error", "message": "⚠️ 掃描任務正在執行中,請勿重複點擊!"}) + "\n"
|
|
||||||
return StreamingResponse(busy_stream(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
# 如果沒人掃描,立刻把狀態改為 True (在建立串流之前就上鎖)
|
|
||||||
IS_SCANNING = True
|
IS_SCANNING = True
|
||||||
|
|
||||||
async def event_generator():
|
|
||||||
global IS_SCANNING
|
|
||||||
try:
|
|
||||||
cmts_query_paths = []
|
cmts_query_paths = []
|
||||||
for p in request.leaf_paths:
|
for p in request.leaf_paths:
|
||||||
clean_p = p.replace("::", " ")
|
clean_p = p.replace("::", " ")
|
||||||
|
|
@ -60,45 +116,28 @@ async def sync_leaf_options(request: SyncOptionsRequest):
|
||||||
|
|
||||||
cmts_query_paths = list(dict.fromkeys(cmts_query_paths))
|
cmts_query_paths = list(dict.fromkeys(cmts_query_paths))
|
||||||
|
|
||||||
# 🌟 因為爬蟲自己會存檔,我們只要專心把進度轉發給前端就好!
|
# 將任務丟入背景執行,並立刻回傳 JSON 給前端
|
||||||
async for chunk in sync_cmts_leaves_async(
|
background_tasks.add_task(run_scan_task, cmts_query_paths)
|
||||||
host=CMTS_DEVICE.get("host"),
|
return {"status": "started"}
|
||||||
username=CMTS_DEVICE.get("username"),
|
|
||||||
password=CMTS_DEVICE.get("password"),
|
|
||||||
leaf_paths=cmts_query_paths
|
|
||||||
):
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
except Exception as e:
|
# ==========================================
|
||||||
yield json.dumps({"event": "error", "message": str(e)}) + "\n"
|
# 🌟 清除快取 API (對應 api.js 的 POST 請求)
|
||||||
|
# ==========================================
|
||||||
finally:
|
|
||||||
# 🌟 3. 無論成功、失敗、或使用者提早關閉網頁,都確保會解鎖
|
|
||||||
IS_SCANNING = False
|
|
||||||
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
# 🌟 新增:清除特定選項快取的 API (使用您原本定義的 CACHE_FILE)
|
|
||||||
@router.post("/clear_cache")
|
@router.post("/clear_cache")
|
||||||
async def clear_specific_cache(paths_to_clear: list = Body(...)):
|
async def clear_specific_cache(paths_to_clear: list = Body(...)):
|
||||||
cleared_count = 0
|
cleared_count = 0
|
||||||
|
|
||||||
# 1. 檢查快取檔案是否存在
|
|
||||||
if not os.path.exists(CACHE_FILE):
|
if not os.path.exists(CACHE_FILE):
|
||||||
return {"status": "success", "cleared_count": 0, "message": "快取檔案不存在"}
|
return {"status": "success", "cleared_count": 0, "message": "快取檔案不存在"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 2. 讀取現有的 JSON 快取資料
|
|
||||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||||
cache_data = json.load(f)
|
cache_data = json.load(f)
|
||||||
|
|
||||||
# 3. 逐一比對並刪除指定的路徑
|
|
||||||
for path in paths_to_clear:
|
for path in paths_to_clear:
|
||||||
if path in cache_data:
|
if path in cache_data:
|
||||||
del cache_data[path]
|
del cache_data[path]
|
||||||
cleared_count += 1
|
cleared_count += 1
|
||||||
|
|
||||||
# 4. 如果有刪除資料,將更新後的內容寫回 JSON 檔案
|
|
||||||
if cleared_count > 0:
|
if cleared_count > 0:
|
||||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(cache_data, f, ensure_ascii=False, indent=4)
|
json.dump(cache_data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
|
||||||
125
static/app.js
125
static/app.js
|
|
@ -37,16 +37,67 @@ import {
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 🌟 1. 全域生命週期與閒置偵測 (Lifecycle & Idle Detection)
|
// 🌟 1. 全域生命週期、閒置偵測與 SSE 廣播監聽
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
let idleTimer;
|
let idleTimer;
|
||||||
const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒)
|
const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒)
|
||||||
|
let globalSSE = null;
|
||||||
|
|
||||||
|
// 初始化全域 SSE 監聽器
|
||||||
|
function initGlobalSSE() {
|
||||||
|
if (globalSSE) return;
|
||||||
|
|
||||||
|
globalSSE = new EventSource('/api/v1/cmts-leaf-options/stream');
|
||||||
|
|
||||||
|
globalSSE.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
const statusEl = document.getElementById('scan-status');
|
||||||
|
|
||||||
|
// 🌟 關鍵修復:只要收到「開始」或「進度」訊號,一律強制鎖定按鈕!(確保中途加入的使用者也會被鎖定)
|
||||||
|
if (data.event === 'scan_start' || data.event === 'progress') {
|
||||||
|
setAdminButtonsState(true, "⏳ 系統更新中...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.event === 'scan_start') {
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `⏳ 系統正在背景更新快取,請稍候...`;
|
||||||
|
statusEl.style.color = "#f39c12";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data.event === 'progress') {
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `⏳ 背景掃描進度:<b>${data.current} / ${data.total}</b> (正在處理: ${data.current_path})`;
|
||||||
|
statusEl.style.color = "#f39c12";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data.event === 'done') {
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `✅ 掃描完美達成!快取已全面更新。`;
|
||||||
|
statusEl.style.color = "#27ae60";
|
||||||
|
setTimeout(() => statusEl.textContent = "", 5000);
|
||||||
|
}
|
||||||
|
// 掃描完成才解鎖
|
||||||
|
setAdminButtonsState(false);
|
||||||
|
localStorage.removeItem('scanLockUntil');
|
||||||
|
}
|
||||||
|
else if (data.event === 'error') {
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `❌ 錯誤: ${data.message}`;
|
||||||
|
statusEl.style.color = "#e74c3c";
|
||||||
|
}
|
||||||
|
// 發生錯誤也要解鎖
|
||||||
|
setAdminButtonsState(false);
|
||||||
|
localStorage.removeItem('scanLockUntil');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 頁面載入與縮放初始化
|
// 頁面載入與縮放初始化
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
initTerminal();
|
initTerminal();
|
||||||
resetIdleTimer(); // 頁面載入時啟動閒置計時器
|
resetIdleTimer(); // 頁面載入時啟動閒置計時器
|
||||||
|
initGlobalSSE(); // 啟動全域廣播監聽
|
||||||
};
|
};
|
||||||
window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); };
|
window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); };
|
||||||
|
|
||||||
|
|
@ -56,12 +107,10 @@ function resetIdleTimer() {
|
||||||
idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT);
|
idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 監聽使用者的互動事件,只要有動作就重置計時器
|
|
||||||
window.addEventListener('mousemove', resetIdleTimer);
|
window.addEventListener('mousemove', resetIdleTimer);
|
||||||
window.addEventListener('keydown', resetIdleTimer);
|
window.addEventListener('keydown', resetIdleTimer);
|
||||||
window.addEventListener('click', resetIdleTimer);
|
window.addEventListener('click', resetIdleTimer);
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 🌟 2. 頁籤切換與 UI 狀態控制 (Tabs & UI State)
|
// 🌟 2. 頁籤切換與 UI 狀態控制 (Tabs & UI State)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -317,7 +366,7 @@ async function enhanceInputWithOptions(path, elementId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 🌟 核心功能:執行掃描 (支援局部 / 全域)
|
// 🌟 核心功能:執行掃描 (廣播升級版)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
async function performScan(isGlobal) {
|
async function performScan(isGlobal) {
|
||||||
const isSomeoneScanning = await apiGetScanStatus();
|
const isSomeoneScanning = await apiGetScanStatus();
|
||||||
|
|
@ -330,13 +379,6 @@ async function performScan(isGlobal) {
|
||||||
if (!treeContainer) return;
|
if (!treeContainer) return;
|
||||||
|
|
||||||
const statusEl = document.getElementById('scan-status');
|
const statusEl = document.getElementById('scan-status');
|
||||||
const LOCK_DURATION_MS = 60000;
|
|
||||||
localStorage.setItem('scanLockUntil', Date.now() + LOCK_DURATION_MS);
|
|
||||||
|
|
||||||
// 鎖定所有 4 顆按鈕
|
|
||||||
setAdminButtonsState(true, "⏳ 掃描冷卻中...");
|
|
||||||
statusEl.innerHTML = `⏳ 正在比對快取資料...`;
|
|
||||||
statusEl.style.color = "#f39c12";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
||||||
|
|
@ -347,7 +389,6 @@ async function performScan(isGlobal) {
|
||||||
|
|
||||||
allContainers.forEach(container => {
|
allContainers.forEach(container => {
|
||||||
const isHiddenByFolder = container.closest('details:not([open])') !== null;
|
const isHiddenByFolder = container.closest('details:not([open])') !== null;
|
||||||
// 如果是全域掃描,或是該節點沒有被收合(局部可見),就納入檢查
|
|
||||||
if (isGlobal || !isHiddenByFolder) {
|
if (isGlobal || !isHiddenByFolder) {
|
||||||
const path = container.getAttribute('data-path');
|
const path = container.getAttribute('data-path');
|
||||||
if (path) {
|
if (path) {
|
||||||
|
|
@ -364,64 +405,41 @@ async function performScan(isGlobal) {
|
||||||
statusEl.textContent = isGlobal ? "✅ 全域所有欄位都已有最新選項,無需掃描!" : "✅ 局部展開的欄位都已有最新選項,無需掃描!";
|
statusEl.textContent = isGlobal ? "✅ 全域所有欄位都已有最新選項,無需掃描!" : "✅ 局部展開的欄位都已有最新選項,無需掃描!";
|
||||||
statusEl.style.color = "#27ae60";
|
statusEl.style.color = "#27ae60";
|
||||||
setTimeout(() => statusEl.textContent = "", 3000);
|
setTimeout(() => statusEl.textContent = "", 3000);
|
||||||
|
|
||||||
setAdminButtonsState(false);
|
|
||||||
localStorage.removeItem('scanLockUntil');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
statusEl.innerHTML = `⏳ 正在建立即時連線... (共需掃描 ${pathsToSync.length} 個項目)`;
|
// 直接發送 POST 請求觸發背景任務,UI 變化交給 SSE 處理
|
||||||
statusEl.style.color = "#e67e22";
|
const response = await fetch('/api/v1/cmts-leaf-options/sync', {
|
||||||
|
method: 'POST',
|
||||||
await apiSyncLeafOptionsStream(pathsToSync, (data) => {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
if (data.event === 'progress') {
|
body: JSON.stringify({ leaf_paths: pathsToSync })
|
||||||
statusEl.innerHTML = `⏳ 背景掃描進度:<b>${data.current} / ${data.total}</b> (正在處理: ${data.current_path})`;
|
|
||||||
statusEl.style.color = "#f39c12";
|
|
||||||
}
|
|
||||||
else if (data.event === 'done') {
|
|
||||||
statusEl.innerHTML = `✅ 掃描完美達成!快取已全面更新。`;
|
|
||||||
statusEl.style.color = "#27ae60";
|
|
||||||
setAdminButtonsState(false);
|
|
||||||
localStorage.removeItem('scanLockUntil');
|
|
||||||
setTimeout(() => statusEl.textContent = "", 5000);
|
|
||||||
}
|
|
||||||
else if (data.event === 'error') {
|
|
||||||
statusEl.innerHTML = `❌ 錯誤: ${data.message}`;
|
|
||||||
statusEl.style.color = "#e74c3c";
|
|
||||||
setAdminButtonsState(false);
|
|
||||||
localStorage.removeItem('scanLockUntil');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === 'busy') {
|
||||||
|
alert(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("掃描請求失敗:", error);
|
console.error("掃描請求發送失敗:", error);
|
||||||
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
|
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
|
||||||
statusEl.style.color = "#e74c3c";
|
statusEl.style.color = "#e74c3c";
|
||||||
setAdminButtonsState(false);
|
|
||||||
localStorage.removeItem('scanLockUntil');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 🌟 核心功能:清除快取 (支援局部 / 全域)
|
// 🌟 核心功能:清除快取 (廣播升級版)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
async function performClear(isGlobal) {
|
async function performClear(isGlobal) {
|
||||||
const isSomeoneScanning = await apiGetScanStatus();
|
const isSomeoneScanning = await apiGetScanStatus();
|
||||||
if (isSomeoneScanning) {
|
if (isSomeoneScanning) {
|
||||||
alert("⚠️ 系統正在進行全域選項掃描更新中!\n為避免資料衝突,請稍候掃描完成再執行清除動作。");
|
alert("⚠️ 系統正在進行全域選項掃描更新中!\n為避免資料衝突,請稍候掃描完成再執行清除動作。");
|
||||||
lockScanButton(60000);
|
|
||||||
const statusEl = document.getElementById('scan-status');
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.innerHTML = `⏳ 其他使用者正在背景更新快取,請稍候...`;
|
|
||||||
statusEl.style.color = "#e67e22";
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pathsToClear = [];
|
let pathsToClear = [];
|
||||||
|
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
// 全域清除:直接向後端要所有的快取 Key
|
|
||||||
try {
|
try {
|
||||||
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
||||||
const optData = await optRes.json();
|
const optData = await optRes.json();
|
||||||
|
|
@ -431,7 +449,6 @@ async function performClear(isGlobal) {
|
||||||
return alert("❌ 無法取得全域快取清單。");
|
return alert("❌ 無法取得全域快取清單。");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 局部清除:只抓畫面上展開的節點
|
|
||||||
const elements = document.querySelectorAll('.leaf-container');
|
const elements = document.querySelectorAll('.leaf-container');
|
||||||
elements.forEach(el => {
|
elements.forEach(el => {
|
||||||
const isHiddenByFolder = el.closest('details:not([open])') !== null;
|
const isHiddenByFolder = el.closest('details:not([open])') !== null;
|
||||||
|
|
@ -449,13 +466,21 @@ async function performClear(isGlobal) {
|
||||||
|
|
||||||
const msg = isGlobal
|
const msg = isGlobal
|
||||||
? `確定要清除「全域」共 ${pathsToClear.length} 個選項的快取嗎?\n(這將會清空所有已儲存的下拉選單資料)`
|
? `確定要清除「全域」共 ${pathsToClear.length} 個選項的快取嗎?\n(這將會清空所有已儲存的下拉選單資料)`
|
||||||
: `確定要清除局部 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「掃描局部缺失選項」)`;
|
: `確定要清除局部 ${pathsToClear.length} 個選項的快取嗎?\n(清除後將自動為您重新掃描)`;
|
||||||
|
|
||||||
if (!confirm(msg)) return;
|
if (!confirm(msg)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await apiClearCache(pathsToClear);
|
const result = await apiClearCache(pathsToClear);
|
||||||
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!`);
|
const statusEl = document.getElementById('scan-status');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = `✅ 成功清除 ${result.cleared_count} 筆快取!準備重新掃描...`;
|
||||||
|
statusEl.style.color = "#27ae60";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除完畢後,自動呼叫簡化版的 performScan
|
||||||
|
await performScan(isGlobal);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("清除快取失敗:", error);
|
console.error("清除快取失敗:", error);
|
||||||
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
|
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue