feat(scan): 出库/借库扫码进度显示 + 重复扫码确认

- 出库 create.vue + 借库 borrow.vue 均增加扫码进度条
  显示: 已扫/总数 种 + 已扫/总数 件 + 进度百分比
- 重复扫码时不再直接 +1,改为弹窗确认「确认 +1 / 取消」
  防止手滑重复扫或误操作
- 扫码进度条选中审批单后显示
This commit is contained in:
yueli
2026-08-31 14:35:26 +08:00
parent 22c8ce55eb
commit e492000c3e
2 changed files with 103 additions and 0 deletions

View File

@ -80,6 +80,20 @@
<div class="scan-section">
<!-- ★ 扫码进度条 -->
<div v-if="selectedRequest && scanTotalQty > 0" class="scan-progress">
<div class="progress-info">
<span>扫码进度</span>
<el-tag type="success" size="small">{{ scanScannedTypes }}/{{ scanTotalTypes }} 种</el-tag>
<el-tag type="primary" size="small">{{ scanScannedQty }}/{{ scanTotalQty }} 件</el-tag>
</div>
<el-progress
:percentage="scanTotalQty > 0 ? Math.min(100, Math.round(scanScannedQty / scanTotalQty * 100)) : 0"
:stroke-width="8"
:color="scanScannedQty >= scanTotalQty ? '#67C23A' : '#409EFF'"
/>
</div>
<template v-if="!selectedRequestId">
<div class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
@ -357,6 +371,17 @@ const selectedRequestId = computed({
const plannedItems = computed(() => selectedRequest.value?.items ?? [])
// ★ 扫码进度:总需扫数(计划数量总和)/ 已扫数(购物车数量总和)
const scanTotalQty = computed(() =>
plannedItems.value.reduce((sum, it) => sum + (Number(it.quantity) || 0), 0)
)
const scanScannedQty = computed(() =>
cartItems.value.reduce((sum, it) => sum + (Number(it.out_quantity) || 0), 0)
)
// 已扫物品种类数 / 计划物品种类数
const scanScannedTypes = computed(() => cartItems.value.length)
const scanTotalTypes = computed(() => plannedItems.value.length)
// ★ 加载已审批通过的申请单
const loadApprovalRequests = async () => {
requestsLoading.value = true
@ -534,6 +559,17 @@ const handleManualInput = async () => {
const maxQty = parseFloat(item.available_quantity)
if (item.out_quantity < maxQty) {
// ★ 重复扫码:弹窗确认是否 +1,防止手滑重复扫
try {
await ElMessageBox.confirm(
`【${item.name} × ${item.spec_model}】已在清单中(当前已扫 ${item.out_quantity} 个)。\n\n确认再 +1 吗?`,
'重复扫码确认',
{ confirmButtonText: '确认 +1', cancelButtonText: '取消', type: 'warning' }
)
} catch (e) {
barcodeInput.value = ''
return // 用户取消,不加
}
item.out_quantity++
ElMessage.success(`数量+1 (当前: ${item.out_quantity})`)
if (navigator.vibrate) navigator.vibrate(50)
@ -837,6 +873,21 @@ onUnmounted(() => {
/* 扫码区(卡片内触发器) */
.scan-section { margin-bottom: 20px; }
.scan-progress {
background: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 12px;
}
.progress-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
}
.camera-placeholder {
height: 120px; background: #f5f7fa; border: 1px dashed #dcdfe6; border-radius: 8px;
display: flex; flex-direction: column; justify-content: center; align-items: center;

View File

@ -64,6 +64,21 @@
</div>
<div class="scan-section">
<!-- ★ 扫码进度条 -->
<div v-if="selectedApproval && scanTotalQty > 0" class="scan-progress">
<div class="progress-info">
<span>扫码进度</span>
<el-tag type="success" size="small">{{ scanScannedTypes }}/{{ scanTotalTypes }} 种</el-tag>
<el-tag type="primary" size="small">{{ scanScannedQty }}/{{ scanTotalQty }} 件</el-tag>
</div>
<el-progress
:percentage="scanTotalQty > 0 ? Math.min(100, Math.round(scanScannedQty / scanTotalQty * 100)) : 0"
:stroke-width="8"
:color="scanScannedQty >= scanTotalQty ? '#67C23A' : '#409EFF'"
/>
</div>
<template v-if="!selectedApprovalId">
<div class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
@ -314,6 +329,17 @@ const selectedApproval = computed(() =>
const plannedItems = computed(() => selectedApproval.value?.items ?? [])
// ★ 扫码进度:总需扫数(计划数量总和)/ 已扫数(购物车数量总和)
const scanTotalQty = computed(() =>
plannedItems.value.reduce((sum, it) => sum + (Number(it.quantity) || 0), 0)
)
const scanScannedQty = computed(() =>
cartItems.value.reduce((sum, it) => sum + (Number(it.out_quantity) || 0), 0)
)
// 已扫物品种类数 / 计划物品种类数
const scanScannedTypes = computed(() => cartItems.value.length)
const scanTotalTypes = computed(() => plannedItems.value.length)
// ★ 加载已通过审批的借库申请单列表
const loadApprovalRequests = async () => {
requestsLoading.value = true
@ -456,6 +482,17 @@ const handleManualInput = async () => {
const maxQty = parseFloat(item.available_quantity)
if (item.out_quantity < maxQty) {
// ★ 重复扫码:弹窗确认是否 +1,防止手滑重复扫
try {
await ElMessageBox.confirm(
`【${item.name} × ${item.spec_model}】已在清单中(当前已扫 ${item.out_quantity} 个)。\n\n确认再 +1 吗?`,
'重复扫码确认',
{ confirmButtonText: '确认 +1', cancelButtonText: '取消', type: 'warning' }
)
} catch (e) {
barcodeInput.value = ''
return // 用户取消,不加
}
item.out_quantity++
ElMessage.success(`数量+1 (当前: ${item.out_quantity})`)
if (navigator.vibrate) navigator.vibrate(50)
@ -721,6 +758,21 @@ onUnmounted(() => {
/* 扫码区 */
.scan-section { margin-bottom: 20px; }
.scan-progress {
background: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 12px;
}
.progress-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
}
.camera-placeholder {
height: 120px; background: #f5f7fa; border: 1px dashed #dcdfe6; border-radius: 8px;
display: flex; flex-direction: column; justify-content: center; align-items: center;