zhb 3 hodín pred
rodič
commit
fc150055e3

+ 1 - 1
components/cwg-notice.vue

@@ -11,7 +11,7 @@
                 </cwg-match-media>
             </view>
             <template #btn>
-                <view class="dropdown-menu dropdown-menu-lg-end p-0 w-300px mt-2 show">
+                <view class="dropdown-menu dropdown-menu-lg-end p-0 w-300px mt-1 show">
                     <view class="px-3 py-3 border-bottom d-flex justify-content-between align-items-center">
                         <h6 class="mb-0" v-t="'News.Notice'"></h6>
                     </view>

+ 134 - 23
components/cwg-scan.vue

@@ -17,6 +17,16 @@ 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()
@@ -60,13 +70,112 @@ function hideScanLoading() {
   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: reject,
+      fail: (err) => {
+        if (isIosPermissionDenied('camera')) {
+          reject({ ...err, _permissionDenied: true })
+          return
+        }
+        reject(err)
+      },
     })
   })
 }
@@ -85,18 +194,25 @@ function scanByAlbum(): Promise<string> {
       success: async (chooseRes) => {
         const path = chooseRes.tempFilePaths[0]
         if (!path) {
-          reject(new Error('no image'))
+          reject({ message: 'no image', _userCancelled: true })
           return
         }
         showScanLoading()
         try {
           resolve(await scanByAlbumApp(path))
-        } catch (err) {
+        } catch (err: any) {
           hideScanLoading()
+          err._decodeFailed = true
           reject(err)
         }
       },
-      fail: reject,
+      fail: (err) => {
+        if (isIosPermissionDenied('album')) {
+          reject({ ...err, _permissionDenied: true })
+          return
+        }
+        reject({ ...err, _userCancelled: true })
+      },
     })
   })
   // #endif
