feat: add QRCode scanner
This commit is contained in:
5
auto-imports.d.ts
vendored
5
auto-imports.d.ts
vendored
@@ -270,6 +270,7 @@ declare global {
|
|||||||
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
|
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
|
||||||
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
|
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
|
||||||
const usePrevious: typeof import('@vueuse/core').usePrevious
|
const usePrevious: typeof import('@vueuse/core').usePrevious
|
||||||
|
const useQRScanner: typeof import('./src/composables/useQRScanner').useQRScanner
|
||||||
const useRafFn: typeof import('@vueuse/core').useRafFn
|
const useRafFn: typeof import('@vueuse/core').useRafFn
|
||||||
const useRefHistory: typeof import('@vueuse/core').useRefHistory
|
const useRefHistory: typeof import('@vueuse/core').useRefHistory
|
||||||
const useResetRef: typeof import('./src/composables/useResetRef').useResetRef
|
const useResetRef: typeof import('./src/composables/useResetRef').useResetRef
|
||||||
@@ -354,6 +355,9 @@ declare global {
|
|||||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
export type { QRScanResult } from './src/composables/useQRScanner'
|
||||||
|
import('./src/composables/useQRScanner')
|
||||||
|
// @ts-ignore
|
||||||
export type { PageInstance, InputInstance, ModalInstance, FormInstance } from './src/utils/ionic-helper'
|
export type { PageInstance, InputInstance, ModalInstance, FormInstance } from './src/utils/ionic-helper'
|
||||||
import('./src/utils/ionic-helper')
|
import('./src/utils/ionic-helper')
|
||||||
}
|
}
|
||||||
@@ -598,6 +602,7 @@ declare module 'vue' {
|
|||||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
|
readonly useQRScanner: UnwrapRef<typeof import('./src/composables/useQRScanner')['useQRScanner']>
|
||||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||||
readonly useResetRef: UnwrapRef<typeof import('./src/composables/useResetRef')['useResetRef']>
|
readonly useResetRef: UnwrapRef<typeof import('./src/composables/useResetRef')['useResetRef']>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const config: CapacitorConfig = {
|
|||||||
appName: "riwa-ionic",
|
appName: "riwa-ionic",
|
||||||
webDir: "dist",
|
webDir: "dist",
|
||||||
server: {
|
server: {
|
||||||
url: "http://localhost:5173", // Vite默认端口
|
url: "http://192.168.1.55:5173", // Vite默认端口
|
||||||
cleartext: true, // 允许HTTP连接
|
cleartext: true, // 允许HTTP连接
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
26
components.d.ts
vendored
26
components.d.ts
vendored
@@ -12,16 +12,9 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
Avatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
|
||||||
Collapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
|
||||||
Datetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
|
||||||
Default: typeof import('./src/components/layout/default.vue')['default']
|
|
||||||
Divider: typeof import('./src/components/ui/divider/index.vue')['default']
|
|
||||||
IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
||||||
IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
||||||
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
Input: typeof import('./src/components/ui/input/index.vue')['default']
|
|
||||||
InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
|
||||||
IonApp: typeof import('@ionic/vue')['IonApp']
|
IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
@@ -48,14 +41,13 @@ declare module 'vue' {
|
|||||||
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
|
IonText: typeof import('@ionic/vue')['IonText']
|
||||||
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
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']
|
||||||
Result: typeof import('./src/components/ui/result/index.vue')['default']
|
QrScanner: typeof import('./src/components/qr-scanner/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
TabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
|
||||||
Tabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
|
||||||
UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
@@ -65,22 +57,14 @@ declare module 'vue' {
|
|||||||
UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
||||||
UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
UiTabsTabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
const Avatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
|
||||||
const Collapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
|
||||||
const Datetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
|
||||||
const Default: typeof import('./src/components/layout/default.vue')['default']
|
|
||||||
const Divider: typeof import('./src/components/ui/divider/index.vue')['default']
|
|
||||||
const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
||||||
const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
||||||
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
const Input: typeof import('./src/components/ui/input/index.vue')['default']
|
|
||||||
const InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
|
||||||
const IonApp: typeof import('@ionic/vue')['IonApp']
|
const IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
@@ -107,14 +91,13 @@ declare global {
|
|||||||
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
|
const IonText: typeof import('@ionic/vue')['IonText']
|
||||||
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
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 Result: typeof import('./src/components/ui/result/index.vue')['default']
|
const QrScanner: typeof import('./src/components/qr-scanner/index.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
const RouterView: typeof import('vue-router')['RouterView']
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
const TabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
|
||||||
const Tabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
|
||||||
const UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
const UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
const UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
const UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
const UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
const UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
@@ -124,5 +107,4 @@ declare global {
|
|||||||
const UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
const UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
const UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
const UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
||||||
const UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
const UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
const UiTabsTabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
|
||||||
}
|
}
|
||||||
@@ -47,5 +47,7 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs access to camera to scan QR codes</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -13,6 +13,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.0.0"),
|
.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: "CapacitorApp", path: "../../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/app"),
|
||||||
|
.package(name: "CapacitorCamera", path: "../../../node_modules/.pnpm/@capacitor+camera@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/camera"),
|
||||||
.package(name: "CapacitorHaptics", path: "../../../node_modules/.pnpm/@capacitor+haptics@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/haptics"),
|
.package(name: "CapacitorHaptics", path: "../../../node_modules/.pnpm/@capacitor+haptics@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/haptics"),
|
||||||
.package(name: "CapacitorKeyboard", path: "../../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/keyboard"),
|
.package(name: "CapacitorKeyboard", path: "../../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/keyboard"),
|
||||||
.package(name: "CapacitorStatusBar", path: "../../../node_modules/.pnpm/@capacitor+status-bar@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/status-bar")
|
.package(name: "CapacitorStatusBar", path: "../../../node_modules/.pnpm/@capacitor+status-bar@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/status-bar")
|
||||||
@@ -24,6 +25,7 @@ let package = Package(
|
|||||||
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
||||||
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
||||||
.product(name: "CapacitorApp", package: "CapacitorApp"),
|
.product(name: "CapacitorApp", package: "CapacitorApp"),
|
||||||
|
.product(name: "CapacitorCamera", package: "CapacitorCamera"),
|
||||||
.product(name: "CapacitorHaptics", package: "CapacitorHaptics"),
|
.product(name: "CapacitorHaptics", package: "CapacitorHaptics"),
|
||||||
.product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"),
|
.product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"),
|
||||||
.product(name: "CapacitorStatusBar", package: "CapacitorStatusBar")
|
.product(name: "CapacitorStatusBar", package: "CapacitorStatusBar")
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -6,15 +6,20 @@
|
|||||||
"description": "An Ionic project",
|
"description": "An Ionic project",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"run:ios": "ionic capacitor run ios -l --external",
|
||||||
|
"run:android": "ionic capacitor run android -l --external",
|
||||||
"test:e2e": "cypress run",
|
"test:e2e": "cypress run",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint --fix"
|
"lint:fix": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/barcode-scanner": "^4.0.1",
|
||||||
|
"@capacitor-mlkit/barcode-scanning": "^8.0.0",
|
||||||
"@capacitor/app": "8.0.0",
|
"@capacitor/app": "8.0.0",
|
||||||
|
"@capacitor/camera": "^8.0.0",
|
||||||
"@capacitor/core": "8.0.0",
|
"@capacitor/core": "8.0.0",
|
||||||
"@capacitor/haptics": "8.0.0",
|
"@capacitor/haptics": "8.0.0",
|
||||||
"@capacitor/ios": "^8.0.0",
|
"@capacitor/ios": "^8.0.0",
|
||||||
@@ -23,7 +28,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.36:9527/api/riwa-api-types-0.0.24.tgz",
|
"@riwa/api-types": "http://192.168.1.36:9527/api/riwa-api-types-0.0.29.tgz",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vee-validate/yup": "^4.15.1",
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"@iconify-json/ic": "^1.2.4",
|
"@iconify-json/ic": "^1.2.4",
|
||||||
"@iconify-json/material-icon-theme": "^1.2.44",
|
"@iconify-json/material-icon-theme": "^1.2.44",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@ionic/cli": "^7.2.1",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
|
|||||||
763
pnpm-lock.yaml
generated
763
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
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