feat:多项功能优化

This commit is contained in:
zhoujianjin
2026-09-18 18:54:54 +08:00
parent 672cd23329
commit 543655d441
48 changed files with 1289 additions and 447 deletions
@@ -10,6 +10,7 @@ const props = defineProps({
multiple: { type: Boolean, default: false },
manageMode: { type: Number, default: 0 }, // 0=全部 1=结构件 2=电气件
itemType: { type: Number, default: 0 }, // 0=全部 1原材料 2半成品 3成品 4其他
excludeItemType: { type: Number, default: 0 }, // 排除指定品类,如结构件/电气件入库排除「其他」(4)
placeholder: { type: String, default: '选择物料' },
clearable: { type: Boolean, default: true },
width: { type: String, default: '100%' }
@@ -27,7 +28,8 @@ async function load(kw = '') {
params: {
page: 1, pageSize: 100, keyword: kw || '',
manageMode: props.manageMode || '',
itemType: props.itemType || ''
itemType: props.itemType || '',
excludeItemType: props.excludeItemType || ''
}
})
options.value = data?.list || []
+4 -4
View File
@@ -5,7 +5,7 @@
export const helpInbound = {
title: '入库(来料入账)',
overview:
'把到货的来料记入 WMS 库存。按管理类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 电气件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n管理类型由 WMS 的"物料档案"决定(电气件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。\n\n本页四个页签(录入在前,记录在后点击才见),参照出库管理:\n· 结构件入库:按批次入库 + Excel 批量导入;\n· 电气件 SN 入库:逐件扫码 + Excel 批量导入;\n· 其他入库:辅料/螺栓/工装/试验设备等非结构件非电气件物料,规格型号强制;\n· 入库记录:分页列表(一单一行),点「明细」在右侧抽屉查看该单的行(懒加载+分页,不内嵌子表)。\n\n库位四级贯通:每笔入库除选「区域」外,还可录「货架/层/位置」三级货位,或点「推荐库位」由系统给出历史常用位+同区空位一键填入。',
'把到货的来料记入 WMS 库存。按管理类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 电气件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n管理类型由 WMS 的"物料档案"决定(电气件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。\n\n本页四个页签(录入在前,记录在后点击才见),参照出库管理:\n· 结构件入库:按批次入库 + Excel 批量导入;\n· 电气件入库:逐件扫码 + Excel 批量导入;\n· 其他入库:辅料/螺栓/工装/试验设备等非结构件非电气件物料,规格型号强制;\n· 入库记录:分页列表(一单一行),点「明细」在右侧抽屉查看该单的行(懒加载+分页,不内嵌子表)。\n\n库位四级贯通:每笔入库除选「区域」外,还可录「货架/层/位置」三级货位,或点「推荐库位」由系统给出历史常用位+同区空位一键填入。',
sections: [
{
title: '结构件入库(页签1',
@@ -23,7 +23,7 @@ export const helpInbound = {
]
},
{
title: '电气件 SN 入库(页签2',
title: '电气件入库(页签2',
overview: '适用于芯片、器件等需要单件追溯的来料,一件一个 SN。录入与 Excel 批量导入都在本页签。',
fields: [
{ name: '物料', source: '下拉选择(必填)。来自"物料档案"类型为"电气件"的物料。', purpose: '入库的电气件物料', fill: '选择图号' },
@@ -216,14 +216,14 @@ 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品类来源:原材料=进货来的(外购);半成品、成品=自己生产的;其他=辅料/工装/试验设备等。成品还可以作为零部件,放进另一个成品的物料清单(BOM)里被再次加工成新的成品。\n\n品类说明:1=原材料(默认) / 2=半成品 / 3=成品 / 4=其他(辅料/工装/试验设备)。编码主数据单一来源在本表:MES 的「物料档案」页面即是本表中"成品"品类的视图(实时拉取本表,不双录)——两系统的「物料档案」指向同一份主数据,以本表为准。库存主表 inventories 的品类与其呼应。列表支持按品类筛选,导出含品类列。\n\n物料档案是库房的另一道地基:先建好物料,入库时才能选到;类型选错会导致入库方式与追溯粒度不符。\n\n命名说明:本档案维护的物料在出库/看板等页面统一以"图号"展示(客户口径)。安全库存在本档案维护,可用量低于它时会在「库房看板」首页触发缺货预警。',
fields: [
{ name: '图号', source: '手工填写,仓库自定义(新增后不可改)。在出库/看板等页面显示为"图号"。', purpose: '物料唯一编号(即图号)', fill: '如 SCREW-M3;新增后不可改编码' },
{ name: '名称', source: '手工填写。', purpose: '物料名称', fill: '如 螺丝 M3' },
{ name: '简称', source: '手工填写(可空)。', purpose: '简写', fill: '可空' },
{ name: '规格', source: '手工填写(可空)。', purpose: '规格型号', fill: '可空' },
{ name: '单位', source: '手工填写。', purpose: '计量单位', fill: '如 个/套' },
{ name: '品类', source: '创建时默认"原材料";成品品类由「产品型号」页维护(自动生成),本页查询/导出覆盖全部品类。', purpose: '区分原材料/半成品/成品,与统一库存主表品类对应', fill: '查询区可选 原材料/半成品/成品' },
{ name: '品类', source: '创建时默认"原材料";成品品类也可在 MES 的「物料档案」页维护(两系统同一份主数据,以本表为准)。', purpose: '区分原材料/半成品/成品/其他,与统一库存主表品类对应;原材料为进货、半成品/成品为自产', fill: '查询区可选 原材料/半成品/成品/其他' },
{ name: '类型', source: '选择:1=结构件(批次管理) / 2=电气件(SN 管理)。语义由库房定:结构件按批次管理,电气件按 SN 逐件管理。', purpose: '决定入库方式:结构件走批次、电气件走 SN 逐件扫码', fill: '结构件(批次) / 电气件(SN)' },
{ name: '安全库存', source: '手工填写整数(默认0)。', purpose: '该物料的最低库存警戒线;可用量低于它会在「库房看板」首页触发缺货预警', fill: '如 1000=不预警' },
{ name: '说明', source: '手工填写(可空)。', purpose: '补充描述', fill: '可空' }
+177 -31
View File
@@ -40,15 +40,34 @@ const zoneFilter = reactive({
const zonePage = reactive({ current: 1, size: 10 })
const zoneDialog = ref(false)
const zoneEditingId = ref(0)
// 区域维护按「区域编码」分组:编辑键是 zoneCode(不是单行 id),一个区域可挂多个货位
const zoneEditingCode = ref('')
// 区域库位:区域 → 货架号 → 第几层 → 位置号(客户诉求 问题记录 L32/L169,手填)
const zoneForm = reactive({ zoneCode: '', zoneName: '', description: '', shelfNo: '', layerNo: '', positionNo: '' })
// 一个区域下可有多个货位,故 locations 为数组;新增时默认给一行
const zoneForm = reactive({ zoneCode: '', zoneName: '', description: '', status: '启用', locations: [] })
const zoneSaving = ref(false)
// 批量生成货位(货架号 × 层区间 × 位置区间),解决「一个货架不止一层、每层不止一个位置号」的逐条录入
const genForm = reactive({ shelfNo: '', layerFrom: 1, layerTo: 1, posFrom: 1, posTo: 1 })
function newZoneLocation() {
return { id: 0, shelfNo: '', layerNo: '', positionNo: '' }
}
// 货位标签(区域-货架-层-位置),仅用于分组子表展示,口径与后端 locLabel 一致
function zoneLocLabel(zoneCode, loc) {
const parts = []
if (zoneCode) parts.push(zoneCode)
if (loc.shelfNo) parts.push(loc.shelfNo + '架')
if (loc.layerNo) parts.push(loc.layerNo + '层')
if (loc.positionNo) parts.push(loc.positionNo + '位')
return parts.length ? parts.join('-') : '(未填写货位)'
}
async function loadZones() {
zoneLoading.value = true
try {
const data = await request.get('/zone/list', {
const data = await request.get('/zone/grouped', {
params: {
zoneCode: zoneFilter.zoneCode.trim().toUpperCase(),
zoneName: zoneFilter.zoneName.trim(),
@@ -70,25 +89,75 @@ function searchZones() {
}
function openZoneCreate() {
zoneEditingId.value = 0
Object.assign(zoneForm, { zoneCode: '', zoneName: '', description: '', shelfNo: '', layerNo: '', positionNo: '' })
zoneEditingCode.value = ''
Object.assign(zoneForm, {
zoneCode: '', zoneName: '', description: '', status: '启用',
locations: [newZoneLocation()]
})
Object.assign(genForm, { shelfNo: '', layerFrom: 1, layerTo: 1, posFrom: 1, posTo: 1 })
zoneDialog.value = true
}
function openZoneEdit(row) {
zoneEditingId.value = row.id
zoneEditingCode.value = row.zoneCode
Object.assign(zoneForm, {
zoneCode: row.zoneCode, zoneName: row.zoneName, description: row.description || '',
shelfNo: row.shelfNo || '', layerNo: row.layerNo || '', positionNo: row.positionNo || ''
zoneCode: row.zoneCode,
zoneName: row.zoneName,
description: row.description || '',
status: row.status || '启用',
locations: (row.locations || []).map((l) => ({
id: l.id, shelfNo: l.shelfNo || '', layerNo: l.layerNo || '', positionNo: l.positionNo || ''
}))
})
if (!zoneForm.locations.length) zoneForm.locations.push(newZoneLocation())
Object.assign(genForm, { shelfNo: '', layerFrom: 1, layerTo: 1, posFrom: 1, posTo: 1 })
zoneDialog.value = true
}
function addZoneLocation() {
zoneForm.locations.push(newZoneLocation())
}
function removeZoneLocation(idx) {
if (zoneForm.locations.length <= 1) {
ElMessage.warning('至少保留一个货位')
return
}
zoneForm.locations.splice(idx, 1)
}
// 按 货架号 × [层区间] × [位置区间] 笛卡尔积批量追加货位行
function genZoneLocations() {
const shelf = (genForm.shelfNo || '').trim()
const lf = Number(genForm.layerFrom), lt = Number(genForm.layerTo)
const pf = Number(genForm.posFrom), pt = Number(genForm.posTo)
if (!Number.isInteger(lf) || !Number.isInteger(lt) || !Number.isInteger(pf) || !Number.isInteger(pt) ||
lf < 0 || lt < lf || pf < 0 || pt < pf) {
ElMessage.warning('层/位置的起止值不合法(起始需 ≤ 结束)')
return
}
const count = (lt - lf + 1) * (pt - pf + 1)
if (count > 200) {
ElMessage.warning('一次最多生成 200 个货位,请缩小范围')
return
}
for (let l = lf; l <= lt; l++) {
for (let p = pf; p <= pt; p++) {
zoneForm.locations.push({ id: 0, shelfNo: shelf, layerNo: String(l), positionNo: String(p) })
}
}
ElMessage.success(`已生成 ${count} 个货位行,可再手动微调`)
}
async function saveZone() {
if (!zoneForm.zoneCode || !zoneForm.zoneName) {
ElMessage.warning('区域编码和区域名称必填')
return
}
if (!zoneForm.locations.length) {
ElMessage.warning('至少填写一个货位')
return
}
try {
await ElMessageBox.confirm('确认保存该区域信息吗?保存后将写入数据库。', '操作确认', {
confirmButtonText: '确认保存',
@@ -100,11 +169,23 @@ async function saveZone() {
}
zoneSaving.value = true
try {
if (zoneEditingId.value) {
await request.post('/zone/update', { id: zoneEditingId.value, ...zoneForm })
const payload = {
zoneCode: zoneForm.zoneCode,
zoneName: zoneForm.zoneName,
description: zoneForm.description,
status: zoneForm.status,
locations: zoneForm.locations.map((l) => ({
id: l.id || 0,
shelfNo: (l.shelfNo || '').trim(),
layerNo: (l.layerNo || '').trim(),
positionNo: (l.positionNo || '').trim()
}))
}
if (zoneEditingCode.value) {
await request.post('/zone/update', payload)
ElMessage.success('区域修改成功')
} else {
await request.post('/zone/create', { ...zoneForm })
await request.post('/zone/create', payload)
ElMessage.success('区域新增成功')
}
zoneDialog.value = false
@@ -346,18 +427,36 @@ async function onMaterialFile(e) {
<el-button type="success" @click="openZoneCreate">新增区域</el-button>
</el-form-item>
</el-form>
<el-table :data="zones" border size="small" v-loading="zoneLoading" empty-text="暂无区域,请点击【新增区域】">
<el-table-column label="ID" prop="id" width="60" align="center" />
<el-table :data="zones" border size="small" v-loading="zoneLoading" row-key="zoneCode"
empty-text="暂无区域请点击新增区域">
<!-- 分组展示一个区域一行展开后是该区域下的货位明细不再重复区域编码/名称 -->
<el-table-column type="expand">
<template #default="{ row }">
<div style="padding:4px 16px 10px">
<el-table :data="row.locations" border size="small" empty-text="该区域暂无货位">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="货架号" width="120" align="center">
<template #default="{ row: loc }">{{ loc.shelfNo || '-' }}</template>
</el-table-column>
<el-table-column label="第几层" width="100" align="center">
<template #default="{ row: loc }">{{ loc.layerNo || '-' }}</template>
</el-table-column>
<el-table-column label="位置号" width="110" align="center">
<template #default="{ row: loc }">{{ loc.positionNo || '-' }}</template>
</el-table-column>
<el-table-column label="货位标签" min-width="180">
<template #default="{ row: loc }">{{ zoneLocLabel(row.zoneCode, loc) }}</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="zoneCode" label="区域编码" min-width="120" />
<el-table-column prop="zoneName" label="区域名称" min-width="150" />
<el-table-column prop="shelfNo" label="货架号" width="90" align="center">
<template #default="{ row }">{{ row.shelfNo || '-' }}</template>
</el-table-column>
<el-table-column prop="layerNo" label="层" width="70" align="center">
<template #default="{ row }">{{ row.layerNo || '-' }}</template>
</el-table-column>
<el-table-column prop="positionNo" label="位置号" width="90" align="center">
<template #default="{ row }">{{ row.positionNo || '-' }}</template>
<el-table-column label="货位数" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.locationCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
@@ -385,27 +484,74 @@ async function onMaterialFile(e) {
@size-change="(s) => { zonePage.size = s; zonePage.current = 1; loadZones() }" />
</div>
<el-dialog v-model="zoneDialog" :title="zoneEditingId ? '编辑区域' : '新增区域'" width="460px"
<el-dialog v-model="zoneDialog" :title="zoneEditingCode ? '编辑区域' : '新增区域'" width="740px"
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 @input="zoneForm.zoneCode = (zoneForm.zoneCode || '').toUpperCase()" />
<el-input v-model="zoneForm.zoneCode" placeholder=" Z05" :disabled="!!zoneEditingCode" clearable
@input="zoneForm.zoneCode = (zoneForm.zoneCode || '').toUpperCase()" />
</el-form-item>
<el-form-item label="区域名称" required>
<el-input v-model="zoneForm.zoneName" placeholder=" 待检区" clearable />
</el-form-item>
<el-form-item label="货架号">
<el-input v-model="zoneForm.shelfNo" placeholder=" A / 03可空手填" clearable />
</el-form-item>
<el-form-item label="第几层">
<el-input v-model="zoneForm.layerNo" placeholder=" 3可空手填" clearable />
</el-form-item>
<el-form-item label="位置号">
<el-input v-model="zoneForm.positionNo" placeholder=" 12可空手填" clearable />
<el-form-item v-if="zoneEditingCode" label="状态">
<el-select v-model="zoneForm.status" style="width:130px">
<el-option label="启用" value="启用" />
<el-option label="停用" value="停用" />
</el-select>
</el-form-item>
<el-form-item label="说明">
<el-input v-model="zoneForm.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
<el-form-item label="货位">
<div style="width:100%">
<el-table :data="zoneForm.locations" border size="small" max-height="240">
<el-table-column label="货架号" width="150">
<template #default="{ row }">
<el-input v-model="row.shelfNo" placeholder=" A" size="small" clearable />
</template>
</el-table-column>
<el-table-column label="第几层" width="140">
<template #default="{ row }">
<el-input v-model="row.layerNo" placeholder=" 3" size="small" clearable />
</template>
</el-table-column>
<el-table-column label="位置号" width="140">
<template #default="{ row }">
<el-input v-model="row.positionNo" placeholder=" 12" size="small" clearable />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeZoneLocation($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top:8px">
<el-button size="small" @click="addZoneLocation">添加一行</el-button>
</div>
<el-divider content-position="left" style="margin:12px 0" />
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<el-input v-model="genForm.shelfNo" placeholder="货架号" size="small" style="width:90px" clearable />
<span style="font-size:12px;color:#606266">层</span>
<el-input-number v-model="genForm.layerFrom" :min="0" :step="1" step-strictly size="small"
controls-position="right" style="width:92px" />
<span style="font-size:12px;color:#606266">至</span>
<el-input-number v-model="genForm.layerTo" :min="0" :step="1" step-strictly size="small"
controls-position="right" style="width:92px" />
<span style="font-size:12px;color:#606266">位置</span>
<el-input-number v-model="genForm.posFrom" :min="0" :step="1" step-strictly size="small"
controls-position="right" style="width:92px" />
<span style="font-size:12px;color:#606266">至</span>
<el-input-number v-model="genForm.posTo" :min="0" :step="1" step-strictly size="small"
controls-position="right" style="width:92px" />
<el-button size="small" type="primary" plain @click="genZoneLocations">生成货位</el-button>
</div>
<div style="color:#909399;font-size:12px;margin-top:6px">
例:货架号 A、层 1 至 3、位置 1 至 4 → 一次生成 12 个货位(A架1层1位 … A架3层4位)
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="zoneDialog = false">取消</el-button>
@@ -28,7 +28,6 @@
<el-table-column label="事件类型" min-width="170">
<template #default="{ row }">
<el-tag :type="familyTag(row.eventType)" size="small">{{ familyName(row.eventType) }}</el-tag>
<span class="evt-code">{{ row.eventType }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="260" show-overflow-tooltip />
@@ -145,11 +144,3 @@ function onReset() {
onMounted(load)
</script>
<style scoped>
.evt-code {
margin-left: 6px;
color: #909399;
font-size: 12px;
}
</style>
+112 -39
View File
@@ -17,17 +17,50 @@ const route = useRoute()
const router = useRouter()
const operator = getRealName()
// ===== 入库表单附件:提交前先在表单里暂存,提交成功拿到入库单号后再挂到该单 =====
const staged1 = ref([]) // 结构件入库 暂存附件
const staged2 = ref([]) // 电气件 SN 入库 暂存附件
const staged3 = ref([]) // 其他入库 暂存附件
// 逐个上传;单个失败不阻断(入库单已生成,附件可稍后在「入库记录」补传),错误提示由请求层统一处理
async function uploadStaged(fileList, inboundNo) {
const files = fileList.value || []
if (!files.length || !inboundNo) return 0
let ok = 0
for (const f of files) {
const raw = f.raw
if (!raw) continue
const fd = new FormData()
fd.append('file', raw)
fd.append('bizType', 'inbound')
fd.append('bizId', inboundNo)
try { await request.post('/attachments/upload', fd); ok++ } catch { /* 忽略单个失败 */ }
}
fileList.value = []
return ok
}
// ===== 多 Tab:结构件入库 / 电气件 SN 入库 在前(常操作),入库记录在后 =====
const activeTab = ref('batch') // batch=结构件入库 / sn=电气件SN入库 / records=入库记录
// 公共字典:区域(物料下拉由 MaterialSelect 组件按需远程搜索,最近 100 条)
const zones = ref([])
async function loadDicts() {
if (zones.value.length) return
// force=true 时强制重新拉取:区域维护新建/编辑后,入库页的区域下拉必须立即看到最新货位
async function loadDicts(force = false) {
if (!force && zones.value.length) return
const zs = await request.get('/zone/picker')
zones.value = Array.isArray(zs) ? zs : zs?.list || []
}
// 区域下拉选项:zones 每行是一个货位,同一区域会重复出现,按区域编码去重后只展示一条
const zoneOptions = computed(() => {
const map = new Map()
zones.value.forEach(z => {
if (!z.zoneCode) return
if (!map.has(z.zoneCode)) map.set(z.zoneCode, { zoneCode: z.zoneCode, zoneName: z.zoneName || '' })
})
return [...map.values()]
})
// 库位三级联动:基于已加载的 zones(每行是完整四级货位 zoneCode+shelfNo+layerNo+positionNo)派生可选项
function zoneShelves(zoneCode) {
const set = new Set()
@@ -62,19 +95,21 @@ const form1 = reactive({
const rules1 = {
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
zoneCode: [{ required: true, message: '请选择入库区域', trigger: 'change' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
inspectionNo: [{ required: true, message: '请填写检测单号', trigger: 'blur' }]
}
async function submitBatch() {
await batchRef.value.validate()
// 必填项校验失败:表单项爆红 + 顶部弹窗提示(原写法 validate() 抛异常后无弹窗)
const valid = await batchRef.value.validate().catch(() => false)
if (!valid) {
ElMessage.warning('请检查必填项后再提交')
return
}
if (!form1.quantity || form1.quantity <= 0) {
ElMessage.warning('数量必须大于 0')
return
}
if (!form1.inspectionNo.trim()) {
ElMessage.warning('请填写检测单号')
return
}
try {
await ElMessageBox.confirm(
'确认入库:物料 ' + form1.materialCode + '|批次 ' + form1.batchNo + '|数量 ' + form1.quantity + '|区域 ' + form1.zoneCode + '',
@@ -101,6 +136,8 @@ async function submitBatch() {
})
lastInboundNo.value = data?.inboundNo || ''
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''},数量 ${data?.quantity ?? form1.quantity}`)
const att = await uploadStaged(staged1, lastInboundNo.value)
if (att) ElMessage.success(`已挂载 ${att} 个附件`)
form1.batchNo = ''
form1.quantity = 1
// 入库成功后刷新记录 Tab
@@ -115,6 +152,13 @@ const snInput = ref('')
const submitting2 = ref(false)
const snList = ref([])
const form2 = reactive({ materialCode: '', zoneCode: '', shelfNo: '', layerNo: '', positionNo: '', ownershipType: 'none', ownershipNo: '', remark: '', inspectionNo: '' })
// 与 Tab1/Tab3 对齐:走 el-form 校验,必填项既爆红又弹窗
const form2Ref = ref(null)
const rules2 = {
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
zoneCode: [{ required: true, message: '请选择入库区域', trigger: 'change' }],
inspectionNo: [{ required: true, message: '请填写检测单号', trigger: 'blur' }]
}
// 支持多种分隔符:中英文逗号、中英文句号、分号、顿号、空格、回车、制表符等
const SN_SEP = /[,.。;;、\s]+/
@@ -135,18 +179,12 @@ function removeSn(index) {
}
async function submitSn() {
if (!form2.materialCode) {
ElMessage.warning('请选择物料')
// 必填项校验失败:表单项爆红 + 顶部弹窗提示
const valid = await form2Ref.value.validate().catch(() => false)
if (!valid) {
ElMessage.warning('请检查必填项后再提交')
return
}
if (!form2.zoneCode) {
ElMessage.warning('请选择入库区域')
return
}
if (!form2.inspectionNo.trim()) {
ElMessage.warning('请填写检测单号')
return
}
if (!snList.value.length) {
ElMessage.warning('请先扫码录入 SN(输入后点击【录入到列表】)')
return
@@ -173,6 +211,8 @@ async function submitSn() {
operator
})
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''}SN 共 ${snList.value.length}`)
const att = await uploadStaged(staged2, data?.inboundNo || '')
if (att) ElMessage.success(`已挂载 ${att} 个附件`)
snList.value = []
snInput.value = ''
// 入库成功后刷新记录 Tab
@@ -235,7 +275,8 @@ const form3 = reactive({
const rules3 = {
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
zoneCode: [{ required: true, message: '请选择入库区域', trigger: 'change' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
inspectionNo: [{ required: true, message: '请填写检测单号', trigger: 'blur' }]
}
const otherMaterial = ref(null) // 选中物料(用于展示规格,规格为空时后端会拒绝)
@@ -244,7 +285,12 @@ function onOtherMaterial(item) {
}
async function submitOther() {
await otherRef.value.validate()
// 必填项校验失败:表单项爆红 + 顶部弹窗提示(原写法 validate() 抛异常后无弹窗)
const valid = await otherRef.value.validate().catch(() => false)
if (!valid) {
ElMessage.warning('请检查必填项后再提交')
return
}
if (!form3.quantity || form3.quantity <= 0) {
ElMessage.warning('数量必须大于 0')
return
@@ -253,10 +299,6 @@ async function submitOther() {
ElMessage.warning('请填写归属编号(工单号 / 科技项目号)')
return
}
if (!form3.inspectionNo.trim()) {
ElMessage.warning('请填写检测单号')
return
}
if (otherMaterial.value && !String(otherMaterial.value.spec || '').trim()) {
ElMessage.warning('该其他类物料未填写规格型号,请先到物料档案补全')
return
@@ -286,6 +328,8 @@ async function submitOther() {
operator
})
ElMessage.success(`其他入库成功:入库单号 ${data?.inboundNo || ''},数量 ${data?.quantity ?? form3.quantity}`)
const att = await uploadStaged(staged3, data?.inboundNo || '')
if (att) ElMessage.success(`已挂载 ${att} 个附件`)
form3.batchNo = ''
form3.quantity = 1
form3.ownershipType = 'none'
@@ -463,6 +507,8 @@ onMounted(() => {
// keep-alive 缓存下 onMounted 只在首次触发;用 onActivated 保证每次跳转进来都能消费路由参数
onActivated(() => {
// 区域维护可能刚新建/编辑过区域或货位,进来时强制刷新区域字典,避免选择区域时看不到最新内容
loadDicts(true)
const q = route.query
// 库存页点入库单号跳转过来 → 自动切到"入库记录"Tab 并带入单号查询(闭环)
if (q.inboundNo) {
@@ -493,7 +539,7 @@ onActivated(() => {
<el-card shadow="never">
<el-form ref="batchRef" :model="form1" :rules="rules1" label-width="110px" style="max-width:560px">
<el-form-item label="物料" prop="materialCode">
<MaterialSelect v-model="form1.materialCode" :manage-mode="1" placeholder="选择结构件物料" />
<MaterialSelect v-model="form1.materialCode" :manage-mode="1" :exclude-item-type="4" placeholder="选择结构件物料" />
</el-form-item>
<el-form-item label="批次号">
<el-input v-model="form1.batchNo" placeholder="留空则自动生成" clearable />
@@ -503,7 +549,7 @@ onActivated(() => {
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<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"
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
@@ -542,9 +588,18 @@ onActivated(() => {
<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>
<el-form-item label="检测单号" prop="inspectionNo">
<el-input v-model="form1.inspectionNo" placeholder="入库检报告单号(必填)" clearable />
</el-form-item>
<el-form-item label="附件">
<el-upload v-model:file-list="staged1" :auto-upload="false" multiple :limit="10"
accept=".pdf,.png,.jpg,.jpeg,.gif,.xlsx,.xls,.doc,.docx,.dwg,.dxf,.zip,.txt,.csv">
<el-button plain>选择附件</el-button>
<template #tip>
<div style="color:#909399;font-size:12px">提交成功后自动挂到该入库单可在入库记录查看</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting1" @click="submitBatch">提交入库</el-button>
</el-form-item>
@@ -570,15 +625,15 @@ onActivated(() => {
</el-tab-pane>
<!-- ============ Tab2 电气件 SN 入库 ============ -->
<el-tab-pane :label="`电气件 SN 入库${snList.length ? `(已扫 ${snList.length}` : ''}`" name="sn">
<el-tab-pane :label="`电气件入库${snList.length ? `(已扫 ${snList.length}` : ''}`" name="sn">
<el-card shadow="never">
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="物料" required>
<el-form ref="form2Ref" :model="form2" :rules="rules2" label-width="110px" style="max-width:680px">
<el-form-item label="物料" prop="materialCode">
<MaterialSelect v-model="form2.materialCode" :manage-mode="2" placeholder="请选择电气件物料必填" />
</el-form-item>
<el-form-item label="区域" required>
<el-form-item label="区域" prop="zoneCode">
<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"
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
@@ -619,9 +674,18 @@ onActivated(() => {
<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>
<el-form-item label="检测单号" prop="inspectionNo">
<el-input v-model="form2.inspectionNo" placeholder="入库检报告单号必填" clearable />
</el-form-item>
<el-form-item label="附件">
<el-upload v-model:file-list="staged2" :auto-upload="false" multiple :limit="10"
accept=".pdf,.png,.jpg,.jpeg,.gif,.xlsx,.xls,.doc,.docx,.dwg,.dxf,.zip,.txt,.csv">
<el-button plain>选择附件</el-button>
<template #tip>
<div style="color:#909399;font-size:12px">提交成功后自动挂到该入库单,可在「入库记录」查看</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<div style="margin-bottom:8px">
@@ -681,7 +745,7 @@ onActivated(() => {
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<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"
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
@@ -712,12 +776,21 @@ onActivated(() => {
<el-form-item label="供应商">
<el-input v-model="form3.supplier" placeholder="供应商名称可空" clearable />
</el-form-item>
<el-form-item label="检测单号" required>
<el-form-item label="检测单号" prop="inspectionNo">
<el-input v-model="form3.inspectionNo" placeholder="入库检报告单号必填" clearable />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form3.remark" placeholder="备注可空" clearable />
</el-form-item>
<el-form-item label="附件">
<el-upload v-model:file-list="staged3" :auto-upload="false" multiple :limit="10"
accept=".pdf,.png,.jpg,.jpeg,.gif,.xlsx,.xls,.doc,.docx,.dwg,.dxf,.zip,.txt,.csv">
<el-button plain>选择附件</el-button>
<template #tip>
<div style="color:#909399;font-size:12px">提交成功后自动挂到该入库单,可在「入库记录」查看</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting3" @click="submitOther">提交入库</el-button>
</el-form-item>
+13 -4
View File
@@ -1,6 +1,6 @@
<script setup>
defineOptions({ name: 'Inventory' })
import { onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { exportXlsxFetch } from '../utils/export'
@@ -42,13 +42,22 @@ const total = ref(0)
const page = reactive({ current: 1, size: 20 })
// 区域下拉数据源
const zoneOptions = ref([])
const zonesRaw = ref([])
// 区域下拉选项:picker 每行是一个货位,同一区域会重复出现,按区域编码去重后只展示一条
const zoneOptions = computed(() => {
const map = new Map()
zonesRaw.value.forEach(z => {
if (!z.zoneCode) return
if (!map.has(z.zoneCode)) map.set(z.zoneCode, { zoneCode: z.zoneCode, zoneName: z.zoneName || '' })
})
return [...map.values()]
})
async function loadZones() {
try {
const data = await request.get('/zone/picker')
zoneOptions.value = data || []
zonesRaw.value = data || []
} catch (e) {
zoneOptions.value = []
zonesRaw.value = []
}
}
+10 -2
View File
@@ -145,6 +145,15 @@ async function loadDicts() {
const zs = await request.get('/zone/picker')
zones.value = Array.isArray(zs) ? zs : zs?.list || []
}
// 区域下拉选项:picker 每行是一个货位,同一区域会重复出现,按区域编码去重后只展示一条
const zoneOptions = computed(() => {
const map = new Map()
zones.value.forEach(z => {
if (!z.zoneCode) return
if (!map.has(z.zoneCode)) map.set(z.zoneCode, { zoneCode: z.zoneCode, zoneName: z.zoneName || '' })
})
return [...map.values()]
})
async function loadGenRows() {
loadingGen.value = true
@@ -166,7 +175,6 @@ async function submitGen() {
}
const payload = {
materialCode: genForm.materialCode, operator,
outboundType: 'general',
outboundCategory: genForm.outboundCategory,
ownershipType: genForm.ownershipType,
ownershipNo: genForm.ownershipType !== 'none' ? genForm.ownershipNo.trim() : undefined,
@@ -368,7 +376,7 @@ onMounted(loadDicts)
<el-form-item label="出库区域">
<el-select v-model="genForm.zoneCode" filterable clearable placeholder="可选" style="width:100%">
<el-option v-for="z in zones" :key="z.code" :value="z.code" :label="`${z.code} ${z.name || ''}`" />
<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="箱号">
@@ -28,7 +28,8 @@ async function loadScopeDicts() {
try {
const z = await request.get('/zone/picker')
const arr = Array.isArray(z) ? z : (z?.list || [])
zonesDict.value = arr.map(x => x.zoneCode || x.code).filter(Boolean)
// picker 每行是一个货位,同一区域会重复出现,去重后区域选项才不重复
zonesDict.value = [...new Set(arr.map(x => x.zoneCode || x.code).filter(Boolean))]
} catch { /* 忽略 */ }
}