change(reverseTrace): 初始化,模拟数据替换为真实数据库数据
新增API接口和类型定义,重构追溯页面逻辑 - 添加三个API接口:按批次码追溯、获取工单投料信息、获取质检明细 - 定义完整的类型结构,与后端数据结构对齐 - 优化页面加载状态和错误处理 - 实现工单展开懒加载和质检明细弹窗功能 - 完善状态显示和空数据提示master
parent
5893e95e75
commit
954540adc7
@ -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>
|
||||
Loading…
Reference in New Issue