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

<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>