diff --git a/inventory-web/src/components/QrScanner/index.vue b/inventory-web/src/components/QrScanner/index.vue index 9987ecf..0f8b633 100644 --- a/inventory-web/src/components/QrScanner/index.vue +++ b/inventory-web/src/components/QrScanner/index.vue @@ -12,7 +12,7 @@
- 扫描成功,3秒后继续... + 扫描成功,2秒后继续...
@@ -50,6 +50,43 @@ const zoomMin = ref(1) const zoomMax = ref(5) const currentZoom = ref(1) +// 音频上下文 +let audioCtx: AudioContext | null = null; + +// 提示音播放函数 +const playBeep = () => { + try { + const AudioContext = window.AudioContext || (window as any).webkitAudioContext; + if (!AudioContext) return; + + if (!audioCtx) { + audioCtx = new AudioContext(); + } + + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + + const oscillator = audioCtx.createOscillator(); + const gainNode = audioCtx.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioCtx.destination); + + // 三角波,清脆响亮 + oscillator.type = 'triangle'; + oscillator.frequency.value = 1500; + + gainNode.gain.setValueAtTime(1.0, audioCtx.currentTime); + + oscillator.start(); + oscillator.stop(audioCtx.currentTime + 0.1); + + } catch (e) { + console.error("播放提示音失败:", e); + } +}; + const startScanning = async () => { try { html5QrCode = new Html5Qrcode(scannerElementId, { @@ -63,16 +100,16 @@ const startScanning = async () => { const config = { fps: 20, - // ★★★ 核心修改点 2:移除了 qrbox 属性 ★★★ - // 移除后,库默认会对每一帧的“全画面”进行解析,不再局限于中间区域 - // qrbox: { width: 300, height: 100 }, - disableFlip: false, videoConstraints: { facingMode: "environment", - // 保持高分辨率以支持微小条码 - width: { min: 1280, ideal: 3840, max: 3840 }, - height: { min: 720, ideal: 2160, max: 2160 }, + // ★★★ 核心修改:设置为 2K (QHD) 分辨率 ★★★ + // min: 1280x720 (保证低端机能启动) + // ideal: 2560x1440 (2K QHD,清晰度与性能的平衡点) + width: { min: 1280, ideal: 2560, max: 3840 }, + height: { min: 720, ideal: 1440, max: 2160 }, + // 16:9 的比例 + aspectRatio: { ideal: 1.7777777778 }, focusMode: "continuous", advanced: [{ focusMode: "macro" }, { zoom: 2.0 }] } @@ -84,14 +121,18 @@ const startScanning = async () => { (decodedText) => { if (isPaused.value) return console.log(`Scan: ${decodedText}`) + isPaused.value = true + + playBeep(); + emit('decode', decodedText) if (navigator.vibrate) navigator.vibrate(200); setTimeout(() => { isPaused.value = false - }, 3000) + }, 2000) }, (errorMessage) => { // ignore @@ -103,12 +144,14 @@ const startScanning = async () => { } catch (err: any) { let msg = '无法启动摄像头' console.error("Scanner Error:", err) + if (err.name === 'OverconstrainedError') { + msg = '摄像头不支持 2K 分辨率,请尝试降低配置' + } errorMsg.value = msg emit('error', msg) } } -// 检测硬件变焦能力 const checkZoomCapability = () => { if (!html5QrCode) return @@ -130,7 +173,6 @@ const checkZoomCapability = () => { } } -// 处理滑块拖动 const handleZoom = () => { if (!html5QrCode) return @@ -154,9 +196,19 @@ const stopScanning = async () => { console.error("Stop failed", e) } } + + if (audioCtx) { + audioCtx.close(); + audioCtx = null; + } } onMounted(() => { + try { + const AudioContext = window.AudioContext || (window as any).webkitAudioContext; + if (AudioContext) audioCtx = new AudioContext(); + } catch(e) {} + setTimeout(() => { startScanning() }, 500) @@ -178,7 +230,6 @@ onUnmounted(() => { justify-content: center; align-items: center; overflow: hidden; - /* 如果是全屏模式,这里不需要圆角,或者保持圆角视你的UI设计而定 */ border-radius: 0; } @@ -214,14 +265,12 @@ onUnmounted(() => { z-index: 20; } -/* --- ★ 修改点 3:视觉层 CSS 更新 --- */ .focus-tip { position: absolute; top: 0; left: 0; width: 100%; height: 100%; - /* 移除了 border 和 box-shadow,不再显示红框和黑色遮罩 */ pointer-events: none; z-index: 10; display: flex; @@ -230,23 +279,21 @@ onUnmounted(() => { } .focus-tip.success { - background: rgba(103, 194, 58, 0.2); /* 成功时全屏微微泛绿 */ + background: rgba(103, 194, 58, 0.2); } -/* 扫描线改为全屏宽度 */ .scan-line { width: 100%; height: 2px; background: rgba(255, 0, 0, 0.5); box-shadow: 0 0 4px rgba(255, 0, 0, 0.8); position: absolute; - /* 动画范围从 10% 到 90% */ animation: scan-move 2.5s infinite linear; } .scan-text { position: absolute; - bottom: 150px; /* 调整文字位置 */ + bottom: 150px; color: rgba(255, 255, 255, 0.8); font-size: 14px; text-shadow: 0 1px 3px rgba(0,0,0,0.8); @@ -275,7 +322,6 @@ onUnmounted(() => { 100% { top: 100%; opacity: 0; } } -/* 变焦控制器 */ .zoom-control { position: absolute; bottom: 80px; diff --git a/inventory-web/src/layout/index.vue b/inventory-web/src/layout/index.vue index e2909e7..a5d1950 100644 --- a/inventory-web/src/layout/index.vue +++ b/inventory-web/src/layout/index.vue @@ -22,7 +22,7 @@ import AppMain from './components/AppMain.vue' } .sidebar-container { - width: 210px; /* 固定侧边栏宽度 */ + width: 180px; /* 固定侧边栏宽度 */ height: 100%; background-color: #304156; /* 侧边栏背景色 */ flex-shrink: 0; /* 防止被挤压 */ @@ -37,7 +37,7 @@ import AppMain from './components/AppMain.vue' flex-direction: column; overflow-y: auto; /* 关键:页面内容过多时,只在右侧区域滚动 */ background-color: #f0f2f5; /* 右侧灰色背景,让白色卡片更明显 */ - padding: 20px; /* 给内部页面留出边距 */ + padding: 10px; /* 给内部页面留出边距 */ box-sizing: border-box; } \ No newline at end of file