|
|
<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="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="日期范围" 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="planCode">
|
|
|
<el-input
|
|
|
v-model="queryParams.planCode"
|
|
|
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>-->
|
|
|
|
|
|
<!-- 工序工单统计(按工序聚合 Top10) -->
|
|
|
<el-row :gutter="10" class="mb-[10px]">
|
|
|
<el-col :span="24">
|
|
|
<el-card shadow="never">
|
|
|
<template #header>
|
|
|
<span class="font-bold">工序统计</span>
|
|
|
</template>
|
|
|
<div ref="processStatsChartRef" style="width: 100%; height: 320px"></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="handleProcessExport" 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 ref="processTableRef" v-loading="loading" :data="processStatsList" border row-key="processId" @expand-change="onProcessRowExpand">
|
|
|
<!-- 工序聚合统计列 -->
|
|
|
<el-table-column label="工序名称" align="center" prop="processName" v-if="columns[0].visible" min-width="160" show-overflow-tooltip />
|
|
|
<el-table-column label="计划数量" align="center" prop="planQty" v-if="columns[1].visible" width="120" />
|
|
|
<el-table-column label="已完成数量" align="center" prop="completedQty" v-if="columns[2].visible" width="120" />
|
|
|
<el-table-column label="未完成数量" align="center" prop="uncompletedQty" v-if="columns[3].visible" width="120" />
|
|
|
<el-table-column label="完成率" align="center" v-if="columns[4].visible" width="160">
|
|
|
<template #default="scope">
|
|
|
<div style="display:flex;align-items:center;justify-content:center;">
|
|
|
<span style="min-width:42px;text-align:right;font-size:12px;color:#606266;">{{ Number(scope.row.completionRateNum || 0) }}%</span>
|
|
|
<el-progress
|
|
|
:percentage="Math.min(Number(scope.row.completionRateNum || 0), 100)"
|
|
|
:color="getProgressColor(Number(scope.row.completionRateNum || 0))"
|
|
|
:stroke-width="8"
|
|
|
:show-text="false"
|
|
|
style="width:90px;margin-left:8px;"
|
|
|
/>
|
|
|
<el-tag v-if="Number(scope.row.completionRateNum || 0) > 100" type="danger" size="small" style="margin-left:8px;">超额 +{{ Number(scope.row.completionRateNum || 0) - 100 }}%</el-tag>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<!-- 展开列:列出该工序下的计划(计划编号) -->
|
|
|
<el-table-column type="expand" width="50">
|
|
|
<template #default="scope">
|
|
|
<el-table :data="buildProcessPlanRows(scope.row)" border size="small" style="width: 100%" v-loading="isProcessLoading(scope.row.processId)" element-loading-text="加载中...">
|
|
|
<el-table-column label="计划编号" prop="planCode" width="140" show-overflow-tooltip />
|
|
|
<el-table-column label="物料编号" prop="materialCode" width="140" show-overflow-tooltip />
|
|
|
<el-table-column label="物料名称" prop="materialName" min-width="160" show-overflow-tooltip />
|
|
|
<el-table-column label="计划数" prop="planAmount" width="100" />
|
|
|
<el-table-column label="已完数" prop="completeAmount" width="100" />
|
|
|
<el-table-column label="剩余数" prop="remainingAmount" width="100" />
|
|
|
<el-table-column label="完成率" width="160">
|
|
|
<template #default="pScope">
|
|
|
<div style="display:flex;align-items:center;justify-content:flex-start;">
|
|
|
<span style="min-width:42px;text-align:right;font-size:12px;color:#606266;">{{ Number(pScope.row.processProgress || 0) }}%</span>
|
|
|
<el-progress
|
|
|
:percentage="Math.min(Number(pScope.row.processProgress || 0), 100)"
|
|
|
:color="getProgressColor(Number(pScope.row.processProgress || 0))"
|
|
|
:stroke-width="6"
|
|
|
:show-text="false"
|
|
|
style="width:100px;margin-left:8px;"
|
|
|
/>
|
|
|
<el-tag v-if="Number(pScope.row.processProgress || 0) > 100" type="danger" size="small" style="margin-left:8px;">超额 +{{ Number(pScope.row.processProgress || 0) - 100 }}%</el-tag>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="状态" width="100">
|
|
|
<template #default="pScope">
|
|
|
<el-tag :type="pScope.row.isCompleted ? 'success' : (pScope.row.isInProgress ? 'warning' : 'info')" size="small">
|
|
|
{{ pScope.row.isCompleted ? '已完' : (pScope.row.isInProgress ? '在制' : '未开') }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="计划编号" align="center" prop="planCode" v-if="false" width="140" />
|
|
|
<el-table-column label="物料编号" align="center" prop="materialCode" v-if="false" width="120" />
|
|
|
<el-table-column label="物料名称" align="center" prop="materialName" v-if="false" width="180" show-overflow-tooltip />
|
|
|
<el-table-column label="规格型号" align="center" prop="materialSpec" v-if="false" width="120" show-overflow-tooltip />
|
|
|
<el-table-column label="计划总数量" align="center" prop="planAmount" v-if="false" width="100" />
|
|
|
<el-table-column label="在制数量" align="center" prop="wipAmount" v-if="false" width="100" />
|
|
|
<el-table-column label="已完成数量" align="center" prop="completeAmount" v-if="false" width="100" />
|
|
|
<el-table-column label="计划开工时间" align="center" prop="planBeginTime" v-if="false" width="150" />
|
|
|
<el-table-column label="实际开工时间" align="center" prop="realBeginTime" v-if="false" width="150" />
|
|
|
<el-table-column label="计划完工时间" align="center" prop="planEndTime" v-if="false" width="150" />
|
|
|
<el-table-column label="当前时间" align="center" prop="currentTime" v-if="false" width="150" />
|
|
|
<el-table-column label="总工序数" align="center" prop="totalProcessCount" v-if="false" width="100" />
|
|
|
<el-table-column label="在制工序" align="center" prop="wipProcesses" v-if="false" width="150" show-overflow-tooltip />
|
|
|
<el-table-column label="剩余工序" align="center" prop="remainingProcesses" v-if="false" width="150" show-overflow-tooltip />
|
|
|
<el-table-column label="整体进度" align="center" prop="overallProgress" v-if="false" 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="false" 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="false" 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';
|
|
|
import { planProcessChildren, processWorkOrderStatsPage, exportProcessWorkOrderStats, processPlanList } from '@/api/mes/prodReport';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
const reportList = ref<WipTrackingReportVO[]>([]);
|
|
|
const processStatsList = ref<any[]>([]);
|
|
|
const loading = ref(true);
|
|
|
const showSearch = ref(true);
|
|
|
const total = ref(0);
|
|
|
const queryFormRef = ref<ElFormInstance>();
|
|
|
const dateRange = ref<string[]>(['', '']);
|
|
|
const processTableRef = ref<any>(null);
|
|
|
|
|
|
// 图表引用
|
|
|
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: true },
|
|
|
{ key: 2, label: '已完成数量', visible: true },
|
|
|
{ key: 3, label: '未完成数量', visible: true },
|
|
|
{ key: 4, label: '完成率', visible: true }
|
|
|
]);
|
|
|
|
|
|
// 计划子节点缓存:key = planCode__processId
|
|
|
const planChildrenCache = ref<Record<string, any[]>>({});
|
|
|
// 工序下计划列表缓存:key = processId
|
|
|
const processPlansCache = ref<Record<string, any[]>>({});
|
|
|
// 工序展开加载状态:key = processId
|
|
|
const processPlansLoading = ref<Record<string, boolean>>({});
|
|
|
function isProcessLoading(pid: any) {
|
|
|
const k = String(pid ?? '');
|
|
|
return !!processPlansLoading.value[k];
|
|
|
}
|
|
|
// 记录已展开的工序ID,搜索后自动恢复展开
|
|
|
const expandedProcessIds = ref<number[]>([]);
|
|
|
function addExpanded(pid: any) {
|
|
|
const id = Number(pid);
|
|
|
if (!expandedProcessIds.value.includes(id)) expandedProcessIds.value.push(id);
|
|
|
}
|
|
|
function removeExpanded(pid: any) {
|
|
|
const id = Number(pid);
|
|
|
expandedProcessIds.value = expandedProcessIds.value.filter(x => x !== id);
|
|
|
}
|
|
|
function reExpandProcessRows() {
|
|
|
nextTick(() => {
|
|
|
expandedProcessIds.value.forEach((pid) => {
|
|
|
const row = (processStatsList.value || []).find((r: any) => String(r?.processId ?? '') === String(pid));
|
|
|
if (row && processTableRef.value?.toggleRowExpansion) {
|
|
|
processTableRef.value.toggleRowExpansion(row, true);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 搜索时需要收起所有已展开的子行,不进行自动恢复
|
|
|
const skipReexpandOnce = ref(false);
|
|
|
function collapseAllProcessRows() {
|
|
|
nextTick(() => {
|
|
|
const rows = processStatsList.value || [];
|
|
|
if (processTableRef.value?.toggleRowExpansion) {
|
|
|
rows.forEach((row: any) => {
|
|
|
processTableRef.value.toggleRowExpansion(row, false);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const queryParams = ref<WipTrackingReportQuery>({
|
|
|
pageNum: 1,
|
|
|
pageSize: 10,
|
|
|
planCode: '',
|
|
|
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;
|
|
|
// 清理缓存,避免筛选条件变化导致子表旧数据残留
|
|
|
processPlansCache.value = {};
|
|
|
planChildrenCache.value = {};
|
|
|
processPlansLoading.value = {};
|
|
|
const baseQuery = { ...queryParams.value };
|
|
|
// 1) 顶层按工序分页
|
|
|
const procReq = processWorkOrderStatsPage(baseQuery);
|
|
|
|
|
|
Promise.all([procReq])
|
|
|
.then(([procRes]: any[]) => {
|
|
|
// 顶层工序聚合分页数据
|
|
|
processStatsList.value = procRes?.rows || [];
|
|
|
total.value = procRes?.total || (processStatsList.value?.length || 0);
|
|
|
nextTick(() => {
|
|
|
updateCharts();
|
|
|
// 根据标记决定是否恢复展开的工序
|
|
|
if (!skipReexpandOnce.value) {
|
|
|
reExpandProcessRows();
|
|
|
}
|
|
|
// 仅跳过一次,后续恢复默认行为
|
|
|
skipReexpandOnce.value = false;
|
|
|
});
|
|
|
loading.value = false;
|
|
|
})
|
|
|
.catch(() => {
|
|
|
loading.value = false;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/** 搜索按钮操作 */
|
|
|
function handleQuery() {
|
|
|
queryParams.value.pageNum = 1;
|
|
|
// 清理缓存,确保加载新筛选条件下的数据
|
|
|
processPlansCache.value = {};
|
|
|
planChildrenCache.value = {};
|
|
|
processPlansLoading.value = {};
|
|
|
// 搜索时收起所有子节点并且不自动恢复展开
|
|
|
expandedProcessIds.value = [];
|
|
|
skipReexpandOnce.value = true;
|
|
|
collapseAllProcessRows();
|
|
|
getList();
|
|
|
}
|
|
|
|
|
|
/** 重置按钮操作 */
|
|
|
function resetQuery() {
|
|
|
dateRange.value = ['', ''];
|
|
|
queryFormRef.value?.resetFields();
|
|
|
queryParams.value = {
|
|
|
pageNum: 1,
|
|
|
pageSize: 10,
|
|
|
planCode: '',
|
|
|
materialCode: '',
|
|
|
materialName: '',
|
|
|
progressStatus: '',
|
|
|
beginDate: '',
|
|
|
endDate: ''
|
|
|
};
|
|
|
handleQuery();
|
|
|
}
|
|
|
|
|
|
/** 导出按钮操作 */
|
|
|
function handleProcessExport() {
|
|
|
proxy?.$modal.confirm('是否确认导出按工序统计数据?').then(() => {
|
|
|
proxy?.$modal.loading('正在导出数据,请稍候...');
|
|
|
return exportProcessWorkOrderStats(queryParams.value);
|
|
|
}).then((response: any) => {
|
|
|
proxy?.$download.blob(response, '工序生产统计.xlsx');
|
|
|
proxy?.$modal.closeLoading();
|
|
|
}).catch(() => {
|
|
|
proxy?.$modal.closeLoading();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/** 获取进度颜色 */
|
|
|
function getProgressColor(percentage: number) {
|
|
|
if (percentage > 100) return '#f56c6c';
|
|
|
if (percentage < 30) return '#f56c6c';
|
|
|
if (percentage < 70) return '#e6a23c';
|
|
|
return '#67c23a';
|
|
|
}
|
|
|
|
|
|
/** 计划状态标签文案 */
|
|
|
function getPlanStatusLabel(status: any) {
|
|
|
const s = String(status ?? '0');
|
|
|
switch (s) {
|
|
|
case '0': return '未派工';
|
|
|
case '1': return '已派工';
|
|
|
case '2': return '进行中';
|
|
|
case '3': return '已完成';
|
|
|
default: return '未知';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 计划状态标签样式 */
|
|
|
function getPlanStatusTagType(status: any) {
|
|
|
const s = String(status ?? '0');
|
|
|
switch (s) {
|
|
|
case '0': return 'info';
|
|
|
case '1': return 'warning';
|
|
|
case '2': return 'primary';
|
|
|
case '3': return 'success';
|
|
|
default: return 'info';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 树形结构配置 */
|
|
|
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 as any).planCode || (row as any).orderCode || '',
|
|
|
label: `${(row as any).planCode || ''}(在制工序(${children.filter(c => c.status === 'in_progress').length}))`,
|
|
|
children
|
|
|
}
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 构建展开区计划列表数据(按工序筛选所有派工计划)
|
|
|
* 从 reportList 的每个订单的 processProgressList 中,筛选出工序名称匹配的项
|
|
|
*/
|
|
|
function buildProcessPlanRows(procRow: any) {
|
|
|
const key = String(procRow?.processId ?? '');
|
|
|
const rows = (key && processPlansCache.value[key]) ? processPlansCache.value[key] : [];
|
|
|
// 进度高的排前(若有字段)
|
|
|
rows.sort((a, b) => Number(b.processProgress || 0) - Number(a.processProgress || 0));
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
/** 外层工序展开时触发:加载该工序下计划列表(不分页) */
|
|
|
async function onProcessRowExpand(row: any, expanded: boolean) {
|
|
|
const key = String(row?.processId ?? '');
|
|
|
// 事件无 expanded 参数时也兼容加载
|
|
|
if (!key) return;
|
|
|
// 同步维护已展开工序集合
|
|
|
if (typeof expanded === 'boolean') {
|
|
|
expanded ? addExpanded(row?.processId) : removeExpanded(row?.processId);
|
|
|
}
|
|
|
if (processPlansCache.value[key] && processPlansCache.value[key].length) return;
|
|
|
// 标记该工序展开区为加载中
|
|
|
processPlansLoading.value[key] = true;
|
|
|
// 组装查询参数
|
|
|
const query: any = {
|
|
|
processId: Number(row?.processId),
|
|
|
beginDate: queryParams.value.beginDate,
|
|
|
endDate: queryParams.value.endDate,
|
|
|
materialCode: queryParams.value.materialCode,
|
|
|
materialName: queryParams.value.materialName,
|
|
|
planCode: queryParams.value.planCode
|
|
|
};
|
|
|
try {
|
|
|
const res: any = await processPlanList(query);
|
|
|
const data = (res?.data || res?.rows || res) || [];
|
|
|
// 规范化并补充子节点的剩余数与完成率
|
|
|
const normalized = (Array.isArray(data) ? data : []).map((it: any) => {
|
|
|
const plan = Number(it?.planAmount ?? it?.planQty ?? it?.planCount ?? 0) || 0;
|
|
|
const completed = Number(it?.completeAmount ?? it?.completedQty ?? it?.completeQty ?? it?.completedCount ?? 0) || 0;
|
|
|
const remaining = Math.max(plan - completed, 0);
|
|
|
const rate = plan > 0 ? Number(((completed * 100.0) / plan).toFixed(2)) : 0;
|
|
|
return {
|
|
|
...it,
|
|
|
planAmount: plan,
|
|
|
completeAmount: completed,
|
|
|
remainingAmount: remaining,
|
|
|
processProgress: rate
|
|
|
};
|
|
|
});
|
|
|
processPlansCache.value[key] = normalized;
|
|
|
} catch (e) {
|
|
|
processPlansCache.value[key] = [];
|
|
|
} finally {
|
|
|
// 结束加载态
|
|
|
processPlansLoading.value[key] = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 生成计划子节点缓存Key */
|
|
|
function getPlanChildKey(row: any) {
|
|
|
return `${row?.planCode || ''}__${row?.processId || ''}`;
|
|
|
}
|
|
|
|
|
|
/** 异步获取计划子节点(按 plan_code + process_id) */
|
|
|
async function fetchPlanChildren(row: any) {
|
|
|
const key = getPlanChildKey(row);
|
|
|
if (planChildrenCache.value[key]) {
|
|
|
return planChildrenCache.value[key];
|
|
|
}
|
|
|
// 参数校验:必须存在有效的 planCode 和数值型 processId
|
|
|
const planCode = row?.planCode;
|
|
|
const processIdNum = Number(row?.processId);
|
|
|
const hasValidProc = !isNaN(processIdNum);
|
|
|
if (!planCode || !hasValidProc) {
|
|
|
planChildrenCache.value[key] = [];
|
|
|
return [];
|
|
|
}
|
|
|
try {
|
|
|
const res: any = await planProcessChildren({ planCode, processId: processIdNum });
|
|
|
const data = (res?.data || res?.rows || res) || [];
|
|
|
planChildrenCache.value[key] = data;
|
|
|
return data;
|
|
|
} catch (e) {
|
|
|
planChildrenCache.value[key] = [];
|
|
|
return [];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 内层计划表展开时触发:加载子节点 */
|
|
|
function onPlanRowExpand(row: any) {
|
|
|
fetchPlanChildren(row);
|
|
|
}
|
|
|
|
|
|
/** 初始化图表 */
|
|
|
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 || !processStatsList.value.length) return;
|
|
|
|
|
|
const sorted = [...processStatsList.value]
|
|
|
.sort((a, b) => (Number(b.planQty || 0) - Number(a.planQty || 0)))
|
|
|
.slice(0, 10);
|
|
|
const processNames = sorted.map((r: any) => r.processName || '未知工序');
|
|
|
const completedData = sorted.map((r: any) => Number(r.completedQty || 0));
|
|
|
const uncompletedData = sorted.map((r: any) => Number(r.uncompletedQty || 0));
|
|
|
const completionRateData = sorted.map((r: any) => Number(r.completionRateNum || 0));
|
|
|
const maxRate = completionRateData.length ? Math.max(...completionRateData) : 0;
|
|
|
const yMax = Math.max(100, Math.ceil(maxRate / 10) * 10);
|
|
|
const showExcessArea = yMax > 100;
|
|
|
|
|
|
const option = {
|
|
|
title: { text: '工序生产统计(Top10)', left: 'center', textStyle: { fontSize: 14 } },
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
legend: { data: ['已完成数量', '未完成数量', '完成率%'], bottom: 0 },
|
|
|
grid: { left: '3%', right: '6%', top: 40, bottom: 60, containLabel: true },
|
|
|
xAxis: { type: 'category', data: processNames },
|
|
|
yAxis: [
|
|
|
{ type: 'value', name: '数量' },
|
|
|
{ type: 'value', name: '完成率%', min: 0, max: yMax, position: 'right', axisLabel: { formatter: '{value}%' } }
|
|
|
],
|
|
|
series: [
|
|
|
{ name: '已完成数量', type: 'bar', data: completedData },
|
|
|
{ name: '未完成数量', type: 'bar', data: uncompletedData },
|
|
|
{ name: '完成率%', type: 'line', yAxisIndex: 1, data: completionRateData, smooth: true,
|
|
|
markArea: showExcessArea ? {
|
|
|
itemStyle: { color: 'rgba(245,108,108,0.12)' },
|
|
|
label: { color: '#f56c6c', position: 'insideTop', formatter: '超额区间' },
|
|
|
data: [ [ { yAxis: 100 }, { yAxis: yMax } ] ]
|
|
|
} : undefined
|
|
|
}
|
|
|
]
|
|
|
};
|
|
|
|
|
|
processStatsChart.setOption(option);
|
|
|
}
|
|
|
|
|
|
/** 合并三张计划表的工序统计结果 */
|
|
|
function buildProcessStatsFromReport(list: any[]) {
|
|
|
const map = new Map<string, { processName: string; planQty: number; completedQty: number; uncompletedQty: number; }>();
|
|
|
(list || []).forEach((order: any) => {
|
|
|
const procs = (order?.processProgressList || []) as any[];
|
|
|
procs.forEach((p: any) => {
|
|
|
const name = String(p?.processName || '未知工序');
|
|
|
const plan = Number(p?.planAmount || 0);
|
|
|
const completed = Number(p?.completeAmount || 0);
|
|
|
const uncompleted = Math.max(plan - completed, 0);
|
|
|
const agg = map.get(name);
|
|
|
if (agg) {
|
|
|
agg.planQty += plan;
|
|
|
agg.completedQty += completed;
|
|
|
agg.uncompletedQty += uncompleted;
|
|
|
} else {
|
|
|
map.set(name, { processName: name, planQty: plan, completedQty: completed, uncompletedQty: uncompleted });
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
return Array.from(map.values()).map((it) => {
|
|
|
const rate = it.planQty > 0 ? Number(((it.completedQty * 100.0) / it.planQty).toFixed(2)) : 0;
|
|
|
return { ...it, completionRateNum: rate, completionRate: `${rate}%` };
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/** 更新工序进度可视化图 */
|
|
|
function updateProcessChart() {
|
|
|
if (!processChart || !reportList.value.length) return;
|
|
|
|
|
|
const processData = reportList.value.slice(0, 10).map(item => ({
|
|
|
name: item.dispatchCode,
|
|
|
value: [
|
|
|
item.dispatchCode,
|
|
|
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>
|
|
|
/* 让子节点数据与工序名同一行,并利用右侧空间 */
|
|
|
.process-tree {
|
|
|
padding: 4px 8px;
|
|
|
}
|
|
|
.process-tree :deep(.el-tree-node__content) {
|
|
|
min-height: 26px;
|
|
|
padding: 2px 6px;
|
|
|
}
|
|
|
.tree-node {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
width: 100%;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
.node-title {
|
|
|
flex: 0 0 auto;
|
|
|
max-width: 240px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
.node-metrics {
|
|
|
flex: 0 0 auto;
|
|
|
color: #606266;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
.node-percent {
|
|
|
flex: 0 0 auto;
|
|
|
color: #909399;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
.node-progress {
|
|
|
flex: 1 1 auto;
|
|
|
min-width: 180px;
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<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: 10px;
|
|
|
}
|
|
|
.tree-node .node-title {
|
|
|
font-size: 12px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
.tree-node .node-metrics {
|
|
|
font-size: 12px;
|
|
|
color: #606266;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
.tree-node .node-percent {
|
|
|
font-size: 12px;
|
|
|
color: #909399;
|
|
|
min-width: 36px;
|
|
|
text-align: right;
|
|
|
}
|
|
|
.node-progress {
|
|
|
width: 90px;
|
|
|
}
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
.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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 树形工序视图样式 */
|
|
|
.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>
|