2026-08-28 15:06:01 +08:00
|
|
|
|
<script setup>
|
|
|
|
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
import request from '../utils/request'
|
|
|
|
|
|
import { getRealName } from '../utils/auth'
|
2026-08-28 17:23:44 +08:00
|
|
|
|
import PageHelp from '../components/PageHelp.vue'
|
|
|
|
|
|
import { helpInbound } from '../help'
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 公共字典:物料 / 区域
|
|
|
|
|
|
const materials = ref([])
|
|
|
|
|
|
const zones = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
const [ms, zs] = await Promise.all([request.get('/material/list'), request.get('/zone/list')])
|
|
|
|
|
|
materials.value = Array.isArray(ms) ? ms : ms?.list || []
|
|
|
|
|
|
zones.value = Array.isArray(zs) ? zs : zs?.list || []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- Tab1 结构件入库(批次,支持多批到货追加) ---------- */
|
|
|
|
|
|
const batchRef = ref()
|
|
|
|
|
|
const submitting1 = ref(false)
|
|
|
|
|
|
const lastInboundNo = ref('')
|
|
|
|
|
|
const form1 = reactive({
|
|
|
|
|
|
materialCode: '', batchNo: '', quantity: 1,
|
2026-08-29 15:30:25 +08:00
|
|
|
|
zoneCode: '', productionDate: '', supplier: '', remark: ''
|
2026-08-28 15:06:01 +08:00
|
|
|
|
})
|
|
|
|
|
|
const rules1 = {
|
|
|
|
|
|
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
|
|
|
|
|
|
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitBatch() {
|
|
|
|
|
|
await batchRef.value.validate()
|
|
|
|
|
|
if (!form1.quantity || form1.quantity <= 0) {
|
|
|
|
|
|
ElMessage.warning('数量必须大于 0')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
submitting1.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/create', {
|
|
|
|
|
|
inboundType: 'purchase',
|
|
|
|
|
|
materialCode: form1.materialCode,
|
|
|
|
|
|
batchNo: form1.batchNo.trim(),
|
|
|
|
|
|
quantity: form1.quantity,
|
|
|
|
|
|
zoneCode: form1.zoneCode,
|
|
|
|
|
|
productionDate: form1.productionDate,
|
|
|
|
|
|
supplier: form1.supplier.trim(),
|
2026-08-29 15:30:25 +08:00
|
|
|
|
remark: form1.remark.trim(),
|
2026-08-28 15:06:01 +08:00
|
|
|
|
operator: getRealName()
|
|
|
|
|
|
})
|
|
|
|
|
|
lastInboundNo.value = data?.inboundNo || ''
|
|
|
|
|
|
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''},数量 ${data?.quantity ?? form1.quantity}`)
|
|
|
|
|
|
form1.batchNo = ''
|
|
|
|
|
|
form1.quantity = 1
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting1.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- Tab2 精密件 SN 入库(连续扫码、实时已扫数量) ---------- */
|
|
|
|
|
|
const snInput = ref('')
|
|
|
|
|
|
const submitting2 = ref(false)
|
|
|
|
|
|
const snList = ref([])
|
2026-08-29 15:30:25 +08:00
|
|
|
|
const form2 = reactive({ materialCode: '', zoneCode: '', remark: '' })
|
|
|
|
|
|
|
|
|
|
|
|
// 支持多种分隔符:中英文逗号、中英文句号、分号、顿号、空格、回车、制表符等
|
|
|
|
|
|
const SN_SEP = /[,,.。;;、\s]+/
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
|
|
|
|
|
function addSnLines() {
|
2026-08-29 15:30:25 +08:00
|
|
|
|
const lines = String(snInput.value).split(SN_SEP)
|
2026-08-28 15:06:01 +08:00
|
|
|
|
for (let raw of lines) {
|
|
|
|
|
|
const sn = raw.trim()
|
|
|
|
|
|
if (!sn) continue
|
|
|
|
|
|
if (snList.value.includes(sn)) continue
|
|
|
|
|
|
snList.value.push(sn)
|
|
|
|
|
|
}
|
|
|
|
|
|
snInput.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeSn(index) {
|
|
|
|
|
|
snList.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitSn() {
|
|
|
|
|
|
if (!form2.materialCode) {
|
|
|
|
|
|
ElMessage.warning('请选择物料')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!snList.value.length) {
|
|
|
|
|
|
ElMessage.warning('请先扫码录入 SN(输入后回车追加)')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
submitting2.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/create', {
|
|
|
|
|
|
inboundType: 'purchase',
|
|
|
|
|
|
materialCode: form2.materialCode,
|
|
|
|
|
|
zoneCode: form2.zoneCode,
|
|
|
|
|
|
snList: [...snList.value],
|
2026-08-29 15:30:25 +08:00
|
|
|
|
remark: form2.remark.trim(),
|
2026-08-28 15:06:01 +08:00
|
|
|
|
operator: getRealName()
|
|
|
|
|
|
})
|
|
|
|
|
|
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''},SN 共 ${snList.value.length} 条`)
|
|
|
|
|
|
snList.value = []
|
|
|
|
|
|
snInput.value = ''
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting2.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- Tab3 Excel 导入 ---------- */
|
|
|
|
|
|
const uploadRef = ref()
|
|
|
|
|
|
const excelFiles = ref([])
|
|
|
|
|
|
const importing = ref(false)
|
|
|
|
|
|
const excelResult = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
function onExcelChange(file, files) {
|
|
|
|
|
|
excelFiles.value = files.slice(-1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onExceed(files) {
|
|
|
|
|
|
uploadRef.value.clearFiles()
|
|
|
|
|
|
const file = files[0]
|
|
|
|
|
|
if (file) uploadRef.value.handleStart(file)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function importExcel() {
|
|
|
|
|
|
const item = excelFiles.value[0] || (uploadRef.value?.uploadFiles?.[0])
|
|
|
|
|
|
const raw = item?.raw
|
|
|
|
|
|
if (!raw) {
|
|
|
|
|
|
ElMessage.warning('请先选择 .xlsx 文件')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const fd = new FormData()
|
|
|
|
|
|
fd.append('file', raw)
|
|
|
|
|
|
importing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/batch-excel', fd)
|
|
|
|
|
|
excelResult.value = data || {}
|
|
|
|
|
|
ElMessage.success(`导入完成:共 ${data?.total ?? 0} 行,成功 ${data?.success ?? 0} 行`)
|
|
|
|
|
|
uploadRef.value?.clearFiles()
|
|
|
|
|
|
excelFiles.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
importing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<el-tabs>
|
|
|
|
|
|
<el-tab-pane label="结构件入库">
|
|
|
|
|
|
<el-form ref="batchRef" :model="form1" :rules="rules1" label-width="110px" style="max-width:560px">
|
|
|
|
|
|
<el-form-item label="物料" prop="materialCode">
|
|
|
|
|
|
<el-select v-model="form1.materialCode" filterable placeholder="选择物料编码" style="width:100%">
|
|
|
|
|
|
<el-option v-for="m in materials" :key="m.id" :value="m.code"
|
|
|
|
|
|
:label="`${m.code} ${m.name || ''}`" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="批次号">
|
|
|
|
|
|
<el-input v-model="form1.batchNo" placeholder="留空则自动生成" clearable />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="数量" prop="quantity">
|
|
|
|
|
|
<el-input-number v-model="form1.quantity" :min="1" :step="1" step-strictly style="width:100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="区域">
|
|
|
|
|
|
<el-select v-model="form1.zoneCode" placeholder="选择库区" clearable style="width:100%">
|
|
|
|
|
|
<el-option v-for="z in zones" :key="z.id" :value="z.zoneCode"
|
|
|
|
|
|
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="生产日期">
|
|
|
|
|
|
<el-date-picker v-model="form1.productionDate" type="date" value-format="YYYY-MM-DD"
|
|
|
|
|
|
placeholder="选择生产日期" style="width:100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="供应商">
|
|
|
|
|
|
<el-input v-model="form1.supplier" placeholder="供应商名称" clearable />
|
|
|
|
|
|
</el-form-item>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="form1.remark" placeholder="备注(可空)" clearable />
|
|
|
|
|
|
</el-form-item>
|
2026-08-28 15:06:01 +08:00
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" :loading="submitting1" @click="submitBatch">提交入库</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<el-alert v-if="lastInboundNo" type="success" :closable="false"
|
|
|
|
|
|
:title="`最近入库单号:${lastInboundNo}`" style="max-width:560px" />
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tab-pane :label="`精密件 SN 入库${snList.length ? `(已扫 ${snList.length})` : ''}`">
|
|
|
|
|
|
<el-form label-width="110px" style="max-width:680px">
|
|
|
|
|
|
<el-form-item label="物料">
|
|
|
|
|
|
<el-select v-model="form2.materialCode" filterable placeholder="选择物料编码" style="width:100%">
|
|
|
|
|
|
<el-option v-for="m in materials" :key="m.id" :value="m.code"
|
|
|
|
|
|
:label="`${m.code} ${m.name || ''}`" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="区域">
|
|
|
|
|
|
<el-select v-model="form2.zoneCode" placeholder="选择库区(可空)" clearable style="width:100%">
|
|
|
|
|
|
<el-option v-for="z in zones" :key="z.id" :value="z.zoneCode"
|
|
|
|
|
|
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="扫码录入">
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<div style="width:100%">
|
|
|
|
|
|
<el-input v-model="snInput" type="textarea" :rows="4"
|
|
|
|
|
|
placeholder="扫入或粘贴 SN,可用 逗号/分号/顿号/空格/回车 分隔,一次可录入多条" />
|
|
|
|
|
|
<el-button type="primary" plain style="margin-top:6px" :disabled="!snInput.trim()"
|
|
|
|
|
|
@click="addSnLines">录入到列表</el-button>
|
|
|
|
|
|
<span style="margin-left:8px;color:#909399;font-size:12px">点【录入到列表】加入下方清单,回车只是换行</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="form2.remark" type="textarea" :rows="2" placeholder="本批 SN 的统一备注(可空)" />
|
2026-08-28 15:06:01 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:8px">
|
|
|
|
|
|
已录入 <b>{{ snList.length }}</b> 条 SN
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="snList.map((v, i) => ({ i, v }))" size="small" border max-height="320" empty-text="暂无 SN">
|
|
|
|
|
|
<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(row.i)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div style="margin-top:12px">
|
|
|
|
|
|
<el-button type="primary" :loading="submitting2" @click="submitSn">提交入库</el-button>
|
|
|
|
|
|
<el-button @click="snList = []">清空列表</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="Excel 导入">
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<el-alert type="info" :closable="false" title="列头顺序(首行忽略):物料编码 | 批次号(可空自动生成) | 数量 | 生产日期 | 供应商 | 区域 | 备注"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
style="max-width:760px;margin-bottom:14px" />
|
|
|
|
|
|
<el-upload ref="uploadRef" drag accept=".xlsx" :limit="1" :auto-upload="false"
|
|
|
|
|
|
:on-change="onExcelChange" :on-exceed="onExceed">
|
|
|
|
|
|
<div class="dropzone">将 .xlsx 文件拖到此处,或点击选择文件</div>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<div style="margin-top:12px">
|
|
|
|
|
|
<el-button type="primary" :loading="importing" @click="importExcel">开始导入</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="excelResult">
|
|
|
|
|
|
<el-divider />
|
|
|
|
|
|
<h4>导入结果:共 {{ excelResult.total }} 行,成功 {{ excelResult.success }} 行,失败 {{ excelResult.total - excelResult.success }} 行</h4>
|
|
|
|
|
|
<el-table :data="excelResult.rows || []" size="small" border max-height="360">
|
|
|
|
|
|
<el-table-column prop="row" label="行号" width="70" align="center" />
|
|
|
|
|
|
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
|
|
|
|
|
|
<el-table-column prop="batchNo" label="批次号" min-width="160" />
|
|
|
|
|
|
<el-table-column prop="quantity" label="数量" width="90" align="center" />
|
|
|
|
|
|
<el-table-column label="结果" min-width="200">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag v-if="!row.error" type="success" size="small">成功</el-tag>
|
|
|
|
|
|
<el-tag v-else type="danger" size="small">{{ row.error }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</el-card>
|
2026-08-28 17:23:44 +08:00
|
|
|
|
<PageHelp :data="helpInbound" />
|
2026-08-28 15:06:01 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dropzone { padding: 30px 0; color: #606266; }
|
|
|
|
|
|
</style>
|