feat(mes): 添加在制品跟踪报表的树形工序展示功能

- 在表格中新增展开列,用于展示工序的树形结构
- 实现 buildProcessTree 方法构建订单与工序的父子关系
- 使用 el-tree 组件渲染工序进度与状态信息
- 添加工序状态的颜色标识与进度条展示
- 调整部分字段的默认显示状态,优化列配置
- 增加工序树样式,支持不同状态的视觉区分
- 修复物料编号列默认不显示的问题
- 隐藏在制工序、剩余工序及工序进度等冗余列
- 提供 tooltip 显示工序计划、完成与剩余数量信息- 完善 tree-node 样式布局与状态类名控制
master
zangch@mesnac.com 2 months ago
parent 6a47a90801
commit 2513fac6da

@ -0,0 +1,675 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<!-- <el-form-item label="日期范围" style="width: 300px">
<el-date-picker
v-model="dateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="生产订单号" prop="orderCode">
<el-input
v-model="queryParams.orderCode"
placeholder="请输入生产订单号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>-->
<!--
<el-form-item label="物料编号" prop="materialCode">
<el-input
v-model="queryParams.materialCode"
placeholder="请输入物料编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
-->
<el-form-item label="物料名称" prop="materialName">
<el-input
v-model="queryParams.materialName"
placeholder="请输入物料名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!--
<el-form-item label="进度状态" prop="progressStatus">
<el-select v-model="queryParams.progressStatus" placeholder="请选择进度状态" clearable @keyup.enter="handleQuery">
<el-option label="正常" value="正常" />
<el-option label="延期" value="延期" />
</el-select>
</el-form-item>-->
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<!-- 统计图表卡片 -->
<!-- <el-row :gutter="10" class="mb-[10px]">
<el-col :span="8">
<el-card shadow="never">
<template #header>
<span class="font-bold">进度状态分布</span>
</template>
<div ref="statusChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<template #header>
<span class="font-bold">整体进度分布</span>
</template>
<div ref="progressChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<template #header>
<span class="font-bold">工序完成率统计</span>
</template>
<div ref="processStatsChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</el-col>
</el-row>-->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span class="font-bold">进度状态分布</span>
</template>
<div ref="statusChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span class="font-bold">整体进度分布</span>
</template>
<div ref="progressChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</el-col>
</el-row>
<!-- 工序进度可视化 -->
<el-card shadow="never" class="mb-[10px]">
<template #header>
<span class="font-bold">工序进度可视化</span>
</template>
<div ref="processChartRef" style="width: 100%; height: 400px"></div>
</el-card>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['mes:prodReport:export']"></el-button>
</el-col>-->
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList" />
</el-row>
</template>
<el-table v-loading="loading" :data="reportList" border>
<el-table-column label="生产订单号" align="center" prop="orderCode" v-if="columns[0].visible" width="140" />
<el-table-column label="物料编号" align="center" prop="materialCode" v-if="columns[1].visible" width="120" />
<el-table-column label="物料名称" align="center" prop="materialName" v-if="columns[2].visible" width="180" show-overflow-tooltip />
<el-table-column label="规格型号" align="center" prop="materialSpec" v-if="columns[3].visible" width="120" show-overflow-tooltip />
<el-table-column label="计划总数量" align="center" prop="planAmount" v-if="columns[4].visible" width="100" />
<el-table-column label="在制数量" align="center" prop="wipAmount" v-if="columns[5].visible" width="100" />
<el-table-column label="已完成数量" align="center" prop="completeAmount" v-if="columns[6].visible" width="100" />
<el-table-column label="计划开工时间" align="center" prop="planBeginTime" v-if="columns[7].visible" width="150" />
<el-table-column label="实际开工时间" align="center" prop="realBeginTime" v-if="columns[8].visible" width="150" />
<el-table-column label="计划完工时间" align="center" prop="planEndTime" v-if="columns[9].visible" width="150" />
<el-table-column label="当前时间" align="center" prop="currentTime" v-if="columns[10].visible" width="150" />
<el-table-column label="总工序数" align="center" prop="totalProcessCount" v-if="columns[11].visible" width="100" />
<el-table-column label="在制工序" align="center" prop="wipProcesses" v-if="columns[12].visible" width="150" show-overflow-tooltip />
<el-table-column label="剩余工序" align="center" prop="remainingProcesses" v-if="columns[13].visible" width="150" show-overflow-tooltip />
<el-table-column label="整体进度" align="center" prop="overallProgress" v-if="columns[14].visible" width="120">
<template #default="scope">
<el-progress
:percentage="parseFloat(scope.row.overallProgress)"
:color="getProgressColor(parseFloat(scope.row.overallProgress))"
:stroke-width="8"
/>
</template>
</el-table-column>
<el-table-column label="进度状态" align="center" prop="progressStatus" v-if="columns[15].visible" width="100">
<template #default="scope">
<el-tag :type="scope.row.progressStatus === '正常' ? 'success' : 'danger'">
{{ scope.row.progressStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="工序进度" align="center" v-if="columns[16].visible" width="350">
<template #default="scope">
<div class="process-progress-container">
<!-- <div
v-for="process in scope.row.processProgressList"
:key="process.processId"
class="process-step"
:class="{
'completed': process.isCompleted === 1,
'in-progress': process.isInProgress === 1,
'pending': process.isCompleted !== 1 && process.isInProgress !== 1
}"
:title="`${process.processName} - ${process.statusDesc}${process.processProgress ? ' (' + process.processProgress + '%)' : ''}`"
>-->
<div
v-for="process in scope.row.processProgressList"
:key="process.processId"
class="process-step"
:class="{
'completed': process.isCompleted === 1,
'in-progress': process.isInProgress === 1,
'pending': process.isCompleted !== 1 && process.isInProgress !== 1
}"
:title="`${process.processName}`"
>
<span class="process-name">{{ process.processName }}</span>
<!-- <span v-if="process.processProgress && process.processProgress > 0" class="process-percentage">
{{ process.processProgress }}%
</span>-->
</div>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="WipTrackingReport" lang="ts">
import { getCurrentInstance, ref, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import type { ElFormInstance } from 'element-plus';
import * as echarts from 'echarts';
import { listWipTrackingReport, exportWipTrackingReport } from '@/api/mes/wipTrackingReport';
import { WipTrackingReportVO, WipTrackingReportQuery, ProcessProgressVO } from '@/api/mes/wipTrackingReport/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<WipTrackingReportVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const dateRange = ref<string[]>(['', '']);
//
const statusChartRef = ref<HTMLDivElement | null>(null);
const progressChartRef = ref<HTMLDivElement | null>(null);
const processChartRef = ref<HTMLDivElement | null>(null);
const processStatsChartRef = ref<HTMLDivElement | null>(null);
let statusChart: echarts.ECharts | null = null;
let progressChart: echarts.ECharts | null = null;
let processChart: echarts.ECharts | null = null;
let processStatsChart: echarts.ECharts | null = null;
//
const columns = ref([
{ key: 0, label: '生产订单号', visible: true },
{ key: 1, label: '物料编号', visible: false },
{ key: 2, label: '物料名称', visible: true },
{ key: 3, label: '规格型号', visible: true },
{ key: 4, label: '计划总数量', visible: true },
{ key: 5, label: '在制数量', visible: false },
{ key: 6, label: '已完成数量', visible: true },
{ key: 7, label: '计划开工时间', visible: true },
{ key: 8, label: '实际开工时间', visible: true },
{ key: 9, label: '计划完工时间', visible: true },
{ key: 10, label: '当前时间', visible: false },
{ key: 11, label: '总工序数', visible: true },
{ key: 12, label: '在制工序', visible: true },
{ key: 13, label: '剩余工序', visible: true },
{ key: 14, label: '整体进度', visible: true },
{ key: 15, label: '进度状态', visible: true },
{ key: 16, label: '工序进度', visible: true }
]);
const queryParams = ref<WipTrackingReportQuery>({
pageNum: 1,
pageSize: 10,
orderCode: '',
materialCode: '',
materialName: '',
progressStatus: '',
beginDate: '',
endDate: ''
});
//
watch(dateRange, (newVal) => {
if (newVal && newVal.length === 2) {
queryParams.value.beginDate = newVal[0];
queryParams.value.endDate = newVal[1];
} else {
queryParams.value.beginDate = '';
queryParams.value.endDate = '';
}
});
/** 查询在制品跟踪报表列表 */
function getList() {
loading.value = true;
listWipTrackingReport(queryParams.value).then((response: any) => {
reportList.value = response.rows;
total.value = response.total;
loading.value = false;
//
nextTick(() => {
updateCharts();
});
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
orderCode: '',
materialCode: '',
materialName: '',
progressStatus: '',
beginDate: '',
endDate: ''
};
handleQuery();
}
/** 导出按钮操作 */
function handleExport() {
proxy?.$modal.confirm('是否确认导出所有在制品跟踪报表数据项?').then(() => {
proxy?.$modal.loading('正在导出数据,请稍候...');
return exportWipTrackingReport(queryParams.value);
}).then((response: any) => {
proxy?.$download.blob(response, '在制品跟踪报表.xlsx');
proxy?.$modal.closeLoading();
}).catch(() => {
proxy?.$modal.closeLoading();
});
}
/** 获取进度颜色 */
function getProgressColor(percentage: number) {
if (percentage < 30) return '#f56c6c';
if (percentage < 70) return '#e6a23c';
return '#67c23a';
}
/** 初始化图表 */
function initCharts() {
if (statusChartRef.value) {
statusChart = echarts.init(statusChartRef.value);
}
if (progressChartRef.value) {
progressChart = echarts.init(progressChartRef.value);
}
if (processChartRef.value) {
processChart = echarts.init(processChartRef.value);
}
if (processStatsChartRef.value) {
processStatsChart = echarts.init(processStatsChartRef.value);
}
}
/** 更新图表数据 */
function updateCharts() {
updateStatusChart();
updateProgressChart();
updateProcessChart();
updateProcessStatsChart();
}
/** 更新进度状态分布图 */
function updateStatusChart() {
if (!statusChart || !reportList.value.length) return;
const statusData = reportList.value.reduce((acc: any, item) => {
const status = item.progressStatus || '未知';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
const option = {
title: {
text: '进度状态分布',
left: 'center',
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: '进度状态',
type: 'pie',
radius: '60%',
data: Object.entries(statusData).map(([name, value]) => ({ name, value })),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
statusChart.setOption(option);
}
/** 更新整体进度分布图 */
function updateProgressChart() {
if (!progressChart || !reportList.value.length) return;
const progressRanges = {
'0-30%': 0,
'30-60%': 0,
'60-90%': 0,
'90-100%': 0
};
reportList.value.forEach(item => {
const progress = item.overallProgressNum || 0;
if (progress < 30) progressRanges['0-30%']++;
else if (progress < 60) progressRanges['30-60%']++;
else if (progress < 90) progressRanges['60-90%']++;
else progressRanges['90-100%']++;
});
const option = {
title: {
text: '整体进度分布',
left: 'center',
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: Object.keys(progressRanges)
},
yAxis: {
type: 'value'
},
series: [
{
name: '订单数量',
type: 'bar',
data: Object.values(progressRanges),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}
]
};
progressChart.setOption(option);
}
/** 更新工序完成率统计图 */
function updateProcessStatsChart() {
if (!processStatsChart || !reportList.value.length) return;
const processStats = {
'已完成': 0,
'进行中': 0,
'未开始': 0
};
reportList.value.forEach(item => {
if (item.processProgressList) {
item.processProgressList.forEach(process => {
if (process.isCompleted) {
processStats['已完成']++;
} else if (process.isInProgress) {
processStats['进行中']++;
} else {
processStats['未开始']++;
}
});
}
});
const option = {
title: {
text: '工序完成率统计',
left: 'center',
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: '工序状态',
type: 'pie',
radius: ['40%', '70%'],
data: Object.entries(processStats).map(([name, value]) => ({ name, value })),
itemStyle: {
color: function(params: any) {
const colors = ['#722ed1', '#52c41a', '#d9d9d9'];
return colors[params.dataIndex];
}
}
}
]
};
processStatsChart.setOption(option);
}
/** 更新工序进度可视化图 */
function updateProcessChart() {
if (!processChart || !reportList.value.length) return;
const processData = reportList.value.slice(0, 10).map(item => ({
name: item.orderCode,
value: [
item.orderCode,
item.overallProgressNum || 0,
item.progressStatus,
item.wipProcesses || '',
item.remainingProcesses || ''
]
}));
const option = {
title: {
text: '工序进度可视化前10个订单',
left: 'center',
textStyle: { fontSize: 14 }
},
/* tooltip: {
trigger: 'axis',
formatter: function(params: any) {
const data = params[0].data.value;
return `
进度: ${data[1]}%<br/>
状态: ${data[2]}<br/>
在制工序: ${data[3]}<br/>
剩余工序: ${data[4]}`;
}
},*/
xAxis: {
type: 'category',
data: processData.map(item => item.name),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '进度(%)',
max: 100
},
series: [
{
name: '整体进度',
type: 'bar',
data: processData.map(item => ({
value: item.value[1],
itemStyle: {
color: item.value[2] === '延期' ? '#f56c6c' : '#67c23a'
}
})),
markLine: {
data: [
{ yAxis: 100, name: '完成线' }
]
}
}
]
};
processChart.setOption(option);
}
/** 窗口大小改变时重新调整图表 */
function handleResize() {
statusChart?.resize();
progressChart?.resize();
processChart?.resize();
processStatsChart?.resize();
}
onMounted(() => {
getList();
nextTick(() => {
initCharts();
window.addEventListener('resize', handleResize);
});
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
statusChart?.dispose();
progressChart?.dispose();
processChart?.dispose();
processStatsChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 10px;
}
.el-progress {
width: 100%;
}
/* 工序进度样式 */
.process-progress-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
align-items: center;
}
.process-step {
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
border: 1px solid;
transition: all 0.3s ease;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
position: relative;
}
.process-step.completed {
background-color: #722ed1;
color: white;
border-color: #722ed1;
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.3);
}
.process-step.in-progress {
background-color: #52c41a;
color: white;
border-color: #52c41a;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
animation: pulse 2s infinite;
}
.process-step.pending {
background-color: #f5f5f5;
color: #666;
border-color: #d9d9d9;
}
.process-step:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.process-name {
font-weight: 600;
margin-bottom: 2px;
}
.process-percentage {
font-size: 10px;
opacity: 0.9;
font-weight: 400;
}
/* 进行中工序的脉动动画 */
@keyframes pulse {
0% {
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
}
50% {
box-shadow: 0 2px 12px rgba(82, 196, 26, 0.6);
}
100% {
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
}
}
</style>

@ -125,7 +125,39 @@
</el-row>
</template>
<el-table v-loading="loading" :data="reportList" border>
<el-table v-loading="loading" :data="reportList" border row-key="orderCode">
<!-- 展开列树形展示工序子节点数量与进度 -->
<el-table-column type="expand" width="50">
<template #default="scope">
<el-tree
:data="buildProcessTree(scope.row)"
:props="treeProps"
:expand-on-click-node="false"
:indent="18"
class="process-tree"
>
<template #default="{ node, data }">
<div class="tree-node" :class="'status-' + (data.status || 'pending')">
<el-tooltip
effect="light"
placement="top"
:content="`计划:${data.planAmount ?? 0},完成:${data.completeAmount ?? 0},剩余:${data.remainingAmount ?? 0}`"
>
<span class="node-title">{{ data.label }}</span>
</el-tooltip>
<el-progress
v-if="data.progress != null"
:percentage="Number(data.progress)"
:color="getProgressColor(Number(data.progress))"
:stroke-width="4"
:show-text="false"
class="node-progress"
/>
</div>
</template>
</el-tree>
</template>
</el-table-column>
<el-table-column label="生产订单号" align="center" prop="orderCode" v-if="columns[0].visible" width="140" />
<el-table-column label="物料编号" align="center" prop="materialCode" v-if="columns[1].visible" width="120" />
<el-table-column label="物料名称" align="center" prop="materialName" v-if="columns[2].visible" width="180" show-overflow-tooltip />
@ -226,7 +258,7 @@ let processStatsChart: echarts.ECharts | null = null;
//
const columns = ref([
{ key: 0, label: '生产订单号', visible: true },
{ key: 1, label: '物料编号', visible: false },
{ key: 1, label: '物料编号', visible: true },
{ key: 2, label: '物料名称', visible: true },
{ key: 3, label: '规格型号', visible: true },
{ key: 4, label: '计划总数量', visible: true },
@ -237,11 +269,11 @@ const columns = ref([
{ key: 9, label: '计划完工时间', visible: true },
{ key: 10, label: '当前时间', visible: false },
{ key: 11, label: '总工序数', visible: true },
{ key: 12, label: '在制工序', visible: true },
{ key: 13, label: '剩余工序', visible: true },
{ key: 12, label: '在制工序', visible: false },
{ key: 13, label: '剩余工序', visible: false },
{ key: 14, label: '整体进度', visible: true },
{ key: 15, label: '进度状态', visible: true },
{ key: 16, label: '工序进度', visible: true }
{ key: 16, label: '工序进度', visible: false }
]);
const queryParams = ref<WipTrackingReportQuery>({
@ -324,6 +356,30 @@ function getProgressColor(percentage: number) {
return '#67c23a';
}
/** 树形结构配置 */
const treeProps = { children: 'children', label: 'label' } as const;
/** 构建工序树(父:订单;子:工序) */
function buildProcessTree(row: WipTrackingReportVO) {
const children = (row.processProgressList || []).map(p => ({
id: p.processId,
label: p.processName,
progress: p.processProgress ?? 0,
planAmount: p.planAmount ?? 0,
completeAmount: p.completeAmount ?? 0,
remainingAmount: p.remainingAmount ?? 0,
status: (p as any).isCompleted === 1 || p.isCompleted ? 'completed' : ((p as any).isInProgress === 1 || p.isInProgress ? 'in_progress' : 'pending')
}));
return [
{
id: row.productOrderId || row.orderCode,
label: `${row.orderCode}(在制工序(${children.filter(c => c.status === 'in_progress').length}`,
children
}
];
}
/** 初始化图表 */
function initCharts() {
if (statusChartRef.value) {
@ -589,6 +645,32 @@ onBeforeUnmount(() => {
});
</script>
<style scoped>
.process-tree {
padding: 6px 8px;
}
.process-tree .el-tree-node__content {
min-height: 28px;
line-height: 28px;
padding: 2px 6px;
}
.tree-node {
display: flex;
align-items: center;
gap: 8px;
}
.tree-node .node-title {
font-size: 12px;
font-weight: 500;
}
.node-progress {
width: 120px;
}
.status-completed .node-title { color: #67c23a; }
.status-in_progress .node-title { color: #e6a23c; }
.status-pending .node-title { color: #909399; }
</style>
<style scoped>
.el-card {
margin-bottom: 10px;
@ -672,4 +754,37 @@ onBeforeUnmount(() => {
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
}
}
/* 树形工序视图样式 */
.process-tree {
padding: 6px 0;
}
.tree-node {
display: grid;
grid-template-columns: 1fr auto;
gap: 6px 12px;
align-items: center;
padding: 6px 8px;
border: 1px solid #dcdfe6;
border-radius: 6px;
margin-bottom: 6px;
}
.tree-node .node-title {
font-weight: 600;
}
.tree-node .node-qty {
color: #666;
font-size: 12px;
}
.status-completed {
border-color: #722ed1;
background: rgba(114, 46, 209, 0.06);
}
.status-in_progress {
border-color: #52c41a;
background: rgba(82, 196, 26, 0.06);
}
.status-pending {
border-color: #d9d9d9;
}
</style>

Loading…
Cancel
Save