feat: 集成二维码扫描功能,更新相关组件和国际化支持
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -138,6 +138,7 @@ API 配置集中在 src/api/index.ts
|
||||
认证逻辑封装在 src/auth/index.ts
|
||||
支持登录、注册组件复用
|
||||
认证状态管理通过 useAuth composable
|
||||
|
||||
开发任务指引
|
||||
当收到开发任务时,请:
|
||||
|
||||
@@ -159,3 +160,4 @@ API 类型来自私有包 @riwa/api-types(需要特定访问权限)
|
||||
Capacitor 配置本地开发服务器地址为 http://localhost:5173
|
||||
国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改
|
||||
样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式
|
||||
函数风格使用function关键字定义,一般不要使用箭头函数
|
||||
|
||||
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -351,7 +351,7 @@ declare global {
|
||||
export type { Language } from './src/composables/useLanguage'
|
||||
import('./src/composables/useLanguage')
|
||||
// @ts-ignore
|
||||
export type { QRScanResult } from './src/composables/useQRScanner'
|
||||
export type { QRScanResult, ScannerOptions } from './src/composables/useQRScanner'
|
||||
import('./src/composables/useQRScanner')
|
||||
// @ts-ignore
|
||||
export type { ThemeMode } from './src/composables/useTheme'
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = G6N4M926R4;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -305,7 +306,7 @@
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = riwa.ionic.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -319,6 +320,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = G6N4M926R4;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -326,7 +328,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = riwa.ionic.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -2,64 +2,65 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CAPACITOR_DEBUG</key>
|
||||
<string>$(CAPACITOR_DEBUG)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>riwa-ionic</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to camera to scan QR codes</string>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CAPACITOR_DEBUG</key>
|
||||
<string>$(CAPACITOR_DEBUG)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>riwa-ionic</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to camera to scan QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs access to photo library to select images for QR code scanning</string>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -13,6 +13,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.0.0"),
|
||||
.package(name: "CapacitorApp", path: "../../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/app"),
|
||||
.package(name: "CapacitorBarcodeScanner", path: "../../../node_modules/.pnpm/@capacitor+barcode-scanner@3.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/barcode-scanner"),
|
||||
.package(name: "CapacitorCamera", path: "../../../node_modules/.pnpm/@capacitor+camera@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/camera"),
|
||||
.package(name: "CapacitorClipboard", path: "../../../node_modules/.pnpm/@capacitor+clipboard@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/clipboard"),
|
||||
.package(name: "CapacitorFileTransfer", path: "../../../node_modules/.pnpm/@capacitor+file-transfer@2.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/file-transfer"),
|
||||
@@ -28,6 +29,7 @@ let package = Package(
|
||||
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
||||
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
||||
.product(name: "CapacitorApp", package: "CapacitorApp"),
|
||||
.product(name: "CapacitorBarcodeScanner", package: "CapacitorBarcodeScanner"),
|
||||
.product(name: "CapacitorCamera", package: "CapacitorCamera"),
|
||||
.product(name: "CapacitorClipboard", package: "CapacitorClipboard"),
|
||||
.product(name: "CapacitorFileTransfer", package: "CapacitorFileTransfer"),
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@capacitor-community/barcode-scanner": "^4.0.1",
|
||||
"@capacitor-mlkit/barcode-scanning": "^8.0.0",
|
||||
"@capacitor/app": "8.0.0",
|
||||
"@capacitor/barcode-scanner": "^3.0.0",
|
||||
"@capacitor/camera": "^8.0.0",
|
||||
"@capacitor/clipboard": "^8.0.0",
|
||||
"@capacitor/core": "8.0.0",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
'@capacitor/app':
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0(@capacitor/core@8.0.0)
|
||||
'@capacitor/barcode-scanner':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(@capacitor/core@8.0.0)
|
||||
'@capacitor/camera':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@capacitor/core@8.0.0)
|
||||
@@ -831,6 +834,11 @@ packages:
|
||||
peerDependencies:
|
||||
'@capacitor/core': '>=8.0.0'
|
||||
|
||||
'@capacitor/barcode-scanner@3.0.0':
|
||||
resolution: {integrity: sha512-UWcN+9uSk1+RxR/Sh794727QH7I47/N1NoTGcumjRjhbhAb6CHJPyxOf1LyIoBILt4PinUsLhegCmzlBDrB3lA==}
|
||||
peerDependencies:
|
||||
'@capacitor/core': '>=8.0.0'
|
||||
|
||||
'@capacitor/camera@8.0.0':
|
||||
resolution: {integrity: sha512-Iu8j2oxoIhY2mLuoEckbL7PFgw1XFm1nqmeWdIkILpcT3H9A+BrSDUDlzWqM/EeaDKo6JnhR59tYHwUhOdXaUg==}
|
||||
peerDependencies:
|
||||
@@ -3141,6 +3149,9 @@ packages:
|
||||
html-entities@2.6.0:
|
||||
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
||||
|
||||
html5-qrcode@2.3.8:
|
||||
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -5839,6 +5850,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@capacitor/core': 8.0.0
|
||||
|
||||
'@capacitor/barcode-scanner@3.0.0(@capacitor/core@8.0.0)':
|
||||
dependencies:
|
||||
'@capacitor/core': 8.0.0
|
||||
html5-qrcode: 2.3.8
|
||||
|
||||
'@capacitor/camera@8.0.0(@capacitor/core@8.0.0)':
|
||||
dependencies:
|
||||
'@capacitor/core': 8.0.0
|
||||
@@ -8363,6 +8379,8 @@ snapshots:
|
||||
|
||||
html-entities@2.6.0: {}
|
||||
|
||||
html5-qrcode@2.3.8: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
|
||||
@@ -1,162 +1,342 @@
|
||||
<!-- filepath: /src/components/ui/qr-scanner/QRScanner.vue -->
|
||||
<script setup lang="ts">
|
||||
import type { QRScanResult } from "@/composables/useQRScanner";
|
||||
import { CapacitorBarcodeScanner } from "@capacitor/barcode-scanner";
|
||||
import { closeOutline, flashlightOutline, imagesOutline } from "ionicons/icons";
|
||||
|
||||
interface Props {
|
||||
modelValue?: boolean;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
"update:modelValue": [value: boolean];
|
||||
"scanSuccess": [result: QRScanResult];
|
||||
"scanError": [error: Error];
|
||||
"scanCancelled": [];
|
||||
scanSuccess: [result: QRScanResult];
|
||||
scanFromGallery: [];
|
||||
cancel: [];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
title: "扫描二维码",
|
||||
subtitle: "将二维码对准扫描框进行扫描",
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { isSupported, startScan, stopScan } = useQRScanner();
|
||||
const { t } = useI18n();
|
||||
const { vibrate } = useHaptics();
|
||||
const isScanning = ref(false);
|
||||
const isTorchEnabled = ref(false);
|
||||
|
||||
// 开始扫描
|
||||
async function handleStartScan() {
|
||||
async function startScanning() {
|
||||
try {
|
||||
if (!isSupported) {
|
||||
throw new Error("当前平台不支持二维码扫描");
|
||||
}
|
||||
|
||||
isScanning.value = true;
|
||||
const result = await startScan();
|
||||
|
||||
if (result) {
|
||||
emit("scanSuccess", result);
|
||||
await handleClose();
|
||||
// 开始扫描 - Google Barcode Scanner 会打开原生UI
|
||||
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||
hint: 17, // ALL - 扫描所有格式
|
||||
scanInstructions: props.title,
|
||||
scanButton: false,
|
||||
cameraDirection: 1, // BACK camera
|
||||
});
|
||||
|
||||
if (result.ScanResult) {
|
||||
// 震动反馈
|
||||
vibrate();
|
||||
|
||||
// 发送扫描结果
|
||||
emit("scanSuccess", {
|
||||
text: result.ScanResult,
|
||||
format: result.format?.toString() || "UNKNOWN",
|
||||
rawValue: result.ScanResult,
|
||||
displayValue: result.ScanResult,
|
||||
});
|
||||
}
|
||||
else {
|
||||
emit("scanCancelled");
|
||||
await handleClose();
|
||||
// 用户取消了扫描
|
||||
emit("cancel");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Scan failed:", error);
|
||||
emit("scanError", error as Error);
|
||||
await handleClose();
|
||||
catch (error: any) {
|
||||
console.error("Start scanning failed:", error);
|
||||
if (error.message !== "User cancelled") {
|
||||
emit("cancel");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
isScanning.value = false;
|
||||
await stopScanning();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭扫描
|
||||
async function handleClose() {
|
||||
if (isScanning.value) {
|
||||
await stopScan();
|
||||
// 停止扫描
|
||||
async function stopScanning() {
|
||||
try {
|
||||
document.querySelector("body")?.classList.remove("barcode-scanner-active");
|
||||
isScanning.value = false;
|
||||
isTorchEnabled.value = false;
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Stop scanning failed:", error);
|
||||
}
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
// 监听模态框打开
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
if (isOpen && isSupported) {
|
||||
nextTick(() => {
|
||||
handleStartScan();
|
||||
});
|
||||
// 切换手电筒
|
||||
async function toggleTorch() {
|
||||
try {
|
||||
// Google Barcode Scanner 不支持手电筒控制
|
||||
// 此功能在使用 Google Scanner UI 时不可用
|
||||
vibrate();
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Toggle torch failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 从相册选择
|
||||
function handleGalleryClick() {
|
||||
stopScanning();
|
||||
emit("scanFromGallery");
|
||||
}
|
||||
|
||||
// 取消扫描
|
||||
function handleCancel() {
|
||||
stopScanning();
|
||||
emit("cancel");
|
||||
}
|
||||
|
||||
// 组件挂载后自动开始扫描
|
||||
onMounted(() => {
|
||||
startScanning();
|
||||
});
|
||||
|
||||
// 组件卸载时确保停止扫描
|
||||
// 组件卸载前停止扫描
|
||||
onUnmounted(() => {
|
||||
if (isScanning.value) {
|
||||
stopScan();
|
||||
}
|
||||
stopScanning();
|
||||
});
|
||||
</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>
|
||||
<div class="qr-scanner-container">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="qr-scanner-header">
|
||||
<IonButton fill="clear" class="header-btn" @click="handleCancel">
|
||||
<IonIcon slot="icon-only" :icon="closeOutline" />
|
||||
</IonButton>
|
||||
|
||||
<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 class="header-title">
|
||||
{{ title }}
|
||||
</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>
|
||||
<IonButton fill="clear" class="header-btn" @click="toggleTorch">
|
||||
<IonIcon slot="icon-only" :icon="flashlightOutline" :class="{ 'torch-on': isTorchEnabled }" />
|
||||
</IonButton>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="p-6 bg-white">
|
||||
<IonButton
|
||||
expand="block"
|
||||
fill="outline"
|
||||
@click="handleClose"
|
||||
>
|
||||
取消扫描
|
||||
</IonButton>
|
||||
</div>
|
||||
<!-- 扫描区域 -->
|
||||
<div class="qr-scanner-content">
|
||||
<!-- 扫描框 -->
|
||||
<div class="scan-frame">
|
||||
<!-- 四个角的装饰 -->
|
||||
<div class="corner corner-tl" />
|
||||
<div class="corner corner-tr" />
|
||||
<div class="corner corner-bl" />
|
||||
<div class="corner corner-br" />
|
||||
|
||||
<!-- 扫描线 -->
|
||||
<div v-if="isScanning" class="scan-line" />
|
||||
</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 class="scan-hint">
|
||||
{{ t("scanner.hint") }}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="qr-scanner-footer">
|
||||
<IonButton expand="block" fill="clear" class="footer-btn" @click="handleGalleryClick">
|
||||
<IonIcon slot="start" :icon="imagesOutline" />
|
||||
{{ t("scanner.fromGallery") }}
|
||||
</IonButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qr-scanner-content {
|
||||
--background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
.qr-scanner-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 当扫描时,隐藏背景以显示相机预览 */
|
||||
.qr-scanner-content.scanning {
|
||||
/* 顶部操作栏 */
|
||||
.qr-scanner-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: env(safe-area-inset-top) 8px 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
--color: white;
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: white;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
|
||||
.torch-on {
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
/* 扫描内容区 */
|
||||
.qr-scanner-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 扫描框 */
|
||||
.scan-frame {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
position: relative;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* 四个角 */
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-color: #00d4ff;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.corner-tl {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-width: 3px 0 0 3px;
|
||||
border-radius: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.corner-tr {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-width: 3px 3px 0 0;
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.corner-bl {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-width: 0 0 3px 3px;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.corner-br {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-width: 0 3px 3px 0;
|
||||
border-radius: 0 0 4px 0;
|
||||
}
|
||||
|
||||
/* 扫描线动画 */
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
|
||||
box-shadow: 0 0 10px #00d4ff;
|
||||
animation: scan 2s linear infinite;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 提示文字 */
|
||||
.scan-hint {
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 底部操作区 */
|
||||
.qr-scanner-footer {
|
||||
padding: 0 20px env(safe-area-inset-bottom);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
padding-top: 20px;
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
--color: white;
|
||||
--border-radius: 12px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.footer-btn ion-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式:隐藏 body 内容以显示相机 */
|
||||
body.barcode-scanner-active {
|
||||
background: transparent !important;
|
||||
--ion-background-color: transparent !important;
|
||||
}
|
||||
|
||||
body.barcode-scanner-active ion-app,
|
||||
body.barcode-scanner-active ion-content {
|
||||
background: transparent !important;
|
||||
--background: transparent !important;
|
||||
}
|
||||
|
||||
/* Modal 样式 */
|
||||
.qr-scanner-modal {
|
||||
--background: transparent;
|
||||
--backdrop-opacity: 0;
|
||||
}
|
||||
|
||||
.qr-scanner-modal ion-modal {
|
||||
--background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,71 @@
|
||||
import { BarcodeScanner } from "@capacitor-community/barcode-scanner";
|
||||
import { CapacitorBarcodeScanner } from "@capacitor/barcode-scanner";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { toastController } from "@ionic/vue";
|
||||
|
||||
export interface QRScanResult {
|
||||
text: string;
|
||||
format: string;
|
||||
rawValue: string;
|
||||
displayValue: string;
|
||||
}
|
||||
|
||||
export interface ScannerOptions {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function useQRScanner() {
|
||||
const { t } = useI18n();
|
||||
const { vibrate } = useHaptics();
|
||||
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 showError = async (message: string) => {
|
||||
const toast = await toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: "top",
|
||||
color: "danger",
|
||||
});
|
||||
await toast.present();
|
||||
};
|
||||
|
||||
// 开始扫描
|
||||
const startScan = async (): Promise<QRScanResult | null> => {
|
||||
const openScanner = async (options?: ScannerOptions): Promise<QRScanResult | null> => {
|
||||
try {
|
||||
// 检查是否为原生平台
|
||||
if (!isSupported) {
|
||||
console.warn("QR Scanner is only supported on native platforms");
|
||||
await showError(t("scanner.notSupported"));
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const hasPermission = await checkPermission();
|
||||
if (!hasPermission) {
|
||||
throw new Error("Camera permission denied");
|
||||
}
|
||||
vibrate();
|
||||
|
||||
// 隐藏背景以显示相机预览
|
||||
await BarcodeScanner.hideBackground();
|
||||
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||
hint: 0,
|
||||
scanInstructions: options?.title || t("scanner.hint"),
|
||||
cameraDirection: 1,
|
||||
});
|
||||
|
||||
// 开始扫描
|
||||
const result = await BarcodeScanner.startScan();
|
||||
|
||||
// 恢复背景
|
||||
await BarcodeScanner.showBackground();
|
||||
|
||||
if (result?.hasContent) {
|
||||
if (result.ScanResult) {
|
||||
vibrate();
|
||||
return {
|
||||
text: result.content,
|
||||
format: result.format || "QR_CODE",
|
||||
text: result.ScanResult,
|
||||
format: result.format?.toString() || "UNKNOWN",
|
||||
rawValue: result.ScanResult,
|
||||
displayValue: result.ScanResult,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
catch (error: any) {
|
||||
console.log("error.message", error.message);
|
||||
if (error.code !== "OS-PLUG-BARC-0006") {
|
||||
await showError(t("scanner.openError"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
checkPermission,
|
||||
startScan,
|
||||
stopScan,
|
||||
openScanner,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
"transfer": "Transfer",
|
||||
"balance": "Balance"
|
||||
},
|
||||
"scanner": {
|
||||
"title": "Scan QR Code",
|
||||
"hint": "Align QR code within frame to scan",
|
||||
"fromGallery": "Choose from Gallery",
|
||||
"notSupported": "QR code scanning not supported on this platform",
|
||||
"permissionDenied": "Camera permission denied",
|
||||
"openError": "Failed to open scanner",
|
||||
"noQRCodeFound": "No QR code detected",
|
||||
"galleryError": "Failed to read from gallery",
|
||||
"galleryNotSupported": "Gallery selection not supported currently"
|
||||
},
|
||||
"recharge": {
|
||||
"channel": {
|
||||
"chainRecharge": "Chain recharge",
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
"transfer": "转账",
|
||||
"balance": "余额"
|
||||
},
|
||||
"scanner": {
|
||||
"title": "扫描二维码",
|
||||
"hint": "将二维码对准扫描框进行扫描",
|
||||
"fromGallery": "从相册选择",
|
||||
"notSupported": "当前平台不支持二维码扫描",
|
||||
"permissionDenied": "相机权限被拒绝",
|
||||
"openError": "打开扫描器失败",
|
||||
"noQRCodeFound": "未识别到二维码",
|
||||
"galleryError": "读取相册失败",
|
||||
"galleryNotSupported": "当前不支持从相册选择"
|
||||
},
|
||||
"recharge": {
|
||||
"channel": {
|
||||
"chainRecharge": "链上充值",
|
||||
|
||||
@@ -10,6 +10,7 @@ import WalletCard from "./components/wallet-card.vue";
|
||||
|
||||
const { vibrate } = useHaptics();
|
||||
const walletStore = useWalletStore();
|
||||
const { openScanner } = useQRScanner();
|
||||
|
||||
async function handleRefresh(event: RefresherCustomEvent) {
|
||||
vibrate();
|
||||
@@ -18,6 +19,20 @@ async function handleRefresh(event: RefresherCustomEvent) {
|
||||
event.target.complete();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 处理扫描二维码
|
||||
async function handleScan() {
|
||||
vibrate();
|
||||
const result = await openScanner({
|
||||
title: "扫描二维码",
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log("扫描结果:", result);
|
||||
// TODO: 根据扫描结果进行相应处理
|
||||
// 例如:跳转到对应页面、显示信息等
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,7 +40,7 @@ async function handleRefresh(event: RefresherCustomEvent) {
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar class="ui-toolbar">
|
||||
<div slot="end">
|
||||
<ion-button fill="clear">
|
||||
<ion-button fill="clear" @click="handleScan">
|
||||
<ion-icon slot="icon-only" :icon="scanOutline" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear">
|
||||
|
||||
Reference in New Issue
Block a user