docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
@@ -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: '只读查看' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 物料档案 -->
|
||||
|
||||
@@ -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),其他为缓存/暂存位;只有产线接驳台绑定工位号。" />
|
||||
|
||||
<!-- 列表页禁止关键字混搜:编码精确、名称模糊、类型/状态各自独立下拉(需求5、N) -->
|
||||
<el-form inline :model="filter">
|
||||
|
||||
@@ -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 层处理
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -11,15 +11,14 @@ import MaterialSelect from '../components/MaterialSelect.vue'
|
||||
import AttachmentPanel from '../components/AttachmentPanel.vue'
|
||||
import { helpSemi } from '../help'
|
||||
|
||||
// 半成品/成品附件(bizType=semi,bizId=SN)
|
||||
// 半成品附件(bizType=semi,bizId=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=semi,bizId=SN):入库资料 / 试验记录单 -->
|
||||
<el-drawer v-model="attachDrawer.visible" :title="`半成品/成品附件:${attachDrawer.sn}`" size="620px">
|
||||
<!-- 半成品附件抽屉(bizType=semi,bizId=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>
|
||||
|
||||
@@ -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 }
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user