Browse Source
feat(inventory): 新增立库手工盘点功能
feat(inventory): 新增立库手工盘点功能
- 在路由中添加手工盘点页面路径 /manualCount - 在主界面菜单中增加"立库手工盘点"入口 - 创建手工盘点页面组件 manualCount.vue - 实现栈板扫描和标签扫描功能 - 支持一键提交和手动提交盘点结果 - 添加已扫描标签查看和删除功能 - 集成PDA扫描相关API接口 - 添加确认标签信息弹窗和已扫描列表弹窗 - 实现完整的盘点流程交互逻辑 - 添加相应的UI样式和组件布局master
4 changed files with 691 additions and 1 deletions
-
16src/api/check/physicalInventory.js
-
4src/router/index.js
-
6src/views/main.vue
-
666src/views/modules/inventory/manualCount.vue
@ -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) |
|||
|
|||
@ -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> |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue