feat(mes): 更新配方追溯页面功能和界面优化

- 将查询字段从机台ID改为机台名称,并调整相关数据绑定
- 优化表单布局和输入组件的样式,提升用户体验
- 添加字典标签显示配方状态、称量类型和使用批次选项
- 增加SPC分析中的运行图功能并完善图表展示
- 重构日期范围处理逻辑,统一应用日期筛选条件
- 调整表格列宽配置,优化数据显示效果
- 完善混炼曲线图渲染逻辑,修复潜在的图表初始化问题
- 优化导出功能实现,使用下载工具替代原有方法
- 整理代码结构,移除注释掉的无用分隔线标记
- 添加SPC参数选项的动态计算和标签解析功能
master
zangch@mesnac.com 2 months ago
parent 7ac5b80ea2
commit 9243aeb087

@ -154,6 +154,7 @@ export interface MixTraceSpcResultVO {
export interface MixTraceQuery {
recipeCode?: string;
machineId?: string | number;
machineName?: string;
materialId?: string | number;
materialName?: string;
recipeState?: string;
@ -173,9 +174,11 @@ export interface MixTraceQuery {
*/
export interface SpcQuery {
machineId?: string | number;
machineName?: string;
materialId?: string | number;
recipeCode?: string;
rubTypecode?: string;
rubType?: string;
mixId?: string | number;
termCode?: string;
paramName?: string;

@ -1,33 +1,59 @@
<template>
<div class="p-2">
<el-tabs v-model="activeTab" type="border-card">
<!-- ==================== Tab1: 追溯列表图5 ==================== -->
<el-tab-pane label="配方追溯" name="trace">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<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="traceQueryFormRef" :model="traceQuery" :inline="true">
<el-form-item label="配方编码" prop="recipeCode">
<el-input v-model="traceQuery.recipeCode" placeholder="请输入配方编码" clearable
@keyup.enter="handleTraceQuery" />
<el-input
v-model="traceQuery.recipeCode"
placeholder="请输入配方编码"
clearable
@keyup.enter="handleTraceQuery"
/>
</el-form-item>
<el-form-item label="机台" prop="machineId">
<el-input v-model="traceQuery.machineId" placeholder="请输入机台ID" clearable />
<el-form-item label="机台" prop="machineName">
<el-input
v-model="traceQuery.machineName"
placeholder="请输入机台名称"
clearable
/>
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="traceQuery.materialName" placeholder="请输入物料名称" clearable />
<el-input
v-model="traceQuery.materialName"
placeholder="请输入物料名称"
clearable
/>
</el-form-item>
<el-form-item label="胶种类型" prop="rubType">
<el-input v-model="traceQuery.rubType" placeholder="请输入胶种类型" clearable />
<el-input
v-model="traceQuery.rubType"
placeholder="请输入胶种类型"
clearable
/>
</el-form-item>
<el-form-item label="操作者" prop="operCode">
<el-input v-model="traceQuery.operCode" placeholder="请输入操作者" clearable />
<el-form-item label="操作员" prop="operCode">
<el-input
v-model="traceQuery.operCode"
placeholder="请输入操作员"
clearable
/>
</el-form-item>
<el-form-item label="创建日期" style="width: 380px;">
<el-date-picker v-model="traceDateRange" type="daterange" range-separator="-"
start-placeholder="开始日期" end-placeholder="结束日期"
value-format="YYYY-MM-DD" />
<el-form-item label="创建日期" style="width: 380px">
<el-date-picker
v-model="traceDateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleTraceQuery"></el-button>
@ -42,78 +68,111 @@
<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:mixTrace:export']">导出</el-button>
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['mes:mixTrace:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getTraceList" />
</el-row>
</template>
<el-table v-loading="traceLoading" :data="traceList" @row-click="handleTraceRowClick"
highlight-current-row border stripe>
<el-table
v-loading="traceLoading"
:data="traceList"
@row-click="handleTraceRowClick"
highlight-current-row
border
stripe
>
<el-table-column label="配方编码" prop="recipeCode" min-width="120" />
<el-table-column label="机台名称" prop="machineName" min-width="100" />
<el-table-column label="物料名称" prop="materialName" min-width="120" />
<el-table-column label="胶种类型" prop="rubType" min-width="100" />
<el-table-column label="胶种编码" prop="rubTypecode" min-width="100" />
<el-table-column label="配方重量" prop="totalWeight" min-width="90" align="right" />
<el-table-column label="版本号" prop="edtCode" min-width="70" align="center" />
<el-table-column label="操作者" prop="operCode" min-width="80" />
<el-table-column label="称量步数" prop="weightCount" min-width="80" align="center" />
<el-table-column label="混炼步数" prop="mixingCount" min-width="80" align="center" />
<el-table-column label="配方状态" prop="recipeState" min-width="80" align="center">
<el-table-column label="机台名称" prop="machineName" min-width="120" />
<el-table-column label="物料名称" prop="materialName" min-width="140" />
<el-table-column label="胶种类型" prop="rubType" min-width="110" />
<el-table-column label="配方重量" prop="totalWeight" min-width="100" align="right" />
<el-table-column label="操作员" prop="operCode" min-width="100" />
<el-table-column label="称量步数" prop="weightCount" min-width="90" align="center" />
<el-table-column label="混炼步数" prop="mixingCount" min-width="90" align="center" />
<el-table-column label="配方状态" prop="recipeState" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.recipeState === '1' ? 'success' : 'info'" size="small">
{{ row.recipeState === '1' ? '正用' : '停用' }}
</el-tag>
<dict-tag :options="recipe_state" :value="row.recipeState" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" min-width="160" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<el-table-column label="创建时间" prop="createTime" min-width="170" />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="traceTotal > 0" :total="traceTotal" v-model:page="traceQuery.pageNum"
v-model:limit="traceQuery.pageSize" @pagination="getTraceList" />
<pagination
v-show="traceTotal > 0"
:total="traceTotal"
v-model:page="traceQuery.pageNum"
v-model:limit="traceQuery.pageSize"
@pagination="getTraceList"
/>
</el-card>
</el-tab-pane>
<!-- ==================== Tab2: SPC分析图6/7/8/10 ==================== -->
<el-tab-pane label="SPC分析" name="spc">
<el-card shadow="hover" class="mb-[10px]">
<el-form ref="spcQueryFormRef" :model="spcQuery" :inline="true">
<el-form-item label="机台ID" prop="machineId">
<el-input v-model="spcQuery.machineId" placeholder="请输入机台ID" clearable />
<el-form-item label="机台" prop="machineName">
<el-input
v-model="spcQuery.machineName"
placeholder="请输入机台名称"
clearable
/>
</el-form-item>
<el-form-item label="配方编码" prop="recipeCode">
<el-input v-model="spcQuery.recipeCode" placeholder="请输入配方编码" clearable />
<el-input
v-model="spcQuery.recipeCode"
placeholder="请输入配方编码"
clearable
/>
</el-form-item>
<el-form-item label="胶种编码" prop="rubTypecode">
<el-input v-model="spcQuery.rubTypecode" placeholder="请输入胶种编码" clearable />
<el-form-item label="胶种类型" prop="rubType">
<el-input
v-model="spcQuery.rubType"
placeholder="请输入胶种类型"
clearable
/>
</el-form-item>
<el-form-item label="工步号" prop="mixId">
<el-input v-model="spcQuery.mixId" placeholder="工步号" clearable style="width: 100px;" />
<el-input
v-model="spcQuery.mixId"
placeholder="请输入工步号"
clearable
style="width: 120px"
/>
</el-form-item>
<el-form-item label="分析参数" prop="paramName">
<el-select v-model="spcQuery.paramName" placeholder="选择分析参数" style="width: 140px;">
<el-option label="混炼温度" value="mixingTemp" />
<el-option label="混炼时间" value="mixingTime" />
<el-option label="混炼能量" value="mixingEnergy" />
<el-option label="混炼功率" value="mixingPower" />
<el-option label="混炼压力" value="mixingPress" />
<el-option label="混炼转速" value="mixingSpeed" />
<el-select v-model="spcQuery.paramName" placeholder="选择分析参数" style="width: 150px">
<el-option
v-for="item in spcParamOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="子组大小" prop="subgroupSize">
<el-input-number v-model="spcQuery.subgroupSize" :min="2" :max="10" style="width: 120px;" />
<el-input-number v-model="spcQuery.subgroupSize" :min="2" :max="10" style="width: 120px" />
</el-form-item>
<el-form-item label="日期范围" style="width: 380px;">
<el-date-picker v-model="spcDateRange" type="daterange" range-separator="-"
start-placeholder="开始" end-placeholder="结束"
value-format="YYYY-MM-DD" />
<el-form-item label="日期范围" style="width: 380px">
<el-date-picker
v-model="spcDateRange"
type="daterange"
range-separator="-"
start-placeholder="开始"
end-placeholder="结束"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSpcQuery"></el-button>
@ -122,7 +181,6 @@
</el-form>
</el-card>
<!-- SPC 统计指标卡片 -->
<el-row :gutter="10" class="mb-[10px]" v-if="spcResult.sampleCount > 0">
<el-col :span="3" v-for="item in spcIndicators" :key="item.label">
<el-card shadow="hover" class="text-center">
@ -134,44 +192,43 @@
</el-col>
</el-row>
<!-- SPC 图表区域 -->
<el-row :gutter="10" class="mb-[10px]" v-if="spcResult.sampleCount > 0">
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>能力分析图直方图</span></template>
<div ref="histogramChartRef" style="height: 350px;" />
<div ref="histogramChartRef" style="height: 350px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>运行图</span></template>
<div ref="runChartRef" style="height: 350px;" />
<div ref="runChartRef" style="height: 350px" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" class="mb-[10px]" v-if="xbarRResult.sampleCount > 0">
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>Xbar 均值图</span></template>
<div ref="xbarChartRef" style="height: 350px;" />
<div ref="xbarChartRef" style="height: 350px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>R 极差图</span></template>
<div ref="rChartRef" style="height: 350px;" />
<div ref="rChartRef" style="height: 350px" />
</el-card>
</el-col>
</el-row>
<!-- SPC 样本列表 -->
<el-card shadow="hover" v-if="spcSampleList.length > 0">
<template #header><span>SPC样本明细</span></template>
<el-table :data="spcSampleList" border stripe max-height="400">
<el-table-column label="配方编码" prop="recipeCode" min-width="120" />
<el-table-column label="机台" prop="machineName" min-width="100" />
<el-table-column label="机台" prop="machineName" min-width="110" />
<el-table-column label="工步号" prop="mixId" width="80" align="center" />
<el-table-column label="工步编码" prop="termCode" min-width="90" />
<el-table-column label="工步" prop="termCode" min-width="120" />
<el-table-column label="实测温度" prop="mixingTemp" width="90" align="right" />
<el-table-column label="设定温度" prop="setTemp" width="90" align="right" />
<el-table-column label="实测时间" prop="mixingTime" width="90" align="right" />
@ -180,83 +237,83 @@
<el-table-column label="实测功率" prop="mixingPower" width="90" align="right" />
<el-table-column label="实测压力" prop="mixingPress" width="90" align="right" />
<el-table-column label="实测转速" prop="mixingSpeed" width="90" align="right" />
<el-table-column label="生产时间" prop="createTime" min-width="160" />
<el-table-column label="生产时间" prop="createTime" min-width="170" />
</el-table>
<pagination v-show="spcSampleTotal > 0" :total="spcSampleTotal"
v-model:page="spcQuery.pageNum" v-model:limit="spcQuery.pageSize"
@pagination="getSpcSamples" />
<pagination
v-show="spcSampleTotal > 0"
:total="spcSampleTotal"
v-model:page="spcQuery.pageNum"
v-model:limit="spcQuery.pageSize"
@pagination="getSpcSamples"
/>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- ==================== 追溯详情抽屉图9 ==================== -->
<el-drawer v-model="detailVisible" title="配方追溯详情" size="70%" direction="rtl" destroy-on-close>
<template v-if="detailData">
<!-- 基础信息 -->
<el-descriptions title="基础信息" :column="3" border class="mb-4">
<el-descriptions-item label="配方编码">{{ detailData.recipeInfo?.recipeCode }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ detailData.recipeInfo?.machineName }}</el-descriptions-item>
<el-descriptions-item label="物料">{{ detailData.recipeInfo?.materialName }}</el-descriptions-item>
<el-descriptions-item label="胶种类型">{{ detailData.recipeInfo?.rubType }}</el-descriptions-item>
<el-descriptions-item label="胶种编码">{{ detailData.recipeInfo?.rubTypecode }}</el-descriptions-item>
<el-descriptions-item label="版本号">{{ detailData.recipeInfo?.edtCode }}</el-descriptions-item>
<el-descriptions-item label="配方重量">{{ detailData.recipeInfo?.totalWeight }}</el-descriptions-item>
<el-descriptions-item label="填充系数">{{ detailData.recipeInfo?.fillCoefficient }}</el-descriptions-item>
<el-descriptions-item label="操作">{{ detailData.recipeInfo?.operCode }}</el-descriptions-item>
<el-descriptions-item label="操作">{{ detailData.recipeInfo?.operCode }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ detailData.recipeInfo?.doneTime }}</el-descriptions-item>
<el-descriptions-item label="配方状态">
<el-tag :type="detailData.recipeInfo?.recipeState === '1' ? 'success' : 'info'" size="small">
{{ detailData.recipeInfo?.recipeState === '1' ? '正用' : '停用' }}
</el-tag>
<dict-tag :options="recipe_state" :value="detailData.recipeInfo?.recipeState" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.recipeInfo?.createTime }}</el-descriptions-item>
</el-descriptions>
<!-- 称量明细 -->
<div class="mb-4">
<h4 class="mb-2 font-bold">称量明细{{ detailData.weightList?.length || 0 }}</h4>
<el-table :data="detailData.weightList" border stripe max-height="300" size="small">
<el-table-column label="序号" prop="weightSeq" width="60" align="center" />
<el-table-column label="称量类型" prop="weightType" width="90" />
<el-table-column label="动作编码" prop="actCode" min-width="90" />
<el-table-column label="设定重量" prop="setWeight" width="90" align="right" />
<el-table-column label="误差允许" prop="errorAllow" width="90" align="right" />
<el-table-column label="秤编码" prop="scaleCode" width="80" />
<el-table-column label="父级编码" prop="fatherCode" width="90" />
<el-table-column label="子级编码" prop="childCode" width="90" />
<el-table-column label="使用批次" prop="ifUseBat" width="80" align="center" />
<el-table-column label="最大比例" prop="maxRate" width="80" />
<el-table-column label="称量类型" prop="weightType" width="100">
<template #default="{ row }">
<dict-tag :options="weight_type" :value="row.weightType" />
</template>
</el-table-column>
<el-table-column label="动作" prop="actCode" min-width="120" />
<el-table-column label="设定重量" prop="setWeight" width="100" align="right" />
<el-table-column label="误差允许" prop="errorAllow" width="100" align="right" />
<el-table-column label="使用批次" prop="ifUseBat" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="sys_yes_no" :value="row.ifUseBat" />
</template>
</el-table-column>
<el-table-column label="最大比例" prop="maxRate" width="90" />
</el-table>
</div>
<!-- 混炼明细 -->
<div class="mb-4">
<h4 class="mb-2 font-bold">混炼明细{{ detailData.mixingList?.length || 0 }}</h4>
<el-table :data="detailData.mixingList" border stripe max-height="400" size="small">
<el-table-column label="步号" prop="mixId" width="60" align="center" />
<el-table-column label="工步编码" prop="termCode" min-width="80" />
<el-table-column label="条件编码" prop="condCode" width="80" />
<el-table-column label="动作编码" prop="actCode" width="80" />
<el-table-column label="设定时间" prop="setTime" width="80" align="right" />
<el-table-column label="实测时间" prop="mixingTime" width="80" align="right" />
<el-table-column label="设定温度" prop="setTemp" width="80" align="right" />
<el-table-column label="实测温度" prop="mixingTemp" width="80" align="right" />
<el-table-column label="设定能量" prop="setEnergy" width="80" align="right" />
<el-table-column label="实测能量" prop="mixingEnergy" width="80" align="right" />
<el-table-column label="设定功率" prop="setPower" width="80" align="right" />
<el-table-column label="实测功率" prop="mixingPower" width="80" align="right" />
<el-table-column label="设定压力" prop="setPres" width="80" align="right" />
<el-table-column label="实测压力" prop="mixingPress" width="80" align="right" />
<el-table-column label="设定转速" prop="setRota" width="80" align="right" />
<el-table-column label="实测转速" prop="mixingSpeed" width="80" align="right" />
<el-table-column label="工步" prop="termCode" min-width="120" />
<el-table-column label="条件" prop="condCode" min-width="120" />
<el-table-column label="动作" prop="actCode" min-width="120" />
<el-table-column label="设定时间" prop="setTime" width="90" align="right" />
<el-table-column label="实测时间" prop="mixingTime" width="90" align="right" />
<el-table-column label="设定温度" prop="setTemp" width="90" align="right" />
<el-table-column label="实测温度" prop="mixingTemp" width="90" align="right" />
<el-table-column label="设定能量" prop="setEnergy" width="90" align="right" />
<el-table-column label="实测能量" prop="mixingEnergy" width="90" align="right" />
<el-table-column label="设定功率" prop="setPower" width="90" align="right" />
<el-table-column label="实测功率" prop="mixingPower" width="90" align="right" />
<el-table-column label="设定压力" prop="setPres" width="90" align="right" />
<el-table-column label="实测压力" prop="mixingPress" width="90" align="right" />
<el-table-column label="设定转速" prop="setRota" width="90" align="right" />
<el-table-column label="实测转速" prop="mixingSpeed" width="90" align="right" />
</el-table>
</div>
<!-- 混炼曲线图图4 -->
<div class="mb-4" v-if="detailData.mixingList && detailData.mixingList.length > 0">
<h4 class="mb-2 font-bold">密炼工作曲线</h4>
<el-card shadow="hover">
<div ref="detailCurveChartRef" style="height: 350px;" />
<div ref="detailCurveChartRef" style="height: 350px" />
</el-card>
</div>
</template>
@ -265,26 +322,33 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, watch } from 'vue';
import { getCurrentInstance } from 'vue';
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import * as echarts from 'echarts';
import {
listMixTrace, exportMixTrace, getMixTraceDetail,
listSpcSamples, getSpcCapability, getSpcXbarR
getMixTraceDetail,
getSpcCapability,
getSpcRunChart,
getSpcXbarR,
listMixTrace,
listSpcSamples
} from '@/api/mes/mixTrace';
import type {
MixTraceListVO, MixTraceDetailVO, MixTraceSpcSampleVO, MixTraceSpcResultVO
MixTraceDetailVO,
MixTraceListVO,
MixTraceSpcResultVO,
MixTraceSpcSampleVO
} from '@/api/mes/mixTrace/types';
import { download } from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { recipe_state, weight_type, mix_trace_spc_param, sys_yes_no } = toRefs<any>(
proxy?.useDict('recipe_state', 'weight_type', 'mix_trace_spc_param', 'sys_yes_no')
);
// ==================== ====================
const activeTab = ref('trace');
const showSearch = ref(true);
// ==================== 5 ====================
const traceLoading = ref(false);
const traceList = ref<MixTraceListVO[]>([]);
const traceTotal = ref(0);
@ -292,7 +356,7 @@ const traceDateRange = ref<string[]>([]);
const traceQueryFormRef = ref();
const traceQuery = reactive<any>({
recipeCode: undefined,
machineId: undefined,
machineName: undefined,
materialName: undefined,
rubType: undefined,
operCode: undefined,
@ -302,15 +366,19 @@ const traceQuery = reactive<any>({
pageSize: 20
});
const getTraceList = async () => {
traceLoading.value = true;
if (traceDateRange.value && traceDateRange.value.length === 2) {
const applyTraceDateRange = () => {
if (traceDateRange.value?.length === 2) {
traceQuery.beginDate = traceDateRange.value[0];
traceQuery.endDate = traceDateRange.value[1];
} else {
traceQuery.beginDate = undefined;
traceQuery.endDate = undefined;
return;
}
traceQuery.beginDate = undefined;
traceQuery.endDate = undefined;
};
const getTraceList = async () => {
traceLoading.value = true;
applyTraceDateRange();
try {
const res = await listMixTrace(traceQuery);
traceList.value = res.rows || [];
@ -334,18 +402,14 @@ const resetTraceQuery = () => {
};
const handleExport = () => {
if (traceDateRange.value && traceDateRange.value.length === 2) {
traceQuery.beginDate = traceDateRange.value[0];
traceQuery.endDate = traceDateRange.value[1];
}
exportMixTrace(traceQuery);
applyTraceDateRange();
download('/mes/mixTrace/export', { ...traceQuery }, `mix_trace_${Date.now()}.xlsx`);
};
const handleTraceRowClick = (row: MixTraceListVO) => {
openDetail(row);
};
// ==================== 9 ====================
const detailVisible = ref(false);
const detailData = ref<MixTraceDetailVO | null>(null);
const detailCurveChartRef = ref<HTMLElement>();
@ -359,13 +423,17 @@ const openDetail = async (row: MixTraceListVO) => {
renderDetailCurveChart();
};
/** 渲染密炼工作曲线图4 */
const renderDetailCurveChart = () => {
if (!detailCurveChartRef.value || !detailData.value?.mixingList?.length) return;
if (detailCurveChart) detailCurveChart.dispose();
if (!detailCurveChartRef.value || !detailData.value?.mixingList?.length) {
return;
}
if (detailCurveChart) {
detailCurveChart.dispose();
}
detailCurveChart = echarts.init(detailCurveChartRef.value);
const mixList = detailData.value.mixingList;
const xData = mixList.map(m => '步' + m.mixId);
const xData = mixList.map((m) => `${m.mixId}`);
detailCurveChart.setOption({
tooltip: { trigger: 'axis' },
@ -377,29 +445,47 @@ const renderDetailCurveChart = () => {
{ type: 'value', name: '时间/转速', position: 'right' }
],
series: [
{ name: '实测温度', type: 'line', data: mixList.map(m => m.mixingTemp), smooth: true },
{ name: '设定温度', type: 'line', data: mixList.map(m => m.setTemp), lineStyle: { type: 'dashed' } },
{ name: '实测能量', type: 'line', data: mixList.map(m => m.mixingEnergy), smooth: true },
{ name: '实测功率', type: 'line', data: mixList.map(m => m.mixingPower), smooth: true },
{ name: '实测转速', type: 'line', yAxisIndex: 1, data: mixList.map(m => m.mixingSpeed), smooth: true },
{ name: '实测时间', type: 'bar', yAxisIndex: 1, data: mixList.map(m => m.mixingTime), barWidth: 15, opacity: 0.4 }
{ name: '实测温度', type: 'line', data: mixList.map((m) => m.mixingTemp), smooth: true },
{
name: '设定温度',
type: 'line',
data: mixList.map((m) => m.setTemp),
lineStyle: { type: 'dashed' }
},
{ name: '实测能量', type: 'line', data: mixList.map((m) => m.mixingEnergy), smooth: true },
{ name: '实测功率', type: 'line', data: mixList.map((m) => m.mixingPower), smooth: true },
{
name: '实测转速',
type: 'line',
yAxisIndex: 1,
data: mixList.map((m) => m.mixingSpeed),
smooth: true
},
{
name: '实测时间',
type: 'bar',
yAxisIndex: 1,
data: mixList.map((m) => m.mixingTime),
barWidth: 15,
opacity: 0.4
}
]
});
};
// ==================== SPC6/7/8/10 ====================
const spcSampleList = ref<MixTraceSpcSampleVO[]>([]);
const spcSampleTotal = ref(0);
const spcResult = ref<MixTraceSpcResultVO>({ sampleCount: 0 } as MixTraceSpcResultVO);
const runChartResult = ref<MixTraceSpcResultVO>({ sampleCount: 0 } as MixTraceSpcResultVO);
const xbarRResult = ref<MixTraceSpcResultVO>({ sampleCount: 0 } as MixTraceSpcResultVO);
const spcDateRange = ref<string[]>([]);
const spcQueryFormRef = ref();
const spcQuery = reactive<any>({
machineId: undefined,
machineName: undefined,
recipeCode: undefined,
rubTypecode: undefined,
rubType: undefined,
mixId: undefined,
paramName: 'mixingTemp',
paramName: undefined,
subgroupSize: 5,
beginDate: undefined,
endDate: undefined,
@ -416,10 +502,13 @@ let runChart: echarts.ECharts | null = null;
let xbarChart: echarts.ECharts | null = null;
let rChart: echarts.ECharts | null = null;
/** SPC 统计指标卡片 */
const spcParamOptions = computed(() => mix_trace_spc_param.value || []);
const spcIndicators = computed(() => {
const r = spcResult.value;
if (!r || !r.sampleCount) return [];
if (!r || !r.sampleCount) {
return [];
}
return [
{ label: '样本数', value: r.sampleCount, color: '#409eff' },
{ label: '均值', value: r.mean?.toFixed(2) ?? '-' },
@ -432,182 +521,267 @@ const spcIndicators = computed(() => {
];
});
const resolveSpcParamLabel = (paramName?: string) => {
if (!paramName) {
return '';
}
const match = spcParamOptions.value.find((item: any) => String(item.value) === String(paramName));
return match?.label || paramName;
};
const cpkColor = (v: number | null | undefined) => {
if (v == null) return '#999';
if (v >= 1.33) return '#67c23a';
if (v >= 1.0) return '#e6a23c';
if (v == null) {
return '#999';
}
if (v >= 1.33) {
return '#67c23a';
}
if (v >= 1.0) {
return '#e6a23c';
}
return '#f56c6c';
};
const buildSpcParams = () => {
if (spcDateRange.value && spcDateRange.value.length === 2) {
const applySpcDateRange = () => {
if (spcDateRange.value?.length === 2) {
spcQuery.beginDate = spcDateRange.value[0];
spcQuery.endDate = spcDateRange.value[1];
} else {
spcQuery.beginDate = undefined;
spcQuery.endDate = undefined;
return;
}
spcQuery.beginDate = undefined;
spcQuery.endDate = undefined;
};
const handleSpcQuery = async () => {
buildSpcParams();
await Promise.all([getSpcSamples(), fetchSpcCapability(), fetchSpcXbarR()]);
applySpcDateRange();
await Promise.all([getSpcSamples(), fetchSpcCapability(), fetchSpcRunChart(), fetchSpcXbarR()]);
};
const resetSpcQuery = () => {
spcQueryFormRef.value?.resetFields();
spcDateRange.value = [];
spcQuery.paramName = 'mixingTemp';
spcQuery.paramName = undefined;
spcQuery.subgroupSize = 5;
spcQuery.pageNum = 1;
};
const getSpcSamples = async () => {
buildSpcParams();
applySpcDateRange();
const res = await listSpcSamples(spcQuery);
spcSampleList.value = res.rows || [];
spcSampleTotal.value = res.total || 0;
};
const fetchSpcCapability = async () => {
buildSpcParams();
applySpcDateRange();
const res = await getSpcCapability(spcQuery);
spcResult.value = res.data || { sampleCount: 0 } as MixTraceSpcResultVO;
spcResult.value = res.data || ({ sampleCount: 0 } as MixTraceSpcResultVO);
await nextTick();
renderHistogramChart();
};
const fetchSpcRunChart = async () => {
applySpcDateRange();
const res = await getSpcRunChart(spcQuery);
runChartResult.value = res.data || ({ sampleCount: 0 } as MixTraceSpcResultVO);
await nextTick();
renderRunChart();
};
const fetchSpcXbarR = async () => {
buildSpcParams();
applySpcDateRange();
const res = await getSpcXbarR(spcQuery);
xbarRResult.value = res.data || { sampleCount: 0 } as MixTraceSpcResultVO;
xbarRResult.value = res.data || ({ sampleCount: 0 } as MixTraceSpcResultVO);
await nextTick();
renderXbarChart();
renderRChart();
};
/** 直方图图7 */
const renderHistogramChart = () => {
if (!histogramChartRef.value) return;
if (!histogramChartRef.value) {
return;
}
const r = spcResult.value;
if (!r.histogramBins?.length) return;
if (histogramChart) histogramChart.dispose();
if (!r.histogramBins?.length) {
return;
}
if (histogramChart) {
histogramChart.dispose();
}
histogramChart = echarts.init(histogramChartRef.value);
histogramChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 20, bottom: 60, top: 30 },
xAxis: { type: 'category', data: r.histogramBins, axisLabel: { rotate: 30, fontSize: 10 } },
yAxis: { type: 'value', name: '频次' },
series: [{
type: 'bar', data: r.histogramCounts,
itemStyle: { color: '#409eff' },
barWidth: '70%'
}],
graphic: [{
type: 'text',
left: 'center', bottom: 5,
style: {
text: `Cp=${r.cp?.toFixed(3) ?? '-'} Cpk=${r.cpk?.toFixed(3) ?? '-'} Ppk=${r.ppk?.toFixed(3) ?? '-'} σ=${r.sigma?.toFixed(4) ?? '-'}`,
fontSize: 12, fill: '#666'
series: [
{
type: 'bar',
data: r.histogramCounts,
itemStyle: { color: '#409eff' },
barWidth: '70%'
}
}]
],
graphic: [
{
type: 'text',
left: 'center',
bottom: 5,
style: {
text: `Cp=${r.cp?.toFixed(3) ?? '-'} Cpk=${r.cpk?.toFixed(3) ?? '-'} Ppk=${r.ppk?.toFixed(3) ?? '-'} Sigma=${r.sigma?.toFixed(4) ?? '-'}`,
fontSize: 12,
fill: '#666'
}
}
]
});
};
/** 运行图图8 */
const renderRunChart = () => {
if (!runChartRef.value) return;
const r = spcResult.value;
if (!r.sampleValues?.length) return;
if (runChart) runChart.dispose();
if (!runChartRef.value) {
return;
}
const r = runChartResult.value;
if (!r.sampleValues?.length) {
return;
}
if (runChart) {
runChart.dispose();
}
runChart = echarts.init(runChartRef.value);
const markLines: any[] = [];
if (r.usl != null) markLines.push({ yAxis: r.usl, name: 'USL', lineStyle: { color: '#f56c6c', type: 'dashed' } });
if (r.lsl != null) markLines.push({ yAxis: r.lsl, name: 'LSL', lineStyle: { color: '#f56c6c', type: 'dashed' } });
if (r.target != null) markLines.push({ yAxis: r.target, name: 'Target', lineStyle: { color: '#67c23a', type: 'solid' } });
if (r.mean != null) markLines.push({ yAxis: r.mean, name: 'Mean', lineStyle: { color: '#409eff', type: 'dotted' } });
if (r.usl != null) {
markLines.push({ yAxis: r.usl, name: 'USL', lineStyle: { color: '#f56c6c', type: 'dashed' } });
}
if (r.lsl != null) {
markLines.push({ yAxis: r.lsl, name: 'LSL', lineStyle: { color: '#f56c6c', type: 'dashed' } });
}
if (r.target != null) {
markLines.push({ yAxis: r.target, name: 'Target', lineStyle: { color: '#67c23a', type: 'solid' } });
}
if (r.mean != null) {
markLines.push({ yAxis: r.mean, name: 'Mean', lineStyle: { color: '#409eff', type: 'dotted' } });
}
runChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, bottom: 30, top: 20 },
xAxis: { type: 'category', data: r.sampleLabels || r.sampleValues.map((_, i) => i + 1) },
yAxis: { type: 'value', name: r.paramLabel || '' },
series: [{
type: 'line', data: r.sampleValues, smooth: false,
symbol: 'circle', symbolSize: 4,
markLine: { silent: true, data: markLines, label: { position: 'end', fontSize: 10 } }
}]
xAxis: {
type: 'category',
data: r.sampleLabels || r.sampleValues.map((_, i) => i + 1)
},
yAxis: {
type: 'value',
name: resolveSpcParamLabel(r.paramName || spcQuery.paramName)
},
series: [
{
type: 'line',
data: r.sampleValues,
smooth: false,
symbol: 'circle',
symbolSize: 4,
markLine: { silent: true, data: markLines, label: { position: 'end', fontSize: 10 } }
}
]
});
};
/** Xbar 均值图 */
const renderXbarChart = () => {
if (!xbarChartRef.value) return;
if (!xbarChartRef.value) {
return;
}
const r = xbarRResult.value;
if (!r.xbarValues?.length) return;
if (xbarChart) xbarChart.dispose();
if (!r.xbarValues?.length) {
return;
}
if (xbarChart) {
xbarChart.dispose();
}
xbarChart = echarts.init(xbarChartRef.value);
xbarChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, bottom: 30, top: 20 },
xAxis: { type: 'category', data: r.subgroupLabels },
yAxis: { type: 'value', name: 'Xbar' },
series: [{
type: 'line', data: r.xbarValues, symbol: 'circle', symbolSize: 5,
markLine: {
silent: true,
label: { position: 'end', fontSize: 10 },
data: [
{ yAxis: r.uclX, name: 'UCL', lineStyle: { color: '#f56c6c', type: 'dashed' } },
{ yAxis: r.clX, name: 'CL', lineStyle: { color: '#67c23a' } },
{ yAxis: r.lclX, name: 'LCL', lineStyle: { color: '#f56c6c', type: 'dashed' } }
]
series: [
{
type: 'line',
data: r.xbarValues,
symbol: 'circle',
symbolSize: 5,
markLine: {
silent: true,
label: { position: 'end', fontSize: 10 },
data: [
{ yAxis: r.uclX, name: 'UCL', lineStyle: { color: '#f56c6c', type: 'dashed' } },
{ yAxis: r.clX, name: 'CL', lineStyle: { color: '#67c23a' } },
{ yAxis: r.lclX, name: 'LCL', lineStyle: { color: '#f56c6c', type: 'dashed' } }
]
}
}
}]
]
});
};
/** R 极差图 */
const renderRChart = () => {
if (!rChartRef.value) return;
if (!rChartRef.value) {
return;
}
const r = xbarRResult.value;
if (!r.rValues?.length) return;
if (rChart) rChart.dispose();
if (!r.rValues?.length) {
return;
}
if (rChart) {
rChart.dispose();
}
rChart = echarts.init(rChartRef.value);
rChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, bottom: 30, top: 20 },
xAxis: { type: 'category', data: r.subgroupLabels },
yAxis: { type: 'value', name: 'R' },
series: [{
type: 'line', data: r.rValues, symbol: 'circle', symbolSize: 5,
markLine: {
silent: true,
label: { position: 'end', fontSize: 10 },
data: [
{ yAxis: r.uclR, name: 'UCL', lineStyle: { color: '#f56c6c', type: 'dashed' } },
{ yAxis: r.clR, name: 'CL', lineStyle: { color: '#67c23a' } },
{ yAxis: r.lclR, name: 'LCL', lineStyle: { color: '#f56c6c', type: 'dashed' } }
]
series: [
{
type: 'line',
data: r.rValues,
symbol: 'circle',
symbolSize: 5,
markLine: {
silent: true,
label: { position: 'end', fontSize: 10 },
data: [
{ yAxis: r.uclR, name: 'UCL', lineStyle: { color: '#f56c6c', type: 'dashed' } },
{ yAxis: r.clR, name: 'CL', lineStyle: { color: '#67c23a' } },
{ yAxis: r.lclR, name: 'LCL', lineStyle: { color: '#f56c6c', type: 'dashed' } }
]
}
}
}]
]
});
};
// ==================== ====================
onMounted(() => {
getTraceList();
});
/** 监听窗口变化自适应图表 */
window.addEventListener('resize', () => {
const handleResize = () => {
histogramChart?.resize();
runChart?.resize();
xbarChart?.resize();
rChart?.resize();
detailCurveChart?.resize();
};
onMounted(() => {
getTraceList();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
histogramChart?.dispose();
runChart?.dispose();
xbarChart?.dispose();
rChart?.dispose();
detailCurveChart?.dispose();
});
</script>

Loading…
Cancel
Save