<script setup>
|
import { ref, onMounted, nextTick, onUnmounted } from "vue";
|
|
// https://github.com/cozmo/jsQR
|
import jsQR from "jsqr";
|
import { ElMessage } from "element-plus";
|
import _ from "lodash";
|
|
// // environment 后摄像头 user 前摄像头
|
// exact?: "environment" | "user";
|
// // whole 全屏 half 半屏
|
// size?: "whole" | "half";
|
// // 清晰度: fasle 正常 true 高清
|
// definition?: boolean;
|
const props = {
|
exact: "environment",
|
size: "whole",
|
definition: false,
|
};
|
|
const video = ref();
|
const canvas2d = ref();
|
const canvasWidth = ref(520);
|
const canvasHeight = ref(500);
|
const c = ref();
|
const track = ref();
|
const isUseTorch = ref(false);
|
const trackStatus = ref(false);
|
const fileRef = ref();
|
|
onMounted(() => {
|
const windowWidth = window.screen.availWidth;
|
const windowHeight = window.screen.availHeight;
|
|
canvasWidth.value = windowWidth;
|
canvasHeight.value = windowHeight;
|
|
nextTick(() => {
|
video.value = document.createElement("video");
|
c.value = document.createElement("canvas");
|
c.value.id = "c";
|
c.value.width = canvasWidth.value;
|
c.value.height = canvasHeight.value;
|
c.value.style.width = "100%";
|
document.querySelector(".canvasBox")?.append(c.value);
|
openScan();
|
});
|
});
|
|
onUnmounted(() => {
|
closeCamera();
|
});
|
// 开始扫描
|
async function openScan() {
|
try {
|
let width = canvasHeight.value;
|
width = props.size === "whole" ? width : width * 0.5;
|
width = props.definition ? width * 1.6 : width;
|
let height = canvasWidth.value;
|
height = props.definition ? height * 1.6 : height;
|
const videoParam = {
|
audio: false,
|
video: {
|
facingMode: { exact: props.exact }, //强制使用摄像头类型
|
width,
|
height,
|
},
|
};
|
// 获取用户摄像头的视频流
|
const stream = await navigator.mediaDevices.getUserMedia(videoParam);
|
if (stream) {
|
video.value.srcObject = stream;
|
video.value.setAttribute("playsinline", true); //内联播放
|
video.value.play();
|
requestAnimationFrame(tick);
|
// 返回所有的媒体内容流的轨道列表
|
track.value = stream.getVideoTracks()?.[0];
|
setTimeout(() => {
|
// 检测摄像头是否支持闪光灯
|
isUseTorch.value = track.value.getCapabilities().torch || null;
|
}, 500);
|
}
|
} catch (error) {
|
ElMessage.warning("设备不支持,请检查是否允许摄像头权限!");
|
console.log("获取本地设备(摄像头)---失败", error);
|
}
|
}
|
function closeCamera() {
|
if (video.value.srcObject) {
|
video.value.srcObject.getTracks().forEach((track) => {
|
track.stop();
|
});
|
}
|
}
|
function tick() {
|
if (video.value.readyState === video.value.HAVE_ENOUGH_DATA) {
|
canvasHeight.value = video.value.videoHeight;
|
canvasWidth.value = video.value.videoWidth;
|
c.value.width = canvasWidth.value;
|
c.value.height = canvasHeight.value;
|
if (canvas2d.value === undefined) {
|
canvas2d.value = c.value.getContext("2d");
|
}
|
|
canvas2d.value.drawImage(
|
video.value,
|
0,
|
0,
|
canvasWidth.value,
|
canvasHeight.value
|
);
|
|
const imageData = canvas2d.value.getImageData(
|
0,
|
0,
|
canvasWidth.value,
|
canvasHeight.value
|
);
|
// 解析二维码数据
|
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
inversionAttempts: "dontInvert",
|
});
|
|
if (!_.isEmpty(code)) {
|
drawLine(
|
code.location.topLeftCorner,
|
code.location.topRightCorner,
|
"#FF3B58"
|
);
|
drawLine(
|
code.location.topRightCorner,
|
code.location.bottomRightCorner,
|
"#FF3B58"
|
);
|
drawLine(
|
code.location.bottomRightCorner,
|
code.location.bottomLeftCorner,
|
"#FF3B58"
|
);
|
drawLine(
|
code.location.bottomLeftCorner,
|
code.location.topLeftCorner,
|
"#FF3B58"
|
);
|
if (code.data) {
|
getData(code.data);
|
}
|
}
|
}
|
requestAnimationFrame(tick);
|
}
|
function drawLine(begin, end, color) {
|
canvas2d.value.beginPath();
|
canvas2d.value.moveTo(begin.x, begin.y);
|
canvas2d.value.lineTo(end.x, end.y);
|
canvas2d.value.lineWidth = 4;
|
canvas2d.value.strokeStyle = color;
|
canvas2d.value.stroke();
|
}
|
const emit = defineEmits(["on-success", "on-close"]);
|
|
function getData(data) {
|
emit("on-success", data);
|
closeCamera();
|
}
|
|
function close() {
|
emit("on-close");
|
closeCamera();
|
}
|
|
function openTrack() {
|
trackStatus.value = !trackStatus.value;
|
track.value.applyConstraints({
|
advanced: [{ torch: trackStatus.value }],
|
});
|
}
|
const handleClickFile = () => {
|
fileRef.value.click();
|
};
|
const getFile = (e) => {
|
const file = e.target.files[0];
|
emit("on-success", file);
|
closeCamera();
|
};
|
</script>
|
|
<template>
|
<div>
|
<div class="canvasBox">
|
<div class="close" @click="close">
|
<el-icon><CircleClose /></el-icon>
|
</div>
|
<div class="box">
|
<div class="line"></div>
|
<div class="angle"></div>
|
</div>
|
<div v-if="isUseTorch" class="box2">
|
<div class="track" @click="openTrack">
|
<div :class="['flash-light', {open : trackStatus}]">
|
</div>
|
{{ trackStatus ? "关闭闪光灯" : "打开闪光灯" }}
|
</div>
|
</div>
|
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.flash-light {
|
display: grid;
|
place-content: center;
|
margin-bottom: 6px;
|
}
|
|
.photo-wrap {
|
position: fixed;
|
bottom: 2.875rem;
|
left: 2.875rem;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 6px;
|
}
|
|
.photo {
|
height: 3.125rem;
|
width: 3.125rem;
|
background-color: rgba(250, 250, 250, 0.8);
|
border-radius: 50%;
|
display: grid;
|
place-items: center;
|
cursor: pointer;
|
}
|
|
.hide_file {
|
display: none;
|
}
|
|
/* page {
|
background-color: #333333;
|
} */
|
|
.canvasBox {
|
width: 100vw;
|
position: relative;
|
top: 0;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
/* background-image: linear-gradient(
|
0deg,
|
transparent 24%,
|
rgba(32, 255, 77, 0.1) 25%,
|
rgba(32, 255, 77, 0.1) 26%,
|
transparent 27%,
|
transparent 74%,
|
rgba(32, 255, 77, 0.1) 75%,
|
rgba(32, 255, 77, 0.1) 76%,
|
transparent 77%,
|
transparent
|
),
|
linear-gradient(
|
90deg,
|
transparent 24%,
|
rgba(32, 255, 77, 0.1) 25%,
|
rgba(32, 255, 77, 0.1) 26%,
|
transparent 27%,
|
transparent 74%,
|
rgba(32, 255, 77, 0.1) 75%,
|
rgba(32, 255, 77, 0.1) 76%,
|
transparent 77%,
|
transparent
|
);
|
background-size: 3rem 3rem;
|
background-position: -1rem -1rem; */
|
z-index: 10;
|
background-color: #1110;
|
}
|
.flash-light {
|
width: 4em;
|
height: 4em;
|
background: url("data:image/svg+xml,%3csvg viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' fill='%23cdcdcd'%3e%3cpath d='M675.7 266.3H348.4c-46.1 0-83.6 37.5-83.6 83.6v96.4c0 70.9 33.3 137.2 89.6 179.7v275.2c0 46.1 37.5 83.7 83.7 83.7h148c46.1 0 83.7-37.5 83.7-83.7V626c56.3-42.5 89.6-108.8 89.6-179.7v-96.2c0.1-22.3-8.5-43.3-24.3-59.2-16-15.8-37-24.6-59.4-24.6z m-327.3 43.8h327.3c10.6 0 20.6 4.2 28.1 11.7 7.5 7.5 11.6 17.5 11.6 28.2v39.4H308.6V350c0-22 17.8-39.9 39.8-39.9z m287.1 286.5c-6 4.1-9.6 10.9-9.6 18.1v286.5c0 22-17.9 39.9-39.9 39.9H438c-22 0-39.9-17.9-39.9-39.9V614.8c0-7.3-3.6-14.1-9.6-18.1-49.5-33.4-79.1-88.7-79.8-148.2h406.6c-0.7 59.4-30.3 114.7-79.8 148.1z'%3e%3c/path%3e%3cpath d='M524 625.6h-23.8c-9.7 0-17.6 7.9-17.6 17.6v122.2c0 9.7 7.9 17.6 17.6 17.6H524c9.7 0 17.6-7.9 17.6-17.6V643.2c0-9.7-7.9-17.6-17.6-17.6z'%3e%3c/path%3e%3c/svg%3e") center center / contain no-repeat;
|
}
|
.flash-light.open {
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' fill='%23cde919'%3e%3cpath d='M675.7 266.3H348.4c-46.1 0-83.6 37.5-83.6 83.6v96.4c0 70.9 33.3 137.2 89.6 179.7v275.2c0 46.1 37.5 83.7 83.7 83.7h148c46.1 0 83.7-37.5 83.7-83.7V626c56.3-42.5 89.6-108.8 89.6-179.7v-96.2c0.1-22.3-8.5-43.3-24.3-59.2-16-15.8-37-24.6-59.4-24.6z m-327.3 43.8h327.3c10.6 0 20.6 4.2 28.1 11.7 7.5 7.5 11.6 17.5 11.6 28.2v39.4H308.6V350c0-22 17.8-39.9 39.8-39.9z m287.1 286.5c-6 4.1-9.6 10.9-9.6 18.1v286.5c0 22-17.9 39.9-39.9 39.9H438c-22 0-39.9-17.9-39.9-39.9V614.8c0-7.3-3.6-14.1-9.6-18.1-49.5-33.4-79.1-88.7-79.8-148.2h406.6c-0.7 59.4-30.3 114.7-79.8 148.1z'%3e%3c/path%3e%3cpath d='M524 625.6h-23.8c-9.7 0-17.6 7.9-17.6 17.6v122.2c0 9.7 7.9 17.6 17.6 17.6H524c9.7 0 17.6-7.9 17.6-17.6V643.2c0-9.7-7.9-17.6-17.6-17.6zM306.7 239.3c11.5 11.5 30.3 11.5 41.8 0 11.5-11.5 11.5-30.3 0-41.8l-37.4-37.4c-11.5-11.5-30.3-11.5-41.8 0-11.5 11.5-11.5 30.3 0 41.8l37.4 37.4zM675.5 239.3c11.5 11.5 30.3 11.5 41.8 0l37.4-37.4c11.5-11.5 11.5-30.3 0-41.8-11.5-11.5-30.3-11.5-41.8 0l-37.4 37.4c-11.4 11.5-11.4 30.3 0 41.8zM512.1 164c16.2 0 29.5-13.3 29.5-29.5V81.6c0-16.2-13.3-29.5-29.5-29.5s-29.5 13.3-29.5 29.5v52.8c0 16.3 13.2 29.6 29.5 29.6z'%3e%3c/path%3e%3c/svg%3e");
|
}
|
.box {
|
width: 11.9375rem;
|
height: 11.9375rem;
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -80%);
|
overflow: hidden;
|
border: 0.1rem solid rgba(0, 255, 51, 0.2);
|
z-index: 11;
|
}
|
.close {
|
position: absolute;
|
top: 2rem;
|
right: 1rem;
|
font-size: 2.25rem;
|
color: #fff;
|
z-index: 32;
|
}
|
|
.line {
|
height: calc(100% - 2px);
|
width: 100%;
|
background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%);
|
border-bottom: 3px solid #00ff33;
|
transform: translateY(-100%);
|
animation: radar-beam 2s infinite alternate;
|
animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);
|
animation-delay: 1.4s;
|
}
|
|
.box:after,
|
.box:before,
|
.angle:after,
|
.angle:before {
|
content: "";
|
display: block;
|
position: absolute;
|
width: 3vw;
|
height: 3vw;
|
z-index: 12;
|
border: 0.2rem solid transparent;
|
}
|
|
.box:after,
|
.box:before {
|
top: 0;
|
border-top-color: #00ff33;
|
}
|
|
.angle:after,
|
.angle:before {
|
bottom: 0;
|
border-bottom-color: #00ff33;
|
}
|
|
.box:before,
|
.angle:before {
|
left: 0;
|
border-left-color: #00ff33;
|
}
|
|
.box:after,
|
.angle:after {
|
right: 0;
|
border-right-color: #00ff33;
|
}
|
|
@keyframes radar-beam {
|
0% {
|
transform: translateY(-100%);
|
}
|
|
100% {
|
transform: translateY(0);
|
}
|
}
|
|
.box2 {
|
width: 18.75rem;
|
height: 12.5rem;
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -80%);
|
z-index: 20;
|
}
|
|
.track {
|
position: absolute;
|
bottom: -8.25rem;
|
left: 50%;
|
transform: translateX(-50%);
|
z-index: 20;
|
color: #fff;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
</style>
|