refactor: upgrade stocktake scanner to fullscreen and implement scan-input-resume loop
This commit is contained in:
@ -49,14 +49,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="scanner-container">
|
<div class="scan-section">
|
||||||
<div v-if="!showList && !showQtyDialog" class="camera-active-box">
|
<div v-if="hasPermission" class="camera-placeholder" @click="openFullscreenScanner">
|
||||||
<QrScanner @decode="onScanSuccess" />
|
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||||
<div class="scan-overlay-tip">扫描条码</div>
|
<span class="text">点击开启全屏扫码</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="camera-paused-box" @click="closeOverlays">
|
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||||
<el-icon :size="50" color="#909399"><EditPen /></el-icon>
|
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||||
<p>操作中...<br>点击返回扫描</p>
|
<span class="text">无扫码权限</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-box">
|
||||||
|
<el-input
|
||||||
|
v-model="barcodeInput"
|
||||||
|
placeholder="扫描或输入条码回车"
|
||||||
|
@keyup.enter="handleManualInput"
|
||||||
|
clearable
|
||||||
|
ref="barcodeRef"
|
||||||
|
size="large"
|
||||||
|
:disabled="!hasPermission"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><EditPen /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="handleManualInput" :disabled="!hasPermission">添加</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -210,6 +229,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- 全屏扫码 Overlay -->
|
||||||
|
<div v-if="showCamera" class="fullscreen-scanner-overlay">
|
||||||
|
<div class="scanner-header">
|
||||||
|
<el-button circle icon="Close" @click="closeScanner" class="close-btn" />
|
||||||
|
<span class="scanner-title">扫码模式</span>
|
||||||
|
<div class="scanner-placeholder"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scanner-body">
|
||||||
|
<QrScanner @decode="onScanSuccess" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scanner-footer">
|
||||||
|
<p>请将条码/二维码放入镜头范围</p>
|
||||||
|
<p v-if="stats.scanned > 0" class="current-count">已盘点: {{ stats.scanned }} 项</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="showFinishDialog" title="📊 盘点结算" width="90%" align-center :close-on-click-modal="false" class="preview-dialog">
|
<el-dialog v-model="showFinishDialog" title="📊 盘点结算" width="90%" align-center :close-on-click-modal="false" class="preview-dialog">
|
||||||
<div class="report-summary">
|
<div class="report-summary">
|
||||||
<div class="summary-row"><span>截止时间:</span><span>{{ new Date().toLocaleString() }}</span></div>
|
<div class="summary-row"><span>截止时间:</span><span>{{ new Date().toLocaleString() }}</span></div>
|
||||||
@ -253,7 +290,7 @@ import { ref, computed, onMounted, nextTick } from 'vue'
|
|||||||
import { getAllStock } from '@/api/inbound/stock'
|
import { getAllStock } from '@/api/inbound/stock'
|
||||||
import QrScanner from '@/components/QrScanner/index.vue'
|
import QrScanner from '@/components/QrScanner/index.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen } from '@element-plus/icons-vue'
|
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close } from '@element-plus/icons-vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
@ -290,6 +327,12 @@ const isSessionActive = ref(false)
|
|||||||
const serverDraftCount = ref(0)
|
const serverDraftCount = ref(0)
|
||||||
const syncStatus = ref<'success' | 'syncing' | 'failed'>('success')
|
const syncStatus = ref<'success' | 'syncing' | 'failed'>('success')
|
||||||
|
|
||||||
|
// 新增:扫码相关状态
|
||||||
|
const showCamera = ref(false)
|
||||||
|
const barcodeInput = ref('')
|
||||||
|
const barcodeRef = ref()
|
||||||
|
const hasPermission = userStore.hasPermission('inventory_stocktake:operation')
|
||||||
|
|
||||||
const showList = ref(false)
|
const showList = ref(false)
|
||||||
const showFinishDialog = ref(false)
|
const showFinishDialog = ref(false)
|
||||||
const showQtyDialog = ref(false)
|
const showQtyDialog = ref(false)
|
||||||
@ -431,7 +474,7 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onScanSuccess = (code: string) => {
|
const onScanSuccess = (code: string) => {
|
||||||
if (!code) return
|
if (!code || loading.value) return
|
||||||
const trimCode = code.trim()
|
const trimCode = code.trim()
|
||||||
|
|
||||||
if (!/^[A-Za-z0-9\-\.]+$/.test(trimCode)) {
|
if (!/^[A-Za-z0-9\-\.]+$/.test(trimCode)) {
|
||||||
@ -439,33 +482,65 @@ const onScanSuccess = (code: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trimCode.length < 3) {
|
||||||
|
ElMessage.warning('扫描结果过短,请对准重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const item = allData.value.find(i => i.uuid === trimCode || i.bar_code === trimCode)
|
const item = allData.value.find(i => i.uuid === trimCode || i.bar_code === trimCode)
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
if (navigator.vibrate) navigator.vibrate(100)
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
|
||||||
const isBatchMultiple = (item.batch_no && item.batch_no.length > 0) && (item.qty_stock > 1);
|
// ★★★ 核心修改:扫码成功后立即关闭全屏扫码,弹出填数对话框
|
||||||
|
showCamera.value = false
|
||||||
|
|
||||||
if (isBatchMultiple) {
|
// 无论是否多批次,都弹出对话框让用户确认数量
|
||||||
openQtyDialog(item)
|
openQtyDialog(item)
|
||||||
} else {
|
|
||||||
if (item.scanned) {
|
|
||||||
openQtyDialog(item)
|
|
||||||
} else {
|
|
||||||
updateAndSync(item, 1)
|
|
||||||
ElMessage.success(`自动确认: ${item.name} +1`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(`不在库条码: ${trimCode}`)
|
ElMessage.error(`不在库条码: ${trimCode}`)
|
||||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开启全屏扫码
|
||||||
|
const openFullscreenScanner = () => {
|
||||||
|
if (!hasPermission) {
|
||||||
|
ElMessage.warning('无扫码权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showCamera.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭全屏扫码
|
||||||
|
const closeScanner = () => {
|
||||||
|
showCamera.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动输入条码
|
||||||
|
const handleManualInput = async () => {
|
||||||
|
const code = barcodeInput.value.trim()
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const item = allData.value.find(i => i.uuid === code || i.bar_code === code)
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
openQtyDialog(item)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`不在库条码: ${code}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
barcodeInput.value = ''
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openQtyDialog = (item: StockItem) => {
|
const openQtyDialog = (item: StockItem) => {
|
||||||
currentItem.value = item
|
currentItem.value = item
|
||||||
inputQty.value = item.scanned ? item.qty_actual : undefined
|
inputQty.value = item.scanned ? item.qty_actual : 1
|
||||||
showQtyDialog.value = true
|
showQtyDialog.value = true
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@ -481,6 +556,13 @@ const handleManualConfirm = () => {
|
|||||||
updateAndSync(currentItem.value, val)
|
updateAndSync(currentItem.value, val)
|
||||||
showQtyDialog.value = false
|
showQtyDialog.value = false
|
||||||
ElMessage.success(`已记录实盘: ${val}`)
|
ElMessage.success(`已记录实盘: ${val}`)
|
||||||
|
|
||||||
|
// ★★★ 核心修改:确认数量后自动重新打开全屏扫码,实现无缝闭环
|
||||||
|
nextTick(() => {
|
||||||
|
if (hasPermission) {
|
||||||
|
showCamera.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAndSync = async (item: StockItem, quantity: number) => {
|
const updateAndSync = async (item: StockItem, quantity: number) => {
|
||||||
@ -761,4 +843,132 @@ const finishStocktake = async () => {
|
|||||||
font-size: 24px !important;
|
font-size: 24px !important;
|
||||||
height: 58px !important;
|
height: 58px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ★★★ 新增:扫码区域样式 ★★★ */
|
||||||
|
.scan-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 180px;
|
||||||
|
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
||||||
|
border: 2px dashed #409EFF;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-placeholder:hover {
|
||||||
|
background: linear-gradient(135deg, #d9ecff 0%, #b3d8ff 100%);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-placeholder .text {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #409EFF;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box :deep(.el-input__wrapper) {
|
||||||
|
box-shadow: 0 0 0 1px #dcdfe6 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box :deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px #c0c4cc inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box :deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px #409EFF inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ★★★ 全屏扫码 Overlay 样式 ★★★ */
|
||||||
|
.fullscreen-scanner-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #000;
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-placeholder {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-footer p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-count {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-placeholder {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user