feat(mes): 完善混合追溯功能界面和API

- 在API中添加MixTraceDetailQuery类型定义和参数支持
- 将追溯详情接口改为可传递查询参数模式
- 重构追溯页面布局为左右分栏结构,左侧显示配方树和托盘条码输入
- 添加右侧列表显示优化的追溯数据表格
- 实现底部双标签页显示每车基本信息和每车明细信息
- 移除原有的抽屉式详情展示,改用内嵌标签页方式
- 添加配方树搜索过滤功能
- 新增托盘条码追溯功能
- 优化表格列显示,调整为更合理的字段展示
- 添加批量导出功能按钮
- 更新类型定义,添加realWeight、testResult等新字段
- 添加新的批次追溯子页面,包含完整的批次查询和导航功能
master
zangch@mesnac.com 2 months ago
parent 0f18acf6f2
commit addf83dddb

@ -3,6 +3,7 @@ import { AxiosPromise } from 'axios';
import { import {
MixTraceListVO, MixTraceListVO,
MixTraceDetailVO, MixTraceDetailVO,
MixTraceDetailQuery,
MixTraceSpcSampleVO, MixTraceSpcSampleVO,
MixTraceSpcResultVO, MixTraceSpcResultVO,
MixTraceQuery, MixTraceQuery,
@ -35,10 +36,14 @@ export const exportMixTrace = (query?: MixTraceQuery): AxiosPromise<any> => {
/** /**
* 9 * 9
*/ */
export const getMixTraceDetail = (recipeId: string | number): AxiosPromise<MixTraceDetailVO> => { export const getMixTraceDetail = (
recipeId: string | number,
query?: MixTraceDetailQuery
): AxiosPromise<MixTraceDetailVO> => {
return request({ return request({
url: '/mes/mixTrace/detail/' + recipeId, url: '/mes/mixTrace/detail/' + recipeId,
method: 'get' method: 'get',
params: query
}); });
}; };

@ -35,7 +35,11 @@ export interface MixTraceListVO {
classTeamName?: string; classTeamName?: string;
planAmount?: number; planAmount?: number;
completeAmount?: number; completeAmount?: number;
realWeight?: number;
testResult?: string;
trainNumber?: number; trainNumber?: number;
totalTrainNo?: number;
planDetailStatus?: string;
realBeginTime?: string; realBeginTime?: string;
realEndTime?: string; realEndTime?: string;
} }
@ -97,6 +101,8 @@ export interface MixTraceSummaryVO {
dischargePower?: number; dischargePower?: number;
dischargeEnergy?: number; dischargeEnergy?: number;
mixingStatus?: string; mixingStatus?: string;
testResult?: string;
recipeTime?: number;
mixingTime?: number; mixingTime?: number;
consumeTime?: number; consumeTime?: number;
intervalTime?: number; intervalTime?: number;
@ -119,8 +125,18 @@ export interface MixTraceMaterialTraceTreeNode {
export interface MixTraceUsageItem { export interface MixTraceUsageItem {
usageId?: string | number; usageId?: string | number;
recipeId?: string | number;
planDetailId?: string | number;
weightId?: string | number;
weightSeq?: number; weightSeq?: number;
traceWeightSeq?: number;
traceWeightId?: string | number;
traceActCode?: string;
traceRowCount?: number;
jarTypeName?: string;
categoryName?: string; categoryName?: string;
wareNum?: number;
weighNum?: string;
materialName?: string; materialName?: string;
setWeight?: number; setWeight?: number;
actualWeight?: number; actualWeight?: number;
@ -130,6 +146,8 @@ export interface MixTraceUsageItem {
controlMode?: string; controlMode?: string;
actCode?: string; actCode?: string;
actName?: string; actName?: string;
actionType?: string;
actionStatus?: string;
} }
export interface MixTraceStepItem { export interface MixTraceStepItem {
@ -195,6 +213,8 @@ export interface RecipeWeightItem {
fatherCode: string | number; fatherCode: string | number;
unitId: string | number; unitId: string | number;
childCode: number; childCode: number;
/** 物料名称关联base_material_info */
materialName?: string;
ifUseBat: string; ifUseBat: string;
maxRate: string; maxRate: string;
} }
@ -292,6 +312,7 @@ export interface MixTraceSpcResultVO {
* *
*/ */
export interface MixTraceQuery { export interface MixTraceQuery {
recipeId?: string | number;
recipeCode?: string; recipeCode?: string;
machineId?: string | number; machineId?: string | number;
machineName?: string; machineName?: string;
@ -313,6 +334,8 @@ export interface MixTraceQuery {
classTeamId?: string | number; classTeamId?: string | number;
shiftName?: string; shiftName?: string;
classTeamName?: string; classTeamName?: string;
trainNumberStart?: string | number;
trainNumberEnd?: string | number;
beginDate?: string; beginDate?: string;
endDate?: string; endDate?: string;

File diff suppressed because it is too large Load Diff

@ -49,48 +49,176 @@
</div> </div>
</transition> </transition>
<el-card shadow="hover"> <!-- 主内容区-右分栏 (mix_code.png 布局) -->
<template #header> <el-row :gutter="10">
<el-row :gutter="10" class="mb8"> <!-- 左侧面板配方树 (§1.1) -->
<el-col :span="1.5"> <el-col :span="5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['mes:mixTrace:export']"></el-button> <el-card shadow="hover" class="tree-panel">
</el-col> <template #header><span>配方树</span></template>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getTraceList" /> <el-input v-model="treeFilterText" placeholder="搜索配方" clearable size="small" class="mb-2" />
</el-row> <el-scrollbar height="340px">
</template> <el-tree
ref="recipeTreeRef"
:data="recipeTreeData"
:filter-node-method="filterTreeNode"
node-key="id"
highlight-current
default-expand-all
:props="{ label: 'label', children: 'children' }"
@node-click="handleTreeNodeClick"
/>
</el-scrollbar>
<el-divider />
<div class="px-1">
<div class="text-sm mb-1 font-bold">托盘条码</div>
<el-input v-model="trayBarcode" clearable size="small" placeholder="扫描或输入条码" />
<el-button type="primary" size="small" class="mt-2 w-full" @click="handleTrayTrace"></el-button>
</div>
</el-card>
</el-col>
<el-table v-loading="traceLoading" :data="traceList" highlight-current-row border stripe> <!-- 右侧面板列表 + 底部详情 (§1.1) -->
<el-table-column label="计划编号" prop="planCode" min-width="140" /> <el-col :span="19">
<el-table-column label="明细编号" prop="planDetailCode" min-width="130" /> <el-card shadow="hover">
<el-table-column label="生产条码" prop="productionBarcode" min-width="130" /> <template #header>
<el-table-column label="配方编码" prop="recipeCode" min-width="120" /> <el-row :gutter="10" class="mb8">
<el-table-column label="机台" prop="machineName" min-width="100" /> <el-col :span="1.5">
<el-table-column label="物料" prop="materialName" min-width="120" /> <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['mes:mixTrace:export']"></el-button>
<el-table-column label="班次" prop="shiftName" min-width="90" /> </el-col>
<el-table-column label="班组" prop="classTeamName" min-width="90" /> <right-toolbar v-model:showSearch="showSearch" @queryTable="getTraceList" />
<el-table-column label="计划数" prop="planAmount" min-width="90" align="right" /> </el-row>
<el-table-column label="完成数" prop="completeAmount" min-width="90" align="right" />
<el-table-column label="密炼车次" prop="trainNumber" min-width="90" align="center" />
<el-table-column label="状态" prop="recipeState" min-width="80" align="center">
<template #default="{ row }"><dict-tag :options="recipe_state" :value="row.recipeState" /></template>
</el-table-column>
<el-table-column label="开始生产时间" prop="realBeginTime" min-width="160" />
<el-table-column label="创建时间" prop="createTime" min-width="160" />
<el-table-column label="操作" fixed="right" width="150" align="center">
<template #default="{ row }">
<el-button link type="primary" @click.stop="openDetail(row)">本车耗用追溯</el-button>
</template> </template>
</el-table-column>
</el-table>
<pagination <!-- 列表字段逐列对齐 mix_code.png (§1.1) -->
v-show="traceTotal > 0" <el-table ref="traceTableRef" v-loading="traceLoading" :data="traceList" highlight-current-row border stripe @current-change="handleRowSelect" max-height="400">
:total="traceTotal" <el-table-column label="机台" prop="machineName" min-width="100" />
v-model:page="traceQuery.pageNum" <el-table-column label="班次" prop="shiftName" min-width="80" />
v-model:limit="traceQuery.pageSize" <el-table-column label="班组" prop="classTeamName" min-width="80" />
@pagination="getTraceList" <el-table-column label="物料名称" prop="materialName" min-width="120" show-overflow-tooltip />
/> <el-table-column label="计划编号" prop="planCode" min-width="130" show-overflow-tooltip />
</el-card> <el-table-column label="车次号" prop="trainNumber" min-width="80" align="center" />
<el-table-column label="开始生产时间" prop="realBeginTime" min-width="160" />
<el-table-column label="设量" prop="totalWeight" min-width="80" align="right" />
<el-table-column label="实量" min-width="80" align="right">
<template #default="{ row }">{{ row.realWeight != null ? Number(row.realWeight).toFixed(1) : '-' }}</template>
</el-table-column>
<el-table-column label="检验结果" min-width="90" align="center">
<template #default="{ row }">{{ row.testResult || '-' }}</template>
</el-table-column>
<el-table-column label="累计车次" prop="totalTrainNo" min-width="90" align="center" />
<el-table-column label="生产状态" prop="planDetailStatus" min-width="90" align="center" />
</el-table>
<pagination v-show="traceTotal > 0" :total="traceTotal" v-model:page="traceQuery.pageNum" v-model:limit="traceQuery.pageSize" @pagination="getTraceList" />
</el-card>
<!-- 底部 Tab每车基本信息 / 每车明细信息 (§1.1 底部双Tab) -->
<el-card shadow="hover" class="mt-[10px]" v-if="selectedRow">
<el-tabs v-model="detailTab" type="border-card">
<!-- 每车基本信息 Tab轻量摘要 -->
<el-tab-pane label="每车基本信息" name="basic">
<el-descriptions :column="6" border size="small" v-if="summary.recipeId" class="trace-summary">
<el-descriptions-item label="配方编码">{{ summary.recipeCode || recipeInfo.recipeCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划编号">{{ summary.planCode || recipeInfo.planCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="生产条码">{{ summary.productionBarcode || recipeInfo.productionBarcode || '-' }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ summary.machineName || recipeInfo.machineName || '-' }}</el-descriptions-item>
<el-descriptions-item label="物料">{{ summary.materialName || recipeInfo.materialName || '-' }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ summary.shiftName || recipeInfo.shiftName || '-' }}</el-descriptions-item>
<el-descriptions-item label="班组">{{ summary.classTeamName || recipeInfo.classTeamName || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划数">{{ n(summary.planAmount) }}</el-descriptions-item>
<el-descriptions-item label="设定重量">{{ n(summary.settingWeight) }}</el-descriptions-item>
<el-descriptions-item label="完成重量">{{ n(summary.completedWeight) }}</el-descriptions-item>
<el-descriptions-item label="每车能量">{{ n(summary.eachCarEnergy) }}</el-descriptions-item>
<el-descriptions-item label="密炼车次">{{ summary.mixingTrainNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="累计车次">{{ summary.totalTrainNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="超差报警">{{ summary.overToleranceAlarm || '-' }}</el-descriptions-item>
<el-descriptions-item label="密炼状态">{{ summary.mixingStatus || '-' }}</el-descriptions-item>
<el-descriptions-item label="检验结果">{{ summary.testResult || '-' }}</el-descriptions-item>
<el-descriptions-item label="排胶温度">{{ n(summary.dischargeTemp) }}</el-descriptions-item>
<el-descriptions-item label="排胶功率">{{ n(summary.dischargePower) }}</el-descriptions-item>
<el-descriptions-item label="排胶能量">{{ n(summary.dischargeEnergy) }}</el-descriptions-item>
<el-descriptions-item label="混炼时间">{{ n(summary.mixingTime) }}</el-descriptions-item>
<el-descriptions-item label="消耗时间">{{ n(summary.consumeTime) }}</el-descriptions-item>
<el-descriptions-item label="间隔时间">{{ n(summary.intervalTime) }}</el-descriptions-item>
<el-descriptions-item label="配方时间">{{ summary.recipeTime ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="开始生产时间">{{ summary.beginProduceTime || '-' }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="加载中..." :image-size="60" />
</el-tab-pane>
<!-- 每车明细信息 Tab完整追溯详情 (§1.2 mix.jpg) -->
<el-tab-pane label="每车明细信息" name="detail" lazy>
<div v-if="detailData" class="trace-detail-content">
<el-row :gutter="8" class="mb-[8px]">
<el-col :span="6" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>本车耗用追溯树</template>
<el-tree :data="treeData" node-key="id" default-expand-all :props="{ label: 'label', children: 'children' }" style="max-height: 300px; overflow: auto" />
</el-card>
</el-col>
<el-col :span="9" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>称量信息</template>
<el-table :data="usageDisplayList" border stripe size="small" max-height="300">
<el-table-column label="序号" prop="weightSeq" width="50" align="center" />
<el-table-column label="类别" prop="categoryName" min-width="80" />
<el-table-column label="物料名称" prop="materialName" min-width="100" show-overflow-tooltip />
<el-table-column label="设量" prop="setWeight" width="70" align="right" />
<el-table-column label="实量" prop="actualWeight" width="70" align="right" />
<el-table-column label="差值" prop="diffWeight" width="70" align="right" />
<el-table-column label="公差" prop="tolerance" width="70" align="right" />
<el-table-column label="超差" width="55" align="center">
<template #default="{ row }">{{ row.overToleranceFlag === '1' ? '是' : row.overToleranceFlag === '0' ? '否' : '-' }}</template>
</el-table-column>
<el-table-column label="称量状态" prop="actionStatus" width="80" align="center" />
</el-table>
</el-card>
</el-col>
<el-col :span="9" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>混炼信息</template>
<el-table :data="stepDisplayList" border stripe size="small" max-height="300">
<el-table-column label="步骤" prop="mixId" width="50" align="center" />
<el-table-column label="条件名称" prop="condName" min-width="90" />
<el-table-column label="动作名称" prop="actName" min-width="90" />
<el-table-column label="时间" prop="mixingTime" width="55" align="right" />
<el-table-column label="温度" prop="mixingTemp" width="55" align="right" />
<el-table-column label="能量" prop="mixingEnergy" width="55" align="right" />
<el-table-column label="功率" prop="mixingPower" width="55" align="right" />
<el-table-column label="压力" prop="mixingPress" width="55" align="right" />
<el-table-column label="转速" prop="mixingSpeed" width="55" align="right" />
<el-table-column label="设时" prop="setTime" width="55" align="right" />
<el-table-column label="设温" prop="setTemp" width="55" align="right" />
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :span="10" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>批次信息</template>
<el-table :data="batchList" border stripe size="small" max-height="260">
<el-table-column label="批次" prop="batchCode" min-width="130" />
<el-table-column label="物料名称" prop="materialName" min-width="120" />
<el-table-column label="入库时间" prop="instockTime" min-width="150" />
<el-table-column label="供应商" prop="supplierName" min-width="100" />
</el-table>
</el-card>
</el-col>
<el-col :span="14" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>历史密炼曲线</template>
<div ref="detailCurveChartRef" style="height: 260px" />
</el-card>
</el-col>
</el-row>
</div>
<el-empty v-else description="加载中..." :image-size="60" />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="SPC分析" name="spc"> <el-tab-pane label="SPC分析" name="spc">
@ -192,120 +320,7 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-drawer <!-- Drawer 已移除详情内容已迁移至底部 Tab每车基本信息/每车明细信息 -->
v-model="detailVisible"
class="mix-trace-drawer"
title="密炼追溯详情"
size="92%"
direction="rtl"
destroy-on-close
>
<template v-if="detailData">
<div class="trace-one-page">
<el-card shadow="never" class="mb-[8px]">
<template #header>配方基础信息与追溯摘要</template>
<el-descriptions :column="6" border size="small" class="trace-summary">
<el-descriptions-item label="配方编码">{{ summary.recipeCode || recipeInfo.recipeCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划编号">{{ summary.planCode || recipeInfo.planCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="明细编号">{{ summary.planDetailCode || recipeInfo.planDetailCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="生产条码">{{ summary.productionBarcode || recipeInfo.productionBarcode || '-' }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ summary.machineName || recipeInfo.machineName || '-' }}</el-descriptions-item>
<el-descriptions-item label="物料">{{ summary.materialName || recipeInfo.materialName || '-' }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ summary.shiftName || recipeInfo.shiftName || '-' }}</el-descriptions-item>
<el-descriptions-item label="班组">{{ summary.classTeamName || recipeInfo.classTeamName || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划数">{{ n(summary.planAmount) }}</el-descriptions-item>
<el-descriptions-item label="设定重量">{{ n(summary.settingWeight) }}</el-descriptions-item>
<el-descriptions-item label="完成重量">{{ n(summary.completedWeight) }}</el-descriptions-item>
<el-descriptions-item label="每车能量">{{ n(summary.eachCarEnergy) }}</el-descriptions-item>
<el-descriptions-item label="密炼车次">{{ summary.mixingTrainNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="累计车次">{{ summary.totalTrainNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="超差报警">{{ summary.overToleranceAlarm || '-' }}</el-descriptions-item>
<el-descriptions-item label="密炼结果">{{ summary.mixingStatus || '-' }}</el-descriptions-item>
<el-descriptions-item label="排胶温度">{{ n(summary.dischargeTemp) }}</el-descriptions-item>
<el-descriptions-item label="排胶功率">{{ n(summary.dischargePower) }}</el-descriptions-item>
<el-descriptions-item label="排胶能量">{{ n(summary.dischargeEnergy) }}</el-descriptions-item>
<el-descriptions-item label="混炼时间">{{ n(summary.mixingTime) }}</el-descriptions-item>
<el-descriptions-item label="消耗时间">{{ n(summary.consumeTime) }}</el-descriptions-item>
<el-descriptions-item label="间隔时间">{{ n(summary.intervalTime) }}</el-descriptions-item>
<el-descriptions-item label="开始生产时间">{{ summary.beginProduceTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束生产时间">{{ summary.endProduceTime || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-row :gutter="8" class="mb-[8px]">
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="6" :span="6" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>本车耗用追溯树</template>
<el-tree
:data="treeData"
node-key="id"
default-expand-all
:props="{ label: 'label', children: 'children' }"
class="mix-trace-tree"
/>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="8" :span="8" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>称量信息追溯 + 配方字段</template>
<el-table :data="usageDisplayList" border stripe size="small" max-height="350">
<el-table-column label="序号" prop="weightSeq" width="64" align="center" />
<el-table-column label="料别" prop="categoryName" min-width="90" />
<el-table-column label="物料名称" prop="materialName" min-width="130" />
<el-table-column label="设量" prop="setWeight" min-width="80" align="right" />
<el-table-column label="实量" prop="actualWeight" min-width="80" align="right" />
<el-table-column label="差值" prop="diffWeight" min-width="80" align="right" />
<el-table-column label="公差" prop="tolerance" min-width="80" align="right" />
<el-table-column label="数量状态" prop="quantityStatus" min-width="90" align="center" />
<el-table-column label="控制方式" prop="controlMode" min-width="90" align="center" />
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="10" :span="10" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>混炼信息追溯 + 配方字段</template>
<el-table :data="stepDisplayList" border stripe size="small" max-height="350">
<el-table-column label="步骤" prop="mixId" width="64" align="center" />
<el-table-column label="条件名称" prop="condName" min-width="110" />
<el-table-column label="动作名称" prop="actName" min-width="110" />
<el-table-column label="时间" prop="mixingTime" width="70" align="right" />
<el-table-column label="温度" prop="mixingTemp" width="70" align="right" />
<el-table-column label="能量" prop="mixingEnergy" width="70" align="right" />
<el-table-column label="功率" prop="mixingPower" width="70" align="right" />
<el-table-column label="压力" prop="mixingPress" width="70" align="right" />
<el-table-column label="转速" prop="mixingSpeed" width="70" align="right" />
<el-table-column label="设时" prop="setTime" width="70" align="right" />
<el-table-column label="设温" prop="setTemp" width="70" align="right" />
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="10" :span="10" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>批次信息</template>
<el-table :data="batchList" border stripe size="small" max-height="300">
<el-table-column label="批次" prop="batchCode" min-width="130" />
<el-table-column label="物料名称" prop="materialName" min-width="120" />
<el-table-column label="入库时间" prop="instockTime" min-width="150" />
<el-table-column label="供应商" prop="supplierName" min-width="110" />
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="14" :span="14" class="trace-panel-col">
<el-card shadow="never" class="trace-panel-card">
<template #header>历史密炼曲线</template>
<div ref="detailCurveChartRef" style="height: 300px" />
</el-card>
</el-col>
</el-row>
</div>
</template>
</el-drawer>
</div> </div>
</template> </template>
@ -331,12 +346,24 @@ const { recipe_state, mix_trace_spc_param } = toRefs<any>(proxy?.useDict('recipe
const activeTab = ref('trace'); const activeTab = ref('trace');
const showSearch = ref(true); const showSearch = ref(true);
/** 当前选中行(列表点击后触发详情加载) */
const selectedRow = ref<any>(null);
/** 底部详情Tabbasic=每车基本信息, detail=每车明细信息 */
const detailTab = ref('basic');
/** 配方树数据(从列表按 recipeId 分组构建) */
const recipeTreeData = ref<any[]>([]);
const recipeTreeRef = ref();
const traceTableRef = ref();
const treeFilterText = ref('');
const trayBarcode = ref('');
const traceLoading = ref(false); const traceLoading = ref(false);
const traceList = ref<any[]>([]); const traceList = ref<any[]>([]);
const traceTotal = ref(0); const traceTotal = ref(0);
const traceDateRange = ref<string[]>([]); const traceDateRange = ref<string[]>([]);
const traceQueryFormRef = ref(); const traceQueryFormRef = ref();
const traceQuery = reactive<MixTraceQuery>({ const traceQuery = reactive<MixTraceQuery>({
recipeId: undefined,
recipeCode: undefined, recipeCode: undefined,
planCode: undefined, planCode: undefined,
planDetailCode: undefined, planDetailCode: undefined,
@ -383,8 +410,17 @@ const handleTraceQuery = () => {
const resetTraceQuery = () => { const resetTraceQuery = () => {
traceQueryFormRef.value?.resetFields(); traceQueryFormRef.value?.resetFields();
traceDateRange.value = []; traceDateRange.value = [];
// recipeId
traceQuery.recipeId = undefined;
traceQuery.productionBarcode = undefined;
trayBarcode.value = '';
traceQuery.pageNum = 1; traceQuery.pageNum = 1;
traceQuery.pageSize = 20; traceQuery.pageSize = 20;
//
selectedRow.value = null;
detailData.value = null;
//
recipeTreeRef.value?.setCurrentKey(null);
handleTraceQuery(); handleTraceQuery();
}; };
@ -393,7 +429,6 @@ const handleExport = () => {
download('/mes/mixTrace/export', { ...traceQuery }, `mix_trace_${Date.now()}.xlsx`); download('/mes/mixTrace/export', { ...traceQuery }, `mix_trace_${Date.now()}.xlsx`);
}; };
const detailVisible = ref(false);
const detailData = ref<MixTraceDetailVO | null>(null); const detailData = ref<MixTraceDetailVO | null>(null);
const detailCurveChartRef = ref<HTMLElement>(); const detailCurveChartRef = ref<HTMLElement>();
let detailCurveChart: echarts.ECharts | null = null; let detailCurveChart: echarts.ECharts | null = null;
@ -421,7 +456,7 @@ const usageDisplayList = computed<any[]>(() => {
return recipeWeightList.value.map((item: any, index: number) => ({ return recipeWeightList.value.map((item: any, index: number) => ({
weightSeq: item.weightSeq ?? index + 1, weightSeq: item.weightSeq ?? index + 1,
categoryName: item.weightType || '-', categoryName: item.weightType || '-',
materialName: item.childCode || '-', materialName: item.materialName || item.childCode || '-',
setWeight: item.setWeight, setWeight: item.setWeight,
actualWeight: '-', actualWeight: '-',
tolerance: item.errorAllow, tolerance: item.errorAllow,
@ -468,9 +503,69 @@ const curveDisplayList = computed<any[]>(() => {
})); }));
}); });
const openDetail = async (row: any) => { /** 点击列表行时加载追溯详情底部Tab展示非Drawer */
const handleRowSelect = (row: any) => {
if (!row) {
selectedRow.value = null;
detailData.value = null;
return;
}
selectedRow.value = row;
loadDetail(row);
};
/** 配方树节点点击:按 recipeId 过滤列表 */
const handleTreeNodeClick = (node: any) => {
if (node.recipeId) {
traceQuery.recipeId = node.recipeId;
handleTraceQuery();
}
};
/** 配方树过滤 */
const filterTreeNode = (value: string, data: any) => {
if (!value) return true;
return (data.label || '').toLowerCase().includes(value.toLowerCase());
};
/** 托盘条码追溯 */
const handleTrayTrace = () => {
if (!trayBarcode.value) {
proxy?.$modal?.msgWarning?.('请输入托盘条码');
return;
}
traceQuery.productionBarcode = trayBarcode.value;
handleTraceQuery();
};
/** 从列表数据构建配方树(父=配方编码+物料名,子=该配方下的记录) */
const buildRecipeTree = (list: any[]) => {
const grouped = new Map<string, { label: string; recipeId: any; children: any[] }>();
for (const item of list) {
const key = String(item.recipeId);
if (!grouped.has(key)) {
grouped.set(key, {
label: `${item.recipeCode || ''} (${item.materialName || ''})`,
recipeId: item.recipeId,
children: []
});
}
grouped.get(key)!.children.push({
id: `${key}_${item.planDetailId || item.trainNumber || Math.random()}`,
label: `车次${item.trainNumber ?? '-'} ${item.planCode || ''}`,
recipeId: item.recipeId,
planDetailId: item.planDetailId
});
}
recipeTreeData.value = Array.from(grouped.values()).map((g, i) => ({
id: `recipe_${g.recipeId || i}`,
...g
}));
};
/** 加载追溯详情到底部Tab */
const loadDetail = async (row: any) => {
const requestSeq = ++detailRequestSeq; const requestSeq = ++detailRequestSeq;
detailVisible.value = true;
detailData.value = null; detailData.value = null;
const q: MixTraceDetailQuery = { const q: MixTraceDetailQuery = {
@ -485,27 +580,17 @@ const openDetail = async (row: any) => {
try { try {
const res = await getMixTraceDetail(row.recipeId, q); const res = await getMixTraceDetail(row.recipeId, q);
if (requestSeq !== detailRequestSeq) { if (requestSeq !== detailRequestSeq) return;
return;
}
if (!res?.data) { if (!res?.data) {
proxy?.$modal?.msgWarning?.('未查询到追溯详情'); proxy?.$modal?.msgWarning?.('未查询到追溯详情');
detailCurveChart?.dispose();
detailCurveChart = null;
detailVisible.value = false;
return; return;
} }
detailData.value = res.data; detailData.value = res.data;
await nextTick(); await nextTick();
renderDetailCurve(); renderDetailCurve();
} catch { } catch {
if (requestSeq !== detailRequestSeq) { if (requestSeq !== detailRequestSeq) return;
return;
}
detailData.value = null; detailData.value = null;
detailCurveChart?.dispose();
detailCurveChart = null;
detailVisible.value = false;
proxy?.$modal?.msgError?.('加载追溯详情失败'); proxy?.$modal?.msgError?.('加载追溯详情失败');
} }
}; };
@ -805,17 +890,32 @@ watch(
{ immediate: true } { immediate: true }
); );
watch(detailVisible, (visible) => { /** 列表数据变化时构建配方树 */
if (!visible) { watch(traceList, (list) => {
detailData.value = null; buildRecipeTree(list);
detailCurveChart?.dispose(); });
detailCurveChart = null;
/** 配方树搜索过滤 */
watch(treeFilterText, (val) => {
recipeTreeRef.value?.filter(val);
});
/** 切换到明细Tab时渲染曲线lazy tab-pane 中 ref 需延迟绑定) */
watch(detailTab, async (tab) => {
if (tab === 'detail' && detailData.value) {
await nextTick();
renderDetailCurve();
} }
}); });
onMounted(() => { onMounted(() => {
getTraceList(); getTraceList();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
//
watch(() => traceQuery.pageNum, () => {
selectedRow.value = null;
detailData.value = null;
});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -829,39 +929,31 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
.text-center { /* 配方树面板 */
text-align: center; .tree-panel :deep(.el-card__body) {
padding: 8px;
} }
.trace-one-page .trace-panel-col { /* 详情面板 */
.trace-detail-content .trace-panel-col {
display: flex; display: flex;
margin-bottom: 8px;
} }
.trace-one-page .trace-panel-card { .trace-detail-content .trace-panel-card {
width: 100%; width: 100%;
} }
.mix-trace-tree { .trace-detail-content :deep(.el-card__header) {
min-height: 300px;
max-height: 350px;
overflow: auto;
}
.mix-trace-drawer :deep(.el-drawer__body) {
background: #f5f7fa;
padding: 10px;
}
.mix-trace-drawer :deep(.el-card__header) {
padding: 8px 12px; padding: 8px 12px;
font-weight: 600; font-weight: 600;
} }
.mix-trace-drawer :deep(.el-card__body) { .trace-detail-content :deep(.el-card__body) {
padding: 8px; padding: 8px;
} }
.mix-trace-drawer :deep(.el-descriptions__label) { .trace-summary :deep(.el-descriptions__label) {
width: 98px; width: 98px;
} }
</style> </style>

@ -0,0 +1,338 @@
<template>
<div class="p-2">
<el-card shadow="hover" class="mb-[10px]">
<el-form :model="query" :inline="true" ref="queryFormRef">
<!-- 每个条件带 checkbox 启用/禁用 (§1.3 mix_lot.png) -->
<el-form-item>
<el-checkbox v-model="enabled.machine" />
<span class="ml-1 mr-2">按生产机台</span>
<el-input v-model="query.machineName" :disabled="!enabled.machine" clearable placeholder="机台名称" style="width: 150px" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="enabled.dateRange" />
<span class="ml-1 mr-2">按生产时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
:disabled="!enabled.dateRange"
range-separator="-"
start-placeholder="开始"
end-placeholder="结束"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="enabled.classTeam" />
<span class="ml-1 mr-2">按班组</span>
<el-input v-model="query.classTeamName" :disabled="!enabled.classTeam" clearable placeholder="班组" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="enabled.shift" />
<span class="ml-1 mr-2">按班次</span>
<el-input v-model="query.shiftName" :disabled="!enabled.shift" clearable placeholder="班次" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="enabled.trainNumber" />
<span class="ml-1 mr-2">按车次</span>
<el-input-number v-model="query.trainNumberStart" :disabled="!enabled.trainNumber" :min="0" controls-position="right" style="width: 100px" />
<span class="mx-1"></span>
<el-input-number v-model="query.trainNumberEnd" :disabled="!enabled.trainNumber" :min="0" controls-position="right" style="width: 100px" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="enabled.recipe" />
<span class="ml-1 mr-2">按配方</span>
<el-input v-model="query.recipeCode" :disabled="!enabled.recipe" clearable placeholder="配方编码" style="width: 150px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="handleReset"></el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 记录导航区 (§1.3 首页/上一车/下一车/末页) -->
<el-card shadow="hover" class="mb-[10px]">
<div class="flex items-center justify-between">
<div>
<el-button-group>
<el-button :disabled="currentIndex <= 0" @click="goFirst" icon="DArrowLeft">首页</el-button>
<el-button :disabled="currentIndex <= 0" @click="goPrev" icon="ArrowLeft">上一车</el-button>
<el-button :disabled="currentIndex >= records.length - 1" @click="goNext"><el-icon class="el-icon--right"><ArrowRight /></el-icon></el-button>
<el-button :disabled="currentIndex >= records.length - 1" @click="goLast"><el-icon class="el-icon--right"><DArrowRight /></el-icon></el-button>
</el-button-group>
<span class="ml-4 text-sm text-gray-500" v-if="records.length > 0">
{{ currentIndex + 1 }} / {{ records.length }}
</span>
</div>
</div>
</el-card>
<!-- 摘要信息区 (§1.3 mix_lot.png 顶部文本) -->
<el-card shadow="hover" class="mb-[10px]" v-if="summary.recipeId" v-loading="detailLoading">
<el-descriptions :column="6" border size="small" class="lot-summary">
<el-descriptions-item label="班组">{{ summary.classTeamName || '-' }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ summary.shiftName || '-' }}</el-descriptions-item>
<el-descriptions-item label="配方">{{ summary.recipeCode || currentRow?.recipeCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ summary.beginProduceTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="配方时间">{{ summary.recipeTime ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="累计能量">{{ n(summary.eachCarEnergy) }}</el-descriptions-item>
<el-descriptions-item label="排胶温度">{{ n(summary.dischargeTemp) }}</el-descriptions-item>
<el-descriptions-item label="车次">{{ summary.mixingTrainNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="混炼时间">{{ n(summary.mixingTime) }}</el-descriptions-item>
<el-descriptions-item label="间隔时间">{{ n(summary.intervalTime) }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ summary.materialName || '-' }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ summary.machineName || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 曲线控制区 + 曲线图 (§1.3) -->
<el-card shadow="hover" v-if="summary.recipeId">
<template #header>
<div class="flex items-center justify-between flex-wrap gap-2">
<span class="font-bold">密炼机利用曲线图</span>
<div class="flex items-center gap-4 flex-wrap">
<span class="text-sm">视点</span>
<el-checkbox v-model="curveVisible.temperature" @change="renderCurve"></el-checkbox>
<el-checkbox v-model="curveVisible.power" @change="renderCurve"></el-checkbox>
<el-checkbox v-model="curveVisible.pressure" @change="renderCurve"></el-checkbox>
<el-checkbox v-model="curveVisible.energy" @change="renderCurve"></el-checkbox>
<el-checkbox v-model="curveVisible.speed" @change="renderCurve"></el-checkbox>
</div>
</div>
</template>
<div ref="curveChartRef" style="height: 400px" />
</el-card>
<!-- 空状态 -->
<el-empty v-if="!listLoading && records.length === 0" description="请设置查询条件后点击查询" />
</div>
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { ArrowRight, DArrowRight } from '@element-plus/icons-vue';
import * as echarts from 'echarts';
import { getMixTraceDetail, listMixTrace } from '@/api/mes/mixTrace';
import type { MixTraceDetailQuery, MixTraceDetailVO, MixTraceQuery, MixTraceStepItem } from '@/api/mes/mixTrace/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
/* ======================== 查询条件 ======================== */
const queryFormRef = ref();
const dateRange = ref<string[]>([]);
/** 各条件 checkbox 启用状态 */
const enabled = reactive({
machine: true,
dateRange: true,
classTeam: false,
shift: false,
trainNumber: false,
recipe: false
});
const query = reactive<MixTraceQuery>({
machineName: undefined,
classTeamName: undefined,
shiftName: undefined,
trainNumberStart: undefined,
trainNumberEnd: undefined,
recipeCode: undefined,
beginDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 500 //
});
/* ======================== 记录列表与导航 ======================== */
const listLoading = ref(false);
const detailLoading = ref(false);
const records = ref<any[]>([]);
const currentIndex = ref(-1);
const currentRow = computed(() => (currentIndex.value >= 0 && currentIndex.value < records.value.length) ? records.value[currentIndex.value] : null);
/* ======================== 详情数据 ======================== */
const detailData = ref<MixTraceDetailVO | null>(null);
const summary = computed<any>(() => detailData.value?.summaryInfo || {});
const stepList = computed<MixTraceStepItem[]>(() => detailData.value?.mixingStepList || []);
const recipeMixingList = computed<any[]>(() => detailData.value?.mixingList || []);
let detailRequestSeq = 0;
/** 从工步数据构建曲线点 */
const curvePoints = computed<any[]>(() => {
const list = stepList.value.length > 0 ? stepList.value : recipeMixingList.value;
return list.map((item: any, i: number) => ({
xLabel: item.mixId != null ? `步骤${item.mixId}` : `步骤${i + 1}`,
temperature: item.mixingTemp,
power: item.mixingPower,
energy: item.mixingEnergy,
pressure: item.mixingPress,
speed: item.mixingSpeed
}));
});
/* ======================== 曲线控制 ======================== */
const curveChartRef = ref<HTMLElement>();
let curveChart: echarts.ECharts | null = null;
const curveVisible = reactive({
temperature: true,
power: true,
pressure: true,
energy: true,
speed: true
});
/* ======================== 方法 ======================== */
const n = (v: any) => (v == null || v === '' ? '-' : Number.isNaN(Number(v)) ? v : Number(v).toFixed(2));
/** 构建查询参数(仅启用的条件) */
const buildQueryParams = (): MixTraceQuery => {
const p: MixTraceQuery = { pageNum: 1, pageSize: 500 };
if (enabled.machine && query.machineName) p.machineName = query.machineName;
if (enabled.dateRange && dateRange.value?.length === 2) {
p.beginDate = dateRange.value[0];
p.endDate = dateRange.value[1];
}
if (enabled.classTeam && query.classTeamName) p.classTeamName = query.classTeamName;
if (enabled.shift && query.shiftName) p.shiftName = query.shiftName;
if (enabled.trainNumber) {
if (query.trainNumberStart != null) p.trainNumberStart = query.trainNumberStart;
if (query.trainNumberEnd != null) p.trainNumberEnd = query.trainNumberEnd;
}
if (enabled.recipe && query.recipeCode) p.recipeCode = query.recipeCode;
return p;
};
const handleQuery = async () => {
listLoading.value = true;
detailData.value = null;
currentIndex.value = -1;
try {
const res = await listMixTrace(buildQueryParams());
records.value = res.rows || [];
if (records.value.length > 0) {
currentIndex.value = 0;
}
} finally {
listLoading.value = false;
}
};
const handleReset = () => {
queryFormRef.value?.resetFields();
dateRange.value = [];
query.machineName = undefined;
query.classTeamName = undefined;
query.shiftName = undefined;
query.trainNumberStart = undefined;
query.trainNumberEnd = undefined;
query.recipeCode = undefined;
records.value = [];
currentIndex.value = -1;
detailData.value = null;
};
/* 导航按钮 */
const goFirst = () => { if (records.value.length > 0) currentIndex.value = 0; };
const goPrev = () => { if (currentIndex.value > 0) currentIndex.value--; };
const goNext = () => { if (currentIndex.value < records.value.length - 1) currentIndex.value++; };
const goLast = () => { if (records.value.length > 0) currentIndex.value = records.value.length - 1; };
/** 加载当前记录的详情 */
const loadCurrentDetail = async () => {
const row = currentRow.value;
if (!row) {
detailData.value = null;
return;
}
const seq = ++detailRequestSeq;
detailLoading.value = true;
try {
const q: MixTraceDetailQuery = {
planId: row.planId,
planCode: row.planCode,
planDetailId: row.planDetailId,
planDetailCode: row.planDetailCode,
productionBarcode: row.productionBarcode,
shiftId: row.shiftId,
classTeamId: row.classTeamId
};
const res = await getMixTraceDetail(row.recipeId, q);
if (seq !== detailRequestSeq) return;
detailData.value = res?.data || null;
await nextTick();
renderCurve();
} catch {
if (seq !== detailRequestSeq) return;
detailData.value = null;
proxy?.$modal?.msgError?.('加载详情失败');
} finally {
detailLoading.value = false;
}
};
/** 渲染曲线图 */
const renderCurve = () => {
if (!curveChartRef.value || !curvePoints.value.length) {
curveChart?.dispose();
curveChart = null;
return;
}
curveChart?.dispose();
curveChart = echarts.init(curveChartRef.value);
const series: any[] = [];
const legend: string[] = [];
const points = curvePoints.value;
if (curveVisible.temperature) { legend.push('温度'); series.push({ name: '温度', type: 'line', data: points.map(p => p.temperature), smooth: true }); }
if (curveVisible.power) { legend.push('功率'); series.push({ name: '功率', type: 'line', data: points.map(p => p.power), smooth: true }); }
if (curveVisible.energy) { legend.push('能量'); series.push({ name: '能量', type: 'line', data: points.map(p => p.energy), smooth: true }); }
if (curveVisible.pressure) { legend.push('压力'); series.push({ name: '压力', type: 'line', data: points.map(p => p.pressure), smooth: true }); }
if (curveVisible.speed) { legend.push('转速'); series.push({ name: '转速', type: 'line', yAxisIndex: 1, data: points.map(p => p.speed), smooth: true }); }
curveChart.setOption({
tooltip: { trigger: 'axis' },
legend: { top: 0, data: legend },
grid: { left: 50, right: 60, top: 40, bottom: 30 },
xAxis: { type: 'category', data: points.map(p => p.xLabel) },
yAxis: [
{ type: 'value', name: '温度/功率/能量/压力' },
{ type: 'value', name: '转速', position: 'right' }
],
series,
toolbox: {
right: 10,
feature: {
dataZoom: { yAxisIndex: 'none' },
restore: {},
saveAsImage: { name: '密炼曲线' }
}
},
dataZoom: [{ type: 'inside' }, { type: 'slider', bottom: 0 }]
});
};
/* ======================== 生命周期 ======================== */
/** 导航索引变化时自动加载对应记录详情 */
watch(currentIndex, () => {
loadCurrentDetail();
});
const handleResize = () => { curveChart?.resize(); };
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
curveChart?.dispose();
});
</script>
<style scoped>
.lot-summary :deep(.el-descriptions__label) {
width: 80px;
}
</style>
Loading…
Cancel
Save