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:
@@ -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 => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user