From 77d1a97e833c80155627a29a89ba6ab55df0a835 Mon Sep 17 00:00:00 2001 From: meswork764 Date: Mon, 20 Jul 2026 18:47:17 +0800 Subject: [PATCH] feat: update production kanban details --- 生产任务看板/doc/MES_Kanban_MonthlyRates.sql | 223 +++++ .../doc/MES_Kanban_ProductionInfo.sql | 97 +++ 生产任务看板/public/config.js | 3 +- .../plan-kanban/components/CenterDetailPanel.vue | 205 +++++ .../plan-kanban/components/DeviceStatusPanel.vue | 278 ++++++ .../plan-kanban/components/MachiningWipPanel.vue | 217 +++++ .../plan-kanban/composables/useCenterDetailData.js | 219 +++++ .../plan-kanban/composables/useDashboardData.js | 26 + .../plan-kanban/composables/useDeviceOeeData.js | 331 ++++++++ .../src/views/plan-kanban/config/modules.js | 1 + 生产任务看板/src/views/plan-kanban/index.vue | 799 ++++++++++++++++-- 11 files changed, 2336 insertions(+), 63 deletions(-) create mode 100644 生产任务看板/doc/MES_Kanban_MonthlyRates.sql create mode 100644 生产任务看板/doc/MES_Kanban_ProductionInfo.sql create mode 100644 生产任务看板/src/views/plan-kanban/components/CenterDetailPanel.vue create mode 100644 生产任务看板/src/views/plan-kanban/components/DeviceStatusPanel.vue create mode 100644 生产任务看板/src/views/plan-kanban/components/MachiningWipPanel.vue create mode 100644 生产任务看板/src/views/plan-kanban/composables/useCenterDetailData.js create mode 100644 生产任务看板/src/views/plan-kanban/composables/useDeviceOeeData.js diff --git a/生产任务看板/doc/MES_Kanban_MonthlyRates.sql b/生产任务看板/doc/MES_Kanban_MonthlyRates.sql new file mode 100644 index 0000000..1ff1fca --- /dev/null +++ b/生产任务看板/doc/MES_Kanban_MonthlyRates.sql @@ -0,0 +1,223 @@ +USE [YL_MESDB] +GO + +CREATE OR ALTER PROCEDURE [dbo].[MES_Kanban_MonthlyRates] +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @today date = CONVERT(date, GETDATE()); + DECLARE @currentStart date = DATEFROMPARTS(YEAR(@today), MONTH(@today), 1); + DECLARE @currentEnd date = DATEADD(month, 1, @currentStart); + DECLARE @previousStart date = DATEADD(month, -1, @currentStart); + DECLARE @previousEnd date = @currentStart; + + DECLARE @rates TABLE ( + [module] varchar(20) NOT NULL, + [currentNumerator] decimal(18, 2) NULL, + [currentDenominator] decimal(18, 2) NULL, + [previousNumerator] decimal(18, 2) NULL, + [previousDenominator] decimal(18, 2) NULL + ); + + -- Sales and purchase source views contain the exception set and total set. + INSERT INTO @rates + SELECT + 'sales', + CASE WHEN currentTotal.total > currentException.total THEN currentTotal.total - currentException.total ELSE 0 END, + currentTotal.total, + CASE WHEN previousTotal.total > previousException.total THEN previousTotal.total - previousException.total ELSE 0 END, + previousTotal.total + FROM ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_m] + WHERE [DocDate] >= @currentStart AND [DocDate] < @currentEnd + ) currentTotal + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_z] + WHERE [DocDate] >= @currentStart AND [DocDate] < @currentEnd + ) currentException + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_m] + WHERE [DocDate] >= @previousStart AND [DocDate] < @previousEnd + ) previousTotal + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_z] + WHERE [DocDate] >= @previousStart AND [DocDate] < @previousEnd + ) previousException; + + INSERT INTO @rates + SELECT + 'research', + SUM(CASE WHEN [预计结束时间] >= @currentStart AND [预计结束时间] < @currentEnd AND [单据状态] = N'已完成' THEN 1 ELSE 0 END), + SUM(CASE WHEN [预计结束时间] >= @currentStart AND [预计结束时间] < @currentEnd THEN 1 ELSE 0 END), + SUM(CASE WHEN [预计结束时间] >= @previousStart AND [预计结束时间] < @previousEnd AND [单据状态] = N'已完成' THEN 1 ELSE 0 END), + SUM(CASE WHEN [预计结束时间] >= @previousStart AND [预计结束时间] < @previousEnd THEN 1 ELSE 0 END) + FROM [dbo].[MES_设计报工_任务] + WHERE [预计结束时间] >= @previousStart AND [预计结束时间] < @currentEnd; + + INSERT INTO @rates + SELECT + 'plan', + SUM(CASE WHEN [计划开始时间] >= @currentStart AND [计划开始时间] < @currentEnd AND ISNULL([齐套], 0) = 1 THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @currentStart AND [计划开始时间] < @currentEnd THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @previousStart AND [计划开始时间] < @previousEnd AND ISNULL([齐套], 0) = 1 THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @previousStart AND [计划开始时间] < @previousEnd THEN 1 ELSE 0 END) + FROM [dbo].[View_生产订单_MES] + WHERE [计划开始时间] >= @previousStart AND [计划开始时间] < @currentEnd + AND [自制件属性] LIKE N'%装配%' + AND [物料编码] NOT LIKE N'%3420-HT2%' + AND [物料编码] NOT LIKE N'%3420-YT2%' + AND [物料编码] NOT LIKE N'%3420-YF2%' + AND [物料编码] NOT LIKE N'%3420-YP2%'; + + INSERT INTO @rates + SELECT + 'purchase', + CASE WHEN currentTotal.total > currentException.total THEN currentTotal.total - currentException.total ELSE 0 END, + currentTotal.total, + CASE WHEN previousTotal.total > previousException.total THEN previousTotal.total - previousException.total ELSE 0 END, + previousTotal.total + FROM ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate_M] + WHERE [DocDate] >= @currentStart AND [DocDate] < @currentEnd + ) currentTotal + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate] + WHERE [DocDate] >= @currentStart AND [DocDate] < @currentEnd + ) currentException + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate_M] + WHERE [DocDate] >= @previousStart AND [DocDate] < @previousEnd + ) previousTotal + CROSS JOIN ( + SELECT COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate] + WHERE [DocDate] >= @previousStart AND [DocDate] < @previousEnd + ) previousException; + + INSERT INTO @rates + SELECT + 'machining', + SUM(CASE WHEN [计划开始时间] >= @currentStart AND [计划开始时间] < @currentEnd + AND [开工时间] IS NOT NULL AND [开工时间] <= DATEADD(day, 1, [计划开始时间]) THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @currentStart AND [计划开始时间] < @currentEnd THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @previousStart AND [计划开始时间] < @previousEnd + AND [开工时间] IS NOT NULL AND [开工时间] <= DATEADD(day, 1, [计划开始时间]) THEN 1 ELSE 0 END), + SUM(CASE WHEN [计划开始时间] >= @previousStart AND [计划开始时间] < @previousEnd THEN 1 ELSE 0 END) + FROM [dbo].[View_生产订单_MES] + WHERE [计划开始时间] >= @previousStart AND [计划开始时间] < @currentEnd + AND [指派对象] <> N'走手续' + AND [指派对象] NOT LIKE N'%装配%' + AND [指派对象] NOT IN (N'期初物料', N'打磨', N'钻床', N'电火花', N'压力测试', N'酸洗/喷漆', N'抛光', N'外协') + AND [排产状态] = 1 + AND [订单编号] > 5000; + + ;WITH assemblyBase AS ( + SELECT + ISNULL([预计送检日期], CASE + WHEN [物料描述] LIKE N'%系统%' THEN DATEADD(day, 3, [钣金预达时间]) + ELSE DATEADD(day, 3, [齐套时间]) + END) AS expectedInspection, + CASE + WHEN ISNULL([任务状态], 0) >= 4 + OR (ISNULL([指派数量], 0) > 0 AND ISNULL([完成数量], 0) >= ISNULL([指派数量], 0)) + THEN [修改时间] + ELSE NULL + END AS completionTime, + CASE + WHEN [物料描述] LIKE N'%系统%' AND [派工时间] IS NOT NULL THEN 1 + WHEN [物料描述] NOT LIKE N'%系统%' AND ISNULL([发料状态], 0) = 1 THEN 1 + ELSE 0 + END AS executable + FROM [dbo].[View_生产订单_MES] + WHERE [物料类型] = N'pit_Resource' + AND [自制件属性] LIKE N'%装配%' + AND [物料描述] NOT LIKE N'%软管总成%' + AND [物料描述] NOT LIKE N'%发货部件%' + AND ISNULL([不装配], N'') <> N'否' + ) + INSERT INTO @rates + SELECT + 'assembly', + SUM(CASE WHEN expectedInspection >= @currentStart AND expectedInspection < @currentEnd AND executable = 1 + AND NOT (completionTime > expectedInspection OR (completionTime IS NULL AND expectedInspection < @today)) THEN 1 ELSE 0 END), + SUM(CASE WHEN expectedInspection >= @currentStart AND expectedInspection < @currentEnd AND executable = 1 THEN 1 ELSE 0 END), + SUM(CASE WHEN expectedInspection >= @previousStart AND expectedInspection < @previousEnd AND executable = 1 + AND NOT (completionTime > expectedInspection OR completionTime IS NULL) THEN 1 ELSE 0 END), + SUM(CASE WHEN expectedInspection >= @previousStart AND expectedInspection < @previousEnd AND executable = 1 THEN 1 ELSE 0 END) + FROM assemblyBase + WHERE expectedInspection >= @previousStart AND expectedInspection < @currentEnd; + + ;WITH qualitySummary AS ( + SELECT + [TaskAID], + SUM(ISNULL([检验数量], 0)) AS inspectedQuantity, + MAX([检测时间]) AS lastInspectionTime + FROM [dbo].[YL_质量检验_质检记录] + WHERE [检测类型编号] = 6 AND [TaskAID] IS NOT NULL + GROUP BY [TaskAID] + ), operationSummary AS ( + SELECT + [TaskAID], + SUM(ISNULL([操作数值], 0)) AS completedQuantity, + MAX(CASE WHEN ISNULL([操作数值], 0) <> 0 THEN COALESCE([报工日期], [结束时间]) END) AS lastReportTime + FROM [dbo].[YL_加工中心_操作记录表] + WHERE ISNULL([操作类别编号], 0) IN (3, 6) AND [TaskAID] IS NOT NULL + GROUP BY [TaskAID] + ), qualityBase AS ( + SELECT + o.lastReportTime, + DATEADD(day, 2, o.lastReportTime) AS inspectionDeadline, + o.completedQuantity, + ISNULL(q.inspectedQuantity, 0) AS inspectedQuantity, + q.lastInspectionTime + FROM operationSummary o + LEFT JOIN qualitySummary q ON q.[TaskAID] = o.[TaskAID] + WHERE o.lastReportTime >= @previousStart AND o.lastReportTime < @currentEnd + AND o.completedQuantity > 0 + ) + INSERT INTO @rates + SELECT + 'quality', + SUM(CASE WHEN lastReportTime >= @currentStart AND lastReportTime < @currentEnd + AND ((inspectedQuantity >= completedQuantity AND lastInspectionTime <= inspectionDeadline) + OR (inspectedQuantity < completedQuantity AND inspectionDeadline >= @today)) THEN 1 ELSE 0 END), + SUM(CASE WHEN lastReportTime >= @currentStart AND lastReportTime < @currentEnd THEN 1 ELSE 0 END), + SUM(CASE WHEN lastReportTime >= @previousStart AND lastReportTime < @previousEnd + AND inspectedQuantity >= completedQuantity AND lastInspectionTime <= inspectionDeadline THEN 1 ELSE 0 END), + SUM(CASE WHEN lastReportTime >= @previousStart AND lastReportTime < @previousEnd THEN 1 ELSE 0 END) + FROM qualityBase; + + -- Temporary placeholder until the warehouse timely-rate formula is confirmed. + INSERT INTO @rates + VALUES ('warehouse', 95, 100, 95, 100); + + SELECT + [module], + CAST(CASE WHEN ISNULL([currentDenominator], 0) = 0 THEN 0 ELSE ISNULL([currentNumerator], 0) * 100.0 / [currentDenominator] END AS decimal(18, 2)) AS [currentRate], + CAST(CASE WHEN ISNULL([previousDenominator], 0) = 0 THEN 0 ELSE ISNULL([previousNumerator], 0) * 100.0 / [previousDenominator] END AS decimal(18, 2)) AS [previousRate], + ISNULL([currentNumerator], 0) AS [currentNumerator], + ISNULL([currentDenominator], 0) AS [currentDenominator], + ISNULL([previousNumerator], 0) AS [previousNumerator], + ISNULL([previousDenominator], 0) AS [previousDenominator] + FROM @rates + ORDER BY CASE [module] + WHEN 'sales' THEN 1 + WHEN 'research' THEN 2 + WHEN 'plan' THEN 3 + WHEN 'purchase' THEN 4 + WHEN 'machining' THEN 5 + WHEN 'assembly' THEN 6 + WHEN 'quality' THEN 7 + WHEN 'warehouse' THEN 8 + ELSE 99 + END; +END +GO diff --git a/生产任务看板/doc/MES_Kanban_ProductionInfo.sql b/生产任务看板/doc/MES_Kanban_ProductionInfo.sql new file mode 100644 index 0000000..3d0a7e6 --- /dev/null +++ b/生产任务看板/doc/MES_Kanban_ProductionInfo.sql @@ -0,0 +1,97 @@ +CREATE OR ALTER PROCEDURE [dbo].[MES_Kanban_ProductionInfo] +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @today date = CONVERT(date, GETDATE()); + DECLARE @tomorrow date = DATEADD(day, 1, @today); + DECLARE @monthStart date = DATEFROMPARTS(YEAR(@today), MONTH(@today), 1); + + DECLARE @currentPlan decimal(18, 2) = 0; + DECLARE @currentOutput decimal(18, 2) = 0; + DECLARE @completionRate decimal(18, 2) = 0; + DECLARE @workHours decimal(18, 1) = 0; + DECLARE @kitRate decimal(18, 2) = 0; + DECLARE @qualityRate decimal(18, 2) = 0; + + ;WITH AssemblyOrders AS + ( + SELECT + CONVERT(nvarchar(100), [订单编号]) AS [订单编号], + MAX(ISNULL(CONVERT(decimal(18, 4), [计划数量订单]), 0)) AS [计划数量], + MAX(ISNULL(CONVERT(decimal(18, 4), [完成数量]), 0)) AS [完成数量] + FROM [dbo].[View_生产订单_MES] + WHERE [计划完成] >= @monthStart + AND [计划完成] < @tomorrow + AND [自制件属性] LIKE N'%装配%' + GROUP BY CONVERT(nvarchar(100), [订单编号]) + ) + SELECT + @currentPlan = ISNULL(SUM([计划数量]), 0), + @currentOutput = ISNULL(SUM([完成数量]), 0) + FROM AssemblyOrders; + + SET @completionRate = CAST( + CASE WHEN @currentPlan > 0 + THEN @currentOutput * 100.0 / @currentPlan + ELSE 0 + END AS decimal(18, 2) + ); + + SELECT @workHours = CAST(ISNULL(SUM(ISNULL([加工工时], 0)), 0) / 3600.0 AS decimal(18, 1)) + FROM [dbo].[View_生产工时视图全部] + WHERE [结束时间] >= @monthStart + AND [结束时间] < @tomorrow + AND ISNULL([阶段标识], 0) = -1 + AND ISNULL([加工工时], 0) > 0; + + SELECT @kitRate = CAST( + CASE WHEN COUNT_BIG(1) > 0 + THEN SUM(CASE WHEN ISNULL([齐套], 0) = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT_BIG(1) + ELSE 0 + END AS decimal(18, 2) + ) + FROM [dbo].[View_生产订单_MES] + WHERE [计划开始时间] >= @monthStart + AND [计划开始时间] < @tomorrow + AND [自制件属性] LIKE N'%装配%' + AND [物料编码] NOT LIKE N'%3420-HT2%' + AND [物料编码] NOT LIKE N'%3420-YT2%' + AND [物料编码] NOT LIKE N'%3420-YF2%' + AND [物料编码] NOT LIKE N'%3420-YP2%'; + + SELECT @qualityRate = CAST( + CASE WHEN ISNULL(SUM([检验数量]), 0) > 0 + THEN ISNULL(SUM([合格数]), 0) * 100.0 / SUM([检验数量]) + ELSE 0 + END AS decimal(18, 2) + ) + FROM [dbo].[View_质量检验_质检记录] + WHERE [工序名称] = N'装配' + AND [检测时间] >= @monthStart + AND [检测时间] < @tomorrow; + + SELECT [Zone], [Sort], [TagName], [TagValue], [TagUnit], [Icon], [color] + FROM + ( + SELECT N'生产' AS [Zone], 1 AS [Sort], N'当前计划' AS [TagName], + CONVERT(nvarchar(50), @currentPlan) AS [TagValue], N'台' AS [TagUnit], + N'AssemblyPlan' AS [Icon], N'#0678b4' AS [color] + UNION ALL + SELECT N'生产', 2, N'当前产量', CONVERT(nvarchar(50), @currentOutput), N'台', + N'ManufacturingExecution', N'#68b720' + UNION ALL + SELECT N'生产', 3, N'计划完成', CONVERT(nvarchar(50), @completionRate), N'%', + N'ProductionCheck', N'#e97378' + UNION ALL + SELECT N'生产', 4, N'工时消耗', CONVERT(nvarchar(50), @workHours), N'h', + N'ManHourStatistics', N'#9d6dd5' + UNION ALL + SELECT N'生产', 5, N'齐套率', CONVERT(nvarchar(50), @kitRate), N'%', + N'GroupMatching', N'#dc7e86' + UNION ALL + SELECT N'生产', 6, N'合格率', CONVERT(nvarchar(50), @qualityRate), N'%', + N'QualityBasicData', N'#fafc80' + ) AS ProductionInfo + ORDER BY [Sort]; +END; diff --git a/生产任务看板/public/config.js b/生产任务看板/public/config.js index 601a5a1..71a5dc7 100644 --- a/生产任务看板/public/config.js +++ b/生产任务看板/public/config.js @@ -1,5 +1,6 @@ window.g = { API_WEB_URL: 'https://192.168.2.92:10101', + BOARD_IFRAME_URL: 'http://192.168.2.92:10008/?dmtmode=DT', }; window.configData = { MachineToolList: [ @@ -202,4 +203,4 @@ window.configData = { {J1:0,J2:50.3,J3:60.0,J4:40,J5:90,J6:153.9}, {J1:0,J2:50.3,J3:60.0,J4:40,J5:90,J6:153.9,E1:1200} ] -} \ No newline at end of file +} diff --git a/生产任务看板/src/views/plan-kanban/components/CenterDetailPanel.vue b/生产任务看板/src/views/plan-kanban/components/CenterDetailPanel.vue new file mode 100644 index 0000000..e4df8b8 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/CenterDetailPanel.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/components/DeviceStatusPanel.vue b/生产任务看板/src/views/plan-kanban/components/DeviceStatusPanel.vue new file mode 100644 index 0000000..6b28f0c --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/DeviceStatusPanel.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/components/MachiningWipPanel.vue b/生产任务看板/src/views/plan-kanban/components/MachiningWipPanel.vue new file mode 100644 index 0000000..0a90f5f --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/MachiningWipPanel.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/composables/useCenterDetailData.js b/生产任务看板/src/views/plan-kanban/composables/useCenterDetailData.js new file mode 100644 index 0000000..76d0b92 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/composables/useCenterDetailData.js @@ -0,0 +1,219 @@ +import { inject, ref } from 'vue' + +const PROCEDURES = { + shipping: '装配中心任务看板_发货状态查询', + assemblyWip: '生产管理_装配状态_获取进行中任务' +} + +const SHIPPING_COLUMNS = [ + { key: 'productionOrder', label: '生产订单', width: '0.85fr' }, + { key: 'salesOrder', label: '销售订单', width: '1fr' }, + { key: 'materialName', label: '物料名称', width: '1.55fr' }, + { key: 'quantity', label: '数量', width: '0.52fr' }, + { key: 'requiredDate', label: '要求发货', width: '0.9fr' }, + { key: 'shippingStatus', label: '状态', width: '0.65fr' } +] + +const ASSEMBLY_WIP_COLUMNS = [ + { key: 'category', label: '分类', width: '0.62fr' }, + { key: 'operator', label: '操作人', width: '0.65fr' }, + { key: 'productionOrder', label: '生产订单', width: '0.78fr' }, + { key: 'materialName', label: '物料名称', width: '1.35fr' }, + { key: 'station', label: '工位', width: '0.72fr' }, + { key: 'progress', label: '完成/分配', width: '0.75fr' }, + { key: 'startTime', label: '开始时间', width: '0.9fr' } +] + +const getRows = response => { + if (Array.isArray(response?.data)) return response.data + if (Array.isArray(response?.data?.rows)) return response.data.rows + if (Array.isArray(response?.data?.result)) return response.data.result + if (Array.isArray(response?.data?.data)) return response.data.data + if (Array.isArray(response?.data?.Table)) return response.data.Table + return [] +} + +const hasValue = value => value !== null && value !== undefined && value !== '' +const textValue = value => hasValue(value) ? String(value).trim() : '' +const pickValue = (row, keys) => { + const key = keys.find(item => hasValue(row?.[item])) + return key ? row[key] : '' +} +const toNumber = value => { + const numberValue = Number(value) + return Number.isFinite(numberValue) ? numberValue : 0 +} +const formatNumber = value => { + if (!hasValue(value)) return '' + return toNumber(value).toLocaleString('zh-CN', { maximumFractionDigits: 2 }) +} +const formatDate = (value, includeTime = false) => { + if (!hasValue(value)) return '' + const text = String(value).replace('T', ' ') + if (includeTime) return text.slice(5, 16) + return text.slice(0, 10) +} +const isUrgent = row => toNumber(pickValue(row, ['加急总数', '加急数量'])) > 0 || + ['1', '是', '加急'].includes(textValue(pickValue(row, ['加急状态', '加急']))) + +const classifyAssemblyTask = row => { + const materialName = textValue(pickValue(row, ['物料描述', '零件名称', '物料名称', '产品名称'])) + const station = textValue(pickValue(row, ['工位名称', '指派对象', '操作人', '二次派工'])) + + if (station.includes('电气') || materialName.includes('电气')) return '电气类' + if (materialName.includes('阀')) return '部件类' + return '机械类' +} + +const formatShippingStatus = row => { + const value = textValue(pickValue(row, ['发货单状态', '发货状态'])) + if (value === 'O' || value === '1' || value.includes('待发')) return '待发货' + if (value === 'C' || value === '2' || value.includes('已发')) return '已发货' + return value +} + +const normalizeShippingRows = rows => rows.map((row, index) => { + const productionOrder = textValue(pickValue(row, ['生产单号', '订单编号', '订单号'])) + const salesOrder = textValue(pickValue(row, ['合同号', '项目号', '项目代码'])) + const materialName = textValue(pickValue(row, ['物料描述', '物料名称', '产品名称'])) + const materialCode = textValue(pickValue(row, ['物料编号', '物料编码', '产品编码'])) + const quantity = formatNumber(pickValue(row, ['交货数量', '计划数量'])) + const requiredDate = formatDate(pickValue(row, ['要求发货时间', '要求发货日期'])) + const shippingStatus = formatShippingStatus(row) + const workHours = formatNumber(row['合同装配工时']) + + return { + id: `shipping-${row.TaskAID || productionOrder || index}-${index}`, + productionOrder, + salesOrder, + materialName, + materialCode, + quantity, + requiredDate, + shippingStatus, + urgent: isUrgent(row), + title: [ + `生产订单: ${productionOrder || '--'}`, + `销售订单: ${salesOrder || '--'}`, + `物料: ${materialName || '--'} (${materialCode || '--'})`, + `交货数量: ${quantity || '--'}`, + `要求发货: ${requiredDate || '--'}`, + `累计装配工时: ${workHours || '0'}` + ].join(' | ') + } +}) + +const normalizeAssemblyWipRows = rows => rows.map((row, index) => { + const operator = textValue(pickValue(row, ['UserName', '操作人', '负责人'])) + const productionOrder = textValue(pickValue(row, ['订单号', '订单编号', '生产订单'])) + const salesOrder = textValue(pickValue(row, ['合同号', '销售订单'])) + const materialName = textValue(pickValue(row, ['物料描述', '零件名称', '物料名称', '产品名称'])) + const materialCode = textValue(pickValue(row, ['物料编号', '零件编码', '物料编码', '产品编码'])) + const station = textValue(pickValue(row, ['工位名称', '指派对象'])) + const assignedQty = formatNumber(pickValue(row, ['分配数', '指派数量', '计划数量'])) + const finishedQty = formatNumber(pickValue(row, ['完成数量', '送检数'])) || '0' + const startTime = formatDate(pickValue(row, ['StartTime', '开始时间']), true) + const category = classifyAssemblyTask(row) + + return { + id: `assembly-${row['操作记录ID'] || row.TaskAID || index}-${index}`, + category, + operator, + productionOrder, + salesOrder, + materialName, + materialCode, + station, + progress: `${finishedQty}/${assignedQty || '0'}`, + startTime, + urgent: isUrgent(row), + title: [ + `分类: ${category}`, + `操作人: ${operator || '--'}`, + `生产/销售订单: ${productionOrder || '--'} / ${salesOrder || '--'}`, + `物料: ${materialName || '--'} (${materialCode || '--'})`, + `工位: ${station || '--'}`, + `完成/分配: ${finishedQty}/${assignedQty || '0'}`, + `开始时间: ${startTime || '--'}` + ].join(' | ') + } +}) + +export function useCenterDetailData() { + const CreateData = inject('CreateData', null) + const ExecDatabase = inject('ExecDatabase', null) + const shippingRows = ref([]) + const shippingStatus = ref('loading') + const shippingMessage = ref('') + const assemblyWipRows = ref([]) + const assemblyWipStatus = ref('loading') + const assemblyWipMessage = ref('') + let activeLoad = null + + const queryProcedure = async name => { + if (!CreateData || !ExecDatabase) throw new Error('公共数据请求方法未注入') + return getRows(await ExecDatabase(CreateData('11', name))) + } + + const applyResult = (result, rowsRef, statusRef, messageRef, normalize, emptyMessage, errorMessage) => { + if (result.status === 'rejected') { + console.error(errorMessage, result.reason) + rowsRef.value = [] + statusRef.value = 'error' + messageRef.value = errorMessage + return + } + rowsRef.value = normalize(result.value) + statusRef.value = rowsRef.value.length ? 'ready' : 'empty' + messageRef.value = rowsRef.value.length ? '' : emptyMessage + } + + const loadCenterDetailData = async () => { + if (activeLoad) return activeLoad + + shippingStatus.value = 'loading' + assemblyWipStatus.value = 'loading' + shippingMessage.value = '' + assemblyWipMessage.value = '' + + activeLoad = Promise.allSettled([ + queryProcedure(PROCEDURES.shipping), + queryProcedure(PROCEDURES.assemblyWip) + ]).then(([shippingResult, assemblyResult]) => { + applyResult( + shippingResult, + shippingRows, + shippingStatus, + shippingMessage, + normalizeShippingRows, + '暂无发货任务', + '发货任务加载失败' + ) + applyResult( + assemblyResult, + assemblyWipRows, + assemblyWipStatus, + assemblyWipMessage, + normalizeAssemblyWipRows, + '暂无装配在制任务', + '装配在制任务加载失败' + ) + }).finally(() => { + activeLoad = null + }) + + return activeLoad + } + + return { + shippingColumns: SHIPPING_COLUMNS, + shippingRows, + shippingStatus, + shippingMessage, + assemblyWipColumns: ASSEMBLY_WIP_COLUMNS, + assemblyWipRows, + assemblyWipStatus, + assemblyWipMessage, + loadCenterDetailData + } +} diff --git a/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js b/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js index 84e5141..9948322 100644 --- a/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js +++ b/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js @@ -348,6 +348,21 @@ const normalizeOverviewModule = (rows, moduleKey) => { } } +const normalizeMonthlyRates = rows => rows.reduce((result, row) => { + const moduleKey = textValue(row.module) + if (moduleKey) { + result[moduleKey] = { + monthlyRate: toRate(row.currentRate), + previousMonthRate: toRate(row.previousRate), + monthlyRateNumerator: toNumber(row.currentNumerator), + monthlyRateDenominator: toNumber(row.currentDenominator), + previousMonthRateNumerator: toNumber(row.previousNumerator), + previousMonthRateDenominator: toNumber(row.previousDenominator) + } + } + return result +}, {}) + const mergeCards = (moduleState, cards, summary = {}) => { moduleState.status = 'ready' moduleState.message = '' @@ -471,6 +486,15 @@ export function useDashboardData() { }) } + const applyMonthlyRates = rows => { + const monthlyRates = normalizeMonthlyRates(rows) + Object.entries(monthlyRates).forEach(([moduleKey, rateSummary]) => { + if (state.modules[moduleKey]) { + Object.assign(state.modules[moduleKey].summary, rateSummary) + } + }) + } + const fetchPlan = async () => { const [rateRows, noProcessRows, scheduleTrendRows, urgentRows, lateCountRows, lateRows, notKittedRows] = await Promise.all([ safeQuery(PROCEDURES.planRate), @@ -724,6 +748,7 @@ export function useDashboardData() { try { const overviewPromise = safeQuery(PROCEDURES.departmentOverview) + const monthlyRatesPromise = safeQuery(PROCEDURES.monthlyRates) await Promise.all([ fetchPlan(), fetchMachining(), @@ -732,6 +757,7 @@ export function useDashboardData() { fetchWarehouse() ]) applyDepartmentOverview(await overviewPromise) + applyMonthlyRates(await monthlyRatesPromise) updatePanorama() state.lastUpdated = new Date().toLocaleString('zh-CN', { hour12: false }) } finally { diff --git a/生产任务看板/src/views/plan-kanban/composables/useDeviceOeeData.js b/生产任务看板/src/views/plan-kanban/composables/useDeviceOeeData.js new file mode 100644 index 0000000..5685075 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/composables/useDeviceOeeData.js @@ -0,0 +1,331 @@ +import { inject, ref } from 'vue' + +const PROCEDURES = { + devices: '设备管理_设备信息_查询', + stations: 'MES_登录_工位与名称_查询', + workHours: '工时统计_设备工时_查询', + quality: '生产管理_产品合格率工位_查询', + deviceStatus: '设备管理_设备状态_查询', + urgentTasks: '机加设备生产运营看板_加急任务查询' +} + +const getRows = response => { + if (Array.isArray(response?.data)) { + return response.data + } + if (Array.isArray(response?.data?.rows)) { + return response.data.rows + } + if (Array.isArray(response?.data?.result)) { + return response.data.result + } + if (Array.isArray(response?.data?.data)) { + return response.data.data + } + return [] +} + +const toNumber = value => { + const numberValue = Number(value) + return Number.isFinite(numberValue) ? numberValue : 0 +} + +const roundRate = value => Number(toNumber(value).toFixed(2)) +const normalizeText = value => String(value ?? '').trim() +const normalizeKey = value => normalizeText(value).replace(/\s+/g, '').toLowerCase() +const pickValue = (row, keys, fallback = '') => { + const key = keys.find(item => row?.[item] !== undefined && row[item] !== null && row[item] !== '') + return key ? row[key] : fallback +} + +const toDateText = date => { + const year = date.getFullYear() + const month = `${date.getMonth() + 1}`.padStart(2, '0') + const day = `${date.getDate()}`.padStart(2, '0') + return `${year}-${month}-${day}` +} + +const getMonthRange = () => { + const today = new Date() + today.setHours(0, 0, 0, 0) + return { + start: toDateText(new Date(today.getFullYear(), today.getMonth(), 1)), + end: toDateText(today) + } +} + +const splitBindings = value => normalizeText(value) + .split(/[,,;;|/、\s]+/) + .map(item => item.trim()) + .filter(Boolean) + +const isMajorDevice = row => ['是', '1', 'true', 'yes'].includes(normalizeText(row['主要设备']).toLowerCase()) + +const mapWithConcurrency = async (items, concurrency, iteratee) => { + const results = new Array(items.length) + let cursor = 0 + + const worker = async () => { + while (cursor < items.length) { + const index = cursor + cursor += 1 + results[index] = await iteratee(items[index], index) + } + } + + await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker)) + return results +} + +const getDeviceState = value => { + const status = normalizeText(value) + const stopped = ['停', '禁用', '故障', '报废'].some(keyword => status.includes(keyword)) + return { + status: stopped ? '停机' : '运行', + statusTone: stopped ? 'off' : 'run' + } +} + +const summarizeWorkHours = (rows, stationNames, standardHours) => { + const stationSet = new Set(stationNames.map(normalizeKey)) + const stationRows = rows.filter(row => stationSet.has(normalizeKey(row.operator))) + const summaryRows = stationRows.filter(row => Number(row.Level) === 1) + const dayRows = stationRows.filter(row => Number(row.Level) === 2 && normalizeText(row.dateLabel)) + const workHours = summaryRows.reduce((total, row) => total + toNumber(row.workHours), 0) + const reportDays = new Set(dayRows.map(row => normalizeText(row.dateLabel))).size + const standardOpenHours = toNumber(standardHours) + const denominator = standardOpenHours * reportDays + + return { + workHours, + reportDays, + standardOpenHours, + utilization: denominator > 0 ? roundRate((workHours / denominator) * 100) : 0 + } +} + +const getTaskKey = (orderNo, processName) => [orderNo, processName].map(normalizeKey).join('|') + +const createTaskWorkMap = rows => { + const workMap = new Map() + rows + .filter(row => Number(row.Level) === 3) + .forEach(row => { + const key = getTaskKey( + pickValue(row, ['orderNo', '订单号', '订单编号', '生产订单']), + pickValue(row, ['processName', '工序名称', '工序']) + ) + if (key !== '|') { + workMap.set(key, (workMap.get(key) || 0) + toNumber(pickValue(row, ['workHours', '加工工时', '加工工时和']))) + } + }) + return workMap +} + +const isUrgentRow = row => { + const urgentQty = toNumber(pickValue(row, ['加急数量', '加急数'])) + const text = normalizeText(pickValue(row, ['加急', '是否加急', '加急状态', '加急标识', '紧急程度'])).toLowerCase() + return urgentQty > 0 || ['是', 'y', 'yes', 'true', '1', '加急'].includes(text) || + (text.includes('加急') && !text.includes('不加急')) +} + +const normalizeWipRows = (statusRows, urgentRows, devices, workHourRows) => { + const statusByDevice = new Map() + statusRows.forEach(row => { + const keys = [ + pickValue(row, ['设备名称', '设备']), + pickValue(row, ['指派对象', '工位', '操作人']) + ].flatMap(splitBindings).map(normalizeKey).filter(Boolean) + keys.forEach(key => { + if (!statusByDevice.has(key)) { + statusByDevice.set(key, row) + } + }) + }) + + const urgentOrders = new Set( + urgentRows + .map(row => normalizeKey(pickValue(row, ['订单编号', '订单号', '生产订单']))) + .filter(Boolean) + ) + const taskWorkMap = createTaskWorkMap(workHourRows) + return devices.map(device => { + const deviceName = normalizeText(device.source['设备名称']) + const matchKeys = [ + deviceName, + ...device.stationNames, + ...splitBindings(device.source['工位绑定']) + ].map(normalizeKey).filter(Boolean) + const row = matchKeys.map(key => statusByDevice.get(key)).find(Boolean) + if (!row) { + return null + } + + const orderNo = normalizeText(pickValue(row, ['订单号', '订单编号', '生产订单'])) + const contractNo = normalizeText(pickValue(row, ['合同号', '销售订单'])) + const processName = normalizeText(pickValue(row, ['工序名称', '工序'])) + const plannedQty = toNumber(pickValue(row, ['分配数', '分配数量', '计划数量'])) + const reportedQty = toNumber(pickValue(row, ['报工数量', '完成数量', '完工数量'])) + const sourceWorkMinutes = toNumber(pickValue(row, ['加工工时和', '加工工时M', '加工工时'])) + const workMinutes = sourceWorkMinutes || (taskWorkMap.get(getTaskKey(orderNo, processName)) || 0) * 60 + const materialName = normalizeText(pickValue(row, ['零件名称', '物料名称', '物料描述'])) + const materialCode = normalizeText(pickValue(row, ['零件编码', '物料编码', '物料编号'])) + + return { + id: device.source['设备流水号'] || normalizeKey(deviceName), + deviceName, + productionOrder: [orderNo, contractNo].filter(Boolean).join('-'), + materialName, + materialCode, + processName, + budgetMinutes: reportedQty > 0 ? Number((workMinutes / reportedQty).toFixed(2)) : null, + plannedQty, + reportedQty, + urgent: urgentOrders.has(normalizeKey(orderNo)) || isUrgentRow(row) + } + }).filter(Boolean) +} + +export function useDeviceOeeData() { + const CreateData = inject('CreateData', null) + const ExecDatabase = inject('ExecDatabase', null) + const rows = ref([]) + const status = ref('loading') + const message = ref('') + const wipRows = ref([]) + const wipStatus = ref('loading') + const wipMessage = ref('') + let activeLoad = null + + const queryProcedure = async (name, params = []) => { + if (!CreateData || !ExecDatabase) { + throw new Error('公共数据请求方法未注入') + } + const payload = CreateData('11', name, params) + return getRows(await ExecDatabase(payload)) + } + + const queryQuality = async stationNames => { + const resultSets = await Promise.all(stationNames.map(async stationName => { + try { + return await queryProcedure(PROCEDURES.quality, [ + ['工位', stationName], + ['合格状态', '全部'] + ]) + } catch (error) { + console.error(`设备合格率加载失败: ${stationName}`, error) + return [] + } + })) + const qualityRows = resultSets.flat() + const defectiveQty = qualityRows.reduce((total, row) => total + toNumber(row['不良数量']), 0) + const plannedQty = qualityRows.reduce((total, row) => total + toNumber(row['计划数量']), 0) + + return { + defectiveQty, + plannedQty, + qualityRate: plannedQty > 0 ? roundRate((1 - defectiveQty / plannedQty) * 100) : 0 + } + } + + const loadDeviceOeeData = async () => { + if (activeLoad) { + return activeLoad + } + + status.value = 'loading' + message.value = '' + wipStatus.value = 'loading' + wipMessage.value = '' + activeLoad = (async () => { + try { + const range = getMonthRange() + const [deviceRows, stationRows, workHourRows, deviceStatusRows, urgentRows] = await Promise.all([ + queryProcedure(PROCEDURES.devices), + queryProcedure(PROCEDURES.stations), + queryProcedure(PROCEDURES.workHours, [ + ['开始日期', range.start], + ['结束日期', range.end] + ]), + queryProcedure(PROCEDURES.deviceStatus), + queryProcedure(PROCEDURES.urgentTasks) + ]) + + const stationMap = new Map(stationRows.map(row => [ + normalizeText(row['工位号']), + normalizeText(row['指南工位号'] || row['工位名称'] || row['工位号']) + ])) + const devices = deviceRows + .filter(row => isMajorDevice(row) && splitBindings(row['工位绑定']).length > 0) + .map(row => { + const stationNames = splitBindings(row['工位绑定']) + .map(binding => stationMap.get(binding) || binding) + .filter(Boolean) + return { + source: row, + stationNames, + ...getDeviceState(row['状态']) + } + }) + + const qualitySummaries = await mapWithConcurrency(devices, 4, device => queryQuality(device.stationNames)) + rows.value = devices.map((device, index) => { + const work = summarizeWorkHours( + workHourRows, + device.stationNames, + device.source['设备标准开机工时'] + ) + const quality = qualitySummaries[index] + const availability = 100 + const oee = roundRate((work.utilization / 100) * (availability / 100) * (quality.qualityRate / 100) * 100) + + return { + id: device.source['设备流水号'], + name: normalizeText(device.source['设备名称']), + stations: device.stationNames.join(' / '), + status: device.status, + statusTone: device.statusTone, + automation: 'run', + auxiliary: 'run', + utilization: work.utilization, + availability, + qualityRate: quality.qualityRate, + oee, + workHours: work.workHours, + reportDays: work.reportDays, + standardOpenHours: work.standardOpenHours, + defectiveQty: quality.defectiveQty, + qualityPlannedQty: quality.plannedQty + } + }) + status.value = rows.value.length ? 'ready' : 'empty' + message.value = rows.value.length ? '' : '暂无主要设备' + wipRows.value = normalizeWipRows(deviceStatusRows, urgentRows, devices, workHourRows) + wipStatus.value = wipRows.value.length ? 'ready' : 'empty' + wipMessage.value = wipRows.value.length ? '' : '暂无机加在制任务' + } catch (error) { + console.error('设备运行状态明细加载失败', error) + rows.value = [] + status.value = 'error' + message.value = '设备数据加载失败' + wipRows.value = [] + wipStatus.value = 'error' + wipMessage.value = '在制任务加载失败' + } + })().finally(() => { + activeLoad = null + }) + + return activeLoad + } + + return { + rows, + status, + message, + wipRows, + wipStatus, + wipMessage, + loadDeviceOeeData + } +} diff --git a/生产任务看板/src/views/plan-kanban/config/modules.js b/生产任务看板/src/views/plan-kanban/config/modules.js index 73888fc..11b2a11 100644 --- a/生产任务看板/src/views/plan-kanban/config/modules.js +++ b/生产任务看板/src/views/plan-kanban/config/modules.js @@ -67,6 +67,7 @@ export const MODULES = [ export const PROCEDURES = { departmentOverview: 'MES_Kanban_DepartmentOverview', + monthlyRates: 'MES_Kanban_MonthlyRates', planRate: '生产任务综合看板_按期齐套率', planNoProcess: '生产任务综合看板_未排产无工艺查询', planScheduleTrend: 'MES_Kanban_PlanScheduleTrend', diff --git a/生产任务看板/src/views/plan-kanban/index.vue b/生产任务看板/src/views/plan-kanban/index.vue index 9754bfd..129b7e8 100644 --- a/生产任务看板/src/views/plan-kanban/index.vue +++ b/生产任务看板/src/views/plan-kanban/index.vue @@ -2,11 +2,11 @@
@@ -22,11 +22,21 @@ /> --> -
+
+
+ +
+
+ +
+
+
+
+ + {{ zone.key }} +
+
+
+
+ +
+
+
{{ item.TagName }}
+
+ {{ item.TagValue }} + {{ item.TagUnit }} +
+
+
+
+ {{ centerLoading ? '加载中' : '暂无数据' }} +
+
+
+
+
+
+ + +
-
+

车间全景

@@ -178,7 +252,7 @@
-
+
@@ -191,18 +265,176 @@ @@ -1135,6 +1532,8 @@ onUnmounted(() => { min-height: 1080px; overflow-x: hidden; overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; color: #f3f9ff; font-family: 'Microsoft YaHei', Arial, sans-serif; background: @@ -1142,6 +1541,20 @@ onUnmounted(() => { #07001f; } +.plan-dashboard::-webkit-scrollbar, +:global(html::-webkit-scrollbar), +:global(body::-webkit-scrollbar) { + display: none; + width: 0; + height: 0; +} + +:global(html), +:global(body) { + scrollbar-width: none; + -ms-overflow-style: none; +} + .dashboard-shell { width: 100%; max-width: 1920px; @@ -1151,7 +1564,7 @@ onUnmounted(() => { } .dashboard-section { - margin-top: 17px; + margin-top: 5px; } :deep(.panel), @@ -1188,22 +1601,284 @@ onUnmounted(() => { } .department-chart-grid { + position: relative; + isolation: isolate; display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); - gap: 14px; + grid-template-areas: + 'sales research center center center center quality warehouse' + 'plan purchase center center center center machining assembly'; + gap: 5px; +} + +.department-center { + position: relative; + z-index: 1; + grid-area: center; + min-width: 0; + min-height: 0; + overflow: hidden; + pointer-events: none; +} + +.center-monitor-iframe { + position: absolute; + z-index: 0; + inset: 0; + display: block; + width: 100%; + height: 100%; + border: 0; +} + +.center-monitor-content { + position: relative; + z-index: 1; + min-height: 100%; + padding: 12px; + pointer-events: none; + background: linear-gradient(180deg, rgba(6, 19, 56, 0.48), transparent 42%); +} + +.device-oee-slot { + position: absolute; + z-index: 2; + bottom: 0; + left: 0; + width: calc(50% - 3px); + height: 202px; + pointer-events: auto; + background: rgba(7, 31, 72, 0.62); +} + +:deep(.device-oee-slot .device-data-row) { + height: 34px; +} + +.shipping-detail-slot { + position: absolute; + z-index: 2; + bottom: 0; + right: 0; + width: calc(50% - 3px); + height: 202px; + pointer-events: auto; + background: rgba(7, 31, 72, 0.62); +} + +.center-monitor-header { + display: flex; + align-items: center; + justify-content: center; + height: 54px; + margin: 0; + color: #f3f9ff; + font-size: 30px; + font-weight: 900; + letter-spacing: 0; + text-shadow: 0 0 14px rgba(28, 230, 255, 0.72); +} + +.center-env-bar { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; + margin-top: 6px; +} + +.center-env-row { + display: flex; + gap: 6px; + width: 100%; +} + +.center-zone-section { + min-width: 0; + padding: 5px 8px 7px; + overflow: hidden; + pointer-events: auto; + border: 1px solid rgba(0, 200, 255, 0.3); + border-radius: 6px; + background: rgba(0, 50, 120, 0.72); + box-shadow: inset 0 0 14px rgba(28, 160, 255, 0.14); +} + +.center-zone-environment { + flex: 7; +} + +.center-zone-production { + flex: 6; +} + +.center-zone-water { + flex: 3; +} + +.center-zone-electricity, +.center-zone-gas { + flex: 4; +} + +.center-zone-title { + display: flex; + align-items: center; + height: 20px; + padding-bottom: 3px; + margin-bottom: 4px; + color: #1ce6ff; + font-size: 12px; + font-weight: 800; + border-bottom: 1px solid rgba(0, 255, 255, 0.2); +} + +.center-zone-icon { + width: 14px; + height: 14px; + margin-right: 5px; +} + +.center-zone-items { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.center-zone-environment .center-env-item { + flex: 0 0 calc(25% - 3px); +} + +.center-zone-production .center-env-item { + flex: 0 0 calc(33.333% - 3px); +} + +.center-zone-water .center-env-item, +.center-zone-electricity .center-env-item, +.center-zone-gas .center-env-item { + flex: 0 0 calc(50% - 2px); +} + +.center-env-item { + display: flex; + align-items: center; + min-width: 0; + height: 34px; + padding: 4px 7px; + border: 1px solid rgba(0, 200, 255, 0.2); + border-radius: 4px; + background: rgba(0, 100, 255, 0.18); +} + +.center-env-icon { + display: flex; + flex: 0 0 22px; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin-right: 6px; + color: #1ce6ff; + border-radius: 50%; + background: rgba(0, 255, 255, 0.1); +} + +.center-env-icon svg { + width: 13px; + height: 13px; +} + +.center-env-info { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.center-env-label { + overflow: hidden; + color: #8cd3ff; + font-size: 10px; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.center-env-value-unit { + overflow: hidden; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.center-env-value { + margin-right: 3px; + color: #ffffff; + font-family: electronicFont, 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 700; +} + +.center-env-unit { + color: #91a8bc; + font-size: 9px; +} + +.center-zone-empty { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 72px; + color: #78a9ce; + font-size: 12px; +} + +.department-sales { + grid-area: sales; +} + +.department-research { + grid-area: research; +} + +.department-plan { + grid-area: plan; +} + +.department-purchase { + grid-area: purchase; +} + +.department-machining { + grid-area: machining; +} + +.department-assembly { + grid-area: assembly; +} + +.department-quality { + grid-area: quality; +} + +.department-warehouse { + grid-area: warehouse; } .department-chart-stack { + position: relative; + z-index: 2; display: flex; flex-direction: column; - gap: 14px; + gap: 5px; min-width: 0; } :deep(.department-mini-chart.chart-panel) { - height: 160px; - min-height: 160px; + height: 159px; + min-height: 159px; padding: 10px 12px; + background: rgba(7, 31, 72, 0.62); } :deep(.department-mini-chart .panel-head) {