- 重构半成品流转逻辑:进度权威源改为 workpiece.currentStationNo,新增 completedText 快照字段,废除 doneProcessCodes - 清理物料品类中半成品类型,存量数据幂等清理,调整物料管理方式文案为批次/序列号 - 新增日排产状态专用更新接口,修复工单工艺校验逻辑 - 新增物料档案代理接口、深路径文件下载接口 - 优化前端界面文案与工位选择逻辑,补充追溯页半成品流转展示 - 调整WMS端物料品类校验与入库接口契约 - 新增/更新数据库表结构与实体类代码
1006 lines
56 KiB
Vue
1006 lines
56 KiB
Vue
<script setup>
|
||
defineOptions({ name: 'BaseData' })
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import request from '../utils/request'
|
||
import { fmtTime } from '../utils/format'
|
||
import { can } from '../utils/perm'
|
||
import { confirmAction } from '../utils/confirm'
|
||
import { exportXlsxFetch } from '../utils/export'
|
||
import { getToken } from '../utils/auth'
|
||
import PageHelp from '../components/PageHelp.vue'
|
||
import AttachmentPanel from '../components/AttachmentPanel.vue'
|
||
import { helpZone, helpMaterial } from '../help'
|
||
|
||
// mode: 'zone' = 区域维护;'material' = 物料档案(同一组件拆成两个独立菜单)
|
||
const props = defineProps({
|
||
mode: { type: String, default: 'zone' }
|
||
})
|
||
|
||
// 帮助文档按菜单各自独立
|
||
const helpData = computed(() => (props.mode === 'material' ? helpMaterial : helpZone))
|
||
|
||
/* ---------- 区域维护(库位四级:区域/货架/层/位置号 4 个独立 Tab) ---------- */
|
||
const zoneLevels = [
|
||
{ level: 1, name: '区域', codeLabel: '区域编码', nameLabel: '区域名称' },
|
||
{ level: 2, name: '货架', codeLabel: '货架号', nameLabel: '货架名称' },
|
||
{ level: 3, name: '层', codeLabel: '层号', nameLabel: '层名称' },
|
||
{ level: 4, name: '位置号', codeLabel: '位置号', nameLabel: '位置名称' }
|
||
]
|
||
const activeZoneTab = ref(1)
|
||
const zoneTabLoaded = reactive({ 1: false, 2: false, 3: false, 4: false })
|
||
|
||
function makeNodeState() {
|
||
return reactive({
|
||
list: [], total: 0, loading: false,
|
||
page: { current: 1, size: 10 },
|
||
filter: { zoneCode: '', shelfNo: '', layerNo: '', code: '', name: '', status: '' }
|
||
})
|
||
}
|
||
const nodeState = { 1: makeNodeState(), 2: makeNodeState(), 3: makeNodeState(), 4: makeNodeState() }
|
||
|
||
// 父级下拉选项:区域选项全局共享;货架/层选项按 Tab 各自独立,避免联动互相污染
|
||
const regionOptions = ref([])
|
||
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 bLayer = ref([])
|
||
const lbShelf = ref([])
|
||
|
||
async function picker(level, params) {
|
||
const data = await request.get('/zone/picker', { params: { level, ...params } })
|
||
return data || []
|
||
}
|
||
async function loadRegions() {
|
||
regionOptions.value = await picker(1, {})
|
||
}
|
||
async function loadShelfOpts(target, zoneCode) {
|
||
target.value = zoneCode ? await picker(2, { zoneCode }) : []
|
||
}
|
||
async function loadLayerOpts(target, zoneCode, shelfNo) {
|
||
target.value = (zoneCode && shelfNo) ? await picker(3, { zoneCode, shelfNo }) : []
|
||
}
|
||
|
||
// 下拉框展开即刷新上级选项(方案2):新建区域/货架后无需切页或重进菜单即可选到最新数据。
|
||
function onDropdownOpen(visible, loader) { if (visible) loader() }
|
||
|
||
async function loadNodes(level) {
|
||
const s = nodeState[level]
|
||
s.loading = true
|
||
try {
|
||
const params = {
|
||
level,
|
||
page: s.page.current,
|
||
pageSize: s.page.size,
|
||
code: (s.filter.code || '').trim(),
|
||
name: (s.filter.name || '').trim(),
|
||
status: s.filter.status
|
||
}
|
||
if (level >= 2) params.zoneCode = (s.filter.zoneCode || '').trim().toUpperCase()
|
||
if (level >= 3) params.shelfNo = (s.filter.shelfNo || '').trim()
|
||
if (level >= 4) params.layerNo = (s.filter.layerNo || '').trim()
|
||
const data = await request.get('/zone/list', { params })
|
||
s.list = data?.list || []
|
||
s.total = data?.total || 0
|
||
} finally {
|
||
s.loading = false
|
||
}
|
||
}
|
||
|
||
// 进入某 Tab:首次加载列表,并确保上级下拉选项就绪
|
||
async function onZoneTabChange(level) {
|
||
activeZoneTab.value = level
|
||
// 切 tab 刷新区域下拉(方案1):在「区域」Tab 新建后切到货架/层/位置号,区域下拉立即是最新。
|
||
await loadRegions()
|
||
const f = nodeState[level].filter
|
||
if (level >= 2 && f.zoneCode) await loadShelfOpts(level === 2 ? f2Shelf : level === 3 ? f3Shelf : f4Shelf, f.zoneCode)
|
||
if ((level === 3 || level === 4) && f.zoneCode && f.shelfNo) {
|
||
await loadLayerOpts(level === 3 ? f3Layer : f4Layer, f.zoneCode, f.shelfNo)
|
||
}
|
||
if (!zoneTabLoaded[level]) {
|
||
await loadNodes(level)
|
||
zoneTabLoaded[level] = true
|
||
}
|
||
}
|
||
|
||
function searchNodes(level) {
|
||
nodeState[level].page.current = 1
|
||
loadNodes(level)
|
||
}
|
||
|
||
// 筛选区:区域/货架/层 联动
|
||
async function onFilterRegion(level) {
|
||
const zc = nodeState[level].filter.zoneCode
|
||
nodeState[level].filter.shelfNo = ''
|
||
nodeState[level].filter.layerNo = ''
|
||
const t = level === 2 ? f2Shelf : level === 3 ? f3Shelf : f4Shelf
|
||
await loadShelfOpts(t, zc)
|
||
if (level === 4) f4Layer.value = []
|
||
}
|
||
async function onFilterShelf(level) {
|
||
const f = nodeState[level].filter
|
||
f.layerNo = ''
|
||
const t = level === 3 ? f3Layer : f4Layer
|
||
await loadLayerOpts(t, f.zoneCode, f.shelfNo)
|
||
}
|
||
|
||
/* ---------- 区域维护:新增/编辑弹窗(按层级复用同一弹窗) ---------- */
|
||
const nodeDialog = ref(false)
|
||
const nodeEditing = ref(false)
|
||
const nodeForm = reactive({
|
||
level: 1, id: 0, code: '', name: '', remark: '', status: '启用',
|
||
regionCode: '', shelfCode: '', layerCode: ''
|
||
})
|
||
const nodeSaving = ref(false)
|
||
|
||
function openNodeCreate(level) {
|
||
nodeEditing.value = false
|
||
cShelf.value = []; cLayer.value = []
|
||
Object.assign(nodeForm, { level, id: 0, code: '', name: '', remark: '', status: '启用', regionCode: '', shelfCode: '', layerCode: '' })
|
||
nodeDialog.value = true
|
||
}
|
||
async function openNodeEdit(row) {
|
||
nodeEditing.value = true
|
||
const f = { level: row.level, id: row.id, code: row.code, name: row.name || row.zoneName || '', remark: row.description || '', status: row.status || '启用', regionCode: '', shelfCode: '', layerCode: '' }
|
||
if (row.level === 2) f.regionCode = row.zoneCode
|
||
if (row.level === 3) { f.regionCode = row.zoneCode; f.shelfCode = row.shelfNo }
|
||
if (row.level === 4) { f.regionCode = row.zoneCode; f.shelfCode = row.shelfNo; f.layerCode = row.layerNo }
|
||
Object.assign(nodeForm, f)
|
||
// 编辑态必须预加载上级下拉选项:否则「所属货架 / 所属层」无选项、且当前值无法回显(用户报的编辑无下拉数据)
|
||
if (row.level >= 3) await loadShelfOpts(cShelf, f.regionCode)
|
||
if (row.level === 4) await loadLayerOpts(cLayer, f.regionCode, f.shelfCode)
|
||
nodeDialog.value = true
|
||
}
|
||
async function onNodeRegionChange() {
|
||
nodeForm.shelfCode = ''
|
||
nodeForm.layerCode = ''
|
||
await loadShelfOpts(cShelf, nodeForm.regionCode)
|
||
}
|
||
async function onNodeShelfChange() {
|
||
nodeForm.layerCode = ''
|
||
await loadLayerOpts(cLayer, nodeForm.regionCode, nodeForm.shelfCode)
|
||
}
|
||
|
||
async function saveNode() {
|
||
if (!nodeForm.code) { ElMessage.warning('编码必填'); return }
|
||
if (nodeForm.level === 1 && !nodeForm.name) { ElMessage.warning('区域名称必填'); return }
|
||
// 父级必须不能为空(逐级必选):货架必选区域;层必选区域+货架;位置号必选区域+货架+层
|
||
if (nodeForm.level >= 2 && !nodeForm.regionCode) { ElMessage.warning('请选择所属区域'); return }
|
||
if (nodeForm.level >= 3 && !nodeForm.shelfCode) { ElMessage.warning('请选择所属货架'); return }
|
||
if (nodeForm.level === 4 && !nodeForm.layerCode) { ElMessage.warning('请选择所属层'); return }
|
||
const okSave = await confirmAction(
|
||
nodeEditing.value ? `确认保存【${nodeForm.code}】的修改?` : `确认新增【${nodeForm.code}${nodeForm.name ? ' ' + nodeForm.name : ''}】?`,
|
||
nodeEditing.value ? '编辑确认' : '新增确认'
|
||
)
|
||
if (!okSave) return
|
||
nodeSaving.value = true
|
||
try {
|
||
if (nodeEditing.value) {
|
||
await request.post('/zone/node/update', { id: nodeForm.id, name: nodeForm.name, remark: nodeForm.remark, status: nodeForm.status })
|
||
ElMessage.success('修改成功')
|
||
} else {
|
||
await request.post('/zone/node/create', {
|
||
level: nodeForm.level, code: nodeForm.code, name: nodeForm.name,
|
||
zoneCode: nodeForm.regionCode, shelfNo: nodeForm.shelfCode, layerNo: nodeForm.layerCode,
|
||
remark: nodeForm.remark
|
||
})
|
||
ElMessage.success('新增成功')
|
||
}
|
||
nodeDialog.value = false
|
||
zoneTabLoaded[nodeForm.level] = false
|
||
await loadNodes(nodeForm.level)
|
||
// 新建/修改区域后立刻刷新区域下拉,保证本页及其他 Tab 马上能选到新区域。
|
||
if (nodeForm.level === 1) await loadRegions()
|
||
} finally {
|
||
nodeSaving.value = false
|
||
}
|
||
}
|
||
|
||
/* ---------- 区域维护:位置号批量生成 ---------- */
|
||
const batchDialog = ref(false)
|
||
const batchForm = reactive({ zoneCode: '', shelfNo: '', layerNo: '', posStart: 1, posEnd: 1 })
|
||
const batchPreview = ref([])
|
||
const batchPreviewing = ref(false)
|
||
const batchSubmitting = ref(false)
|
||
|
||
function openBatchGen() {
|
||
Object.assign(batchForm, { zoneCode: '', shelfNo: '', layerNo: '', posStart: 1, posEnd: 1 })
|
||
bShelf.value = []; bLayer.value = []
|
||
batchPreview.value = []
|
||
batchDialog.value = true
|
||
}
|
||
async function onBatchRegionChange() {
|
||
batchForm.shelfNo = ''
|
||
batchForm.layerNo = ''
|
||
bLayer.value = []
|
||
await loadShelfOpts(bShelf, batchForm.zoneCode)
|
||
}
|
||
// 选定货架后加载该货架下已存在的层(只能选具体层,批量生成只生成位置、不创建父级层)
|
||
async function onBatchShelfChange() {
|
||
batchForm.layerNo = ''
|
||
await loadLayerOpts(bLayer, batchForm.zoneCode, batchForm.shelfNo)
|
||
}
|
||
async function previewBatch() {
|
||
if (!batchForm.zoneCode || !batchForm.shelfNo) { ElMessage.warning('请选择区域与货架'); return }
|
||
if (!batchForm.layerNo) { ElMessage.warning('请选择具体的层'); return }
|
||
batchPreviewing.value = true
|
||
try {
|
||
const data = await request.post('/zone/batch-generate', { ...batchForm, preview: true })
|
||
batchPreview.value = data?.list || []
|
||
} finally {
|
||
batchPreviewing.value = false
|
||
}
|
||
}
|
||
async function submitBatch() {
|
||
const go = await confirmAction(`已预检 ${batchPreview.value.length} 个位置,确认批量生成到库位表?(已存在的自动跳过)`, '批量生成确认')
|
||
if (!go) return
|
||
batchSubmitting.value = true
|
||
try {
|
||
const data = await request.post('/zone/batch-generate', { ...batchForm, preview: false })
|
||
ElMessage.success(`已生成 ${data?.created || 0} 个位置,跳过已存在 ${data?.skipped || 0} 个`)
|
||
batchDialog.value = false
|
||
zoneTabLoaded[4] = false
|
||
if (activeZoneTab.value === 4) await loadNodes(4)
|
||
} finally {
|
||
batchSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
/* ---------- 区域维护:层批量生成 ---------- */
|
||
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)
|
||
const materialLoading = ref(false)
|
||
const matFilter = reactive({
|
||
materialCode: '', name: '', spec: '', manageMode: '', itemType: '', startDate: '', endDate: ''
|
||
})
|
||
const matPage = reactive({ current: 1, size: 10 })
|
||
|
||
const matDialog = ref(false)
|
||
const mEditingId = ref(0)
|
||
const mForm = reactive({
|
||
code: '', name: '', spec: '', unit: '',
|
||
manageMode: 1, templateCode: '', description: '',
|
||
itemType: 1, safetyStock: 0, isActive: true
|
||
})
|
||
const mSaving = ref(false)
|
||
|
||
async function loadMaterials() {
|
||
materialLoading.value = true
|
||
try {
|
||
const data = await request.get('/material/query', {
|
||
params: {
|
||
materialCode: matFilter.materialCode.trim(),
|
||
name: matFilter.name.trim(),
|
||
spec: matFilter.spec.trim(),
|
||
manageMode: matFilter.manageMode,
|
||
itemType: matFilter.itemType,
|
||
startDate: matFilter.startDate,
|
||
endDate: matFilter.endDate,
|
||
page: matPage.current,
|
||
pageSize: matPage.size
|
||
}
|
||
})
|
||
materials.value = data?.list || []
|
||
materialTotal.value = data?.total || 0
|
||
} finally {
|
||
materialLoading.value = false
|
||
}
|
||
}
|
||
|
||
function searchMaterials() {
|
||
matPage.current = 1
|
||
loadMaterials()
|
||
}
|
||
|
||
function resetMForm() {
|
||
mEditingId.value = 0
|
||
Object.assign(mForm, {
|
||
code: '', name: '', spec: '', unit: '',
|
||
manageMode: 1, templateCode: '', description: '',
|
||
itemType: 1, safetyStock: 0, isActive: true
|
||
})
|
||
}
|
||
|
||
function openMatCreate() {
|
||
resetMForm()
|
||
matDialog.value = true
|
||
}
|
||
|
||
function openMatEdit(row) {
|
||
mEditingId.value = row.id
|
||
Object.assign(mForm, {
|
||
code: row.code, name: row.name,
|
||
spec: row.spec || '', unit: row.unit || '', manageMode: row.manageMode,
|
||
templateCode: row.templateCode || '',
|
||
itemType: row.itemType || 1,
|
||
safetyStock: row.safetyStock || 0,
|
||
description: row.description || '',
|
||
isActive: row.isActive !== false
|
||
})
|
||
matDialog.value = true
|
||
}
|
||
|
||
async function saveMaterial() {
|
||
// 必填口径(2026-09-19 用户裁决):除「说明」外全部必填。
|
||
// 图号只在新增时必填(编辑不可改);「其他」类不做 SN,类型由后端收敛。
|
||
if (!mEditingId.value && !String(mForm.code || '').trim()) {
|
||
ElMessage.warning('图号为必填项')
|
||
return
|
||
}
|
||
const required = [
|
||
['name', mForm.name, '名称'],
|
||
['spec', mForm.spec, '规格型号'],
|
||
['unit', mForm.unit, '单位']
|
||
]
|
||
for (const [, val, label] of required) {
|
||
if (!String(val || '').trim()) {
|
||
ElMessage.warning(label + '为必填项')
|
||
return
|
||
}
|
||
}
|
||
if (!Number(mForm.itemType)) {
|
||
ElMessage.warning('品类为必填项')
|
||
return
|
||
}
|
||
if (![1, 2, 3].includes(Number(mForm.manageMode))) {
|
||
ElMessage.warning('类型为必填项')
|
||
return
|
||
}
|
||
const okSave = await confirmAction(
|
||
mEditingId.value ? `确认保存物料「${mForm.code}」的修改?` : `确认新增物料「${String(mForm.code || '').trim()}」?保存后可继续上传图纸/附件。`,
|
||
mEditingId.value ? '编辑确认' : '新增确认'
|
||
)
|
||
if (!okSave) return
|
||
mSaving.value = true
|
||
try {
|
||
// 类型(1结构件/2电气件/3其他)与品类相互独立,原样提交
|
||
const manageMode = Number(mForm.manageMode)
|
||
const base = {
|
||
name: String(mForm.name || '').trim(),
|
||
spec: String(mForm.spec || '').trim(),
|
||
unit: String(mForm.unit || '').trim(),
|
||
description: mForm.description,
|
||
itemType: Number(mForm.itemType),
|
||
manageMode,
|
||
safetyStock: Number(mForm.safetyStock) || 0,
|
||
isActive: !!mForm.isActive
|
||
}
|
||
if (mEditingId.value) {
|
||
await request.post('/material/update', { id: mEditingId.value, ...base })
|
||
ElMessage.success('物料修改成功')
|
||
matDialog.value = false
|
||
} else {
|
||
const created = await request.post('/material/create', { code: String(mForm.code || '').trim(), ...base })
|
||
ElMessage.success('物料新增成功,可继续上传图纸/附件')
|
||
// 创建成功后切到编辑态:保持弹窗打开,便于立即上传附件
|
||
mEditingId.value = created?.id || 0
|
||
if (created?.code) mForm.code = created.code
|
||
}
|
||
await loadMaterials()
|
||
} finally {
|
||
mSaving.value = false
|
||
}
|
||
}
|
||
|
||
// 物料附件(bizType=material,bizId=图号):零件图纸 PDF 等
|
||
const attachDrawer = reactive({ visible: false, code: '', name: '' })
|
||
function openMatAttach(row) {
|
||
Object.assign(attachDrawer, { visible: true, code: row.code, name: row.name })
|
||
}
|
||
|
||
// 品类显示(1原材料/3成品/4其他;半成品不是物料品类,防御性保留显示)
|
||
function itemTypeText(t) {
|
||
return t === 3 ? '成品' : t === 4 ? '其他' : '原材料'
|
||
}
|
||
function itemTypeTagType(t) {
|
||
return t === 3 ? 'success' : t === 4 ? 'danger' : 'info'
|
||
}
|
||
|
||
// 区域维护 / 物料档案共用同一组件:切换菜单时复用,watch(mode, immediate) 保证进入即加载
|
||
watch(
|
||
() => props.mode,
|
||
(m) => { if (m === 'material') loadMaterials(); else { loadRegions(); onZoneTabChange(1) } },
|
||
{ immediate: true }
|
||
)
|
||
|
||
/* ---------- 物料档案:导入 / 导出 ---------- */
|
||
const matExporting = ref(false)
|
||
const matImporting = ref(false)
|
||
const matFileRef = ref(null)
|
||
|
||
async function exportMaterials() {
|
||
matExporting.value = true
|
||
try {
|
||
const name = await exportXlsxFetch('/api/material/export', {
|
||
materialCode: matFilter.materialCode.trim(),
|
||
name: matFilter.name.trim(),
|
||
spec: matFilter.spec.trim(),
|
||
manageMode: matFilter.manageMode,
|
||
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))
|
||
} finally {
|
||
matExporting.value = false
|
||
}
|
||
}
|
||
|
||
function pickMaterialFile() {
|
||
matFileRef.value?.click()
|
||
}
|
||
|
||
async function onMaterialFile(e) {
|
||
const file = e.target.files?.[0]
|
||
e.target.value = '' // 允许重复选择同一文件
|
||
if (!file) return
|
||
const go = await confirmAction(`确认导入物料文件「${file.name}」?导入将批量写入物料档案(任一行错误则整体回退)。`, '批量导入确认')
|
||
if (!go) return
|
||
matImporting.value = true
|
||
try {
|
||
const fd = new FormData()
|
||
fd.append('file', file)
|
||
// 注意:token 是纯文本存储,不能用 JSON.parse(会对 JWT 抛 SyntaxError 误报"导入异常")
|
||
const res = await fetch('/api/material/import', {
|
||
method: 'POST',
|
||
headers: { Authorization: 'Bearer ' + getToken() },
|
||
body: fd
|
||
})
|
||
const body = await res.json().catch(() => null)
|
||
const d = body?.data || {}
|
||
if (!res.ok || body?.code !== 0) {
|
||
// 业务/权限错误:优先展示后端中文 message;解析不出 body 时按 HTTP 状态给中文提示
|
||
const httpText = res.status === 401 ? '登录已过期,请重新登录' : `服务器错误(${res.status})`
|
||
ElMessage.error('导入失败:' + (body?.message || httpText))
|
||
return
|
||
}
|
||
if (d.errors && d.errors.length) {
|
||
const lines = d.errors.slice(0, 10)
|
||
.map((x) => `第${x.row}行 ${x.materialCode || ''}:${x.message}`).join(';')
|
||
ElMessage.error('导入失败:' + lines + (d.errors.length > 10 ? ' …' : ''))
|
||
return
|
||
}
|
||
ElMessage.success(`导入成功 ${d.success || 0} 条`)
|
||
await loadMaterials()
|
||
} catch (err) {
|
||
ElMessage.error('导入失败:网络异常或登录已过期,请刷新页面重试')
|
||
} finally {
|
||
matImporting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<el-card shadow="never">
|
||
<!-- 区域维护:库位四级 4 个独立 Tab -->
|
||
<template v-if="mode !== 'material'">
|
||
<el-tabs v-model="activeZoneTab" @tab-change="(n) => onZoneTabChange(Number(n))">
|
||
<!-- 区域 -->
|
||
<el-tab-pane :name="1" label="区域">
|
||
<el-form inline style="margin-bottom:14px" @submit.prevent="searchNodes(1)">
|
||
<el-form-item label="区域编码">
|
||
<el-input v-model="nodeState[1].filter.code" placeholder="区域编码" clearable style="width:140px" @keyup.enter="searchNodes(1)" />
|
||
</el-form-item>
|
||
<el-form-item label="区域名称">
|
||
<el-input v-model="nodeState[1].filter.name" placeholder="区域名称" clearable style="width:140px" @keyup.enter="searchNodes(1)" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="nodeState[1].filter.status" placeholder="全部" clearable style="width:110px" @change="searchNodes(1)">
|
||
<el-option label="启用" value="启用" />
|
||
<el-option label="停用" value="停用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="nodeState[1].loading" @click="searchNodes(1)">查询</el-button>
|
||
<el-button v-if="can('zone:create')" type="success" @click="openNodeCreate(1)">新增区域</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table :data="nodeState[1].list" border size="small" v-loading="nodeState[1].loading" row-key="id" empty-text="暂无区域,请点击【新增区域】">
|
||
<el-table-column prop="code" label="区域编码" min-width="110" />
|
||
<el-table-column label="区域名称" min-width="140">
|
||
<template #default="{ row }">{{ row.zoneName || row.name || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="实时库存" width="100" align="center">
|
||
<template #default="{ row }"><el-tag size="small" :type="row.stockQty > 0 ? 'success' : 'info'">{{ row.stockQty || 0 }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">{{ row.status || '启用' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="description" label="备注" min-width="160" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.description || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" width="155" align="center">
|
||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button v-if="can('zone:edit')" link type="primary" size="small" @click="openNodeEdit(row)">编辑</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pager">
|
||
<el-pagination background layout="total, sizes, prev, pager, next" :total="nodeState[1].total"
|
||
:current-page="nodeState[1].page.current" :page-size="nodeState[1].page.size" :page-sizes="[10, 20, 50, 100]"
|
||
@current-change="(p) => { nodeState[1].page.current = p; loadNodes(1) }"
|
||
@size-change="(s) => { nodeState[1].page.size = s; nodeState[1].page.current = 1; loadNodes(1) }" />
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 货架 -->
|
||
<el-tab-pane :name="2" label="货架">
|
||
<el-form inline style="margin-bottom:14px" @submit.prevent="searchNodes(2)">
|
||
<el-form-item label="区域">
|
||
<el-select v-model="nodeState[2].filter.zoneCode" placeholder="全部区域" clearable filterable style="width:160px" @change="onFilterRegion(2)" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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="货架号">
|
||
<el-input v-model="nodeState[2].filter.code" placeholder="货架号" clearable style="width:120px" @keyup.enter="searchNodes(2)" />
|
||
</el-form-item>
|
||
<el-form-item label="名称">
|
||
<el-input v-model="nodeState[2].filter.name" placeholder="货架名称" clearable style="width:120px" @keyup.enter="searchNodes(2)" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="nodeState[2].filter.status" placeholder="全部" clearable style="width:110px" @change="searchNodes(2)">
|
||
<el-option label="启用" value="启用" />
|
||
<el-option label="停用" value="停用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="nodeState[2].loading" @click="searchNodes(2)">查询</el-button>
|
||
<el-button v-if="can('zone:create')" type="success" @click="openNodeCreate(2)">新增货架</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table :data="nodeState[2].list" border size="small" v-loading="nodeState[2].loading" row-key="id" empty-text="暂无货架">
|
||
<el-table-column label="区域" min-width="130">
|
||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="code" label="货架号" width="110" align="center" />
|
||
<el-table-column label="货架名称" min-width="130"><template #default="{ row }">{{ row.name || '-' }}</template></el-table-column>
|
||
<el-table-column label="实时库存" width="100" align="center"><template #default="{ row }"><el-tag size="small" :type="row.stockQty > 0 ? 'success' : 'info'">{{ row.stockQty || 0 }}</el-tag></template></el-table-column>
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template #default="{ row }"><el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">{{ row.status || '启用' }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ row.description || '-' }}</template></el-table-column>
|
||
<el-table-column label="创建时间" width="155" align="center">
|
||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||
<template #default="{ row }"><el-button v-if="can('zone:edit')" link type="primary" size="small" @click="openNodeEdit(row)">编辑</el-button></template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pager">
|
||
<el-pagination background layout="total, sizes, prev, pager, next" :total="nodeState[2].total"
|
||
:current-page="nodeState[2].page.current" :page-size="nodeState[2].page.size" :page-sizes="[10, 20, 50, 100]"
|
||
@current-change="(p) => { nodeState[2].page.current = p; loadNodes(2) }"
|
||
@size-change="(s) => { nodeState[2].page.size = s; nodeState[2].page.current = 1; loadNodes(2) }" />
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 层 -->
|
||
<el-tab-pane :name="3" label="层">
|
||
<el-form inline style="margin-bottom:14px" @submit.prevent="searchNodes(3)">
|
||
<el-form-item label="区域">
|
||
<el-select v-model="nodeState[3].filter.zoneCode" placeholder="全部区域" clearable filterable style="width:150px" @change="onFilterRegion(3)" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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="货架">
|
||
<el-select v-model="nodeState[3].filter.shelfNo" placeholder="全部货架" clearable filterable style="width:130px" @change="onFilterShelf(3)" @visible-change="(v) => onDropdownOpen(v, () => loadShelfOpts(f3Shelf, nodeState[3].filter.zoneCode))">
|
||
<el-option v-for="s in f3Shelf" :key="s.code" :value="s.code" :label="s.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="层号">
|
||
<el-input v-model="nodeState[3].filter.code" placeholder="层号" clearable style="width:100px" @keyup.enter="searchNodes(3)" />
|
||
</el-form-item>
|
||
<el-form-item label="名称">
|
||
<el-input v-model="nodeState[3].filter.name" placeholder="层名称" clearable style="width:110px" @keyup.enter="searchNodes(3)" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="nodeState[3].filter.status" placeholder="全部" clearable style="width:110px" @change="searchNodes(3)">
|
||
<el-option label="启用" value="启用" />
|
||
<el-option label="停用" value="停用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<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="暂无层">
|
||
<el-table-column label="区域" min-width="120"><template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template></el-table-column>
|
||
<el-table-column label="货架号" width="100" align="center"><template #default="{ row }">{{ row.shelfNo || '-' }}</template></el-table-column>
|
||
<el-table-column prop="code" label="层号" width="90" align="center" />
|
||
<el-table-column label="层名称" min-width="120"><template #default="{ row }">{{ row.name || '-' }}</template></el-table-column>
|
||
<el-table-column label="实时库存" width="100" align="center"><template #default="{ row }"><el-tag size="small" :type="row.stockQty > 0 ? 'success' : 'info'">{{ row.stockQty || 0 }}</el-tag></template></el-table-column>
|
||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">{{ row.status || '启用' }}</el-tag></template></el-table-column>
|
||
<el-table-column label="备注" min-width="130" show-overflow-tooltip><template #default="{ row }">{{ row.description || '-' }}</template></el-table-column>
|
||
<el-table-column label="创建时间" width="155" align="center"><template #default="{ row }">{{ fmtTime(row.createdAt) }}</template></el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right"><template #default="{ row }"><el-button v-if="can('zone:edit')" link type="primary" size="small" @click="openNodeEdit(row)">编辑</el-button></template></el-table-column>
|
||
</el-table>
|
||
<div class="pager">
|
||
<el-pagination background layout="total, sizes, prev, pager, next" :total="nodeState[3].total"
|
||
:current-page="nodeState[3].page.current" :page-size="nodeState[3].page.size" :page-sizes="[10, 20, 50, 100]"
|
||
@current-change="(p) => { nodeState[3].page.current = p; loadNodes(3) }"
|
||
@size-change="(s) => { nodeState[3].page.size = s; nodeState[3].page.current = 1; loadNodes(3) }" />
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 位置号 -->
|
||
<el-tab-pane :name="4" label="位置号">
|
||
<el-form inline style="margin-bottom:14px" @submit.prevent="searchNodes(4)">
|
||
<el-form-item label="区域">
|
||
<el-select v-model="nodeState[4].filter.zoneCode" placeholder="全部区域" clearable filterable style="width:150px" @change="onFilterRegion(4)" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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="货架">
|
||
<el-select v-model="nodeState[4].filter.shelfNo" placeholder="全部货架" clearable filterable style="width:130px" @change="onFilterShelf(4)" @visible-change="(v) => onDropdownOpen(v, () => loadShelfOpts(f4Shelf, nodeState[4].filter.zoneCode))">
|
||
<el-option v-for="s in f4Shelf" :key="s.code" :value="s.code" :label="s.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="层">
|
||
<el-select v-model="nodeState[4].filter.layerNo" placeholder="全部层" clearable filterable style="width:110px" @change="searchNodes(4)" @visible-change="(v) => onDropdownOpen(v, () => loadLayerOpts(f4Layer, nodeState[4].filter.zoneCode, nodeState[4].filter.shelfNo))">
|
||
<el-option v-for="l in f4Layer" :key="l.code" :value="l.code" :label="'层 '+l.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="位置号">
|
||
<el-input v-model="nodeState[4].filter.code" placeholder="位置号" clearable style="width:130px" @keyup.enter="searchNodes(4)" />
|
||
</el-form-item>
|
||
<el-form-item label="名称">
|
||
<el-input v-model="nodeState[4].filter.name" placeholder="位置名称" clearable style="width:130px" @keyup.enter="searchNodes(4)" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="nodeState[4].filter.status" placeholder="全部" clearable style="width:110px" @change="searchNodes(4)">
|
||
<el-option label="启用" value="启用" />
|
||
<el-option label="停用" value="停用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="nodeState[4].loading" @click="searchNodes(4)">查询</el-button>
|
||
<el-button v-if="can('zone:create')" type="success" @click="openNodeCreate(4)">新增位置号</el-button>
|
||
<el-button v-if="can('zone:create')" type="warning" @click="openBatchGen">批量生成</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table :data="nodeState[4].list" border size="small" v-loading="nodeState[4].loading" row-key="id" empty-text="暂无位置号">
|
||
<el-table-column label="区域" min-width="110"><template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template></el-table-column>
|
||
<el-table-column label="货架号" width="90" align="center"><template #default="{ row }">{{ row.shelfNo || '-' }}</template></el-table-column>
|
||
<el-table-column label="层" width="70" align="center"><template #default="{ row }">{{ row.layerNo || '-' }}</template></el-table-column>
|
||
<el-table-column prop="code" label="位置号" width="90" align="center" />
|
||
<el-table-column label="位置名称" min-width="140"><template #default="{ row }">{{ row.name || '-' }}</template></el-table-column>
|
||
<el-table-column label="实时库存" width="100" align="center"><template #default="{ row }"><el-tag size="small" :type="row.stockQty > 0 ? 'success' : 'info'">{{ row.stockQty || 0 }}</el-tag></template></el-table-column>
|
||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status === '停用' ? 'info' : 'success'" size="small">{{ row.status || '启用' }}</el-tag></template></el-table-column>
|
||
<el-table-column label="备注" min-width="120" show-overflow-tooltip><template #default="{ row }">{{ row.description || '-' }}</template></el-table-column>
|
||
<el-table-column label="创建时间" width="155" align="center"><template #default="{ row }">{{ fmtTime(row.createdAt) }}</template></el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right"><template #default="{ row }"><el-button v-if="can('zone:edit')" link type="primary" size="small" @click="openNodeEdit(row)">编辑</el-button></template></el-table-column>
|
||
</el-table>
|
||
<div class="pager">
|
||
<el-pagination background layout="total, sizes, prev, pager, next" :total="nodeState[4].total"
|
||
:current-page="nodeState[4].page.current" :page-size="nodeState[4].page.size" :page-sizes="[10, 20, 50, 100]"
|
||
@current-change="(p) => { nodeState[4].page.current = p; loadNodes(4) }"
|
||
@size-change="(s) => { nodeState[4].page.size = s; nodeState[4].page.current = 1; loadNodes(4) }" />
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 新增/编辑节点弹窗 -->
|
||
<el-dialog v-model="nodeDialog" :title="(nodeEditing ? '编辑' : '新增') + (zoneLevels.find(z=>z.level===nodeForm.level)?.name || '')" width="560px" append-to-body destroy-on-close>
|
||
<el-form label-width="90px">
|
||
<el-form-item v-if="nodeForm.level === 2 || nodeForm.level === 3 || nodeForm.level === 4" label="所属区域" required>
|
||
<el-select v-model="nodeForm.regionCode" placeholder="请选择区域" filterable style="width:100%" @change="onNodeRegionChange" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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 v-if="nodeForm.level === 3 || nodeForm.level === 4" label="所属货架" required>
|
||
<el-select v-model="nodeForm.shelfCode" placeholder="请选择货架" filterable :disabled="!nodeForm.regionCode" style="width:100%" @change="onNodeShelfChange" @visible-change="(v) => onDropdownOpen(v, () => loadShelfOpts(cShelf, nodeForm.regionCode))">
|
||
<el-option v-for="s in cShelf" :key="s.code" :value="s.code" :label="s.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="nodeForm.level === 4" label="所属层" required>
|
||
<el-select v-model="nodeForm.layerCode" placeholder="请选择层" filterable :disabled="!nodeForm.shelfCode" style="width:100%" @visible-change="(v) => onDropdownOpen(v, () => loadLayerOpts(cLayer, nodeForm.regionCode, nodeForm.shelfCode))">
|
||
<el-option v-for="l in cLayer" :key="l.code" :value="l.code" :label="'层 '+l.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="zoneLevels.find(z=>z.level===nodeForm.level)?.codeLabel" required>
|
||
<el-input v-model="nodeForm.code" :placeholder="zoneLevels.find(z=>z.level===nodeForm.level)?.codeLabel" :disabled="nodeEditing && nodeForm.level===1" clearable
|
||
@input="nodeForm.code = (nodeForm.code || '').toUpperCase()" />
|
||
</el-form-item>
|
||
<el-form-item :label="zoneLevels.find(z=>z.level===nodeForm.level)?.nameLabel">
|
||
<el-input v-model="nodeForm.name" :placeholder="(zoneLevels.find(z=>z.level===nodeForm.level)?.nameLabel || '') + '(可空)'" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="nodeForm.remark" type="textarea" :rows="2" placeholder="备注(可空)" />
|
||
</el-form-item>
|
||
<el-form-item v-if="nodeEditing" label="状态">
|
||
<el-select v-model="nodeForm.status" style="width:130px">
|
||
<el-option label="启用" value="启用" />
|
||
<el-option label="停用" value="停用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="nodeDialog = false">取消</el-button>
|
||
<el-button type="primary" :loading="nodeSaving" @click="saveNode">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 位置号批量生成弹窗 -->
|
||
<el-dialog v-model="batchDialog" title="批量生成位置号" width="720px" append-to-body destroy-on-close>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="区域" required>
|
||
<el-select v-model="batchForm.zoneCode" placeholder="请选择区域" filterable style="width:100%" @change="onBatchRegionChange" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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="batchForm.shelfNo" placeholder="请选择货架" filterable :disabled="!batchForm.zoneCode" style="width:100%" @change="onBatchShelfChange" @visible-change="(v) => onDropdownOpen(v, () => loadShelfOpts(bShelf, batchForm.zoneCode))">
|
||
<el-option v-for="s in bShelf" :key="s.code" :value="s.code" :label="s.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="层" required>
|
||
<el-select v-model="batchForm.layerNo" placeholder="请选择具体的层" filterable :disabled="!batchForm.shelfNo" style="width:100%" @visible-change="(v) => onDropdownOpen(v, () => loadLayerOpts(bLayer, batchForm.zoneCode, batchForm.shelfNo))">
|
||
<el-option v-for="l in bLayer" :key="l.code" :value="l.code" :label="'层 '+l.code" />
|
||
</el-select>
|
||
<div style="color:#909399;font-size:12px;margin-top:4px">只能选择已存在的层;批量生成只在该层下生成位置号,不会创建父级层。</div>
|
||
</el-form-item>
|
||
<el-form-item label="位置范围">
|
||
<el-input-number v-model="batchForm.posStart" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
|
||
<span style="margin:0 8px">至</span>
|
||
<el-input-number v-model="batchForm.posEnd" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="batchPreviewing" @click="previewBatch">预览</el-button>
|
||
<el-button type="success" :loading="batchSubmitting" :disabled="!batchPreview.length" @click="submitBatch">确认生成</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table v-if="batchPreview.length" :data="batchPreview" border size="small" max-height="300">
|
||
<el-table-column prop="layerNo" label="层" width="80" align="center" />
|
||
<el-table-column prop="positionNo" label="位置号" width="100" align="center" />
|
||
<el-table-column prop="positionName" 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="batchPreview.length" style="color:#909399;font-size:12px;margin-top:6px">
|
||
共 {{ 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" @visible-change="(v) => onDropdownOpen(v, loadRegions)">
|
||
<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%" @visible-change="(v) => onDropdownOpen(v, () => loadShelfOpts(lbShelf, layerBatchForm.zoneCode))">
|
||
<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>
|
||
|
||
<!-- 物料档案 -->
|
||
<template v-else>
|
||
<el-form inline style="margin-bottom:14px" @submit.prevent="searchMaterials">
|
||
<el-form-item label="图号">
|
||
<el-input v-model="matFilter.materialCode" placeholder="图号" clearable style="width:140px"
|
||
@keyup.enter="searchMaterials" />
|
||
</el-form-item>
|
||
<el-form-item label="名称">
|
||
<el-input v-model="matFilter.name" placeholder="物料名称" clearable style="width:140px"
|
||
@keyup.enter="searchMaterials" />
|
||
</el-form-item>
|
||
<el-form-item label="规格">
|
||
<el-input v-model="matFilter.spec" placeholder="规格型号" clearable style="width:130px"
|
||
@keyup.enter="searchMaterials" />
|
||
</el-form-item>
|
||
<el-form-item label="类型">
|
||
<el-select v-model="matFilter.manageMode" placeholder="全部" clearable style="width:150px" @change="searchMaterials">
|
||
<el-option label="批次" :value="1" />
|
||
<el-option label="序列号" :value="2" />
|
||
<el-option label="其他(辅料/工装/试验设备)" :value="3" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="品类">
|
||
<el-select v-model="matFilter.itemType" placeholder="全部" clearable style="width:120px" @change="searchMaterials">
|
||
<el-option label="原材料" :value="1" />
|
||
<el-option label="成品" :value="3" />
|
||
<el-option label="其他" :value="4" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker v-model="matFilter.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始"
|
||
style="width:130px" @change="searchMaterials" />
|
||
<span style="margin:0 6px">至</span>
|
||
<el-date-picker v-model="matFilter.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束"
|
||
style="width:130px" @change="searchMaterials" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="materialLoading" @click="searchMaterials">查询</el-button>
|
||
<el-button v-if="can('material:create')" type="success" @click="openMatCreate">新增物料</el-button>
|
||
<el-button v-if="can('material:import')" type="warning" :loading="matImporting" @click="pickMaterialFile">导入</el-button>
|
||
<el-button v-if="can('material:export')" :loading="matExporting" @click="exportMaterials">导出</el-button>
|
||
<el-tooltip v-if="can('material:import')" content="Excel 首行表头按名称识别(列顺序可变、可含多余列):图号 | 名称 | 规格 | 单位 | 类型(结构件/电气件/其他) | 品类(原材料/成品/其他,可选;半成品不是物料品类) | 说明(除「说明」「品类」外均为必填)">
|
||
<el-link type="info" :underline="false" style="margin-left:4px">导入说明</el-link>
|
||
</el-tooltip>
|
||
<input ref="matFileRef" type="file" accept=".xlsx,.xls" style="display:none" @change="onMaterialFile" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table :data="materials" border size="small" v-loading="materialLoading"
|
||
empty-text="暂无物料,请点击【新增物料】">
|
||
<el-table-column label="ID" prop="id" width="60" align="center" />
|
||
<el-table-column prop="code" label="图号" min-width="120" />
|
||
<el-table-column prop="name" label="名称" min-width="120" />
|
||
<el-table-column prop="spec" label="规格" min-width="90">
|
||
<template #default="{ row }">{{ row.spec || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="unit" label="单位" width="70" align="center">
|
||
<template #default="{ row }">{{ row.unit || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="品类" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="itemTypeTagType(row.itemType)" size="small">{{ itemTypeText(row.itemType) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.manageMode === 3 ? 'info' : (row.manageMode === 2 ? 'warning' : 'primary')" size="small">
|
||
{{ row.manageMode === 3 ? '其他' : (row.manageMode === 2 ? '序列号' : '批次') }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="安全库存" width="100" align="center">
|
||
<template #default="{ row }">{{ row.safetyStock || 0 }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="启用" width="80" align="center">
|
||
<template #default="{ row }"><el-tag :type="row.isActive !== false ? 'success' : 'info'" size="small">{{ row.isActive !== false ? '启用' : '停用' }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" width="155" align="center">
|
||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150" align="center">
|
||
<template #default="{ row }">
|
||
<el-button v-if="can('material:edit')" link type="primary" size="small" @click="openMatEdit(row)">编辑</el-button>
|
||
<el-button link type="success" size="small" @click="openMatAttach(row)">附件</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pager">
|
||
<el-pagination background layout="total, sizes, prev, pager, next" :total="materialTotal"
|
||
:current-page="matPage.current" :page-size="matPage.size" :page-sizes="[10, 20, 50, 100]"
|
||
@current-change="(p) => { matPage.current = p; loadMaterials() }"
|
||
@size-change="(s) => { matPage.size = s; matPage.current = 1; loadMaterials() }" />
|
||
</div>
|
||
|
||
<el-dialog v-model="matDialog" :title="mEditingId ? '编辑物料' : '新增物料'" width="560px"
|
||
append-to-body destroy-on-close>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="图号" required>
|
||
<el-input v-model="mForm.code" placeholder="图号,如 SCREW-M3" :disabled="!!mEditingId" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="名称" required>
|
||
<el-input v-model="mForm.name" placeholder="如 螺丝 M3" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="规格" required>
|
||
<el-input v-model="mForm.spec" placeholder="如 M3×8" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="单位" required>
|
||
<el-input v-model="mForm.unit" placeholder="如 个/套" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="品类" required>
|
||
<el-select v-model="mForm.itemType" placeholder="请选择" style="width:100%">
|
||
<el-option :value="1" label="原材料" />
|
||
<el-option :value="3" label="成品" />
|
||
<el-option :value="4" label="其他(辅料/工装/试验设备)" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="类型" required>
|
||
<el-select v-model="mForm.manageMode" placeholder="请选择" style="width:100%">
|
||
<el-option :value="1" label="批次" />
|
||
<el-option :value="2" label="序列号" />
|
||
<el-option :value="3" label="其他(辅料/工装/试验设备)" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="安全库存" required>
|
||
<el-input-number v-model="mForm.safetyStock" :min="0" :step="1" step-strictly style="width:100%" />
|
||
<span style="color:#909399;font-size:12px;margin-left:8px">低于该值触发缺货预警(0=不预警)</span>
|
||
</el-form-item>
|
||
<el-form-item label="说明">
|
||
<el-input v-model="mForm.description" type="textarea" :rows="2" placeholder="备注(可空)" />
|
||
</el-form-item>
|
||
<el-form-item label="启用">
|
||
<el-switch v-model="mForm.isActive" />
|
||
<span style="color:#909399;font-size:12px;margin-left:8px">停用后不在新建入库 / 备料下拉出现</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<AttachmentPanel v-if="mEditingId" biz-type="material" :biz-id="mForm.code" title="附件(零件图纸 / 资料)" />
|
||
<template #footer>
|
||
<el-button @click="matDialog = false">取消</el-button>
|
||
<el-button type="primary" :loading="mSaving" @click="saveMaterial">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
</el-card>
|
||
|
||
<!-- 物料附件抽屉(图纸 PDF 等) -->
|
||
<el-drawer v-model="attachDrawer.visible" :title="`物料附件:${attachDrawer.code} ${attachDrawer.name}`" size="620px">
|
||
<AttachmentPanel biz-type="material" :biz-id="attachDrawer.code" title="附件(零件图纸 / 资料)" />
|
||
</el-drawer>
|
||
<PageHelp :data="helpData" :page="props.mode === 'material' ? 'Material' : 'Zone'" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.pager { display: flex; justify-content: flex-end; margin-top: 10px; }
|
||
</style> |