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 @@
+
+
+
+ {{ title }}
+ {{ rows.length }} 条
+
+
+
+ {{ column.label }}
+
+
+
+
+
+
+ {{ row[column.key] || '--' }}
+
+
+
+
+
+ {{ status === 'loading' ? '加载中' : message }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ 设备运行状态明细
+
+ 运行
+ 停机
+
+
+
+
+ 设备
+ 状态
+ 自动化
+ 辅机
+ 利用率
+ 开动率
+ 合格率
+ OEE
+
+
+
+
+
+ {{ row.name }}
+ {{ row.status }}
+
+
+ {{ formatRate(row.utilization) }}
+ {{ formatRate(row.availability) }}
+ {{ formatFixedRate(row.qualityRate) }}
+ {{ formatRate(row.oee) }}
+
+
+
+
+ {{ status === 'loading' ? '加载中' : message }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ 设备
+ 生产订单
+ 物料名称
+ 物料编码
+ 工序
+ 预算工时
+ 数量
+ 进度
+
+
+
+
+
+ {{ row.deviceName || '--' }}
+ {{ row.productionOrder || '--' }}
+ {{ row.materialName || '--' }}
+ {{ row.materialCode || '--' }}
+ {{ row.processName || '--' }}
+ {{ formatBudget(row.budgetMinutes) }}
+ {{ formatNumber(row.plannedQty) }}
+ {{ formatNumber(row.reportedQty) }}/{{ formatNumber(row.plannedQty) }}
+
+
+
+
+ {{ status === 'loading' ? '加载中' : message }}
+
+
+
+
+
+
+
+
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) {