feat(mes): 初始化密炼追溯报表功能和SPC分析功能

- 新增配方追溯列表页面,支持按配方编码、机台、物料等条件查询
- 实现追溯详情查看功能,展示配方基础信息、称量明细和混炼明细
- 集成密炼工作曲线图表显示混炼过程数据
- 添加SPC统计分析功能,包括能力分析图、运行图、Xbar均值图和R极差图
- 实现SPC样本数据列表展示和统计指标卡片显示
- 新增追溯数据导出功能和搜索重置功能
- 创建API接口文件封装追溯和SPC相关请求方法
- 定义完整的类型定义文件包含所有追溯和SPC数据结构
master
zangch@mesnac.com 1 day ago
parent a60eeabac8
commit 7ac5b80ea2

@ -0,0 +1,87 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import {
MixTraceListVO,
MixTraceDetailVO,
MixTraceSpcSampleVO,
MixTraceSpcResultVO,
MixTraceQuery,
SpcQuery
} from './types';
/**
* 5-
*/
export const listMixTrace = (query?: MixTraceQuery): AxiosPromise<MixTraceListVO[]> => {
return request({
url: '/mes/mixTrace/list',
method: 'get',
params: query
});
};
/**
*
*/
export const exportMixTrace = (query?: MixTraceQuery): AxiosPromise<any> => {
return request({
url: '/mes/mixTrace/export',
method: 'post',
params: query,
responseType: 'blob'
});
};
/**
* 9
*/
export const getMixTraceDetail = (recipeId: string | number): AxiosPromise<MixTraceDetailVO> => {
return request({
url: '/mes/mixTrace/detail/' + recipeId,
method: 'get'
});
};
/**
* SPC6-
*/
export const listSpcSamples = (query?: SpcQuery): AxiosPromise<MixTraceSpcSampleVO[]> => {
return request({
url: '/mes/mixTrace/spc/samples',
method: 'get',
params: query
});
};
/**
* SPC7
*/
export const getSpcCapability = (query?: SpcQuery): AxiosPromise<MixTraceSpcResultVO> => {
return request({
url: '/mes/mixTrace/spc/capability',
method: 'get',
params: query
});
};
/**
* SPC8
*/
export const getSpcRunChart = (query?: SpcQuery): AxiosPromise<MixTraceSpcResultVO> => {
return request({
url: '/mes/mixTrace/spc/runChart',
method: 'get',
params: query
});
};
/**
* SPC10
*/
export const getSpcXbarR = (query?: SpcQuery): AxiosPromise<MixTraceSpcResultVO> => {
return request({
url: '/mes/mixTrace/spc/xbarR',
method: 'get',
params: query
});
};

@ -0,0 +1,187 @@
/**
* VO5
*/
export interface MixTraceListVO {
recipeId: string | number;
recipeCode: string;
machineId: string | number;
machineName: string;
materialId: string | number;
materialName: string;
edtCode: number;
userEdtCode: string;
recipeState: string;
recipeType: number;
recipeTypecode: string;
rubType: string;
rubTypecode: string;
totalWeight: number;
fillCoefficient: number;
operCode: string;
auditFlag: string;
doneTime: number;
createTime: string;
weightCount: number;
mixingCount: number;
}
/**
* VO9
*/
export interface MixTraceDetailVO {
recipeInfo: MixTraceListVO;
weightList: RecipeWeightItem[];
mixingList: RecipeMixingItem[];
}
/**
*
*/
export interface RecipeWeightItem {
weightId: string | number;
recipeId: string | number;
weightSeq: number;
machineId: string | number;
edtCode: number;
weightType: string;
scaleCode: number;
actCode: string;
setWeight: number;
errorAllow: number;
fatherCode: string | number;
unitId: string | number;
childCode: number;
ifUseBat: string;
maxRate: string;
}
/**
*
*/
export interface RecipeMixingItem {
mixingId: string | number;
recipeId: string | number;
mixId: string | number;
machineId: string | number;
edtCode: number;
condCode: string;
mixingTime: number;
mixingTemp: number;
mixingEnergy: number;
mixingPower: number;
mixingPress: number;
mixingSpeed: number;
actCode: string;
fatherCode: string;
childCode: string;
termCode: string;
setTime: number;
setTemp: number;
setEnergy: number;
setPower: number;
setPres: number;
setRota: number;
}
/**
* SPCVO6
*/
export interface MixTraceSpcSampleVO {
recipeId: string | number;
recipeCode: string;
machineId: string | number;
machineName: string;
mixId: number;
termCode: string;
actCode: string;
condCode: string;
mixingTime: number;
mixingTemp: number;
mixingEnergy: number;
mixingPower: number;
mixingPress: number;
mixingSpeed: number;
setTime: number;
setTemp: number;
setEnergy: number;
setPower: number;
setPres: number;
setRota: number;
createTime: string;
}
/**
* SPCVO7/8/10
*/
export interface MixTraceSpcResultVO {
paramName: string;
paramLabel: string;
sampleCount: number;
mean: number;
sigma: number;
minValue: number;
maxValue: number;
usl: number;
lsl: number;
target: number;
cp: number;
cpk: number;
cpu: number;
cpl: number;
pp: number;
ppk: number;
sampleValues: number[];
sampleLabels: string[];
subgroupSize: number;
xbarValues: number[];
rValues: number[];
subgroupLabels: string[];
xbarbar: number;
rbar: number;
uclX: number;
clX: number;
lclX: number;
uclR: number;
clR: number;
lclR: number;
histogramCounts: number[];
histogramBins: string[];
}
/**
*
*/
export interface MixTraceQuery {
recipeCode?: string;
machineId?: string | number;
materialId?: string | number;
materialName?: string;
recipeState?: string;
rubType?: string;
rubTypecode?: string;
recipeTypecode?: string;
operCode?: string;
auditFlag?: string;
beginDate?: string;
endDate?: string;
pageNum?: number;
pageSize?: number;
}
/**
* SPC
*/
export interface SpcQuery {
machineId?: string | number;
materialId?: string | number;
recipeCode?: string;
rubTypecode?: string;
mixId?: string | number;
termCode?: string;
paramName?: string;
subgroupSize?: number;
beginDate?: string;
endDate?: string;
pageNum?: number;
pageSize?: number;
}

