feat: 完成客户第12条需求+多系统改造

1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
This commit is contained in:
SunYF
2026-09-19 17:04:58 +08:00
parent 62e45b7379
commit 05b0b8a909
89 changed files with 7912 additions and 429 deletions
+112 -8
View File
@@ -1,6 +1,6 @@
<script setup>
defineOptions({ name: 'Outbound' })
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { getRealName } from '../utils/auth'
@@ -118,6 +118,7 @@ const OWNERSHIP_OPTIONS = [
const zones = ref([])
const genForm = reactive({
materialCode: '', mode: 1, batchNo: '', qty: 1, zoneCode: '',
shelfNo: '', layerNo: '', positionNo: '',
boxNo: '', contractNo: '', remark: '',
outboundCategory: 'other', ownershipType: 'none', ownershipNo: '', targetStation: ''
})
@@ -155,6 +156,73 @@ const zoneOptions = computed(() => {
return [...map.values()]
})
// 库位三级联动(与入库同口径):基于已加载 zones(每行是完整四级货位)派生可选项
function zoneShelves(zoneCode) {
const set = new Set()
zones.value.forEach(z => { if (z.zoneCode === zoneCode && z.shelfNo) set.add(z.shelfNo) })
return [...set].sort()
}
function zoneLayers(zoneCode, shelfNo) {
const set = new Set()
zones.value.forEach(z => { if (z.zoneCode === zoneCode && z.shelfNo === shelfNo && z.layerNo) set.add(z.layerNo) })
return [...set].sort()
}
function zonePositions(zoneCode, shelfNo, layerNo) {
const set = new Set()
zones.value.forEach(z => { if (z.zoneCode === zoneCode && z.shelfNo === shelfNo && z.layerNo === layerNo && z.positionNo) set.add(z.positionNo) })
return [...set].sort()
}
function onGenZoneChange() {
genForm.shelfNo = ''; genForm.layerNo = ''; genForm.positionNo = ''
if (genForm.materialCode || genForm.batchNo) fillZoneFromHistory()
}
function onGenShelfChange() { genForm.layerNo = ''; genForm.positionNo = '' }
function onGenLayerChange() { genForm.positionNo = '' }
function locText(row) {
const parts = []
if (row?.shelfNo) parts.push(row.shelfNo + '架')
if (row?.layerNo) parts.push(row.layerNo + '层')
if (row?.positionNo) parts.push(row.positionNo + '位')
return parts.length ? parts.join('/') : '-'
}
// 出库库位联动(客户诉求 一.9「出库相同」):与入库同口径回填 区域/货架/层/位置 四级。
// 优先级:① 指定批次在库的实际库位 ② 该物料最近一次入库库位 ③ 无历史时按所选区域推荐同区空位。
async function fillZoneFromHistory() {
if (!genForm.materialCode && !genForm.batchNo) return
let filled = false
try {
const loc = await request.get('/outbound/location-hint', {
params: { materialCode: genForm.materialCode, batchNo: genForm.batchNo }
})
if (loc && loc.zoneCode) {
genForm.zoneCode = loc.zoneCode
genForm.shelfNo = loc.shelfNo || ''
genForm.layerNo = loc.layerNo || ''
genForm.positionNo = loc.positionNo || ''
filled = true
}
} catch {
/* 查询失败不影响主流程 */
}
if (filled) return
// 无历史库位:按区域推荐同区空位(与入库同源接口,返回数组取第一条)
try {
const v = await request.get('/zone/vacant', { params: { zoneCode: genForm.zoneCode || '', limit: 1 } })
const hit = Array.isArray(v) ? v[0] : (v?.list || [])[0]
if (hit) {
genForm.zoneCode = hit.zoneCode
genForm.shelfNo = hit.shelfNo || ''
genForm.layerNo = hit.layerNo || ''
genForm.positionNo = hit.positionNo || ''
}
} catch {
/* 忽略 */
}
}
watch(() => genForm.materialCode, (v) => { if (v) { genForm.batchNo = ''; fillZoneFromHistory() } })
watch(() => genForm.batchNo, (v) => { if (v && genForm.materialCode) fillZoneFromHistory() })
async function loadGenRows() {
loadingGen.value = true
try {
@@ -180,6 +248,9 @@ async function submitGen() {
ownershipNo: genForm.ownershipType !== 'none' ? genForm.ownershipNo.trim() : undefined,
targetStation: genForm.targetStation.trim() || undefined,
zoneCode: genForm.zoneCode || undefined,
shelfNo: genForm.shelfNo || undefined,
layerNo: genForm.layerNo || undefined,
positionNo: genForm.positionNo || undefined,
boxNo: genForm.boxNo.trim() || undefined,
contractNo: genForm.contractNo.trim() || undefined,
remark: genForm.remark.trim() || undefined
@@ -198,7 +269,7 @@ async function submitGen() {
try {
const res = await request.post('/outbound/general', payload)
ElMessage.success(`通用出库成功:${res?.outboundNo}${res?.boxNo ? ' 箱号 ' + res.boxNo : ''}`)
Object.assign(genForm, { materialCode: '', mode: 1, batchNo: '', qty: 1, zoneCode: '', boxNo: '', contractNo: '', remark: '', outboundCategory: 'other', ownershipType: 'none', ownershipNo: '', targetStation: '' })
Object.assign(genForm, { materialCode: '', mode: 1, batchNo: '', qty: 1, zoneCode: '', shelfNo: '', layerNo: '', positionNo: '', boxNo: '', contractNo: '', remark: '', outboundCategory: 'other', ownershipType: 'none', ownershipNo: '', targetStation: '' })
snInput.value = ''
snList.value = []
genPage.current = 1
@@ -208,7 +279,7 @@ async function submitGen() {
}
/* ===================== 出库记录(统一主表,全部 + 导出) ===================== */
const recFilters = reactive({ outboundType: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: [] })
const recFilters = reactive({ outboundType: '', status: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: [] })
const recRows = ref([])
const recTotal = ref(0)
const loadingRec = ref(false)
@@ -221,6 +292,7 @@ async function loadRecords() {
try {
const params = {
outboundType: recFilters.outboundType,
status: recFilters.status,
materialCode: recFilters.materialCode.trim(),
boxNo: recFilters.boxNo.trim(),
ownershipType: recFilters.ownershipType,
@@ -236,11 +308,12 @@ async function loadRecords() {
} finally { loadingRec.value = false }
}
function searchRec() { recPage.current = 1; loadRecords() }
function resetRec() { Object.assign(recFilters, { outboundType: '', materialCode: '', boxNo: '', dateRange: [] }); searchRec() }
function resetRec() { Object.assign(recFilters, { outboundType: '', status: '', materialCode: '', boxNo: '', dateRange: [] }); searchRec() }
function downloadCSV() {
exportXlsxFetch('/api/outbound/export', {
outboundType: recFilters.outboundType,
status: recFilters.status,
materialCode: recFilters.materialCode.trim(),
boxNo: recFilters.boxNo.trim()
}, '出库管理.xlsx')
@@ -374,11 +447,24 @@ onMounted(loadDicts)
</el-form-item>
</template>
<el-form-item label="出库区域">
<el-select v-model="genForm.zoneCode" filterable clearable placeholder="可选" style="width:100%">
<el-form-item label="区域">
<el-select v-model="genForm.zoneCode" filterable clearable placeholder="选择区域(选物料/批次后自动带出最近库位)" style="width:100%" @change="onGenZoneChange">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode" :label="`${z.zoneCode} ${z.zoneName}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / 层 / 位置">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="genForm.shelfNo" placeholder="货架" clearable style="width:120px" @change="onGenShelfChange">
<el-option v-for="s in zoneShelves(genForm.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
</el-select>
<el-select v-model="genForm.layerNo" placeholder="层" clearable style="width:100px" :disabled="!genForm.shelfNo" @change="onGenLayerChange">
<el-option v-for="l in zoneLayers(genForm.zoneCode, genForm.shelfNo)" :key="l" :label="l + ' 层'" :value="l" />
</el-select>
<el-select v-model="genForm.positionNo" placeholder="位置" clearable style="width:110px" :disabled="!genForm.shelfNo">
<el-option v-for="p in zonePositions(genForm.zoneCode, genForm.shelfNo, genForm.layerNo)" :key="p" :label="p + ' 位'" :value="p" />
</el-select>
</div>
</el-form-item>
<el-form-item label="箱号">
<el-input v-model="genForm.boxNo" placeholder="选填:整箱发货时填写,装箱信息随出库单记录(无需单独录入装箱)" clearable style="width:100%" />
</el-form-item>
@@ -401,6 +487,9 @@ onMounted(loadDicts)
<el-table-column prop="batchNo" label="批次号" min-width="120">
<template #default="{ row }">{{ row.batchNo || '-' }}</template>
</el-table-column>
<el-table-column label="库位" min-width="120" align="center">
<template #default="{ row }">{{ row.zoneCode ? row.zoneCode + ' ' + locText(row) : '-' }}</template>
</el-table-column>
<el-table-column label="SN" min-width="180">
<template #default="{ row }">{{ row.snCodes || '-' }}</template>
</el-table-column>
@@ -440,9 +529,16 @@ onMounted(loadDicts)
<el-form-item label="类型">
<el-select v-model="recFilters.outboundType" clearable placeholder="全部" style="width:140px">
<el-option label="备料出库" value="workorder" />
<el-option label="工位补料" value="refill" />
<el-option label="通用出库" value="general" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="recFilters.status" clearable placeholder="全部" style="width:120px">
<el-option label="已出库" value="已出库" />
<el-option label="已送达" value="已送达" />
</el-select>
</el-form-item>
<el-form-item label="物料">
<MaterialSelect v-model="recFilters.materialCode" placeholder="物料" width="180px" />
</el-form-item>
@@ -466,11 +562,16 @@ onMounted(loadDicts)
<el-table-column prop="outboundNo" label="出库单号" min-width="150" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.outboundType === 'general' ? 'warning' : 'primary'" size="small">
{{ row.outboundType === 'general' ? '通用' : '备料' }}
<el-tag :type="row.outboundType === 'general' ? 'warning' : row.outboundType === 'refill' ? 'danger' : 'primary'" size="small">
{{ row.outboundType === 'general' ? '通用' : row.outboundType === 'refill' ? '补料' : '备料' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '已送达' ? 'success' : 'info'" size="small">{{ row.status || '已出库' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderNo" label="工单号" min-width="130">
<template #default="{ row }">{{ row.orderNo || '-' }}</template>
</el-table-column>
@@ -478,6 +579,9 @@ onMounted(loadDicts)
<el-table-column prop="batchNo" label="批次号" min-width="110">
<template #default="{ row }">{{ row.batchNo || '-' }}</template>
</el-table-column>
<el-table-column label="库位" min-width="120" align="center">
<template #default="{ row }">{{ row.zoneCode ? row.zoneCode + ' ' + locText(row) : '-' }}</template>
</el-table-column>
<el-table-column label="SN" min-width="160">
<template #default="{ row }">{{ row.snCodes || '-' }}</template>
</el-table-column>