Browse Source

feat(inventory): 新增立库手工盘点功能

- 在路由中添加手工盘点页面路径 /manualCount
- 在主界面菜单中增加"立库手工盘点"入口
- 创建手工盘点页面组件 manualCount.vue
- 实现栈板扫描和标签扫描功能
- 支持一键提交和手动提交盘点结果
- 添加已扫描标签查看和删除功能
- 集成PDA扫描相关API接口
- 添加确认标签信息弹窗和已扫描列表弹窗
- 实现完整的盘点流程交互逻辑
- 添加相应的UI样式和组件布局
master
常熟吴彦祖 3 weeks ago
parent
commit
48febc1e4c
  1. 16
      src/api/check/physicalInventory.js
  2. 4
      src/router/index.js
  3. 6
      src/views/main.vue
  4. 666
      src/views/modules/inventory/manualCount.vue

16
src/api/check/physicalInventory.js

@ -0,0 +1,16 @@
import { createAPI } from "@/utils/httpRequest.js";
// ==================== PDA手工盘点 ==================== - rqrq
// PDA扫描栈板 - 验证并返回盘点信息
export const pdaScanPallet = data => createAPI(`/check/physicalInventory/pda/scanPallet`, 'post', data)
// PDA扫描标签 - 获取标签信息
export const pdaScanLabel = data => createAPI(`/check/physicalInventory/pda/scanLabel`, 'post', data)
// PDA一键提交盘点(默认全部OK)
export const pdaQuickSubmitCount = data => createAPI(`/check/physicalInventory/pda/quickSubmitCount`, 'post', data)
// PDA提交盘点结果
export const pdaSubmitCount = data => createAPI(`/check/physicalInventory/pda/submitCount`, 'post', data)

4
src/router/index.js

