From 5f6e2a5ce08b2ef32d417418159dac469504a189 Mon Sep 17 00:00:00 2001 From: egg Date: Sun, 22 Feb 2026 13:50:54 +0800 Subject: [PATCH] feat(reject-history): add detail columns, CSV export polish, and metric filter sync Add EQUIPMENTNAME and REJECTCOMMENT columns to the detail table, list SQL, and per-LOT base query. Rewrite CSV export to use per-LOT rows with Chinese headers, BOM UTF-8 encoding, and fetch-based blob download with loading spinner. Sync trend chart legend filter (reject/defect) to detail table and export via metric_filter parameter through the full stack. Fix chart sizing with containLabel and autoresize throttle. Co-Authored-By: Claude Opus 4.6 --- frontend/src/reject-history/App.vue | 41 ++++++++- .../reject-history/components/DetailTable.vue | 6 +- .../reject-history/components/FilterPanel.vue | 5 +- .../reject-history/components/TrendChart.vue | 4 +- frontend/src/reject-history/style.css | 23 +++++ .../routes/reject_history_routes.py | 4 + .../services/reject_history_service.py | 86 ++++++++++++------- .../sql/reject_history/export.sql | 18 ++-- src/mes_dashboard/sql/reject_history/list.sql | 1 + .../reject_history/performance_daily_lot.sql | 3 + 10 files changed, 145 insertions(+), 46 deletions(-) diff --git a/frontend/src/reject-history/App.vue b/frontend/src/reject-history/App.vue index 1fea5d9..9494bcd 100644 --- a/frontend/src/reject-history/App.vue +++ b/frontend/src/reject-history/App.vue @@ -71,6 +71,7 @@ const loading = reactive({ querying: false, options: false, list: false, + exporting: false, }); const errorMessage = ref(''); @@ -267,12 +268,19 @@ function buildCommonParams({ reason = committedFilters.reason } = {}) { return buildRejectCommonQueryParams(committedFilters, { reason }); } +function metricFilterParam() { + const mode = paretoMetricMode.value; + if (mode === 'reject' || mode === 'defect') return mode; + return 'all'; +} + function buildListParams() { const effectiveReason = detailReason.value || committedFilters.reason; const params = { ...buildCommonParams({ reason: effectiveReason }), page: page.value, per_page: DEFAULT_PER_PAGE, + metric_filter: metricFilterParam(), }; if (selectedTrendDates.value.length > 0) { const sorted = [...selectedTrendDates.value].sort(); @@ -561,7 +569,9 @@ function onTrendDateClick(dateStr) { function onTrendLegendChange(selected) { trendLegendSelected.value = selected; + page.value = 1; updateUrlState(); + void loadListOnly(); } function onParetoClick(reason) { @@ -625,7 +635,9 @@ async function removeFilterChip(chip) { await loadDataSections(); } -function exportCsv() { +async function exportCsv() { + if (loading.exporting) return; + const effectiveReason = detailReason.value || committedFilters.reason; const queryParams = buildRejectCommonQueryParams(committedFilters, { reason: effectiveReason }); const params = new URLSearchParams(); @@ -638,8 +650,33 @@ function exportCsv() { appendArrayParams(params, 'workcenter_groups', queryParams.workcenter_groups || []); appendArrayParams(params, 'packages', queryParams.packages || []); appendArrayParams(params, 'reasons', queryParams.reasons || []); + params.set('metric_filter', metricFilterParam()); - window.location.href = `/api/reject-history/export?${params.toString()}`; + loading.exporting = true; + errorMessage.value = ''; + try { + const response = await fetch(`/api/reject-history/export?${params.toString()}`); + if (!response.ok) { + throw new Error('匯出 CSV 失敗'); + } + const blob = await response.blob(); + const disposition = response.headers.get('Content-Disposition') || ''; + const filenameMatch = disposition.match(/filename=(.+?)(?:;|$)/); + const filename = filenameMatch ? filenameMatch[1] : `reject_history_${queryParams.start_date}_to_${queryParams.end_date}.csv`; + + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + errorMessage.value = error?.message || '匯出 CSV 失敗'; + } finally { + loading.exporting = false; + } } const totalScrapQty = computed(() => { diff --git a/frontend/src/reject-history/components/DetailTable.vue b/frontend/src/reject-history/components/DetailTable.vue index 04f94c6..648adea 100644 --- a/frontend/src/reject-history/components/DetailTable.vue +++ b/frontend/src/reject-history/components/DetailTable.vue @@ -42,6 +42,8 @@ function formatNumber(value) { TYPE PRODUCT 原因 + EQUIPMENT + COMMENT 扣帳報廢量 {{ showRejectBreakdown ? '▾' : '▸' }} @@ -65,6 +67,8 @@ function formatNumber(value) { {{ row.PJ_TYPE }} {{ row.PRODUCTNAME || '' }} {{ row.LOSSREASONNAME }} + {{ row.EQUIPMENTNAME || '' }} + {{ row.REJECTCOMMENT || '' }} {{ formatNumber(row.REJECT_TOTAL_QTY) }}