From 5ff44e4de0c41cfcf08a15b7d6ef41e9b98cb2ee Mon Sep 17 00:00:00 2001 From: Seven Date: Wed, 24 Dec 2025 04:34:40 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=89=AB=E6=8F=8F=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/copilot-instructions.md | 2 + auto-imports.d.ts | 2 +- ios/App/App.xcodeproj/project.pbxproj | 6 +- ios/App/App/Info.plist | 121 ++++---- ios/App/CapApp-SPM/Package.swift | 2 + package.json | 1 + pnpm-lock.yaml | 18 ++ src/components/qr-scanner/index.vue | 388 +++++++++++++++++++------- src/composables/useQRScanner.ts | 89 +++--- src/locales/en-US.json | 11 + src/locales/zh-CN.json | 11 + src/views/user/index.vue | 17 +- 12 files changed, 450 insertions(+), 218 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index dbc3f52..9ebfcd1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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关键字定义,一般不要使用箭头函数 diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 65740c1..d19b18a 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -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' diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index aa68e9e..6a044b7 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -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; diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 1942361..2bfab14 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -2,64 +2,65 @@ - CAPACITOR_DEBUG - $(CAPACITOR_DEBUG) - CFBundleDevelopmentRegion - en - CFBundleDisplayName - riwa-ionic - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - NSCameraUsageDescription - This app needs access to camera to scan QR codes - NSPrivacyAccessedAPITypes - - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp - NSPrivacyAccessedAPITypeReasons - - C617.1 - - - + CAPACITOR_DEBUG + $(CAPACITOR_DEBUG) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + riwa-ionic + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSCameraUsageDescription + This app needs access to camera to scan QR codes + NSPhotoLibraryUsageDescription + This app needs access to photo library to select images for QR code scanning + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + - \ No newline at end of file + diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index cfbfd4e..d410f9a 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -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"), diff --git a/package.json b/package.json index b7aa638..d5b9ffe 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d39f4fd..90b2429 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/qr-scanner/index.vue b/src/components/qr-scanner/index.vue index ed4af41..c5fa52e 100644 --- a/src/components/qr-scanner/index.vue +++ b/src/components/qr-scanner/index.vue @@ -1,162 +1,342 @@ - + + diff --git a/src/composables/useQRScanner.ts b/src/composables/useQRScanner.ts index 6cec0f0..ac79842 100644 --- a/src/composables/useQRScanner.ts +++ b/src/composables/useQRScanner.ts @@ -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 => { - 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 => { + const openScanner = async (options?: ScannerOptions): Promise => { 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, }; } diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 27b4eb4..93e5a73 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -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", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 083b3a5..4c97dc0 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -6,6 +6,17 @@ "transfer": "转账", "balance": "余额" }, + "scanner": { + "title": "扫描二维码", + "hint": "将二维码对准扫描框进行扫描", + "fromGallery": "从相册选择", + "notSupported": "当前平台不支持二维码扫描", + "permissionDenied": "相机权限被拒绝", + "openError": "打开扫描器失败", + "noQRCodeFound": "未识别到二维码", + "galleryError": "读取相册失败", + "galleryNotSupported": "当前不支持从相册选择" + }, "recharge": { "channel": { "chainRecharge": "链上充值", diff --git a/src/views/user/index.vue b/src/views/user/index.vue index a983851..4bc85eb 100644 --- a/src/views/user/index.vue +++ b/src/views/user/index.vue @@ -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: 根据扫描结果进行相应处理 + // 例如:跳转到对应页面、显示信息等 + } +}