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.

1097 lines
36 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="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>