uni-app 全量权限:统一别名、归一状态与多端分流

插件:lf-permission 1.0.0
地址:https://ext.dcloud.net.cn/plugin?id=29096

业务场景:相机、定位、相册、通知、悬浮窗等权限在 Android、iOS、鸿蒙、小程序、H5 上检测与申请方式不同。本文说明别名表、状态机、各端实现、用法与真机问题。


需求

三类能力:

  • 检测:只读 check,不弹系统授权框
  • 申请:request / requestMany;特殊权限跳转设置页
  • 组合:ensure = 检测 → 申请 → 按需打开设置 → 再检测

主 API:

check(permission)
request(permission)
requestMany(permissions, { stopOnDenied })
ensure(permission, { request, openSettingOnDenied })
openSetting({ type, permission })

返回统一结构:

{
  permission: 'camera',
  status: 'granted', // granted | denied | permanentlyDenied | limited | serviceOff | unsupported | unknown
  platform: 'android',
  native: 'android.permission.CAMERA',
  kind: 'runtime',
  special: false,
  canRequest: true,
  supported: true,
  message: 'granted'
}

状态含义:

status 含义
granted 已授权
denied 未授权
permanentlyDenied 永久拒绝 /「不再询问」
limited 有限授权(iOS 部分相册)
serviceOff 系统开关关闭
unsupported 当前端无此项
unknown 无法判断

平台差异

环境 行为
Android plus.android.requestPermissions;API 33+ 使用 READ_MEDIA_* / POST_NOTIFICATIONS;悬浮窗等特殊权限走 Intent
iOS uni.getAppAuthorizeSetting + 原生 authorization / request;永久拒绝后打开 App 设置
鸿蒙 uni 授权查询与 openAppAuthorizeSetting
小程序 getSetting / authorize / openSetting,别名映射 scope.*
H5 Permissions API / getUserMedia / Notification;openSetting 返回 false
nvue 不依赖 document 监听 plusready,轮询 plus 就绪
uni-app x UTS 代码在 utssdk.pending;Vue / App 使用 index.js

处理流程:

别名 / 原生权限串
      │
      ├─ android ── runtime 申请 / special 跳设置
      ├─ ios ────── AppAuthorize + 原生 request
      ├─ harmony ── uni 授权 API
      ├─ mp-* ───── scope authorize
      └─ web ────── Permissions / getUserMedia

权限种类:

kind 含义 行为
runtime 运行时权限 request 弹系统框
special 特殊权限 check + openSetting
service 系统开关 如定位服务;关闭为 serviceOff
privacy 隐私权限 iOS 首次触发系统弹框

引入

import {
  check,
  request,
  requestMany,
  ensure,
  openSetting,
  isSupported,
  listPermissions,
  getPlatform,
  PermissionStatus,
  SettingType
} from '@/uni_modules/lf-permission/index.js'

使用 index.js 入口。

插件不写入工程权限声明。用到哪项,在 manifest / iOS Privacy / 小程序后台声明哪项。


用法

只读检测

const ret = await check('camera')
if (ret.status === PermissionStatus.GRANTED) {
  // 已授权
} else if (ret.status === PermissionStatus.UNSUPPORTED) {
  // 当前端无此项
} else {
  // 未授权
}

申请与永久拒绝

const ret = await request('location')
if (ret.status === PermissionStatus.GRANTED) {
  // 继续
} else if (ret.status === PermissionStatus.PERMANENTLY_DENIED) {
  await openSetting({ permission: 'location' })
}

ensure

const ret = await ensure('microphone', {
  request: true,
  openSettingOnDenied: true
})
if (ret.status !== PermissionStatus.GRANTED) {
  uni.showToast({ title: '需要麦克风权限', icon: 'none' })
  return
}

定位:先系统开关,再 App 权限

系统定位开关与 App 定位权限是两层状态,分开检测:

