feat: add QRCode scanner
This commit is contained in:
162
src/components/qr-scanner/index.vue
Normal file
162
src/components/qr-scanner/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<!-- filepath: /src/components/ui/qr-scanner/QRScanner.vue -->
|
||||
<script setup lang="ts">
|
||||
import type { QRScanResult } from "@/composables/useQRScanner";
|
||||
|
||||
interface Props {
|
||||
modelValue?: boolean;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
"update:modelValue": [value: boolean];
|
||||
"scan-success": [result: QRScanResult];
|
||||
"scan-error": [error: Error];
|
||||
"scan-cancelled": [];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
title: "扫描二维码",
|
||||
subtitle: "将二维码对准扫描框进行扫描",
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { isSupported, startScan, stopScan } = useQRScanner();
|
||||
const isScanning = ref(false);
|
||||
|
||||
// 开始扫描
|
||||
async function handleStartScan() {
|
||||
try {
|
||||
if (!isSupported) {
|
||||
throw new Error("当前平台不支持二维码扫描");
|
||||
}
|
||||
|
||||
isScanning.value = true;
|
||||
const result = await startScan();
|
||||
|
||||
if (result) {
|
||||
emit("scan-success", result);
|
||||
await handleClose();
|
||||
}
|
||||
else {
|
||||
emit("scan-cancelled");
|
||||
await handleClose();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Scan failed:", error);
|
||||
emit("scan-error", error as Error);
|
||||
await handleClose();
|
||||
}
|
||||
finally {
|
||||
isScanning.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭扫描
|
||||
async function handleClose() {
|
||||
if (isScanning.value) {
|
||||
await stopScan();
|
||||
isScanning.value = false;
|
||||
}
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
// 监听模态框打开
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
if (isOpen && isSupported) {
|
||||
nextTick(() => {
|
||||
handleStartScan();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时确保停止扫描
|
||||
onUnmounted(() => {
|
||||
if (isScanning.value) {
|
||||
stopScan();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IonModal :is-open="modelValue" @did-dismiss="handleClose">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>{{ title }}</IonTitle>
|
||||
<IonButtons slot="end">
|
||||
<IonButton @click="handleClose">
|
||||
<IonIcon :icon="close" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent class="qr-scanner-content">
|
||||
<div v-if="!isSupported" class="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<IonIcon :icon="qrCodeOutline" size="large" class="text-gray-400 mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
不支持的平台
|
||||
</h3>
|
||||
<p class="text-gray-600">
|
||||
二维码扫描仅在移动设备上支持
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isScanning" class="flex flex-col h-full">
|
||||
<!-- 扫描指示 -->
|
||||
<div class="flex flex-col items-center justify-center flex-1 p-6 text-center text-white">
|
||||
<div class="relative mb-8">
|
||||
<div class="w-64 h-64 border-2 border-white border-dashed rounded-lg animate-pulse" />
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<IonIcon :icon="qrCodeOutline" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="text-sm opacity-80">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="p-6 bg-white">
|
||||
<IonButton
|
||||
expand="block"
|
||||
fill="outline"
|
||||
@click="handleClose"
|
||||
>
|
||||
取消扫描
|
||||
</IonButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<IonIcon :icon="qrCodeOutline" size="large" class="text-primary mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
准备扫描
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
点击开始按钮开始扫描二维码
|
||||
</p>
|
||||
<IonButton expand="block" @click="handleStartScan">
|
||||
开始扫描
|
||||
</IonButton>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qr-scanner-content {
|
||||
--background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* 当扫描时,隐藏背景以显示相机预览 */
|
||||
.qr-scanner-content.scanning {
|
||||
--background: transparent;
|
||||
}
|
||||
</style>
|
||||
82
src/composables/useQRScanner.ts
Normal file
82
src/composables/useQRScanner.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { BarcodeScanner } from "@capacitor-community/barcode-scanner";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
|
||||
export interface QRScanResult {
|
||||
text: string;
|
||||
format: string;
|
||||
}
|
||||
|
||||
export function useQRScanner() {
|
||||
const isSupported = Capacitor.isNativePlatform();
|
||||
|
||||
// 检查权限
|
||||
const checkPermission = async (): Promise<boolean> => {
|
||||
try {
|
||||
const status = await BarcodeScanner.checkPermission({ force: true });
|
||||
return status.granted || false;
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Permission check failed:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始扫描
|
||||
const startScan = async (): Promise<QRScanResult | null> => {
|
||||
try {
|
||||
// 检查是否为原生平台
|
||||
if (!isSupported) {
|
||||
console.warn("QR Scanner is only supported on native platforms");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const hasPermission = await checkPermission();
|
||||
if (!hasPermission) {
|
||||
throw new Error("Camera permission denied");
|
||||
}
|
||||
|
||||
// 隐藏背景以显示相机预览
|
||||
await BarcodeScanner.hideBackground();
|
||||
|
||||
// 开始扫描
|
||||
const result = await BarcodeScanner.startScan();
|
||||
|
||||
// 恢复背景
|
||||
await BarcodeScanner.showBackground();
|
||||
|
||||
if (result?.hasContent) {
|
||||
return {
|
||||
text: result.content,
|
||||
format: result.format || "QR_CODE",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (error) {
|
||||
// 确保恢复背景
|
||||
await BarcodeScanner.showBackground();
|
||||
console.error("QR scan failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 停止扫描
|
||||
const stopScan = async () => {
|
||||
try {
|
||||
await BarcodeScanner.stopScan();
|
||||
await BarcodeScanner.showBackground();
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Stop scan failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
checkPermission,
|
||||
startScan,
|
||||
stopScan,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user