2026-08-28 15:06:01 +08:00
|
|
|
|
<script setup>
|
2026-09-03 14:20:37 +08:00
|
|
|
|
defineOptions({ name: 'Inbound' })
|
2026-09-03 18:13:59 +08:00
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
2026-09-03 14:20:37 +08:00
|
|
|
|
import { useRoute } from 'vue-router'
|
2026-09-03 18:13:59 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-08-28 15:06:01 +08:00
|
|
|
|
import request from '../utils/request'
|
2026-09-03 18:13:59 +08:00
|
|
|
|
import { getRealName } from '../utils/auth'
|
2026-09-03 14:20:37 +08:00
|
|
|
|
import { fmtTime } from '../utils/format'
|
|
|
|
|
|
import { exportXlsxFetch } from '../utils/export'
|
2026-09-03 18:13:59 +08:00
|
|
|
|
import { can } from '../utils/perm'
|
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
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
|
const route = useRoute()
|
2026-09-03 18:13:59 +08:00
|
|
|
|
const operator = getRealName()
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
2026-09-03 18:13:59 +08:00
|
|
|
|
// ===== 多 Tab:结构件入库 / 精密件 SN 入库 在前(常操作),入库记录在后 =====
|
|
|
|
|
|
const activeTab = ref('batch') // batch=结构件入库 / sn=精密件SN入库 / records=入库记录
|
|
|
|
|
|
|
|
|
|
|
|
// 公共字典:物料 / 区域
|
|
|
|
|
|
const materials = ref([])
|
|
|
|
|
|
const zones = ref([])
|
|
|
|
|
|
async function loadDicts() {
|
|
|
|
|
|
if (materials.value.length && zones.value.length) return
|
|
|
|
|
|
const [ms, zs] = await Promise.all([request.get('/material/list'), request.get('/zone/picker')])
|
|
|
|
|
|
materials.value = Array.isArray(ms) ? ms : ms?.list || []
|
|
|
|
|
|
zones.value = Array.isArray(zs) ? zs : zs?.list || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按当前页过滤物料:结构件页只显示批次管理(manageMode=1),SN 页只显示序列号管理(manageMode=2)
|
|
|
|
|
|
const batchMaterials = computed(() => materials.value.filter((m) => Number(m.manageMode) === 1))
|
|
|
|
|
|
const snMaterials = computed(() => materials.value.filter((m) => Number(m.manageMode) === 2))
|
|
|
|
|
|
|
|
|
|
|
|
/* ===================== Tab1 结构件入库(批次,支持多批到货追加) ===================== */
|
|
|
|
|
|
const batchRef = ref()
|
|
|
|
|
|
const submitting1 = ref(false)
|
|
|
|
|
|
const lastInboundNo = ref('')
|
|
|
|
|
|
const form1 = reactive({
|
|
|
|
|
|
materialCode: '', batchNo: '', quantity: 1,
|
|
|
|
|
|
zoneCode: '', productionDate: '', supplier: '', remark: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
const rules1 = {
|
|
|
|
|
|
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
|
|
|
|
|
|
zoneCode: [{ 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
|
|
|
|
|
|
}
|
|
|
|
|
|
const mat1 = materials.value.find(m => m.code === form1.materialCode)
|
|
|
|
|
|
if (mat1 && Number(mat1.manageMode) === 2) {
|
|
|
|
|
|
ElMessage.warning('该物料为精密件(按 SN 管理),请切换到「精密件 SN 入库」页扫码录入后再提交')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
submitting1.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/create', {
|
|
|
|
|
|
inboundType: 'purchase',
|
|
|
|
|
|
materialCode: form1.materialCode,
|
|
|
|
|
|
batchNo: form1.batchNo.trim(),
|
|
|
|
|
|
quantity: form1.quantity,
|
|
|
|
|
|
zoneCode: form1.zoneCode,
|
|
|
|
|
|
productionDate: form1.productionDate,
|
|
|
|
|
|
supplier: form1.supplier.trim(),
|
|
|
|
|
|
remark: form1.remark.trim(),
|
|
|
|
|
|
operator
|
|
|
|
|
|
})
|
|
|
|
|
|
lastInboundNo.value = data?.inboundNo || ''
|
|
|
|
|
|
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''},数量 ${data?.quantity ?? form1.quantity}`)
|
|
|
|
|
|
form1.batchNo = ''
|
|
|
|
|
|
form1.quantity = 1
|
|
|
|
|
|
// 入库成功后刷新记录 Tab
|
|
|
|
|
|
await loadRecords()
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting1.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ===================== Tab2 精密件 SN 入库(连续扫码、实时已扫数量) ===================== */
|
|
|
|
|
|
const snInput = ref('')
|
|
|
|
|
|
const submitting2 = ref(false)
|
|
|
|
|
|
const snList = ref([])
|
|
|
|
|
|
const form2 = reactive({ materialCode: '', zoneCode: '', remark: '' })
|
|
|
|
|
|
|
|
|
|
|
|
// 支持多种分隔符:中英文逗号、中英文句号、分号、顿号、空格、回车、制表符等
|
|
|
|
|
|
const SN_SEP = /[,,.。;;、\s]+/
|
|
|
|
|
|
|
|
|
|
|
|
function addSnLines() {
|
|
|
|
|
|
const lines = String(snInput.value).split(SN_SEP)
|
|
|
|
|
|
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 (!form2.zoneCode) {
|
|
|
|
|
|
ElMessage.warning('请选择入库区域')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!snList.value.length) {
|
|
|
|
|
|
ElMessage.warning('请先扫码录入 SN(输入后点击【录入到列表】)')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const mat2 = materials.value.find(m => m.code === form2.materialCode)
|
|
|
|
|
|
if (mat2 && Number(mat2.manageMode) === 1) {
|
|
|
|
|
|
ElMessage.warning('该物料为结构件(按批次管理),请切换到「结构件入库」页按数量入库')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
submitting2.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/create', {
|
|
|
|
|
|
inboundType: 'purchase',
|
|
|
|
|
|
materialCode: form2.materialCode,
|
|
|
|
|
|
zoneCode: form2.zoneCode,
|
|
|
|
|
|
snList: [...snList.value],
|
|
|
|
|
|
remark: form2.remark.trim(),
|
|
|
|
|
|
operator
|
|
|
|
|
|
})
|
|
|
|
|
|
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''},SN 共 ${snList.value.length} 条`)
|
|
|
|
|
|
snList.value = []
|
|
|
|
|
|
snInput.value = ''
|
|
|
|
|
|
// 入库成功后刷新记录 Tab
|
|
|
|
|
|
await loadRecords()
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting2.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ===================== 各 Tab 批量导入(结构件 batch / 精密件 sn) ===================== */
|
|
|
|
|
|
const importing = ref(false)
|
|
|
|
|
|
const excelResult = ref(null)
|
|
|
|
|
|
const batchUploadRef = ref()
|
|
|
|
|
|
const snUploadRef = ref()
|
|
|
|
|
|
|
|
|
|
|
|
function pickRaw(which) {
|
|
|
|
|
|
const refEl = which === 'batch' ? batchUploadRef.value : snUploadRef.value
|
|
|
|
|
|
return refEl?.uploadFiles?.[0]?.raw
|
|
|
|
|
|
}
|
|
|
|
|
|
function onExceed(files, which) {
|
|
|
|
|
|
const refEl = which === 'batch' ? batchUploadRef.value : snUploadRef.value
|
|
|
|
|
|
refEl?.clearFiles()
|
|
|
|
|
|
const file = files[0]
|
|
|
|
|
|
if (file) refEl?.handleStart(file)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function importExcel(mode, which) {
|
|
|
|
|
|
const raw = pickRaw(which)
|
|
|
|
|
|
if (!raw) { ElMessage.warning('请先选择 .xlsx 文件'); return }
|
|
|
|
|
|
const fd = new FormData()
|
|
|
|
|
|
fd.append('file', raw)
|
|
|
|
|
|
fd.append('mode', mode)
|
|
|
|
|
|
importing.value = true
|
|
|
|
|
|
excelResult.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/inbound/batch-excel', fd)
|
|
|
|
|
|
excelResult.value = data || {}
|
|
|
|
|
|
if (data?.failed > 0) {
|
|
|
|
|
|
ElMessage.error(`导入被拒绝:共 ${data.failed} 行存在错误,已整体回退(未发生任何写入),请修正后重传`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.success(`批量导入成功:共 ${data?.success ?? 0} 行`)
|
|
|
|
|
|
await loadRecords()
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
importing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ===================== Tab3 入库记录(统一主表 + 抽屉明细,不分页内嵌子表) ===================== */
|
2026-09-03 14:20:37 +08:00
|
|
|
|
// 查询条件
|
|
|
|
|
|
const filters = reactive({ inboundNo: '', materialCode: '', manageMode: '', keyword: '' })
|
|
|
|
|
|
const dateRange = ref([]) // [开始日期, 结束日期] YYYY-MM-DD
|
2026-09-03 18:13:59 +08:00
|
|
|
|
const loadingRec = ref(false)
|
2026-09-03 14:20:37 +08:00
|
|
|
|
const list = ref([])
|
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
|
const page = reactive({ current: 1, size: 10 })
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
|
// 列表数据:结构件(manageMode=1)与精密件(manageMode=2)同主表(inbound_order)
|
2026-09-03 18:13:59 +08:00
|
|
|
|
async function loadRecords() {
|
|
|
|
|
|
loadingRec.value = true
|
2026-08-28 15:06:01 +08:00
|
|
|
|
try {
|
2026-09-03 14:20:37 +08:00
|
|
|
|
const data = await request.get('/inbound/query', {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
inboundNo: filters.inboundNo.trim(),
|
|
|
|
|
|
materialCode: filters.materialCode.trim(),
|
|
|
|
|
|
manageMode: filters.manageMode,
|
|
|
|
|
|
keyword: filters.keyword.trim(),
|
|
|
|
|
|
startDate: dateRange.value?.[0] || '',
|
|
|
|
|
|
endDate: dateRange.value?.[1] || '',
|
|
|
|
|
|
page: page.current,
|
|
|
|
|
|
pageSize: page.size
|
|
|
|
|
|
}
|
2026-08-28 15:06:01 +08:00
|
|
|
|
})
|
2026-09-03 14:20:37 +08:00
|
|
|
|
list.value = data?.list || []
|
|
|
|
|
|
total.value = data?.total || 0
|
2026-08-28 15:06:01 +08:00
|
|
|
|
} finally {
|
2026-09-03 18:13:59 +08:00
|
|
|
|
loadingRec.value = false
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
|
function search() {
|
|
|
|
|
|
page.current = 1
|
2026-09-03 18:13:59 +08:00
|
|
|
|
loadRecords()
|
2026-09-03 14:20:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
function resetFilters() {
|
|
|
|
|
|
filters.inboundNo = ''
|
|
|
|
|
|
filters.materialCode = ''
|
|
|
|
|
|
filters.manageMode = ''
|
|
|
|
|
|
filters.keyword = ''
|
|
|
|
|
|
dateRange.value = []
|
|
|
|
|
|
search()
|
|
|
|
|
|
}
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
2026-09-03 18:13:59 +08:00
|
|
|
|
// 导出:按当前查询条件导出全部(后端 /inbound/export 返回 xlsx)
|
2026-09-03 14:20:37 +08:00
|
|
|
|
async function exportCsv() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await exportXlsxFetch('/api/inbound/export', {
|
|
|
|
|
|
inboundNo: filters.inboundNo.trim(),
|
|
|
|
|
|
materialCode: filters.materialCode.trim(),
|
|
|
|
|
|
manageMode: filters.manageMode,
|
|
|
|
|
|
keyword: filters.keyword.trim(),
|
|
|
|
|
|
startDate: dateRange.value?.[0] || '',
|
|
|
|
|
|
endDate: dateRange.value?.[1] || ''
|
|
|
|
|
|
}, '入库管理.xlsx')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
/* 错误提示由请求层统一处理 */
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 18:13:59 +08:00
|
|
|
|
// 入库单明细:点击"明细"用右侧抽屉懒加载 + 分页(不在列表内嵌子表,避免表格套表格)
|
|
|
|
|
|
const detailDrawer = reactive({ visible: false, inboundNo: '', list: [], total: 0, page: 1, size: 50, loading: false, loaded: false })
|
2026-09-03 14:20:37 +08:00
|
|
|
|
async function openDetail(row) {
|
|
|
|
|
|
const no = row.inboundNo
|
2026-09-03 18:13:59 +08:00
|
|
|
|
Object.assign(detailDrawer, { visible: true, inboundNo: no, list: [], total: 0, page: 1, size: 50, loaded: false })
|
|
|
|
|
|
await fetchDetailPage()
|
|
|
|
|
|
}
|
|
|
|
|
|
async function fetchDetailPage() {
|
|
|
|
|
|
const st = detailDrawer
|
2026-09-03 14:20:37 +08:00
|
|
|
|
st.loading = true
|
2026-08-28 15:06:01 +08:00
|
|
|
|
try {
|
2026-09-03 14:20:37 +08:00
|
|
|
|
const d = await request.get('/inbound/details', {
|
2026-09-03 18:13:59 +08:00
|
|
|
|
params: { inboundNo: st.inboundNo, page: st.page, pageSize: st.size }
|
2026-08-28 15:06:01 +08:00
|
|
|
|
})
|
2026-09-03 14:20:37 +08:00
|
|
|
|
st.list = d?.list || []
|
|
|
|
|
|
st.total = d?.total || 0
|
|
|
|
|
|
st.loaded = true
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
/* 拦截器统一提示 */
|
2026-08-28 15:06:01 +08:00
|
|
|
|
} finally {
|
2026-09-03 14:20:37 +08:00
|
|
|
|
st.loading = false
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-03 18:13:59 +08:00
|
|
|
|
function changeDetailPage(p) {
|
|
|
|
|
|
detailDrawer.page = p
|
|
|
|
|
|
fetchDetailPage()
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
|
function typeLabel(m) {
|
|
|
|
|
|
return m === 1 ? '结构件' : m === 2 ? '精密件' : '-'
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 18:13:59 +08:00
|
|
|
|
function onTabChange(tab) {
|
|
|
|
|
|
if (tab === 'batch') { loadDicts() }
|
|
|
|
|
|
else if (tab === 'sn') { loadDicts() }
|
|
|
|
|
|
else if (tab === 'records') { loadDicts(); loadRecords() }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
|
onMounted(() => {
|
2026-09-03 18:13:59 +08:00
|
|
|
|
loadDicts()
|
|
|
|
|
|
// 库存页点入库单号跳转过来 → 自动切到"入库记录"Tab 并带入单号查询(闭环)
|
2026-09-03 14:20:37 +08:00
|
|
|
|
if (route.query.inboundNo) {
|
2026-09-03 18:13:59 +08:00
|
|
|
|
activeTab.value = 'records'
|
2026-09-03 14:20:37 +08:00
|
|
|
|
filters.inboundNo = String(route.query.inboundNo)
|
2026-09-03 18:13:59 +08:00
|
|
|
|
loadRecords()
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-03 14:20:37 +08:00
|
|
|
|
})
|
2026-08-28 15:06:01 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-09-03 18:13:59 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
|
|
|
|
|
<!-- ============ Tab1 结构件入库 ============ -->
|
|
|
|
|
|
<el-tab-pane label="结构件入库" name="batch">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<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 batchMaterials" :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="区域" prop="zoneCode">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="form1.remark" placeholder="备注(可空)" clearable />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<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-divider />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="margin-bottom:8px;color:#909399;font-size:12px">
|
|
|
|
|
|
批量导入(Excel):列头顺序(首行忽略)
|
|
|
|
|
|
<b>物料编码 | 批次号(可空自动生成) | 数量 | 生产日期 | 供应商 | 区域 | 备注</b>。
|
|
|
|
|
|
整表一次性全导入,任一行错误则整体回退(不写入任何数据)。
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</div>
|
2026-09-03 18:13:59 +08:00
|
|
|
|
<el-upload ref="batchUploadRef" :auto-upload="false" accept=".xlsx" :limit="1" drag style="max-width:560px"
|
|
|
|
|
|
:on-exceed="(f) => onExceed(f, 'batch')">
|
|
|
|
|
|
<div class="dropzone">将结构件 .xlsx 拖到此处,或点击选择</div>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<el-button type="primary" :loading="importing" style="margin-top:8px"
|
|
|
|
|
|
@click="importExcel('batch', 'batch')">开始批量导入</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ Tab2 精密件 SN 入库 ============ -->
|
|
|
|
|
|
<el-tab-pane :label="`精密件 SN 入库${snList.length ? `(已扫 ${snList.length})` : ''}`" name="sn">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<el-form label-width="110px" style="max-width:680px">
|
|
|
|
|
|
<el-form-item label="物料" required>
|
|
|
|
|
|
<el-select v-model="form2.materialCode" filterable placeholder="请选择精密件物料(必填)" style="width:100%">
|
|
|
|
|
|
<el-option v-for="m in snMaterials" :key="m.id" :value="m.code"
|
|
|
|
|
|
:label="`${m.code} ${m.name || ''}`" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="区域" required>
|
|
|
|
|
|
<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="扫码录入">
|
|
|
|
|
|
<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>
|
2026-09-03 14:20:37 +08:00
|
|
|
|
</div>
|
2026-09-03 18:13:59 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="form2.remark" type="textarea" :rows="2" placeholder="本批 SN 的统一备注(可空)" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:8px">
|
|
|
|
|
|
已录入 <b>{{ snList.length }}</b> 条 SN
|
2026-09-03 14:20:37 +08:00
|
|
|
|
</div>
|
2026-09-03 18:13:59 +08:00
|
|
|
|
<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-divider />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="margin-bottom:8px;color:#909399;font-size:12px">
|
|
|
|
|
|
批量导入(Excel):列头顺序(首行忽略)
|
|
|
|
|
|
<b>物料编码 | SN | 区域 | 生产日期 | 供应商</b>。
|
|
|
|
|
|
整表一次性全导入,任一行错误则整体回退(不写入任何数据)。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-upload ref="snUploadRef" :auto-upload="false" accept=".xlsx" :limit="1" drag style="max-width:680px"
|
|
|
|
|
|
:on-exceed="(f) => onExceed(f, 'sn')">
|
|
|
|
|
|
<div class="dropzone">将精密件 .xlsx 拖到此处,或点击选择</div>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<el-button type="primary" :loading="importing" style="margin-top:8px"
|
|
|
|
|
|
@click="importExcel('sn', 'sn')">开始批量导入</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ Tab3 入库记录(统一主表 + 抽屉明细) ============ -->
|
|
|
|
|
|
<el-tab-pane label="入库记录" name="records">
|
|
|
|
|
|
<el-card shadow="never" class="mb12">
|
|
|
|
|
|
<el-form inline @submit.prevent="search">
|
|
|
|
|
|
<el-form-item label="入库单号">
|
|
|
|
|
|
<el-input v-model="filters.inboundNo" placeholder="入库单号(模糊)" clearable style="width:180px" @keyup.enter="search" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="物料编码">
|
|
|
|
|
|
<el-input v-model="filters.materialCode" placeholder="物料编码(模糊)" clearable style="width:160px" @keyup.enter="search" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="类型">
|
|
|
|
|
|
<el-select v-model="filters.manageMode" placeholder="全部" clearable style="width:120px" @change="search">
|
|
|
|
|
|
<el-option label="全部" value="" />
|
|
|
|
|
|
<el-option label="结构件" value="1" />
|
|
|
|
|
|
<el-option label="精密件" value="2" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="关键字">
|
|
|
|
|
|
<el-input v-model="filters.keyword" placeholder="物料名称/单号" clearable style="width:180px" @keyup.enter="search" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="入库时间">
|
|
|
|
|
|
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD"
|
|
|
|
|
|
range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" :loading="loadingRec" @click="search">查询</el-button>
|
|
|
|
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button v-if="can('inbound:export')" :disabled="!total" @click="exportCsv">导出</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 入库单列表(一单一行,结构件/精密件同表) -->
|
|
|
|
|
|
<el-card shadow="never" header="入库单(每行一笔入库;点「明细」在右侧抽屉查看该单的行)">
|
|
|
|
|
|
<el-table :data="list" border size="small" v-loading="loadingRec" empty-text="暂无入库数据">
|
|
|
|
|
|
<el-table-column prop="inboundNo" label="入库单号" min-width="160" />
|
|
|
|
|
|
<el-table-column label="类型" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag size="small" :type="row.manageMode === 1 ? 'primary' : row.manageMode === 2 ? 'warning' : 'info'">
|
|
|
|
|
|
{{ typeLabel(row.manageMode) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
|
|
|
|
|
|
<el-table-column prop="materialName" label="物料名称" min-width="140">
|
|
|
|
|
|
<template #default="{ row }">{{ row.materialName || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="数量" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">{{ row.quantity ?? '' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="batchNo" label="批次号" min-width="150">
|
|
|
|
|
|
<template #default="{ row }">{{ row.batchNo || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="zoneCode" label="区域" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">{{ row.zoneCode || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="operator" label="操作人" width="100">
|
|
|
|
|
|
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="入库时间" width="160" align="center">
|
|
|
|
|
|
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="110" align="center" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-button link type="primary" size="small" @click="openDetail(row)">
|
|
|
|
|
|
明细{{ row.detailCount ? '(' + row.detailCount + ')' : '' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pager">
|
|
|
|
|
|
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
|
|
|
|
|
|
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
@current-change="(p) => { page.current = p; loadRecords() }"
|
|
|
|
|
|
@size-change="(s) => { page.size = s; page.current = 1; loadRecords() }" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 批量导入结果(结构件/精密件共用) -->
|
|
|
|
|
|
<el-card v-if="excelResult" shadow="never" class="mb12"
|
|
|
|
|
|
:header="excelResult.failed > 0 ? '导入结果:被拒绝(整体回退,未发生写入)' : '导入结果:成功'">
|
|
|
|
|
|
<el-alert v-if="excelResult.failed > 0" type="error" :closable="false"
|
|
|
|
|
|
:title="`共 ${excelResult.failed} 行存在错误,已整体回退(未写入任何数据),请修正后重传`" />
|
|
|
|
|
|
<el-alert v-else type="success" :closable="false" :title="`批量导入成功:共 ${excelResult.success} 行`" />
|
|
|
|
|
|
<el-table v-if="excelResult.failed > 0" :data="excelResult.errors || []" size="small" border
|
|
|
|
|
|
max-height="320" style="margin-top:10px">
|
|
|
|
|
|
<el-table-column prop="row" label="行号" width="70" align="center" />
|
|
|
|
|
|
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
|
|
|
|
|
|
<el-table-column prop="message" label="错误原因" min-width="260" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 入库单明细抽屉:懒加载 + 分页,替代表格套表格 -->
|
|
|
|
|
|
<el-drawer v-model="detailDrawer.visible" :title="`入库单明细:${detailDrawer.inboundNo}`" size="540px">
|
|
|
|
|
|
<el-table :data="detailDrawer.list" border size="small" v-loading="detailDrawer.loading"
|
|
|
|
|
|
empty-text="暂无明细">
|
|
|
|
|
|
<el-table-column label="批次号" min-width="180">
|
|
|
|
|
|
<template #default="{ row }">{{ row.batchNo || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="SN 序列号" min-width="190">
|
|
|
|
|
|
<template #default="{ row }">{{ row.snCode || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="quantity" label="数量" width="90" align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div v-if="detailDrawer.total > detailDrawer.size" class="pager" style="margin-top:12px">
|
|
|
|
|
|
<el-pagination background layout="total, prev, pager, next"
|
|
|
|
|
|
:total="detailDrawer.total" :current-page="detailDrawer.page"
|
|
|
|
|
|
:page-size="detailDrawer.size"
|
|
|
|
|
|
@current-change="changeDetailPage" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<PageHelp :data="helpInbound" page="Inbound" />
|
2026-08-28 15:06:01 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-03 18:13:59 +08:00
|
|
|
|
.dropzone { padding: 30px 0; color: #606266; }
|
2026-09-03 14:20:37 +08:00
|
|
|
|
.mb12 { margin-bottom: 12px; }
|
|
|
|
|
|
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
2026-09-03 18:13:59 +08:00
|
|
|
|
</style>
|