refactor: 全链路统一将物料编码改称图号,优化入库流程

1.  统一全系统UI与文档中的"物料编码"为客户口径"图号",覆盖前端页面、后端导出表头与帮助文档
2.  优化入库模块:
    - 新增归属类型"其他"并完善注释
    - 入库时自动回写入库单批次号
    - 优化库位选择为三级联动下拉选择,替换原手动输入
    - 新增入库归属编号录入功能
3. 更新需求文档,同步完成图号全链路改造的验收标记
This commit is contained in:
SunYF
2026-09-17 18:27:07 +08:00
parent 1cc93795ce
commit e31b85e87a
20 changed files with 162 additions and 68 deletions
+3 -3
View File
@@ -61,7 +61,7 @@ export const helpInbound = {
export const helpOutbound = {
title: '出库管理(统一主表)',
overview:
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n两个出库页签 + 一个记录页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/样品/报废/发货),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息)。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n说明:出库列表中的"图号"即物料编码(客户口径统一称"图号");通用出库涵盖退料/样品/报废/发货等场景,无固定分类下拉,具体去向记在"备注"。质量门禁:质量状态为「不合格/待退」的批次禁止领料上线,须先完成不合格处置。',
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n两个出库页签 + 一个记录页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/样品/报废/发货),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息)。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n说明:出库列表统一以"图号"展示物料(客户口径统一称"图号");通用出库涵盖退料/样品/报废/发货等场景,无固定分类下拉,具体去向记在"备注"。质量门禁:质量状态为「不合格/待退」的批次禁止领料上线,须先完成不合格处置。',
fields: [
{ name: '备料出库-工单号', source: '手动输入。数据来自 MES 已建的工单及导入的物料台账。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '输入工单号回车,查询该工单的物料台账' },
{ name: '备料出库-目标工位', source: '下拉选择 DOCK01~DOCK20(选填,可过滤搜索)。', purpose: 'AGV 配送送达的接驳台(toDock 参数)', fill: '选择如 DOCK05;不选默认运到产线入口 DOCK01' },
@@ -216,9 +216,9 @@ export const helpZone = {
export const helpMaterial = {
title: '物料档案',
overview:
'定义每一种物料的字典:叫什么、什么规格、什么品类(原材料/半成品/成品)、是【结构件(批次)】还是【电气件(SN)】。\n此处维护的物料会作为下拉选项出现在入库/出库/装箱等页面;其中"类型"直接决定后续入库方式——结构件走批次(一批多件)、电气件走 SN 逐件扫码(单件追溯)。\n\n品类说明:1=原材料(默认) / 2=半成品 / 3=成品。编码主数据单一来源在本表:产品型号档案即本表中"成品"品类的行(MES 实时拉取,不双录);库存主表 inventories 的品类与其呼应。列表支持按品类筛选,导出含品类列。\n\n物料档案是库房的另一道地基:先建好物料,入库时才能选到;类型选错会导致入库方式与追溯粒度不符。\n\n命名说明:物料编码在出库/看板等页面统一显示为"图号"(客户口径)。安全库存在本档案维护,可用量低于它时会在「库房看板」首页触发缺货预警。',
'定义每一种物料的字典:叫什么、什么规格、什么品类(原材料/半成品/成品)、是【结构件(批次)】还是【电气件(SN)】。\n此处维护的物料会作为下拉选项出现在入库/出库/装箱等页面;其中"类型"直接决定后续入库方式——结构件走批次(一批多件)、电气件走 SN 逐件扫码(单件追溯)。\n\n品类说明:1=原材料(默认) / 2=半成品 / 3=成品。编码主数据单一来源在本表:产品型号档案即本表中"成品"品类的行(MES 实时拉取,不双录);库存主表 inventories 的品类与其呼应。列表支持按品类筛选,导出含品类列。\n\n物料档案是库房的另一道地基:先建好物料,入库时才能选到;类型选错会导致入库方式与追溯粒度不符。\n\n命名说明:本档案维护的物料在出库/看板等页面统一"图号"展示(客户口径)。安全库存在本档案维护,可用量低于它时会在「库房看板」首页触发缺货预警。',
fields: [
{ name: '物料编码', source: '手工填写,仓库自定义(新增后不可改)。在出库/看板等页面显示为"图号"。', purpose: '物料唯一编号(即图号)', fill: '如 SCREW-M3;新增后不可改编码' },
{ name: '图号', source: '手工填写,仓库自定义(新增后不可改)。在出库/看板等页面显示为"图号"。', purpose: '物料唯一编号(即图号)', fill: '如 SCREW-M3;新增后不可改编码' },
{ name: '名称', source: '手工填写。', purpose: '物料名称', fill: '如 螺丝 M3' },
{ name: '简称', source: '手工填写(可空)。', purpose: '简写', fill: '可空' },
{ name: '规格', source: '手工填写(可空)。', purpose: '规格型号', fill: '可空' },
+32 -10
View File
@@ -1,7 +1,7 @@
<script setup>
defineOptions({ name: 'BaseData' })
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { can } from '../utils/perm'
@@ -50,7 +50,7 @@ async function loadZones() {
try {
const data = await request.get('/zone/list', {
params: {
zoneCode: zoneFilter.zoneCode.trim(),
zoneCode: zoneFilter.zoneCode.trim().toUpperCase(),
zoneName: zoneFilter.zoneName.trim(),
status: zoneFilter.status,
page: zonePage.current,
@@ -89,6 +89,15 @@ async function saveZone() {
ElMessage.warning('区域编码和区域名称必填')
return
}
try {
await ElMessageBox.confirm('确认保存该区域信息吗?保存后将写入数据库。', '操作确认', {
confirmButtonText: '确认保存',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
zoneSaving.value = true
try {
if (zoneEditingId.value) {
@@ -188,6 +197,15 @@ async function saveMaterial() {
ElMessage.warning('「其他」类物料必须填写规格型号')
return
}
try {
await ElMessageBox.confirm('确认保存该物料档案吗?保存后将写入数据库。', '操作确认', {
confirmButtonText: '确认保存',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
mSaving.value = true
try {
if (mEditingId.value) {
@@ -199,18 +217,21 @@ async function saveMaterial() {
manageMode: mForm.manageMode
})
ElMessage.success('物料修改成功')
matDialog.value = false
} else {
await request.post('/material/create', { ...mForm, itemType: Number(mForm.itemType), safetyStock: Number(mForm.safetyStock) || 0 })
ElMessage.success('物料新增成功')
const created = await request.post('/material/create', { ...mForm, itemType: Number(mForm.itemType), safetyStock: Number(mForm.safetyStock) || 0 })
ElMessage.success('物料新增成功,可继续上传图纸/附件')
// 创建成功后切到编辑态:保持弹窗打开,便于立即上传附件
mEditingId.value = created?.id || 0
if (created?.code) mForm.code = created.code
}
matDialog.value = false
await loadMaterials()
} finally {
mSaving.value = false
}
}
// 物料附件(bizType=materialbizId=物料编码):零件图纸 PDF 等
// 物料附件(bizType=materialbizId=图号):零件图纸 PDF 等
const attachDrawer = reactive({ visible: false, code: '', name: '' })
function openMatAttach(row) {
Object.assign(attachDrawer, { visible: true, code: row.code, name: row.name })
@@ -308,7 +329,7 @@ async function onMaterialFile(e) {
<el-form inline style="margin-bottom:14px" @submit.prevent="searchZones">
<el-form-item label="区域编码">
<el-input v-model="zoneFilter.zoneCode" placeholder="区域编码" clearable style="width:140px"
@keyup.enter="searchZones" />
@keyup.enter="searchZones" @input="zoneFilter.zoneCode = (zoneFilter.zoneCode || '').toUpperCase()" />
</el-form-item>
<el-form-item label="区域名称">
<el-input v-model="zoneFilter.zoneName" placeholder="区域名称" clearable style="width:150px"
@@ -368,7 +389,7 @@ async function onMaterialFile(e) {
append-to-body destroy-on-close>
<el-form label-width="90px">
<el-form-item label="区域编码" required>
<el-input v-model="zoneForm.zoneCode" placeholder=" Z05" :disabled="!!zoneEditingId" clearable />
<el-input v-model="zoneForm.zoneCode" placeholder=" Z05" :disabled="!!zoneEditingId" clearable @input="zoneForm.zoneCode = (zoneForm.zoneCode || '').toUpperCase()" />
</el-form-item>
<el-form-item label="区域名称" required>
<el-input v-model="zoneForm.zoneName" placeholder=" 待检区" clearable />
@@ -486,8 +507,8 @@ async function onMaterialFile(e) {
<el-dialog v-model="matDialog" :title="mEditingId ? '编辑物料' : '新增物料'" width="560px"
append-to-body destroy-on-close>
<el-form label-width="90px">
<el-form-item label="物料编码/图号" required>
<el-input v-model="mForm.code" placeholder="图号即物料编码 SCREW-M3" :disabled="!!mEditingId" clearable />
<el-form-item label="图号" required>
<el-input v-model="mForm.code" placeholder="图号 SCREW-M3" :disabled="!!mEditingId" clearable />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="mForm.name" placeholder=" 螺丝 M3" clearable />
@@ -523,6 +544,7 @@ async function onMaterialFile(e) {
<el-input v-model="mForm.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
</el-form>
<AttachmentPanel v-if="mEditingId" biz-type="material" :biz-id="mForm.code" title="附件零件图纸 / 资料" />
<template #footer>
<el-button @click="matDialog = false">取消</el-button>
<el-button type="primary" :loading="mSaving" @click="saveMaterial">保存</el-button>
+86 -19
View File
@@ -28,6 +28,27 @@ async function loadDicts() {
zones.value = Array.isArray(zs) ? zs : zs?.list || []
}
// 库位三级联动:基于已加载的 zones(每行是完整四级货位 zoneCode+shelfNo+layerNo+positionNo)派生可选项
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 onZoneChange(form) { form.shelfNo = ''; form.layerNo = ''; form.positionNo = '' }
function onShelfChange(form) { form.layerNo = ''; form.positionNo = '' }
function onLayerChange(form) { form.positionNo = '' }
/* ===================== Tab1 结构件入库(批次,支持多批到货追加) ===================== */
const batchRef = ref()
const submitting1 = ref(false)
@@ -35,6 +56,7 @@ const lastInboundNo = ref('')
const form1 = reactive({
materialCode: '', batchNo: '', quantity: 1,
zoneCode: '', shelfNo: '', layerNo: '', positionNo: '',
ownershipType: 'none', ownershipNo: '',
productionDate: '', supplier: '', remark: '', inspectionNo: ''
})
const rules1 = {
@@ -71,6 +93,8 @@ async function submitBatch() {
positionNo: form1.positionNo.trim(),
productionDate: form1.productionDate,
supplier: form1.supplier.trim(),
ownershipType: form1.ownershipType,
ownershipNo: form1.ownershipNo.trim(),
remark: form1.remark.trim(),
inspectionNo: form1.inspectionNo.trim(),
operator
@@ -90,7 +114,7 @@ async function submitBatch() {
const snInput = ref('')
const submitting2 = ref(false)
const snList = ref([])
const form2 = reactive({ materialCode: '', zoneCode: '', shelfNo: '', layerNo: '', positionNo: '', remark: '', inspectionNo: '' })
const form2 = reactive({ materialCode: '', zoneCode: '', shelfNo: '', layerNo: '', positionNo: '', ownershipType: 'none', ownershipNo: '', remark: '', inspectionNo: '' })
// 支持多种分隔符:中英文逗号、中英文句号、分号、顿号、空格、回车、制表符等
const SN_SEP = /[,.。;;、\s]+/
@@ -142,6 +166,8 @@ async function submitSn() {
layerNo: form2.layerNo.trim(),
positionNo: form2.positionNo.trim(),
snList: [...snList.value],
ownershipType: form2.ownershipType,
ownershipNo: form2.ownershipNo.trim(),
remark: form2.remark.trim(),
inspectionNo: form2.inspectionNo.trim(),
operator
@@ -476,16 +502,22 @@ onActivated(() => {
<el-input-number v-model="form1.quantity" :min="1" :step="1" step-strictly style="width:100%" />
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<el-select v-model="form1.zoneCode" placeholder="请选择入库区域(必填)" clearable style="width:100%">
<el-select v-model="form1.zoneCode" placeholder="请选择入库区域(必填)" clearable style="width:100%" @change="onZoneChange(form1)">
<el-option v-for="z in zones" :key="z.id" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架/层/位">
<div style="display:flex;gap:6px;width:100%">
<el-input v-model="form1.shelfNo" placeholder="货架 如A" clearable />
<el-input v-model="form1.layerNo" placeholder="层 如3" clearable />
<el-input v-model="form1.positionNo" placeholder="位置 如12" clearable />
<el-form-item label="货架 / 层 / 位置">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form1.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form1)">
<el-option v-for="s in zoneShelves(form1.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
</el-select>
<el-select v-model="form1.layerNo" placeholder="层" clearable style="width:100px" :disabled="!form1.shelfNo" @change="onLayerChange(form1)">
<el-option v-for="l in zoneLayers(form1.zoneCode, form1.shelfNo)" :key="l" :label="l + ' 层'" :value="l" />
</el-select>
<el-select v-model="form1.positionNo" placeholder="位置" clearable style="width:110px" :disabled="!form1.shelfNo">
<el-option v-for="p in zonePositions(form1.zoneCode, form1.shelfNo, form1.layerNo)" :key="p" :label="p + ' 位'" :value="p" />
</el-select>
</div>
<el-button link type="primary" size="small" style="margin-top:2px" @click="openRecommend('form1')">推荐库位</el-button>
</el-form-item>
@@ -499,6 +531,17 @@ onActivated(() => {
<el-form-item label="备注">
<el-input v-model="form1.remark" placeholder="备注(可空)" clearable />
</el-form-item>
<el-form-item label="归属">
<el-select v-model="form1.ownershipType" style="width:160px;margin-right:8px">
<el-option label="无" value="none" />
<el-option label="工单号" value="workorder" />
<el-option label="科技项目号" value="project" />
<el-option label="其他" value="other" />
<el-option label="其他" value="other" />
</el-select>
<el-input v-if="form1.ownershipType !== 'none'" v-model="form1.ownershipNo"
placeholder="请填写归属编号" clearable style="width:260px" />
</el-form-item>
<el-form-item label="检测单号" required>
<el-input v-model="form1.inspectionNo" placeholder="入库检报告单号(必填)" clearable />
</el-form-item>
@@ -534,16 +577,22 @@ onActivated(() => {
<MaterialSelect v-model="form2.materialCode" :manage-mode="2" placeholder="请选择电气件物料必填" />
</el-form-item>
<el-form-item label="区域" required>
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%">
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form2)">
<el-option v-for="z in zones" :key="z.id" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架//">
<div style="display:flex;gap:6px;width:100%">
<el-input v-model="form2.shelfNo" placeholder="货架 如A" clearable />
<el-input v-model="form2.layerNo" placeholder=" 如3" clearable />
<el-input v-model="form2.positionNo" placeholder="位置 如12" clearable />
<el-form-item label="货架 / / ">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form2.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form2)">
<el-option v-for="s in zoneShelves(form2.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
</el-select>
<el-select v-model="form2.layerNo" placeholder="" clearable style="width:100px" :disabled="!form2.shelfNo" @change="onLayerChange(form2)">
<el-option v-for="l in zoneLayers(form2.zoneCode, form2.shelfNo)" :key="l" :label="l + ' 层'" :value="l" />
</el-select>
<el-select v-model="form2.positionNo" placeholder="位置" clearable style="width:110px" :disabled="!form2.shelfNo">
<el-option v-for="p in zonePositions(form2.zoneCode, form2.shelfNo, form2.layerNo)" :key="p" :label="p + ' 位'" :value="p" />
</el-select>
</div>
<el-button link type="primary" size="small" style="margin-top:2px" @click="openRecommend('form2')">推荐库位</el-button>
</el-form-item>
@@ -559,6 +608,17 @@ onActivated(() => {
<el-form-item label="备注">
<el-input v-model="form2.remark" type="textarea" :rows="2" placeholder="本批 SN 的统一备注可空" />
</el-form-item>
<el-form-item label="归属">
<el-select v-model="form2.ownershipType" style="width:160px;margin-right:8px">
<el-option label="" value="none" />
<el-option label="工单号" value="workorder" />
<el-option label="科技项目号" value="project" />
<el-option label="其他" value="other" />
<el-option label="其他" value="other" />
</el-select>
<el-input v-if="form2.ownershipType !== 'none'" v-model="form2.ownershipNo"
placeholder="请填写归属编号" clearable style="width:260px" />
</el-form-item>
<el-form-item label="检测单号" required>
<el-input v-model="form2.inspectionNo" placeholder="入库检报告单号必填" clearable />
</el-form-item>
@@ -620,16 +680,22 @@ onActivated(() => {
<el-input-number v-model="form3.quantity" :min="1" :step="1" step-strictly style="width:100%" />
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<el-select v-model="form3.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%">
<el-select v-model="form3.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form3)">
<el-option v-for="z in zones" :key="z.id" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架//">
<div style="display:flex;gap:6px;width:100%">
<el-input v-model="form3.shelfNo" placeholder="货架 如A" clearable />
<el-input v-model="form3.layerNo" placeholder=" 如3" clearable />
<el-input v-model="form3.positionNo" placeholder="位置 如12" clearable />
<el-form-item label="货架 / / ">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form3.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form3)">
<el-option v-for="s in zoneShelves(form3.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
</el-select>
<el-select v-model="form3.layerNo" placeholder="" clearable style="width:100px" :disabled="!form3.shelfNo" @change="onLayerChange(form3)">
<el-option v-for="l in zoneLayers(form3.zoneCode, form3.shelfNo)" :key="l" :label="l + ' 层'" :value="l" />
</el-select>
<el-select v-model="form3.positionNo" placeholder="位置" clearable style="width:110px" :disabled="!form3.shelfNo">
<el-option v-for="p in zonePositions(form3.zoneCode, form3.shelfNo, form3.layerNo)" :key="p" :label="p + ' 位'" :value="p" />
</el-select>
</div>
<el-button link type="primary" size="small" style="margin-top:2px" @click="openRecommend('form3')">推荐库位</el-button>
</el-form-item>
@@ -638,6 +704,7 @@ onActivated(() => {
<el-option label="" value="none" />
<el-option label="工单号" value="workorder" />
<el-option label="科技项目号" value="project" />
<el-option label="其他" value="other" />
</el-select>
<el-input v-if="form3.ownershipType !== 'none'" v-model="form3.ownershipNo"
placeholder="请填写归属编号" clearable style="width:260px" />
@@ -56,7 +56,7 @@ const inputPlaceholder = computed(() =>
)
// 库存拾取器:直接选已存在的批次号/SN,避免手输。
// 规范:库存量不确定 → 只加载最近 100 条(created_at desc),输入走远程模糊搜索(keyword 匹配 物料编码/批次号/SN
// 规范:库存量不确定 → 只加载最近 100 条(created_at desc),输入走远程模糊搜索(keyword 匹配 图号/批次号/SN
const pickerOptions = ref([])
const pickerLoading = ref(false)
async function loadPicker(kw = '') {