盘库操作初设计
This commit is contained in:
219
inventory-web/src/components/QrScanner/index.vue
Normal file
219
inventory-web/src/components/QrScanner/index.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="qr-scanner-container">
|
||||
<div id="qr-reader" class="scanner-box"></div>
|
||||
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
|
||||
<div class="focus-tip" v-if="!errorMsg">
|
||||
<div class="scan-line"></div>
|
||||
<div class="scan-text">将条码对准取景框</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode'
|
||||
|
||||
const emit = defineEmits(['decode', 'error'])
|
||||
const errorMsg = ref('')
|
||||
let html5QrCode: Html5Qrcode | null = null
|
||||
const scannerElementId = "qr-reader"
|
||||
|
||||
const startScanning = async () => {
|
||||
try {
|
||||
// 1. 实例化
|
||||
html5QrCode = new Html5Qrcode(scannerElementId, {
|
||||
useBarCodeDetectorIfSupported: true,
|
||||
formatsToSupport: [
|
||||
Html5QrcodeSupportedFormats.CODE_128,
|
||||
Html5QrcodeSupportedFormats.QR_CODE
|
||||
],
|
||||
verbose: false
|
||||
})
|
||||
|
||||
// 2. 启动配置
|
||||
const config = {
|
||||
fps: 25,
|
||||
qrbox: { width: 250, height: 100 }, // 扫描区域设置
|
||||
// ★★★ 修复点1:移除 aspectRatio,让画面自适应容器长宽比 ★★★
|
||||
// aspectRatio: 1.0,
|
||||
disableFlip: false,
|
||||
videoConstraints: {
|
||||
facingMode: "environment",
|
||||
// 限制分辨率,保证速度
|
||||
width: { min: 640, ideal: 1280, max: 1920 },
|
||||
height: { min: 480, ideal: 720, max: 1080 },
|
||||
focusMode: "continuous"
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 启动
|
||||
await html5QrCode.start(
|
||||
{ facingMode: "environment" },
|
||||
config,
|
||||
(decodedText) => {
|
||||
console.log(`Scan: ${decodedText}`)
|
||||
emit('decode', decodedText)
|
||||
},
|
||||
(errorMessage) => {
|
||||
// ignore
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
let msg = '无法启动摄像头'
|
||||
const errStr = err.toString()
|
||||
if (errStr.includes('Permission')) msg = '请允许摄像头权限'
|
||||
else if (errStr.includes('Secure')) msg = '需要 HTTPS 或 localhost'
|
||||
else if (errStr.includes('NotFound')) msg = '未检测到摄像头'
|
||||
|
||||
console.error("Scanner Error:", err)
|
||||
errorMsg.value = msg
|
||||
emit('error', msg)
|
||||
}
|
||||
}
|
||||
|
||||
const stopScanning = async () => {
|
||||
if (html5QrCode) {
|
||||
try {
|
||||
if (html5QrCode.isScanning) {
|
||||
await html5QrCode.stop()
|
||||
}
|
||||
html5QrCode.clear()
|
||||
} catch (e) {
|
||||
console.error("Stop failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
startScanning()
|
||||
}, 300)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopScanning()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qr-scanner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden; /* 关键:裁剪多余画面 */
|
||||
border-radius: 12px; /* 保持圆角 */
|
||||
}
|
||||
|
||||
.scanner-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ★★★ 修复点2:强制 Video 元素填满容器 ★★★ */
|
||||
:deep(#qr-reader) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(#qr-reader video) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important; /* 关键:保持比例铺满,裁剪多余部分 */
|
||||
border-radius: 12px;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 隐藏 html5-qrcode 可能自带的遮罩层,我们用自己的 */
|
||||
:deep(#qr-reader__scan_region) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 自定义错误提示 */
|
||||
.error-msg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background: rgba(245, 108, 108, 0.85);
|
||||
padding: 15px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* --- 视觉辅助线 --- */
|
||||
.focus-tip {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 250px;
|
||||
height: 120px;
|
||||
/* 使用半透明黑边框模拟遮罩效果,突出中间区域 */
|
||||
box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 四个角的装饰 */
|
||||
.focus-tip::before,
|
||||
.focus-tip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-color: #409EFF;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
.focus-tip::before {
|
||||
top: -1px; left: -1px;
|
||||
border-top-width: 3px;
|
||||
border-left-width: 3px;
|
||||
}
|
||||
.focus-tip::after {
|
||||
bottom: -1px; right: -1px;
|
||||
border-bottom-width: 3px;
|
||||
border-right-width: 3px;
|
||||
}
|
||||
|
||||
/* 红色扫描线动画 */
|
||||
.scan-line {
|
||||
width: 90%;
|
||||
height: 2px;
|
||||
background: #ff0000;
|
||||
box-shadow: 0 0 4px #ff0000;
|
||||
animation: scan-move 2s infinite linear;
|
||||
}
|
||||
|
||||
.scan-text {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes scan-move {
|
||||
0% { transform: translateY(-40px); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(40px); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user