|
|
|
|
@ -81,6 +81,12 @@ let jsQrCanvas: HTMLCanvasElement | null = null
|
|
|
|
|
let jsQrCtx: CanvasRenderingContext2D | null = null
|
|
|
|
|
const lastScannedRaw = ref('')
|
|
|
|
|
|
|
|
|
|
/** Android 上 BarcodeDetector 对 video 帧往往解不出码;走 jsQR 更稳定 */
|
|
|
|
|
function shouldUseBarcodeDetector(): boolean {
|
|
|
|
|
if (typeof navigator === 'undefined') return true
|
|
|
|
|
return !/Android/i.test(navigator.userAgent || '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scannedToken = ref('')
|
|
|
|
|
const preview = ref<{
|
|
|
|
|
reservation: ReservationRow
|
|
|
|
|
@ -381,15 +387,44 @@ async function openCamera() {
|
|
|
|
|
}
|
|
|
|
|
lastScannedRaw.value = ''
|
|
|
|
|
cameraVisible.value = true
|
|
|
|
|
try {
|
|
|
|
|
streamRef.value = await getUserMedia({
|
|
|
|
|
video: { facingMode: 'environment' },
|
|
|
|
|
const tryConstraints: MediaStreamConstraints[] = [
|
|
|
|
|
{
|
|
|
|
|
video: {
|
|
|
|
|
facingMode: { ideal: 'environment' },
|
|
|
|
|
width: { ideal: 1280 },
|
|
|
|
|
height: { ideal: 720 },
|
|
|
|
|
},
|
|
|
|
|
audio: false,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
{ video: { facingMode: 'environment' }, audio: false },
|
|
|
|
|
{ video: true, audio: false },
|
|
|
|
|
]
|
|
|
|
|
try {
|
|
|
|
|
let stream: MediaStream | null = null
|
|
|
|
|
let lastErr: unknown
|
|
|
|
|
for (const constraints of tryConstraints) {
|
|
|
|
|
try {
|
|
|
|
|
stream = await getUserMedia(constraints)
|
|
|
|
|
break
|
|
|
|
|
} catch (e) {
|
|
|
|
|
lastErr = e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!stream) {
|
|
|
|
|
throw lastErr instanceof Error ? lastErr : new Error('getUserMedia failed')
|
|
|
|
|
}
|
|
|
|
|
streamRef.value = stream
|
|
|
|
|
await new Promise((r) => requestAnimationFrame(r))
|
|
|
|
|
if (videoRef.value) {
|
|
|
|
|
videoRef.value.srcObject = streamRef.value
|
|
|
|
|
await videoRef.value.play()
|
|
|
|
|
const el = videoRef.value
|
|
|
|
|
if (el) {
|
|
|
|
|
el.setAttribute('playsinline', 'true')
|
|
|
|
|
el.setAttribute('webkit-playsinline', 'true')
|
|
|
|
|
el.playsInline = true
|
|
|
|
|
el.muted = true
|
|
|
|
|
el.srcObject = streamRef.value
|
|
|
|
|
await el.play().catch(() => {
|
|
|
|
|
/* 部分 WebView 需用户手势后 play,轮询仍会在就绪后解码 */
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
startScanLoop()
|
|
|
|
|
} catch {
|
|
|
|
|
@ -406,7 +441,7 @@ function startScanLoop() {
|
|
|
|
|
|
|
|
|
|
if (scanTimer) window.clearInterval(scanTimer)
|
|
|
|
|
|
|
|
|
|
if (Detector) {
|
|
|
|
|
if (Detector && shouldUseBarcodeDetector()) {
|
|
|
|
|
const detector = new Detector({ formats: ['qr_code'] })
|
|
|
|
|
scanTimer = window.setInterval(async () => {
|
|
|
|
|
if (!videoRef.value) return
|
|
|
|
|
@ -435,7 +470,10 @@ function startScanLoop() {
|
|
|
|
|
}
|
|
|
|
|
const ctx = jsQrCtx
|
|
|
|
|
const canvas = jsQrCanvas
|
|
|
|
|
const maxDim = 640
|
|
|
|
|
/** Android 略提高采样分辨率,便于 jsQR 识别 */
|
|
|
|
|
const maxDim = /Android/i.test(navigator.userAgent || '') ? 960 : 640
|
|
|
|
|
let lastDw = 0
|
|
|
|
|
let lastDh = 0
|
|
|
|
|
|
|
|
|
|
scanTimer = window.setInterval(() => {
|
|
|
|
|
const video = videoRef.value
|
|
|
|
|
@ -451,8 +489,12 @@ function startScanLoop() {
|
|
|
|
|
dw = Math.floor(vw * scale)
|
|
|
|
|
dh = Math.floor(vh * scale)
|
|
|
|
|
}
|
|
|
|
|
canvas.width = dw
|
|
|
|
|
canvas.height = dh
|
|
|
|
|
if (dw !== lastDw || dh !== lastDh) {
|
|
|
|
|
canvas.width = dw
|
|
|
|
|
canvas.height = dh
|
|
|
|
|
lastDw = dw
|
|
|
|
|
lastDh = dh
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
ctx.drawImage(video, 0, 0, vw, vh, 0, 0, dw, dh)
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, dw, dh)
|
|
|
|
|
@ -548,7 +590,14 @@ onBeforeUnmount(() => {
|
|
|
|
|
@cancel="closeCamera"
|
|
|
|
|
>
|
|
|
|
|
<div class="cam-wrap">
|
|
|
|
|
<video ref="videoRef" class="cam-video" muted playsinline />
|
|
|
|
|
<video
|
|
|
|
|
ref="videoRef"
|
|
|
|
|
class="cam-video"
|
|
|
|
|
muted
|
|
|
|
|
playsinline
|
|
|
|
|
autoplay
|
|
|
|
|
webkit-playsinline="true"
|
|
|
|
|
/>
|
|
|
|
|
<p class="cam-tip">请将二维码置于取景框中央</p>
|
|
|
|
|
<a-button long @click="closeCamera">取消</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
|