@ -0,0 +1,618 @@
<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">
<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-form-item>
<el-form-item label="机台" prop="machineId">
<el-input v-model="traceQuery.machineId" placeholder="请输入机台ID" clearable />
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<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-form-item>
<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>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleTraceQuery"></el-button>
<el-button icon="Refresh" @click="resetTraceQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<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-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-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">
<template #default="{ row }">
<el-tag :type="row.recipeState === '1' ? 'success' : 'info'" size="small">
{{ row.recipeState === '1' ? '正用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" min-width="160" />
<el-table-column label="操作" width="80" 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" />
</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>
<el-form-item label="配方编码" prop="recipeCode">
<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>
<el-form-item label="工步号" prop="mixId">
<el-input v-model="spcQuery.mixId" placeholder="工步号" clearable style="width: 100px;" />
</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>
</el-form-item>
<el-form-item label="子组大小" prop="subgroupSize">
<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>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSpcQuery"></el-button>
<el-button icon="Refresh" @click="resetSpcQuery"></el-button>
</el-form-item>
</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">
<div class="text-xs text-gray-500">{{ item.label }}</div>
<div class="text-lg font-bold mt-1" :style="{ color: item.color || '#333' }">
{{ item.value }}
</div>
</el-card>
</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;" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>运行图</span></template>
<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;" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><span>R 极差图</span></template>
<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="mixId" width="80" align="center" />
<el-table-column label="工步编码" prop="termCode" min-width="90" />
<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" />
<el-table-column label="设定时间" prop="setTime" width="90" align="right" />
<el-table-column label="实测能量" prop="mixingEnergy" width="90" align="right" />
<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>
<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?.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>
</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>
</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>
</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;" />
</el-card>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, watch } from 'vue';
import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import * as echarts from 'echarts';
import {
listMixTrace, exportMixTrace, getMixTraceDetail,
listSpcSamples, getSpcCapability, getSpcXbarR
} from '@/api/mes/mixTrace';
import type {
MixTraceListVO, MixTraceDetailVO, MixTraceSpcSampleVO, MixTraceSpcResultVO
} from '@/api/mes/mixTrace/types';
import { download } from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// ==================== ====================
const activeTab = ref('trace');
const showSearch = ref(true);
// ==================== 5 ====================
const traceLoading = ref(false);
const traceList = ref<MixTraceListVO[]>([]);
const traceTotal = ref(0);
const traceDateRange = ref<string[]>([]);
const traceQueryFormRef = ref();
const traceQuery = reactive<any>({
recipeCode: undefined,
machineId: undefined,
materialName: undefined,
rubType: undefined,
operCode: undefined,
beginDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 20
});
const getTraceList = async () => {
traceLoading.value = true;
if (traceDateRange.value && traceDateRange.value.length === 2) {
traceQuery.beginDate = traceDateRange.value[0];
traceQuery.endDate = traceDateRange.value[1];
} else {
traceQuery.beginDate = undefined;
traceQuery.endDate = undefined;
}
try {
const res = await listMixTrace(traceQuery);
traceList.value = res.rows || [];
traceTotal.value = res.total || 0;
} finally {
traceLoading.value = false;
}
};
const handleTraceQuery = () => {
traceQuery.pageNum = 1;
getTraceList();
};
const resetTraceQuery = () => {
traceQueryFormRef.value?.resetFields();
traceDateRange.value = [];
traceQuery.beginDate = undefined;
traceQuery.endDate = undefined;
handleTraceQuery();
};
const handleExport = () => {
if (traceDateRange.value && traceDateRange.value.length === 2) {
traceQuery.beginDate = traceDateRange.value[0];
traceQuery.endDate = traceDateRange.value[1];
}
exportMixTrace(traceQuery);
};
const handleTraceRowClick = (row: MixTraceListVO) => {
openDetail(row);
};
// ==================== 9 ====================
const detailVisible = ref(false);
const detailData = ref<MixTraceDetailVO | null>(null);
const detailCurveChartRef = ref<HTMLElement>();
let detailCurveChart: echarts.ECharts | null = null;
const openDetail = async (row: MixTraceListVO) => {
detailVisible.value = true;
const res = await getMixTraceDetail(row.recipeId);
detailData.value = res.data;
await nextTick();
renderDetailCurveChart();
};
/** 渲染密炼工作曲线图4 */
const renderDetailCurveChart = () => {
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);
detailCurveChart.setOption({
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 60, right: 20, bottom: 30, top: 40 },
xAxis: { type: 'category', data: xData },
yAxis: [
{ type: 'value', name: '温度/能量/功率' },
{ 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 }
]
});
};
// ==================== SPC6/7/8/10 ====================
const spcSampleList = ref<MixTraceSpcSampleVO[]>([]);
const spcSampleTotal = ref(0);
const spcResult = 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,
recipeCode: undefined,
rubTypecode: undefined,
mixId: undefined,
paramName: 'mixingTemp',
subgroupSize: 5,
beginDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 50
});
const histogramChartRef = ref<HTMLElement>();
const runChartRef = ref<HTMLElement>();
const xbarChartRef = ref<HTMLElement>();
const rChartRef = ref<HTMLElement>();
let histogramChart: echarts.ECharts | null = null;
let runChart: echarts.ECharts | null = null;
let xbarChart: echarts.ECharts | null = null;
let rChart: echarts.ECharts | null = null;
/** SPC 统计指标卡片 */
const spcIndicators = computed(() => {
const r = spcResult.value;
if (!r || !r.sampleCount) return [];
return [
{ label: '样本数', value: r.sampleCount, color: '#409eff' },
{ label: '均值', value: r.mean?.toFixed(2) ?? '-' },
{ label: '标准差', value: r.sigma?.toFixed(4) ?? '-' },
{ label: 'Cp', value: r.cp?.toFixed(3) ?? '-', color: cpkColor(r.cp) },
{ label: 'Cpk', value: r.cpk?.toFixed(3) ?? '-', color: cpkColor(r.cpk) },
{ label: 'Ppk', value: r.ppk?.toFixed(3) ?? '-', color: cpkColor(r.ppk) },
{ label: 'USL', value: r.usl?.toFixed(2) ?? '-' },
{ label: 'LSL', value: r.lsl?.toFixed(2) ?? '-' }
];
});
const cpkColor = (v: number | null | undefined) => {
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) {
spcQuery.beginDate = spcDateRange.value[0];
spcQuery.endDate = spcDateRange.value[1];
} else {
spcQuery.beginDate = undefined;
spcQuery.endDate = undefined;
}
};
const handleSpcQuery = async () => {
buildSpcParams();
await Promise.all([getSpcSamples(), fetchSpcCapability(), fetchSpcXbarR()]);
};
const resetSpcQuery = () => {
spcQueryFormRef.value?.resetFields();
spcDateRange.value = [];
spcQuery.paramName = 'mixingTemp';
spcQuery.subgroupSize = 5;
spcQuery.pageNum = 1;
};
const getSpcSamples = async () => {
buildSpcParams();
const res = await listSpcSamples(spcQuery);
spcSampleList.value = res.rows || [];
spcSampleTotal.value = res.total || 0;
};
const fetchSpcCapability = async () => {
buildSpcParams();
const res = await getSpcCapability(spcQuery);
spcResult.value = res.data || { sampleCount: 0 } as MixTraceSpcResultVO;
await nextTick();
renderHistogramChart();
renderRunChart();
};
const fetchSpcXbarR = async () => {
buildSpcParams();
const res = await getSpcXbarR(spcQuery);
xbarRResult.value = res.data || { sampleCount: 0 } as MixTraceSpcResultVO;
await nextTick();
renderXbarChart();
renderRChart();
};
/** 直方图图7 */
const renderHistogramChart = () => {
if (!histogramChartRef.value) return;
const r = spcResult.value;
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'
}
}]
});
};
/** 运行图图8 */
const renderRunChart = () => {
if (!runChartRef.value) return;
const r = spcResult.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' } });
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 } }
}]
});
};
/** Xbar 均值图 */
const renderXbarChart = () => {
if (!xbarChartRef.value) return;
const r = xbarRResult.value;
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' } }
]
}
}]
});
};
/** R 极差图 */
const renderRChart = () => {
if (!rChartRef.value) return;
const r = xbarRResult.value;
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' } }
]
}
}]
});
};
// ==================== ====================
onMounted(() => {
getTraceList();
});
/** 监听窗口变化自适应图表 */
window.addEventListener('resize', () => {
histogramChart?.resize();
runChart?.resize();
xbarChart?.resize();
rChart?.resize();
detailCurveChart?.resize();
});
</script>
<style scoped>
.text-center {
text-align: center;
}
</style>
Loading…
Cancel
Save