docs(test): 更新电表箱装配业务回归测试文档

- 将文档从代码审计任务转换为端到端业务回归测试链路
- 重新组织文档结构为链路总览、示例数据基线和详细步骤
- 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统
- 定义统一的测试数据包括产品型号、物料清单、工位派工等
- 提供详细的步骤断言和数量等式验证方法
- 建立贯穿全链路的数据流向和状态变更追踪体系
This commit is contained in:
SunYF
2026-09-21 10:48:53 +08:00
parent 0d34bcd6a3
commit 132c3ed6bd
63 changed files with 3309 additions and 5784 deletions
@@ -54,6 +54,16 @@ function preview(row) {
window.open(row.url, '_blank')
}
// 下载:后端 /uploads/{id}?dl=1 返回 Content-Disposition: attachment,用临时 <a> 触发真实下载(绕开弹窗拦截)
function download(row) {
const a = document.createElement('a')
a.href = row.url + '?dl=1'
a.download = row.fileName || ''
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
async function remove(row) {
try {
await ElMessageBox.confirm(`确认删除附件「${row.fileName}」?`, '删除确认', { type: 'warning' })
@@ -91,10 +101,14 @@ defineExpose({ load })
上传附件
</el-button>
</el-upload>
<span class="ap-tip">支持 PDF / 图纸 / 图片 / Office单个不超过 20MB查看新窗口打开</span>
<span class="ap-tip">支持 PDF / 图纸 / 图片 / Office单个不超过 20MB文件名或下载下载查看新窗口预览</span>
</div>
<el-table :data="list" size="small" border v-loading="loading" empty-text="暂无附件">
<el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column label="文件名" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="download(row)">{{ row.fileName }}</el-link>
</template>
</el-table-column>
<el-table-column label="大小" width="100" align="center">
<template #default="{ row }">{{ sizeText(row.fileSize) }}</template>
</el-table-column>
@@ -104,9 +118,10 @@ defineExpose({ load })
<el-table-column label="上传时间" width="165" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" align="center">
<el-table-column label="操作" width="180" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="preview(row)">查看</el-button>
<el-button link type="primary" size="small" @click="download(row)">下载</el-button>
<el-button link type="danger" size="small" :disabled="disabled" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
@@ -70,13 +70,16 @@ function onChange(v) {
}
}
// 展开下拉即重拉最近 100 条(仅在未输入搜索词时):新建/修改物料后无需刷新页面即可选到最新数据。
function onVisible(v) { if (v) load('') }
onMounted(() => load(''))
</script>
<template>
<el-select :model-value="modelValue" :multiple="multiple" filterable remote clearable
:remote-method="load" :loading="loading" :placeholder="placeholder"
:style="{ width }" @change="onChange">
:style="{ width }" @change="onChange" @visible-change="onVisible">
<el-option v-for="m in options" :key="m.id" :value="m.code" :label="optionLabel(m)">
<span style="float:left">{{ m.code }} {{ m.name || '' }}</span>
<span style="float:right;color:#8492a6;font-size:12px;margin-left:16px">
+2 -16
View File
@@ -244,28 +244,14 @@ export const helpZone = {
{ name: '名称(四级)', source: '手工填写(可空)。区域/货架/层/位置四级均可命名。', purpose: '本级中文名称,便于识别', fill: '区域如 合格区/待检区;位置如 A架3层12位' },
{ name: '货架号', source: '新增货架时从「所属区域」下拉选,再填货架号。', purpose: '库位第二级,细化存放位置', fill: '如 A、B' },
{ name: '层号', source: '新增层时从「所属区域+货架」下拉选,再填层号。', purpose: '库位第三级', fill: '如 3' },
{ name: '位置号', source: '新增位置号时从「区域+货架+(层)」下拉选,再填位置号;层留空=该货架默认位置。', purpose: '库位第四级,最小存放单位', fill: '如 12' },
{ name: '位置号', source: '新增位置号时从「区域+货架+层」下拉逐级选(层必选,父级不能为空),再填位置号。', purpose: '库位第四级,最小存放单位', fill: '如 12' },
{ name: '实时库存', source: '系统实时聚合(不缓存)。', purpose: '直观看到该区域/货架/层/位置当前在库总量', fill: '只读;区域为全区域合计,逐级向下细化' },
{ name: '状态', source: '启用/停用(新建默认启用)。', purpose: '不删除、只停用;停用后不再进入可选库位下拉', fill: '编辑弹窗切换启用/停用' },
{ name: '批量生成', source: '位置号页签「批量生成」按钮。', purpose: '一次生成一批位置号(货架 × 层范围 × 位置范围)', fill: '选区域+货架+层范围+位置范围 → 预览 → 确认生成;已存在的自动跳过' },
{ name: '批量生成', source: '位置号页签「批量生成」按钮。', purpose: '在选定的某个已存在层下,一次生成一批位置号(位置范围)', fill: '选区域+货架+具体层+位置范围 → 预览 → 确认生成;只生成位置号、不创建父级层,已存在的自动跳过' },
{ name: '备注', source: '手工填写(可空),四级均可。', purpose: '补充描述', fill: '可空' }
]
}
export const helpDock = {
title: '接驳台维护',
overview:
'查看全厂接驳台(AGV 取送货的物理停靠位)。接驳台是系统预置的只读主数据,本页仅供查看,不提供新增/修改/删除。\n\n三种类型:\n· 产线:位于产线工位旁,与工位一一对应(DOCK01~20 对应工位 1~20,一个工位一个接驳台);\n· 库房:库房收货/发货口(如 DOCK21),不绑定工位;\n· 其他:缓存区、暂存位等,不绑定工位。\n\n接驳台编码是产线与 AGV 任务的引用锚点;MES「关联工位」页绑定接驳台时从本页数据实时拉取。',
fields: [
{ name: '接驳台编码', source: '系统预置,只读。', purpose: '接驳台唯一编号,产线/AGV 任务据此定位', fill: '只读查看,如 DOCK01~DOCK21' },
{ name: '接驳台名称', source: '系统预置,只读。', purpose: '接驳台中文名,便于识别', fill: '只读查看' },
{ name: '类型', source: '系统预置:产线 / 库房 / 其他。', purpose: '区分接驳台所处位置与用途', fill: '只读;产线接驳台绑定工位号,库房/其他不绑定' },
{ name: '绑定工位号', source: '仅产线类型有,与工位 1:1。', purpose: '该接驳台服务哪个工位', fill: '只读,如 3 表示 3 号工位' },
{ name: '托盘状态', source: '系统自动维护(PLC 托盘传感器 / AGV 到位卸货)。', purpose: '看该接驳台当前是否被占用', fill: '只读,实时反映是否有托盘占用' },
{ name: '状态', source: '系统预置:启用 / 停用。', purpose: '停用的接驳台不再参与配送与下拉选择', fill: '只读查看' }
]
}
export const helpAttachment = {
title: '附件中心',
overview:
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
HomeFilled, Download, Upload, Search, CircleCheck, List,
Box, Tickets, Monitor, EditPen, Fold, Expand, Location, Goods, Key, Operation, Van, Clock, RefreshLeft, Aim, Folder
Box, Tickets, Monitor, EditPen, Fold, Expand, Location, Goods, Key, Operation, Van, Clock, RefreshLeft, Folder
} from '@element-plus/icons-vue'
import { getUser, getToken, logout, setSession } from '../utils/auth'
import request from '../utils/request'
@@ -93,8 +93,7 @@ const menuGroups = [
{
title: '基础资料', icon: Goods, children: [
{ path: '/material', title: '物料档案', icon: Goods, code: 'material:view' },
{ path: '/zone', title: '区域维护', icon: Location, code: 'zone:view' },
{ path: '/dock', title: '接驳台维护', icon: Aim, code: 'dock:manage' }
{ path: '/zone', title: '区域维护', icon: Location, code: 'zone:view' }
]
},
{
+55 -49
View File
@@ -20,16 +20,6 @@ const props = defineProps({
// 帮助文档按菜单各自独立
const helpData = computed(() => (props.mode === 'material' ? helpMaterial : helpZone))
// 默认近3个月起始日期
function defaultStart() {
const d = new Date()
d.setMonth(d.getMonth() - 3)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
/* ---------- 区域维护(库位四级:区域/货架/层/位置号 4 个独立 Tab ---------- */
const zoneLevels = [
{ level: 1, name: '区域', codeLabel: '区域编码', nameLabel: '区域名称' },
@@ -54,7 +44,7 @@ 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 bShelf = ref([]); const bLayer = ref([])
const lbShelf = ref([])
async function picker(level, params) {
@@ -71,6 +61,9 @@ 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
@@ -97,6 +90,8 @@ async function loadNodes(level) {
// 进入某 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) {
@@ -140,24 +135,22 @@ 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
}
function openNodeEdit(row) {
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
}
function nodeParentCode() {
if (nodeForm.level === 1) return ''
if (nodeForm.level === 2) return nodeForm.regionCode
if (nodeForm.level === 3) return nodeForm.shelfCode
return nodeForm.layerCode || nodeForm.shelfCode
}
async function onNodeRegionChange() {
nodeForm.shelfCode = ''
nodeForm.layerCode = ''
@@ -171,9 +164,10 @@ async function onNodeShelfChange() {
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.shelfCode) { 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 ? '编辑确认' : '新增确认'
@@ -187,13 +181,16 @@ async function saveNode() {
} else {
await request.post('/zone/node/create', {
level: nodeForm.level, code: nodeForm.code, name: nodeForm.name,
parentCode: nodeParentCode(), remark: nodeForm.remark
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
}
@@ -201,22 +198,31 @@ async function saveNode() {
/* ---------- 区域维护:位置号批量生成 ---------- */
const batchDialog = ref(false)
const batchForm = reactive({ zoneCode: '', shelfNo: '', layerStart: 1, layerEnd: 1, posStart: 1, posEnd: 1 })
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: '', layerStart: 1, layerEnd: 1, posStart: 1, posEnd: 1 })
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 })
@@ -286,7 +292,7 @@ const materials = ref([])
const materialTotal = ref(0)
const materialLoading = ref(false)
const matFilter = reactive({
materialCode: '', name: '', spec: '', manageMode: '', itemType: '', startDate: defaultStart(), endDate: ''
materialCode: '', name: '', spec: '', manageMode: '', itemType: '', startDate: '', endDate: ''
})
const matPage = reactive({ current: 1, size: 10 })
@@ -573,7 +579,7 @@ async function onMaterialFile(e) {
<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)">
<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>
@@ -624,12 +630,12 @@ async function onMaterialFile(e) {
<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)">
<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)">
<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>
@@ -674,17 +680,17 @@ async function onMaterialFile(e) {
<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)">
<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)">
<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)">
<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>
@@ -730,25 +736,24 @@ async function onMaterialFile(e) {
<!-- 新增/编辑节点弹窗 -->
<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 :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 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">
<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">
<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="所属层">
<el-select v-model="nodeForm.layerCode" placeholder="选填留空=货架默认位置" filterable :disabled="!nodeForm.shelfCode" style="width:100%">
<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>
<div style="color:#909399;font-size:12px;margin-top:4px">留空则作为该货架的默认位置(无层)</div>
</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 />
@@ -773,19 +778,20 @@ async function onMaterialFile(e) {
<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">
<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%">
<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="范围">
<el-input-number v-model="batchForm.layerStart" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
<span style="margin:0 8px">至</span>
<el-input-number v-model="batchForm.layerEnd" :min="0" :step="1" step-strictly controls-position="right" style="width:120px" />
<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" />
@@ -814,12 +820,12 @@ async function onMaterialFile(e) {
<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">
<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%">
<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>
@@ -887,7 +893,7 @@ async function onMaterialFile(e) {
<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-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" />
-126
View File
@@ -1,126 +0,0 @@
<template>
<div class="dock-page">
<el-card shadow="never">
<el-alert type="info" :closable="false" show-icon style="margin-bottom:12px"
title="接驳台是产线与库房之间供 AGV 取送货的物理停靠位,为系统预置的只读主数据,本页仅供查看。类型分 产线 / 库房 / 其他:产线接驳台与工位一一对应(DOCK01~20 对应工位 1~20),库房接驳台为收货/发货口(DOCK21),其他为缓存/暂存位;只有产线接驳台绑定工位号。" />
<!-- 列表页禁止关键字混搜编码精确名称模糊类型/状态各自独立下拉需求5N -->
<el-form inline :model="filter">
<el-form-item label="接驳台编码">
<el-input v-model="filter.dockCode" clearable style="width:160px" placeholder="精确匹配,如 DOCK01" @keyup.enter="search" />
</el-form-item>
<el-form-item label="接驳台名称">
<el-input v-model="filter.name" clearable style="width:180px" placeholder="名称模糊匹配" @keyup.enter="search" />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="filter.dockType" clearable style="width:120px" placeholder="全部">
<el-option label="产线" value="line" />
<el-option label="库房" value="store" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filter.status" style="width:110px" placeholder="全部">
<el-option label="全部" value="" />
<el-option label="启用" value="ENABLED" />
<el-option label="停用" value="DISABLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small" v-loading="loading">
<el-table-column prop="dockCode" label="接驳台编码" width="140" />
<el-table-column prop="name" label="接驳台名称" min-width="200" show-overflow-tooltip />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="tagType(row.dockType)">{{ typeLabel(row.dockType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定工位" width="110" align="center">
<template #default="{ row }">{{ row.stationNo > 0 ? '工位' + row.stationNo : '—' }}</template>
</el-table-column>
<!-- 占用状态现场调度参考空闲 / 有托盘 -->
<el-table-column label="占用状态" width="110" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.hasPallet ? 'warning' : 'info'">{{ row.hasPallet ? '有托盘' : '空闲' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="palletRef" label="托盘关联" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.palletRef || '—' }}</template>
</el-table-column>
<el-table-column label="启用状态" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === 'ENABLED' ? 'success' : 'info'">
{{ row.status === 'ENABLED' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<template #empty>
<el-empty description="暂无接驳台数据" />
</template>
</el-table>
<el-pagination v-model:current-page="page.current" v-model:page-size="page.size" :page-sizes="[20, 50, 100]"
:total="total" background layout="total, sizes, prev, pager, next, jumper" style="margin-top:12px"
@size-change="search" @current-change="load" />
</el-card>
</div>
<PageHelp :data="helpDock" page="Dock" />
</template>
<script setup>
defineOptions({ name: 'Dock' })
import { reactive, ref, onMounted } from 'vue'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpDock } from '../help'
const list = ref([])
const total = ref(0)
const loading = ref(false)
const page = reactive({ current: 1, size: 20 })
// 默认只看启用;停用需主动选择(需求7 一致的默认口径)
const filter = reactive({ dockCode: '', name: '', dockType: '', status: 'ENABLED' })
const TYPE_LABEL = { line: '产线', store: '库房', other: '其他' }
function typeLabel(t) { return TYPE_LABEL[t] || '其他' }
function tagType(t) { return t === 'line' ? 'success' : (t === 'store' ? 'warning' : 'info') }
async function load() {
loading.value = true
try {
const params = { page: page.current, pageSize: page.size }
if (filter.dockCode.trim()) params.dockCode = filter.dockCode.trim()
if (filter.name.trim()) params.name = filter.name.trim()
if (filter.dockType) params.dockType = filter.dockType
if (filter.status) params.status = filter.status
const data = await request.get('/docks', { params })
list.value = data?.list || []
total.value = data?.total || 0
} finally {
loading.value = false
}
}
function search() {
page.current = 1
load()
}
function reset() {
filter.dockCode = ''
filter.name = ''
filter.dockType = ''
filter.status = 'ENABLED'
search()
}
onMounted(load)
</script>
+6 -6
View File
@@ -617,12 +617,12 @@ onActivated(() => {
<el-input-number v-model="form1.quantity" :min="1" :step="1" step-strictly style="width:100%" />
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<el-select v-model="form1.zoneCode" placeholder="请选择入库区域(必填)" clearable style="width:100%" @change="onZoneChange(form1)">
<el-select v-model="form1.zoneCode" placeholder="请选择入库区域(必填)" clearable style="width:100%" @change="onZoneChange(form1)" @visible-change="(v) => v && loadDicts(true)">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / 层 / 位置">
<el-form-item label="库位">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form1.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form1)">
<el-option v-for="s in zoneShelves(form1.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
@@ -705,12 +705,12 @@ onActivated(() => {
</el-tooltip>
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form2)">
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form2)" @visible-change="(v) => v && loadDicts(true)">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / / 位置">
<el-form-item label="库位">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form2.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form2)">
<el-option v-for="s in zoneShelves(form2.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
@@ -822,12 +822,12 @@ onActivated(() => {
<el-input-number v-model="form3.quantity" :min="1" :step="1" step-strictly style="width:100%" />
</el-form-item>
<el-form-item label="区域" prop="zoneCode">
<el-select v-model="form3.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form3)">
<el-select v-model="form3.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%" @change="onZoneChange(form3)" @visible-change="(v) => v && loadDicts(true)">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / / 位置">
<el-form-item label="库位">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="form3.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange(form3)">
<el-option v-for="s in zoneShelves(form3.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
@@ -318,7 +318,7 @@ onMounted(() => {
</el-form-item>
<template v-if="viewTab === 'detail'">
<el-form-item label="区域">
<el-select v-model="filters.zoneCode" placeholder="全部区域" clearable style="width:140px" @change="search">
<el-select v-model="filters.zoneCode" placeholder="全部区域" clearable style="width:140px" @change="search" @visible-change="(v) => v && loadZones()">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :label="z.zoneName" :value="z.zoneCode" />
</el-select>
</el-form-item>
+6 -2
View File
@@ -158,6 +158,10 @@ async function loadDicts() {
loadStations()
if (zones.value.length) return
// 物料下拉由 MaterialSelect 组件按需远程搜索(最近 100 条),此处只加载区域
await reloadZones()
}
// 区域字典强刷:区域维护新建/编辑后,出库页区域下拉展开即重拉,避免看不到最新货位
async function reloadZones() {
const zs = await request.get('/zone/picker')
zones.value = Array.isArray(zs) ? zs : zs?.list || []
}
@@ -533,11 +537,11 @@ onMounted(loadDicts)
</template>
<el-form-item label="区域">
<el-select v-model="genForm.zoneCode" filterable clearable placeholder="选择区域(选物料/批次后自动带出最近库位)" style="width:100%" @change="onGenZoneChange">
<el-select v-model="genForm.zoneCode" filterable clearable placeholder="选择区域(选物料/批次后自动带出最近库位)" style="width:100%" @change="onGenZoneChange" @visible-change="(v) => v && reloadZones()">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode" :label="`${z.zoneCode} ${z.zoneName}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / 层 / 位置">
<el-form-item label="库位">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<el-select v-model="genForm.shelfNo" placeholder="货架" clearable style="width:120px" @change="onGenShelfChange">
<el-option v-for="s in zoneShelves(genForm.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
@@ -40,8 +40,9 @@ function search() {
/* ===================== 库位四级联动(与入库一致,数据来自 /zone/picker ===================== */
const zones = ref([])
async function loadZones() {
if (zones.value.length) return
// force=true 强制重拉:区域维护新建后,退回区域下拉展开/弹窗打开即可见最新货位
async function loadZones(force = false) {
if (!force && zones.value.length) return
const zs = await request.get('/zone/picker')
zones.value = Array.isArray(zs) ? zs : zs?.list || []
}
@@ -91,7 +92,7 @@ async function openConfirm(row) {
dlg.snInput = ''
dlg.zoneCode = ''; dlg.shelfNo = ''; dlg.layerNo = ''; dlg.positionNo = ''
dlg.manageMode = 1
await loadZones()
await loadZones(true)
// 预填:管理粒度 + 原库位建议(库管可手动改)
try {
const s = await request.get('/return-order/suggest-location', { params: { materialCode: row.materialCode } })
@@ -254,12 +255,12 @@ onMounted(load)
</el-form-item>
<el-form-item label="退回区域">
<el-select v-model="dlg.zoneCode" placeholder="请选择退回区域" clearable style="width:100%" @change="onZoneChange">
<el-select v-model="dlg.zoneCode" placeholder="请选择退回区域" clearable style="width:100%" @change="onZoneChange" @visible-change="(v) => v && loadZones(true)">
<el-option v-for="z in zoneOptions" :key="z.zoneCode" :value="z.zoneCode"
:label="`${z.zoneCode} ${z.zoneName || ''}`" />
</el-select>
</el-form-item>
<el-form-item label="货架 / / 位置">
<el-form-item label="库位">
<div style="display:flex;gap:6px;flex-wrap:wrap">
<el-select v-model="dlg.shelfNo" placeholder="货架" clearable style="width:120px" @change="onShelfChange">
<el-option v-for="s in zoneShelves(dlg.zoneCode)" :key="s" :label="s + ' 架'" :value="s" />
@@ -23,7 +23,6 @@ const routes = [
{ path: 'agv', name: 'Agv', component: () => import('../pages/Agv.vue'), meta: { title: 'AGV配送', keepAlive: true, componentName: 'Agv' } },
{ path: 'zone', name: 'Zone', component: () => import('../pages/BaseData.vue'), props: { mode: 'zone' }, meta: { title: '区域维护', keepAlive: true, componentName: 'BaseData' } },
{ path: 'material', name: 'Material', component: () => import('../pages/BaseData.vue'), props: { mode: 'material' }, meta: { title: '物料档案', keepAlive: true, componentName: 'BaseData' } },
{ path: 'dock', name: 'Dock', component: () => import('../pages/Dock.vue'), meta: { title: '接驳台维护', keepAlive: true, componentName: 'Dock' } },
{ path: 'change-password', name: 'ChangePassword', component: () => import('../pages/ChangePassword.vue'), meta: { title: '修改密码' } },
{ path: 'users', name: 'UserManage', component: () => import('../pages/UserManage.vue'), meta: { title: '账号管理', keepAlive: true, componentName: 'UserManage' } },
{ path: 'roles', name: 'RoleManage', component: () => import('../pages/RoleManage.vue'), meta: { title: '角色管理', keepAlive: true, componentName: 'RoleManage' } },