feat(scan): 扫码时检测单据失效,避免白扫一场

场景
----
库管正在扫码,这张单被撤回/驳回/执行了。若不检测,库管会一直扫到
提交时才发现单据已作废(后端会拒绝),前面的工作白做。

实现
----
新增 checkRequestStillValid():重新拉一次「已通过(status=1)」列表,
看当前单据是否还在其中。不在 → 已被撤回/驳回/执行,立即弹窗告知并
清空界面(草稿后端也已同步清除)。复用现有列表接口,无需新增端点。

触发时机(覆盖两类场景):
  · 页面重新可见时(visibilitychange)—— PDA 熄屏唤醒、切回标签页
  · 每 60 秒一次 —— 库管一直在页面上扫、没切走

三点取舍:
  · 仅购物车非空时检测(正在作业才打扰),空清单不发请求;
  · 60 秒轮询而非实时推送 —— 引入 WebSocket 长连接成本过高,
    此处最多浪费 1 分钟扫码,相比"扫完几十分钟才发现"已足够;
  · 检测失败不阻断作业(仅 console.warn)—— 后端提交时仍会做最终校验,
    前端检测是为了早发现,不是安全防线。

出库 create.vue 与借库 borrow.vue 两页同款实现。
This commit is contained in:
yueli
2026-09-11 09:06:51 +08:00
parent 33acdc758c
commit ce767e39ac
2 changed files with 112 additions and 3 deletions

View File

@ -1133,6 +1133,7 @@ const handleSignCancel = () => { showSignatureDialog.value = false }
onUnmounted(() => {
if (signaturePreviewUrl.value) URL.revokeObjectURL(signaturePreviewUrl.value)
if (draftTimer) clearTimeout(draftTimer)
// 其余清理(定时检测、事件监听)见文件末尾的统一 onUnmounted
})
// ★ 离开页面前:立即存草稿(防抖中未落盘的内容会丢)
@ -1181,8 +1182,65 @@ const handleBeforeUnload = () => {
} catch (e) { /* 尽力而为,失败不影响 */ }
}
onMounted(() => { window.addEventListener('beforeunload', handleBeforeUnload) })
onUnmounted(() => { window.removeEventListener('beforeunload', handleBeforeUnload) })
// ============================================================================
// ★ 单据失效检测
//
// 场景:库管正在扫码,管理员/申请人把这张单撤回了(或已执行完)。
// 若不检测,库管会一直扫到提交时才发现单据已作废 —— 白扫一场。
//
// 检测方式:重新拉一次「已通过(status=1)」列表,看当前单据是否还在其中。
// 不在 → 说明已被撤回/驳回/执行,立即告知并清空界面。
// 复用现有列表接口,无需新增端点。
//
// 触发时机:
// · 页面重新可见时PDA 熄屏唤醒、切回浏览器)
// · 每 60 秒一次(仅在购物车非空、即正在作业时)
// ============================================================================
const checkRequestStillValid = async () => {
const rid = activeRequestId.value
if (!rid || cartItems.value.length === 0) return // 没在作业,不打扰
try {
const res: any = await getApprovalRequestList({ status: 1, page: 1, pageSize: 100 })
const stillValid = (res.data?.items || []).some((r: any) => r.id === rid)
if (stillValid) return
// 单据已不在「已通过」列表中 → 失效
const no = selectedRequest.value?.request_no || rid
cartItems.value = []
selectedRequest.value = null
activeRequestId.value = null
ElMessageBox.alert(
`申请单【${no}】已被撤回或已执行,不能再继续出库。\n\n` +
`本次已扫的内容已清空(草稿也已随之清除)。\n` +
`如需出库,请重新提交申请。`,
'⚠️ 申请单已失效',
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
} catch (e) {
// 网络异常时不打断作业 —— 后端在提交时仍会做最终校验
console.warn('单据状态检测失败', e)
}
}
// 页面重新可见时检测PDA 熄屏唤醒、切回标签页)
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') checkRequestStillValid()
}
let validCheckTimer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
document.addEventListener('visibilitychange', onVisibilityChange)
validCheckTimer = setInterval(checkRequestStillValid, 60000)
})
onUnmounted(() => {
if (validCheckTimer) clearInterval(validCheckTimer)
document.removeEventListener('visibilitychange', onVisibilityChange)
window.removeEventListener('beforeunload', handleBeforeUnload)
})
</script>
<style scoped>

View File

@ -1034,6 +1034,7 @@ onUnmounted(() => {
if (signaturePreviewUrl.value) URL.revokeObjectURL(signaturePreviewUrl.value)
if (draftTimer) clearTimeout(draftTimer)
window.removeEventListener('beforeunload', handleBeforeUnload)
// 其余清理(定时检测、事件监听)见文件末尾的统一 onUnmounted
})
// ★ 离开页面前立即存草稿(防抖中未落盘的内容会丢)
@ -1071,7 +1072,57 @@ const handleBeforeUnload = () => {
} catch (e) { /* 尽力而为 */ }
}
onMounted(() => { window.addEventListener('beforeunload', handleBeforeUnload) })
// ============================================================================
// ★ 单据失效检测(与扫码出库页同款)
//
// 场景:库管正在扫码,这张单被撤回/驳回了。若不检测,库管会一直扫到
// 提交时才发现单据已作废 —— 白扫一场。
//
// 检测方式:重新拉一次「已通过(status=1)」列表,看当前单据是否还在其中。
// 触发页面重新可见时PDA 熄屏唤醒)+ 每 60 秒(仅购物车非空时)。
// ============================================================================
const checkRequestStillValid = async () => {
const rid = activeApprovalId.value
if (!rid || cartItems.value.length === 0) return // 没在作业,不打扰
try {
const res: any = await getBorrowApprovalList({ status: 1, page: 1, limit: 100 })
const stillValid = (res.data?.items || []).some((r: any) => r.id === rid)
if (stillValid) return
const no = selectedApproval.value?.request_no || rid
cartItems.value = []
selectedApprovalId.value = null
activeApprovalId.value = null
ElMessageBox.alert(
`申请单【${no}】已被撤回或已执行,不能再继续借出。\n\n` +
`本次已扫的内容已清空(草稿也已随之清除)。\n` +
`如需借出,请重新提交申请。`,
'⚠️ 申请单已失效',
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
} catch (e) {
// 网络异常时不打断作业 —— 后端在提交时仍会做最终校验
console.warn('单据状态检测失败', e)
}
}
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') checkRequestStillValid()
}
let validCheckTimer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
document.addEventListener('visibilitychange', onVisibilityChange)
validCheckTimer = setInterval(checkRequestStillValid, 60000)
})
onUnmounted(() => {
if (validCheckTimer) clearInterval(validCheckTimer)
document.removeEventListener('visibilitychange', onVisibilityChange)
})
</script>
<style scoped>