Files
bj_power/bj_power_wms/frontend/src/pages/Semi.vue
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

487 lines
22 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
defineOptions({ name: 'Semi' })
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { getRealName } from '../utils/auth'
import PageHelp from '../components/PageHelp.vue'
import MaterialSelect from '../components/MaterialSelect.vue'
import AttachmentPanel from '../components/AttachmentPanel.vue'
import { helpSemi } from '../help'
// 半成品/成品附件(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/图号/归属/产品状态/相关标准/试验记录单号/记录员/记录日期/工单号。 */
const operator = getRealName()
const todayStr = new Date().toLocaleDateString('sv-SE')
function newInForm() {
return {
materialCode: '', zoneCode: '', orderNo: '', completedProcess: '',
// 归属:与入库管理同口径——必选(无「无」选项),选其他可不填编号,工单号/科技项目号必填编号
ownershipType: '', ownershipNo: '',
productStatus: '', relatedStandard: '', testRecordNo: '',
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 })
function addSnLines(cat) {
for (let raw of String(snInputs[cat]).split(/\s+/)) {
const sn = raw.trim()
if (sn && !snLists[cat].includes(sn)) snLists[cat].push(sn)
}
snInputs[cat] = ''
}
function removeSn(cat, i) {
snLists[cat].splice(i, 1)
}
async function submitInbound(cat) {
const f = inForms[cat]
const label = cat === 3 ? '成品' : '半成品'
if (!f.materialCode) {
ElMessage.warning('请选择物料')
return
}
if (!snLists[cat].length) {
ElMessage.warning('请扫码录入 SN(回车追加)')
return
}
if (cat === 3 && !f.productStatus) {
ElMessage.warning('成品入库请选择产品状态(已通过/部分通过/返修)')
return
}
// 归属必选(与入库管理同口径,不再有「无」);选其他可不填编号,工单号/科技项目号必须填编号
if (!f.ownershipType) {
ElMessage.warning('请选择归属(工单号 / 科技项目号 / 其他)')
return
}
if ((f.ownershipType === 'workorder' || f.ownershipType === 'project') && !f.ownershipNo.trim()) {
ElMessage.warning('请填写归属编号(工单号 / 科技项目号)')
return
}
inbounding[cat] = true
let ok = 0
try {
const recordedAt = f.recordedAt ? Math.floor(new Date(f.recordedAt + 'T00:00:00').getTime() / 1000) : 0
for (const sn of snLists[cat]) {
try {
await request.post('/semi/inbound', {
sn,
materialCode: f.materialCode,
completedProcess: f.completedProcess.trim(),
zoneCode: f.zoneCode || undefined,
orderNo: f.orderNo.trim() || undefined,
operator,
category: cat,
ownershipType: f.ownershipType,
ownershipNo: f.ownershipNo.trim() || undefined,
productStatus: f.productStatus || undefined,
relatedStandard: f.relatedStandard.trim() || undefined,
testRecordNo: f.testRecordNo.trim() || undefined,
recordedBy: f.recordedBy.trim() || undefined,
recordedAt: recordedAt || undefined
})
ok++
} catch { /* 单条失败不中断后续 */ }
}
ElMessage.success(`${label}入库完成:成功 ${ok} / ${snLists[cat].length} 条`)
snLists[cat] = []
snInputs[cat] = ''
loadList()
} finally {
inbounding[cat] = false
}
}
/* ---------- 半成品/成品出库(扫SN带出 + 出库原因按钮,客户诉求 L224 ---------- */
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 })
// 扫 SN 回车:带出该件信息(物料/已完成工序/工单/归属),再点原因按钮出库
async function scanSn(cat) {
const f = outForms[cat]
const sn = f.sn.trim()
if (!sn) return
f.info = null
f.reason = ''
try {
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 ? '成品' : '半成品'}记录`)
return
}
if (hit.status !== '在库') {
ElMessage.warning(`该 SN 当前状态为「${hit.status}」,不可出库`)
return
}
f.info = hit
} catch { /* request 已统一提示 */ }
}
async function submitOutbound(cat) {
const f = outForms[cat]
const label = cat === 3 ? '成品' : '半成品'
const sn = f.sn.trim()
if (!sn) {
ElMessage.warning('请扫描或输入 SN')
return
}
if (!f.reason) {
ElMessage.warning('请选择出库原因')
return
}
if (f.reason === '其他') {
ElMessage.warning('出库原因为「其他」时,请在备注式说明后重试(当前按所选原因提交)')
}
outbounding[cat] = true
try {
await request.post('/semi/outbound', { sn, operator, reason: f.reason, category: cat })
ElMessage.success(`${label}出库成功:${sn}${f.reason}`)
f.sn = ''
f.info = null
f.reason = ''
await loadList()
} finally {
outbounding[cat] = false
}
}
/* ---------- 半成品/成品查询(L230 加归属;L222 品类筛选) ---------- */
const qForm = reactive({ materialCode: '', status: '', sn: '', ownershipNo: '', category: '' })
const list = ref([])
const total = ref(0)
const loading = ref(false)
const page = reactive({ current: 1, size: 10 })
async function loadList() {
loading.value = true
try {
const data = await request.get('/semi/query', {
params: {
materialCode: qForm.materialCode.trim(),
status: qForm.status,
sn: qForm.sn.trim(),
ownershipNo: qForm.ownershipNo.trim(),
category: qForm.category || undefined,
page: page.current,
pageSize: page.size
}
})
list.value = data?.list || []
total.value = data?.total || 0
} finally {
loading.value = false
}
}
function search() {
page.current = 1
loadList()
}
onMounted(() => {
loadList()
})
</script>
<template>
<el-card shadow="never">
<el-tabs>
<!-- ============ 半成品入库 ============ -->
<el-tab-pane label="半成品入库">
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="物料">
<MaterialSelect v-model="inForms[2].materialCode" placeholder="选择物料(图号/名称搜索)" />
</el-form-item>
<el-form-item label="已完成工序">
<el-input v-model="inForms[2].completedProcess" placeholder="如:1,3,5(可空)" clearable />
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="inForms[2].orderNo" placeholder="来源工单号(可空)" clearable />
</el-form-item>
<el-form-item label="归属">
<div style="display:flex;gap:8px;width:100%">
<el-select v-model="inForms[2].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[2].ownershipNo" placeholder="归属编号" clearable />
</div>
</el-form-item>
<el-form-item label="相关标准">
<el-input v-model="inForms[2].relatedStandard" placeholder="检验细则/试验方案号(可空)" clearable />
</el-form-item>
<el-form-item label="试验记录单号">
<el-input v-model="inForms[2].testRecordNo" placeholder="可空" clearable />
</el-form-item>
<el-form-item label="记录员">
<el-input v-model="inForms[2].recordedBy" placeholder="记录员" clearable />
</el-form-item>
<el-form-item label="记录日期">
<el-date-picker v-model="inForms[2].recordedAt" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="区域">
<el-input v-model="inForms[2].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[2]" type="textarea" :rows="3" autofocus
placeholder="连续扫描 SN,每次回车追加(可一次多条,空白分隔)"
@keydown.enter.prevent="addSnLines(2)" />
</el-form-item>
</el-form>
<div style="margin-bottom:8px">待入库 SN<b>{{ snLists[2].length }}</b> </div>
<el-table :data="snLists[2].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(2, row.i)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top:12px">
<el-button type="primary" :loading="inbounding[2]" @click="submitInbound(2)">提交半成品入库</el-button>
<el-button @click="snLists[2] = []">清空</el-button>
</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>
<el-form-item label="SN">
<el-input v-model="outForms[2].sn" placeholder="扫描或输入 SN,回车带出信息" clearable autofocus
@keyup.enter="scanSn(2)" />
</el-form-item>
</el-form>
<el-alert v-if="outForms[2].info" type="info" :closable="false" style="max-width:680px;margin-bottom:12px">
<div>图号<b>{{ outForms[2].info.materialCode || '-' }}</b> 名称{{ outForms[2].info.materialName || '-' }}</div>
<div>已完成工序{{ outForms[2].info.completedProcess || '-' }} 工单号{{ outForms[2].info.orderNo || '-' }} 归属{{ outForms[2].info.ownershipNo || '-' }}</div>
</el-alert>
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="出库原因">
<el-radio-group v-model="outForms[2].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[2]" :disabled="!outForms[2].info"
@click="submitOutbound(2)">半成品出库</el-button>
</el-form-item>
</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-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>
<el-form-item label="图号">
<el-input v-model="qForm.materialCode" placeholder="图号" clearable style="width:160px" />
</el-form-item>
<el-form-item label="归属单号">
<el-input v-model="qForm.ownershipNo" placeholder="工单号/科技项目号" clearable style="width:160px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="qForm.status" clearable placeholder="请选择状态" style="width:110px">
<el-option value="在库" label="在库" />
<el-option value="出库" label="出库" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
</el-form-item>
</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">
<template #default="{ row }">{{ row.materialName || '-' }}</template>
</el-table-column>
<el-table-column prop="completedProcess" label="已完成工序" min-width="100">
<template #default="{ row }">{{ row.completedProcess || '-' }}</template>
</el-table-column>
<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>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '在库' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="outboundReason" label="出库原因" width="90" align="center">
<template #default="{ row }">{{ row.outboundReason || '-' }}</template>
</el-table-column>
<el-table-column prop="orderNo" label="工单号" min-width="120">
<template #default="{ row }">{{ row.orderNo || '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="150" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="success" size="small" @click="openAttach(row)">附件</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, prev, pager, next" :total="total"
:current-page="page.current" :page-size="page.size"
@current-change="(p) => { page.current = p; loadList() }" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 半成品/成品附件抽屉(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" />
</template>
<style scoped>
.pager { display: flex; justify-content: flex-end; margin-top: 10px; }
</style>