<template>
|
<div v-if="show" class="qr-scanner">
|
<div class="qr-header">
|
<i class="iconfont icon-dituweizhixinxi_chahao" @click="close"></i>
|
<div class="title">扫一扫</div>
|
</div>
|
<div class="qr-video-box">
|
<video ref="videoRef" autoplay playsinline muted class="qr-video"></video>
|
<!-- 四个角标 -->
|
<span class="corner tl"></span>
|
<span class="corner tr"></span>
|
<span class="corner bl"></span>
|
<span class="corner br"></span>
|
|
<!-- 扫描线 -->
|
<span class="scan-line"></span>
|
</div>
|
<!-- <p v-if="code">识别结果:{{ code }}</p>
|
<button @click="startScan">开始扫描</button>
|
<button @click="stopScan" v-if="isScanning">停止扫描</button> -->
|
</div>
|
</template>
|
|
<script lang="ts">
|
import { ref, onBeforeUnmount } from "vue";
|
import { BrowserQRCodeReader, IScannerControls } from "@zxing/browser";
|
import { ElMessage } from "element-plus";
|
|
export default {
|
name: "QrScanner",
|
emits: ["scan"],
|
setup(props, { emit }) {
|
const show = ref(false);
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
const code = ref("");
|
const isScanning = ref(false);
|
let codeReader: BrowserQRCodeReader | null = null;
|
let controls: IScannerControls | null = null;
|
|
const startScan = async () => {
|
if (isScanning.value) return;
|
isScanning.value = true;
|
|
codeReader = new BrowserQRCodeReader();
|
|
try {
|
const devices = await BrowserQRCodeReader.listVideoInputDevices();
|
if (devices.length === 0) {
|
console.error("扫码失败:", "未找到摄像头");
|
ElMessage({ message: "未找到摄像头", type: "warning" });
|
return;
|
}
|
|
// 选择后置摄像头(mobile label 中常带 'back' 或 'rear')
|
let rearCamera = devices.find((d) => /back|rear|后/i.test(d.label));
|
|
const selectedDeviceId = rearCamera
|
? rearCamera.deviceId
|
: devices[0].deviceId;
|
|
controls = await codeReader.decodeFromVideoDevice(
|
selectedDeviceId,
|
videoRef.value!,
|
(res) => {
|
if (res) {
|
code.value = res.getText();
|
console.log("code: ", code.value);
|
emit("scan", { code: code.value, success: true });
|
// stopScan(); // 识别成功后停止扫描
|
close();
|
}
|
}
|
);
|
} catch (err) {
|
console.error("扫码失败:", err);
|
ElMessage({
|
message: "无法访问摄像头,请检查权限或使用 HTTPS",
|
type: "warning",
|
});
|
}
|
};
|
|
const stopScan = () => {
|
isScanning.value = false;
|
controls?.stop();
|
controls = null;
|
};
|
|
const close = () => {
|
stopScan();
|
show.value = false;
|
emit("scan", { code: "", success: false });
|
};
|
|
const open = () => {
|
show.value = true;
|
startScan();
|
};
|
|
onBeforeUnmount(() => {
|
stopScan();
|
});
|
|
return {
|
show,
|
videoRef,
|
code,
|
isScanning,
|
startScan,
|
stopScan,
|
close,
|
open,
|
};
|
},
|
};
|
</script>
|
|
<style lang="less" scoped>
|
.qr-scanner {
|
position: fixed;
|
width: 100vw;
|
height: 100vh;
|
top: 0;
|
left: 0;
|
z-index: 9999;
|
background: rgba(0, 0, 0, 0.8);
|
.qr-header {
|
position: relative;
|
padding: 8px 5px;
|
color: #fff;
|
.title {
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translateX(-50%) translateY(-50%);
|
font-size: 6px;
|
font-weight: bold;
|
}
|
.iconfont {
|
position: absolute;
|
left: 5px;
|
top: 50%;
|
transform: translateY(-50%);
|
font-size: 9px;
|
}
|
}
|
.qr-video-box {
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
width: 130px;
|
height: 130px;
|
background: rgba(0, 0, 0, 0);
|
.qr-video {
|
width: 100%;
|
height: 100%;
|
object-fit: cover;
|
}
|
/* 角标通用样式 */
|
.corner {
|
position: absolute;
|
width: 26px; // 角标边长
|
height: 26px;
|
border: 3px solid #409eff; // 角标颜色/粗细
|
}
|
.corner.tl {
|
top: 0;
|
left: 0;
|
border-right: none;
|
border-bottom: none;
|
}
|
.corner.tr {
|
top: 0;
|
right: 0;
|
border-left: none;
|
border-bottom: none;
|
}
|
.corner.bl {
|
bottom: 0;
|
left: 0;
|
border-right: none;
|
border-top: none;
|
}
|
.corner.br {
|
bottom: 0;
|
right: 0;
|
border-left: none;
|
border-top: none;
|
}
|
|
/* 扫描线 */
|
.scan-line {
|
position: absolute;
|
left: 0;
|
right: 0;
|
height: 2px;
|
top: 0;
|
background: linear-gradient(
|
90deg,
|
rgba(0, 0, 0, 0) 0%,
|
rgba(64, 158, 255, 0.7) 15%,
|
rgba(64, 158, 255, 0.95) 50%,
|
rgba(64, 158, 255, 0.7) 85%,
|
rgba(0, 0, 0, 0) 100%
|
);
|
box-shadow: 0 0 8px rgba(64, 158, 255, 0.8),
|
0 0 16px rgba(64, 158, 255, 0.5);
|
animation: scan-move 2.2s linear infinite alternate;
|
will-change: transform;
|
}
|
}
|
}
|
@keyframes scan-move {
|
0% {
|
transform: translateY(0);
|
}
|
100% {
|
transform: translateY(128px);
|
}
|
}
|
</style>
|