| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- <template>
- <view class="cwg-language cursor-pointer" :data-tooltip="t('Downloadpage.item1')">
- <view class="pc-header-btn" @click="handleScanClick">
- <cwg-icon name="crm-scan" color="#97A1C0" :size="20" />
- <cwg-match-media :min-width="791">
- <view>{{ t('vu.login.item9') }}</view>
- </cwg-match-media>
- </view>
- <cwg-confirm-popup />
- <canvas :canvas-id="CANVAS_ID" :id="CANVAS_ID" class="scan-decode-canvas" />
- </view>
- </template>
- <script setup lang="ts">
- import { ref, getCurrentInstance } from 'vue'
- import { useI18n } from 'vue-i18n'
- import { userApi } from '@/api/user'
- import { useConfirm } from '@/hooks/useConfirm'
- import { decodeQrFromAlbumImage, CANVAS_ID } from '@/utils/decodeQrImage'
- import {
- ensureScanCameraPermission,
- ensureScanAlbumPermission,
- isPermissionError,
- isIosPermissionDenied,
- shouldGuideToSettings,
- openAppPermissionSetting,
- } from '@/utils/scanPermission'
- type ScanPermissionType = 'camera' | 'album'
- const confirm = useConfirm()
- const { t } = useI18n()
- const instance = getCurrentInstance()
- const SCAN_LOGIN_SCENE_REG = /^[a-f0-9]{32}$/i
- function parseScene(result: string): string {
- if (!result) return ''
- const trimmed = result.trim()
- const directMatch = trimmed.match(/[a-f0-9]{32}/i)
- if (directMatch?.[0]) return directMatch[0]
- try {
- const url = new URL(trimmed)
- const sceneParam = url.searchParams.get('scene')
- if (sceneParam && SCAN_LOGIN_SCENE_REG.test(sceneParam)) return sceneParam
- const lastPath = url.pathname.split('/').pop() || ''
- if (SCAN_LOGIN_SCENE_REG.test(lastPath)) return lastPath
- return trimmed
- } catch {
- return trimmed
- }
- }
- function isValidScanLoginScene(value: string): boolean {
- return SCAN_LOGIN_SCENE_REG.test(value)
- }
- const scene = ref('')
- const scanLoadingVisible = ref(false)
- function showScanLoading() {
- if (scanLoadingVisible.value) return
- scanLoadingVisible.value = true
- uni.showLoading({ title: t('vu.login.item14'), mask: true })
- }
- function hideScanLoading() {
- if (!scanLoadingVisible.value) return
- scanLoadingVisible.value = false
- uni.hideLoading()
- }
- function isUserCancelled(error: any) {
- if (error?._userCancelled) return true
- const errMsg = String(error?.errMsg || error?.message || error).toLowerCase()
- return (
- errMsg.includes('cancel')
- || errMsg.includes('取消')
- || errMsg.includes('no image')
- )
- }
- function isQrDecodeError(error: any) {
- if (error?._decodeFailed) return true
- const errMsg = String(error?.errMsg || error?.message || error).toLowerCase()
- return (
- errMsg.includes('decode')
- || errMsg.includes('识别')
- || errMsg.includes('image load')
- || error?.code === 8
- )
- }
- function getPermissionGuideContent(type: ScanPermissionType) {
- const usage = type === 'camera'
- ? (t('mine.p16') || '相机的使用')
- : (t('mine.p17') || '相册的使用')
- const tip = t('mine.p14') || '首次使用需授权。若已拒绝,请前往系统设置开启权限。'
- return `${usage},${tip}`
- }
- async function guideToPermissionSetting(type: ScanPermissionType) {
- const content = getPermissionGuideContent(type)
- await new Promise((resolve) => setTimeout(resolve, 400))
- return new Promise<void>((resolve) => {
- uni.showModal({
- title: t('Msg.SystemPrompt') || '系统提示',
- content,
- confirmText: t('mine.p13') || '去设置',
- cancelText: t('Btn.Cancel') || '取消',
- showCancel: true,
- success: (res) => {
- if (res.confirm) {
- openAppPermissionSetting()
- }
- resolve()
- },
- fail: () => resolve(),
- })
- })
- }
- async function ensureScanPermission(type: ScanPermissionType): Promise<boolean> {
- const state = type === 'camera'
- ? await ensureScanCameraPermission()
- : await ensureScanAlbumPermission()
- if (state === 'granted') {
- return true
- }
- await guideToPermissionSetting(type)
- return false
- }
- async function runWithPermission<T>(
- type: ScanPermissionType,
- action: () => Promise<T>,
- ): Promise<T> {
- if (!await ensureScanPermission(type)) {
- const err: any = new Error('permission denied')
- err._permissionDenied = true
- err._alreadyGuided = true
- throw err
- }
- try {
- return await action()
- } catch (error: any) {
- if (!error?._alreadyGuided && (
- isIosPermissionDenied(type)
- || shouldGuideToSettings(
- type === 'camera'
- ? await ensureScanCameraPermission()
- : await ensureScanAlbumPermission(),
- )
- || isPermissionError(error)
- )) {
- await guideToPermissionSetting(type)
- error._permissionDenied = true
- error._alreadyGuided = true
- }
- throw error
- }
- }
- function scanByCamera(): Promise<string> {
- return new Promise((resolve, reject) => {
- uni.scanCode({
- onlyFromCamera: true,
- scanType: ['qrCode'],
- success: (res) => resolve(res.result),
- fail: (err) => {
- if (isIosPermissionDenied('camera')) {
- reject({ ...err, _permissionDenied: true })
- return
- }
- reject(err)
- },
- })
- })
- }
- function scanByAlbumApp(path: string): Promise<string> {
- return decodeQrFromAlbumImage(path, instance)
- }
- function scanByAlbum(): Promise<string> {
- // #ifdef APP-PLUS
- return new Promise((resolve, reject) => {
- uni.chooseImage({
- count: 1,
- sourceType: ['album'],
- sizeType: ['compressed'],
- success: async (chooseRes) => {
- const path = chooseRes.tempFilePaths[0]
- if (!path) {
- reject({ message: 'no image', _userCancelled: true })
- return
- }
- showScanLoading()
- try {
- resolve(await scanByAlbumApp(path))
- } catch (err: any) {
- hideScanLoading()
- err._decodeFailed = true
- reject(err)
- }
- },
- fail: (err) => {
- if (isIosPermissionDenied('album')) {
- reject({ ...err, _permissionDenied: true })
- return
- }
- reject({ ...err, _userCancelled: true })
- },
- })
- })
- // #endif
- // #ifndef APP-PLUS
- return new Promise((resolve, reject) => {
- uni.scanCode({
- onlyFromCamera: false,
- scanType: ['qrCode'],
- success: (res) => resolve(res.result),
- fail: reject,
- })
- })
- // #endif
- }
- function pickScanResult(): Promise<string> {
- return new Promise((resolve, reject) => {
- uni.showActionSheet({
- itemList: [t('vu.login.item13'), t('vu.login.item12')],
- success: (sheetRes) => {
- resolve(sheetRes.tapIndex)
- },
- fail: reject,
- })
- }).then(async (tapIndex) => {
- // 等 actionSheet 完全关闭后再检查权限、弹窗,避免 iOS 吞掉 showModal
- await new Promise((r) => setTimeout(r, 400))
- if (tapIndex === 0) {
- return runWithPermission('camera', scanByCamera)
- }
- return runWithPermission('album', scanByAlbum)
- })
- }
- async function processScanResult(rawResult: string) {
- showScanLoading()
- try {
- scene.value = parseScene(rawResult)
- if (!isValidScanLoginScene(scene.value)) {
- hideScanLoading()
- uni.$u.toast(t('vu.login.item10') || t('login.msg0'))
- return
- }
- await userApi.updateAppScanLoginStatus({ scene: scene.value, status: 3 })
- hideScanLoading()
- await confirm({
- title: t('vu.login.item8'),
- content: t('vu.login.item7'),
- confirmText: t('newSignin.item7'),
- cancelText: t('Btn.Cancel'),
- })
- const res = await userApi.confirmAppScanLogin({ scene: scene.value })
- scene.value = ''
- if (res.code === 200) {
- uni.$u.toast(res.msg || t('login.msg0_1'))
- } else {
- uni.$u.toast(res.msg || t('login.msg0'))
- }
- } catch (error) {
- hideScanLoading()
- throw error
- }
- }
- async function handleScanClick() {
- try {
- const rawResult = await pickScanResult()
- await processScanResult(rawResult)
- } catch (error: any) {
- const errMsg = String(error?.errMsg || error?.message || error)
- if (isUserCancelled(error)) {
- if (isValidScanLoginScene(scene.value)) {
- try {
- await userApi.updateAppScanLoginStatus({
- scene: scene.value,
- status: 0,
- })
- scene.value = ''
- } catch {
- // 二维码已过期等情况,忽略回滚失败
- }
- }
- return
- }
- scene.value = ''
- if (isPermissionError(error) || errMsg.includes('permission denied')) {
- return
- }
- if (isQrDecodeError(error)) {
- uni.$u.toast(
- t('vu.login.item15')
- || '图片二维码反光/模糊,请截图后重试或正对屏幕拍摄',
- )
- return
- }
- uni.$u.toast(error?.msg || error?.errMsg || t('login.msg0'))
- }
- }
- </script>
- <style scoped lang="scss">
- @import "@/uni.scss";
- .pc-header-btn {
- width: auto;
- display: flex;
- align-items: center;
- cursor: pointer;
- gap: px2rpx(6);
- padding: 0 px2rpx(5);
- }
- .cwg-language {
- @media screen and (max-width: 991px) {
- :deep(.cwg-dropdown-menu-container) {
- right: px2rpx(-20) !important;
- }
- }
- }
- :deep(.cwg-dropdown-menu-container .menu .menu-item) {
- min-height: px2rpx(36);
- }
- :deep(.cwg-dropdown) {
- overflow: visible !important;
- }
- .scan-decode-canvas {
- position: fixed;
- left: -9999px;
- top: -9999px;
- width: 1200px;
- height: 1200px;
- opacity: 0;
- pointer-events: none;
- }
- </style>
|