feat: 更新二维码扫描组件,添加文件上传功能和错误处理
This commit is contained in:
@@ -1,33 +1,233 @@
|
||||
<script lang='ts' setup>
|
||||
import { chevronBackOutline, images } from "ionicons/icons";
|
||||
import jsQR from "jsqr";
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [value: string];
|
||||
error: [error: Error];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const videoInst = useTemplateRef<HTMLVideoElement>("videoInst");
|
||||
const canvasInst = useTemplateRef<HTMLCanvasElement>("canvasInst");
|
||||
const fileInputInst = useTemplateRef<HTMLInputElement>("fileInputInst");
|
||||
|
||||
const scanning = ref(true);
|
||||
let stream: MediaStream | null = null;
|
||||
|
||||
async function start() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
console.error("getUserMedia not supported on your browser!");
|
||||
const error = new Error("getUserMedia not supported on your browser!");
|
||||
console.error(error);
|
||||
emit("error", error);
|
||||
return;
|
||||
}
|
||||
if (!videoInst.value) {
|
||||
console.error("video element not found!");
|
||||
const error = new Error("video element not found!");
|
||||
console.error(error);
|
||||
emit("error", error);
|
||||
return;
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: { facingMode: "environment" },
|
||||
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: { facingMode: "environment", width: 512, height: 512 },
|
||||
});
|
||||
videoInst.value.srcObject = stream;
|
||||
videoInst.value.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen
|
||||
await videoInst.value.play();
|
||||
scanning.value = true;
|
||||
scanFrame();
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to start camera:", error);
|
||||
emit("error", error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function scanFrame() {
|
||||
if (!scanning.value || !videoInst.value || !canvasInst.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const video = videoInst.value;
|
||||
const canvas = canvasInst.value;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (!ctx)
|
||||
return;
|
||||
|
||||
if (video.readyState !== video.HAVE_ENOUGH_DATA) {
|
||||
requestAnimationFrame(scanFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: "dontInvert",
|
||||
});
|
||||
videoInst.value.srcObject = stream;
|
||||
videoInst.value.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen
|
||||
await videoInst.value.play();
|
||||
|
||||
if (code) {
|
||||
scanning.value = false;
|
||||
emit("success", code.data);
|
||||
stop();
|
||||
}
|
||||
else {
|
||||
requestAnimationFrame(scanFrame);
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
scanning.value = false;
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
stream = null;
|
||||
}
|
||||
if (videoInst.value) {
|
||||
videoInst.value.srcObject = null;
|
||||
}
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function handleSelectImage() {
|
||||
fileInputInst.value?.click();
|
||||
}
|
||||
|
||||
async function handleFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const image = new Image();
|
||||
const canvas = canvasInst.value;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取图片
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: "dontInvert",
|
||||
});
|
||||
|
||||
if (code) {
|
||||
emit("success", code.data);
|
||||
stop();
|
||||
}
|
||||
else {
|
||||
const error = new Error("未识别到二维码");
|
||||
emit("error", error);
|
||||
}
|
||||
};
|
||||
image.src = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to scan image:", error);
|
||||
emit("error", error as Error);
|
||||
}
|
||||
finally {
|
||||
if (target) {
|
||||
target.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stop();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
start,
|
||||
stop,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<video ref="videoInst" />
|
||||
<div class="relative h-full w-full overflow-hidden bg-black">
|
||||
<video ref="videoInst" class="h-full w-full object-cover" />
|
||||
<canvas ref="canvasInst" class="hidden" />
|
||||
<input
|
||||
ref="fileInputInst"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
|
||||
<div class="absolute left-0 top-0 z-10 p-4 pt-[calc(1rem+var(--ion-safe-area-top,0px))] pl-[calc(1rem+var(--ion-safe-area-left,0px))]">
|
||||
<button class="z-1 flex items-center" @click="stop">
|
||||
<ion-icon :icon="chevronBackOutline" class="text-2xl text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center bg-black/50">
|
||||
<div class="qr-scanner-frame relative size-70 bg-transparent">
|
||||
<div class="absolute left-0 top-0 size-8 rounded-tl border-3 border-white border-b-0 border-r-0" />
|
||||
<div class="absolute right-0 top-0 size-8 rounded-tr border-3 border-white border-b-0 border-l-0" />
|
||||
<div class="absolute bottom-0 left-0 size-8 rounded-bl border-3 border-white border-r-0 border-t-0" />
|
||||
<div class="absolute bottom-0 right-0 size-8 rounded-br border-3 border-white border-l-0 border-t-0" />
|
||||
|
||||
<div
|
||||
v-if="scanning"
|
||||
class="qr-scanner-line absolute inset-x-0 top-0 h-0.5 animate-scan bg-linear-to-r from-transparent via-white to-transparent shadow-[0_0_8px_rgba(255,255,255,0.8)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 rounded-lg bg-black/60 px-6 py-3 text-sm text-white backdrop-blur">
|
||||
将二维码放入框内,即可自动扫描
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<ion-button color="light" @click="handleSelectImage">
|
||||
<ion-icon slot="start" :icon="images" />
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
<style lang='css' scoped>
|
||||
.qr-scanner-frame {
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(280px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scan {
|
||||
animation: scan 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CapacitorBarcodeScanner, CapacitorBarcodeScannerCameraDirection, CapacitorBarcodeScannerScanOrientation, CapacitorBarcodeScannerTypeHint } from "@capacitor/barcode-scanner";
|
||||
import { toastController } from "@ionic/vue";
|
||||
import { modalController, toastController } from "@ionic/vue";
|
||||
import QRCode from "@/components/qr-scanner/index.vue";
|
||||
|
||||
export interface QRScanResult {
|
||||
text: string;
|
||||
format: string;
|
||||
rawValue: string;
|
||||
displayValue: string;
|
||||
}
|
||||
|
||||
export interface ScannerOptions {
|
||||
@@ -15,36 +14,71 @@ export interface ScannerOptions {
|
||||
export function useQRScanner() {
|
||||
const { t } = useI18n();
|
||||
const { vibrate } = useHaptics();
|
||||
const platform = usePlatform();
|
||||
|
||||
async function open(options?: ScannerOptions) {
|
||||
try {
|
||||
vibrate();
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
try {
|
||||
vibrate();
|
||||
if (platform === "browser") {
|
||||
const modal = await modalController.create({
|
||||
component: QRCode,
|
||||
componentProps: {
|
||||
onClose: () => modal.dismiss(),
|
||||
onError: (error) => {
|
||||
toastController.create({
|
||||
message: String(error),
|
||||
duration: 2000,
|
||||
position: "bottom",
|
||||
color: "danger",
|
||||
}).then(toast => toast.present());
|
||||
},
|
||||
onSuccess: (result: string) => {
|
||||
toastController.create({
|
||||
message: String(result),
|
||||
duration: 2000,
|
||||
position: "bottom",
|
||||
color: "success",
|
||||
}).then(toast => toast.present());
|
||||
modal.dismiss(result);
|
||||
},
|
||||
},
|
||||
animated: false,
|
||||
});
|
||||
await modal.present();
|
||||
modal.onDidDismiss<string>().then((res) => {
|
||||
if (res.data)
|
||||
resolve(res.data);
|
||||
|
||||
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||
hint: CapacitorBarcodeScannerTypeHint.QR_CODE,
|
||||
scanInstructions: options?.title || t("scanner.hint"),
|
||||
cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
|
||||
scanOrientation: CapacitorBarcodeScannerScanOrientation.PORTRAIT,
|
||||
});
|
||||
reject();
|
||||
});
|
||||
}
|
||||
else {
|
||||
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||
hint: CapacitorBarcodeScannerTypeHint.QR_CODE,
|
||||
scanInstructions: options?.title || t("scanner.hint"),
|
||||
cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
|
||||
scanOrientation: CapacitorBarcodeScannerScanOrientation.PORTRAIT,
|
||||
});
|
||||
|
||||
vibrate();
|
||||
return {
|
||||
text: result.ScanResult,
|
||||
format: result.format,
|
||||
};
|
||||
}
|
||||
catch (error: any) {
|
||||
console.log("error.message", error.message);
|
||||
if (error.code !== "OS-PLUG-BARC-0006") {
|
||||
const toast = await toastController.create({
|
||||
message: t("scanner.openError"),
|
||||
duration: 2000,
|
||||
position: "bottom",
|
||||
color: "danger",
|
||||
});
|
||||
await toast.present();
|
||||
vibrate();
|
||||
resolve(result.ScanResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.log("error.message", error.message);
|
||||
if (error.code !== "OS-PLUG-BARC-0006") {
|
||||
const toast = await toastController.create({
|
||||
message: t("scanner.openError"),
|
||||
duration: 2000,
|
||||
position: "bottom",
|
||||
color: "danger",
|
||||
});
|
||||
await toast.present();
|
||||
reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function close() {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user