From b5bc8d735839653548b4ab83e12753b05387a2f4 Mon Sep 17 00:00:00 2001 From: meswork764 Date: Sat, 11 Jul 2026 18:07:38 +0800 Subject: [PATCH] feat: add production task overview dashboard --- .../doc/MES_Kanban_DepartmentOverview.sql | 323 ++++ 生产任务看板/src/router/index.js | 28 +- .../src/views/plan-kanban/components/ChartPanel.vue | 261 ++++ .../plan-kanban/components/DashboardHeader.vue | 149 ++ .../src/views/plan-kanban/components/DataTable.vue | 191 +++ .../src/views/plan-kanban/components/MetricCard.vue | 128 ++ .../plan-kanban/composables/useDashboardData.js | 753 +++++++++ .../src/views/plan-kanban/composables/useEchart.js | 45 + .../src/views/plan-kanban/config/modules.js | 148 ++ 生产任务看板/src/views/plan-kanban/index.vue | 1368 +++++++++++++++++ 10 files changed, 3382 insertions(+), 12 deletions(-) create mode 100644 生产任务看板/doc/MES_Kanban_DepartmentOverview.sql create mode 100644 生产任务看板/src/views/plan-kanban/components/ChartPanel.vue create mode 100644 生产任务看板/src/views/plan-kanban/components/DashboardHeader.vue create mode 100644 生产任务看板/src/views/plan-kanban/components/DataTable.vue create mode 100644 生产任务看板/src/views/plan-kanban/components/MetricCard.vue create mode 100644 生产任务看板/src/views/plan-kanban/composables/useDashboardData.js create mode 100644 生产任务看板/src/views/plan-kanban/composables/useEchart.js create mode 100644 生产任务看板/src/views/plan-kanban/config/modules.js create mode 100644 生产任务看板/src/views/plan-kanban/index.vue diff --git a/生产任务看板/doc/MES_Kanban_DepartmentOverview.sql b/生产任务看板/doc/MES_Kanban_DepartmentOverview.sql new file mode 100644 index 0000000..9cf8c70 --- /dev/null +++ b/生产任务看板/doc/MES_Kanban_DepartmentOverview.sql @@ -0,0 +1,323 @@ +USE [YL_MESDB] +GO + +CREATE OR ALTER PROCEDURE [dbo].[MES_Kanban_DepartmentOverview] +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @today date = CONVERT(date, GETDATE()); + DECLARE @tomorrow date = DATEADD(day, 1, @today); + DECLARE @start date = DATEADD(day, -4, @today); + DECLARE @monthStart date = DATEFROMPARTS(YEAR(@today), MONTH(@today), 1); + + CREATE TABLE #result ( + [module] varchar(20) NOT NULL, + [chart] varchar(20) NOT NULL, + [series] nvarchar(30) NOT NULL, + [label] nvarchar(50) NOT NULL, + [sort] int NOT NULL, + [value] decimal(18, 2) NOT NULL, + [extraValue] decimal(18, 2) NULL + ); + + INSERT INTO #result ([module], [chart], [series], [label], [sort], [value], [extraValue]) + SELECT 'sales', 'bar', N'任务数', category, sort, COUNT_BIG(1), NULL + FROM ( + SELECT + CASE + WHEN [销售产品名称] = N'同轴阀' THEN N'同轴阀' + WHEN [销售产品名称] = N'同轴阀组' THEN N'同轴阀组' + WHEN [销售产品名称] LIKE N'%系统%' THEN N'系统' + ELSE N'其他' + END AS category, + CASE + WHEN [销售产品名称] = N'同轴阀' THEN 1 + WHEN [销售产品名称] = N'同轴阀组' THEN 2 + WHEN [销售产品名称] LIKE N'%系统%' THEN 3 + ELSE 4 + END AS sort + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_TransitTaskClass] + ) source + GROUP BY category, sort; + + INSERT INTO #result + SELECT 'sales', 'summary', N'滞留', N'销售滞留', 1, COUNT_BIG(1), NULL + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleBlgDet]; + + INSERT INTO #result + SELECT + 'sales', 'rate', N'准交率', N'准交率', 1, + CAST(CASE WHEN denominator.total = 0 THEN 0 ELSE numerator.total * 100.0 / denominator.total END AS decimal(18, 2)), + denominator.total + FROM (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_z]) numerator + CROSS JOIN (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_SaleOTRate_m]) denominator; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [DocDate]) AS dt, COUNT(DISTINCT [单据编号]) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_TransitTaskClass] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + ), reduced AS ( + SELECT CONVERT(date, [DocDate]) AS dt, COUNT(DISTINCT [单据号]) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_BizIncDec] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + ) + INSERT INTO #result + SELECT 'sales', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + ;WITH categories AS ( + SELECT + CASE WHEN [类别] IN (N'分类一', N'分类二', N'分类三', N'分类四') THEN [类别] ELSE N'其他' END AS category, + CASE [类别] WHEN N'分类一' THEN 1 WHEN N'分类二' THEN 2 WHEN N'分类三' THEN 3 WHEN N'分类四' THEN 4 ELSE 5 END AS sort + FROM [dbo].[MES_设计报工_任务] + WHERE ISNULL([单据状态], N'') <> N'已完成' + ) + INSERT INTO #result + SELECT 'research', 'bar', N'任务数', category, sort, COUNT_BIG(1), NULL + FROM categories + GROUP BY category, sort; + + INSERT INTO #result + SELECT 'research', 'summary', N'滞留', N'研发滞留', 1, COUNT_BIG(1), NULL + FROM [dbo].[MES_设计报工_任务] + WHERE ISNULL([单据状态], N'') <> N'已完成' + AND [预计结束时间] < GETDATE(); + + INSERT INTO #result + SELECT + 'research', 'rate', N'完成率', N'按期完成率', 1, + CAST(CASE WHEN COUNT_BIG(1) = 0 THEN 0 ELSE SUM(CASE WHEN [单据状态] = N'已完成' THEN 1 ELSE 0 END) * 100.0 / COUNT_BIG(1) END AS decimal(18, 2)), + COUNT_BIG(1) + FROM [dbo].[MES_设计报工_任务] + WHERE [预计结束时间] >= @monthStart AND [预计结束时间] < @tomorrow; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [创建日期]) AS dt, COUNT_BIG(1) AS total + FROM [dbo].[MES_设计报工_任务] + WHERE [创建日期] >= @start AND [创建日期] < @tomorrow + GROUP BY CONVERT(date, [创建日期]) + ), reduced AS ( + SELECT CONVERT(date, [最后编辑时间]) AS dt, COUNT_BIG(1) AS total + FROM [dbo].[MES_设计报工_任务] + WHERE [单据状态] = N'已完成' AND [最后编辑时间] >= @start AND [最后编辑时间] < @tomorrow + GROUP BY CONVERT(date, [最后编辑时间]) + ) + INSERT INTO #result + SELECT 'research', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + INSERT INTO #result + SELECT 'purchase', 'bar', N'任务数', N'申请', 1, COUNT_BIG(1), NULL + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_InTransitTask] + UNION ALL + SELECT 'purchase', 'bar', N'任务数', N'订单', 2, COUNT_BIG(1), NULL + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POAddDet]; + + INSERT INTO #result + SELECT + 'purchase', 'rate', N'及时率', N'外购及时率', 1, + CAST(CASE WHEN denominator.total = 0 THEN 0 ELSE numerator.total * 100.0 / denominator.total END AS decimal(18, 2)), + denominator.total + FROM (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate]) numerator + CROSS JOIN (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate_M]) denominator; + + INSERT INTO #result + SELECT + 'purchase', 'summary', N'滞留', N'采购滞留', 1, + CASE WHEN denominator.total > numerator.total THEN denominator.total - numerator.total ELSE 0 END, + NULL + FROM (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate]) numerator + CROSS JOIN (SELECT COUNT_BIG(1) AS total FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POOnTimeRate_M]) denominator; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT dt, SUM(total) AS total + FROM ( + SELECT CONVERT(date, [DocDate]) AS dt, COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_PRQAddList] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + UNION ALL + SELECT CONVERT(date, [DocDate]), COUNT_BIG(1) + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POAddDet] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + ) source + GROUP BY dt + ), reduced AS ( + SELECT dt, SUM(total) AS total + FROM ( + SELECT CONVERT(date, [DocDate]) AS dt, COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_PRQClearList] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + UNION ALL + SELECT CONVERT(date, [DocDate]), COUNT_BIG(1) + FROM [SAP].[SBO_YL].[dbo].[SAP_MES_POClearList] + WHERE [DocDate] >= @start AND [DocDate] < @tomorrow + GROUP BY CONVERT(date, [DocDate]) + ) source + GROUP BY dt + ) + INSERT INTO #result + SELECT 'purchase', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + INSERT INTO #result + SELECT 'machining', 'summary', N'任务', N'未完成任务', 0, COUNT(DISTINCT [TaskAID]), NULL + FROM [dbo].[View_生产订单_MES] + WHERE ISNULL([任务状态], 0) < 4 + AND [指派对象] <> N'外协' AND [指派对象] NOT LIKE N'%装配%'; + + INSERT INTO #result + SELECT 'machining', 'summary', N'加急', N'加急任务', 1, COUNT(DISTINCT [订单编号]), NULL + FROM [dbo].[View_生产订单_MES] + WHERE ISNULL([加急总数], 0) > 0 AND [指派对象] NOT LIKE N'%装配%'; + + INSERT INTO #result + SELECT 'machining', 'summary', N'滞留', N'未及时开工', 2, COUNT_BIG(1), NULL + FROM [dbo].[View_生产订单_MES] + WHERE [计划开始时间] < GETDATE() AND [开工时间] IS NULL + AND [排产状态] = 1 + AND [指派对象] <> N'外协' AND [指派对象] NOT LIKE N'%装配%'; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [派工时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[View_生产订单_MES] + WHERE [派工时间] >= @start AND [派工时间] < @tomorrow + AND [指派对象] <> N'外协' AND [指派对象] NOT LIKE N'%装配%' + GROUP BY CONVERT(date, [派工时间]) + ), reduced AS ( + SELECT CONVERT(date, [修改时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[View_生产订单_MES] + WHERE [修改时间] >= @start AND [修改时间] < @tomorrow + AND ISNULL([任务状态], 0) = 4 + AND [指派对象] <> N'外协' AND [指派对象] NOT LIKE N'%装配%' + GROUP BY CONVERT(date, [修改时间]) + ) + INSERT INTO #result + SELECT 'machining', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [派工时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[View_生产订单_MES] + WHERE [派工时间] >= @start AND [派工时间] < @tomorrow + AND [自制件属性] LIKE N'%装配%' + GROUP BY CONVERT(date, [派工时间]) + ), reduced AS ( + SELECT CONVERT(date, [修改时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[View_生产订单_MES] + WHERE [修改时间] >= @start AND [修改时间] < @tomorrow + AND [自制件属性] LIKE N'%装配%' + AND ISNULL([指派数量], 0) > 0 + AND ISNULL([完成数量], 0) >= ISNULL([指派数量], 0) + GROUP BY CONVERT(date, [修改时间]) + ) + INSERT INTO #result + SELECT 'assembly', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [开始时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[YL_加工中心_操作记录表] + WHERE [开始时间] >= @start AND [开始时间] < @tomorrow + AND [操作类别] IN (N'报工', N'辅助结束') + GROUP BY CONVERT(date, [开始时间]) + ), reduced AS ( + SELECT CONVERT(date, [检测时间]) AS dt, COUNT(DISTINCT [TaskAID]) AS total + FROM [dbo].[YL_质量检验_质检记录] + WHERE [检测时间] >= @start AND [检测时间] < @tomorrow + GROUP BY CONVERT(date, [检测时间]) + ) + INSERT INTO #result + SELECT 'quality', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + ;WITH dates AS ( + SELECT DATEADD(day, number, @start) AS dt, number + 1 AS sort + FROM (VALUES (0), (1), (2), (3), (4)) source(number) + ), added AS ( + SELECT CONVERT(date, [doc_date]) AS dt, COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[UBT_MES_DP_receipttask_material_2Days] + WHERE [doc_date] >= @start AND [doc_date] < @tomorrow + GROUP BY CONVERT(date, [doc_date]) + ), reduced AS ( + SELECT dt, SUM(total) AS total + FROM ( + SELECT TRY_CONVERT(date, [拣配日期]) AS dt, COUNT_BIG(1) AS total + FROM [SAP].[SBO_YL].[dbo].[UBT_MES_DP_OpenOPKL_material_101] + WHERE TRY_CONVERT(date, [拣配日期]) >= @start AND TRY_CONVERT(date, [拣配日期]) < @tomorrow + GROUP BY TRY_CONVERT(date, [拣配日期]) + UNION ALL + SELECT TRY_CONVERT(date, [拣配日期]), COUNT_BIG(1) + FROM [SAP].[SBO_YL].[dbo].[UBT_MES_DP_OpenOPKL_material_103] + WHERE TRY_CONVERT(date, [拣配日期]) >= @start AND TRY_CONVERT(date, [拣配日期]) < @tomorrow + GROUP BY TRY_CONVERT(date, [拣配日期]) + ) source + GROUP BY dt + ) + INSERT INTO #result + SELECT 'warehouse', 'trend', N'增减', CONVERT(char(10), dates.dt, 120), dates.sort, + ISNULL(added.total, 0), ISNULL(reduced.total, 0) + FROM dates + LEFT JOIN added ON added.dt = dates.dt + LEFT JOIN reduced ON reduced.dt = dates.dt; + + INSERT INTO #result + VALUES ('warehouse', 'rate', N'及时率', N'及时率', 1, 95, NULL); + + SELECT [module], [chart], [series], [label], [sort], [value], [extraValue] + FROM #result + ORDER BY + CASE [module] + WHEN 'sales' THEN 1 + WHEN 'research' THEN 2 + WHEN 'purchase' THEN 3 + WHEN 'machining' THEN 4 + WHEN 'assembly' THEN 5 + WHEN 'quality' THEN 6 + WHEN 'warehouse' THEN 7 + ELSE 99 + END, + [chart], [sort]; +END +GO diff --git a/生产任务看板/src/router/index.js b/生产任务看板/src/router/index.js index dd07ac7..cb5fb59 100644 --- a/生产任务看板/src/router/index.js +++ b/生产任务看板/src/router/index.js @@ -1,24 +1,28 @@ -import {createRouter, createWebHashHistory} from "vue-router"; +import { createRouter, createWebHashHistory } from "vue-router"; -// cookies - -// 创建routes const routes = [ { - // 重定向 - path: "/home", - redirect: "/", - }, - { - //前台首页 path: "/", name: "root", component: () => import("@/views/index.vue"), children: [], - } + }, + { + path: "/home", + redirect: "/", + }, + { + path: "/plan-kanban", + name: "planKanban", + component: () => import("@/views/plan-kanban/index.vue"), + }, + { + path: "/production-dashboard", + name: "productionDashboard", + redirect: "/", + }, ]; -//History路由模型 const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes, diff --git a/生产任务看板/src/views/plan-kanban/components/ChartPanel.vue b/生产任务看板/src/views/plan-kanban/components/ChartPanel.vue new file mode 100644 index 0000000..d5399e2 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/ChartPanel.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/components/DashboardHeader.vue b/生产任务看板/src/views/plan-kanban/components/DashboardHeader.vue new file mode 100644 index 0000000..eee83b0 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/DashboardHeader.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/components/DataTable.vue b/生产任务看板/src/views/plan-kanban/components/DataTable.vue new file mode 100644 index 0000000..036fa08 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/DataTable.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/components/MetricCard.vue b/生产任务看板/src/views/plan-kanban/components/MetricCard.vue new file mode 100644 index 0000000..de83b6c --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/components/MetricCard.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js b/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js new file mode 100644 index 0000000..84e5141 --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/composables/useDashboardData.js @@ -0,0 +1,753 @@ +import { inject, reactive } from 'vue' +import { PROCEDURES } from '../config/modules' + +const emptyModule = (status = 'loading') => ({ + status, + message: '', + cards: [], + summary: {} +}) + +const qualityParams = [ + ['@任务类型', '', 'nvarchar', false], + ['@生产订单', '', 'nvarchar', false], + ['@销售订单', '', 'nvarchar', false], + ['@物料名称', '', 'nvarchar', false], + ['@物料编码', '', 'nvarchar', false] +] + +const emptyRows = Object.freeze([]) + +const getRows = response => { + if (!response) { + return [] + } + if (Array.isArray(response.data)) { + return response.data + } + if (response.data && Array.isArray(response.data.result)) { + return response.data.result + } + if (response.data && Array.isArray(response.data.data)) { + return response.data.data + } + if (response.data && Array.isArray(response.data.Table)) { + return response.data.Table + } + return [] +} + +const firstRow = rows => (Array.isArray(rows) && rows.length > 0 ? rows[0] : {}) + +const toNumber = value => { + if (value === null || value === undefined || value === '') { + return 0 + } + const numberValue = Number(value) + return Number.isFinite(numberValue) ? numberValue : 0 +} + +const toRate = value => { + const numberValue = toNumber(value) + if (numberValue > 0 && numberValue <= 1) { + return Number((numberValue * 100).toFixed(2)) + } + return Number(numberValue.toFixed(2)) +} + +const formatPercent = value => { + const rate = toRate(value) + return rate ? `${rate}%` : '--' +} + +const formatNumber = value => { + const numberValue = toNumber(value) + if (!numberValue) { + return '0' + } + if (Number.isInteger(numberValue)) { + return `${numberValue}` + } + return `${Number(numberValue.toFixed(2))}` +} + +const formatDate = value => { + if (!value) { + return '' + } + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + return String(value).slice(0, 10) + } + const month = `${date.getMonth() + 1}`.padStart(2, '0') + const day = `${date.getDate()}`.padStart(2, '0') + return `${date.getFullYear()}-${month}-${day}` +} + +const toDateKey = value => { + if (!value) { + return '' + } + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + return String(value).slice(0, 10) + } + const month = `${date.getMonth() + 1}`.padStart(2, '0') + const day = `${date.getDate()}`.padStart(2, '0') + return `${date.getFullYear()}-${month}-${day}` +} + +const toShortDate = value => { + const dateKey = toDateKey(value) + if (!dateKey) { + return '' + } + return dateKey.slice(5) +} + +const hasValue = value => value !== null && value !== undefined && value !== '' + +const pickValue = (row, keys) => { + for (const key of keys) { + if (hasValue(row[key])) { + return row[key] + } + } + + const rowKeys = Object.keys(row || {}) + for (const key of keys) { + const hitKey = rowKeys.find(rowKey => rowKey === key || rowKey.includes(key)) + if (hitKey && hasValue(row[hitKey])) { + return row[hitKey] + } + } + + return '' +} + +const textValue = value => (hasValue(value) ? String(value).trim() : '') + +const sumRows = (rows, key) => rows.reduce((total, row) => total + toNumber(row[key]), 0) + +const recentDateBuckets = (days = 5) => { + const today = new Date() + today.setHours(0, 0, 0, 0) + + return Array.from({ length: days }, (_, index) => { + const date = new Date(today) + date.setDate(today.getDate() - (days - 1 - index)) + const dateKey = toDateKey(date) + return { + date: dateKey, + label: toShortDate(dateKey), + taskCount: 0, + workloadCount: 0 + } + }) +} + +const isSummaryName = value => { + const text = String(value || '') + return text.includes('汇总') || text.includes('合计') +} + +const createCard = (label, value, extra = {}) => ({ + label, + value, + ...extra +}) + +const normalizeUrgentRows = rows => rows.map(row => ({ + orderNo: textValue(pickValue(row, ['订单编号', '订单号', '生产订单'])), + contractNo: textValue(pickValue(row, ['合同号', '销售订单'])), + materialName: textValue(pickValue(row, ['物料名称', '物料描述', '零件名称', 'U_Name', 'ItemName'])), + materialCode: textValue(pickValue(row, ['物料编号', '物料编码', '零件编码', 'ItemCode'])), + urgentQty: formatNumber(pickValue(row, ['加急数量', '加急总数'])), + property: textValue(pickValue(row, ['自制件属性', '属性'])), + planFinish: formatDate(pickValue(row, ['计划完成', '计划完成时间'])) +})) + +const normalizeNotKittedRows = rows => rows.map(row => ({ + orderNo: textValue(pickValue(row, ['订单编号', '订单号', '生产订单'])), + contractNo: textValue(pickValue(row, ['合同号', '销售订单'])), + materialName: textValue(pickValue(row, ['物料名称', '物料描述', '零件名称', 'U_Name', 'ItemName'])), + materialCode: textValue(pickValue(row, ['物料编码', '物料编号', '零件编码', 'ItemCode'])), + planQty: formatNumber(pickValue(row, ['计划数量订单', '计划数量', '工序计划数量'])), + processName: textValue(pickValue(row, ['工序名称'])), + issue: textValue(pickValue(row, ['未完成说明', '发料状态', '拣配状态'])) +})) + +const normalizeLateStartRows = rows => rows.map(row => ({ + orderNo: textValue(pickValue(row, ['订单号', '订单编号', '生产订单'])), + contractNo: textValue(pickValue(row, ['合同号', '销售订单'])), + partName: textValue(pickValue(row, ['零件名称', '物料名称', '物料描述'])), + partCode: textValue(pickValue(row, ['零件编码', '物料编码', '物料编号'])), + processName: textValue(pickValue(row, ['工序名称'])), + assignee: textValue(pickValue(row, ['指派对象', '负责人', '操作人'])), + planStart: formatDate(pickValue(row, ['计划开始', '计划开始时间'])), + taskStatus: textValue(pickValue(row, ['任务状态'])) +})) + +const normalizeMachiningRows = rows => rows.map(row => ({ + orderNo: textValue(pickValue(row, ['订单号', '订单编号', '生产订单'])), + partName: textValue(pickValue(row, ['零件名称', '物料名称', '物料描述'])), + partCode: textValue(pickValue(row, ['零件编码', '物料编码', '物料编号'])), + assignee: textValue(pickValue(row, ['指派对象', '工位'])), + processName: textValue(pickValue(row, ['工序名称'])), + assignedQty: formatNumber(pickValue(row, ['分配数', '计划数量'])), + finishedQty: formatNumber(pickValue(row, ['完成数量', '合格数'])), + machineStatus: textValue(pickValue(row, ['加工状态', '任务状态'])) +})) + +const normalizeAssemblyRows = rows => rows.map(row => ({ + operator: textValue(pickValue(row, ['操作人', '负责人'])), + orderNo: textValue(pickValue(row, ['订单号', '订单编号', '生产订单'])), + contractNo: textValue(pickValue(row, ['合同号', '销售订单'])), + partName: textValue(pickValue(row, ['零件名称', '物料名称', '物料描述'])), + partCode: textValue(pickValue(row, ['零件编码', '物料编码', '物料编号'])), + assignedQty: formatNumber(pickValue(row, ['分配数', '计划数量'])), + finishedQty: formatNumber(pickValue(row, ['完成数量'])), + materialStatus: textValue(pickValue(row, ['发料状态', '拣配状态'])) +})) + +const normalizeWarehouseRows = rows => rows.map(row => ({ + category: textValue(pickValue(row, ['类别', '分类'])), + docNo: textValue(pickValue(row, ['单号', '生产订单', '合同号'])), + materialName: textValue(pickValue(row, ['物料名称', '物料描述', '零件名称', 'U_Name', 'ItemName'])), + qty: textValue(pickValue(row, ['加急数量', '数量', '状态', '加急状态'])) +})) + +const countUrgentRows = rows => { + const urgentOrders = new Set() + let fallbackCount = 0 + + rows.forEach((row, index) => { + const urgentQty = toNumber(pickValue(row, ['加急数量', '加急数'])) + const urgentText = textValue(pickValue(row, ['加急', '是否加急', '加急状态', '加急标识', '紧急程度'])) + const normalizedUrgentText = urgentText.toLowerCase() + const isUrgent = urgentQty > 0 || + ['是', 'y', 'yes', 'true', '1', '加急'].some(flag => normalizedUrgentText === flag) || + (urgentText.includes('加急') && !urgentText.includes('不加急')) + + if (!isUrgent) { + return + } + + const orderNo = textValue(pickValue(row, ['订单编号', '订单号', '生产订单'])) + if (orderNo) { + urgentOrders.add(orderNo) + } else { + fallbackCount += 1 + urgentOrders.add(`__row_${index}`) + } + }) + + return urgentOrders.size || fallbackCount +} + +const normalizePlanScheduleTrend = rows => { + const buckets = recentDateBuckets(5) + const bucketMap = new Map(buckets.map(item => [item.date, item])) + const taskOrderSets = new Map(buckets.map(item => [item.date, new Set()])) + const workloadOrderSets = new Map(buckets.map(item => [item.date, new Set()])) + + rows.forEach(row => { + const orderNo = textValue(pickValue(row, ['订单编号', '订单号', '生产订单'])) + const syncDate = toDateKey(pickValue(row, ['同步时间', '同步日期'])) + const modifyDate = toDateKey(pickValue(row, ['修改时间', '修改日期'])) + const syncBucket = bucketMap.get(syncDate) + const modifyBucket = bucketMap.get(modifyDate) + + if (orderNo && syncBucket) { + taskOrderSets.get(syncDate).add(orderNo) + } + if (orderNo && modifyBucket) { + workloadOrderSets.get(modifyDate).add(orderNo) + } + + const date = toDateKey(pickValue(row, ['日期', '统计日期', '时间', 'date'])) + const bucket = bucketMap.get(date) + if (!bucket) { + return + } + + const taskCount = pickValue(row, ['taskCount', '任务量', '计划任务量', '同步订单数', '同步订单个数', '计划订单数']) + const workloadCount = pickValue(row, ['workloadCount', '工作量', '生产任务量', '修改订单数', '修改订单个数', '工作订单数', '订单编号数']) + const metricName = textValue(pickValue(row, ['指标', '类型', '名称'])) + const metricValue = pickValue(row, ['数量', '个数', '订单数', '订单个数', '值']) + + if (metricName.includes('工作') || metricName.includes('生产任务') || metricName.includes('修改')) { + if (hasValue(workloadCount)) { + bucket.workloadCount = toNumber(workloadCount) + } else if (hasValue(metricValue)) { + bucket.workloadCount = toNumber(metricValue) + } + } else if (metricName.includes('任务') || metricName.includes('计划') || metricName.includes('同步')) { + if (hasValue(taskCount)) { + bucket.taskCount = toNumber(taskCount) + } else if (hasValue(metricValue)) { + bucket.taskCount = toNumber(metricValue) + } + } else { + if (hasValue(taskCount)) { + bucket.taskCount = toNumber(taskCount) + } + if (hasValue(workloadCount)) { + bucket.workloadCount = toNumber(workloadCount) + } + } + }) + + buckets.forEach(bucket => { + const taskOrderCount = taskOrderSets.get(bucket.date).size + const workloadOrderCount = workloadOrderSets.get(bucket.date).size + if (taskOrderCount) { + bucket.taskCount = taskOrderCount + } + if (workloadOrderCount) { + bucket.workloadCount = workloadOrderCount + } + }) + + return buckets +} + +const normalizeOverviewModule = (rows, moduleKey) => { + const moduleRows = rows.filter(row => row.module === moduleKey) + const barItems = moduleRows + .filter(row => row.chart === 'bar') + .sort((left, right) => toNumber(left.sort) - toNumber(right.sort)) + .map(row => ({ + name: textValue(row.label), + value: toNumber(row.value) + })) + const trend = moduleRows + .filter(row => row.chart === 'trend') + .sort((left, right) => toNumber(left.sort) - toNumber(right.sort)) + .map(row => ({ + date: textValue(row.label), + label: toShortDate(row.label), + taskCount: toNumber(row.value), + workloadCount: toNumber(row.extraValue) + })) + const rateRow = moduleRows.find(row => row.chart === 'rate') || {} + const summary = moduleRows + .filter(row => row.chart === 'summary') + .reduce((result, row) => { + result[textValue(row.series)] = toNumber(row.value) + return result + }, {}) + + return { + barItems, + trend, + trendLoaded: trend.length > 0, + rate: toRate(rateRow.value), + rateTotal: toNumber(rateRow.extraValue), + overviewSummary: summary + } +} + +const mergeCards = (moduleState, cards, summary = {}) => { + moduleState.status = 'ready' + moduleState.message = '' + moduleState.cards = cards + moduleState.summary = summary +} + +const markModule = (moduleState, status, message = '') => { + moduleState.status = status + moduleState.message = message +} + +export function useDashboardData() { + const CreateData = inject('CreateData', null) + const ExecDatabase = inject('ExecDatabase', null) + + const state = reactive({ + loading: false, + refreshing: false, + lastUpdated: '', + errors: [], + modules: { + sales: { + ...emptyModule('empty'), + cards: [ + createCard('准交率', '--'), + createCard('今日新增', '--'), + createCard('售中', '--') + ], + message: '暂无数据' + }, + research: { + ...emptyModule('empty'), + cards: [ + createCard('执行率', '--'), + createCard('立项数', '--'), + createCard('研发任务', '--') + ], + message: '暂无数据' + }, + plan: emptyModule(), + purchase: { + ...emptyModule('pending'), + cards: [ + createCard('准供率', '--'), + createCard('外购', '--'), + createCard('外协', '--'), + createCard('采购执行', '--') + ], + message: 'SAP 采购指标待接口接入' + }, + machining: emptyModule(), + assembly: emptyModule(), + panorama: emptyModule(), + quality: emptyModule(), + warehouse: emptyModule() + }, + tables: { + urgent: [], + notKitted: [], + lateStart: [], + machiningWip: [], + assemblyWip: [], + warehouse: [] + } + }) + + const queryProcedure = async (name, params = []) => { + if (!CreateData || !ExecDatabase) { + throw new Error('公共数据请求方法未注入') + } + const payload = CreateData('11', name, params) + const response = await ExecDatabase(payload) + return getRows(response) + } + + const safeQuery = async (name, params = []) => { + try { + return await queryProcedure(name, params) + } catch (error) { + state.errors.push({ + procedure: name, + message: error?.message || '请求失败' + }) + return emptyRows + } + } + + const applyDepartmentOverview = rows => { + if (!rows.length) { + ;['sales', 'research', 'purchase'].forEach(key => { + markModule(state.modules[key], 'empty', '暂无数据') + }) + return + } + + const sales = normalizeOverviewModule(rows, 'sales') + const research = normalizeOverviewModule(rows, 'research') + const purchase = normalizeOverviewModule(rows, 'purchase') + + mergeCards(state.modules.sales, [ + createCard('准交率', formatPercent(sales.rate), { valueType: 'rate' }), + createCard('在途订单', formatNumber(sales.barItems.reduce((total, item) => total + item.value, 0))), + createCard('销售滞留', formatNumber(sales.overviewSummary['滞留'])) + ], sales) + + mergeCards(state.modules.research, [ + createCard('按期完成率', formatPercent(research.rate), { valueType: 'rate' }), + createCard('未完成任务', formatNumber(research.barItems.reduce((total, item) => total + item.value, 0))), + createCard('研发滞留', formatNumber(research.overviewSummary['滞留'])) + ], research) + + mergeCards(state.modules.purchase, [ + createCard('外购及时率', formatPercent(purchase.rate), { valueType: 'rate' }), + createCard('采购任务', formatNumber(purchase.barItems.reduce((total, item) => total + item.value, 0))), + createCard('采购滞留', formatNumber(purchase.overviewSummary['滞留'])) + ], purchase) + + ;['machining', 'assembly', 'quality', 'warehouse'].forEach(key => { + Object.assign(state.modules[key].summary, normalizeOverviewModule(rows, key)) + }) + } + + const fetchPlan = async () => { + const [rateRows, noProcessRows, scheduleTrendRows, urgentRows, lateCountRows, lateRows, notKittedRows] = await Promise.all([ + safeQuery(PROCEDURES.planRate), + safeQuery(PROCEDURES.planNoProcess), + safeQuery(PROCEDURES.planScheduleTrend), + safeQuery(PROCEDURES.urgentTasks), + safeQuery(PROCEDURES.lateStartCount), + safeQuery(PROCEDURES.lateStartRows), + safeQuery(PROCEDURES.notKittedRows) + ]) + + const rate = firstRow(rateRows) + const noProcess = firstRow(noProcessRows) + const lateCount = firstRow(lateCountRows) + const totalTasks = toNumber(rate['总任务数']) + const kittedTasks = toNumber(rate['齐套任务数']) + const unKittedTasks = toNumber(rate['未齐套任务数']) + const kitRate = toRate(rate['齐套率']) + const urgentTotal = toNumber(firstRow(urgentRows)['加急订单总数']) || urgentRows.length + const urgentSelf = urgentRows.filter(row => String(row['自制件属性'] || '').includes('自制')).length + const urgentPurchase = urgentRows.filter(row => String(row['自制件属性'] || '').includes('外购')).length + const lateStartTotal = toNumber(lateCount['未及时开工总数']) || sumRows(lateRows, '未及时开工总数') + const pendingSchedule = toNumber(noProcess['未排产任务数']) + const assemblyPending = toNumber(noProcess['装配未排产数']) + const machiningPending = Math.max(pendingSchedule - assemblyPending, 0) + const pendingUrgent = toNumber(pickValue(noProcess, [ + '未排产加急数', + '未排产加急任务数', + '未排产加急订单数', + '未排产加急件数', + '加急未排产数', + '加急未排产任务数', + '加急数', + '加急数量', + '加急任务数', + '加急订单数' + ])) || countUrgentRows(noProcessRows) + const scheduleTrend = normalizePlanScheduleTrend(scheduleTrendRows) + const scheduleTrendLoaded = scheduleTrendRows.length > 0 + + state.tables.urgent = normalizeUrgentRows(urgentRows) + state.tables.notKitted = normalizeNotKittedRows(notKittedRows) + state.tables.lateStart = normalizeLateStartRows(lateRows) + + mergeCards(state.modules.plan, [ + createCard('按期齐套率', formatPercent(kitRate), { valueType: 'rate' }), + createCard('未齐套', formatNumber(unKittedTasks), { subValue: `应齐套 ${formatNumber(totalTasks)}` }), + createCard('待排产', formatNumber(noProcess['未排产任务数'])), + createCard('无生产工艺', formatNumber(noProcess['无生产工艺计划数'])), + createCard('加急件', formatNumber(urgentTotal), { subValue: `自制 ${urgentSelf} / 外购 ${urgentPurchase}` }), + createCard('未及时开工', formatNumber(lateStartTotal)) + ], { + kitRate, + totalTasks, + kittedTasks, + unKittedTasks, + feedKitted: toNumber(rate['发料齐套任务数']), + feedMissing: toNumber(rate['发料缺件任务数']), + notIssued: toNumber(rate['未发料任务数']), + pendingSchedule, + machiningPending, + assemblyPending, + pendingUrgent, + noProcessPlan: toNumber(noProcess['无生产工艺计划数']), + urgentTotal, + urgentSelf, + urgentPurchase, + lateStartTotal, + scheduleTrend, + scheduleTrendLoaded + }) + } + + const fetchMachining = async () => { + const [rateRows, deviceRows, wipRows] = await Promise.all([ + safeQuery(PROCEDURES.machiningRate), + safeQuery(PROCEDURES.machiningDevice), + safeQuery(PROCEDURES.machiningWip) + ]) + const rate = firstRow(rateRows) + const device = firstRow(deviceRows) + const executeRate = toRate(rate['计划执行率_百分数']) + const shouldStart = toNumber(rate['应开工任务数']) + const onTimeStart = toNumber(rate['按期开始任务数']) + const wipTotal = toNumber(firstRow(wipRows)['总数']) || wipRows.length + + state.tables.machiningWip = normalizeMachiningRows(wipRows) + + mergeCards(state.modules.machining, [ + createCard('执行率', formatPercent(executeRate), { valueType: 'rate' }), + createCard('及时开工', formatNumber(onTimeStart), { subValue: `应开工 ${formatNumber(shouldStart)}` }), + createCard('在制任务', formatNumber(wipTotal)), + createCard('设备开动', formatNumber(device['运行数量']), { subValue: `设备总数 ${formatNumber(device['总数'])}` }) + ], { + executeRate, + shouldStart, + onTimeStart, + wipTotal, + device: { + off: toNumber(device['关机数量']), + standby: toNumber(device['待机数量']), + running: toNumber(device['运行数量']), + alarm: toNumber(device['报警数量']), + total: toNumber(device['总数']) + } + }) + } + + const fetchAssembly = async () => { + const [rateRows, executableRows, wipRows, abnormalRows] = await Promise.all([ + safeQuery(PROCEDURES.assemblyRate), + safeQuery(PROCEDURES.assemblyExecutable), + safeQuery(PROCEDURES.assemblyWip), + safeQuery(PROCEDURES.assemblyAbnormal) + ]) + const rate = firstRow(rateRows) + const executable = firstRow(executableRows) + const abnormal = firstRow(abnormalRows) + const executableTotal = toNumber(executable['阀类可执行任务数']) + toNumber(executable['系统类可执行任务数']) + const wipTotal = wipRows.length + + state.tables.assemblyWip = normalizeAssemblyRows(wipRows) + + mergeCards(state.modules.assembly, [ + createCard('装配合格率', formatPercent(rate['总合格率']), { valueType: 'rate' }), + createCard('可执行任务', formatNumber(executableTotal), { + subValue: `阀类 ${formatNumber(executable['阀类可执行任务数'])} / 系统 ${formatNumber(executable['系统类可执行任务数'])}` + }), + createCard('及时送检', formatNumber(executable['及时送检数'])), + createCard('在制任务', formatNumber(wipTotal)), + createCard('异常未关闭', formatNumber(abnormal['装配异常未关闭总数'])) + ], { + qualityRate: toRate(rate['总合格率']), + inspectionQty: toNumber(rate['总检验数量']), + passQty: toNumber(rate['总合格数']), + badQty: toNumber(rate['总不合格数']), + executableTotal, + valveExecutable: toNumber(executable['阀类可执行任务数']), + systemExecutable: toNumber(executable['系统类可执行任务数']), + electricTasks: toNumber(executable['系统类电气任务数']), + mechanicalTasks: toNumber(executable['系统类机械任务数']), + timelyInspection: toNumber(executable['及时送检数']), + wipTotal, + abnormalTotal: toNumber(abnormal['装配异常未关闭总数']) + }) + } + + const fetchQuality = async () => { + const [timelyRows, unfinishedRows, testTaskRows] = await Promise.all([ + safeQuery(PROCEDURES.qualityTimely, qualityParams), + safeQuery(PROCEDURES.qualityUnfinished), + safeQuery(PROCEDURES.qualityTestTasks, qualityParams) + ]) + const timely = firstRow(timelyRows) + const detailRows = unfinishedRows.filter(row => !isSummaryName(row['质检类型'])) + const unfinishedTotal = sumRows(detailRows, '未完成数量') + const urgentTotal = sumRows(detailRows, '加急数量') + + mergeCards(state.modules.quality, [ + createCard('质检及时率', formatPercent(timely['质检及时率']), { valueType: 'rate' }), + createCard('报检任务数', formatNumber(timely['报检任务数'])), + createCard('超期未检验', formatNumber(timely['超期未检验数'])), + createCard('未完成任务', formatNumber(unfinishedTotal), { subValue: `加急 ${formatNumber(urgentTotal)}` }) + ], { + timelyRate: toRate(timely['质检及时率']), + inspectionTasks: toNumber(timely['报检任务数']), + overdueTasks: toNumber(timely['超期未检验数']), + unfinishedTotal, + urgentTotal, + systemTasks: testTaskRows.filter(row => textValue(row['任务类型']).includes('系统')).length, + componentTasks: testTaskRows.filter(row => !textValue(row['任务类型']).includes('系统')).length, + unfinishedTypes: detailRows.map(row => ({ + name: row['质检类型'], + value: toNumber(row['未完成数量']), + urgent: toNumber(row['加急数量']) + })) + }) + } + + const fetchWarehouse = async () => { + const [summaryRows, urgentInboundRows, urgentPickingRows, pickingRows] = await Promise.all([ + safeQuery(PROCEDURES.warehouseSummary), + safeQuery(PROCEDURES.warehouseUrgentInbound), + safeQuery(PROCEDURES.warehouseUrgentPicking), + safeQuery(PROCEDURES.warehousePicking) + ]) + const totalTasks = sumRows(summaryRows, '任务数') + const totalQty = sumRows(summaryRows, '数量') + const urgentQty = sumRows(summaryRows, '加急数量') + const byStage = summaryRows.map(row => ({ + name: `${row['环节'] || ''}${row['分类'] ? `-${row['分类']}` : ''}`, + task: toNumber(row['任务数']), + qty: toNumber(row['数量']), + urgent: toNumber(row['加急数量']) + })) + + state.tables.warehouse = [ + ...normalizeWarehouseRows(urgentInboundRows), + ...normalizeWarehouseRows(urgentPickingRows), + ...normalizeWarehouseRows(pickingRows) + ] + + mergeCards(state.modules.warehouse, [ + createCard('仓储任务', formatNumber(totalTasks)), + createCard('任务数量', formatNumber(totalQty)), + createCard('加急数量', formatNumber(urgentQty)), + createCard('拣配明细', formatNumber(pickingRows.length)) + ], { + totalTasks, + totalQty, + urgentQty, + byStage, + urgentInboundCount: urgentInboundRows.length, + urgentPickingCount: urgentPickingRows.length, + pickingCount: pickingRows.length + }) + } + + const updatePanorama = () => { + const machining = state.modules.machining.summary + const assembly = state.modules.assembly.summary + const quality = state.modules.quality.summary + const warehouse = state.modules.warehouse.summary + mergeCards(state.modules.panorama, [ + createCard('机加在制', formatNumber(machining.wipTotal)), + createCard('装配在制', formatNumber(assembly.wipTotal)), + createCard('装配可执行', formatNumber(assembly.executableTotal)), + createCard('测试/质检', formatNumber(quality.unfinishedTotal)), + createCard('仓储流转', formatNumber(warehouse.totalTasks)) + ], { + stations: [ + { name: '机加在制', value: toNumber(machining.wipTotal), status: '加工中' }, + { name: '装配在制', value: toNumber(assembly.wipTotal), status: '加工中' }, + { name: '装配可执行', value: toNumber(assembly.executableTotal), status: '待执行' }, + { name: '测试/质检', value: toNumber(quality.unfinishedTotal), status: '检验' }, + { name: '仓储流转', value: toNumber(warehouse.totalTasks), status: '流转' } + ] + }) + } + + const loadDashboardData = async (options = {}) => { + const silent = options.silent === true + state.errors = [] + state.loading = !silent + state.refreshing = silent + + ;['sales', 'research', 'plan', 'purchase', 'machining', 'assembly', 'quality', 'warehouse'].forEach(key => { + markModule(state.modules[key], 'loading') + }) + markModule(state.modules.panorama, 'loading') + + try { + const overviewPromise = safeQuery(PROCEDURES.departmentOverview) + await Promise.all([ + fetchPlan(), + fetchMachining(), + fetchAssembly(), + fetchQuality(), + fetchWarehouse() + ]) + applyDepartmentOverview(await overviewPromise) + updatePanorama() + state.lastUpdated = new Date().toLocaleString('zh-CN', { hour12: false }) + } finally { + state.loading = false + state.refreshing = false + ;['sales', 'research', 'plan', 'purchase', 'machining', 'assembly', 'quality', 'warehouse', 'panorama'].forEach(key => { + const moduleState = state.modules[key] + if (moduleState.status === 'loading') { + markModule(moduleState, 'error', '数据加载失败') + } + }) + } + } + + return { + state, + loadDashboardData + } +} diff --git a/生产任务看板/src/views/plan-kanban/composables/useEchart.js b/生产任务看板/src/views/plan-kanban/composables/useEchart.js new file mode 100644 index 0000000..a90c98a --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/composables/useEchart.js @@ -0,0 +1,45 @@ +import * as echarts from 'echarts' +import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue' + +export function useEchart(optionRef) { + const elRef = ref(null) + let chart = null + + const render = async () => { + await nextTick() + if (!elRef.value) { + return + } + if (!chart) { + chart = echarts.init(elRef.value) + } + chart.setOption(optionRef.value || {}, true) + } + + const resize = () => { + if (chart) { + chart.resize() + } + } + + onMounted(() => { + render() + window.addEventListener('resize', resize) + }) + + watch(optionRef, render, { deep: true }) + + onUnmounted(() => { + window.removeEventListener('resize', resize) + if (chart) { + chart.dispose() + chart = null + } + }) + + return { + elRef, + resize, + render + } +} diff --git a/生产任务看板/src/views/plan-kanban/config/modules.js b/生产任务看板/src/views/plan-kanban/config/modules.js new file mode 100644 index 0000000..73888fc --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/config/modules.js @@ -0,0 +1,148 @@ +export const BLUE_COLORS = { + background: '#07001f', + panel: '#071f48', + panelDeep: '#061338', + border: '#2bd8ff', + title: '#8cd3ff', + text: '#eaf7ff', + muted: '#78a9ce', + cyan: '#1ce6ff', + blue: '#168dff', + lightBlue: '#6fd8ff', + deepBlue: '#0b4fba', + warning: '#ffb84d', + danger: '#ff5c7a' +} + +export const MODULES = [ + { + key: 'sales', + title: '销售', + subtitle: '准交率 / 今日新增 / 售中订单', + emptyText: '暂无数据' + }, + { + key: 'research', + title: '研发', + subtitle: '执行率 / 立项 / 研发任务', + emptyText: '暂无数据' + }, + { + key: 'plan', + title: '计划', + subtitle: '按期齐套 / 生产计划 / 加急件' + }, + { + key: 'purchase', + title: '采购', + subtitle: '准供率 / 外购 / 外协', + emptyText: '待接口接入' + }, + { + key: 'machining', + title: '机加车间', + subtitle: '执行率 / 开工 / 在制 / 设备' + }, + { + key: 'assembly', + title: '装配车间', + subtitle: '合格率 / 可执行 / 在制' + }, + { + key: 'panorama', + title: '车间全景', + subtitle: '机加 / 机械 / 电气 / 测试在产' + }, + { + key: 'quality', + title: '质量', + subtitle: '质检及时率 / 未完成任务' + }, + { + key: 'warehouse', + title: '仓储', + subtitle: '入库 / 拣配 / 发货' + } +] + +export const PROCEDURES = { + departmentOverview: 'MES_Kanban_DepartmentOverview', + planRate: '生产任务综合看板_按期齐套率', + planNoProcess: '生产任务综合看板_未排产无工艺查询', + planScheduleTrend: 'MES_Kanban_PlanScheduleTrend', + urgentTasks: '生产任务综合看板_加急任务查询', + lateStartCount: '生产任务综合看板_未及时开工查询_Count', + lateStartRows: '生产任务综合看板_未及时开工查询', + notKittedRows: '生产任务综合看板_未按期齐套任务', + machiningRate: '机加设备生产运营看板_计划执行率', + machiningDevice: '机加设备生产运营看板_设备运行状态查询', + machiningWip: '机加设备生产运营看板_机加在制查询', + assemblyRate: '装配中心任务看板_装配合格率', + assemblyExecutable: '装配中心任务看板_可执行任务数', + assemblyWip: '装配中心任务看板_装配在制任务查询', + assemblyAbnormal: '装配中心任务看板_异常未关闭总数', + qualityTimely: '质量管理_测试装配任务_质检及时率', + qualityUnfinished: '质量任务看板_未完成统计', + qualityTestTasks: '质检测试看板_测试装配任务_查询', + warehouseSummary: '仓库大屏_汇总查询', + warehouseUrgentInbound: '仓库大屏_加急入库列表', + warehouseUrgentPicking: '仓库大屏_加急拣配列表', + warehousePicking: '仓库大屏_拣配任务列表' +} + +export const TABLE_COLUMNS = { + urgent: [ + { key: 'orderNo', label: '生产订单', width: '12%' }, + { key: 'contractNo', label: '销售订单', width: '13%' }, + { key: 'materialName', label: '物料名称', width: '22%' }, + { key: 'materialCode', label: '物料编码', width: '22%' }, + { key: 'urgentQty', label: '加急数量', width: '10%' }, + { key: 'property', label: '属性', width: '8%' }, + { key: 'planFinish', label: '计划完成', width: '13%' } + ], + notKitted: [ + { key: 'orderNo', label: '生产订单', width: '12%' }, + { key: 'contractNo', label: '销售订单', width: '13%' }, + { key: 'materialName', label: '物料名称', width: '20%' }, + { key: 'materialCode', label: '物料编码', width: '21%' }, + { key: 'planQty', label: '计划数量', width: '10%' }, + { key: 'processName', label: '工序', width: '10%' }, + { key: 'issue', label: '未完成说明', width: '14%' } + ], + lateStart: [ + { key: 'orderNo', label: '生产订单', width: '11%' }, + { key: 'contractNo', label: '销售订单', width: '12%' }, + { key: 'partName', label: '零件名称', width: '18%' }, + { key: 'partCode', label: '零件编码', width: '20%' }, + { key: 'processName', label: '工序', width: '10%' }, + { key: 'assignee', label: '负责人', width: '10%' }, + { key: 'planStart', label: '计划开始', width: '11%' }, + { key: 'taskStatus', label: '任务状态', width: '8%' } + ], + machiningWip: [ + { key: 'orderNo', label: '生产订单', width: '11%' }, + { key: 'partName', label: '零件名称', width: '19%' }, + { key: 'partCode', label: '零件编码', width: '21%' }, + { key: 'assignee', label: '工位', width: '9%' }, + { key: 'processName', label: '工序', width: '10%' }, + { key: 'assignedQty', label: '分配数', width: '9%' }, + { key: 'finishedQty', label: '完成数', width: '9%' }, + { key: 'machineStatus', label: '加工状态', width: '12%' } + ], + assemblyWip: [ + { key: 'operator', label: '负责人', width: '10%' }, + { key: 'orderNo', label: '生产订单', width: '11%' }, + { key: 'contractNo', label: '销售订单', width: '12%' }, + { key: 'partName', label: '零件名称', width: '18%' }, + { key: 'partCode', label: '零件编码', width: '21%' }, + { key: 'assignedQty', label: '分配数', width: '9%' }, + { key: 'finishedQty', label: '完成数', width: '9%' }, + { key: 'materialStatus', label: '发料状态', width: '10%' } + ], + warehouse: [ + { key: 'category', label: '类别', width: '12%' }, + { key: 'docNo', label: '单号', width: '24%' }, + { key: 'materialName', label: '物料名称', width: '44%' }, + { key: 'qty', label: '数量/状态', width: '20%' } + ] +} diff --git a/生产任务看板/src/views/plan-kanban/index.vue b/生产任务看板/src/views/plan-kanban/index.vue new file mode 100644 index 0000000..9754bfd --- /dev/null +++ b/生产任务看板/src/views/plan-kanban/index.vue @@ -0,0 +1,1368 @@ + + + + +