docs(business): 生成业务契约文档并重构MES客户端代码

- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
SunYF
2026-09-20 18:25:44 +08:00
parent 5f68e77d5f
commit 8e6e8e1d20
46 changed files with 1052 additions and 3954 deletions
+9 -9
View File
@@ -62,9 +62,9 @@ export const helpInbound = {
export const helpOutbound = {
title: '出库管理(统一主表)',
overview:
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n三个页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/退货/样品/报废/发货/其他),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息),含工单号与目标工位。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n通用出库操作顺序(客户诉求 问题17/18/20):① 先选物料(下拉统一显示 编码/图号/类型/品类)→ ② 系统自动判定出库类型(电气件/结构件,无需手选)→ ③ 选出库类别(纯标签,不联动任何地方;选「其他」时备注必填原因)→ ④ 填归属(工单编号/科技项目号/其他,非其他时编号必填)→ ⑤ 结构件从可用批次下拉选批次(默认 FIFO 首个)并填数量 / 电气件逐个扫 SN → ⑥ 选目标工位(下拉,来自 MES 工位列表,不可手输)。\n\n说明:出库单号由系统自动生成、不可修改,规则与入库一致(入库 IB+ / 出库 OB+ 同一编号规则);出库无法与入库批次/合同对应,故出库类别仅是标签记录,列表与出库单都显示。质量门禁:发货/样品/其他类出库仅允许「合格」品;退货/报废/退料类豁免(本身即处置不合格品)。',
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n三个页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";工单号下拉选择(来自 MES 实时工单,未完成优先,无输入默认展示前 20 条),支持输入模糊搜索;MES 不可达或工单未同步时下拉可能为空,可手输兜底(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/退货/样品/报废/发货/其他),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息),含工单号与目标工位。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n通用出库操作顺序(客户诉求 问题17/18/20):① 先选物料(下拉统一显示 编码/图号/类型/品类)→ ② 系统自动判定出库类型(电气件/结构件,无需手选)→ ③ 选出库类别(纯标签,不联动任何地方;选「其他」时备注必填原因)→ ④ 填归属(工单编号/科技项目号/其他,非其他时编号必填)→ ⑤ 结构件从可用批次下拉选批次(默认 FIFO 首个)并填数量 / 电气件逐个扫 SN → ⑥ 选目标工位(下拉,来自 MES 工位列表,不可手输)。\n\n说明:出库单号由系统自动生成、不可修改,规则与入库一致(入库 IB+ / 出库 OB+ 同一编号规则);出库无法与入库批次/合同对应,故出库类别仅是标签记录,列表与出库单都显示。质量门禁:发货/样品/其他类出库仅允许「合格」品;退货/报废/退料类豁免(本身即处置不合格品)。',
fields: [
{ name: '备料出库-工单号', source: '手动输入。数据来自 MES 已建的工单及导入的物料台账。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '输入工单号回车,查询该工单的物料台账' },
{ name: '备料出库-工单号', source: '下拉选择 + 模糊搜索。数据来自 MES 实时工单(未完成优先,无输入默认前 20 条)。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '下拉选工单号(或输入关键字模糊搜索;MES 不可达时可手输兜底),选中即查询该工单的物料台账' },
{ name: '备料出库-目标工位', source: '下拉选择 DOCK01~DOCK20(选填,可过滤搜索)。', purpose: 'AGV 配送送达的接驳台(toDock 参数)', fill: '选择如 DOCK05;不选默认运到产线入口 DOCK01' },
{ name: '备料出库-一键分配出库', source: '系统自动按 FIFO 分配。数据来自合格区Z02的批次库存。', purpose: '从合格区按先进先出凑够缺口并出库', fill: '点按钮,系统自动算批次并二次确认' },
{ name: '通用出库-物料', source: '下拉选择(全项目统一组件)。', purpose: '选定要出库的物料', fill: '下拉显示 编码/图号/类型(电气件/结构件/其他)/品类(原材料/半成品/成品/其他),支持远程搜索' },
@@ -255,14 +255,14 @@ export const helpZone = {
export const helpDock = {
title: '接驳台维护',
overview:
'维护全厂接驳台(AGV 取送货的物理停靠位)主数据。接驳台是独立主数据,不依附工位:除了产线各工位旁,库房收货口、线边缓存区等其他位置也有接驳台位。\n\n三种类型:\n· 产线:位于产线工位旁,必须绑定一个工位号,且与工位一一对应(一个工位只能有一个接驳台、一个接驳台只能给一个工位);\n· 库房:库房收货/发货口(如 DOCK21),不绑定工位;\n· 其他:缓存区、暂存位等,不绑定工位。\n\n接驳台编码是产线与 AGV 任务的引用锚点,创建后不可修改;MES「关联工位」页绑定接驳台时从本页数据实时拉取。有托盘占用的接驳台不允许删除,需先处理托盘。',
'查看全厂接驳台(AGV 取送货的物理停靠位)。接驳台是系统预置的只读主数据,本页仅供查看,不提供新增/修改/删除。\n\n三种类型:\n· 产线:位于产线工位旁,与工位一一对应(DOCK01~20 对应工位 1~20,一个工位一个接驳台);\n· 库房:库房收货/发货口(如 DOCK21),不绑定工位;\n· 其他:缓存区、暂存位等,不绑定工位。\n\n接驳台编码是产线与 AGV 任务的引用锚点;MES「关联工位」页绑定接驳台时从本页数据实时拉取。',
fields: [
{ name: '接驳台编码', source: '手工填写,创建后不可改。', purpose: '接驳台唯一编号,产线/AGV 任务据此定位', fill: '如 DOCK21、DOCK22;建议 DOCK+两位数字' },
{ name: '接驳台名称', source: '手工填写。', purpose: '接驳台中文名,便于识别', fill: '如 库房收货接驳台、三号缓存接驳台' },
{ name: '类型', source: '选择:产线 / 库房 / 其他。', purpose: '区分接驳台所处位置与用途', fill: '产线接驳台需填工位号库房/其他不' },
{ name: '绑定工位号', source: '仅产线类型填写。', purpose: '该接驳台服务哪个工位,与工位 1:1', fill: '如 3 表示 3 号工位;该工位已绑定其他接驳台时会提示' },
{ name: '托盘状态', source: '系统自动维护(PLC 托盘传感器 / AGV 到位卸货)。', purpose: '看该接驳台当前是否被占用', fill: '不可手改;有托盘的接驳台不允许删除' },
{ name: '状态', source: '选择:启用 / 停用。', purpose: '停用的接驳台不再参与配送与下拉选择', fill: '默认启用' }
{ name: '接驳台编码', source: '系统预置,只读。', purpose: '接驳台唯一编号,产线/AGV 任务据此定位', fill: '只读查看,如 DOCK01~DOCK21' },
{ name: '接驳台名称', source: '系统预置,只读。', purpose: '接驳台中文名,便于识别', fill: '只读查看' },
{ name: '类型', source: '系统预置:产线 / 库房 / 其他。', purpose: '区分接驳台所处位置与用途', fill: '只读;产线接驳台绑定工位号库房/其他不绑定' },
{ name: '绑定工位号', source: '仅产线类型有,与工位 1:1。', purpose: '该接驳台服务哪个工位', fill: '只读,如 3 表示 3 号工位' },
{ name: '托盘状态', source: '系统自动维护(PLC 托盘传感器 / AGV 到位卸货)。', purpose: '看该接驳台当前是否被占用', fill: '只读,实时反映是否有托盘占用' },
{ name: '状态', source: '系统预置:启用 / 停用。', purpose: '停用的接驳台不再参与配送与下拉选择', fill: '只读查看' }
]
}
+83 -2
View File
@@ -55,6 +55,7 @@ const f2Shelf = ref([]); const f3Shelf = ref([]); const f3Layer = ref([])
const f4Shelf = ref([]); const f4Layer = ref([])
const cShelf = ref([]); const cLayer = ref([])
const bShelf = ref([])
const lbShelf = ref([])
async function picker(level, params) {
const data = await request.get('/zone/picker', { params: { level, ...params } })
@@ -239,6 +240,47 @@ async function submitBatch() {
}
}
/* ---------- 区域维护:层批量生成 ---------- */
const layerBatchDialog = ref(false)
const layerBatchForm = reactive({ zoneCode: '', shelfNo: '', layerStart: 1, layerEnd: 1 })
const layerBatchPreview = ref([])
const layerBatchPreviewing = ref(false)
const layerBatchSubmitting = ref(false)
function openLayerBatchGen() {
Object.assign(layerBatchForm, { zoneCode: '', shelfNo: '', layerStart: 1, layerEnd: 1 })
layerBatchPreview.value = []
layerBatchDialog.value = true
}
async function onLayerBatchRegionChange() {
layerBatchForm.shelfNo = ''
await loadShelfOpts(lbShelf, layerBatchForm.zoneCode)
}
async function previewLayerBatch() {
if (!layerBatchForm.zoneCode || !layerBatchForm.shelfNo) { ElMessage.warning('请选择区域与货架'); return }
layerBatchPreviewing.value = true
try {
const data = await request.post('/zone/batch-generate', { ...layerBatchForm, level: 3, preview: true })
layerBatchPreview.value = data?.list || []
} finally {
layerBatchPreviewing.value = false
}
}
async function submitLayerBatch() {
const go = await confirmAction(`已预检 ${layerBatchPreview.value.length} 个层,确认批量生成到库位表?(已存在的自动跳过)`, '批量生成确认')
if (!go) return
layerBatchSubmitting.value = true
try {
const data = await request.post('/zone/batch-generate', { ...layerBatchForm, level: 3, preview: false })
ElMessage.success(`已生成 ${data?.created || 0} 个层,跳过已存在 ${data?.skipped || 0}`)
layerBatchDialog.value = false
zoneTabLoaded[3] = false
if (activeZoneTab.value === 3) await loadNodes(3)
} finally {
layerBatchSubmitting.value = false
}
}
/* ---------- 物料档案维护 ---------- */
const materials = ref([])
const materialTotal = ref(0)
@@ -411,8 +453,11 @@ async function exportMaterials() {
name: matFilter.name.trim(),
spec: matFilter.spec.trim(),
manageMode: matFilter.manageMode,
itemType: matFilter.itemType
}, '物料档案.xlsx')
itemType: matFilter.itemType,
startDate: matFilter.startDate,
endDate: matFilter.endDate
}, '物料档案.xlsx', { countUrl: '/material/query', name: '物料档案' })
if (!name) return
ElMessage.success('已导出:' + name)
} catch (e) {
ElMessage.error('导出失败:' + (e?.message || e))
@@ -603,6 +648,7 @@ async function onMaterialFile(e) {
<el-form-item>
<el-button type="primary" :loading="nodeState[3].loading" @click="searchNodes(3)">查询</el-button>
<el-button v-if="can('zone:create')" type="success" @click="openNodeCreate(3)">新增层</el-button>
<el-button v-if="can('zone:create')" type="warning" @click="openLayerBatchGen">批量生成层</el-button>
</el-form-item>
</el-form>
<el-table :data="nodeState[3].list" border size="small" v-loading="nodeState[3].loading" row-key="id" empty-text="暂无层">
@@ -763,6 +809,41 @@ async function onMaterialFile(e) {
共 {{ batchPreview.length }} 个位置,其中已存在 {{ batchPreview.filter(x=>x.exists).length }} 个(生成时自动跳过)。
</div>
</el-dialog>
<!-- 层批量生成弹窗 -->
<el-dialog v-model="layerBatchDialog" title="批量生成层" width="640px" append-to-body destroy-on-close>
<el-form label-width="90px">
<el-form-item label="区域" required>
<el-select v-model="layerBatchForm.zoneCode" placeholder="请选择区域" filterable style="width:100%" @change="onLayerBatchRegionChange">
<el-option v-for="r in regionOptions" :key="r.code" :value="r.code" :label="r.code + (r.zoneName ? ' '+r.zoneName : '')" />
</el-select>
</el-form-item>
<el-form-item label="货架" required>
<el-select v-model="layerBatchForm.shelfNo" placeholder="请选择货架" filterable :disabled="!layerBatchForm.zoneCode" style="width:100%">
<el-option v-for="s in lbShelf" :key="s.code" :value="s.code" :label="s.code" />
</el-select>
</el-form-item>
<el-form-item label="层范围">
<el-input-number v-model="layerBatchForm.layerStart" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
<span style="margin:0 8px">至</span>
<el-input-number v-model="layerBatchForm.layerEnd" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="layerBatchPreviewing" @click="previewLayerBatch">预览</el-button>
<el-button type="success" :loading="layerBatchSubmitting" :disabled="!layerBatchPreview.length" @click="submitLayerBatch">确认生成</el-button>
</el-form-item>
</el-form>
<el-table v-if="layerBatchPreview.length" :data="layerBatchPreview" border size="small" max-height="300">
<el-table-column prop="layerNo" label="层号" width="90" align="center" />
<el-table-column prop="layerName" label="层名称" min-width="200" />
<el-table-column label="是否已存在" width="110" align="center">
<template #default="{ row }"><el-tag :type="row.exists ? 'info' : 'success'" size="small">{{ row.exists ? '已存在' : '新增' }}</el-tag></template>
</el-table-column>
</el-table>
<div v-if="layerBatchPreview.length" style="color:#909399;font-size:12px;margin-top:6px">
共 {{ layerBatchPreview.length }} 个层,其中已存在 {{ layerBatchPreview.filter(x=>x.exists).length }} 个(生成时自动跳过)。
</div>
</el-dialog>
</template>
<!-- 物料档案 -->
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="dock-page">
<el-card shadow="never">
<el-alert type="info" :closable="false" show-icon style="margin-bottom:12px"
title="接驳台为系统预置只读主数据:由实施按《产线布局说明》初始化(DOCK01~20 产线、DOCK21 库房),本页仅可查看,不提供新增/修改/删除。如需调整由实施维护种子数据或数据库。类型分 产线 / 库房 / 其他;只有产线接驳台绑定工位号,且与工位一一对应。" />
title="接驳台是产线与库房之间供 AGV 取送货的物理停靠位,为系统预置只读主数据,本页仅供查看。类型分 产线 / 库房 / 其他:产线接驳台与工位一一对应(DOCK01~20 对应工位 1~20),库房接驳台为收货/发货口(DOCK21),其他为缓存/暂存位;只有产线接驳台绑定工位号。" />
<!-- 列表页禁止关键字混搜编码精确名称模糊类型/状态各自独立下拉需求5N -->
<el-form inline :model="filter">
+1 -1
View File
@@ -440,7 +440,7 @@ async function exportCsv() {
voided: filters.voided,
startDate: dateRange.value?.[0] || '',
endDate: dateRange.value?.[1] || ''
}, '入库管理.xlsx')
}, '入库管理.xlsx', { countUrl: '/inbound/query', name: '入库记录' })
} catch (e) {
/* 错误提示由请求层统一处理 */
}
@@ -333,7 +333,7 @@ async function exportCsv() {
inspector: filters.inspector.trim(),
startDate: filters.startDate,
endDate: filters.endDate
}, '检验记录.xlsx')
}, '检验记录.xlsx', { countUrl: '/inspection/query', name: '检验记录' })
} catch (e) {
/* 错误提示由请求层统一处理 */
}
@@ -233,11 +233,11 @@ async function exportCsv() {
materialCode: filters.materialCode.trim(),
materialName: filters.materialName.trim(),
manageMode: filters.manageMode
}, '物料汇总.xlsx')
}, '物料汇总.xlsx', { count: matTotal.value, name: '物料汇总' })
return
}
if (viewTab.value === 'summary') {
await exportXlsxFetch('/api/stock/export', { view: 'zone' }, '区域汇总.xlsx')
await exportXlsxFetch('/api/stock/export', { view: 'zone' }, '区域汇总.xlsx', { count: summaryRows.value.length, name: '区域汇总' })
return
}
await exportXlsxFetch('/api/stock/export', {
@@ -248,7 +248,7 @@ async function exportCsv() {
manageMode: filters.manageMode,
startDate: filters.startDate,
endDate: filters.endDate
}, '库存汇总.xlsx')
}, '库存汇总.xlsx', { countUrl: '/stock/query', name: '库存明细' })
} catch (e) {
// 错误信息已由 request 层处理
}
+39 -13
View File
@@ -24,14 +24,22 @@ const loadingPrep = ref(false)
const queried = ref(false)
const rows = ref([])
onMounted(loadOrders)
async function loadOrders() {
const orders = ref([])
const ordersLoading = ref(false)
// 工单号来自 MES 实时查询:无输入默认拉「未完成优先」的前 20 条;输入时按工单号模糊远程搜索。
async function searchOrders(query) {
ordersLoading.value = true
try {
const data = await request.get('/orders', { params: { page: 1, pageSize: 200 } })
const data = await request.get('/orders', { params: { orderNo: query || '', limit: 20 } })
orders.value = Array.isArray(data) ? data : data?.list || []
} catch { orders.value = [] }
finally { ordersLoading.value = false }
}
const orders = ref([])
async function loadOrders() { await searchOrders('') }
function orderLabel(o) { return o.productName ? `${o.workOrderNo} ${o.productName}` : o.workOrderNo }
// 选中下拉项或手输兜底(allow-create)后直接查该工单台账
function onOrderPick(v) { if (v && String(v).trim()) queryPrep() }
onMounted(loadOrders)
const pendingRows = computed(() => rows.value.filter(r => r.status !== '领料完结'))
function gapOf(row) { return Math.max((row.totalQty || 0) - (row.outQty || 0), 0) }
@@ -326,7 +334,14 @@ async function submitGen() {
}
/* ===================== 出库记录(统一主表,全部 + 导出) ===================== */
const recFilters = reactive({ outboundType: '', status: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: [] })
// 出库记录默认展示近 3 个月(与后端导出时间硬规则一致,保证「列表条数 = 导出条数」)
function recDefaultRange() {
const p = (n) => String(n).padStart(2, '0')
const fmt = (d) => `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
const end = new Date(); const start = new Date(); start.setMonth(start.getMonth() - 3)
return [fmt(start), fmt(end)]
}
const recFilters = reactive({ outboundType: '', status: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: recDefaultRange() })
const recRows = ref([])
const recTotal = ref(0)
const loadingRec = ref(false)
@@ -346,8 +361,8 @@ async function loadRecords() {
page: recPage.current, pageSize: recPage.size
}
if (Array.isArray(recFilters.dateRange) && recFilters.dateRange.length === 2) {
params.startAt = new Date(recFilters.dateRange[0]).getTime()
params.endAt = new Date(recFilters.dateRange[1]).getTime() + 86399999
params.startDate = recFilters.dateRange[0]
params.endDate = recFilters.dateRange[1]
}
const data = await request.get('/outbound/query', { params })
recRows.value = (data?.list || []).map(r => ({ ...r, snCodes: snOf(r) }))
@@ -355,16 +370,20 @@ async function loadRecords() {
} finally { loadingRec.value = false }
}
function searchRec() { recPage.current = 1; loadRecords() }
function resetRec() { Object.assign(recFilters, { outboundType: '', status: '', materialCode: '', boxNo: '', dateRange: [] }); searchRec() }
function resetRec() { Object.assign(recFilters, { outboundType: '', status: '', materialCode: '', boxNo: '', dateRange: recDefaultRange() }); searchRec() }
function downloadCSV() {
// 时间窗口与列表保持一致:未选/已清空时回落近 3 个月,保证确认框条数 = 实际导出条数
const dr = (Array.isArray(recFilters.dateRange) && recFilters.dateRange.length === 2) ? recFilters.dateRange : recDefaultRange()
exportXlsxFetch('/api/outbound/export', {
outboundType: recFilters.outboundType,
status: recFilters.status,
materialCode: recFilters.materialCode.trim(),
boxNo: recFilters.boxNo.trim()
}, '出库管理.xlsx')
.then(() => ElMessage.success('导出成功'))
boxNo: recFilters.boxNo.trim(),
startDate: dr[0],
endDate: dr[1]
}, '出库管理.xlsx', { countUrl: '/outbound/query', name: '出库记录' })
.then((name) => { if (name) ElMessage.success('已导出:' + name) })
.catch(() => ElMessage.error('导出失败'))
}
@@ -387,8 +406,15 @@ onMounted(loadDicts)
<el-card shadow="never" class="mb12">
<el-form inline label-width="90px" @submit.prevent>
<el-form-item label="工单号">
<el-input v-model="queryForm.orderNo" placeholder="输入工单号回车查询" clearable autofocus
style="width:260px" @keyup.enter="queryPrep" />
<el-select v-model="queryForm.orderNo" filterable remote allow-create default-first-option
clearable reserve-keyword :remote-method="searchOrders" :loading="ordersLoading"
placeholder="选择或输入工单号(默认未完成优先前20条,可模糊搜索)"
style="width:340px" @change="onOrderPick">
<el-option v-for="o in orders" :key="o.workOrderNo" :value="o.workOrderNo" :label="orderLabel(o)">
<span style="float:left">{{ o.workOrderNo }}</span>
<span style="float:right;color:#909399;font-size:12px">{{ o.productName }} {{ o.status }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="操作人">
<el-input :model-value="operator" disabled style="width:160px" />
+20 -137
View File
@@ -11,15 +11,14 @@ import MaterialSelect from '../components/MaterialSelect.vue'
import AttachmentPanel from '../components/AttachmentPanel.vue'
import { helpSemi } from '../help'
// 半成品/成品附件(bizType=semibizId=SN
// 半成品附件(bizType=semibizId=SN
const attachDrawer = reactive({ visible: false, sn: '' })
function openAttach(row) {
Object.assign(attachDrawer, { visible: true, sn: row.sn })
}
/* ---------- 半成品/成品入库 ----------
* 客户诉求 问题记录 L222:半成品与成品入库要区分(category 2/3);
* L218 成品入库 9 字段:SN/图号/归属/产品状态/相关标准/试验记录单号/记录员/记录日期/工单号。 */
/* ---------- 半成品入库category=2 ----------
* 成品由 MES 完工自动回流入库、经 WMS 完工检验,不在本页手工入库。 */
const operator = getRealName()
const todayStr = new Date().toLocaleDateString('sv-SE')
@@ -33,11 +32,11 @@ function newInForm() {
recordedBy: operator, recordedAt: todayStr
}
}
// inForms[2]=半成品入库表单inForms[3]=成品入库表单
const inForms = reactive({ 2: newInForm(), 3: newInForm() })
const snInputs = reactive({ 2: '', 3: '' })
const snLists = reactive({ 2: [], 3: [] })
const inbounding = reactive({ 2: false, 3: false })
// inForms[2]=半成品入库表单category=2
const inForms = reactive({ 2: newInForm() })
const snInputs = reactive({ 2: '' })
const snLists = reactive({ 2: [] })
const inbounding = reactive({ 2: false })
function addSnLines(cat) {
for (let raw of String(snInputs[cat]).split(/\s+/)) {
@@ -53,7 +52,7 @@ function removeSn(cat, i) {
async function submitInbound(cat) {
const f = inForms[cat]
const label = cat === 3 ? '成品' : '半成品'
const label = '半成品'
if (!f.materialCode) {
ElMessage.warning('请选择物料')
return
@@ -62,10 +61,6 @@ async function submitInbound(cat) {
ElMessage.warning('请扫码录入 SN(回车追加)')
return
}
if (cat === 3 && !f.productStatus) {
ElMessage.warning('成品入库请选择产品状态(已通过/部分通过/返修)')
return
}
// 归属必选(与入库管理同口径,不再有「无」);选其他可不填编号,工单号/科技项目号必须填编号
if (!f.ownershipType) {
ElMessage.warning('请选择归属(工单编号 / 科技项目号 / 其他)')
@@ -109,11 +104,11 @@ async function submitInbound(cat) {
}
}
/* ---------- 半成品/成品出库(扫SN带出 + 出库原因按钮,客户诉求 L224 ---------- */
/* ---------- 半成品出库(扫SN带出 + 出库原因按钮);成品发货走「出库管理」通用出库 ---------- */
const outReasons = ['包装', '继续生产', '检验试验', '其他']
// outForms[2]=半成品出库outForms[3]=成品出库
const outForms = reactive({ 2: { sn: '', info: null, reason: '' }, 3: { sn: '', info: null, reason: '' } })
const outbounding = reactive({ 2: false, 3: false })
// outForms[2]=半成品出库category=2
const outForms = reactive({ 2: { sn: '', info: null, reason: '' } })
const outbounding = reactive({ 2: false })
// 扫 SN 回车:带出该件信息(物料/已完成工序/工单/归属),再点原因按钮出库
async function scanSn(cat) {
@@ -126,7 +121,7 @@ async function scanSn(cat) {
const data = await request.get('/semi/query', { params: { sn, category: cat, page: 1, pageSize: 5 } })
const hit = (data?.list || []).find((r) => r.sn === sn)
if (!hit) {
ElMessage.warning(`未找到该 SN 的${cat === 3 ? '成品' : '半成品'}记录`)
ElMessage.warning(`未找到该 SN 的半成品记录`)
return
}
if (hit.status !== '在库') {
@@ -139,7 +134,7 @@ async function scanSn(cat) {
async function submitOutbound(cat) {
const f = outForms[cat]
const label = cat === 3 ? '成品' : '半成品'
const label = '半成品'
const sn = f.sn.trim()
if (!sn) {
ElMessage.warning('请扫描或输入 SN')
@@ -167,8 +162,8 @@ async function submitOutbound(cat) {
}
}
/* ---------- 半成品/成品查询(L230 加归属L222 品类筛选 ---------- */
const qForm = reactive({ materialCode: '', status: '', sn: '', ownershipNo: '', category: '' })
/* ---------- 半成品查询(L230 加归属) ---------- */
const qForm = reactive({ materialCode: '', status: '', sn: '', ownershipNo: '' })
const list = ref([])
const total = ref(0)
const loading = ref(false)
@@ -183,7 +178,6 @@ async function loadList() {
status: qForm.status,
sn: qForm.sn.trim(),
ownershipNo: qForm.ownershipNo.trim(),
category: qForm.category || undefined,
page: page.current,
pageSize: page.size
}
@@ -271,73 +265,6 @@ onMounted(() => {
</div>
</el-tab-pane>
<!-- ============ 成品入库 ============ -->
<el-tab-pane label="成品入库">
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="物料">
<MaterialSelect v-model="inForms[3].materialCode" placeholder="选择物料(图号/名称搜索)" />
</el-form-item>
<el-form-item label="产品状态">
<el-radio-group v-model="inForms[3].productStatus">
<el-radio-button value="已通过">已通过</el-radio-button>
<el-radio-button value="部分通过">部分通过</el-radio-button>
<el-radio-button value="返修">返修</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="inForms[3].orderNo" placeholder="来源工单号(可空)" clearable />
</el-form-item>
<el-form-item label="归属">
<div style="display:flex;gap:8px;width:100%">
<el-select v-model="inForms[3].ownershipType" placeholder="请选择" style="width:150px">
<el-option value="workorder" label="工单编号" />
<el-option value="project" label="科技项目号" />
<el-option value="other" label="其他" />
</el-select>
<el-input v-model="inForms[3].ownershipNo" placeholder="归属编号" clearable />
</div>
</el-form-item>
<el-form-item label="相关标准">
<el-input v-model="inForms[3].relatedStandard" placeholder="检验细则/试验方案号(可空)" clearable />
</el-form-item>
<el-form-item label="试验记录单号">
<el-input v-model="inForms[3].testRecordNo" placeholder="可空" clearable />
</el-form-item>
<el-form-item label="记录员">
<el-input v-model="inForms[3].recordedBy" placeholder="记录员" clearable />
</el-form-item>
<el-form-item label="记录日期">
<el-date-picker v-model="inForms[3].recordedAt" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="区域">
<el-input v-model="inForms[3].zoneCode" placeholder="库区(可空)" clearable />
</el-form-item>
<el-form-item label="操作人">
<el-input :model-value="operator" disabled />
</el-form-item>
<el-form-item label="扫码录入">
<el-input v-model="snInputs[3]" type="textarea" :rows="3"
placeholder="连续扫描 SN,每次回车追加(可一次多条,空白分隔)"
@keydown.enter.prevent="addSnLines(3)" />
</el-form-item>
</el-form>
<div style="margin-bottom:8px">待入库 SN<b>{{ snLists[3].length }}</b> </div>
<el-table :data="snLists[3].map((v, i) => ({ i, v }))" size="small" border max-height="220"
empty-text="暂无 SN" style="max-width:760px">
<el-table-column type="index" label="#" width="56" align="center" />
<el-table-column prop="v" label="SN 序列号" min-width="220" />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="removeSn(3, row.i)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top:12px">
<el-button type="primary" :loading="inbounding[3]" @click="submitInbound(3)">提交成品入库</el-button>
<el-button @click="snLists[3] = []">清空</el-button>
</div>
</el-tab-pane>
<!-- ============ 半成品出库 ============ -->
<el-tab-pane label="半成品出库">
<el-form label-width="110px" style="max-width:680px" @submit.prevent>
@@ -366,43 +293,9 @@ onMounted(() => {
</el-form>
</el-tab-pane>
<!-- ============ 成品出库 ============ -->
<el-tab-pane label="成品出库">
<el-form label-width="110px" style="max-width:680px" @submit.prevent>
<el-form-item label="SN">
<el-input v-model="outForms[3].sn" placeholder="扫描或输入 SN,回车带出信息" clearable
@keyup.enter="scanSn(3)" />
</el-form-item>
</el-form>
<el-alert v-if="outForms[3].info" type="info" :closable="false" style="max-width:680px;margin-bottom:12px">
<div>图号<b>{{ outForms[3].info.materialCode || '-' }}</b> 名称{{ outForms[3].info.materialName || '-' }} 产品状态{{ outForms[3].info.productStatus || '-' }}</div>
<div>工单号{{ outForms[3].info.orderNo || '-' }} 归属{{ outForms[3].info.ownershipNo || '-' }} 试验记录单号{{ outForms[3].info.testRecordNo || '-' }}</div>
</el-alert>
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="出库原因">
<el-radio-group v-model="outForms[3].reason">
<el-radio-button v-for="r in outReasons" :key="r" :value="r">{{ r }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="操作人">
<el-input :model-value="operator" disabled style="max-width:300px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="outbounding[3]" :disabled="!outForms[3].info"
@click="submitOutbound(3)">成品出库</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- ============ 查询 ============ -->
<el-tab-pane label="半成品/成品查询">
<el-tab-pane label="半成品查询">
<el-form inline @submit.prevent="search">
<el-form-item label="品类">
<el-select v-model="qForm.category" clearable placeholder="全部" style="width:110px">
<el-option :value="2" label="半成品" />
<el-option :value="3" label="成品" />
</el-select>
</el-form-item>
<el-form-item label="SN">
<el-input v-model="qForm.sn" placeholder="SN(模糊)" clearable style="width:160px" />
</el-form-item>
@@ -424,13 +317,6 @@ onMounted(() => {
</el-form>
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无记录">
<el-table-column prop="id" label="ID" width="64" align="center" />
<el-table-column label="品类" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.category === 3 ? 'warning' : 'primary'" size="small">
{{ row.category === 3 ? '成品' : '半成品' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sn" label="SN" min-width="170" />
<el-table-column prop="materialCode" label="图号" min-width="130" />
<el-table-column prop="materialName" label="名称" min-width="120">
@@ -442,9 +328,6 @@ onMounted(() => {
<el-table-column prop="ownershipNo" label="归属单号" min-width="120">
<template #default="{ row }">{{ row.ownershipNo || '-' }}</template>
</el-table-column>
<el-table-column prop="productStatus" label="产品状态" width="90" align="center">
<template #default="{ row }">{{ row.productStatus || '-' }}</template>
</el-table-column>
<el-table-column prop="zoneCode" label="区域" width="80" align="center">
<template #default="{ row }">{{ row.zoneCode || '-' }}</template>
</el-table-column>
@@ -477,8 +360,8 @@ onMounted(() => {
</el-tabs>
</el-card>
<!-- 半成品/成品附件抽屉(bizType=semibizId=SN):入库资料 / 试验记录单 -->
<el-drawer v-model="attachDrawer.visible" :title="`半成品/成品附件:${attachDrawer.sn}`" size="620px">
<!-- 半成品附件抽屉(bizType=semibizId=SN):入库资料 / 试验记录单 -->
<el-drawer v-model="attachDrawer.visible" :title="`半成品附件:${attachDrawer.sn}`" size="620px">
<AttachmentPanel biz-type="semi" :biz-id="attachDrawer.sn" title="附件入库资料 / 试验记录" />
</el-drawer>
<PageHelp :data="helpSemi" page="Semi" />
@@ -258,9 +258,10 @@ function statusTagType(s) {
}
}
async function exportDetail(no) {
async function exportDetail(row) {
try {
await exportXlsxFetch('/api/stocktake/export', { stocktakeNo: no }, `盘点明细_${no}.xlsx`)
await exportXlsxFetch('/api/stocktake/export', { stocktakeNo: row.stocktakeNo }, `盘点明细_${row.stocktakeNo}.xlsx`,
{ count: row.totalTargets, name: '盘点明细' })
} catch (e) {
ElMessage.error('导出失败:' + (e?.message || e))
}
@@ -503,7 +504,7 @@ const currentStep = computed(() => {
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="exportDetail(row.stocktakeNo)">导出明细</el-button>
<el-button link type="primary" size="small" @click="exportDetail(row)">导出明细</el-button>
</template>
</el-table-column>
</el-table>
+50 -3
View File
@@ -1,5 +1,42 @@
import request from './request'
import { getToken } from './auth'
import { confirmAction } from './confirm'
// 导出前统一确认:告知用户即将导出的数据条数。
// count 为 null/undefined 时(无法预知条数)提示“全部”;否则回显具体条数。
export async function confirmExport(count, name = '数据') {
const msg = (count === null || count === undefined)
? `即将导出全部${name},是否继续?`
: `即将导出 ${count}${name},是否继续?`
return confirmAction(msg, '导出确认', { confirmButtonText: '确认导出', cancelButtonText: '取消' })
}
function ymd(d) {
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
// effExportParams 归一化导出日期参数,避免后端 normalizeExportRange「只传一端即报错(400)」:
// 仅当 params 显式带有 startDate/endDate 键时生效;一端为空则补齐(缺结束=今天,缺起始=结束前推3个月);
// 两端都空则保持为空(由后端按各自规则处理,列表与导出用同一份参数,条数才一致)。
export function effExportParams(params) {
if (!params || (!('startDate' in params) && !('endDate' in params))) return params
let s = params.startDate || ''
let e = params.endDate || ''
if (s && !e) e = ymd(new Date())
else if (!s && e) { const d = new Date(e); d.setMonth(d.getMonth() - 3); s = ymd(d) }
return { ...params, startDate: s, endDate: e }
}
// countByQuery 用与导出完全相同的筛选参数请求列表接口(pageSize=1)拿精确总数,
// 保证「确认框条数」= 实际导出条数(列表与导出共用同一套 where 条件)。
async function countByQuery(countUrl, params) {
try {
const data = await request.get(countUrl, { params: { ...params, page: 1, pageSize: 1 } })
const t = data?.total
return (t === null || t === undefined) ? null : Number(t)
} catch { return null }
}
// 下载 xlsx 导出:后端返回 .xlsx 二进制,文件名由后端 Content-Disposition 指定。
// 各导出接口:{path}?format=xlsx&<筛选参数>
@@ -33,8 +70,18 @@ export function exportXlsx(path, params, fallbackName = '导出.xlsx') {
}
// 带 token 的二进制下载(保底方式:直接构造带 Authorization 的 fetch
export async function exportXlsxFetch(path, params, fallbackName = '导出.xlsx') {
const qs = new URLSearchParams({ format: 'xlsx', ...params }).toString()
// confirm:导出前确认。默认 true=弹确认框但不显示条数(提示“全部”);
// 传 { count, name } 直接回显条数;或传 { countUrl, name } 由本函数用相同筛选参数拉取精确条数;
// 传 false 可跳过确认(不建议)。
export async function exportXlsxFetch(path, params, fallbackName = '导出.xlsx', confirm = true) {
const eff = effExportParams(params)
if (confirm) {
const opt = confirm === true ? {} : confirm
let count = (opt.count === null || opt.count === undefined) ? null : opt.count
if (count === null && opt.countUrl) count = await countByQuery(opt.countUrl, eff)
if (!(await confirmExport(count, opt.name))) return null
}
const qs = new URLSearchParams({ format: 'xlsx', ...eff }).toString()
const res = await fetch(`${path}?${qs}`, {
headers: { Authorization: `Bearer ${getToken()}` }
})
@@ -64,4 +111,4 @@ export async function exportXlsxFetch(path, params, fallbackName = '导出.xlsx'
return filename
}
export default { exportXlsx, exportXlsxFetch }
export default { exportXlsx, exportXlsxFetch, confirmExport }
+4 -25
View File
@@ -14,36 +14,15 @@ import (
//
// 生产环境口径(用户裁决 2026-09-20):种子只允许写入系统跑起来所必需的固定主数据,
// 严禁预置演示物料/演示库存/演示账号等假业务数据。具体:
// - 区域:库房四大基础分区 Z01~Z04(库存落位的根节点,level=1);
// - 管理员账号:admin(初始密码 123456,登录后应立即修改);其余账号由管理员在「账号管理」按需创建、分配角色;
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改)。
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改,只能靠种子写入)。
//
// 不预置区域/库位:区域是现场库房布局,因项目而异,且「区域维护」页面可增删改,由实施按现场实际建立,不硬编码进种子。
// 权限与角色不在此:由 handler.SeedRBAC 幂等初始化(reset-all/seed 命令与 POST /api/rbac/seed 共用同一函数)。
func SeedIfEmpty(client *ent.Client) error {
ctx := context.Background()
// 1. 区域(层级节点 level=1
zones, _ := client.Zone.Query().Count(ctx)
if zones == 0 {
for _, z := range []struct{ code, name string }{
{"Z01", "待检区"},
{"Z02", "标准存储区"},
{"Z03", "暂存区"},
{"Z04", "退货区"},
} {
client.Zone.Create().
SetLevel(1).
SetCode(z.code).
SetZoneName(z.name).
SetZoneCode(z.code).
SetParentCode("").
SetStatus("启用").
SaveX(ctx)
}
slog.Info("seed: zones created")
}
// 2. 管理员账号(唯一预置账号;初始密码 123456,登录后应立即修改)
// 1. 管理员账号(唯一预置账号;初始密码 123456,登录后应立即修改
users, _ := client.User.Query().Count(ctx)
if users == 0 {
adminHash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
@@ -56,7 +35,7 @@ func SeedIfEmpty(client *ent.Client) error {
slog.Info("seed: admin user created (admin/123456)")
}
// 3. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
// 2. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
docks, _ := client.Dock.Query().Count(ctx)
if docks == 0 {
for i := 1; i <= 20; i++ {
+3 -7
View File
@@ -240,8 +240,8 @@ func semiView(inv *ent.Inventory) *semiViewRow {
}
}
// querySemiHandler 半成品/成品查询(读统一库存主表 category=2/3
// 客户诉求 L230:查询增加归属(工单号/科技项目号);L222:半成品成品分开查(category 参数)
// querySemiHandler 半成品查询(读统一库存主表 category=2 半成品)。
// 半成品管理页只处理半成品成品由 MES 完工回流入库、经 WMS 完工检验,在「库存查询」页按品类查看,不在本页手工出入库
func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
page := atoi(r.URL.Query().Get("page"), 1)
@@ -250,12 +250,8 @@ func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
status := r.URL.Query().Get("status")
sn := r.URL.Query().Get("sn")
ownershipNo := r.URL.Query().Get("ownershipNo")
category := atoi(r.URL.Query().Get("category"), 0) // 2=半成品 3=成品 0=两者都查
q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryIn(invCatSemi, invCatFinished))
if category == invCatSemi || category == invCatFinished {
q = q.Where(inventory.CategoryEQ(category))
}
q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi))
if materialCode != "" {
q = q.Where(inventory.MaterialCodeEqualFold(materialCode))
}
+14 -2
View File
@@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"net/http"
"net/url"
"sort"
"time"
@@ -26,8 +27,15 @@ type mesOrder struct {
// ordersHandler 调用 MES 内部接口获取工单列表,把未完成工单排前面,供备料出库下拉选择
func ordersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
url := ctx.Config.Mes.BaseURL + "/api/internal/order/query?orderNo=" + r.URL.Query().Get("orderNo")
req, err := http.NewRequest(http.MethodGet, url, nil)
orderNo := r.URL.Query().Get("orderNo")
// limit:无输入条件时默认返回 20 条(未完成优先),供备料出库工单号下拉首屏展示;
// 模糊搜索时同样返回匹配结果的前 limit 条。
limit := atoi(r.URL.Query().Get("limit"), 20)
if limit <= 0 {
limit = 20
}
mesURL := ctx.Config.Mes.BaseURL + "/api/internal/order/query?orderNo=" + url.QueryEscape(orderNo)
req, err := http.NewRequest(http.MethodGet, mesURL, nil)
if err != nil {
// 构造请求都失败属代码级异常,返回空列表让备料出库页可降级(仍可手输工单号)
ok(w, []mesOrder{})
@@ -66,6 +74,10 @@ func ordersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
return list[i].WorkOrderNo < list[j].WorkOrderNo
})
// 排序(未完成优先)后截断:无输入默认前 20 条;模糊搜索返回匹配的前 20 条。
if len(list) > limit {
list = list[:limit]
}
ok(w, list)
}
}
+60 -4
View File
@@ -463,11 +463,13 @@ type zoneBatchGenReq struct {
LayerEnd int `json:"layerEnd"`
PosStart int `json:"posStart"`
PosEnd int `json:"posEnd"`
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,同时确保层存在)
Preview bool `json:"preview"`
}
// batchGenerateHandler 批量生成位置号:选区域→货架→层范围→位置范围,先预览后提交。
// 自动确保层节点(level3)存在;已存在的位置号跳过不重复建
// batchGenerateHandler 批量生成库位节点:选区域→货架→范围,先预览后提交。
// level=3:只生成「层」节点(按层范围);level=4(默认):生成「位置号」节点(层×位置),并自动确保层节点存在
// 已存在的节点跳过不重复建。
func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req zoneBatchGenReq
@@ -481,8 +483,16 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "请选择区域与货架")
return
}
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart || req.PosStart < 0 || req.PosEnd < req.PosStart {
fail(w, http.StatusBadRequest, "层/位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
level := req.Level
if level != 3 {
level = 4
}
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
if level == 4 && (req.PosStart < 0 || req.PosEnd < req.PosStart) {
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
@@ -496,6 +506,52 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
for l := req.LayerStart; l <= req.LayerEnd; l++ {
layers = append(layers, l)
}
// ---------- level=3:批量生成层节点 ----------
if level == 3 {
if len(layers) > 2000 {
fail(w, http.StatusBadRequest, "一次最多生成 2000 个层,请缩小范围")
return
}
type genLayer struct {
LayerNo string `json:"layerNo"`
LayerName string `json:"layerName"`
Exists bool `json:"exists"`
}
layerList := make([]genLayer, 0, len(layers))
for _, l := range layers {
ls := strconv.Itoa(l)
exists, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0())
layerList = append(layerList, genLayer{LayerNo: ls, LayerName: req.ShelfNo + "架" + ls + "层", Exists: exists})
}
if req.Preview {
ok(w, map[string]any{"preview": true, "level": 3, "list": layerList, "total": len(layerList)})
return
}
createdL, skippedL := 0, 0
for _, it := range layerList {
if it.Exists {
skippedL++
continue
}
if _, err := ctx.EntClient.Zone.Create().
SetLevel(3).SetCode(it.LayerNo).SetParentCode(req.ShelfNo).
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(it.LayerNo).
SetName(it.LayerName).SetStatus("启用").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
return
}
createdL++
}
ctx.EventLog.Write(ctx0(), "zone.batch", r.Header.Get("X-Username"), "zone", req.ShelfNo,
"批量生成层 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+strconv.Itoa(req.LayerStart)+"-"+strconv.Itoa(req.LayerEnd)+
":新建"+strconv.Itoa(createdL)+" 跳过"+strconv.Itoa(skippedL), nil)
ok(w, map[string]any{"preview": false, "level": 3, "created": createdL, "skipped": skippedL, "total": len(layers)})
return
}
// ---------- level=4:批量生成位置号 ----------
poss := make([]int, 0)
for p := req.PosStart; p <= req.PosEnd; p++ {
poss = append(poss, p)
+1 -1
View File
@@ -6,7 +6,7 @@
<title>WMS库房管理</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-Cv7S4CDQ.js"></script>
<script type="module" crossorigin src="/assets/index-K2_ija_j.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_gcVHML.css">
</head>
<body>