@@ -116,18 +232,18 @@ function pickScanResult(): Promise<string> {
   return new Promise((resolve, reject) => {
     uni.showActionSheet({
       itemList: [t('vu.login.item13'), t('vu.login.item12')],
-      success: async (sheetRes) => {
-        try {
-          const result = sheetRes.tapIndex === 0
-            ? await scanByCamera()
-            : await scanByAlbum()
-          resolve(result)
-        } catch (err) {
-          reject(err)
-        }
+      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)
   })
 }
 
@@ -169,8 +285,7 @@ async function handleScanClick() {
     await processScanResult(rawResult)
   } catch (error: any) {
     const errMsg = String(error?.errMsg || error?.message || error)
-    const isCancel = errMsg.includes('cancel') || errMsg.includes('Cancel')
-    if (isCancel) {
+    if (isUserCancelled(error)) {
       if (isValidScanLoginScene(scene.value)) {
         try {
           await userApi.updateAppScanLoginStatus({
@@ -185,14 +300,10 @@ async function handleScanClick() {
       return
     }
     scene.value = ''
-    const isDecodeError = (
-      errMsg.includes('fail')
-      || errMsg.includes('识别')
-      || errMsg.includes('decode')
-      || errMsg.includes('image load')
-      || error?.code === 8
-    )
-    if (isDecodeError) {
+    if (isPermissionError(error) || errMsg.includes('permission denied')) {
+      return
+    }
+    if (isQrDecodeError(error)) {
       uni.$u.toast(
         t('vu.login.item15')
         || '图片二维码反光/模糊,请截图后重试或正对屏幕拍摄',

+ 1 - 1
config/index.ts

@@ -3,7 +3,7 @@ import { getDomainParts, setDomainParts, buildHostUrls } from './domainState'
 // #ifdef H5
 const [p, h] = [window.location.protocol, window.location.host]
 const isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h)
-const [h5Ho, h5Dt] = isIP ? ['cwgbroker', 'club'] : h.split('.').slice(-2)
+const [h5Ho, h5Dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2)
 const h5Ht = p == 'http:' ? 'https:' : p
 setDomainParts({ ho: h5Ho, dt: h5Dt, ht: h5Ht })
 // #endif

+ 0 - 16
pages/common/notice.vue

@@ -175,22 +175,6 @@ listApi.value = newsApi.newsNoticeList
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
 
 .search-bar {
     display: flex;

+ 0 - 16
pages/customer/recording-history.vue

@@ -369,22 +369,6 @@ listApi.value = customApi.CustomRecordAccount
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
 
 .search-bar {
     display: flex;

+ 0 - 17
pages/customer/settings.vue

@@ -338,23 +338,6 @@ listApi.value = customApi.CustomRecordAccount
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
-
 .search-bar {
     display: flex;
     align-items: center;

+ 0 - 17
pages/customer/trade-history.vue

@@ -266,23 +266,6 @@ onLoad(async (e) => {
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
-
 .symbol-cell {
     display: inline-flex;
     align-items: flex-start;

+ 0 - 17
pages/customer/trade-position.vue

@@ -255,23 +255,6 @@ onLoad(async (e) => {
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
-
 .symbol-cell {
     display: inline-flex;
     align-items: flex-start;

+ 0 - 17
pages/follow/record.vue

@@ -362,23 +362,6 @@ watch(() => search.type, (newVal) => {
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
-
 .search-bar {
     display: flex;
     align-items: center;

+ 0 - 17
pages/follow/transfer-history.vue

@@ -257,23 +257,6 @@ onLoad((e) => {
     }
 }
 
-.operation-btn {
-    :deep(span) {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        gap: px2rpx(4);
-        cursor: pointer;
-        background-color: var(--color-slate-150);
-        padding: px2rpx(8) 0;
-    }
-}
-
-.operation-btn.disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-}
-
 .symbol-cell {
     display: inline-flex;
     align-items: flex-start;

+ 241 - 0
utils/scanPermission.js

@@ -0,0 +1,241 @@
+import { permission } from '@/js_sdk/wa-permission/permission.js'
+
+function isAppPlus() {
+  return typeof plus !== 'undefined' && !!plus?.os?.name
+}
+
+export function isIosApp() {
+  if (!isAppPlus()) return false
+  const name = String(plus.os.name || '')
+  return name === 'iOS' || name.includes('iPhone')
+}
+
+function normalizeIosStatus(status) {
+  if (status === null || status === undefined) return -1
+  const num = Number(status)
+  return Number.isNaN(num) ? -1 : num
+}
+
+function getUniAuthorizeSetting() {
+  try {
+    if (typeof uni.getAppAuthorizeSetting === 'function') {
+      return uni.getAppAuthorizeSetting()
+    }
+  } catch (e) {
+    console.error('getUniAuthorizeSetting error', e)
+  }
+  return null
+}
+
+function getUniCameraAuth() {
+  const setting = getUniAuthorizeSetting()
+  return setting?.cameraAuthorized || null
+}
+
+function getUniAlbumAuth() {
+  const setting = getUniAuthorizeSetting()
+  return setting?.albumAuthorized || null
+}
+
+function getIosAuthStatus(type) {
+  if (!isIosApp()) return -1
+  try {
+    if (type === 'camera') {
+      const AVCaptureDevice = plus.ios.importClass('AVCaptureDevice')
+      const status = AVCaptureDevice.authorizationStatusForMediaType('video')
+      return normalizeIosStatus(status)
+    }
+    if (type === 'album') {
+      const PHPhotoLibrary = plus.ios.importClass('PHPhotoLibrary')
+      const status = PHPhotoLibrary.authorizationStatus()
+      return normalizeIosStatus(status)
+    }
+  } catch (e) {
+    console.error('getIosAuthStatus error', e)
+  }
+  return -1
+}
+
+function isIosCameraGranted() {
+  const uniAuth = getUniCameraAuth()
+  if (uniAuth === 'authorized') return true
+  if (uniAuth === 'denied' || uniAuth === 'config error') return false
+
+  const status = getIosAuthStatus('camera')
+  if (status === 3) return true
+  try {
+    return permission.judgeIosPermission('camera') === true
+  } catch {
+    return false
+  }
+}
+
+function isIosAlbumGranted() {
+  const uniAuth = getUniAlbumAuth()
+  if (uniAuth === 'authorized' || uniAuth === 'limited') return true
+  if (uniAuth === 'denied' || uniAuth === 'config error') return false
+
+  const status = getIosAuthStatus('album')
+  return status === 3 || status === 4
+}
+
+function isIosCameraDenied() {
+  const uniAuth = getUniCameraAuth()
+  if (uniAuth === 'denied' || uniAuth === 'config error') return true
+
+  const status = getIosAuthStatus('camera')
+  return status === 1 || status === 2
+}
+
+function isIosAlbumDenied() {
+  const uniAuth = getUniAlbumAuth()
+  if (uniAuth === 'denied' || uniAuth === 'config error') return true
+
+  const status = getIosAuthStatus('album')
+  return status === 1 || status === 2
+}
+
+function requestIosCameraPermission() {
+  return new Promise((resolve) => {
+    let settled = false
+    const done = (granted) => {
+      if (settled) return
+      settled = true
+      resolve(!!granted)
+    }
+
+    setTimeout(() => done(false), 15000)
+
+    try {
+      const AVCaptureDevice = plus.ios.importClass('AVCaptureDevice')
+      AVCaptureDevice.requestAccessForMediaTypecompletionHandler('video', (granted) => {
+        done(granted)
+      })
+    } catch (e) {
+      console.error('requestIosCameraPermission error', e)
+      done(false)
+    }
+  })
+}
+
+function requestIosAlbumPermission() {
+  return new Promise((resolve) => {
+    let settled = false
+    const done = (status) => {
+      if (settled) return
+      settled = true
+      resolve(normalizeIosStatus(status))
+    }
+
+    setTimeout(() => done(-1), 15000)
+
+    try {
+      const PHPhotoLibrary = plus.ios.importClass('PHPhotoLibrary')
+      PHPhotoLibrary.requestAuthorization((status) => {
+        done(status)
+      })
+    } catch (e) {
+      console.error('requestIosAlbumPermission error', e)
+      done(-1)
+    }
+  })
+}
+
+async function ensureIosCameraPermission() {
+  if (isIosCameraGranted()) {
+    return 'granted'
+  }
+
+  if (isIosCameraDenied()) {
+    return 'denied'
+  }
+
+  const granted = await requestIosCameraPermission()
+  if (granted || isIosCameraGranted()) {
+    return 'granted'
+  }
+
+  return 'denied'
+}
+
+async function ensureIosAlbumPermission() {
+  if (isIosAlbumGranted()) {
+    return 'granted'
+  }
+
+  if (isIosAlbumDenied()) {
+    return 'denied'
+  }
+
+  const status = await requestIosAlbumPermission()
+  if (status === 3 || status === 4 || isIosAlbumGranted()) {
+    return 'granted'
+  }
+
+  return 'denied'
+}
+
+function getAndroidAlbumPermission() {
+  const Build = plus.android.importClass('android.os.Build')
+  return Build.VERSION.SDK_INT >= 33
+    ? 'android.permission.READ_MEDIA_IMAGES'
+    : 'android.permission.READ_EXTERNAL_STORAGE'
+}
+
+async function ensureAndroidPermission(permissionID) {
+  const granted = await permission.checkAndroidPermission(permissionID)
+  if (granted === 1) return 'granted'
+
+  const result = await permission.requestAndroidPermission(permissionID)
+  if (result === 1) return 'granted'
+  if (result === -1) return 'denied_always'
+  return 'denied'
+}
+
+export async function ensureScanCameraPermission() {
+  if (!isAppPlus()) return 'granted'
+  if (isIosApp()) return ensureIosCameraPermission()
+  return ensureAndroidPermission('android.permission.CAMERA')
+}
+
+export async function ensureScanAlbumPermission() {
+  if (!isAppPlus()) return 'granted'
+  if (isIosApp()) return ensureIosAlbumPermission()
+  return ensureAndroidPermission(getAndroidAlbumPermission())
+}
+
+export function isIosPermissionDenied(type) {
+  if (!isIosApp()) return false
+  return type === 'camera' ? isIosCameraDenied() : isIosAlbumDenied()
+}
+
+export function isPermissionError(error) {
+  const msg = String(error?.errMsg || error?.message || error).toLowerCase()
+  return (
+    error?._permissionDenied === true
+    || msg.includes('permission')
+    || msg.includes('authorize')
+    || msg.includes('auth deny')
+    || msg.includes('auth')
+    || msg.includes('权限')
+    || msg.includes('no permission')
+    || msg.includes('denied')
+    || msg.includes('拒绝')
+    || msg.includes('未授权')
+  )
+}
+
+export function shouldGuideToSettings(state) {
+  if (state === 'denied_always') return true
+  if (state === 'denied' && isIosApp()) return true
+  return false
+}
+
+export function openAppPermissionSetting() {
+  if (!isAppPlus()) return
+  if (typeof uni.openAppAuthorizeSetting === 'function') {
+    uni.openAppAuthorizeSetting({})
+    return
+  }
+  permission.gotoAppPermissionSetting()
+}