Files
bj_power/bj_power_wms/frontend/src/pages/BaseData.vue
T
SunYF d3eda6b588 feat&refactor: 2026-09-22 半成品业务模型重构与功能完善
- 重构半成品流转逻辑:进度权威源改为 workpiece.currentStationNo,新增 completedText 快照字段,废除 doneProcessCodes
- 清理物料品类中半成品类型,存量数据幂等清理,调整物料管理方式文案为批次/序列号
- 新增日排产状态专用更新接口,修复工单工艺校验逻辑
- 新增物料档案代理接口、深路径文件下载接口
- 优化前端界面文案与工位选择逻辑,补充追溯页半成品流转展示
- 调整WMS端物料品类校验与入库接口契约
- 新增/更新数据库表结构与实体类代码
2026-09-22 14:42:52 +08:00

1006 lines
56 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: '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=materialbizId=图号):零件图纸 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>