refactor(wms): 合并库存表为统一inventory,重构业务逻辑

1. 合并inventory_batch与serial_number为统一inventory表,用manage_mode区分结构件批次/精密件SN
2. 移除冗余的material_type字段与相关逻辑
3. 重构库存查询、检验、入库出库等业务逻辑适配新表结构
4. 调整前端路由与菜单,拆分基础数据为区域维护和物料档案
5. 优化入库校验逻辑,避免空单问题
6. 调整Vite构建配置,关闭自动清空输出目录
7. 为各模块添加详细中文注释,统一业务术语
8. 生成并更新Ent自动代码文件
This commit is contained in:
SunYF
2026-09-02 08:21:11 +08:00
parent 47573e44b0
commit 68f50ee46e
70 changed files with 5895 additions and 9818 deletions
@@ -44,7 +44,7 @@
<!-- 既无顶层字段也无分组 -->
<el-empty v-if="!data.fields && !(data.sections && data.sections.length)"
description="该页面暂未编写操作说明,请先完成基础数据维护后查看效果" :image-size="70" />
description="该页面暂未编写操作说明" :image-size="70" />
</div>
</el-drawer>
</template>
+8 -8
View File
@@ -5,16 +5,16 @@
export const helpInbound = {
title: '入库(来料入账)',
overview:
'把到货的来料记入 WMS 库存。按物料类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 精密件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n物料类型由 WMS 的"物料档案"决定(精密件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。',
'把到货的来料记入 WMS 库存。按管理类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 精密件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n管理类型由 WMS 的"物料档案"决定(精密件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。',
sections: [
{
title: 'Tab1 结构件入库(按批次)',
overview: '适用于螺丝、机加工件等标准件,一次到货入一批。',
overview: '适用于螺丝、机加工件等结构件,一次到货入一批。',
fields: [
{ name: '物料', source: '下拉选择。数据来自 WMS"物料档案"里类型为"结构件"的物料。', purpose: '入库的是什么物料;档案里必须存在才能选到', fill: '选择物料编码,如 SCREW-M3;选不到=还没建该物料档案' },
{ name: '批次号', source: '留空系统自动生成(日期+序号);也可手工填。', purpose: '区别不同到货批次,实现先进先出(FIFO)', fill: '默认留空自动生成;供应商自带批号时可手填' },
{ name: '数量', source: '手工填写整数。', purpose: '本批到货数量', fill: '整数 ≥1' },
{ name: '区域', source: '下拉选择(必填)。数据来自"基础数据-区域维护"。', purpose: '放到哪个库区(待检/合格/不合格…)', fill: '必填;新到货一般先放"待检"区,检验合格后再转"合格"区' },
{ name: '区域', source: '下拉选择(必填)。数据来自"区域维护"。', purpose: '放到哪个库区(待检/合格/不合格…)', fill: '必填;新到货一般先放"待检"区,检验合格后再转"合格"区' },
{ name: '生产日期', source: '手动选择日期。', purpose: '生产时间,用于时效和批次管理', fill: '选择日期' },
{ name: '供应商', source: '手工填写。', purpose: '来料来源', fill: '供应商名称,可空' }
]
@@ -24,7 +24,7 @@ export const helpInbound = {
overview: '适用于芯片、器件等需要单件追溯的来料,一件一个 SN。',
fields: [
{ name: '物料', source: '下拉选择(必填)。来自"物料档案"类型为"精密件"的物料。', purpose: '入库的精密件物料', fill: '选择物料编码' },
{ name: '区域', source: '下拉选择(必填)。来自"基础数据-区域维护"。', purpose: '放到哪个库区', fill: '必填;新到货一般先放"待检"区' },
{ name: '区域', source: '下拉选择(必填)。来自"区域维护"。', purpose: '放到哪个库区', fill: '必填;新到货一般先放"待检"区' },
{ name: '扫码录入', source: '扫码枪连续扫码。来自每颗器件自带的 SN 条码。', purpose: '逐件登记 SN,实现单件追溯', fill: '扫入/粘贴 SN 后回车追加,可一次连续多条' }
]
},
@@ -56,7 +56,7 @@ export const helpInventory = {
'查看当前库存:结构件按批次台账(数量/锁定/区域),精密件按 SN 序列号(状态/区域)。\n数据由入库、出库、盘点等业务实时维护,本页只读查询。',
fields: [
{ name: '物料编码', source: '筛选条件。', purpose: '只看某物料', fill: '输入编码,可空=全部' },
{ name: '库存类型', source: '单选。', purpose: '看批次台账还是 SN 序列', fill: '全部 / 批次 / SN 序列' },
{ name: '库存类型', source: '单选(批次 / SN 序列)。', purpose: '看批次台账还是 SN 序列,各自独立分页', fill: '批次 / SN 序列' },
{ name: '关键字', source: '筛选条件。', purpose: '按批次号/物料名称/SN 模糊过滤', fill: '输入关键字,可空' }
]
}
@@ -64,7 +64,7 @@ export const helpInventory = {
export const helpInspection = {
title: '质量检验(来料检)',
overview:
'对入库的未检物料/SN 做检验:把"未检"翻转为【合格】或【不合格】。\n合格才能被出库锁定(出库自动分配只取合格区Z02)。\n\n数据链路:入库(状态=未检) → 本页检验翻转 → 合格Z02 / 不合格隔离。',
'对入库的未检物料/SN 做检验:把"未检"翻转为【合格】或【不合格】。\n检验只修改"质量状态"字段,与物料/SN 的存放区域无关。\n合格才能参与出库与锁库(库存校验只取质量状态=合格的批次)。',
fields: [
{ name: '目标类型', source: '单选。', purpose: '检验对象是批次还是 SN', fill: '批次 BATCH / 序列号 SN' },
{ name: '检验结论', source: '单选。', purpose: '判定结果', fill: '合格 / 不合格' },
@@ -141,7 +141,7 @@ export const helpLedger = {
}
export const helpBaseData = {
title: '基础数据(区域 / 物料档案',
title: '区域维护 / 物料档案',
overview:
'维护库房系统的基础字典,供入库/出库/库存等所有业务下拉框选择。\n· 区域:货品存放的库区(如待检、合格Z02、不合格…);入库时要指明放到哪个区。\n· 物料档案:定义每一种物料叫什么、什么规格、是【结构件(批次)】还是【精密件(SN)】,决定后续入库方式。\n\n这两个字典是库房的第一道地基:先建好区域和物料,入库、检验、出库才能正常跑到数据。',
sections: [
@@ -164,7 +164,7 @@ export const helpBaseData = {
{ name: '规格', source: '手工填写(可空)。', purpose: '规格型号', fill: '可空' },
{ name: '单位', source: '手工填写。', purpose: '计量单位', fill: '如 个/套' },
{ name: '类型', source: '选择。语义由库房定:结构件按批次管理,精密件按 SN 逐件管理。', purpose: '决定入库方式:结构件走批次、精密件走 SN', fill: '结构件(批次) / 精密件(SN)' },
{ name: '物料类型', source: '选择(可空)。', purpose: '业务归类', fill: '标准件/电子件/包装件/半成品/成品' }
{ name: '生命周期阶段', source: '物料主数据不再登记生命周期阶段(原“物料类型”字段已移除)。原料→半成品→成品的流转由库存实体各自承载:组件统一入 inventory 表(manage_mode1=结构件批次 / 2=精密件SN),半成品入 semifinished 固定业务表(首页“半成品在库”即统计此表)。', purpose: '避免主数据预判物料命运,消除与 semifinished 业务的歧义', fill: '无需填写;半成品/成品状态以实际库存与 semifinished 表为准' }
]
}
]
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
HomeFilled, Download, Upload, Search, CircleCheck, List,
Box, Tickets, Monitor, EditPen, Fold, Expand
Box, Tickets, Monitor, EditPen, Fold, Expand, Location, Goods
} from '@element-plus/icons-vue'
import { getUser, logout } from '../utils/auth'
@@ -57,7 +57,8 @@ const menus = [
{ path: '/semi', title: '半成品/成品', icon: Box },
{ path: '/package', title: '装箱管理', icon: Box },
{ path: '/ledger', title: '备料台账', icon: Tickets },
{ path: '/base-data', title: '基础数据', icon: EditPen }
{ path: '/zone', title: '区域维护', icon: Location },
{ path: '/material', title: '物料档案', icon: Goods }
]
// 打开当前页面帮助(各页面 PageHelp 监听 wms:open-page-help 事件弹出抽屉)
@@ -1,11 +1,16 @@
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpBaseData } from '../help'
/* ---------- Tab1 区域维护 ---------- */
// mode: 'zone' = 区域维护;'material' = 物料档案(同一组件拆成两个独立菜单)
const props = defineProps({
mode: { type: String, default: 'zone' }
})
/* ---------- 区域维护 ---------- */
const zones = ref([])
const zoneTotal = ref(0)
const zoneLoading = ref(false)
@@ -70,19 +75,18 @@ async function saveZone() {
}
}
/* ---------- Tab2 物料档案维护 ---------- */
/* ---------- 物料档案维护 ---------- */
const materials = ref([])
const materialTotal = ref(0)
const materialLoading = ref(false)
const materialKeyword = ref('')
const materialTypeFilter = ref('')
const matPage = reactive({ current: 1, size: 10 })
const matDialog = ref(false)
const mEditingId = ref(0)
const mForm = reactive({
code: '', name: '', shortName: '', spec: '', unit: '',
manageMode: 1, materialType: '', templateCode: '', description: ''
manageMode: 1, templateCode: '', description: ''
})
const mSaving = ref(false)
@@ -92,7 +96,6 @@ async function loadMaterials() {
const data = await request.get('/material/query', {
params: {
keyword: materialKeyword.value.trim(),
materialType: materialTypeFilter.value,
page: matPage.current,
pageSize: matPage.size
}
@@ -113,7 +116,7 @@ function resetMForm() {
mEditingId.value = 0
Object.assign(mForm, {
code: '', name: '', shortName: '', spec: '', unit: '',
manageMode: 1, materialType: '', templateCode: '', description: ''
manageMode: 1, templateCode: '', description: ''
})
}
@@ -127,7 +130,7 @@ function openMatEdit(row) {
Object.assign(mForm, {
code: row.code, name: row.name, shortName: row.shortName || '',
spec: row.spec || '', unit: row.unit || '', manageMode: row.manageMode,
materialType: row.materialType || '', templateCode: row.templateCode || '',
templateCode: row.templateCode || '',
description: row.description || ''
})
matDialog.value = true
@@ -159,169 +162,146 @@ async function saveMaterial() {
}
onMounted(() => {
loadZones()
loadMaterials()
if (props.mode === 'material') loadMaterials()
else loadZones()
})
</script>
<template>
<el-card shadow="never">
<el-tabs>
<el-tab-pane :label="`区域维护(${zoneTotal}`">
<el-form inline style="margin-bottom:14px" @submit.prevent="searchZones">
<el-form-item label="关键字">
<el-input v-model="zoneKeyword" placeholder="区域编码/名称" clearable style="width:200px"
@keyup.enter="searchZones" />
<!-- 区域维护 -->
<template v-if="mode !== 'material'">
<el-form inline style="margin-bottom:14px" @submit.prevent="searchZones">
<el-form-item label="关键字">
<el-input v-model="zoneKeyword" placeholder="区域编码/名称" clearable style="width:200px"
@keyup.enter="searchZones" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="zoneLoading" @click="searchZones">查询</el-button>
<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 prop="zoneCode" label="区域编码" min-width="140" />
<el-table-column prop="zoneName" label="区域名称" min-width="160" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">
{{ row.status || '启用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="200">
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openZoneEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, prev, pager, next" :total="zoneTotal"
:current-page="zonePage.current" :page-size="zonePage.size"
@current-change="(p) => { zonePage.current = p; loadZones() }" />
</div>
<el-dialog v-model="zoneDialog" :title="zoneEditingId ? '编辑区域' : '新增区域'" width="460px"
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-form-item>
<el-form-item>
<el-button type="primary" :loading="zoneLoading" @click="searchZones">查询</el-button>
<el-button type="success" @click="openZoneCreate">新增区域</el-button>
<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.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
</el-form>
<el-table :data="zones" border size="small" v-loading="zoneLoading" empty-text="暂无区域,请点击【新增区域】">
<el-table-column prop="zoneCode" label="区域编码" min-width="140" />
<el-table-column prop="zoneName" label="区域名称" min-width="160" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">
{{ row.status || '启用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="200">
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openZoneEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, prev, pager, next" :total="zoneTotal"
:current-page="zonePage.current" :page-size="zonePage.size"
@current-change="(p) => { zonePage.current = p; loadZones() }" />
</div>
</el-tab-pane>
<template #footer>
<el-button @click="zoneDialog = false">取消</el-button>
<el-button type="primary" :loading="zoneSaving" @click="saveZone">保存</el-button>
</template>
</el-dialog>
</template>
<el-tab-pane :label="`物料档案(${materialTotal}`">
<el-form inline style="margin-bottom:14px" @submit.prevent="searchMaterials">
<el-form-item label="关键字">
<el-input v-model="materialKeyword" placeholder="物料编码/名称" clearable style="width:200px"
@keyup.enter="searchMaterials" />
<!-- 物料档案 -->
<template v-else>
<el-form inline style="margin-bottom:14px" @submit.prevent="searchMaterials">
<el-form-item label="关键字">
<el-input v-model="materialKeyword" placeholder="物料编码/名称" clearable style="width:200px"
@keyup.enter="searchMaterials" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="materialLoading" @click="searchMaterials">查询</el-button>
<el-button type="success" @click="openMatCreate">新增物料</el-button>
</el-form-item>
</el-form>
<el-table :data="materials" border size="small" v-loading="materialLoading"
empty-text="暂无物料请点击新增物料">
<el-table-column prop="code" label="物料编码" min-width="120" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="spec" label="规格" min-width="90">
<template #default="{ row }">{{ row.spec || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="70" align="center">
<template #default="{ row }">{{ row.unit || '-' }}</template>
</el-table-column>
<el-table-column label="类型" width="150" align="center">
<template #default="{ row }">
<el-tag :type="row.manageMode === 2 ? 'warning' : 'info'" size="small">
{{ row.manageMode === 2 ? '精密件(SN)' : '结构件(批次)' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openMatEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="materialTotal"
:current-page="matPage.current" :page-size="matPage.size" :page-sizes="[10, 20, 50, 100]"
@current-change="(p) => { matPage.current = p; loadMaterials() }"
@size-change="(s) => { matPage.size = s; matPage.current = 1; loadMaterials() }" />
</div>
<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>
<el-form-item label="物料类型">
<el-select v-model="materialTypeFilter" placeholder="全部" clearable style="width:130px">
<el-option label="标准件" value="标准件" />
<el-option label="电子件" value="电子件" />
<el-option label="包装件" value="包装件" />
<el-option label="半成品" value="半成品" />
<el-option label="成品" value="成品" />
<el-form-item label="名称" required>
<el-input v-model="mForm.name" placeholder=" 螺丝 M3" clearable />
</el-form-item>
<el-form-item label="简称">
<el-input v-model="mForm.shortName" placeholder="简称可空" clearable />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="mForm.spec" placeholder="规格可空" clearable />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="mForm.unit" placeholder=" /" clearable />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="mForm.manageMode" placeholder="请选择" style="width:100%">
<el-option :value="1" label="结构件批次管理" />
<el-option :value="2" label="精密件SN 管理" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="materialLoading" @click="searchMaterials">查询</el-button>
<el-button type="success" @click="openMatCreate">新增物料</el-button>
<el-form-item label="说明">
<el-input v-model="mForm.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
</el-form>
<el-table :data="materials" border size="small" v-loading="materialLoading"
empty-text="暂无物料请点击新增物料">
<el-table-column prop="code" label="物料编码" min-width="120" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="spec" label="规格" min-width="90">
<template #default="{ row }">{{ row.spec || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="70" align="center">
<template #default="{ row }">{{ row.unit || '-' }}</template>
</el-table-column>
<el-table-column label="类型" width="150" align="center">
<template #default="{ row }">
<el-tag :type="row.manageMode === 2 ? 'warning' : 'info'" size="small">
{{ row.manageMode === 2 ? '精密件(SN)' : '结构件(批次)' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="materialType" label="物料类型" width="90" align="center">
<template #default="{ row }">{{ row.materialType || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openMatEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="materialTotal"
:current-page="matPage.current" :page-size="matPage.size" :page-sizes="[10, 20, 50, 100]"
@current-change="(p) => { matPage.current = p; loadMaterials() }"
@size-change="(s) => { matPage.size = s; matPage.current = 1; loadMaterials() }" />
</div>
</el-tab-pane>
</el-tabs>
<!-- 区域弹窗 -->
<el-dialog v-model="zoneDialog" :title="zoneEditingId ? '编辑区域' : '新增区域'" width="460px"
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-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.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="zoneDialog = false">取消</el-button>
<el-button type="primary" :loading="zoneSaving" @click="saveZone">保存</el-button>
</template>
</el-dialog>
<!-- 物料弹窗 -->
<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>
<el-form-item label="名称" required>
<el-input v-model="mForm.name" placeholder=" 螺丝 M3" clearable />
</el-form-item>
<el-form-item label="简称">
<el-input v-model="mForm.shortName" placeholder="简称可空" clearable />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="mForm.spec" placeholder="规格可空" clearable />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="mForm.unit" placeholder=" /" clearable />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="mForm.manageMode" placeholder="请选择" style="width:100%">
<el-option :value="1" label="结构件批次管理" />
<el-option :value="2" label="精密件SN 管理" />
</el-select>
</el-form-item>
<el-form-item label="物料类型">
<el-select v-model="mForm.materialType" placeholder="请选择" clearable style="width:100%">
<el-option label="标准件" value="标准件" />
<el-option label="电子件" value="电子件" />
<el-option label="包装件" value="包装件" />
<el-option label="半成品" value="半成品" />
<el-option label="成品" value="成品" />
</el-select>
</el-form-item>
<el-form-item label="说明">
<el-input v-model="mForm.description" type="textarea" :rows="2" placeholder="备注可空" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="matDialog = false">取消</el-button>
<el-button type="primary" :loading="mSaving" @click="saveMaterial">保存</el-button>
</template>
</el-dialog>
<template #footer>
<el-button @click="matDialog = false">取消</el-button>
<el-button type="primary" :loading="mSaving" @click="saveMaterial">保存</el-button>
</template>
</el-dialog>
</template>
</el-card>
<PageHelp :data="helpBaseData" />
</template>
@@ -1,7 +1,7 @@
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { CircleCheck, Download, List, Monitor, Search, Upload, Box, Tickets, EditPen } from '@element-plus/icons-vue'
import { CircleCheck, Download, List, Monitor, Search, Upload, Box, Tickets, EditPen, Location, Goods } from '@element-plus/icons-vue'
import request from '../utils/request'
import { fmtNum } from '../utils/format'
@@ -36,7 +36,8 @@ const entries = [
{ path: '/semi', title: '半成品/成品', icon: Box },
{ path: '/package', title: '装箱管理', icon: Tickets },
{ path: '/ledger', title: '备料台账', icon: Tickets },
{ path: '/base-data', title: '基础数据', icon: EditPen },
{ path: '/zone', title: '区域维护', icon: Location },
{ path: '/material', title: '物料档案', icon: Goods },
{ path: '/display', title: '免登录大屏', icon: Monitor }
]
</script>
@@ -36,6 +36,13 @@ async function submitBatch() {
ElMessage.warning('数量必须大于 0')
return
}
// 管理粒度校验:精密件必须走 SN 入库页。
// 否则后端因 snList 为空直接拒绝,历史上会在失败后残留一张无明细无库存的"空单"。
const mat1 = materials.value.find(m => m.code === form1.materialCode)
if (mat1 && Number(mat1.manageMode) === 2) {
ElMessage.warning('该物料为精密件(按 SN 管理),请切换到「精密件 SN 入库」页扫码录入后再提交')
return
}
submitting1.value = true
try {
const data = await request.post('/inbound/create', {
@@ -95,6 +102,12 @@ async function submitSn() {
ElMessage.warning('请先扫码录入 SN(输入后点击【录入到列表】)')
return
}
// 管理粒度校验(反向):结构件按批次管理,不能带 SN 清单提交
const mat2 = materials.value.find(m => m.code === form2.materialCode)
if (mat2 && Number(mat2.manageMode) === 1) {
ElMessage.warning('该物料为结构件(按批次管理),请切换到「结构件入库」页按数量入库')
return
}
submitting2.value = true
try {
const data = await request.post('/inbound/create', {
@@ -1,31 +1,45 @@
<script setup>
import { onMounted, reactive, ref } from 'vue'
import request from '../utils/request'
import { fmtDateOnly, fmtTime } from '../utils/format'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpInventory } from '../help'
const filters = reactive({ materialCode: '', keyword: '', type: '' })
// 视图切换:库存明细 / 区域汇总
const viewTab = ref('detail')
// manageMode: ''=全部 1=结构件(批次) 2=精密件(SN)
const filters = reactive({ materialCode: '', keyword: '', manageMode: '', zoneCode: '' })
const loading = ref(false)
const batchList = ref([])
const serialList = ref([])
const list = ref([])
const total = ref(0)
const page = reactive({ current: 1, size: 10 })
// 区域下拉数据源(/zone/picker 返回 Zone 数组:zoneCode/zoneName
const zoneOptions = ref([])
async function loadZones() {
try {
const data = await request.get('/zone/picker')
zoneOptions.value = data || []
} catch (e) {
zoneOptions.value = []
}
}
// 统一库存查询:结构件与精密件同表(inventory),一次返回 list
async function load() {
loading.value = true
try {
const data = await request.get('/stock/query', {
params: {
materialCode: filters.materialCode.trim(),
zoneCode: filters.zoneCode,
manageMode: filters.manageMode,
keyword: filters.keyword.trim(),
type: filters.type,
page: page.current,
pageSize: page.size
}
})
batchList.value = data?.batch || []
serialList.value = data?.serial || []
list.value = data?.list || []
total.value = data?.total || 0
} finally {
loading.value = false
@@ -37,101 +51,178 @@ function search() {
load()
}
onMounted(load)
// 可用量:结构件=数量-锁定;精密件=在库时 1-锁定 否则 0
function avail(row) {
if (row.manageMode === 1) return (row.quantity || 0) - (row.lockedQty || 0)
if (row.status === '在库') return 1 - (row.lockedQty || 0)
return 0
}
function typeLabel(m) {
return m === 1 ? '结构件' : m === 2 ? '精密件' : '-'
}
function identifier(row) {
return row.manageMode === 2 ? row.snCode : row.batchNo
}
// 一键导出:当前查询结果(批次+SN 合并)导出为 CSV
function exportCsv() {
if (!list.value.length) {
return
}
const header = ['类型', '标识', '物料编码', '物料名称', '数量', '锁定量', '可用量', '区域', '质量状态', '状态']
const lines = [header.join(',')]
for (const r of list.value) {
const row = [
typeLabel(r.manageMode),
identifier(r) || '',
r.materialCode || '',
r.materialName || '',
r.quantity || 0,
r.lockedQty || 0,
avail(r),
r.zoneCode || '',
r.qualityStatus || '',
r.status || ''
]
lines.push(row.map((c) => `"${String(c).replace(/"/g, '""')}"`).join(','))
}
const blob = new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '库存导出.csv'
a.click()
URL.revokeObjectURL(url)
}
// 区域汇总(各区域 × 质量状态 × 类型 的库存数量)
const summaryLoading = ref(false)
const summaryRows = ref([])
async function loadSummary() {
summaryLoading.value = true
try {
const data = await request.get('/stock/zone-summary')
summaryRows.value = data?.rows || []
} finally {
summaryLoading.value = false
}
}
function handleTabChange(name) {
if (name === 'summary') loadSummary()
}
onMounted(() => {
loadZones()
load()
})
</script>
<template>
<div>
<el-card shadow="never" class="mb12">
<el-form inline @submit.prevent="search">
<el-form-item label="物料编码">
<el-input v-model="filters.materialCode" placeholder="物料编码" clearable style="width:200px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="库存类型">
<el-radio-group v-model="filters.type" @change="search">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="batch">批次</el-radio-button>
<el-radio-button value="serial">SN 序列</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="关键字">
<el-input v-model="filters.keyword" placeholder="批次号/物料名称/SN" clearable style="width:220px"
@keyup.enter="search" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-tabs v-model="viewTab" @tab-change="handleTabChange">
<el-tab-pane label="库存明细" name="detail">
<el-card shadow="never" class="mb12">
<el-form inline @submit.prevent="search">
<el-form-item label="物料编码">
<el-input v-model="filters.materialCode" placeholder="物料编码" clearable style="width:200px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="区域">
<el-select v-model="filters.zoneCode" placeholder="全部区域" clearable style="width:160px" @change="search">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :label="z.zoneName" :value="z.zoneCode" />
</el-select>
</el-form-item>
<el-form-item label="库存类型">
<el-radio-group v-model="filters.manageMode" @change="search">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="1">结构件</el-radio-button>
<el-radio-button value="2">精密件</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="关键字">
<el-input v-model="filters.keyword" placeholder="批次号/物料名称/SN" clearable style="width:220px"
@keyup.enter="search" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
<el-button :disabled="!list.length" @click="exportCsv">导出 CSV</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-if="!filters.type || filters.type === 'batch'" shadow="never" class="mb12"
header="批次库存(结构件)">
<el-table :data="batchList" border size="small" v-loading="loading" empty-text="暂无批次数据">
<el-table-column prop="batchNo" label="批次号" min-width="170" />
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
<el-table-column prop="materialName" label="物料名称" min-width="130">
<template #default="{ row }">{{ row.materialName || '-' }}</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" align="center" />
<el-table-column prop="lockedQty" label="锁定量" width="90" align="center" />
<el-table-column prop="zoneCode" label="区域" width="80" align="center">
<template #default="{ row }">{{ row.zoneCode || '-' }}</template>
</el-table-column>
<el-table-column label="质量状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.qualityStatus === '合格' ? 'success' : row.qualityStatus === '不合格' ? 'danger' : 'info'"
size="small">{{ row.qualityStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column label="生产日期" width="110" align="center">
<template #default="{ row }">{{ fmtDateOnly(row.productionDate) }}</template>
</el-table-column>
<el-table-column prop="supplier" label="供应商" min-width="120">
<template #default="{ row }">{{ row.supplier || '-' }}</template>
</el-table-column>
<el-table-column label="入库时间" width="150" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" header="统一库存(结构件批次 + 精密件 SN)">
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无库存数据">
<el-table-column label="类型" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.manageMode === 1 ? 'primary' : 'warning'">{{ typeLabel(row.manageMode) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="批次号 / SN" min-width="190">
<template #default="{ row }">{{ identifier(row) || '-' }}</template>
</el-table-column>
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
<el-table-column prop="materialName" label="物料名称" min-width="130">
<template #default="{ row }">{{ row.materialName || '-' }}</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" align="center" />
<el-table-column prop="lockedQty" label="锁定量" width="90" align="center" />
<el-table-column label="可用量" width="90" align="center">
<template #default="{ row }">
<span :style="{ color: avail(row) <= 0 ? '#f56c6c' : '' }">{{ avail(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="zoneCode" label="区域" width="80" align="center">
<template #default="{ row }">{{ row.zoneCode || '-' }}</template>
</el-table-column>
<el-table-column label="质量状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.qualityStatus === '合格' ? 'success' : row.qualityStatus === '不合格' ? 'danger' : 'info'"
size="small">{{ row.qualityStatus || '未检' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" 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-column label="入库时间" width="150" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card v-if="!filters.type || filters.type === 'serial'" shadow="never" header="SN 库存(精密件)">
<el-table :data="serialList" border size="small" v-loading="loading" empty-text="暂无 SN 数据">
<el-table-column prop="snCode" label="SN 序列号" min-width="190" />
<el-table-column prop="materialCode" label="物料编码" min-width="150" />
<el-table-column prop="status" label="状态" width="90" 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-column prop="currentZone" label="区域" width="90" align="center">
<template #default="{ row }">{{ row.currentZone || '-' }}</template>
</el-table-column>
<el-table-column label="质量状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.qualityStatus === '合格' ? 'success' : row.qualityStatus === '不合格' ? 'danger' : 'info'"
size="small">{{ row.qualityStatus || '未检' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="入库时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-card>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
@current-change="(p) => { page.current = p; load() }"
@size-change="(s) => { page.size = s; page.current = 1; load() }" />
</div>
</el-tab-pane>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
@current-change="(p) => { page.current = p; load() }"
@size-change="(s) => { page.size = s; page.current = 1; load() }" />
</div>
<el-tab-pane label="区域汇总" name="summary">
<el-card shadow="never" header="各区域库存数量 区域 + 质量状态 + 类型">
<el-table :data="summaryRows" border size="small" v-loading="summaryLoading" empty-text="暂无数据">
<el-table-column prop="zoneCode" label="区域编码" width="110" />
<el-table-column prop="zoneName" label="区域名称" min-width="140" />
<el-table-column prop="quality" label="质量状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.quality === '合格' ? 'success' : row.quality === '不合格' ? 'danger' : 'info'"
size="small">{{ row.quality }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100" align="center" />
<el-table-column prop="qty" label="数量" width="120" align="center" />
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
<PageHelp :data="helpInventory" />
</div>
<PageHelp :data="helpInventory" />
</template>
<style scoped>
.mb12 { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; }
</style>
</style>
@@ -18,7 +18,8 @@ const routes = [
{ path: 'semi', name: 'Semi', component: () => import('../pages/Semi.vue'), meta: { title: '半成品/成品' } },
{ path: 'package', name: 'Package', component: () => import('../pages/Package.vue'), meta: { title: '装箱管理' } },
{ path: 'ledger', name: 'Ledger', component: () => import('../pages/Ledger.vue'), meta: { title: '备料台账' } },
{ path: 'base-data', name: 'BaseData', component: () => import('../pages/BaseData.vue'), meta: { title: '基础数据' } },
{ path: 'zone', name: 'Zone', component: () => import('../pages/BaseData.vue'), props: { mode: 'zone' }, meta: { title: '区域维护' } },
{ path: 'material', name: 'Material', component: () => import('../pages/BaseData.vue'), props: { mode: 'material' }, meta: { title: '物料档案' } },
{ path: 'change-password', name: 'ChangePassword', component: () => import('../pages/ChangePassword.vue'), meta: { title: '修改密码' } }
]
},
+2 -2
View File
@@ -6,8 +6,8 @@
<title>库房客户端</title>
<!-- 系统图标:库房箱体(内联 SVG,避免依赖外部文件) -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%231668dc'/><path d='M16 7l9 4.5v9L16 25l-9-4.5v-9L16 7z' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/><path d='M7 11.5L16 16l9-4.5M16 16v9' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/></svg>">
<script type="module" crossorigin src="/assets/index-BWAZ_x_Q.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DncRC_8X.css">
<script type="module" crossorigin src="/assets/index-B3vta_Fe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B-hDjIvo.css">
</head>
<body>
<div id="app"></div>