You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
18 KiB
Vue
441 lines
18 KiB
Vue
<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" :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" v-loading="loading">
|
|
<!-- 成品信息 -->
|
|
<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' : traceData.productInfo?.status === '已质检' ? 'warning' : '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?.hasOutbound && traceData.customerInfo?.data">
|
|
<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" v-if="traceData.qcInfo">
|
|
<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 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" />
|
|
<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" v-if="traceData.productionOrder">
|
|
<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="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">
|
|
<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 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
|
|
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="is-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' : row.status === '已开始' ? 'warning' : 'info'" 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 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="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" />
|
|
<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 { 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<ReverseTraceData | null>(null)
|
|
const workOrderTableRef = ref()
|
|
const expandedRowKeys = ref<string[]>([])
|
|
|
|
// 原材料质检明细弹窗状态
|
|
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 = []
|
|
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(res.msg || '未查询到追溯数据')
|
|
}
|
|
} 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 = []
|
|
}
|
|
|
|
/** 扫描按钮(预留扫码枪接口) */
|
|
const handleScan = () => {
|
|
ElMessage.info('扫描功能开发中,请手动输入批次码')
|
|
}
|
|
|
|
/**
|
|
* 工单展开/折叠事件处理
|
|
* 展开时按 planId + industryType 懒加载投料信息
|
|
* 折叠时仅更新展开行key列表
|
|
*/
|
|
const handleExpandChange = async (row: WorkOrder, expanded?: boolean) => {
|
|
const key = String(row.planId)
|
|
|
|
if (expanded) {
|
|
// 首次展开且尚未加载投料数据,触发懒加载
|
|
if (!row.materialInputs && !row.loading) {
|
|
row.loading = true
|
|
if (!expandedRowKeys.value.includes(key)) {
|
|
expandedRowKeys.value.push(key)
|
|
}
|
|
|
|
try {
|
|
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
|
|
}
|
|
} 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 = 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>
|
|
.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;
|
|
}
|
|
|
|
.text-gray {
|
|
color: #909399;
|
|
}
|
|
</style>
|