feat: add production task overview dashboard

This commit is contained in:
meswork764
2026-07-11 18:07:38 +08:00
parent 1e3f9299df
commit b5bc8d7358
10 changed files with 3382 additions and 12 deletions

View File

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

View File

@@ -1,24 +1,28 @@
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,

View File

@@ -0,0 +1,261 @@
<template>
<article class="chart-panel panel">
<div class="panel-head">
<div class="panel-title">
<h2>{{ title }}</h2>
<p v-if="subtitle">{{ subtitle }}</p>
</div>
<div v-if="stats.length" class="stat-box" :class="`stat-${resolvedStatsVariant}`">
<template v-if="resolvedStatsVariant === 'summary'">
<div class="summary-total">
<b>{{ stats[0].value }}</b>
<span>{{ stats[0].unit || '' }}</span>
</div>
<div v-if="stats[1]" class="summary-urgent">
<span>{{ stats[1].label }}</span>
<b>{{ stats[1].value }}</b>
<em>{{ stats[1].unit || '' }}</em>
</div>
</template>
<template v-else>
<div v-for="item in stats" :key="item.label" class="stat-line">
<span>{{ item.label }}</span>
<b>{{ item.value }}</b>
</div>
</template>
</div>
<span v-else-if="badge" class="badge">{{ badge }}</span>
</div>
<div class="chart-body" :style="{ height: normalizedHeight }">
<div ref="elRef" class="chart-canvas"></div>
<div v-if="showOverlay" class="state-overlay">
<div class="state-icon"></div>
<div>{{ overlayText }}</div>
</div>
</div>
</article>
</template>
<script setup>
import { computed } from 'vue'
import { useEchart } from '../composables/useEchart'
const props = defineProps({
title: {
type: String,
required: true
},
subtitle: {
type: String,
default: ''
},
badge: {
type: String,
default: ''
},
stats: {
type: Array,
default: () => []
},
statsVariant: {
type: String,
default: 'compact'
},
option: {
type: Object,
default: () => ({})
},
status: {
type: String,
default: 'ready'
},
emptyText: {
type: String,
default: '暂无数据'
},
height: {
type: [Number, String],
default: 260
}
})
const optionRef = computed(() => props.option)
const { elRef } = useEchart(optionRef)
const normalizedHeight = computed(() => (typeof props.height === 'number' ? `${props.height}px` : props.height))
const resolvedStatsVariant = computed(() => (props.statsVariant === 'compact' && props.stats.length === 2 ? 'summary' : props.statsVariant))
const showOverlay = computed(() => props.status !== 'ready')
const overlayText = computed(() => {
if (props.status === 'loading') {
return '加载中'
}
if (props.status === 'error') {
return '数据加载失败'
}
if (props.status === 'pending') {
return '待接口接入'
}
return props.emptyText
})
</script>
<style scoped lang="scss">
.chart-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 18px;
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
min-height: 58px;
}
.panel-title {
min-width: 0;
}
h2 {
margin: 0;
color: #8cd3ff;
font-size: 22px;
font-weight: 800;
line-height: 1.2;
}
p {
margin: 7px 0 0;
color: #78a9ce;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge {
flex: 0 0 auto;
padding: 4px 10px;
color: #eaf7ff;
font-size: 13px;
border: 1px solid rgba(43, 216, 255, 0.46);
border-radius: 4px;
background: rgba(22, 141, 255, 0.22);
}
.stat-box {
flex: 0 0 168px;
min-height: 52px;
padding: 8px 10px;
border: 1px solid rgba(43, 216, 255, 0.46);
border-radius: 4px;
background: rgba(6, 19, 56, 0.72);
box-shadow: inset 0 0 12px rgba(43, 216, 255, 0.12);
}
.stat-summary {
flex-basis: 138px;
min-height: 66px;
padding: 5px 10px 7px;
text-align: right;
border-color: transparent;
background: transparent;
box-shadow: none;
}
.summary-total {
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: 5px;
color: #eaf7ff;
}
.summary-total b {
color: #38c8ff;
font-size: 42px;
font-weight: 900;
line-height: 0.9;
text-shadow: 0 0 12px rgba(56, 200, 255, 0.42);
}
.summary-total span {
padding-bottom: 2px;
color: #eaf7ff;
font-size: 16px;
font-weight: 800;
}
.summary-urgent {
margin-top: 4px;
color: #eaf7ff;
font-size: 14px;
font-weight: 800;
}
.summary-urgent b {
margin: 0 4px;
color: #ff3d57;
font-size: 18px;
font-weight: 900;
}
.summary-urgent em {
color: #eaf7ff;
font-style: normal;
}
.stat-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #78a9ce;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.stat-line b {
color: #eaf7ff;
font-size: 13px;
font-weight: 800;
}
.chart-body {
position: relative;
min-height: 160px;
margin-top: 12px;
}
.chart-canvas {
width: 100%;
height: 100%;
}
.state-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: #78a9ce;
font-size: 16px;
border: 1px dashed rgba(43, 216, 255, 0.28);
background: rgba(7, 31, 72, 0.78);
}
.state-icon {
width: 54px;
height: 32px;
border: 1px solid rgba(43, 216, 255, 0.42);
border-top-width: 5px;
box-shadow: inset 0 0 16px rgba(43, 216, 255, 0.18);
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<header class="dashboard-header">
<div class="brand">
<img class="brand-logo" :src="logoUrl" alt="RENLY" />
</div>
<div class="title-wrap">
<h1>{{ title }}</h1>
<div class="subtitle">{{ subtitle }}</div>
</div>
<div class="header-meta">
<div class="current-time">{{ currentTime }}</div>
<div class="refresh-row">
<span v-if="lastUpdated" class="updated">更新 {{ lastUpdated }}</span>
<button class="refresh-btn" type="button" :disabled="refreshing" @click="$emit('refresh')">
<el-icon><Refresh /></el-icon>
<span>{{ refreshing ? '刷新中' : '刷新' }}</span>
</button>
</div>
</div>
</header>
</template>
<script setup>
import { Refresh } from '@element-plus/icons-vue'
defineProps({
title: {
type: String,
default: '计划看板'
},
subtitle: {
type: String,
default: '生产计划综合监控'
},
currentTime: {
type: String,
default: ''
},
lastUpdated: {
type: String,
default: ''
},
refreshing: {
type: Boolean,
default: false
},
logoUrl: {
type: String,
default: './public/ClientLogo.png'
}
})
defineEmits(['refresh'])
</script>
<style scoped lang="scss">
.dashboard-header {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: 340px 1fr 460px;
align-items: center;
height: 96px;
border-bottom: 1px solid rgba(43, 216, 255, 0.62);
background: rgba(7, 0, 31, 0.96);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.26);
}
.brand {
display: flex;
align-items: center;
height: 100%;
}
.brand-logo {
width: 224px;
height: 58px;
padding: 5px 10px;
object-fit: contain;
object-position: left center;
background: #ffffff;
border-radius: 6px;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.28));
}
.title-wrap {
min-width: 0;
text-align: center;
}
h1 {
margin: 0;
color: #f3f9ff;
font-size: 48px;
font-weight: 900;
line-height: 1.05;
letter-spacing: 0;
}
.subtitle {
margin-top: 8px;
color: #8cd3ff;
font-size: 17px;
}
.header-meta {
padding-right: 18px;
text-align: right;
}
.current-time {
color: #eef7ff;
font-size: 22px;
font-weight: 700;
}
.refresh-row {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.updated {
color: #78a9ce;
font-size: 14px;
}
.refresh-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
color: #eaf7ff;
font-size: 14px;
border: 1px solid rgba(43, 216, 255, 0.62);
border-radius: 4px;
background: rgba(14, 75, 156, 0.62);
cursor: pointer;
}
.refresh-btn:disabled {
cursor: not-allowed;
opacity: 0.62;
}
</style>

