refactor(mes): 重构生产计划监控功能

- 修改接口名称和返回类型:queryMoritorPageList -> queryMoritorList,返回类型从 TableDataInfo 改为 List
- 新增 PlanMonitorVo 和 ShiftGroupVo 类用于监控数据展示
- 优化前端监控页面,按机台和班次分组展示计划信息
- 重构后端查询逻辑,按时间区间参数查询监控数据
- 更新数据库查询语句,使用 WITH 子句进行预处理
master
zch 1 month ago
parent 4a1d2789d0
commit 04ff12df21

@ -127,9 +127,9 @@ export const issuePlanInfo = (workshopId: string | number, planId: string | numb
* @returns {*} * @returns {*}
*/ */
export const queryMoritorPageList = (query?: PlanInfoQuery): AxiosPromise<PlanInfoVO[]> => { export const queryMoritorList = (query?: PlanInfoQuery): AxiosPromise<PlanInfoVO[]> => {
return request({ return request({
url: '/mes/planInfo/queryMoritorPageList', url: '/mes/planInfo/queryMoritorList',
method: 'get', method: 'get',
params: query params: query
}); });

@ -537,5 +537,41 @@ export interface PlanInfoQuery extends PageQuery {
materialName?: string; materialName?: string;
} }
// 计划监控班次分组视图对象
export interface ShiftGroupVO {
/**
* ID
*/
shiftId: string | number;
/**
*
*/
shiftName: string;
/**
*
*/
plans: PlanInfoVO[];
}
// 生产计划监控视图对象
export interface PlanMonitorVO {
/**
* ID
*/
machineId: string | number;
/**
*
*/
machineName: string;
/**
*
*/
shifts: ShiftGroupVO[];
}

@ -6,7 +6,7 @@
<el-card shadow='hover'> <el-card shadow='hover'>
<el-form ref='queryFormRef' :model='queryParams' :inline='true'> <el-form ref='queryFormRef' :model='queryParams' :inline='true'>
<el-form-item label='机台名称' prop='releaseId'> <el-form-item label='机台名称' prop='releaseId'>
<el-select v-model='queryParams.releaseId' placeholder='请选择机台名称'> <el-select v-model='queryParams.releaseId' placeholder='请选择机台名称' clearable>
<el-option <el-option
v-for='item in releaseList' v-for='item in releaseList'
:key='item.machineId' :key='item.machineId'
@ -30,10 +30,12 @@
<el-form-item label="班次" prop="shiftId"> <el-form-item label="班次" prop="shiftId">
<el-select v-model="queryParams.shiftId" placeholder="请选择班次" clearable> <el-select v-model="queryParams.shiftId" placeholder="请选择班次" clearable>
<el-option v-for="item in shiftList" <el-option
:key="item.shiftId" v-for="item in shiftList"
:label="item.shiftName" :key="item.shiftId"
:value="item.shiftId" /> :label="item.shiftName"
:value="item.shiftId"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -46,45 +48,57 @@
</div> </div>
</transition> </transition>
<el-card shadow='never'> <el-card shadow='hover' v-loading="loading">
<el-table v-loading='loading' :data='uniqueMachines' border> <el-table :data="planInfoList" style="width: 100%" border>
<!-- 机台列 --> <!-- 机台列 -->
<el-table-column label='机台' align='center' prop='machineName' fixed="left" width="120" /> <el-table-column prop="machineName" label="机台" align="center" width="120" />
<!-- 动态生成班次列 --> <!-- 动态班次列 -->
<template v-for="shift in uniqueShifts" :key="shift.shiftId"> <template v-for="shift in uniqueShifts" :key="shift.shiftId">
<el-table-column :label="shift.shiftName" align='center' min-width="460"> <el-table-column :label="shift.shiftName" align="center">
<el-table-column label='物料' align='center' min-width="220"> <!-- 物料列 -->
<template #default="scope"> <el-table-column label="物料" align="left" min-width="300">
<div v-for="(item, index) in getItemsByMachineAndShift(scope.row.machineId, shift.shiftId)" :key="index" class="cell-content material-cell"> <template #default="{ row }">
<el-tooltip :content="item.materialName" placement="top" :hide-after="1500"> <div class="cell-content">
<span class="material-text">{{ item.materialName }}</span> <div v-for="plan in getPlansForShift(row, shift.shiftId)" :key="plan.planId" class="cell-item material-item">
</el-tooltip> <el-tooltip
:content="plan.materialName"
placement="top"
:show-after="500">
<span class="material-name">{{ plan.materialName }}</span>
</el-tooltip>
</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label='计划数' align='center' width="80"> <!-- 计划数列 -->
<template #default="scope"> <el-table-column label="计划数" align="center" width="100">
<div v-for="(item, index) in getItemsByMachineAndShift(scope.row.machineId, shift.shiftId)" :key="index" class="cell-content plan-cell"> <template #default="{ row }">
{{ item.planAmount }} <div class="cell-content">
<div v-for="plan in getPlansForShift(row, shift.shiftId)" :key="plan.planId" class="cell-item">
{{ plan.planAmount }}
</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label='实际数' align='center' width="80"> <!-- 完成数列 -->
<template #default="scope"> <el-table-column label="完成数" align="center" width="100">
<div v-for="(item, index) in getItemsByMachineAndShift(scope.row.machineId, shift.shiftId)" :key="index" class="cell-content actual-cell"> <template #default="{ row }">
{{ item.completeAmount || 0 }} <div class="cell-content">
<div v-for="plan in getPlansForShift(row, shift.shiftId)" :key="plan.planId" class="cell-item">
{{ plan.completeAmount || 0 }}
</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label='计划监控' align='center' width="120"> <!-- 计划监控列 -->
<template #default="scope"> <el-table-column label="计划监控" align="center" width="120">
<div v-for="(item, index) in getItemsByMachineAndShift(scope.row.machineId, shift.shiftId)" :key="index" class="cell-content progress-cell"> <template #default="{ row }">
<div class="progress-bar"> <div class="cell-content">
<div class="progress-bar-inner" <div v-for="plan in getPlansForShift(row, shift.shiftId)" :key="plan.planId" class="cell-item monitor-item">
:style="{ width: getItemCompletionPercentage(item) + '%', backgroundColor: getProgressBarColor(item) }"> <span class="completion-rate" :style="{ backgroundColor: getCompletionBgColor(plan), color: getCompletionTextColor(plan) }">
{{ getItemCompletionPercentage(item) }}% {{ getCompletionPercentage(plan) }}%
</div> </span>
</div> </div>
</div> </div>
</template> </template>
@ -107,7 +121,7 @@ import {
delPlanInfo, delPlanInfo,
addPlanInfo, addPlanInfo,
updatePlanInfo, updatePlanInfo,
orderAddProductPlanList, issuePlanInfo, queryMoritorPageList orderAddProductPlanList, issuePlanInfo, queryMoritorPageList, queryMoritorList
} from '@/api/mes/planInfo'; } from '@/api/mes/planInfo';
import { PlanInfoVO, PlanInfoQuery, PlanInfoForm } from '@/api/mes/planInfo/types'; import { PlanInfoVO, PlanInfoQuery, PlanInfoForm } from '@/api/mes/planInfo/types';
import { getBaseShiftInfoList } from '@/api/mes/baseShiftInfo'; import { getBaseShiftInfoList } from '@/api/mes/baseShiftInfo';
@ -118,6 +132,8 @@ import { getProcessInfoList } from '@/api/mes/baseProcessInfo';
import { getStationInfoList } from '@/api/mes/baseStationInfo'; import { getStationInfoList } from '@/api/mes/baseStationInfo';
import { getProdBaseMachineInfoList } from '@/api/mes/prodBaseMachineInfo'; import { getProdBaseMachineInfoList } from '@/api/mes/prodBaseMachineInfo';
import { ProdBaseMachineInfoVO } from '@/api/mes/prodBaseMachineInfo/types'; import { ProdBaseMachineInfoVO } from '@/api/mes/prodBaseMachineInfo/types';
import type { PlanMonitorVO, ShiftGroupVO } from '@/api/mes/planInfo/types';
import { ElMessage } from 'element-plus';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { const {
@ -219,8 +235,8 @@ const initFormData: PlanInfoForm = {
const data = reactive<PageData<PlanInfoForm, PlanInfoQuery>>({ const data = reactive<PageData<PlanInfoForm, PlanInfoQuery>>({
form: { ...initFormData }, form: { ...initFormData },
queryParams: { queryParams: {
pageNum: 1, pageNum: undefined,
pageSize: 10, pageSize: undefined,
planId: undefined, planId: undefined,
productOrderId: undefined, productOrderId: undefined,
saleOrderId: undefined, saleOrderId: undefined,
@ -275,7 +291,7 @@ const getWorkshopId = async () => {
/** 查询生产计划信息列表 */ /** 查询生产计划信息列表 */
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
const res = await queryMoritorPageList(queryParams.value); const res = await queryMoritorList(queryParams.value);
planInfoList.value = res.rows; planInfoList.value = res.rows;
total.value = res.total; total.value = res.total;
loading.value = false; loading.value = false;
@ -285,9 +301,6 @@ const getList = async () => {
const cancel = () => { const cancel = () => {
reset(); reset();
dialog.visible = false; dialog.visible = false;
sfpBatchDialog.visible = false;
formingBatchDialog.visible = false;
vulBatchDialog.visible = false;
}; };
/** 表单重置 */ /** 表单重置 */
@ -298,18 +311,42 @@ const reset = () => {
}; };
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = async () => {
queryParams.value.pageNum = 1; try {
getList(); loading.value = true;
//
if (!dateRange.value || dateRange.value.length !== 2) {
ElMessage.warning('请选择时间区间');
return;
}
//
queryParams.value.params = {
monitorBeginTime: dateRange.value[0],
monitorEndTime: dateRange.value[1]
};
const res = await queryMoritorList(queryParams.value);
if (res.code === 200) {
planInfoList.value = res.data;
console.log('查询结果:', planInfoList.value);
} else {
ElMessage.error(res.msg);
}
} catch (error) {
console.error('查询失败:', error);
ElMessage.error('查询失败');
} finally {
loading.value = false;
}
}; };
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
dateRange.value = []; dateRange.value = [];
data.queryParams.params.monitorBeginTime = undefined; queryParams.value.params.monitorBeginTime = undefined;
data.queryParams.params.monitorEndTime = undefined; queryParams.value.params.monitorEndTime = undefined;
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
handleQuery();
}; };
@ -359,7 +396,8 @@ onMounted(() => {
// getClassTeamSelect(); // getClassTeamSelect();
// getProcessSelect(); // getProcessSelect();
getReleaseSelect(); getReleaseSelect();
getList(); // getList();
handleQuery();
}); });
/*使 v-for /*使 v-for
@ -385,87 +423,50 @@ const uniqueMachines = computed(() => {
// //
const uniqueShifts = computed(() => { const uniqueShifts = computed(() => {
const shifts = []; const shiftsMap = new Map();
const shiftIds = new Set();//id,
planInfoList.value.forEach((machine: any) => {
// machine.shifts.forEach((shift: any) => {
const hasUnassignedShift = planInfoList.value.some(item => !item.shiftId); if (!shiftsMap.has(shift.shiftId)) {
if (hasUnassignedShift) { shiftsMap.set(shift.shiftId, {
shifts.push({ shiftId: shift.shiftId,
shiftId: 'default', shiftName: shift.shiftName
shiftName: '未分班' });
}
}); });
shiftIds.add('default');
}
//
planInfoList.value.forEach(item => {
if (item.shiftId && !shiftIds.has(item.shiftId)) {
shiftIds.add(item.shiftId);
shifts.push({
shiftId: item.shiftId,
shiftName: item.shiftName || `班次${item.shiftId}`//id
});
}
}); });
return shifts; // shiftId
return Array.from(shiftsMap.values()).sort((a, b) => a.shiftId - b.shiftId);
}); });
// //
const groupedPlanInfo = computed(() => {//groupedPlanInfo const getPlansForShift = (machine: PlanMonitorVO, shiftId: number) => {
const grouped = {}; const shift = machine.shifts.find((s: any) => s.shiftId === shiftId);
return shift?.plans || [];
planInfoList.value.forEach(item => { };
const machineId = item.releaseId;
const shiftId = item.shiftId || 'default'; // 使'default'nullshiftId
if (!machineId) return;//
if (!grouped[machineId]) {
grouped[machineId] = {};//
}
if (!grouped[machineId][shiftId]) {
grouped[machineId][shiftId] = [];//
}
grouped[machineId][shiftId].push(item);//
});
return grouped;
});
/* //
* v-for="(item, index) in getItemsByMachineAndShift(scope.row.machineId, shift.shiftId)"遍历当前机台在当前班次下的生产计划信息生成具体的行内容 const getCompletionBgColor = (plan: any) => {
* */ const percentage = getCompletionPercentage(plan);
// IDID if (percentage >= 100) return '#f0f9eb';
const getItemsByMachineAndShift = (machineId, shiftId) => { if (percentage >= 60) return '#fdf6ec';
if (shiftId === 'default') { return '#fef0f0';
//
return planInfoList.value.filter(item =>
item.releaseId === machineId && !item.shiftId
);
}
//
return planInfoList.value.filter(item =>
item.releaseId === machineId && item.shiftId === shiftId
);
}; };
// //
const getItemCompletionPercentage = (item) => { const getCompletionTextColor = (plan: any) => {
if (!item.planAmount) return 0; const percentage = getCompletionPercentage(plan);
const percentage = (item.completeAmount || 0) / item.planAmount * 100; if (percentage >= 100) return '#67C23A';
return Math.round(percentage * 100) / 100; if (percentage >= 60) return '#E6A23C';
return '#F56C6C';
}; };
// //
const getProgressBarColor = (item) => { const getCompletionPercentage = (plan: any) => {
const percentage = getItemCompletionPercentage(item); if (!plan.planAmount) return 0;
if (percentage >= 100) return '#67C23A'; // 100%绿 const percentage = (plan.completeAmount || 0) / plan.planAmount * 100;
if (percentage >= 60) return '#E6A23C'; // 60% return Math.round(percentage * 100) / 100;
return '#F56C6C'; //
}; };
// //
@ -503,20 +504,20 @@ const handleDateRangeChange = (val) => {
/* 材料项样式 */ /* 材料项样式 */
.material-item { .material-item {
/* 底部外边距8px */ padding: 0 12px;
margin-bottom: 8px; height: 36px;
/* 内边距4px */ line-height: 36px;
padding: 4px; display: flex;
/* 底部边框1px颜色#eee */ align-items: center;
border-bottom: 1px solid #eee; overflow: hidden;
} }
/* 最后一个材料项样式 */ .material-name {
.material-item:last-child { display: block;
/* 底部外边距0 */ overflow: hidden;
margin-bottom: 0; text-overflow: ellipsis;
/* 去除底部边框 */ white-space: nowrap;
border-bottom: none; width: 100%;
} }
/* 进度条样式 */ /* 进度条样式 */
@ -567,12 +568,6 @@ const handleDateRangeChange = (val) => {
border-bottom: none; border-bottom: none;
} }
.material-name {
font-weight: bold;
margin-bottom: 4px;
text-align: left;
}
.material-count { .material-count {
text-align: left; text-align: left;
margin-bottom: 2px; margin-bottom: 2px;
@ -594,70 +589,83 @@ const handleDateRangeChange = (val) => {
/* 每个单元格内容的通用样式 */ /* 每个单元格内容的通用样式 */
.cell-content { .cell-content {
height: 40px; display: flex;
line-height: 40px; flex-direction: column;
margin-bottom: 8px; }
padding: 0 5px;
border-bottom: 1px solid #eee; .cell-item {
overflow: hidden; height: 36px;
text-overflow: ellipsis; line-height: 36px;
white-space: nowrap; padding: 0 12px;
border-bottom: 1px solid #EBEEF5;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.cell-content:last-child { .cell-item:last-child {
margin-bottom: 0;
border-bottom: none; border-bottom: none;
} }
/* 物料单元格特定样式 */ /* 计划监控列的特殊样式 */
.material-cell { .monitor-item {
justify-content: left; justify-content: center; /* 水平居中 */
padding-left: 10px;
font-weight: 500;
} }
.material-text { .completion-rate {
overflow: hidden; display: inline-block;
text-overflow: ellipsis; padding: 2px 8px;
white-space: nowrap; border-radius: 4px;
width: 100%; font-size: 12px;
line-height: 1.5;
min-width: 45px;
text-align: center;
} }
/* 计划数单元格特定样式 */ .completion-zero {
.plan-cell { background-color: #fef0f0;
justify-content: center; color: #f56c6c;
} }
/* 实际数单元格特定样式 */ .completion-partial {
.actual-cell { background-color: #fef0f0;
justify-content: center; color: #f56c6c;
} }
/* 进度条单元格特定样式 */ .completion-full {
.progress-cell { background-color: #f0f9eb;
justify-content: center; color: #67c23a;
padding: 0 5px;
} }
/* 进度条样式 */ :deep(.el-table) {
.progress-bar { font-size: 14px;
width: 100%; table-layout: fixed; /* 使用固定表格布局 */
height: 24px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
} }
.progress-bar-inner { :deep(.el-table th) {
height: 100%; background-color: #f5f7fa;
background-color: #67C23A; }
display: flex;
align-items: center; :deep(.el-table td) {
justify-content: center; padding: 0;
color: white; }
min-width: 30px;
transition: width 0.3s ease; :deep(.el-table .cell) {
padding: 0;
white-space: normal; /* 允许文本换行 */
}
/* 确保物料列文字左对齐 */
:deep(.el-table .cell-content .material-item) {
justify-content: flex-start;
}
/* 调整其他列的宽度 */
:deep(.el-table .el-table__cell) {
padding: 0;
}
/* 确保tooltip正常显示 */
:deep(.el-tooltip__trigger) {
width: 100%;
} }
</style> </style>

Loading…
Cancel
Save