@ -119,6 +119,8 @@ const globalRoutes = [
{path: "/mrReturnPickingDetail", name: "mrReturnPickingDetail", component: resolve => require(["@/views/modules/mr-issue/mrReturnPickingDetail.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true } },
// 盘点
{path: "/stocktaking",name: "stocktaking", component: resolve => require(["@/views/modules/inventory/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
// 手工盘点 - rqrq
{path: "/manualCount",name: "manualCount", component: resolve => require(["@/views/modules/inventory/manualCount.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: false}},
// 标签查询
{path: "/labelQuery",name: "labelQuery", component: resolve => require(["@/views/modules/inventory/label-query.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: "/wmsCancelReserve",name: "wmsCancelReserve", component: resolve => require(["@/views/modules/inventory/wms-cancel-reserve.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
@ -185,7 +187,7 @@ router.beforeEach((to, from, next) => {
return
}
}
// 添加动态(菜单)路由
// 1. 已经添加 or 全局路由, 直接访问
// 2. 获取菜单列表, 添加并保存本地存储

6
src/views/main.vue

@ -217,6 +217,12 @@
</div>
<div class="menu-text">合托Call板</div>
</div>
<div class="menu-item" @click="navigateWithWarehouseCheck('manualCount')">
<div class="menu-icon purchase">
<van-icon name="shopping-cart-o" size="24" />
</div>
<div class="menu-text">立库手工盘点</div>
</div>
</div>
</div>
<!-- 库内管理 -->

666
src/views/modules/inventory/manualCount.vue

@ -0,0 +1,666 @@
<template>
<div>
<div class="pda-container">
<!-- 头部栏 - rqrq -->
<div class="header-bar">
<div class="header-left" @click="handleBack">
<i class="el-icon-arrow-left"></i>
<span>手工盘点</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<div class="table-body" style="max-height: 500px; overflow-y: auto;">
<div class="main-content form-section">
<!-- 第一行栈板扫描 - rqrq -->
<div class="input-group">
<label class="input-label">栈板编码</label>
<div style="display: flex; gap: 8px;">
<el-input
v-model="palletCode"
placeholder="请扫描栈板编码"
class="form-input"
style="flex: 1;"
clearable
inputmode="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
@keyup.enter.native="handlePalletScan"
ref="palletInput"
:disabled="palletScanned"
/>
</div>
</div>
<!-- 扫描栈板成功后显示的区域 - rqrq -->
<template v-if="palletScanned">
<!-- 盘点单号显示 -->
<!-- <div class="input-group">-->
<!-- <label class="input-label">盘点单号</label>-->
<!-- <el-input v-model="countNo" readonly class="form-input"></el-input>-->
<!-- </div>-->
<!-- 扫描标签输入框 - rqrq -->
<div class="input-group">
<label class="input-label">扫描标签</label>
<el-input
v-model="labelCode"
placeholder="请扫描标签"
class="form-input"
clearable
inputmode="none"
autocomplete="off"
@keyup.enter.native="handleLabelScan"
ref="labelInput"
/>
</div>
<!-- 按钮区域 - rqrq -->
<div class="input-group" style="margin-top: 10px;">
<div style="display: flex; gap: 8px;">
<button
class="action-btn primary"
style="flex: 1;"
@click="handleQuickSubmit"
:disabled="submitLoading">
{{ submitLoading ? '提交中...' : '一键提交' }}
</button>
<button
class="action-btn warning"
style="flex: 1;"
@click="handleSubmitCount"
:disabled="submitLoading">
{{ submitLoading ? '提交中...' : '提交盘点' }}
</button>
</div>
</div>
<!-- 查看已扫描标签按钮 - rqrq -->
<div class="input-group" style="margin-top: 5px;">
<button
class="action-btn secondary"
style="width: 100%;"
@click="showScannedList">
查看已扫描({{ scannedLabels.length }})
</button>
</div>
</template>
</div>
<!-- 栈板盘点标签明细 - rqrq -->
<div v-if="palletScanned" class="rma-list">
<div class="list-title">
盘点标签明细 ({{ labelList.length }})
</div>
<div class="detail-table">
<div class="table-header">
<div class="col-label">标签号</div>
<div class="col-part">物料号</div>
<div class="col-status">状态</div>
</div>
<div class="table-body-scroll">
<div
v-for="(item, index) in labelList"
:key="index"
class="table-row"
:class="{'row-checked': item.countFlag === 'Y'}">
<div class="col-label">{{ item.unitId }}</div>
<div class="col-part">{{ item.partNo }}</div>
<div class="col-status" :style="{color: item.countFlag === 'Y' ? '#67C23A' : '#F56C6C'}">
{{ item.countFlagDesc }}
</div>
</div>
<div v-if="labelList.length === 0" class="empty-row">
暂无数据
</div>
</div>
</div>
</div>
</div>
<!-- 重新扫描按钮 - rqrq -->
<!-- <div v-if="palletScanned" class="footer-bar">-->
<!-- <button class="action-btn secondary" style="width: 100%;" @click="handleReset">-->
<!-- 重新扫描-->
<!-- </button>-->
<!-- </div>-->
</div>
<!-- 扫描标签弹框 - rqrq -->
<el-dialog
title="确认标签信息"
:visible.sync="labelDialogVisible"
width="90%"
:close-on-click-modal="false"
:show-close="true"
:modal="true"
:modal-append-to-body="true"
:append-to-body="true">
<div class="label-info">
<div class="info-row">
<span class="info-label">标签号</span>
<span class="info-value">{{ currentLabel.unitId }}</span>
</div>
<div class="info-row">
<span class="info-label">物料编码</span>
<span class="info-value">{{ currentLabel.partNo }}</span>
</div>
<div class="info-row">
<span class="info-label">批号</span>
<span class="info-value">{{ currentLabel.batchNo }}</span>
</div>
<div class="info-row">
<span class="info-label">数量</span>
<el-input
v-model="currentLabel.qty"
type="number"
style="width: 100%;">
</el-input>
</div>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn primary" @click="confirmLabelScan"> </button>
<button class="action-btn secondary" style="margin-left: 10px;" @click="labelDialogVisible = false"> </button>
</div>
</el-dialog>
<!-- 已扫描标签列表弹框 - rqrq -->
<el-dialog
title="已扫描标签列表"
:visible.sync="scannedDialogVisible"
width="90%"
:close-on-click-modal="false"
:show-close="true"
:modal="true"
:modal-append-to-body="true"
:append-to-body="true">
<div class="scanned-list">
<div v-for="(item, index) in scannedLabels" :key="index" class="scanned-item">
<div class="scanned-info">
<div>标签{{ item.unitId }}</div>
<div>物料{{ item.partNo }}</div>
<div>数量{{ item.qty }}</div>
</div>
<button class="action-btn danger" @click="removeScannedLabel(index)">删除</button>
</div>
<div v-if="scannedLabels.length === 0" class="empty-list">
暂无扫描记录
</div>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn secondary" @click="scannedDialogVisible = false"> </button>
</div>
</el-dialog>
</div>
</template>
<script>
import { pdaScanPallet, pdaScanLabel, pdaQuickSubmitCount, pdaSubmitCount } from '@/api/check/physicalInventory'
export default {
name: 'manualCount',
data() {
return {
// - rqrq
palletCode: '',
palletScanned: false,
countNo: '',
cleanPalletId: '',
// - rqrq
labelList: [],
// - rqrq
labelCode: '',
labelDialogVisible: false,
currentLabel: {
unitId: '',
partNo: '',
batchNo: '',
qty: 0
},
// - rqrq
scannedLabels: [],
scannedDialogVisible: false,
// - rqrq
submitLoading: false
}
},
mounted() {
this.$nextTick(() => {
this.$refs.palletInput.focus()
})
},
methods: {
// - rqrq
handleBack() {
this.$router.go(-1)
},
// - rqrq
handlePalletScan() {
if (!this.palletCode.trim()) {
this.$message.error('请输入栈板编码')
return
}
const params = {
site: this.$store.state.user.site,
palletId: this.palletCode
}
pdaScanPallet(params).then(({ data }) => {
if (data && data.code === 0) {
const result = data.data
this.cleanPalletId = result.cleanPalletId
this.countNo = result.countNo
this.labelList = result.labelList || []
this.palletCode = result.cleanPalletId //
this.palletScanned = true
this.scannedLabels = [] //
this.$message.success('栈板验证成功')
// - rqrq
this.$nextTick(() => {
this.$refs.labelInput.focus()
})
} else {
this.$message.error(data.msg || '扫描失败')
}
}).catch(() => {
this.$message.error('网络错误')
})
},
// - rqrq
handleLabelScan() {
if (!this.labelCode.trim()) {
return
}
const params = {
site: this.$store.state.user.site,
unitId: this.labelCode.trim()
}
pdaScanLabel(params).then(({ data }) => {
if (data && data.code === 0) {
const label = data.row
this.currentLabel = {
unitId: label.unitId,
partNo: label.partNo,
batchNo: label.batchNo,
qty: label.qty
}
this.labelDialogVisible = true
this.labelCode = '' //
} else {
this.$message.error(data.msg || '标签不存在')
this.labelCode = ''
this.$refs.labelInput.focus()
}
}).catch(() => {
this.$message.error('网络错误')
this.labelCode = ''
this.$refs.labelInput.focus()
})
},
// - rqrq
confirmLabelScan() {
// - rqrq
const existIndex = this.scannedLabels.findIndex(item => item.unitId === this.currentLabel.unitId)
if (existIndex >= 0) {
//
this.scannedLabels[existIndex].qty = this.currentLabel.qty
this.$message.success('标签数量已更新')
} else {
//
this.scannedLabels.push({
unitId: this.currentLabel.unitId,
partNo: this.currentLabel.partNo,
qty: this.currentLabel.qty
})
this.$message.success('标签已添加')
}
this.labelDialogVisible = false
// - rqrq
this.$nextTick(() => {
this.$refs.labelInput.focus()
})
},
// - rqrq
showScannedList() {
this.scannedDialogVisible = true
},
// - rqrq
removeScannedLabel(index) {
this.scannedLabels.splice(index, 1)
this.$message.success('已删除')
},
// OK- rqrq
handleQuickSubmit() {
this.$confirm('一键提交将默认所有标签盘点正常,确定要提交吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.submitLoading = true
const params = {
site: this.$store.state.user.site,
countNo: this.countNo,
palletId: this.cleanPalletId,
username: this.$store.state.user.name
}
pdaQuickSubmitCount(params).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('提交成功,处理标签数:' + data.result)
this.handleReset()
} else {
this.$message.error(data.msg || '提交失败')
}
}).catch(() => {
this.$message.error('网络错误')
}).finally(() => {
this.submitLoading = false
})
}).catch(() => {})
},
// - rqrq
handleSubmitCount() {
if (this.scannedLabels.length === 0) {
this.$confirm('您没有扫描任何标签,将标记所有标签为盘亏,确定要提交吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.doSubmitCount()
}).catch(() => {})
} else {
this.$confirm('确定要提交盘点结果吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
this.doSubmitCount()
}).catch(() => {})
}
},
// - rqrq
doSubmitCount() {
this.submitLoading = true
const params = {
site: this.$store.state.user.site,
countNo: this.countNo,
palletId: this.cleanPalletId,
scannedLabels: this.scannedLabels,
username: this.$store.state.user.name
}
pdaSubmitCount(params).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('提交成功,处理标签数:' + data.result)
this.handleReset()
} else {
this.$message.error(data.msg || '提交失败')
}
}).catch(() => {
this.$message.error('网络错误')
}).finally(() => {
this.submitLoading = false
})
},
// - rqrq
handleReset() {
this.palletCode = ''
this.palletScanned = false
this.countNo = ''
this.cleanPalletId = ''
this.labelList = []
this.labelCode = ''
this.scannedLabels = []
this.$nextTick(() => {
this.$refs.palletInput.focus()
})
}
}
}
</script>
<style scoped>
/* 表格样式 - rqrq */
.detail-table {
background: white;
border-radius: 6px;
overflow: hidden;
border: 1px solid #e0e0e0;
}
.table-header,
.table-row {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #e0e0e0;
}
.table-header {
background: #f5f5f5;
font-weight: bold;
font-size: 14px;
}
.table-row {
font-size: 13px;
}
.table-row:last-child {
border-bottom: none;
}
.table-row.row-checked {
background-color: #e8f5e9;
}
.table-body-scroll {
max-height: 200px;
overflow-y: auto;
}
.col-label {
flex: 2;
text-align: center;
word-break: break-all;
}
.col-part {
flex: 1.5;
text-align: center;
word-break: break-all;
}
.col-status {
flex: 0.8;
text-align: center;
}
/* 空数据提示 - rqrq */
.empty-row {
text-align: center;
color: #999;
padding: 20px;
}
/* 按钮样式覆盖 - 使用绿色主题 - rqrq */
.action-btn {
padding: 12px 16px;
border: 1px solid #17B3A3;
background: white;
color: #17B3A3;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-block;
min-width: 60px;
text-align: center;
}
.action-btn:hover {
background: #17B3A3;
color: white;
}
.action-btn:active {
transform: scale(0.98);
}
.action-btn.primary {
background-color: #17B3A3;
border-color: #17B3A3;
color: white;
}
.action-btn.primary:hover {
background-color: #15a093;
border-color: #15a093;
}
.action-btn.warning {
background-color: #E6A23C;
border-color: #E6A23C;
color: white;
}
.action-btn.warning:hover {
background-color: #f0b757;
border-color: #f0b757;
}
.action-btn.secondary {
background-color: #f5f5f5;
border-color: #dcdfe6;
color: #606266;
}
.action-btn.secondary:hover {
background-color: #e4e7ed;
border-color: #d3d4d6;
}
.action-btn.danger {
background-color: #F56C6C;
border-color: #F56C6C;
color: white;
padding: 8px 12px;
font-size: 12px;
}
.action-btn.danger:hover {
background-color: #f78989;
border-color: #f78989;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: #ccc !important;
border-color: #ccc !important;
}
/* 弹窗底部按钮区域 - rqrq */
.dialog-footer {
text-align: center;
padding: 10px 0;
}
/* 底部操作栏 - rqrq */
.footer-bar {
padding: 16px;
background-color: white;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
/* 列表标题使用绿色 - rqrq */
.list-title {
font-size: 16px;
font-weight: bold;
color: #17b3a3;
margin: 12px 0 8px 0;
}
/* 标签信息弹框样式 - rqrq */
.label-info {
padding: 10px;
}
.label-info .info-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.label-info .info-label {
width: 80px;
color: #606266;
font-size: 14px;
}
.label-info .info-value {
flex: 1;
color: #303133;
font-size: 14px;
}
/* 已扫描列表样式 - rqrq */
.scanned-list {
max-height: 300px;
overflow-y: auto;
}
.scanned-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #EBEEF5;
}
.scanned-info {
font-size: 12px;
color: #606266;
}
.scanned-info div {
margin-bottom: 4px;
}
.empty-list {
padding: 30px;
text-align: center;
color: #909399;
}
</style>
Loading…
Cancel
Save