feat: 完成WMS库房管理系统功能迭代与界面优化
1. 统一系统名称为WMS库房管理,替换所有页面标题、登录页、侧边栏文字 2. 新增公共物料下拉选择组件MaterialSelect,替换多处本地物料字典加载逻辑 3. 用户列表、AGV任务列表、入库/出库/盘点/库存列表新增ID与创建时间列 4. 用户列表、物料查询等接口改为后端真分页,按创建时间倒序排序 5. 库存查询新增全局模糊搜索,支持物料编码/批次号/SN匹配 6. 库存汇总新增区域汇总视图与导出功能,修复导出表头与数据字段 7. 优化侧边栏菜单样式与布局,修复菜单图标间距与文字对齐问题 8. 完善库存查询、区域汇总的文档注释与导出字段
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>库房客户端</title>
|
||||
<title>WMS库房管理</title>
|
||||
<!-- 系统图标:库房箱体(内联 SVG,避免依赖外部文件) -->
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%231668dc'/><path d='M16 7l9 4.5v9L16 25l-9-4.5v-9L16 7z' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/><path d='M7 11.5L16 16l9-4.5M16 16v9' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/></svg>">
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
// 物料下拉选择器(公共组件)
|
||||
// 规范:数据量不确定的下拉只加载最近 100 条(created_at desc),支持远程模糊搜索(keyword 查询,结果仍封顶 100)。
|
||||
// 选中后通过 item-change 事件抛出完整物料对象(无匹配为 null),供页面校验 manageMode 等。
|
||||
import { ref, onMounted } from 'vue'
|
||||
import request from '../utils/request'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Array], default: '' },
|
||||
multiple: { type: Boolean, default: false },
|
||||
manageMode: { type: Number, default: 0 }, // 0=全部 1=结构件 2=精密件
|
||||
placeholder: { type: String, default: '选择物料' },
|
||||
clearable: { type: Boolean, default: true },
|
||||
width: { type: String, default: '100%' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'item-change'])
|
||||
|
||||
const options = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 最近 100 条 + keyword 模糊搜索(后端 /material/query 兼容空 keyword)
|
||||
async function load(kw = '') {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.get('/material/query', {
|
||||
params: { page: 1, pageSize: 100, keyword: kw || '', manageMode: props.manageMode || '' }
|
||||
})
|
||||
options.value = data?.list || []
|
||||
} catch {
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(v) {
|
||||
emit('update:modelValue', v)
|
||||
if (props.multiple) {
|
||||
emit('item-change', options.value.filter((o) => v.includes(o.code)))
|
||||
} else {
|
||||
emit('item-change', options.value.find((o) => o.code === v) || null)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-select :model-value="modelValue" :multiple="multiple" filterable remote clearable
|
||||
:remote-method="load" :loading="loading" :placeholder="placeholder"
|
||||
:style="{ width }" @change="onChange">
|
||||
<el-option v-for="m in options" :key="m.id" :value="m.code"
|
||||
:label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
// WMS 库房客户端 页面级操作说明:整体流程 + 各字段 数据来源/用途/怎么填
|
||||
// WMS库房管理 页面级操作说明:整体流程 + 各字段 数据来源/用途/怎么填
|
||||
// 由 components/PageHelp.vue 在左下角 "?" 按钮弹出展示。
|
||||
// 说明:这里的"数据来源"特指该字段数据由谁产生、从哪个系统/页面来,帮助跑通真实流程。
|
||||
|
||||
@@ -60,7 +60,7 @@ export const helpOutbound = {
|
||||
export const helpInventory = {
|
||||
title: '库存查询',
|
||||
overview:
|
||||
'查看当前库存:结构件按批次(数量/锁定/区域),精密件按 SN(状态/区域)。数据由入库、出库、检验、盘点等业务实时维护,本页只读。\n\n本页三个视图页签(首屏默认「物料汇总」):\n· 物料汇总:一物料一行,直接回答"这个物料总共有多少"——总量/锁定/可用 + 质量分布(合格/未检/不合格) + 分布在哪些区域;点行下钻该物料的批次/SN 明细。\n· 库存明细:按 物料 × 区域 × 质量 聚合,可组合 区域/质量状态/入库时间 筛选(物料汇总不受区域/质量筛选影响)。\n· 区域汇总:按 区域 × 质量状态 × 类型 看各库区的货量。\n\n层级概念:入库单 → 批次/SN → 库存行。区域/质量状态/批次/最近入库单都是某物料的一对多属性,放在下钻明细中查看,不在物料行上并排平铺。',
|
||||
'查看当前库存:结构件按批次(数量/锁定/区域),精密件按 SN(状态/区域)。数据由入库、出库、检验、盘点等业务实时维护,本页只读。\n\n本页三个视图页签(首屏默认「物料汇总」):\n· 物料汇总:一物料一行,直接回答"这个物料总共有多少"——总量/锁定/可用 + 质量分布(合格/未检/不合格) + 分布在哪些区域;ID 与创建时间取自物料主表,另有最近入库单/最近入库时间;点行下钻该物料的批次/SN 明细。\n· 库存明细:按 物料 × 区域 × 质量 聚合,可组合 区域/质量状态/入库时间 筛选(物料汇总不受区域/质量筛选影响)。\n· 区域汇总:按 区域 × 质量状态 × 类型 看各库区的货量;ID 与创建时间取自区域主表,另有组内最近入库时间;支持全量导出。\n\n层级概念:入库单 → 批次/SN → 库存行。区域/质量状态/批次/最近入库单都是某物料的一对多属性,放在下钻明细中查看,不在物料行上并排平铺。',
|
||||
fields: [
|
||||
{ name: '物料编码 / 物料名称', source: '筛选条件。', purpose: '只看某个物料', fill: '输入编码或名称,可空=全部' },
|
||||
{ name: '库存类型', source: '单选(全部/结构件/精密件)。', purpose: '按管理粒度过滤', fill: '全部 / 结构件(批次) / 精密件(SN)' },
|
||||
@@ -68,7 +68,8 @@ export const helpInventory = {
|
||||
{ name: '物料汇总-分布区域', source: '系统统计该物料当前占用了哪些区域。', purpose: '一个物料可跨多个区域存放(批次可拆分),物料行不写死单一区域', fill: '如 Z02、Z05;点行下钻可看每个批次/SN 具体在哪个区' },
|
||||
{ name: '库存明细-区域/质量/入库时间', source: '仅「库存明细」视图生效的筛选。', purpose: '在物料基础上再按存放区/质量状态/入库时间收窄', fill: '可组合筛选' },
|
||||
{ name: '批次/SN 明细(下钻)', source: '点击任一行弹出抽屉。', purpose: '看该物料的每个批次/SN 所在区域、质量状态、锁定与来源入库单', fill: '抽屉内分页展示;点入库单号可跳转入库页' },
|
||||
{ name: '导出', source: '按钮。后端按当前筛选全量导出 xlsx。', purpose: '把结果导出存档', fill: '物料汇总视图导出「物料汇总.xlsx」;库存明细视图导出「库存汇总.xlsx」' }
|
||||
{ name: 'ID / 创建时间', source: '物料汇总行取物料主表(material),区域汇总行取区域主表(zone)。', purpose: '行标识与主档建立时间,用于追溯与对账', fill: '系统自动带出,只读;物料档案/区域缺失时显示 -' },
|
||||
{ name: '导出', source: '按钮。后端按当前筛选全量导出 xlsx。', purpose: '把结果导出存档', fill: '物料汇总视图导出「物料汇总.xlsx」;库存明细视图导出「库存汇总.xlsx」;区域汇总视图导出「区域汇总.xlsx」' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -167,22 +167,22 @@ function onLogout() {
|
||||
:class="{ 'drawer': isMobile, 'drawer-open': isMobile && mobileOpen }">
|
||||
<div class="brand">
|
||||
<el-icon :size="22"><Box /></el-icon>
|
||||
<span v-show="!isCollapse">库房客户端</span>
|
||||
<span v-show="!isCollapse">WMS库房管理</span>
|
||||
</div>
|
||||
<el-menu :key="menuKey" :default-active="route.path" :default-openeds="defaultOpeneds" class="menu"
|
||||
background-color="#001529" text-color="#c2cad8" active-text-color="#ffffff" router
|
||||
background-color="#001529" text-color="#a6adb4" active-text-color="#409eff" router
|
||||
:collapse="isCollapse && !isMobile">
|
||||
<el-menu-item v-if="showTopMenu" :index="topMenu.path">
|
||||
<el-icon><component :is="topMenu.icon" /></el-icon>
|
||||
<el-icon style="margin-right:8px"><component :is="topMenu.icon" /></el-icon>
|
||||
<span>{{ topMenu.title }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-for="g in menus" :key="g.title" :index="'group:' + g.title">
|
||||
<template #title>
|
||||
<el-icon><component :is="g.icon" /></el-icon>
|
||||
<el-icon style="margin-right:8px"><component :is="g.icon" /></el-icon>
|
||||
<span>{{ g.title }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="m in g.children" :key="m.path" :index="m.path">
|
||||
<el-icon><component :is="m.icon" /></el-icon>
|
||||
<el-icon style="margin-right:8px"><component :is="m.icon" /></el-icon>
|
||||
<span>{{ m.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
@@ -231,10 +231,9 @@ function onLogout() {
|
||||
.drawer-open { transform: translateX(0); }
|
||||
.drawer-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, .45); z-index: 1000; }
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
height: 56px; padding: 0 16px;
|
||||
color: #fff; font-weight: 600; font-size: 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
height: 60px;
|
||||
color: #fff; font-weight: 600; font-size: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu { border-right: none; }
|
||||
|
||||
@@ -162,6 +162,7 @@ onMounted(() => { loadDocks(); loadTasks() })
|
||||
<el-button @click="taskPage.current = 1; loadTasks()">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="tasks" v-loading="loadingTasks" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="taskNo" label="任务号" width="150" />
|
||||
<el-table-column prop="sourceDock" label="源" width="80" align="center" />
|
||||
<el-table-column prop="targetDock" label="目标" width="80" align="center" />
|
||||
@@ -186,6 +187,9 @@ onMounted(() => { loadDocks(); loadTasks() })
|
||||
<el-table-column label="到位时间" width="150">
|
||||
<template #default="{ row }">{{ row.arrivedAt ? fmtTime(row.arrivedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ row.createdAt ? fmtTime(row.createdAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="下发人" width="100" />
|
||||
<el-table-column prop="hikTaskCode" label="RCS任务码" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { fmtTime } from '../utils/format'
|
||||
import { exportXlsxFetch } from '../utils/export'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import MaterialSelect from '../components/MaterialSelect.vue'
|
||||
import { helpInbound } from '../help'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -17,20 +18,14 @@ const operator = getRealName()
|
||||
// ===== 多 Tab:结构件入库 / 精密件 SN 入库 在前(常操作),入库记录在后 =====
|
||||
const activeTab = ref('batch') // batch=结构件入库 / sn=精密件SN入库 / records=入库记录
|
||||
|
||||
// 公共字典:物料 / 区域
|
||||
const materials = ref([])
|
||||
// 公共字典:区域(物料下拉由 MaterialSelect 组件按需远程搜索,最近 100 条)
|
||||
const zones = ref([])
|
||||
async function loadDicts() {
|
||||
if (materials.value.length && zones.value.length) return
|
||||
const [ms, zs] = await Promise.all([request.get('/material/list'), request.get('/zone/picker')])
|
||||
materials.value = Array.isArray(ms) ? ms : ms?.list || []
|
||||
if (zones.value.length) return
|
||||
const zs = await request.get('/zone/picker')
|
||||
zones.value = Array.isArray(zs) ? zs : zs?.list || []
|
||||
}
|
||||
|
||||
// 按当前页过滤物料:结构件页只显示批次管理(manageMode=1),SN 页只显示序列号管理(manageMode=2)
|
||||
const batchMaterials = computed(() => materials.value.filter((m) => Number(m.manageMode) === 1))
|
||||
const snMaterials = computed(() => materials.value.filter((m) => Number(m.manageMode) === 2))
|
||||
|
||||
/* ===================== Tab1 结构件入库(批次,支持多批到货追加) ===================== */
|
||||
const batchRef = ref()
|
||||
const submitting1 = ref(false)
|
||||
@@ -51,11 +46,6 @@ async function submitBatch() {
|
||||
ElMessage.warning('数量必须大于 0')
|
||||
return
|
||||
}
|
||||
const mat1 = materials.value.find(m => m.code === form1.materialCode)
|
||||
if (mat1 && Number(mat1.manageMode) === 2) {
|
||||
ElMessage.warning('该物料为精密件(按 SN 管理),请切换到「精密件 SN 入库」页扫码录入后再提交')
|
||||
return
|
||||
}
|
||||
submitting1.value = true
|
||||
try {
|
||||
const data = await request.post('/inbound/create', {
|
||||
@@ -117,11 +107,6 @@ async function submitSn() {
|
||||
ElMessage.warning('请先扫码录入 SN(输入后点击【录入到列表】)')
|
||||
return
|
||||
}
|
||||
const mat2 = materials.value.find(m => m.code === form2.materialCode)
|
||||
if (mat2 && Number(mat2.manageMode) === 1) {
|
||||
ElMessage.warning('该物料为结构件(按批次管理),请切换到「结构件入库」页按数量入库')
|
||||
return
|
||||
}
|
||||
submitting2.value = true
|
||||
try {
|
||||
const data = await request.post('/inbound/create', {
|
||||
@@ -299,10 +284,7 @@ onMounted(() => {
|
||||
<el-card shadow="never">
|
||||
<el-form ref="batchRef" :model="form1" :rules="rules1" label-width="110px" style="max-width:560px">
|
||||
<el-form-item label="物料" prop="materialCode">
|
||||
<el-select v-model="form1.materialCode" filterable placeholder="选择结构件物料" style="width:100%">
|
||||
<el-option v-for="m in batchMaterials" :key="m.id" :value="m.code"
|
||||
:label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="form1.materialCode" :manage-mode="1" placeholder="选择结构件物料" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批次号">
|
||||
<el-input v-model="form1.batchNo" placeholder="留空则自动生成" clearable />
|
||||
@@ -355,10 +337,7 @@ onMounted(() => {
|
||||
<el-card shadow="never">
|
||||
<el-form label-width="110px" style="max-width:680px">
|
||||
<el-form-item label="物料" required>
|
||||
<el-select v-model="form2.materialCode" filterable placeholder="请选择精密件物料(必填)" style="width:100%">
|
||||
<el-option v-for="m in snMaterials" :key="m.id" :value="m.code"
|
||||
:label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="form2.materialCode" :manage-mode="2" placeholder="请选择精密件物料(必填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="区域" required>
|
||||
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域(必填)" clearable style="width:100%">
|
||||
@@ -451,6 +430,7 @@ onMounted(() => {
|
||||
<!-- 入库单列表(一单一行,结构件/精密件同表) -->
|
||||
<el-card shadow="never" header="入库单(每行一笔入库;点「明细」在右侧抽屉查看该单的行)">
|
||||
<el-table :data="list" border size="small" v-loading="loadingRec" empty-text="暂无入库数据">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="inboundNo" label="入库单号" min-width="160" />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -38,17 +38,22 @@ const inputPlaceholder = computed(() =>
|
||||
: '扫入或粘贴 SN 序列号,可用 逗号/分号/空格/回车 分隔,一次可录多条'
|
||||
)
|
||||
|
||||
// 库存拾取器:直接选已存在的批次号/SN,避免手输(垃圾设计)
|
||||
// 库存拾取器:直接选已存在的批次号/SN,避免手输。
|
||||
// 规范:库存量不确定 → 只加载最近 100 条(created_at desc),输入走远程模糊搜索(keyword 匹配 物料编码/批次号/SN)
|
||||
const pickerOptions = ref([])
|
||||
async function loadPicker() {
|
||||
const pickerLoading = ref(false)
|
||||
async function loadPicker(kw = '') {
|
||||
pickerLoading.value = true
|
||||
try {
|
||||
const mm = targetType.value === 'BATCH' ? 1 : 2
|
||||
const data = await request.get('/stock/query', {
|
||||
params: { manageMode: mm, page: 1, pageSize: 500 }
|
||||
params: { manageMode: mm, keyword: kw || '', page: 1, pageSize: 100 }
|
||||
})
|
||||
pickerOptions.value = data?.list || []
|
||||
} catch (e) {
|
||||
pickerOptions.value = []
|
||||
} finally {
|
||||
pickerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +213,7 @@ onMounted(() => {
|
||||
<el-form label-width="96px">
|
||||
<!-- 目标类型直接改变输入框标题与占位,直观体现当前录入对象 -->
|
||||
<el-form-item label="目标类型">
|
||||
<el-radio-group v-model="targetType" @change="loadPicker">
|
||||
<el-radio-group v-model="targetType" @change="() => loadPicker()">
|
||||
<el-radio-button value="BATCH">批次 (结构件)</el-radio-button>
|
||||
<el-radio-button value="SN">序列号 SN (精密件)</el-radio-button>
|
||||
</el-radio-group>
|
||||
@@ -239,13 +244,16 @@ onMounted(() => {
|
||||
|
||||
<el-divider content-position="left">待检编号(可扫/选多条,统一提交)</el-divider>
|
||||
|
||||
<!-- 从库存直接拾取,避免手输批次号 -->
|
||||
<!-- 从库存直接拾取,避免手输批次号(远程模糊搜索,最近 100 条) -->
|
||||
<el-form-item :label="`从库存选${inputLabel}`">
|
||||
<el-select
|
||||
v-model="pickerValue"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:placeholder="`选择已存在的${inputLabel}(来自库存)`"
|
||||
:remote-method="loadPicker"
|
||||
:loading="pickerLoading"
|
||||
:placeholder="`输入批次号/SN/物料编码搜索(最近100条)`"
|
||||
style="width:100%"
|
||||
@change="addFromPicker"
|
||||
>
|
||||
|
||||
@@ -190,7 +190,7 @@ function gotoInbound(no) {
|
||||
router.push({ path: '/inbound', query: { inboundNo: no } })
|
||||
}
|
||||
|
||||
// 导出:按当前视图导出对应粒度(物料汇总/库存明细)
|
||||
// 导出:按当前视图导出对应粒度(物料汇总 / 库存明细 / 区域汇总)
|
||||
async function exportCsv() {
|
||||
try {
|
||||
if (viewTab.value === 'material') {
|
||||
@@ -202,6 +202,10 @@ async function exportCsv() {
|
||||
}, '物料汇总.xlsx')
|
||||
return
|
||||
}
|
||||
if (viewTab.value === 'summary') {
|
||||
await exportXlsxFetch('/api/stock/export', { view: 'zone' }, '区域汇总.xlsx')
|
||||
return
|
||||
}
|
||||
await exportXlsxFetch('/api/stock/export', {
|
||||
materialCode: filters.materialCode.trim(),
|
||||
materialName: filters.materialName.trim(),
|
||||
@@ -282,6 +286,9 @@ onMounted(() => {
|
||||
<el-card v-if="viewTab === 'material'" shadow="never" header="物料汇总(一物料一行:总数 + 质量分布 + 分布区域,点击行下钻批次/SN 明细)">
|
||||
<el-table :data="matRows" border size="small" v-loading="matLoading" empty-text="暂无库存数据"
|
||||
@row-click="openDetails" row-key="materialCode">
|
||||
<el-table-column label="ID" prop="id" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="link">{{ row.materialCode }}</span>
|
||||
@@ -333,6 +340,9 @@ onMounted(() => {
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物料创建时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ row.materialCreatedAt ? fmtTime(row.materialCreatedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近入库时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
@@ -405,8 +415,18 @@ onMounted(() => {
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 视图三:区域汇总 ============ -->
|
||||
<el-card v-if="viewTab === 'summary'" shadow="never" header="各区域库存数量(按 区域 + 质量状态 + 类型)">
|
||||
<el-card v-if="viewTab === 'summary'" shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>各区域库存数量(按 区域 + 质量状态 + 类型)</span>
|
||||
<el-button v-if="can('inventory:export')" :disabled="!summaryRows.length" size="small"
|
||||
@click="exportCsv">导出区域汇总</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="summaryRows" border size="small" v-loading="summaryLoading" empty-text="暂无数据">
|
||||
<el-table-column label="ID" prop="id" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="zoneCode" label="区域编码" width="110" />
|
||||
<el-table-column prop="zoneName" label="区域名称" min-width="140" />
|
||||
<el-table-column prop="quality" label="质量状态" width="110" align="center">
|
||||
@@ -417,6 +437,12 @@ onMounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100" align="center" />
|
||||
<el-table-column prop="qty" label="数量" width="120" align="center" />
|
||||
<el-table-column label="区域创建时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ row.zoneCreatedAt ? fmtTime(row.zoneCreatedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近入库时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ row.lastCreatedAt ? fmtTime(row.lastCreatedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ async function onSubmit() {
|
||||
<div class="login-page">
|
||||
<el-card class="card">
|
||||
<div class="head">
|
||||
<h2>库房客户端</h2>
|
||||
<h2>WMS库房管理</h2>
|
||||
<p>请使用 WMS 账号登录</p>
|
||||
</div>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" @keyup.enter="onSubmit">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fmtTime } from '../utils/format'
|
||||
import { exportXlsxFetch } from '../utils/export'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import MaterialSelect from '../components/MaterialSelect.vue'
|
||||
import { helpOutbound } from '../help'
|
||||
|
||||
const activeTab = ref('prep') // prep=备料出库 / general=通用出库 / records=出库记录
|
||||
@@ -98,7 +99,7 @@ async function queryPrep() {
|
||||
}
|
||||
|
||||
/* ===================== 通用出库(手动,不依赖工单) ===================== */
|
||||
const materials = ref([])
|
||||
// 物料下拉由 MaterialSelect 组件按需远程搜索(最近 100 条)
|
||||
const zones = ref([])
|
||||
const genForm = reactive({ materialCode: '', mode: 1, batchNo: '', qty: 1, zoneCode: '', boxNo: '', contractNo: '', remark: '' })
|
||||
const snInput = ref('')
|
||||
@@ -120,9 +121,9 @@ function addSnLines() {
|
||||
function removeSn(i) { snList.value.splice(i, 1) }
|
||||
|
||||
async function loadDicts() {
|
||||
if (materials.value.length) return
|
||||
const [ms, zs] = await Promise.all([request.get('/material/list'), request.get('/zone/picker')])
|
||||
materials.value = Array.isArray(ms) ? ms : ms?.list || []
|
||||
if (zones.value.length) return
|
||||
// 物料下拉由 MaterialSelect 组件按需远程搜索(最近 100 条),此处只加载区域
|
||||
const zs = await request.get('/zone/picker')
|
||||
zones.value = Array.isArray(zs) ? zs : zs?.list || []
|
||||
}
|
||||
|
||||
@@ -278,9 +279,7 @@ onMounted(loadDicts)
|
||||
<el-card shadow="never" class="mb12" header="通用出库(不依赖工单:退料 / 样品 / 报废 / 发货)">
|
||||
<el-form label-width="96px" style="max-width:880px">
|
||||
<el-form-item label="物料" required>
|
||||
<el-select v-model="genForm.materialCode" filterable placeholder="选择物料" style="width:100%">
|
||||
<el-option v-for="m in materials" :key="m.id" :value="m.code" :label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="genForm.materialCode" placeholder="选择物料" />
|
||||
</el-form-item>
|
||||
<el-form-item label="出库类型">
|
||||
<el-radio-group v-model="genForm.mode">
|
||||
@@ -378,9 +377,7 @@ onMounted(loadDicts)
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="物料">
|
||||
<el-select v-model="recFilters.materialCode" filterable clearable placeholder="物料" style="width:180px">
|
||||
<el-option v-for="m in materials" :key="m.id" :value="m.code" :label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="recFilters.materialCode" placeholder="物料" width="180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="箱号">
|
||||
<el-input v-model="recFilters.boxNo" placeholder="按箱号查询" clearable style="width:160px" @keyup.enter="searchRec" />
|
||||
|
||||
@@ -6,16 +6,13 @@ import request from '../utils/request'
|
||||
import { fmtTime } from '../utils/format'
|
||||
import { getRealName } from '../utils/auth'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import MaterialSelect from '../components/MaterialSelect.vue'
|
||||
import { helpSemi } from '../help'
|
||||
|
||||
/* ---------- 半成品/成品入库(携带已完成工序,连续扫码) ---------- */
|
||||
const materials = ref([])
|
||||
// 物料下拉由 MaterialSelect 组件按需远程搜索(最近 100 条)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const ms = await request.get('/material/list')
|
||||
materials.value = Array.isArray(ms) ? ms : ms?.list || []
|
||||
} catch { /* 字典加载失败不阻塞入库 */ }
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
|
||||
@@ -129,10 +126,7 @@ function search() {
|
||||
<el-tab-pane label="半成品/成品入库">
|
||||
<el-form label-width="110px" style="max-width:680px">
|
||||
<el-form-item label="物料">
|
||||
<el-select v-model="inForm.materialCode" filterable placeholder="选择物料编码" style="width:100%">
|
||||
<el-option v-for="m in materials" :key="m.id" :value="m.code"
|
||||
:label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="inForm.materialCode" placeholder="选择物料编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已完成工序">
|
||||
<el-input v-model="inForm.completedProcess" placeholder="如:1,3,5(可空)" clearable />
|
||||
@@ -200,6 +194,7 @@ function search() {
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无记录">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="sn" label="SN" min-width="200" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
|
||||
<el-table-column prop="completedProcess" label="已完成工序" min-width="120">
|
||||
|
||||
@@ -6,6 +6,7 @@ import request from '../utils/request'
|
||||
import { fmtTime } from '../utils/format'
|
||||
import { getRealName } from '../utils/auth'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import MaterialSelect from '../components/MaterialSelect.vue'
|
||||
import { helpStocktake } from '../help'
|
||||
|
||||
/* ---------- 发起盘点(可选范围:全库 / 按区域 / 按物料) ---------- */
|
||||
@@ -21,19 +22,14 @@ const scope = reactive({
|
||||
qualityStatus: '' // 空=全部
|
||||
})
|
||||
|
||||
// 发起范围所需的字典
|
||||
// 发起范围所需的字典(物料下拉由 MaterialSelect 组件按需远程搜索,最近 100 条)
|
||||
const zonesDict = ref([])
|
||||
const materialsDict = ref([])
|
||||
async function loadScopeDicts() {
|
||||
try {
|
||||
const z = await request.get('/zone/picker')
|
||||
const arr = Array.isArray(z) ? z : (z?.list || [])
|
||||
zonesDict.value = arr.map(x => x.zoneCode || x.code).filter(Boolean)
|
||||
} catch { /* 忽略 */ }
|
||||
try {
|
||||
const m = await request.get('/material/query', { params: { page: 1, pageSize: 500 } })
|
||||
materialsDict.value = (m?.list || []).map(x => ({ code: x.code, name: x.name }))
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
function openScope() {
|
||||
@@ -222,11 +218,8 @@ const currentStep = computed(() => {
|
||||
</el-form-item>
|
||||
<template v-if="scope.type === 'material'">
|
||||
<el-form-item label="物料">
|
||||
<el-select v-model="scope.materialCodes" multiple filterable clearable
|
||||
placeholder="选择盘点物料(可多选)" style="width:100%">
|
||||
<el-option v-for="m in materialsDict" :key="m.code"
|
||||
:value="m.code" :label="`${m.code} ${m.name || ''}`" />
|
||||
</el-select>
|
||||
<MaterialSelect v-model="scope.materialCodes" multiple
|
||||
placeholder="选择盘点物料(可多选,输入可搜索)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="scope.manageMode" clearable placeholder="全部类型" style="width:100%">
|
||||
@@ -319,6 +312,7 @@ const currentStep = computed(() => {
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="history" border size="small" empty-text="暂无盘点记录">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="stocktakeNo" label="盘点单号" min-width="200" />
|
||||
<el-table-column prop="operator" label="操作人" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||||
|
||||
@@ -22,28 +22,27 @@ async function loadRoles() {
|
||||
} catch { /* 无权限时不阻断用户页 */ }
|
||||
}
|
||||
|
||||
const all = ref([]) // 全量(无分页全量拉取,页面前端分页)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const page = reactive({ current: 1, size: 20 })
|
||||
|
||||
// 后端真分页(created_at desc 最新置顶),禁止前端全量拉取后本地分页
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.get('/user/list')
|
||||
all.value = data?.list || []
|
||||
sliceList()
|
||||
const data = await request.get('/user/list', {
|
||||
params: { page: page.current, pageSize: page.size }
|
||||
})
|
||||
list.value = data?.list || []
|
||||
total.value = data?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function sliceList() {
|
||||
const start = (page.current - 1) * page.size
|
||||
list.value = all.value.slice(start, start + page.size)
|
||||
}
|
||||
function onPage() {
|
||||
sliceList()
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function roleName(row) {
|
||||
@@ -185,6 +184,9 @@ onMounted(() => {
|
||||
<el-table-column label="最近登录" width="150" align="center">
|
||||
<template #default="{ row }">{{ row.lastLoginAt ? fmtTime(row.lastLoginAt) : '从未' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ row.createdAt ? fmtTime(row.createdAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="can('user:edit')" link type="primary" size="small" :disabled="row.username === 'admin'"
|
||||
@@ -196,8 +198,8 @@ onMounted(() => {
|
||||
</el-table>
|
||||
|
||||
<el-pagination v-model:current-page="page.current" v-model:page-size="page.size" :page-sizes="[10, 20, 50]"
|
||||
:total="all.length" background layout="total, sizes, prev, pager, next, jumper" style="margin-top:12px"
|
||||
@size-change="onPage" @current-change="onPage" />
|
||||
:total="total" background layout="total, sizes, prev, pager, next, jumper" style="margin-top:12px"
|
||||
@size-change="() => { page.current = 1; loadUsers() }" @current-change="onPage" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑账号' : '新增账号'" width="520px"
|
||||
|
||||
@@ -44,7 +44,7 @@ router.beforeEach((to) => {
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
document.title = to.meta?.title ? `${to.meta.title} - 库房客户端` : '库房客户端'
|
||||
document.title = to.meta?.title ? `${to.meta.title} - WMS库房管理` : 'WMS库房管理'
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -3,11 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>库房客户端</title>
|
||||
<title>WMS库房管理</title>
|
||||
<!-- 系统图标:库房箱体(内联 SVG,避免依赖外部文件) -->
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%231668dc'/><path d='M16 7l9 4.5v9L16 25l-9-4.5v-9L16 7z' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/><path d='M7 11.5L16 16l9-4.5M16 16v9' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-BU25nTUO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-3pBbrjFT.css">
|
||||
<script type="module" crossorigin src="/assets/index-yykpqais.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DTC9tJqp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user