View File

@@ -0,0 +1,191 @@
<template>
<article class="data-table panel">
<div class="table-head">
<h2>{{ title }}</h2>
<span class="row-count">{{ rows.length }} </span>
</div>
<div
class="table-wrap"
:style="{ height: normalizedMaxHeight, '--scroll-duration': scrollDuration }"
>
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key" :style="{ width: column.width || 'auto' }">
{{ column.label }}
</th>
</tr>
</thead>
<tbody :class="{ 'auto-scroll': shouldAutoScroll }">
<tr v-if="rows.length === 0">
<td class="empty-cell" :colspan="columns.length">{{ emptyText }}</td>
</tr>
<tr v-for="(row, index) in displayRows" v-else :key="row.id || `${title}-${index}`">
<td v-for="column in columns" :key="column.key" :style="{ width: column.width || 'auto' }">
<span>{{ row[column.key] || '--' }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</article>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: {
type: String,
required: true
},
columns: {
type: Array,
required: true
},
rows: {
type: Array,
default: () => []
},
emptyText: {
type: String,
default: '暂无数据'
},
maxHeight: {
type: [Number, String],
default: 360
}
})
const normalizedMaxHeight = computed(() => (typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight))
const visibleRows = computed(() => {
const height = Number.parseFloat(props.maxHeight)
if (!Number.isFinite(height)) {
return 7
}
return Math.max(Math.floor((height - 42) / 42), 1)
})
const shouldAutoScroll = computed(() => props.rows.length > visibleRows.value)
const displayRows = computed(() => (shouldAutoScroll.value ? [...props.rows, ...props.rows] : props.rows))
const scrollDuration = computed(() => `${Math.max(props.rows.length * 3, 24)}s`)
</script>
<style scoped lang="scss">
.data-table {
min-width: 0;
padding: 16px 16px 18px;
}
.table-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
h2 {
margin: 0;
color: #8cd3ff;
font-size: 21px;
font-weight: 800;
}
.row-count {
color: #78a9ce;
font-size: 13px;
}
.table-wrap {
overflow: hidden;
border: 1px solid rgba(53, 149, 217, 0.42);
}
table {
width: 100%;
min-width: 0;
table-layout: fixed;
border-collapse: collapse;
}
thead {
position: relative;
z-index: 2;
display: table;
width: 100%;
table-layout: fixed;
}
tbody {
display: block;
width: 100%;
}
tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
tbody.auto-scroll {
animation: table-scroll-up var(--scroll-duration, 24s) linear infinite;
}
.table-wrap:hover tbody.auto-scroll {
animation-play-state: paused;
}
@keyframes table-scroll-up {
from {
transform: translateY(0);
}
to {
transform: translateY(-50%);
}
}
th,
td {
height: 42px;
padding: 0 8px;
color: #f1f8ff;
font-size: 13px;
line-height: 1.25;
text-align: left;
border: 1px solid rgba(53, 149, 217, 0.42);
word-break: break-all;
}
th {
color: #cceeff;
font-weight: 800;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-break: normal;
background: #082654;
box-shadow: inset 0 0 12px rgba(27, 130, 214, 0.32);
}
td {
background: rgba(7, 0, 31, 0.62);
}
tbody tr:nth-child(even) td {
background: rgba(8, 38, 84, 0.38);
}
td span {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.empty-cell {
height: 120px;
color: #78a9ce;
text-align: center;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<article class="metric-card" :class="[`status-${status}`, { compact }]">
<div class="metric-label">{{ label }}</div>
<div class="metric-value" :class="{ rate: valueType === 'rate' }">{{ value }}</div>
<div v-if="subValue" class="metric-sub">{{ subValue }}</div>
<div v-else class="metric-sub muted">{{ placeholder }}</div>
</article>
</template>
<script setup>
defineProps({
label: {
type: String,
required: true
},
value: {
type: [String, Number],
default: '--'
},
subValue: {
type: String,
default: ''
},
status: {
type: String,
default: 'ready'
},
valueType: {
type: String,
default: 'number'
},
placeholder: {
type: String,
default: '--'
},
compact: {
type: Boolean,
default: false
}
})
</script>
<style scoped lang="scss">
.metric-card {
position: relative;
min-width: 0;
min-height: 112px;
padding: 18px 18px 15px;
overflow: hidden;
border: 1px solid rgba(43, 216, 255, 0.45);
border-radius: 6px;
background:
linear-gradient(135deg, rgba(19, 111, 190, 0.24), rgba(4, 20, 62, 0.18)),
rgba(7, 31, 72, 0.74);
box-shadow: inset 0 0 16px rgba(27, 130, 214, 0.18);
}
.metric-card::before {
position: absolute;
right: -18px;
bottom: -28px;
width: 92px;
height: 92px;
content: '';
border: 1px solid rgba(43, 216, 255, 0.2);
border-radius: 50%;
}
.metric-label {
position: relative;
z-index: 1;
color: #8cd3ff;
font-size: 17px;
font-weight: 700;
line-height: 1.25;
}
.metric-value {
position: relative;
z-index: 1;
margin-top: 12px;
color: #ffffff;
font-size: 40px;
font-weight: 900;
line-height: 1;
white-space: nowrap;
text-shadow: 0 0 12px rgba(28, 230, 255, 0.32);
}
.metric-value.rate {
color: #1ce6ff;
}
.metric-sub {
position: relative;
z-index: 1;
min-height: 18px;
margin-top: 9px;
color: #d8f4ff;
font-size: 14px;
line-height: 1.25;
}
.metric-sub.muted {
color: rgba(120, 169, 206, 0.62);
}
.status-empty,
.status-pending {
border-style: dashed;
background: rgba(7, 31, 72, 0.48);
}
.status-empty .metric-value,
.status-pending .metric-value {
color: #78a9ce;
text-shadow: none;
}
.compact {
min-height: 92px;
padding: 14px 14px 12px;
}
.compact .metric-value {
font-size: 32px;
}
</style>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff