feat: update production kanban details

This commit is contained in:
meswork764
2026-07-20 18:47:17 +08:00
parent b5bc8d7358
commit 77d1a97e83
11 changed files with 2336 additions and 63 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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}
]
}
}

View File

@@ -0,0 +1,205 @@
<template>
<section class="center-detail-panel panel">
<header class="detail-panel-head">
<h2>{{ title }}</h2>
<span v-if="status === 'ready'" class="detail-count">{{ rows.length }} </span>
</header>
<div class="detail-table-head detail-grid-row" :style="gridStyle">
<span v-for="column in columns" :key="column.key">{{ column.label }}</span>
</div>
<div class="detail-table-body">
<div
v-if="status === 'ready'"
class="detail-table-track"
:class="{ scrolling: shouldScroll }"
:style="scrollStyle"
>
<div
v-for="(row, index) in displayRows"
:key="`${row.id}-${index}`"
class="detail-grid-row detail-data-row"
:class="{ urgent: row.urgent }"
:style="gridStyle"
:title="row.title"
>
<span
v-for="column in columns"
:key="column.key"
:class="[`cell-${column.key}`, { 'category-cell': column.key === 'category' }]"
>
{{ row[column.key] || '--' }}
</span>
</div>
</div>
<div v-else class="detail-panel-state">
<span v-if="status === 'loading'" class="detail-loading-icon"></span>
{{ status === 'loading' ? '加载中' : message }}
</div>
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: { type: String, required: true },
columns: { type: Array, required: true },
rows: { type: Array, default: () => [] },
status: { type: String, default: 'loading' },
message: { type: String, default: '' },
visibleRows: { type: Number, default: 9 }
})
const rowHeight = 34
const shouldScroll = computed(() => props.rows.length > props.visibleRows)
const displayRows = computed(() => shouldScroll.value ? [...props.rows, ...props.rows] : props.rows)
const gridStyle = computed(() => ({
gridTemplateColumns: props.columns.map(column => column.width || '1fr').join(' ')
}))
const scrollStyle = computed(() => ({
'--scroll-distance': `${props.rows.length * rowHeight}px`,
'--scroll-duration': `${Math.max(props.rows.length * 1.8, 20)}s`
}))
</script>
<style scoped lang="scss">
.center-detail-panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
color: #dcecff;
background: rgba(7, 31, 72, 0.72);
}
.detail-panel-head {
display: flex;
flex: 0 0 36px;
align-items: center;
justify-content: space-between;
min-width: 0;
padding: 0 10px;
border-bottom: 1px solid rgba(43, 216, 255, 0.28);
}
.detail-panel-head h2 {
margin: 0;
color: #8cd3ff;
font-size: 14px;
font-weight: 900;
letter-spacing: 0;
white-space: nowrap;
}
.detail-count {
color: #1ce6ff;
font-size: 10px;
}
.detail-grid-row {
display: grid;
align-items: center;
min-width: 0;
text-align: center;
}
.detail-table-head {
flex: 0 0 30px;
color: #bfe8ff;
font-size: 9px;
font-weight: 800;
background: rgba(10, 75, 145, 0.76);
}
.detail-table-head span,
.detail-data-row > span {
min-width: 0;
padding: 0 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-table-body {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
}
.detail-table-track.scrolling {
animation: detail-table-scroll var(--scroll-duration) linear infinite;
}
.detail-table-track.scrolling:hover {
animation-play-state: paused;
}
.detail-data-row {
height: 34px;
color: #dcecff;
font-size: 9px;
border-bottom: 1px solid rgba(43, 216, 255, 0.1);
}
.detail-data-row:nth-child(odd) {
background: rgba(4, 25, 59, 0.38);
}
.detail-data-row:nth-child(even) {
background: rgba(4, 18, 47, 0.24);
}
.detail-data-row.urgent,
.detail-data-row.urgent .category-cell {
color: #ff637a;
background: rgba(116, 19, 37, 0.22);
}
.category-cell {
color: #1ce6ff;
font-weight: 800;
}
.cell-materialName {
display: -webkit-box;
overflow-wrap: anywhere;
line-height: 12px;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.detail-panel-state {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
color: #78a9ce;
font-size: 12px;
}
.detail-loading-icon {
width: 14px;
height: 14px;
border: 2px solid rgba(43, 216, 255, 0.25);
border-top-color: #2bd8ff;
border-radius: 50%;
animation: detail-loading 0.8s linear infinite;
}
@keyframes detail-table-scroll {
from { transform: translateY(0); }
to { transform: translateY(calc(-1 * var(--scroll-distance))); }
}
@keyframes detail-loading {
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<section class="device-status-panel panel">
<header class="device-panel-head">
<h2>设备运行状态明细</h2>
<div class="device-legend">
<span><i class="status-dot status-run"></i>运行</span>
<span><i class="status-dot status-off"></i>停机</span>
</div>
</header>
<div class="device-table-head device-grid-row">
<span>设备</span>
<span>状态</span>
<span>自动化</span>
<span>辅机</span>
<span>利用率</span>
<span>开动率</span>
<span>合格率</span>
<span>OEE</span>
</div>
<div class="device-table-body">
<div
v-if="status === 'ready'"
class="device-table-track"
:class="{ scrolling: shouldScroll }"
:style="scrollStyle"
>
<div
v-for="(row, index) in displayRows"
:key="`${row.id}-${index}`"
class="device-grid-row device-data-row"
:title="getRowTitle(row)"
>
<span class="device-name" :title="`${row.name} / ${row.stations}`">{{ row.name }}</span>
<span class="device-state"><i class="status-dot" :class="`status-${row.statusTone}`"></i>{{ row.status }}</span>
<span><i class="status-dot status-run"></i></span>
<span><i class="status-dot status-run"></i></span>
<strong :class="rateClass(row.utilization)">{{ formatRate(row.utilization) }}</strong>
<strong>{{ formatRate(row.availability) }}</strong>
<strong :class="rateClass(row.qualityRate)">{{ formatFixedRate(row.qualityRate) }}</strong>
<strong :class="rateClass(row.oee)">{{ formatRate(row.oee) }}</strong>
</div>
</div>
<div v-else class="device-panel-state">
<span v-if="status === 'loading'" class="device-loading-icon"></span>
{{ status === 'loading' ? '加载中' : message }}
</div>
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
rows: {
type: Array,
default: () => []
},
status: {
type: String,
default: 'loading'
},
message: {
type: String,
default: ''
},
visibleRows: {
type: Number,
default: 8
},
rowHeight: {
type: Number,
default: 28
}
})
const shouldScroll = computed(() => props.rows.length > props.visibleRows)
const displayRows = computed(() => shouldScroll.value ? [...props.rows, ...props.rows] : props.rows)
const scrollStyle = computed(() => ({
'--row-height': `${props.rowHeight}px`,
'--scroll-distance': `${props.rows.length * props.rowHeight}px`,
'--scroll-duration': `${Math.max(props.rows.length * 1.6, 18)}s`
}))
const formatRate = value => `${Number(value || 0).toFixed(2).replace(/\.00$/, '')}%`
const formatFixedRate = value => `${Number(value || 0).toFixed(2)}%`
const rateClass = value => ({
'rate-good': Number(value || 0) >= 85,
'rate-warning': Number(value || 0) >= 60 && Number(value || 0) < 85,
'rate-danger': Number(value || 0) < 60
})
const getRowTitle = row => [
`工位: ${row.stations || '--'}`,
`本月加工工时: ${Number(row.workHours || 0).toFixed(2)}h`,
`报工天数: ${row.reportDays}`,
`设备标准开机工时: ${Number(row.standardOpenHours || 0).toFixed(2)}h/天`,
`质检不良/计划: ${row.defectiveQty}/${row.qualityPlannedQty}`
].join(' | ')
</script>
<style scoped lang="scss">
.device-status-panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
color: #dcecff;
background: rgba(7, 31, 72, 0.62);
}
.device-panel-head {
display: flex;
flex: 0 0 36px;
align-items: center;
justify-content: space-between;
min-width: 0;
padding: 0 10px;
border-bottom: 1px solid rgba(43, 216, 255, 0.28);
}
.device-panel-head h2 {
margin: 0;
color: #8cd3ff;
font-size: 14px;
font-weight: 900;
letter-spacing: 0;
white-space: nowrap;
}
.device-legend {
display: flex;
gap: 8px;
color: #91b6d3;
font-size: 10px;
white-space: nowrap;
}
.device-legend span,
.device-state {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.device-grid-row {
display: grid;
grid-template-columns: 1.15fr 0.8fr 0.72fr 0.62fr 0.9fr 0.9fr 0.88fr 0.78fr;
align-items: center;
min-width: 0;
text-align: center;
}
.device-table-head {
flex: 0 0 30px;
color: #bfe8ff;
font-size: 10px;
font-weight: 800;
background: rgba(10, 75, 145, 0.86);
}
.device-table-head span,
.device-data-row > * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-table-body {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
}
.device-table-track.scrolling {
animation: device-table-scroll var(--scroll-duration) linear infinite;
}
.device-table-track.scrolling:hover {
animation-play-state: paused;
}
.device-data-row {
height: var(--row-height, 28px);
padding: 0 4px;
color: #dcecff;
font-size: 10px;
border-bottom: 1px solid rgba(43, 216, 255, 0.1);
}
.device-data-row:nth-child(odd) {
background: rgba(4, 25, 59, 0.48);
}
.device-data-row:nth-child(even) {
background: rgba(4, 18, 47, 0.3);
}
.device-name {
color: #ffffff;
font-weight: 800;
}
.device-data-row strong {
font-size: 10px;
}
.status-dot {
display: inline-block;
flex: 0 0 7px;
width: 7px;
height: 7px;
border-radius: 50%;
}
.status-run {
background: #2fd47a;
box-shadow: 0 0 7px rgba(47, 212, 122, 0.9);
}
.status-off {
background: #78899b;
box-shadow: 0 0 5px rgba(120, 137, 155, 0.6);
}
.rate-good {
color: #2fd47a;
}
.rate-warning {
color: #ffb84d;
}
.rate-danger {
color: #ff637a;
}
.device-panel-state {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
color: #78a9ce;
font-size: 12px;
}
.device-loading-icon {
width: 14px;
height: 14px;
border: 2px solid rgba(43, 216, 255, 0.25);
border-top-color: #2bd8ff;
border-radius: 50%;
animation: device-loading 0.8s linear infinite;
}
@keyframes device-table-scroll {
from {
transform: translateY(0);
}
to {
transform: translateY(calc(-1 * var(--scroll-distance)));
}
}
@keyframes device-loading {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,217 @@
<template>
<section class="machining-wip-panel panel">
<header class="wip-panel-head">
<h2>机加在制任务</h2>
<span>加急任务红色显示</span>
</header>
<div class="wip-table-head wip-grid-row">
<span>设备</span>
<span>生产订单</span>
<span>物料名称</span>
<span>物料编码</span>
<span>工序</span>
<span>预算工时</span>
<span>数量</span>
<span>进度</span>
</div>
<div class="wip-table-body">
<div
v-if="status === 'ready'"
class="wip-table-track"
:class="{ scrolling: shouldScroll }"
:style="scrollStyle"
>
<div
v-for="(row, index) in displayRows"
:key="`${row.id}-${index}`"
class="wip-grid-row wip-data-row"
:class="{ urgent: row.urgent }"
:title="getRowTitle(row)"
>
<strong>{{ row.deviceName || '--' }}</strong>
<span>{{ row.productionOrder || '--' }}</span>
<span class="material-text">{{ row.materialName || '--' }}</span>
<span class="material-text">{{ row.materialCode || '--' }}</span>
<span>{{ row.processName || '--' }}</span>
<span>{{ formatBudget(row.budgetMinutes) }}</span>
<span>{{ formatNumber(row.plannedQty) }}</span>
<span>{{ formatNumber(row.reportedQty) }}/{{ formatNumber(row.plannedQty) }}</span>
</div>
</div>
<div v-else class="wip-panel-state">
<span v-if="status === 'loading'" class="wip-loading-icon"></span>
{{ status === 'loading' ? '加载中' : message }}
</div>
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
rows: { type: Array, default: () => [] },
status: { type: String, default: 'loading' },
message: { type: String, default: '' }
})
const rowHeight = 36
const shouldScroll = computed(() => props.rows.length > 7)
const displayRows = computed(() => shouldScroll.value ? [...props.rows, ...props.rows] : props.rows)
const scrollStyle = computed(() => ({
'--scroll-distance': `${props.rows.length * rowHeight}px`,
'--scroll-duration': `${Math.max(props.rows.length * 1.8, 20)}s`
}))
const formatNumber = value => Number(value || 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })
const formatBudget = value => value === null || value === undefined ? '--' : Number(value).toFixed(2)
const getRowTitle = row => [
`设备: ${row.deviceName || '--'}`,
`生产订单: ${row.productionOrder || '--'}`,
`物料: ${row.materialName || '--'} (${row.materialCode || '--'})`,
`工序: ${row.processName || '--'}`,
`预算工时: ${formatBudget(row.budgetMinutes)}`,
`进度: ${formatNumber(row.reportedQty)}/${formatNumber(row.plannedQty)}`
].join(' | ')
</script>
<style scoped lang="scss">
.machining-wip-panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
color: #dcecff;
background: rgba(7, 31, 72, 0.62);
}
.wip-panel-head {
display: flex;
flex: 0 0 36px;
align-items: center;
justify-content: space-between;
min-width: 0;
padding: 0 10px;
border-bottom: 1px solid rgba(43, 216, 255, 0.28);
}
.wip-panel-head h2 {
margin: 0;
color: #8cd3ff;
font-size: 14px;
font-weight: 900;
letter-spacing: 0;
white-space: nowrap;
}
.wip-panel-head span {
color: #ff637a;
font-size: 9px;
}
.wip-grid-row {
display: grid;
grid-template-columns: 0.55fr 1.35fr 1.05fr 1.7fr 0.72fr 0.7fr 0.42fr 0.7fr;
align-items: center;
min-width: 0;
text-align: center;
}
.wip-table-head {
flex: 0 0 30px;
color: #bfe8ff;
font-size: 9px;
font-weight: 800;
background: rgba(10, 75, 145, 0.72);
}
.wip-table-head span,
.wip-data-row > * {
min-width: 0;
padding: 0 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wip-data-row .material-text {
display: -webkit-box;
line-height: 12px;
overflow-wrap: anywhere;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.wip-table-body {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
}
.wip-table-track.scrolling {
animation: wip-table-scroll var(--scroll-duration) linear infinite;
}
.wip-table-track.scrolling:hover {
animation-play-state: paused;
}
.wip-data-row {
height: 36px;
color: #dcecff;
font-size: 9px;
border-bottom: 1px solid rgba(43, 216, 255, 0.1);
}
.wip-data-row:nth-child(odd) {
background: rgba(4, 25, 59, 0.34);
}
.wip-data-row:nth-child(even) {
background: rgba(4, 18, 47, 0.2);
}
.wip-data-row strong {
color: #ffffff;
}
.wip-data-row.urgent,
.wip-data-row.urgent strong {
color: #ff5e6c;
background: rgba(116, 19, 37, 0.28);
}
.wip-panel-state {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
color: #78a9ce;
font-size: 12px;
}
.wip-loading-icon {
width: 14px;
height: 14px;
border: 2px solid rgba(43, 216, 255, 0.25);
border-top-color: #2bd8ff;
border-radius: 50%;
animation: wip-loading 0.8s linear infinite;
}
@keyframes wip-table-scroll {
from { transform: translateY(0); }
to { transform: translateY(calc(-1 * var(--scroll-distance))); }
}
@keyframes wip-loading {
to { transform: rotate(360deg); }
}
</style>

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -67,6 +67,7 @@ export const MODULES = [
export const PROCEDURES = {
departmentOverview: 'MES_Kanban_DepartmentOverview',
monthlyRates: 'MES_Kanban_MonthlyRates',
planRate: '生产任务综合看板_按期齐套率',
planNoProcess: '生产任务综合看板_未排产无工艺查询',
planScheduleTrend: 'MES_Kanban_PlanScheduleTrend',

File diff suppressed because it is too large Load Diff