async function getMyLocation() {
  let service = await check('locationService')
  if (service.status === PermissionStatus.SERVICE_OFF) {
    await openSetting({ type: SettingType.LOCATION_SERVICE })
    service = await check('locationService')
    if (service.status !== PermissionStatus.GRANTED) {
      uni.showToast({ title: '请开启系统定位', icon: 'none' })
      return
    }
  }

  const appLoc = await ensure('location', {
    request: true,
    openSettingOnDenied: true
  })
  if (appLoc.status !== PermissionStatus.GRANTED) {
    uni.showToast({ title: '需要定位权限', icon: 'none' })
    return
  }

  uni.getLocation({
    type: 'gcj02',
    success: (res) => {
      console.log(res.latitude, res.longitude)
    }
  })
}

从设置页返回后复查

特殊权限打开设置后,授权结果在用户返回时才变化。在 onShowcheck

export default {
  data() {
    return { waiting: '' }
  },
  async onShow() {
    if (!this.waiting) return
    const ret = await check(this.waiting)
    this.waiting = ''
    // 按 ret.status 继续业务
  },
  methods: {
    async askOverlay() {
      this.waiting = 'overlay'
      await openSetting({ permission: 'overlay' })
    }
  }
}

打开设置类型

await openSetting({ type: SettingType.APP })
await openSetting({ type: SettingType.LOCATION_SERVICE })
await openSetting({ type: SettingType.NOTIFICATION })
await openSetting({ type: SettingType.OVERLAY })
await openSetting({ type: SettingType.BATTERY })
await openSetting({ type: SettingType.INSTALL })
await openSetting({ type: SettingType.MANAGE_STORAGE })

权限别名(部分)

完整表见 readme.md / permissions.js。业务使用别名;也支持直接传 android.permission.XXXscope.xxxohos.permission.XXX

别名 说明 kind Android iOS / 小程序
location 精确定位 runtime ACCESS_FINE_LOCATION WhenInUse / scope.userLocation
locationService 系统定位开关 service 系统 Location 系统开关 / locationEnabled
camera 相机 runtime CAMERA camera / scope.camera
microphone 麦克风 runtime RECORD_AUDIO record / scope.record
photoRead 读相册/媒体 runtime API 33+ READ_MEDIA_* photoLibrary
notification 通知 runtime API 33+ POST_NOTIFICATIONS 通知授权
overlay 悬浮窗 special SYSTEM_ALERT_WINDOW 仅 Android
bluetooth 蓝牙 runtime API 31+ BLUETOOTH_* bluetooth / scope.bluetooth
manageExternalStorage 所有文件访问 special MANAGE_EXTERNAL_STORAGE 审核风险高

电话、短信、通话记录等敏感权限在别名表中。无业务需求时不要在 manifest 声明。


实现要点

Android 运行时权限

plus.android.requestPermissions(
  natives,
  (resultObj) => {
    // granted / deniedPresent / deniedAlways
    // deniedAlways → permanentlyDenied
  },
  (error) => { /* unknown */ }
)

检测使用 ContextCompat.checkSelfPermission;不可用时回退 support 包或 checkSelfPermission

媒体与存储按 SDK_INT 择权:

  • API 33+:READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / POST_NOTIFICATIONS
  • 更低:READ_EXTERNAL_STORAGE

Android 特殊权限

Settings.canDrawOverlays(main)
// 未授权 → ACTION_MANAGE_OVERLAY_PERMISSION

同类:writeSettingsbatteryOptimizationinstallPackagesmanageExternalStoragescheduleExactAlarmnotificationListener
request 对这些别名打开设置页,不调用 requestPermissions

iOS

读取 uni.getAppAuthorizeSetting() 字段(cameraAuthorizedlocationAuthorized 等)。
字段不足时使用原生 authorizationStatus
request 触发对应 request*Authorization;状态为 permanentlyDenied 时打开 app-settings:

系统定位开关与 App 定位权限分离:locationServicelocation

小程序

uni.getSetting → authSetting[scope]
uni.authorize({ scope })
uni.openSetting() // 永久拒绝后

