Files
DashBoard/apps/templates/resource_status.html
ymirliu b71dc9f6f8 fix: 修正 DOWN 機台明細查詢與顯示邏輯
1. 修正 Oracle ORA-32034 錯誤 - 將巢狀 WITH 子句改為同層級 CTE
2. Last Update 改用資料庫最新 LASTSTATUSCHANGEDATE,非系統時間
3. Down Time 改為 MAX(LASTSTATUSCHANGEDATE) - 各機台時間差
4. 機台明細表預設只顯示 UDT/SDT (DOWN 狀態)
5. 標題更新為 "DOWN 機台明細"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 11:51:21 +08:00

938 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全廠機況 Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
:root {
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #222;
--muted: #666;
--border: #e2e6ef;
--primary: #667eea;
--primary-dark: #5568d3;
--shadow: 0 2px 10px rgba(0,0,0,0.08);
--shadow-strong: 0 4px 15px rgba(102, 126, 234, 0.2);
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--neutral: #64748b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft JhengHei', Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.dashboard {
max-width: 1800px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 18px 22px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
margin-bottom: 16px;
box-shadow: var(--shadow-strong);
}
.header h1 {
font-size: 24px;
color: #fff;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.last-update {
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
}
/* Filters */
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
background: rgba(255, 255, 255, 0.9);
padding: 10px 14px;
border-radius: 10px;
box-shadow: var(--shadow);
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
}
.filter-group label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 13px;
color: var(--text);
}
.filter-group input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--primary);
}
.btn-query {
background: var(--primary);
color: white;
border: none;
padding: 9px 20px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-query:hover {
background: var(--primary-dark);
}
.btn-query:disabled {
background: #a6b0f5;
cursor: not-allowed;
}
/* KPI Cards */
.kpi-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 14px;
margin-bottom: 16px;
}
.kpi-card {
background: var(--card-bg);
border-radius: 10px;
padding: 18px;
text-align: center;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.kpi-label {
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
.kpi-value {
font-size: 30px;
font-weight: bold;
}
.kpi-value.green { color: var(--success); }
.kpi-value.blue { color: var(--primary); }
.kpi-value.red { color: var(--danger); }
.kpi-value.yellow { color: var(--warning); }
.kpi-sub {
font-size: 12px;
color: var(--muted);
margin-top: 4px;
}
/* Workcenter Cards Grid */
.workcenter-section {
margin-bottom: 16px;
}
.section-title {
font-size: 16px;
color: var(--primary);
margin-bottom: 10px;
padding-left: 10px;
border-left: 3px solid var(--primary);
}
.workcenter-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.wc-card {
background: var(--card-bg);
border-radius: 8px;
padding: 14px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.wc-card:hover {
border-color: var(--primary);
transform: translateY(-2px);
}
.wc-card.selected {
border-color: var(--success);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.25);
}
.wc-card.has-issue {
border-left: 4px solid var(--danger);
}
.wc-name {
font-size: 14px;
font-weight: 600;
color: var(--text);
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wc-stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.wc-ou {
font-size: 22px;
font-weight: bold;
}
.wc-ou.high { color: var(--success); }
.wc-ou.medium { color: var(--warning); }
.wc-ou.low { color: var(--danger); }
.wc-counts {
text-align: right;
font-size: 11px;
color: var(--muted);
}
.wc-counts .down {
color: var(--danger);
font-weight: bold;
}
.wc-mini-chart {
height: 34px;
margin-top: 8px;
}
/* Detail Table */
.detail-section {
background: var(--card-bg);
border-radius: 10px;
padding: 16px;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.detail-title {
font-size: 16px;
color: var(--primary);
}
.detail-count {
color: var(--muted);
font-size: 13px;
}
.table-container {
overflow-x: auto;
max-height: 350px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
thead {
position: sticky;
top: 0;
background: #eef2ff;
z-index: 1;
}
th {
padding: 10px 8px;
text-align: left;
color: #3f4aa7;
font-weight: 600;
white-space: nowrap;
border-bottom: 1px solid var(--border);
}
td {
padding: 8px;
border-bottom: 1px solid var(--border);
color: var(--text);
}
tbody tr:hover {
background: #f1f5ff;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
}
.status-prd { background: #bbf7d0; color: #166534; }
.status-sby { background: #bfdbfe; color: #1e40af; }
.status-udt { background: #fecaca; color: #991b1b; }
.status-sdt { background: #fef3c7; color: #92400e; }
.status-egt { background: #e2e8f0; color: #475569; }
.status-nst { background: #e9d5ff; color: #6b21a8; }
.status-other { background: #e5e7eb; color: #374151; }
.flag-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
margin-right: 3px;
}
.flag-prod { background: #dcfce7; color: #166534; }
.flag-key { background: #fee2e2; color: #991b1b; }
.flag-monitor { background: #dbeafe; color: #1e40af; }
.down-time {
color: var(--danger);
font-weight: bold;
}
.job-info {
font-size: 11px;
color: var(--muted);
}
/* Loading */
.loading-spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 10px;
}
.loading-text {
color: var(--primary);
font-size: 14px;
}
.placeholder {
text-align: center;
padding: 40px;
color: var(--muted);
}
/* Responsive */
@media (max-width: 1400px) {
.kpi-row {
grid-template-columns: repeat(5, 1fr);
}
.workcenter-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 1200px) {
.kpi-row {
grid-template-columns: repeat(3, 1fr);
}
.workcenter-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.kpi-row {
grid-template-columns: repeat(2, 1fr);
}
.workcenter-grid {
grid-template-columns: repeat(2, 1fr);
}
.header {
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="dashboard">
<!-- Header -->
<div class="header">
<h1>全廠機況 Dashboard</h1>
<div class="header-right">
<div class="filters">
<div class="filter-group">
<label><input type="checkbox" id="filterProduction"> 生產設備</label>
<label><input type="checkbox" id="filterKey"> 關鍵設備</label>
<label><input type="checkbox" id="filterMonitor"> 監控設備</label>
</div>
<button id="btnQuery" class="btn-query" onclick="loadDashboard()">查詢</button>
</div>
<span id="lastUpdate" class="last-update"></span>
</div>
</div>
<!-- KPI Row: OU / RUN / DOWN / IDLE / ENG -->
<div class="kpi-row">
<div class="kpi-card">
<div class="kpi-label">OU%</div>
<div class="kpi-value green" id="kpiOu">-</div>
<div class="kpi-sub" id="kpiOuFormula">PRD / (PRD+SBY+EGT+SDT+UDT)</div>
</div>
<div class="kpi-card">
<div class="kpi-label">RUN (PRD)</div>
<div class="kpi-value green" id="kpiRun">-</div>
<div class="kpi-sub" id="kpiRunCount">RUN: -</div>
</div>
<div class="kpi-card">
<div class="kpi-label">DOWN (UDT+SDT)</div>
<div class="kpi-value red" id="kpiDown">-</div>
<div class="kpi-sub" id="kpiDownDetail">UDT: - / SDT: -</div>
</div>
<div class="kpi-card">
<div class="kpi-label">IDLE (SBY+NST)</div>
<div class="kpi-value yellow" id="kpiIdle">-</div>
<div class="kpi-sub" id="kpiIdleDetail">SBY: - / NST: -</div>
</div>
<div class="kpi-card">
<div class="kpi-label">ENG (EGT)</div>
<div class="kpi-value blue" id="kpiEng">-</div>
<div class="kpi-sub" id="kpiEngCount">EGT: -</div>
</div>
</div>
<!-- Workcenter Cards -->
<div class="workcenter-section">
<div class="section-title">各工站狀態 (點選卡片查看明細)</div>
<div class="workcenter-grid" id="workcenterGrid">
<div class="placeholder">請點擊「查詢」載入資料</div>
</div>
</div>
<!-- Detail Table -->
<div class="detail-section" style="position: relative;">
<div class="detail-header">
<div class="detail-title" id="detailTitle">DOWN 機台明細 (UDT/SDT)</div>
<div class="detail-count" id="detailCount"></div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>機台</th>
<th>工作中心</th>
<th>狀態</th>
<th>原因</th>
<th>最後狀態時間</th>
<th>Down Time</th>
<th>批號 (PJ_LOTID)</th>
<th>症狀 (JOB)</th>
<th>原因碼 (JOB)</th>
<th>標記</th>
</tr>
</thead>
<tbody id="detailTableBody">
<tr><td colspan="10" class="placeholder">請點擊「查詢」載入資料</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
let selectedWorkcenter = null;
let selectedOriginalWcs = null; // 儲存合併群組的原始工站列表
let workcenterData = {}; // 快取工站資料
let isLoading = false;
let miniCharts = {}; // 快取 ECharts 實例
// 狀態顏色定義 (統一)
const STATUS_COLORS = {
PRD: '#00ff88', // 綠色 - 生產中
SBY: '#17a2b8', // 青色 - 待機
UDT: '#ff4757', // 紅色 - 非計畫停機
SDT: '#ffc107', // 黃色 - 計畫停機
EGT: '#6c757d', // 灰色 - 工程時間
NST: '#9b59b6' // 紫色 - NST
};
function getFilters() {
const filters = {};
if (document.getElementById('filterProduction').checked) filters.isProduction = true;
if (document.getElementById('filterKey').checked) filters.isKey = true;
if (document.getElementById('filterMonitor').checked) filters.isMonitor = true;
filters.days_back = 365;
// 多選廠區
return Object.keys(filters).length > 0 ? filters : null;
}
function formatNumber(num) {
if (num === null || num === undefined) return '-';
return num.toLocaleString('zh-TW');
}
function getStatusClass(status) {
if (!status) return 'status-other';
const s = status.toUpperCase();
if (s === 'PRD') return 'status-prd';
if (s === 'SBY') return 'status-sby';
if (s === 'UDT') return 'status-udt';
if (s === 'SDT') return 'status-sdt';
if (s === 'EGT') return 'status-egt';
if (s === 'NST') return 'status-nst';
return 'status-other';
}
function getOuClass(ouPct) {
if (ouPct >= 80) return 'high';
if (ouPct >= 50) return 'medium';
return 'low';
}
function formatDownTime(minutes) {
if (!minutes || minutes <= 0) return '-';
if (minutes < 60) return `${Math.round(minutes)}m`;
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
return `${hours}h ${mins}m`;
}
async function loadKPI() {
try {
const response = await fetch('/api/dashboard/kpi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters: getFilters() })
});
const result = await response.json();
if (result.success) {
const d = result.data;
// 計算各項百分比
const total = d.total || 1;
const runPct = ((d.run / total) * 100).toFixed(1);
const downPct = ((d.down / total) * 100).toFixed(1);
const idlePct = ((d.idle / total) * 100).toFixed(1);
const engPct = ((d.eng / total) * 100).toFixed(1);
// OU%
document.getElementById('kpiOu').textContent = d.ou_pct + '%';
document.getElementById('kpiOuFormula').textContent = `PRD / (PRD+SBY+EGT+SDT+UDT)`;
// RUN (PRD) - 顯示百分比
document.getElementById('kpiRun').textContent = runPct + '%';
document.getElementById('kpiRunCount').textContent = `RUN: ${formatNumber(d.run)}`;
// DOWN (UDT+SDT) - 顯示百分比
document.getElementById('kpiDown').textContent = downPct + '%';
document.getElementById('kpiDownDetail').textContent = `UDT: ${formatNumber(d.udt)} / SDT: ${formatNumber(d.sdt)}`;
// IDLE (SBY+NST) - 顯示百分比
document.getElementById('kpiIdle').textContent = idlePct + '%';
document.getElementById('kpiIdleDetail').textContent = `SBY: ${formatNumber(d.sby)} / NST: ${formatNumber(d.nst)}`;
// ENG (EGT) - 顯示百分比
document.getElementById('kpiEng').textContent = engPct + '%';
document.getElementById('kpiEngCount').textContent = `EGT: ${formatNumber(d.eng)}`;
}
} catch (error) {
console.error('KPI 載入失敗:', error);
}
}
async function loadWorkcenterCards() {
const grid = document.getElementById('workcenterGrid');
grid.innerHTML = '<div class="placeholder"><span class="loading-spinner"></span>載入中...</div>';
try {
const response = await fetch('/api/dashboard/workcenter_cards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters: getFilters() })
});
const result = await response.json();
if (result.success && result.data.length > 0) {
// 快取工站資料
workcenterData = {};
result.data.forEach(wc => {
workcenterData[wc.workcenter] = wc;
});
let html = '';
result.data.forEach(wc => {
const ouClass = getOuClass(wc.ou_pct);
const hasIssue = wc.udt > 0 || wc.sdt > 0;
const selectedClass = selectedWorkcenter === wc.workcenter ? 'selected' : '';
// 如果有多個原始工站,顯示提示
const tooltip = wc.original_wcs && wc.original_wcs.length > 1
? `${wc.workcenter} (含: ${wc.original_wcs.join(', ')})`
: wc.workcenter;
html += `
<div class="wc-card ${hasIssue ? 'has-issue' : ''} ${selectedClass}"
onclick="selectWorkcenter('${wc.workcenter}')">
<div class="wc-name" title="${tooltip}">${wc.workcenter}</div>
<div class="wc-stats">
<div class="wc-ou ${ouClass}">${wc.ou_pct}%</div>
<div class="wc-counts">
<div>Total: ${wc.total}</div>
<div>RUN: ${wc.prd}</div>
${wc.down > 0 ? `<div class="down">Down: ${wc.down}</div>` : ''}
</div>
</div>
<div class="wc-mini-chart" id="miniChart_${encodeURIComponent(wc.workcenter)}"></div>
</div>
`;
});
grid.innerHTML = html;
// Render mini charts
result.data.forEach(wc => {
renderMiniChart(wc);
});
} else {
grid.innerHTML = '<div class="placeholder">查無資料</div>';
}
} catch (error) {
console.error('工站卡片載入失敗:', error);
grid.innerHTML = '<div class="placeholder">載入失敗</div>';
}
}
function renderMiniChart(wc, retryCount = 0) {
// 使用 encodeURIComponent 確保中文名稱生成唯一 ID
const chartId = `miniChart_${encodeURIComponent(wc.workcenter)}`;
const chartDom = document.getElementById(chartId);
if (!chartDom) {
console.warn(`圖表容器不存在: ${chartId}`);
return;
}
// 銷毀舊的實例(如果存在)
if (miniCharts[chartId]) {
miniCharts[chartId].dispose();
}
// 確保容器有尺寸,如果沒有則延遲重試
if (chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
if (retryCount < 3) {
setTimeout(() => renderMiniChart(wc, retryCount + 1), 100);
}
return;
}
const chart = echarts.init(chartDom);
miniCharts[chartId] = chart;
const total = wc.total || 1;
// 計算各狀態百分比
const calcPct = (val) => ((val / total) * 100).toFixed(1);
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
const statusMap = {
'PRD': '生產中',
'SBY': '待機',
'UDT': '非計畫停機',
'SDT': '計畫停機',
'EGT': '工程時間',
'NST': '未排單'
};
const statusName = statusMap[params.seriesName] || params.seriesName;
const count = params.value;
const pct = calcPct(count);
return `<div style="font-weight:bold;margin-bottom:5px;">${statusName}</div>
<div>機台數: ${count}</div>
<div>佔比: ${pct}%</div>`;
},
backgroundColor: 'rgba(22, 33, 62, 0.95)',
borderColor: '#00d4ff',
borderWidth: 1,
textStyle: { color: '#eee', fontSize: 12 },
padding: [8, 12]
},
grid: { left: 0, right: 0, top: 0, bottom: 0 },
xAxis: { type: 'value', show: false, max: total },
yAxis: { type: 'category', show: false, data: [''] },
series: [
{
name: 'PRD',
type: 'bar',
stack: 'total',
data: [wc.prd],
itemStyle: { color: STATUS_COLORS.PRD },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.PRD } }
},
{
name: 'SBY',
type: 'bar',
stack: 'total',
data: [wc.sby],
itemStyle: { color: STATUS_COLORS.SBY },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.SBY } }
},
{
name: 'UDT',
type: 'bar',
stack: 'total',
data: [wc.udt],
itemStyle: { color: STATUS_COLORS.UDT },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.UDT } }
},
{
name: 'SDT',
type: 'bar',
stack: 'total',
data: [wc.sdt],
itemStyle: { color: STATUS_COLORS.SDT },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.SDT } }
},
{
name: 'EGT',
type: 'bar',
stack: 'total',
data: [wc.egt],
itemStyle: { color: STATUS_COLORS.EGT },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.EGT } }
},
{
name: 'NST',
type: 'bar',
stack: 'total',
data: [wc.nst],
itemStyle: { color: STATUS_COLORS.NST },
barWidth: '100%',
emphasis: { itemStyle: { color: STATUS_COLORS.NST } }
}
]
};
chart.setOption(option);
}
// 全局 resize 監聽器 - 調整所有圖表大小
window.addEventListener('resize', () => {
Object.values(miniCharts).forEach(chart => {
if (chart && !chart.isDisposed()) {
chart.resize();
}
});
});
function selectWorkcenter(workcenter) {
if (selectedWorkcenter === workcenter) {
selectedWorkcenter = null;
selectedOriginalWcs = null;
} else {
selectedWorkcenter = workcenter;
// 取得合併群組的原始工站列表
if (workcenterData[workcenter] && workcenterData[workcenter].original_wcs) {
selectedOriginalWcs = workcenterData[workcenter].original_wcs;
} else {
selectedOriginalWcs = [workcenter];
}
}
// Update card selection
document.querySelectorAll('.wc-card').forEach(card => {
card.classList.remove('selected');
});
if (selectedWorkcenter) {
const cards = document.querySelectorAll('.wc-card');
cards.forEach(card => {
if (card.querySelector('.wc-name').textContent === selectedWorkcenter) {
card.classList.add('selected');
}
});
}
loadDetail();
}
async function loadDetail() {
const tbody = document.getElementById('detailTableBody');
const title = document.getElementById('detailTitle');
const count = document.getElementById('detailCount');
tbody.innerHTML = '<tr><td colspan="10" class="placeholder"><span class="loading-spinner"></span>載入中...</td></tr>';
const filters = getFilters() || {};
if (selectedWorkcenter) {
filters.workcenter = selectedWorkcenter;
// 傳遞原始工站列表,讓後端可以用 IN 查詢
if (selectedOriginalWcs && selectedOriginalWcs.length > 0) {
filters.original_wcs = selectedOriginalWcs;
}
title.textContent = `DOWN 機台明細 - ${selectedWorkcenter}`;
} else {
title.textContent = 'DOWN 機台明細 (全部)';
}
try {
const response = await fetch('/api/dashboard/detail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters: Object.keys(filters).length > 0 ? filters : null, limit: 200, offset: 0 })
});
const result = await response.json();
if (result.success) {
count.textContent = `(${result.count} 筆)`;
// 更新 Last Update 使用 API 返回的最新狀態時間
if (result.max_status_time) {
document.getElementById('lastUpdate').textContent = `Last Update: ${result.max_status_time}`;
}
if (result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="placeholder">查無資料</td></tr>';
return;
}
let html = '';
result.data.forEach(row => {
const statusClass = getStatusClass(row.NEWSTATUSNAME);
const downTime = formatDownTime(row.DOWN_MINUTES);
let flags = '';
if (row.PJ_ISPRODUCTION === 1) flags += '<span class="flag-badge flag-prod">生產</span>';
if (row.PJ_ISKEY === 1) flags += '<span class="flag-badge flag-key">關鍵</span>';
if (row.PJ_ISMONITOR === 1) flags += '<span class="flag-badge flag-monitor">監控</span>';
// PJ_LOTID 來自 DW_MES_RESOURCE
// SYMPTOMCODENAME, CAUSECODENAME 來自 DW_MES_JOB (透過 JOBID 關聯)
// DOWN_MINUTES 使用最新 LASTSTATUSCHANGEDATE - 每台機台自己的時間
html += `
<tr>
<td><strong>${row.RESOURCENAME || '-'}</strong></td>
<td>${row.WORKCENTERNAME || '-'}</td>
<td><span class="status-badge ${statusClass}">${row.NEWSTATUSNAME || '-'}</span></td>
<td>${row.NEWREASONNAME || '-'}</td>
<td>${row.LASTSTATUSCHANGEDATE || '-'}</td>
<td class="${row.DOWN_MINUTES > 0 ? 'down-time' : ''}">${downTime}</td>
<td class="job-info">${row.PJ_LOTID || '-'}</td>
<td class="job-info">${row.SYMPTOMCODENAME || '-'}</td>
<td class="job-info">${row.CAUSECODENAME || '-'}</td>
<td>${flags || '-'}</td>
</tr>
`;
});
tbody.innerHTML = html;
} else {
tbody.innerHTML = `<tr><td colspan="10" class="placeholder">${result.error}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = `<tr><td colspan="10" class="placeholder">載入失敗: ${error.message}</td></tr>`;
}
}
async function loadDashboard() {
if (isLoading) return;
isLoading = true;
const btn = document.getElementById('btnQuery');
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span>查詢中...';
// Last Update 會由 loadDetail() 中的 API 回應更新
try {
await Promise.all([
loadKPI(),
loadWorkcenterCards(),
loadDetail()
]);
} finally {
isLoading = false;
btn.disabled = false;
btn.textContent = '查詢';
}
}
// Page init - 使用標記確保只執行一次
</script>
</body>
</html>