fix: 修復前端連線問題 - Portal iframe 延遲載入與請求取消機制

問題:
- Portal 同時載入 4 個 iframe,產生大量並行請求
- 瀏覽器連線數限制(約 6 個)導致請求排隊 timeout
- loadAllData 的並行請求沒有 AbortController 支援

修復:
- Portal: 移除 WIP 工站明細 tab(改從 Overview 進入)
- Portal: iframe 延遲載入(data-src),點擊 tab 時才載入
- wip_overview: loadAllData 加入統一 AbortController
- wip_overview: fetchSummary/fetchHold 支援 signal 參數
- wip_detail: loadAllData 加入 AbortController 支援

效果:
- Portal 載入時只載入 1 個 iframe(減少 75% 初始請求)
- 快速切換篩選時,舊請求會被取消
- 避免請求堆積導致 timeout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-27 20:44:27 +08:00
parent cbc19c9920
commit 3d4db4f530
3 changed files with 51 additions and 22 deletions

View File

@@ -97,18 +97,17 @@
<div class="tabs">
<button class="tab active" data-target="wipOverviewFrame">WIP 即時概況</button>
<button class="tab" data-target="wipDetailFrame">WIP 工站明細</button>
<button class="tab" data-target="resourceFrame">機台狀態報表</button>
<button class="tab" data-target="tableFrame">數據表查詢工具</button>
<button class="tab" data-target="excelQueryFrame">Excel 批次查詢</button>
</div>
<div class="panel">
<!-- Lazy load: only first iframe has src, others load on tab click -->
<iframe id="wipOverviewFrame" class="active" src="/wip-overview" title="WIP 即時概況"></iframe>
<iframe id="wipDetailFrame" src="/wip-detail" title="WIP 工站明細"></iframe>
<iframe id="resourceFrame" src="/resource" title="機台狀態報表"></iframe>
<iframe id="tableFrame" src="/tables" title="數據表查詢工具"></iframe>
<iframe id="excelQueryFrame" src="/excel-query" title="Excel 批次查詢"></iframe>
<iframe id="resourceFrame" data-src="/resource" title="機台狀態報表"></iframe>
<iframe id="tableFrame" data-src="/tables" title="數據表查詢工具"></iframe>
<iframe id="excelQueryFrame" data-src="/excel-query" title="Excel 批次查詢"></iframe>
</div>
</div>
@@ -130,7 +129,14 @@
tabs.forEach(tab => tab.classList.remove('active'));
frames.forEach(frame => frame.classList.remove('active'));
document.querySelector(`[data-target="${targetId}"]`).classList.add('active');
document.getElementById(targetId).classList.add('active');
const targetFrame = document.getElementById(targetId);
targetFrame.classList.add('active');
// Lazy load: load iframe src on first activation
if (targetFrame.dataset.src && !targetFrame.src) {
targetFrame.src = targetFrame.dataset.src;
}
}
tabs.forEach(tab => {

View File

@@ -728,8 +728,9 @@
// WIP Status filter (separate from other filters)
let activeStatusFilter = null; // null | 'run' | 'queue' | 'hold'
// AbortController for cancelling in-flight table requests
let tableAbortController = null;
// AbortController for cancelling in-flight requests
let tableAbortController = null; // For loadTableOnly()
let loadAllAbortController = null; // For loadAllData()
// ============================================================
// Utility Functions
@@ -963,7 +964,14 @@
// Data Loading
// ============================================================
async function loadAllData(showOverlay = true) {
if (state.isLoading) return;
// Cancel any in-flight request to prevent connection pile-up
if (loadAllAbortController) {
loadAllAbortController.abort();
console.log('[WIP Detail] Previous request cancelled');
}
loadAllAbortController = new AbortController();
const signal = loadAllAbortController.signal;
state.isLoading = true;
if (showOverlay) {
@@ -987,7 +995,7 @@
}
// Load detail data (main data - this is critical)
state.data = await fetchDetail();
state.data = await fetchDetail(signal);
renderSummary(state.data.summary);
renderTable(state.data);
@@ -999,6 +1007,11 @@
}, 1500);
} catch (error) {
// Ignore abort errors (expected when user triggers new request)
if (error.name === 'AbortError') {
console.log('[WIP Detail] Request cancelled (new request started)');
return;
}
console.error('Data load failed:', error);
document.getElementById('refreshError').classList.add('active');
} finally {

View File

@@ -765,8 +765,9 @@
// Status filter state (null = no filter, 'run'/'queue'/'hold' = filtered)
let activeStatusFilter = null;
// AbortController for cancelling in-flight matrix requests
let matrixAbortController = null;
// AbortController for cancelling in-flight requests
let matrixAbortController = null; // For loadMatrixOnly()
let loadAllAbortController = null; // For loadAllData()
// ============================================================
// Utility Functions
@@ -974,10 +975,10 @@
}
}
async function fetchSummary() {
async function fetchSummary(signal = null) {
const queryParams = buildQueryParams();
const url = `/api/wip/overview/summary${queryParams ? '?' + queryParams : ''}`;
const response = await fetchWithTimeout(url);
const response = await fetchWithTimeout(url, API_TIMEOUT, signal);
const result = await response.json();
if (result.success) {
return result.data;
@@ -1007,10 +1008,10 @@
throw new Error(result.error || 'Failed to fetch matrix');
}
async function fetchHold() {
async function fetchHold(signal = null) {
const queryParams = buildQueryParams();
const url = `/api/wip/overview/hold${queryParams ? '?' + queryParams : ''}`;
const response = await fetchWithTimeout(url);
const response = await fetchWithTimeout(url, API_TIMEOUT, signal);
const result = await response.json();
if (result.success) {
return result.data;
@@ -1224,10 +1225,14 @@
// Data Loading
// ============================================================
async function loadAllData(showOverlay = true) {
if (state.isLoading) {
console.log('[WIP Overview] Load skipped (already loading)');
return;
// Cancel any in-flight request to prevent connection pile-up
if (loadAllAbortController) {
loadAllAbortController.abort();
console.log('[WIP Overview] Previous request cancelled');
}
loadAllAbortController = new AbortController();
const signal = loadAllAbortController.signal;
state.isLoading = true;
console.log('[WIP Overview] Loading data...', showOverlay ? '(with overlay)' : '(background)');
@@ -1243,9 +1248,9 @@
try {
const startTime = performance.now();
const [summary, matrix, hold] = await Promise.all([
fetchSummary(),
fetchMatrix(),
fetchHold()
fetchSummary(signal),
fetchMatrix(signal),
fetchHold(signal)
]);
const elapsed = Math.round(performance.now() - startTime);
@@ -1267,6 +1272,11 @@
}, 1500);
} catch (error) {
// Ignore abort errors (expected when user triggers new request)
if (error.name === 'AbortError') {
console.log('[WIP Overview] Request cancelled (new request started)');
return;
}
console.error('[WIP Overview] Data load failed:', error);
state.lastError = true;
document.getElementById('refreshError').classList.add('active');