change(reverseTrace): 初始化,模拟数据替换为真实数据库数据

新增API接口和类型定义,重构追溯页面逻辑
- 添加三个API接口:按批次码追溯、获取工单投料信息、获取质检明细
- 定义完整的类型结构,与后端数据结构对齐
- 优化页面加载状态和错误处理
- 实现工单展开懒加载和质检明细弹窗功能
- 完善状态显示和空数据提示
master
zangch@mesnac.com 1 month ago
parent 5893e95e75
commit 954540adc7

@ -0,0 +1,37 @@
import request from '@/utils/request'
import type { AxiosPromise } from 'axios'
import type { ReverseTraceData, MaterialInput, QcCheckItem } from './types'
/**
* - API
*
*
* 1. ->
* 2. planId + industryType ->
* 3. inspectionId ->
*/
/** 按成品批次码进行全链路反向追溯 */
export function getReverseTraceByBatch(batchCode: string): AxiosPromise<ReverseTraceData> {
return request({
url: `/mes/reverseTrace/batch/${batchCode}`,
method: 'get'
})
}
/** 查询工单的原材料投料信息(展开行懒加载) */
export function getMaterialInputs(planId: number, industryType: string): AxiosPromise<MaterialInput[]> {
return request({
url: '/mes/reverseTrace/workOrder/materialInputs',
method: 'get',
params: { planId, industryType }
})
}
/** 查询质检检验项明细(弹窗懒加载) */
export function getQcCheckItems(inspectionId: number): AxiosPromise<QcCheckItem[]> {
return request({
url: `/mes/reverseTrace/qc/detail/${inspectionId}`,
method: 'get'
})
}

@ -0,0 +1,133 @@
/**
* - TypeScript
* ReverseTraceVo / TraceMaterialInputVo / TraceQcCheckItemVo
*/
/** 质检检验项明细 */
export interface QcCheckItem {
/** 检验项目名称 */
itemName: string
/** 标准值 */
standard: string
/** 实际检测值 */
actual: string
/** 检验结果: 合格/不合格/未判定 */
result: string
}
/** 成品信息 */
export interface ProductInfo {
batchCode: string
productCode: string
productName: string
spec: string
productionDate: string
/** 状态: 已出库/已质检/生产完成 */
status: string
}
/** 客户明细数据 */
export interface CustomerData {
customerCode: string
customerName: string
contactPerson: string
contactPhone: string
deliveryAddress: string
outboundTime: string
outboundQty: string
invoiceNo: string
}
/** 客户信息(条件展示) */
export interface CustomerInfo {
/** 是否有出库记录 */
hasOutbound: boolean
/** 客户明细hasOutbound=true时有值 */
data: CustomerData | null
}
/** 成品质检信息 */
export interface QcInfo {
/** 质检主表ID点击"检验明细"时使用) */
inspectionId: number
qcCode: string
batchCode: string
/** 为后续扩展预留,当前页面暂未展示 */
productCode: string
productName: string
spec: string
qcTime: string
qcType: string
inspector: string
result: string
/** 质检明细(成品质检直接返回) */
checkItems: QcCheckItem[]
}
/** 生产订单信息 */
export interface ProductionOrder {
orderCode: string
batchCode: string
productName: string
dispatchType: string
dispatchTypeName: string
dispatchInfo: string
planQty: string
dispatchedQty: string
completedQty: string
startTime: string
endTime: string
status: string
}
/** 生产工单 */
export interface WorkOrder {
/** 工单ID展开时传给后端查询投料信息 */
planId: number
/** 行业类型(展开时传回后端) */
industryType: string
processSeq: number
workOrderCode: string
processCode: string
processName: string
machineNo: string
machineName: string
startTime: string
endTime: string
worker: string
status: string
/** 投料明细(展开懒加载填充) */
materialInputs?: MaterialInput[]
/** 加载状态 */
loading?: boolean
}
/** 原材料投料信息 */
export interface MaterialInput {
/** 触发本条投料链路的产出条码 */
productionBarcode: string
/** 投入扫描记录主键,便于后续穿透到扫描明细 */
inputScanId: number
materialCode: string
materialName: string
batchCode: string
supplier: string
qty: string
unit: string
inTime: string
qcCode: string
qcResult: string
/** 质检主表ID点击"检验明细"时使用) */
inspectionId: number
}
/** 整页追溯数据 */
export interface ReverseTraceData {
/** 行业类型: TIRE/JJ */
industryType: string
productInfo: ProductInfo
customerInfo: CustomerInfo
qcInfo: QcInfo
productionOrder: ProductionOrder
workOrderList: WorkOrder[]
}

