cwg-scan.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <template>
  2. <view class="cwg-language cursor-pointer" :data-tooltip="t('Downloadpage.item1')">
  3. <view class="pc-header-btn" @click="handleScanClick">
  4. <cwg-icon name="crm-scan" color="#97A1C0" :size="20" />
  5. <cwg-match-media :min-width="791">
  6. <view>{{ t('vu.login.item9') }}</view>
  7. </cwg-match-media>
  8. </view>
  9. <cwg-confirm-popup />
  10. <canvas :canvas-id="CANVAS_ID" :id="CANVAS_ID" class="scan-decode-canvas" />
  11. </view>
  12. </template>
  13. <script setup lang="ts">
  14. import { ref, getCurrentInstance } from 'vue'
  15. import { useI18n } from 'vue-i18n'
  16. import { userApi } from '@/api/user'
  17. import { useConfirm } from '@/hooks/useConfirm'
  18. import { decodeQrFromAlbumImage, CANVAS_ID } from '@/utils/decodeQrImage'
  19. const confirm = useConfirm()
  20. const { t } = useI18n()
  21. const instance = getCurrentInstance()
  22. const SCAN_LOGIN_SCENE_REG = /^[a-f0-9]{32}$/i
  23. function parseScene(result: string): string {
  24. if (!result) return ''
  25. const trimmed = result.trim()
  26. const directMatch = trimmed.match(/[a-f0-9]{32}/i)
  27. if (directMatch?.[0]) return directMatch[0]
  28. try {
  29. const url = new URL(trimmed)
  30. const sceneParam = url.searchParams.get('scene')
  31. if (sceneParam && SCAN_LOGIN_SCENE_REG.test(sceneParam)) return sceneParam
  32. const lastPath = url.pathname.split('/').pop() || ''
  33. if (SCAN_LOGIN_SCENE_REG.test(lastPath)) return lastPath
  34. return trimmed
  35. } catch {
  36. return trimmed
  37. }
  38. }
  39. function isValidScanLoginScene(value: string): boolean {
  40. return SCAN_LOGIN_SCENE_REG.test(value)
  41. }
  42. const scene = ref('')
  43. const scanLoadingVisible = ref(false)
  44. function showScanLoading() {
  45. if (scanLoadingVisible.value) return
  46. scanLoadingVisible.value = true
  47. uni.showLoading({ title: t('vu.login.item14'), mask: true })
  48. }
  49. function hideScanLoading() {
  50. if (!scanLoadingVisible.value) return
  51. scanLoadingVisible.value = false
  52. uni.hideLoading()
  53. }
  54. function scanByCamera(): Promise<string> {
  55. return new Promise((resolve, reject) => {
  56. uni.scanCode({
  57. onlyFromCamera: true,
  58. scanType: ['qrCode'],
  59. success: (res) => resolve(res.result),
  60. fail: reject,
  61. })
  62. })
  63. }
  64. function scanByAlbumApp(path: string): Promise<string> {
  65. return decodeQrFromAlbumImage(path, instance)
  66. }
  67. function scanByAlbum(): Promise<string> {
  68. // #ifdef APP-PLUS
  69. return new Promise((resolve, reject) => {
  70. uni.chooseImage({
  71. count: 1,
  72. sourceType: ['album'],
  73. sizeType: ['compressed'],
  74. success: async (chooseRes) => {
  75. const path = chooseRes.tempFilePaths[0]
  76. if (!path) {
  77. reject(new Error('no image'))
  78. return
  79. }
  80. showScanLoading()
  81. try {
  82. resolve(await scanByAlbumApp(path))
  83. } catch (err) {
  84. hideScanLoading()
  85. reject(err)
  86. }
  87. },
  88. fail: reject,
  89. })
  90. })
  91. // #endif
  92. // #ifndef APP-PLUS
  93. return new Promise((resolve, reject) => {
  94. uni.scanCode({
  95. onlyFromCamera: false,
  96. scanType: ['qrCode'],
  97. success: (res) => resolve(res.result),
  98. fail: reject,
  99. })
  100. })
  101. // #endif
  102. }
  103. function pickScanResult(): Promise<string> {
  104. return new Promise((resolve, reject) => {
  105. uni.showActionSheet({
  106. itemList: [t('vu.login.item13'), t('vu.login.item12')],
  107. success: async (sheetRes) => {
  108. try {
  109. const result = sheetRes.tapIndex === 0
  110. ? await scanByCamera()
  111. : await scanByAlbum()
  112. resolve(result)
  113. } catch (err) {
  114. reject(err)
  115. }
  116. },
  117. fail: reject,
  118. })
  119. })
  120. }
  121. async function processScanResult(rawResult: string) {
  122. showScanLoading()
  123. try {
  124. scene.value = parseScene(rawResult)
  125. if (!isValidScanLoginScene(scene.value)) {
  126. hideScanLoading()
  127. uni.$u.toast(t('vu.login.item10') || t('login.msg0'))
  128. return
  129. }
  130. await userApi.updateAppScanLoginStatus({ scene: scene.value, status: 3 })
  131. hideScanLoading()
  132. await confirm({
  133. title: t('vu.login.item8'),
  134. content: t('vu.login.item7'),
  135. confirmText: t('newSignin.item7'),
  136. cancelText: t('Btn.Cancel'),
  137. })
  138. const res = await userApi.confirmAppScanLogin({ scene: scene.value })
  139. scene.value = ''
  140. if (res.code === 200) {
  141. uni.$u.toast(res.msg || t('login.msg0_1'))
  142. } else {
  143. uni.$u.toast(res.msg || t('login.msg0'))
  144. }
  145. } catch (error) {
  146. hideScanLoading()
  147. throw error
  148. }
  149. }
  150. async function handleScanClick() {
  151. try {
  152. const rawResult = await pickScanResult()
  153. await processScanResult(rawResult)
  154. } catch (error: any) {
  155. const errMsg = String(error?.errMsg || error?.message || error)
  156. const isCancel = errMsg.includes('cancel') || errMsg.includes('Cancel')
  157. if (isCancel) {
  158. if (isValidScanLoginScene(scene.value)) {
  159. try {
  160. await userApi.updateAppScanLoginStatus({
  161. scene: scene.value,
  162. status: 0,
  163. })
  164. scene.value = ''
  165. } catch {
  166. // 二维码已过期等情况,忽略回滚失败
  167. }
  168. }
  169. return
  170. }
  171. scene.value = ''
  172. const isDecodeError = (
  173. errMsg.includes('fail')
  174. || errMsg.includes('识别')
  175. || errMsg.includes('decode')
  176. || errMsg.includes('image load')
  177. || error?.code === 8
  178. )
  179. if (isDecodeError) {
  180. uni.$u.toast(
  181. t('vu.login.item15')
  182. || '图片二维码反光/模糊,请截图后重试或正对屏幕拍摄',
  183. )
  184. return
  185. }
  186. uni.$u.toast(error?.msg || error?.errMsg || t('login.msg0'))
  187. }
  188. }
  189. </script>
  190. <style scoped lang="scss">
  191. @import "@/uni.scss";
  192. .pc-header-btn {
  193. width: auto;
  194. display: flex;
  195. align-items: center;
  196. cursor: pointer;
  197. gap: px2rpx(6);
  198. padding: 0 px2rpx(5);
  199. }
  200. .cwg-language {
  201. @media screen and (max-width: 991px) {
  202. :deep(.cwg-dropdown-menu-container) {
  203. right: px2rpx(-20) !important;
  204. }
  205. }
  206. }
  207. :deep(.cwg-dropdown-menu-container .menu .menu-item) {
  208. min-height: px2rpx(36);
  209. }
  210. :deep(.cwg-dropdown) {
  211. overflow: visible !important;
  212. }
  213. .scan-decode-canvas {
  214. position: fixed;
  215. left: -9999px;
  216. top: -9999px;
  217. width: 1200px;
  218. height: 1200px;
  219. opacity: 0;
  220. pointer-events: none;
  221. }
  222. </style>