feat:多项功能优化
This commit is contained in:
@@ -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 || []
|
||||
|
||||
@@ -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: '如 100;0=不预警' },
|
||||
{ name: '说明', source: '手工填写(可空)。', purpose: '补充描述', fill: '可空' }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { /* 忽略 */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user