diff --git a/src/api/mes/prodReport/index.ts b/src/api/mes/prodReport/index.ts index 950917d..c949020 100644 --- a/src/api/mes/prodReport/index.ts +++ b/src/api/mes/prodReport/index.ts @@ -157,3 +157,59 @@ export const exportHourlyOutputByHour = (query) => { responseType: 'blob' }); }; + +/** + * 工序生产统计(按工序聚合):完成数量、未完成数量与完成率 + */ +export const processWorkOrderStats = (query): AxiosPromise<[]> => { + return request({ + url: '/mes/prodReport/processWorkOrderStats', + method: 'get', + params: query + }); +}; + +/** + * 工序生产统计(分页,按工序聚合) + */ +export const processWorkOrderStatsPage = (query): AxiosPromise<[]> => { + return request({ + url: '/mes/prodReport/processWorkOrderStats/page', + method: 'get', + params: query + }); +}; + +/** + * 工序生产统计导出(按工序聚合) + */ +export const exportProcessWorkOrderStats = (query) => { + return request({ + url: '/mes/prodReport/processWorkOrderStats/export', + method: 'post', + params: query, + responseType: 'blob' + }); +}; + +/** + * 工序下的计划子节点查询:按 plan_code + process_id 过滤 + */ +export const planProcessChildren = (query): AxiosPromise<[]> => { + return request({ + url: '/mes/prodReport/planProcessChildren', + method: 'get', + params: query + }); +}; + +/** + * 按工序查询计划列表(不分页) + */ +export const processPlanList = (query): AxiosPromise<[]> => { + return request({ + url: '/mes/prodReport/processPlanList', + method: 'get', + params: query + }); +}; diff --git a/src/views/mes/wipTrackingReport/backup.vue b/src/views/mes/wipTrackingReport/backup.vue index 9bb782a..6bacd7f 100644 --- a/src/views/mes/wipTrackingReport/backup.vue +++ b/src/views/mes/wipTrackingReport/backup.vue @@ -106,6 +106,18 @@ + + + + + + 工序工单统计 + + + + + + @@ -126,7 +138,7 @@ - + @@ -225,7 +237,7 @@ let processStatsChart: echarts.ECharts | null = null; // 列显隐信息 const columns = ref([ - { key: 0, label: '生产订单号', visible: true }, + { key: 0, label: '派工单号', visible: true }, { key: 1, label: '物料编号', visible: false }, { key: 2, label: '物料名称', visible: true }, { key: 3, label: '规格型号', visible: true }, @@ -247,7 +259,7 @@ const columns = ref([ const queryParams = ref({ pageNum: 1, pageSize: 10, - orderCode: '', + planCode: '', materialCode: '', materialName: '', progressStatus: '', @@ -294,7 +306,7 @@ function resetQuery() { queryParams.value = { pageNum: 1, pageSize: 10, - orderCode: '', + planCode: '', materialCode: '', materialName: '', progressStatus: '', @@ -449,49 +461,49 @@ function updateProgressChart() { function updateProcessStatsChart() { if (!processStatsChart || !reportList.value.length) return; - const processStats = { - '已完成': 0, - '进行中': 0, - '未开始': 0 - }; + const nowTs = Date.now(); + const statsMap = new Map(); reportList.value.forEach(item => { - if (item.processProgressList) { - item.processProgressList.forEach(process => { - if (process.isCompleted) { - processStats['已完成']++; - } else if (process.isInProgress) { - processStats['进行中']++; - } else { - processStats['未开始']++; - } - }); - } + (item.processProgressList || []).forEach((proc: any) => { + const name = proc.processName || '未知工序'; + const rec = statsMap.get(name) || { total: 0, notStarted: 0, incomplete: 0, delayed: 0, completed: 0 }; + + rec.total += 1; + const isCompleted = proc.isCompleted === 1 || !!proc.isCompleted; + const planStatus = proc.planStatus; + if (!isCompleted) rec.incomplete += 1; else rec.completed += 1; + if (planStatus === '0') rec.notStarted += 1; + + const planEndTime = proc.planEndTime ? new Date(proc.planEndTime).getTime() : null; + if (planEndTime && planEndTime < nowTs && !isCompleted) rec.delayed += 1; + + statsMap.set(name, rec); + }); }); + const topEntries = Array.from(statsMap.entries()).sort((a, b) => b[1].total - a[1].total).slice(0, 10); + const processNames = topEntries.map(([n]) => n); + const notStartedData = topEntries.map(([, v]) => v.notStarted); + const incompleteData = topEntries.map(([, v]) => v.incomplete); + const delayedData = topEntries.map(([, v]) => v.delayed); + const completionRateData = topEntries.map(([, v]) => (v.total > 0 ? +(v.completed * 100 / v.total).toFixed(2) : 0)); + const option = { - title: { - text: '工序完成率统计', - left: 'center', - textStyle: { fontSize: 14 } - }, - tooltip: { - trigger: 'item', - formatter: '{a} {b}: {c} ({d}%)' - }, + title: { text: '工序工单统计(Top10)', left: 'center', textStyle: { fontSize: 14 } }, + tooltip: { trigger: 'axis' }, + legend: { data: ['未开工', '未完成', '延期', '完成率%'], bottom: 0 }, + grid: { left: '3%', right: '6%', top: 40, bottom: 60, containLabel: true }, + xAxis: { type: 'category', data: processNames }, + yAxis: [ + { type: 'value', name: '工单数' }, + { type: 'value', name: '完成率%', min: 0, max: 100, position: 'right' } + ], series: [ - { - name: '工序状态', - type: 'pie', - radius: ['40%', '70%'], - data: Object.entries(processStats).map(([name, value]) => ({ name, value })), - itemStyle: { - color: function(params: any) { - const colors = ['#722ed1', '#52c41a', '#d9d9d9']; - return colors[params.dataIndex]; - } - } - } + { name: '未开工', type: 'bar', stack: 'count', data: notStartedData }, + { name: '未完成', type: 'bar', stack: 'count', data: incompleteData }, + { name: '延期', type: 'bar', stack: 'count', data: delayedData }, + { name: '完成率%', type: 'line', yAxisIndex: 1, data: completionRateData, smooth: true } ] }; diff --git a/src/views/mes/wipTrackingReport/index.vue b/src/views/mes/wipTrackingReport/index.vue index b78cfdb..2aa25fc 100644 --- a/src/views/mes/wipTrackingReport/index.vue +++ b/src/views/mes/wipTrackingReport/index.vue @@ -4,6 +4,34 @@ + + + + + + + + + + + + - + @@ -148,80 +176,38 @@ {{ Number(scope.row.completionRateNum || 0) }}% + 超额 +{{ Number(scope.row.completionRateNum || 0) - 100 }}% - - - - - - - - - - - - - - - - {{ Number(cScope.row.planAmount) > 0 ? Math.round((Number(cScope.row.completeAmount || 0) * 100.0) / Number(cScope.row.planAmount)) : 0 }}% - - - - - - - - - - - - - {{ getPlanStatusLabel(cScope.row.planStatus) }} - - - - - - - - + - + {{ Number(pScope.row.processProgress || 0) }}% + 超额 +{{ Number(pScope.row.processProgress || 0) - 100 }}% @@ -312,7 +298,7 @@ import type { ElFormInstance } from 'element-plus'; import * as echarts from 'echarts'; import { listWipTrackingReport, exportWipTrackingReport } from '@/api/mes/wipTrackingReport'; import { WipTrackingReportVO, WipTrackingReportQuery, ProcessProgressVO } from '@/api/mes/wipTrackingReport/types'; -import { planProcessChildren } from '@/api/mes/prodReport'; +import { planProcessChildren, processWorkOrderStatsPage, exportProcessWorkOrderStats, processPlanList } from '@/api/mes/prodReport'; const { proxy } = getCurrentInstance() as ComponentInternalInstance; @@ -323,6 +309,7 @@ const showSearch = ref(true); const total = ref(0); const queryFormRef = ref(); const dateRange = ref(['', '']); +const processTableRef = ref(null); // 图表引用 const statusChartRef = ref(null); @@ -345,6 +332,47 @@ const columns = ref([ // 计划子节点缓存:key = planCode__processId const planChildrenCache = ref>({}); +// 工序下计划列表缓存:key = processId +const processPlansCache = ref>({}); +// 工序展开加载状态:key = processId +const processPlansLoading = ref>({}); +function isProcessLoading(pid: any) { + const k = String(pid ?? ''); + return !!processPlansLoading.value[k]; +} +// 记录已展开的工序ID,搜索后自动恢复展开 +const expandedProcessIds = ref([]); +function addExpanded(pid: any) { + const id = Number(pid); + if (!expandedProcessIds.value.includes(id)) expandedProcessIds.value.push(id); +} +function removeExpanded(pid: any) { + const id = Number(pid); + expandedProcessIds.value = expandedProcessIds.value.filter(x => x !== id); +} +function reExpandProcessRows() { + nextTick(() => { + expandedProcessIds.value.forEach((pid) => { + const row = (processStatsList.value || []).find((r: any) => String(r?.processId ?? '') === String(pid)); + if (row && processTableRef.value?.toggleRowExpansion) { + processTableRef.value.toggleRowExpansion(row, true); + } + }); + }); +} + +// 搜索时需要收起所有已展开的子行,不进行自动恢复 +const skipReexpandOnce = ref(false); +function collapseAllProcessRows() { + nextTick(() => { + const rows = processStatsList.value || []; + if (processTableRef.value?.toggleRowExpansion) { + rows.forEach((row: any) => { + processTableRef.value.toggleRowExpansion(row, false); + }); + } + }); +} const queryParams = ref({ pageNum: 1, @@ -371,22 +399,29 @@ watch(dateRange, (newVal) => { /** 查询在制品跟踪报表列表 */ function getList() { loading.value = true; - // 为保证“工序统计”与子节点计划列表不受后端按计划分页影响, - // 这里按工序视角一次性拉取足够大的计划数据集进行前端聚合。 + // 清理缓存,避免筛选条件变化导致子表旧数据残留 + processPlansCache.value = {}; + planChildrenCache.value = {}; + processPlansLoading.value = {}; const baseQuery = { ...queryParams.value }; - const allQuery = { ...baseQuery, pageNum: 1, pageSize: 100000 }; - listWipTrackingReport(allQuery) - .then((response: any) => { - // 使用全量计划数据进行工序聚合统计与子列表构建 - reportList.value = response?.rows || []; - // 顶层分页改为按工序维度,total 等于工序统计项数量 - processStatsList.value = buildProcessStatsFromReport(reportList.value); - total.value = processStatsList.value.length; - loading.value = false; - + // 1) 顶层按工序分页 + const procReq = processWorkOrderStatsPage(baseQuery); + + Promise.all([procReq]) + .then(([procRes]: any[]) => { + // 顶层工序聚合分页数据 + processStatsList.value = procRes?.rows || []; + total.value = procRes?.total || (processStatsList.value?.length || 0); nextTick(() => { updateCharts(); + // 根据标记决定是否恢复展开的工序 + if (!skipReexpandOnce.value) { + reExpandProcessRows(); + } + // 仅跳过一次,后续恢复默认行为 + skipReexpandOnce.value = false; }); + loading.value = false; }) .catch(() => { loading.value = false; @@ -396,6 +431,14 @@ function getList() { /** 搜索按钮操作 */ function handleQuery() { queryParams.value.pageNum = 1; + // 清理缓存,确保加载新筛选条件下的数据 + processPlansCache.value = {}; + planChildrenCache.value = {}; + processPlansLoading.value = {}; + // 搜索时收起所有子节点并且不自动恢复展开 + expandedProcessIds.value = []; + skipReexpandOnce.value = true; + collapseAllProcessRows(); getList(); } @@ -417,12 +460,12 @@ function resetQuery() { } /** 导出按钮操作 */ -function handleExport() { - proxy?.$modal.confirm('是否确认导出所有在制品跟踪报表数据项?').then(() => { +function handleProcessExport() { + proxy?.$modal.confirm('是否确认导出按工序统计数据?').then(() => { proxy?.$modal.loading('正在导出数据,请稍候...'); - return exportWipTrackingReport(queryParams.value); + return exportProcessWorkOrderStats(queryParams.value); }).then((response: any) => { - proxy?.$download.blob(response, '在制品跟踪报表.xlsx'); + proxy?.$download.blob(response, '工序生产统计.xlsx'); proxy?.$modal.closeLoading(); }).catch(() => { proxy?.$modal.closeLoading(); @@ -431,6 +474,7 @@ function handleExport() { /** 获取进度颜色 */ function getProgressColor(percentage: number) { + if (percentage > 100) return '#f56c6c'; if (percentage < 30) return '#f56c6c'; if (percentage < 70) return '#e6a23c'; return '#67c23a'; @@ -489,43 +533,46 @@ function buildProcessTree(row: WipTrackingReportVO) { * 从 reportList 的每个订单的 processProgressList 中,筛选出工序名称匹配的项 */ function buildProcessPlanRows(procRow: any) { - const rows: any[] = []; - const targetProcessName = procRow?.processName; - if (!targetProcessName) return rows; - - (reportList.value || []).forEach(order => { - const procs = (order as any).processProgressList || []; - procs.forEach((p: any) => { - if (p?.processName === targetProcessName) { - const planAmount = Number(p?.planAmount ?? 0); - const completeAmount = Number(p?.completeAmount ?? 0); - const remainingAmount = p?.remainingAmount != null - ? Number(p.remainingAmount) - : Math.max(planAmount - completeAmount, 0); - const progressNum = p?.processProgress != null - ? Number(p.processProgress) - : (planAmount > 0 ? Math.round((completeAmount * 100.0) / planAmount) : 0); - rows.push({ - planCode: p?.planCode || (order as any).planCode || '', - processId: p?.processId || '', - materialName: p?.materialName || (order as any).materialName || '', - materialCode: p?.materialCode || (order as any).materialCode || '', - planAmount, - completeAmount, - remainingAmount, - processProgress: progressNum, - isCompleted: (p?.isCompleted === 1) || !!p?.isCompleted, - isInProgress: (p?.isInProgress === 1) || !!p?.isInProgress - }); - } - }); - }); - - // 进度高的排前 + const key = String(procRow?.processId ?? ''); + const rows = (key && processPlansCache.value[key]) ? processPlansCache.value[key] : []; + // 进度高的排前(若有字段) rows.sort((a, b) => Number(b.processProgress || 0) - Number(a.processProgress || 0)); return rows; } +/** 外层工序展开时触发:加载该工序下计划列表(不分页) */ +async function onProcessRowExpand(row: any, expanded: boolean) { + const key = String(row?.processId ?? ''); + // 事件无 expanded 参数时也兼容加载 + if (!key) return; + // 同步维护已展开工序集合 + if (typeof expanded === 'boolean') { + expanded ? addExpanded(row?.processId) : removeExpanded(row?.processId); + } + if (processPlansCache.value[key] && processPlansCache.value[key].length) return; + // 标记该工序展开区为加载中 + processPlansLoading.value[key] = true; + // 组装查询参数 + const query: any = { + processId: Number(row?.processId), + beginDate: queryParams.value.beginDate, + endDate: queryParams.value.endDate, + materialCode: queryParams.value.materialCode, + materialName: queryParams.value.materialName, + planCode: queryParams.value.planCode + }; + try { + const res: any = await processPlanList(query); + const data = (res?.data || res?.rows || res) || []; + processPlansCache.value[key] = data; + } catch (e) { + processPlansCache.value[key] = []; + } finally { + // 结束加载态 + processPlansLoading.value[key] = false; + } +} + /** 生成计划子节点缓存Key */ function getPlanChildKey(row: any) { return `${row?.planCode || ''}__${row?.processId || ''}`; @@ -693,6 +740,9 @@ function updateProcessStatsChart() { const completedData = sorted.map((r: any) => Number(r.completedQty || 0)); const uncompletedData = sorted.map((r: any) => Number(r.uncompletedQty || 0)); const completionRateData = sorted.map((r: any) => Number(r.completionRateNum || 0)); + const maxRate = completionRateData.length ? Math.max(...completionRateData) : 0; + const yMax = Math.max(100, Math.ceil(maxRate / 10) * 10); + const showExcessArea = yMax > 100; const option = { title: { text: '工序生产统计(Top10)', left: 'center', textStyle: { fontSize: 14 } }, @@ -702,12 +752,18 @@ function updateProcessStatsChart() { xAxis: { type: 'category', data: processNames }, yAxis: [ { type: 'value', name: '数量' }, - { type: 'value', name: '完成率%', min: 0, max: 100, position: 'right' } + { type: 'value', name: '完成率%', min: 0, max: yMax, position: 'right', axisLabel: { formatter: '{value}%' } } ], series: [ { name: '已完成数量', type: 'bar', data: completedData }, { name: '未完成数量', type: 'bar', data: uncompletedData }, - { name: '完成率%', type: 'line', yAxisIndex: 1, data: completionRateData, smooth: true } + { name: '完成率%', type: 'line', yAxisIndex: 1, data: completionRateData, smooth: true, + markArea: showExcessArea ? { + itemStyle: { color: 'rgba(245,108,108,0.12)' }, + label: { color: '#f56c6c', position: 'insideTop', formatter: '超额区间' }, + data: [ [ { yAxis: 100 }, { yAxis: yMax } ] ] + } : undefined + } ] };