@ -0,0 +1,463 @@
<template>
<div class="reverse-trace-container">
<el-card class="query-card" shadow="never">
<el-form :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="成品批次码">
<el-input
v-model="queryParams.batchCode"
placeholder="扫描或输入成品批次码"
clearable
style="width: 280px"
@keyup.enter="handleQuery"
>
<template #append>
<el-button icon="Scan" @click="handleScan" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<div v-if="traceData" class="trace-content">
<el-row :gutter="10">
<el-col :span="24">
<el-card shadow="hover" class="product-info-card">
<template #header>
<span class="card-title">成品信息</span>
</template>
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="批次码">{{ traceData.productInfo.batchCode }}</el-descriptions-item>
<el-descriptions-item label="产品编码">{{ traceData.productInfo.productCode }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ traceData.productInfo.productName }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ traceData.productInfo.spec }}</el-descriptions-item>
<el-descriptions-item label="生产日期">{{ traceData.productInfo.productionDate }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="traceData.productInfo.status === '已入库' ? 'success' : 'info'">
{{ traceData.productInfo.status }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2" v-if="traceData.customerInfo && traceData.customerInfo.hasOutbound">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">客户信息</span>
</template>
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="客户编码">{{ traceData.customerInfo.data.customerCode }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ traceData.customerInfo.data.customerName }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ traceData.customerInfo.data.contactPerson }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ traceData.customerInfo.data.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="交货地址" :span="2">{{ traceData.customerInfo.data.deliveryAddress }}</el-descriptions-item>
<el-descriptions-item label="出库时间">{{ traceData.customerInfo.data.outboundTime }}</el-descriptions-item>
<el-descriptions-item label="出库数量">{{ traceData.customerInfo.data.outboundQty }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">质检信息</span>
</template>
<el-descriptions :column="4" border size="small" class="mb-3">
<el-descriptions-item label="质检单号">{{ traceData.qcInfo.qcCode }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ traceData.qcInfo.batchCode }}</el-descriptions-item>
<el-descriptions-item label="质检时间">{{ traceData.qcInfo.qcTime }}</el-descriptions-item>
<el-descriptions-item label="质检类型">{{ traceData.qcInfo.qcType }}</el-descriptions-item>
<el-descriptions-item label="质检员">{{ traceData.qcInfo.inspector }}</el-descriptions-item>
<el-descriptions-item label="结果">
<el-tag :type="traceData.qcInfo.result === '合格' ? 'success' : 'danger'">
{{ traceData.qcInfo.result }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-table :data="traceData.qcInfo.checkItems" border size="small" max-height="200">
<el-table-column prop="itemName" label="检验项目" min-width="120" />
<el-table-column prop="standard" label="标准" min-width="180" show-overflow-tooltip />
<el-table-column prop="actual" label="实际值" min-width="120" />
<el-table-column prop="result" label="结果" min-width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.result === '合格' ? 'success' : 'danger'" size="small">
{{ row.result }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">生产订单信息</span>
</template>
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="订单号">{{ traceData.productionOrder.orderCode }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ traceData.productionOrder.batchCode }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ traceData.productionOrder.productName }}</el-descriptions-item>
<el-descriptions-item label="派工类型">
{{ traceData.productionOrder.dispatchType }} - {{ traceData.productionOrder.dispatchTypeName }}
</el-descriptions-item>
<el-descriptions-item label="派工信息">{{ traceData.productionOrder.dispatchInfo }}</el-descriptions-item>
<el-descriptions-item label="计划数量">{{ traceData.productionOrder.planQty }}</el-descriptions-item>
<el-descriptions-item label="已派工数量">{{ traceData.productionOrder.dispatchedQty }}</el-descriptions-item>
<el-descriptions-item label="完成数量">{{ traceData.productionOrder.completedQty }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ traceData.productionOrder.startTime }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ traceData.productionOrder.endTime }}</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag :type="traceData.productionOrder.status === '已完成' ? 'success' : 'warning'" size="small">
{{ traceData.productionOrder.status }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">生产工单信息</span>
</template>
<el-table ref="workOrderTableRef" :data="traceData.workOrderList" border size="small" max-height="400" row-key="workOrderCode" :expand-row-keys="expandedRowKeys" @expand-change="handleExpandChange">
<el-table-column type="expand" width="50" align="center">
<template #default="{ row }">
<div class="expand-content" v-if="row.materialInputs">
<el-table :data="row.materialInputs" border size="small" class="material-input-table" v-loading="row.loading">
<el-table-column prop="materialCode" label="物料编码" min-width="120" />
<el-table-column prop="materialName" label="物料名称" min-width="150" />
<el-table-column prop="batchCode" label="批次码" min-width="140" />
<el-table-column prop="supplier" label="供应商" min-width="150" show-overflow-tooltip />
<el-table-column prop="qty" label="投料数量" min-width="100" align="right" />
<el-table-column prop="unit" label="单位" min-width="60" align="center" />
<el-table-column prop="inTime" label="投料时间" min-width="160" />
<el-table-column prop="qcCode" label="质检单号" min-width="120" />
<el-table-column prop="qcResult" label="质检结果" min-width="90" align="center">
<template #default="{ row: materialRow }">
<el-tag :type="materialRow.qcResult === '合格' ? 'success' : 'danger'" size="small">
{{ materialRow.qcResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row: materialRow }">
<el-button type="primary" link size="small" @click="showMaterialQcDetail(materialRow)">
检验明细
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="expand-loading">
<el-icon class="el-icon-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</template>
</el-table-column>
<el-table-column prop="processSeq" label="工序序号" width="90" align="center" />
<el-table-column prop="workOrderCode" label="工单编码" min-width="130" />
<el-table-column prop="processCode" label="工序编码" min-width="100" />
<el-table-column prop="processName" label="工序名称" min-width="100" />
<el-table-column prop="machineNo" label="机台编号" min-width="100" />
<el-table-column prop="machineName" label="机台名称" min-width="120" />
<el-table-column prop="startTime" label="开始时间" min-width="160" />
<el-table-column prop="endTime" label="结束时间" min-width="160" />
<el-table-column prop="worker" label="作业员" min-width="80" />
<el-table-column prop="status" label="状态" min-width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '已完成' ? 'success' : 'warning'" size="small">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
<el-empty v-else-if="!loading && hasSearched" description="请输入成品批次码进行追溯" />
<el-dialog v-model="materialQcDialogVisible" title="原材料检验明细" width="700px" append-to-body>
<el-descriptions :column="2" border size="small" class="mb-3">
<el-descriptions-item label="物料编码">{{ currentMaterialInput?.materialCode }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ currentMaterialInput?.materialName }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ currentMaterialInput?.batchCode }}</el-descriptions-item>
<el-descriptions-item label="质检单号">{{ currentMaterialInput?.qcCode }}</el-descriptions-item>
</el-descriptions>
<el-table :data="currentMaterialInput?.checkItems" border size="small" max-height="300">
<el-table-column prop="itemName" label="检验项目" min-width="120" />
<el-table-column prop="standard" label="标准" min-width="180" show-overflow-tooltip />
<el-table-column prop="actual" label="实际值" min-width="120" />
<el-table-column prop="result" label="结果" min-width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.result === '合格' ? 'success' : 'danger'" size="small">
{{ row.result }}
</el-tag>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="materialQcDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="ReverseTrace">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import { reverseTraceByBatch, getMaterialInputsByWorkOrder } from './data/mockData';
interface CheckItem {
itemName: string;
standard: string;
actual: string;
result: string;
}
interface MaterialInput {
materialCode: string;
materialName: string;
batchCode: string;
supplier: string;
qty: number;
unit: string;
inTime: string;
qcCode: string;
qcResult: string;
checkItems: CheckItem[];
}
interface WorkOrder {
workOrderCode: string;
processSeq: number;
processCode: string;
processName: string;
machineNo: string;
machineName: string;
startTime: string;
endTime: string;
worker: string;
status: string;
materialInputs?: MaterialInput[];
loading?: boolean;
}
interface ProductionOrder {
orderCode: string;
batchCode: string;
productCode: string;
productName: string;
dispatchType: number;
dispatchTypeName: string;
dispatchInfo: string;
planQty: number;
dispatchedQty: number;
completedQty: number;
startTime: string;
endTime: string;
status: string;
}
interface CustomerInfo {
hasOutbound: boolean;
data: {
customerCode: string;
customerName: string;
contactPerson: string;
contactPhone: string;
deliveryAddress: string;
outboundTime: string;
outboundQty: number;
invoiceNo: string;
} | null;
}
interface TraceData {
productInfo: {
batchCode: string;
productCode: string;
productName: string;
spec: string;
productionDate: string;
status: string;
};
qcInfo: {
qcCode: string;
batchCode: string;
productCode: string;
productName: string;
spec: string;
qcTime: string;
qcType: string;
inspector: string;
result: string;
checkItems: CheckItem[];
};
customerInfo?: CustomerInfo;
productionOrder: ProductionOrder;
workOrderList: WorkOrder[];
}
const queryParams = reactive({
batchCode: ''
});
const loading = ref(false);
const hasSearched = ref(false);
const traceData = ref<TraceData | null>(null);
const workOrderTableRef = ref();
const expandedRowKeys = ref<string[]>([]);
const materialQcDialogVisible = ref(false);
const currentMaterialInput = ref<MaterialInput | null>(null);
const handleQuery = () => {
if (!queryParams.batchCode) {
ElMessage.warning('请输入成品批次码');
return;
}
loading.value = true;
hasSearched.value = true;
expandedRowKeys.value = [];
setTimeout(() => {
const result = reverseTraceByBatch(queryParams.batchCode);
if (result.code === 200) {
traceData.value = result.data;
ElMessage.success('追溯成功');
} else {
traceData.value = null;
ElMessage.error(result.message || '追溯失败');
}
loading.value = false;
}, 300);
};
const resetQuery = () => {
queryParams.batchCode = '';
traceData.value = null;
hasSearched.value = false;
expandedRowKeys.value = [];
};
const handleScan = () => {
ElMessage.info('扫描功能开发中,请手动输入批次码');
};
const handleExpandChange = async (row: WorkOrder, expanded?: boolean) => {
const key = row.workOrderCode;
if (expanded) {
if (!row.materialInputs && !row.loading) {
row.loading = true;
if (!expandedRowKeys.value.includes(key)) {
expandedRowKeys.value.push(key);
}
try {
const result = await getMaterialInputsByWorkOrder(row.workOrderCode) as any;
if (result.code === 200) {
row.materialInputs = result.data;
}
} finally {
row.loading = false;
}
} else if (!expandedRowKeys.value.includes(key)) {
expandedRowKeys.value.push(key);
}
} else {
const index = expandedRowKeys.value.indexOf(key);
if (index > -1) {
expandedRowKeys.value.splice(index, 1);
}
}
};
const showMaterialQcDetail = (row: MaterialInput) => {
currentMaterialInput.value = row;
materialQcDialogVisible.value = true;
};
</script>
<style scoped>
.reverse-trace-container {
padding: 10px;
}
.query-card {
margin-bottom: 10px;
}
.trace-content {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-title {
font-weight: 600;
font-size: 14px;
}
.mt-2 {
margin-top: 10px;
}
.mb-3 {
margin-bottom: 12px;
}
.expand-content {
padding: 10px 10px 10px 50px;
}
.expand-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
}
.expand-loading .el-icon-loading {
margin-right: 8px;
}
.material-input-table {
background-color: #f5f7fa;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
</style>

@ -16,13 +16,14 @@
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button type="primary" icon="Search" :loading="loading" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<div v-if="traceData" class="trace-content">
<div v-if="traceData" class="trace-content" v-loading="loading">
<!-- 成品信息 -->
<el-row :gutter="10">
<el-col :span="24">
<el-card shadow="hover" class="product-info-card">
@ -30,14 +31,14 @@
<span class="card-title">成品信息</span>
</template>
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="批次码">{{ traceData.productInfo.batchCode }}</el-descriptions-item>
<el-descriptions-item label="产品编码">{{ traceData.productInfo.productCode }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ traceData.productInfo.productName }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ traceData.productInfo.spec }}</el-descriptions-item>
<el-descriptions-item label="生产日期">{{ traceData.productInfo.productionDate }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ traceData.productInfo?.batchCode }}</el-descriptions-item>
<el-descriptions-item label="产品编码">{{ traceData.productInfo?.productCode }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ traceData.productInfo?.productName }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ traceData.productInfo?.spec }}</el-descriptions-item>
<el-descriptions-item label="生产日期">{{ traceData.productInfo?.productionDate }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="traceData.productInfo.status === '已入库' ? 'success' : 'info'">
{{ traceData.productInfo.status }}
<el-tag :type="traceData.productInfo?.status === '已出库' ? 'success' : traceData.productInfo?.status === '已质检' ? 'warning' : 'info'">
{{ traceData.productInfo?.status }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
@ -45,7 +46,8 @@
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2" v-if="traceData.customerInfo && traceData.customerInfo.hasOutbound">
<!-- 客户信息仅出库时显示 -->
<el-row :gutter="10" class="mt-2" v-if="traceData.customerInfo?.hasOutbound && traceData.customerInfo?.data">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
@ -64,7 +66,8 @@
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2">
<!-- 质检信息 -->
<el-row :gutter="10" class="mt-2" v-if="traceData.qcInfo">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
@ -82,7 +85,7 @@
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-table :data="traceData.qcInfo.checkItems" border size="small" max-height="200">
<el-table v-if="traceData.qcInfo.checkItems?.length" :data="traceData.qcInfo.checkItems" border size="small" max-height="200">
<el-table-column prop="itemName" label="检验项目" min-width="120" />
<el-table-column prop="standard" label="标准" min-width="180" show-overflow-tooltip />
<el-table-column prop="actual" label="实际值" min-width="120" />
@ -98,7 +101,8 @@
</el-col>
</el-row>
<el-row :gutter="10" class="mt-2">
<!-- 生产订单信息 -->
<el-row :gutter="10" class="mt-2" v-if="traceData.productionOrder">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
@ -127,13 +131,23 @@
</el-col>
</el-row>
<!-- 生产工单信息主子模式展开显示原材料投料 -->
<el-row :gutter="10" class="mt-2">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">生产工单信息</span>
</template>
<el-table ref="workOrderTableRef" :data="traceData.workOrderList" border size="small" max-height="400" row-key="workOrderCode" :expand-row-keys="expandedRowKeys" @expand-change="handleExpandChange">
<el-table
ref="workOrderTableRef"
:data="traceData.workOrderList"
border
size="small"
max-height="400"
row-key="planId"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
>
<el-table-column type="expand" width="50" align="center">
<template #default="{ row }">
<div class="expand-content" v-if="row.materialInputs">
@ -148,22 +162,30 @@
<el-table-column prop="qcCode" label="质检单号" min-width="120" />
<el-table-column prop="qcResult" label="质检结果" min-width="90" align="center">
<template #default="{ row: materialRow }">
<el-tag :type="materialRow.qcResult === '合格' ? 'success' : 'danger'" size="small">
<el-tag v-if="materialRow.qcResult" :type="materialRow.qcResult === '合格' ? 'success' : 'danger'" size="small">
{{ materialRow.qcResult }}
</el-tag>
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row: materialRow }">
<el-button type="primary" link size="small" @click="showMaterialQcDetail(materialRow)">
<el-button
v-if="materialRow.inspectionId"
type="primary"
link
size="small"
@click="showMaterialQcDetail(materialRow)"
>
检验明细
</el-button>
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="expand-loading">
<el-icon class="el-icon-loading"><Loading /></el-icon>
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</template>
@ -179,7 +201,7 @@
<el-table-column prop="worker" label="作业员" min-width="80" />
<el-table-column prop="status" label="状态" min-width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '已完成' ? 'success' : 'warning'" size="small">
<el-tag :type="row.status === '已完成' ? 'success' : row.status === '已开始' ? 'warning' : 'info'" size="small">
{{ row.status }}
</el-tag>
</template>
@ -190,16 +212,17 @@
</el-row>
</div>
<el-empty v-else-if="!loading && hasSearched" description="请输入成品批次码进行追溯" />
<el-empty v-else-if="!loading && hasSearched" description="未查询到该批次码对应的追溯数据" />
<!-- 原材料质检明细弹窗 -->
<el-dialog v-model="materialQcDialogVisible" title="原材料检验明细" width="700px" append-to-body>
<el-descriptions :column="2" border size="small" class="mb-3">
<el-descriptions-item label="物料编码">{{ currentMaterialInput?.materialCode }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ currentMaterialInput?.materialName }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ currentMaterialInput?.batchCode }}</el-descriptions-item>
<el-descriptions-item label="质检单号">{{ currentMaterialInput?.qcCode }}</el-descriptions-item>
<el-descriptions v-if="currentMaterialInput" :column="2" border size="small" class="mb-3">
<el-descriptions-item label="物料编码">{{ currentMaterialInput.materialCode }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ currentMaterialInput.materialName }}</el-descriptions-item>
<el-descriptions-item label="批次码">{{ currentMaterialInput.batchCode }}</el-descriptions-item>
<el-descriptions-item label="质检单号">{{ currentMaterialInput.qcCode }}</el-descriptions-item>
</el-descriptions>
<el-table :data="currentMaterialInput?.checkItems" border size="small" max-height="300">
<el-table :data="currentMaterialCheckItems" border size="small" max-height="300" v-loading="materialQcLoading">
<el-table-column prop="itemName" label="检验项目" min-width="120" />
<el-table-column prop="standard" label="标准" min-width="180" show-overflow-tooltip />
<el-table-column prop="actual" label="实际值" min-width="120" />
@ -219,177 +242,139 @@
</template>
<script setup lang="ts" name="ReverseTrace">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import { reverseTraceByBatch, getMaterialInputsByWorkOrder } from './data/mockData';
interface CheckItem {
itemName: string;
standard: string;
actual: string;
result: string;
}
interface MaterialInput {
materialCode: string;
materialName: string;
batchCode: string;
supplier: string;
qty: number;
unit: string;
inTime: string;
qcCode: string;
qcResult: string;
checkItems: CheckItem[];
}
interface WorkOrder {
workOrderCode: string;
processSeq: number;
processCode: string;
processName: string;
machineNo: string;
machineName: string;
startTime: string;
endTime: string;
worker: string;
status: string;
materialInputs?: MaterialInput[];
loading?: boolean;
}
interface ProductionOrder {
orderCode: string;
batchCode: string;
productCode: string;
productName: string;
dispatchType: number;
dispatchTypeName: string;
dispatchInfo: string;
planQty: number;
dispatchedQty: number;
completedQty: number;
startTime: string;
endTime: string;
status: string;
}
interface CustomerInfo {
hasOutbound: boolean;
data: {
customerCode: string;
customerName: string;
contactPerson: string;
contactPhone: string;
deliveryAddress: string;
outboundTime: string;
outboundQty: number;
invoiceNo: string;
} | null;
}
interface TraceData {
productInfo: {
batchCode: string;
productCode: string;
productName: string;
spec: string;
productionDate: string;
status: string;
};
qcInfo: {
qcCode: string;
batchCode: string;
productCode: string;
productName: string;
spec: string;
qcTime: string;
qcType: string;
inspector: string;
result: string;
checkItems: CheckItem[];
};
customerInfo?: CustomerInfo;
productionOrder: ProductionOrder;
workOrderList: WorkOrder[];
}
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import { getReverseTraceByBatch, getMaterialInputs, getQcCheckItems } from '@/api/mes/reverseTrace'
import type { ReverseTraceData, WorkOrder, MaterialInput, QcCheckItem } from '@/api/mes/reverseTrace/types'
const queryParams = reactive({
batchCode: ''
});
})
const loading = ref(false);
const hasSearched = ref(false);
const traceData = ref<TraceData | null>(null);
const workOrderTableRef = ref();
const expandedRowKeys = ref<string[]>([]);
const materialQcDialogVisible = ref(false);
const currentMaterialInput = ref<MaterialInput | null>(null);
const loading = ref(false)
const hasSearched = ref(false)
const traceData = ref<ReverseTraceData | null>(null)
const workOrderTableRef = ref()
const expandedRowKeys = ref<string[]>([])
const handleQuery = () => {
if (!queryParams.batchCode) {
ElMessage.warning('请输入成品批次码');
return;
//
const materialQcDialogVisible = ref(false)
const currentMaterialInput = ref<MaterialInput | null>(null)
const currentMaterialCheckItems = ref<QcCheckItem[]>([])
const materialQcLoading = ref(false)
/** 追溯查询 */
const handleQuery = async () => {
if (!queryParams.batchCode.trim()) {
ElMessage.warning('请输入成品批次码')
return
}
loading.value = true;
hasSearched.value = true;
expandedRowKeys.value = [];
setTimeout(() => {
const result = reverseTraceByBatch(queryParams.batchCode);
if (result.code === 200) {
traceData.value = result.data;
ElMessage.success('追溯成功');
loading.value = true
hasSearched.value = true
expandedRowKeys.value = []
traceData.value = null
try {
const res = await getReverseTraceByBatch(queryParams.batchCode.trim())
if (res.code === 200 && res.data) {
traceData.value = res.data
ElMessage.success('追溯成功')
} else {
traceData.value = null;
ElMessage.error(result.message || '追溯失败');
traceData.value = null
ElMessage.error(res.msg || '未查询到追溯数据')
}
loading.value = false;
}, 300);
};
} catch (error: any) {
traceData.value = null
ElMessage.error(error?.message || '追溯查询失败,请稍后重试')
} finally {
loading.value = false
}
}
/** 重置查询 */
const resetQuery = () => {
queryParams.batchCode = '';
traceData.value = null;
hasSearched.value = false;
expandedRowKeys.value = [];
};
queryParams.batchCode = ''
traceData.value = null
hasSearched.value = false
expandedRowKeys.value = []
}
/** 扫描按钮(预留扫码枪接口) */
const handleScan = () => {
ElMessage.info('扫描功能开发中,请手动输入批次码');
};
ElMessage.info('扫描功能开发中,请手动输入批次码')
}
/**
* 工单展开/折叠事件处理
* 展开时按 planId + industryType 懒加载投料信息
* 折叠时仅更新展开行key列表
*/
const handleExpandChange = async (row: WorkOrder, expanded?: boolean) => {
const key = row.workOrderCode;
const key = String(row.planId)
if (expanded) {
//
if (!row.materialInputs && !row.loading) {
row.loading = true;
row.loading = true
if (!expandedRowKeys.value.includes(key)) {
expandedRowKeys.value.push(key);
expandedRowKeys.value.push(key)
}
try {
const result = await getMaterialInputsByWorkOrder(row.workOrderCode) as any;
if (result.code === 200) {
row.materialInputs = result.data;
const industryType = row.industryType || traceData.value?.industryType || ''
const res = await getMaterialInputs(row.planId, industryType)
if (res.code === 200) {
row.materialInputs = res.data || []
} else {
row.materialInputs = []
ElMessage.warning(res.msg || '未查询到投料信息')
}
} catch {
row.materialInputs = []
ElMessage.error('加载投料信息失败')
} finally {
row.loading = false;
row.loading = false
}
} else if (!expandedRowKeys.value.includes(key)) {
expandedRowKeys.value.push(key);
expandedRowKeys.value.push(key)
}
} else {
const index = expandedRowKeys.value.indexOf(key);
const index = expandedRowKeys.value.indexOf(key)
if (index > -1) {
expandedRowKeys.value.splice(index, 1);
expandedRowKeys.value.splice(index, 1)
}
}
};
}
const showMaterialQcDetail = (row: MaterialInput) => {
currentMaterialInput.value = row;
materialQcDialogVisible.value = true;
};
/**
* 显示原材料质检明细弹窗
* 先打开弹窗再异步加载数据让用户立即感知操作已响应
*/
const showMaterialQcDetail = async (row: MaterialInput) => {
currentMaterialInput.value = row
currentMaterialCheckItems.value = []
materialQcDialogVisible.value = true
if (!row.inspectionId) {
return
}
materialQcLoading.value = true
try {
const res = await getQcCheckItems(row.inspectionId)
if (res.code === 200) {
currentMaterialCheckItems.value = res.data || []
} else {
ElMessage.warning(res.msg || '未查询到检验明细')
}
} catch {
ElMessage.error('加载检验明细失败')
} finally {
materialQcLoading.value = false
}
}
</script>
<style scoped>
@ -449,15 +434,7 @@ const showMaterialQcDetail = (row: MaterialInput) => {
background-color: #f5f7fa;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
.text-gray {
color: #909399;
}
</style>

Loading…
Cancel
Save