别名 locationscope.userLocationcamerascope.camera

nvue / plus 就绪

// 有 document:监听 plusready
// 无 DOM:轮询 typeof plus !== 'undefined'

各端声明(部分)

Android(用到再加):

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

iOS Privacy Key(未填用途文案时系统拒绝授权):

用途 Key
相机 NSCameraUsageDescription
麦克风 NSMicrophoneUsageDescription
相册 NSPhotoLibraryUsageDescription
定位 NSLocationWhenInUseUsageDescription

真机问题

  1. 永久拒绝后继续弹授权框无效
    状态为 permanentlyDenied。调用 openSetting 打开设置页。

  2. App 定位已授权仍拿不到坐标
    系统定位总开关为 serviceOff。先 check('locationService')

  3. iOS 跳转定位设置无反应
    私有 App-Prefs 在高版本受限。回退 app-settings: / uni.openAppAuthorizeSetting

  4. Android 13 读相册失败
    使用别名 photoRead,插件按 API 选择 READ_MEDIA_*

  5. 通知权限在 Android 12 申请无效果
    POST_NOTIFICATIONS 从 API 33 起才是运行时权限。更低版本检测通知开关,打开通知设置页。

  6. 悬浮窗 request 后立刻 check 仍是 denied
    用户还在设置页。在 onShow 复查。

  7. 导入报 UTS 编译失败
    使用 @/uni_modules/lf-permission/index.js。UTS 代码位于 utssdk.pending

  8. nvue 中 plus 为 undefined
    未等待 plus 就绪。插件内部轮询;业务侧对权限 API 使用 await

  9. 小程序 authorize 失败且无法再弹
    状态为 permanentlyDenied,调用 openSetting

  10. H5 openSetting 为 false
    浏览器无法打开站点权限页。用文案引导用户到地址栏站点设置。

  11. 声明了电话权限但业务未用
    应用市场上架被拒。无需求不要写入 phone / sms 等权限。

  12. manageExternalStorage 上架失败
    所有文件访问属敏感特殊权限。无强需求不要使用。

  13. 后台定位申请失败
    Android 10+ 须先具备前台定位,再申请 locationBackground

  14. 业务用数字码判断权限
    使用 PermissionStatus 字符串比较,例如 ret.status === PermissionStatus.GRANTED


排查

console.log(getPlatform())
console.log(await check('camera'))
console.log(listPermissions({ onlyCurrent: true }))

检查项:

  1. 当前端 platform 与预期是否一致
  2. ret.native 是否为当前 API 应申请的权限串
  3. manifest / plist / 小程序是否已声明
  4. statusdenied 还是 permanentlyDenied / serviceOff
  5. 特殊权限是否在 onShow 复查
  6. 引入路径是否为 index.js

Demo:/uni_modules/lf-permission/pages/demo/demo
逐项「检查 / 申请 / ensure / 设置」。


源码目录

uni_modules/lf-permission/
├── index.js              # 对外 API
├── constants.js          # 状态 / SettingType
├── permissions.js        # 别名全表
├── core.js               # 结果构造、平台探测
├── platforms/
│   ├── android.js
│   ├── ios.js
│   ├── harmony.js
│   ├── mp.js
│   └── web.js
├── pages/demo/demo.vue
├── utssdk.pending/       # uni-app x(启用时改名为 utssdk)
├── readme.md
├── changelog.md
└── article.md

接入要求:

  1. 插件目录为 uni_modules/lf-permission
  2. @/uni_modules/lf-permission/index.js 引入
  3. 按业务声明各端权限与 iOS 用途文案
  4. 定位场景区分 locationServicelocation
  5. 特殊权限在设置返回后复查
  6. 敏感权限无需求不声明

调用示例:

await ensure('camera', {
  request: true,
  openSettingOnDenied: true
})

问题反馈

插件:https://ext.dcloud.net.cn/plugin?id=29096

邮箱:lingfugroup@gmail.com

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