feat: 更新二维码扫描组件,添加文件上传功能和错误处理
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -58,7 +58,6 @@ declare module 'vue' {
|
|||||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
IonText: typeof import('@ionic/vue')['IonText']
|
IonText: typeof import('@ionic/vue')['IonText']
|
||||||
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
IonToggle: typeof import('@ionic/vue')['IonToggle']
|
|
||||||
IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
||||||
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']
|
PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']
|
||||||
@@ -118,7 +117,6 @@ declare global {
|
|||||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
const IonText: typeof import('@ionic/vue')['IonText']
|
const IonText: typeof import('@ionic/vue')['IonText']
|
||||||
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
const IonToggle: typeof import('@ionic/vue')['IonToggle']
|
|
||||||
const IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
const IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
||||||
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
const PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']
|
const PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']
|
||||||
|
|||||||
@@ -32,5 +32,6 @@ export default antfu({
|
|||||||
"vue/no-deprecated-slot-attribute": "off",
|
"vue/no-deprecated-slot-attribute": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"prefer-promise-reject-errors": "off",
|
"prefer-promise-reject-errors": "off",
|
||||||
|
"no-async-promise-executor": "off",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@elysiajs/eden": "^1.4.5",
|
"@elysiajs/eden": "^1.4.5",
|
||||||
"@ionic/vue": "^8.7.11",
|
"@ionic/vue": "^8.7.11",
|
||||||
"@ionic/vue-router": "^8.7.11",
|
"@ionic/vue-router": "^8.7.11",
|
||||||
"@riwa/api-types": "http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz",
|
"@riwa/api-types": "http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
@@ -44,10 +44,12 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"ethers": "^6.16.0",
|
"ethers": "^6.16.0",
|
||||||
"ionicons": "^8.0.13",
|
"ionicons": "^8.0.13",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"lightweight-charts": "^5.1.0",
|
"lightweight-charts": "^5.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"qr-scanner-wechat": "^0.1.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"vconsole": "^3.15.1",
|
"vconsole": "^3.15.1",
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -57,8 +57,8 @@ importers:
|
|||||||
specifier: ^8.7.11
|
specifier: ^8.7.11
|
||||||
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
'@riwa/api-types':
|
'@riwa/api-types':
|
||||||
specifier: http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz
|
specifier: http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz
|
||||||
version: '@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
|
version: '@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
||||||
@@ -86,6 +86,9 @@ importers:
|
|||||||
ionicons:
|
ionicons:
|
||||||
specifier: ^8.0.13
|
specifier: ^8.0.13
|
||||||
version: 8.0.13
|
version: 8.0.13
|
||||||
|
jsqr:
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
lightweight-charts:
|
lightweight-charts:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
@@ -98,6 +101,9 @@ importers:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||||
|
qr-scanner-wechat:
|
||||||
|
specifier: ^0.1.3
|
||||||
|
version: 0.1.3
|
||||||
qrcode:
|
qrcode:
|
||||||
specifier: ^1.5.4
|
specifier: ^1.5.4
|
||||||
version: 1.5.4
|
version: 1.5.4
|
||||||
@@ -2826,9 +2832,9 @@ packages:
|
|||||||
'@remirror/core-constants@3.0.0':
|
'@remirror/core-constants@3.0.0':
|
||||||
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
||||||
|
|
||||||
'@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz':
|
'@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz':
|
||||||
resolution: {tarball: http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz}
|
resolution: {tarball: http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz}
|
||||||
version: 0.0.121
|
version: 0.0.123
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@elysiajs/eden': ^1.4.5
|
'@elysiajs/eden': ^1.4.5
|
||||||
|
|
||||||
@@ -6333,6 +6339,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
|
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
|
||||||
engines: {'0': node >=0.6.0}
|
engines: {'0': node >=0.6.0}
|
||||||
|
|
||||||
|
jsqr@1.4.0:
|
||||||
|
resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@@ -7653,6 +7662,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
qr-scanner-wechat@0.1.3:
|
||||||
|
resolution: {integrity: sha512-d2dfaXjcgNDxaSweEljvJl6cOIMbXeyW8YijZDbRFUvVpJ672zt5i64lQSBgjvKVZZSSHE2KiRf2SAuiQa286A==}
|
||||||
|
|
||||||
qrcode@1.5.4:
|
qrcode@1.5.4:
|
||||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -12187,7 +12199,7 @@ snapshots:
|
|||||||
|
|
||||||
'@remirror/core-constants@3.0.0': {}
|
'@remirror/core-constants@3.0.0': {}
|
||||||
|
|
||||||
'@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.121.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
'@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
|
||||||
@@ -15932,6 +15944,8 @@ snapshots:
|
|||||||
json-schema: 0.4.0
|
json-schema: 0.4.0
|
||||||
verror: 1.10.0
|
verror: 1.10.0
|
||||||
|
|
||||||
|
jsqr@1.4.0: {}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@@ -17694,6 +17708,8 @@ snapshots:
|
|||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
qr-scanner-wechat@0.1.3: {}
|
||||||
|
|
||||||
qrcode@1.5.4:
|
qrcode@1.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
dijkstrajs: 1.0.3
|
dijkstrajs: 1.0.3
|
||||||
|
|||||||
@@ -1,33 +1,233 @@
|
|||||||
<script lang='ts' setup>
|
<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 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() {
|
async function start() {
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!videoInst.value) {
|
if (!videoInst.value) {
|
||||||
console.error("video element not found!");
|
const error = new Error("video element not found!");
|
||||||
|
console.error(error);
|
||||||
|
emit("error", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: false,
|
try {
|
||||||
video: { facingMode: "environment" },
|
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
|
if (code) {
|
||||||
await videoInst.value.play();
|
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(() => {
|
onMounted(() => {
|
||||||
start();
|
start();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="relative h-full w-full overflow-hidden bg-black">
|
||||||
<video ref="videoInst" />
|
<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>
|
</div>
|
||||||
</template>
|
</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 { 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 {
|
export interface QRScanResult {
|
||||||
text: string;
|
text: string;
|
||||||
format: string;
|
format: string;
|
||||||
rawValue: string;
|
|
||||||
displayValue: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScannerOptions {
|
export interface ScannerOptions {
|
||||||
@@ -15,36 +14,71 @@ export interface ScannerOptions {
|
|||||||
export function useQRScanner() {
|
export function useQRScanner() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { vibrate } = useHaptics();
|
const { vibrate } = useHaptics();
|
||||||
|
const platform = usePlatform();
|
||||||
|
|
||||||
async function open(options?: ScannerOptions) {
|
async function open(options?: ScannerOptions) {
|
||||||
try {
|
return new Promise<string>(async (resolve, reject) => {
|
||||||
vibrate();
|
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({
|
reject();
|
||||||
hint: CapacitorBarcodeScannerTypeHint.QR_CODE,
|
});
|
||||||
scanInstructions: options?.title || t("scanner.hint"),
|
}
|
||||||
cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
|
else {
|
||||||
scanOrientation: CapacitorBarcodeScannerScanOrientation.PORTRAIT,
|
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||||
});
|
hint: CapacitorBarcodeScannerTypeHint.QR_CODE,
|
||||||
|
scanInstructions: options?.title || t("scanner.hint"),
|
||||||
|
cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
|
||||||
|
scanOrientation: CapacitorBarcodeScannerScanOrientation.PORTRAIT,
|
||||||
|
});
|
||||||
|
|
||||||
vibrate();
|
vibrate();
|
||||||
return {
|
resolve(result.ScanResult);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
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() {
|
function close() {
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user