feat: 完成WMS库房管理系统功能迭代与界面优化

1.  统一系统名称为WMS库房管理,替换所有页面标题、登录页、侧边栏文字
2.  新增公共物料下拉选择组件MaterialSelect,替换多处本地物料字典加载逻辑
3.  用户列表、AGV任务列表、入库/出库/盘点/库存列表新增ID与创建时间列
4.  用户列表、物料查询等接口改为后端真分页,按创建时间倒序排序
5.  库存查询新增全局模糊搜索,支持物料编码/批次号/SN匹配
6.  库存汇总新增区域汇总视图与导出功能,修复导出表头与数据字段
7.  优化侧边栏菜单样式与布局,修复菜单图标间距与文字对齐问题
8.  完善库存查询、区域汇总的文档注释与导出字段
This commit is contained in:
SunYF
2026-09-07 15:26:37 +08:00
parent 6ee131a4bd
commit 649d2cc358
17 changed files with 364 additions and 181 deletions
+1 -1
View File
@@ -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>
+4 -3
View File
@@ -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 -3
View File
@@ -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>