You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

676 lines
20 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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