uni-app 全量权限:统一别名、归一状态与多端分流
uni-app 全量权限:统一别名、归一状态与多端分流
插件:lf-permission1.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 | unknownplatform:'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 / 小程序后台声明哪项。
用法
只读检测
constret=awaitcheck('camera')if(ret.status===PermissionStatus.GRANTED){// 已授权}elseif(ret.status===PermissionStatus.UNSUPPORTED){// 当前端无此项}else{// 未授权}申请与永久拒绝
constret=awaitrequest('location')if(ret.status===PermissionStatus.GRANTED){// 继续}elseif(ret.status===PermissionStatus.PERMANENTLY_DENIED){awaitopenSetting({permission:'location'})}ensure
constret=awaitensure('microphone',{request:true,openSettingOnDenied:true})if(ret.status!==PermissionStatus.GRANTED){uni.showToast({title:'需要麦克风权限',icon:'none'})return}定位:先系统开关,再 App 权限
系统定位开关与 App 定位权限是两层状态,分开检测:
asyncfunctiongetMyLocation(){letservice=awaitcheck('locationService')if(service.status===PermissionStatus.SERVICE_OFF){awaitopenSetting({type:SettingType.LOCATION_SERVICE})service=awaitcheck('locationService')if(service.status!==PermissionStatus.GRANTED){uni.showToast({title:'请开启系统定位',icon:'none'})return}}constappLoc=awaitensure('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)}})}从设置页返回后复查
特殊权限打开设置后,授权结果在用户返回时才变化。在onShow再check:
exportdefault{data(){return{waiting:''}},asynconShow(){if(!this.waiting)returnconstret=awaitcheck(this.waiting)this.waiting=''// 按 ret.status 继续业务},methods:{asyncaskOverlay(){this.waiting='overlay'awaitopenSetting({permission:'overlay'})}}}打开设置类型
awaitopenSetting({type:SettingType.APP})awaitopenSetting({type:SettingType.LOCATION_SERVICE})awaitopenSetting({type:SettingType.NOTIFICATION})awaitopenSetting({type:SettingType.OVERLAY})awaitopenSetting({type:SettingType.BATTERY})awaitopenSetting({type:SettingType.INSTALL})awaitopenSetting({type:SettingType.MANAGE_STORAGE})权限别名(部分)
完整表见readme.md/permissions.js。业务使用别名;也支持直接传android.permission.XXX、scope.xxx、ohos.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同类:writeSettings、batteryOptimization、installPackages、manageExternalStorage、scheduleExactAlarm、notificationListener。request对这些别名打开设置页,不调用requestPermissions。
iOS
读取uni.getAppAuthorizeSetting()字段(cameraAuthorized、locationAuthorized等)。
字段不足时使用原生authorizationStatus。request触发对应request*Authorization;状态为permanentlyDenied时打开app-settings:。
系统定位开关与 App 定位权限分离:locationService与location。
小程序
uni.getSetting → authSetting[scope]uni.authorize({scope})uni.openSetting()// 永久拒绝后别名location→scope.userLocation,camera→scope.camera。
nvue / plus 就绪
// 有 document:监听 plusready// 无 DOM:轮询 typeof plus !== 'undefined'各端声明(部分)
Android(用到再加):
<uses-permissionandroid:name="android.permission.CAMERA"/><uses-permissionandroid:name="android.permission.ACCESS_FINE_LOCATION"/><uses-permissionandroid:name="android.permission.RECORD_AUDIO"/><uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS"/>iOS Privacy Key(未填用途文案时系统拒绝授权):
| 用途 | Key |
|---|---|
| 相机 | NSCameraUsageDescription |
| 麦克风 | NSMicrophoneUsageDescription |
| 相册 | NSPhotoLibraryUsageDescription |
| 定位 | NSLocationWhenInUseUsageDescription |
真机问题
永久拒绝后继续弹授权框无效
状态为permanentlyDenied。调用openSetting打开设置页。App 定位已授权仍拿不到坐标
系统定位总开关为serviceOff。先check('locationService')。iOS 跳转定位设置无反应
私有App-Prefs在高版本受限。回退app-settings:/uni.openAppAuthorizeSetting。Android 13 读相册失败
使用别名photoRead,插件按 API 选择READ_MEDIA_*。通知权限在 Android 12 申请无效果
POST_NOTIFICATIONS从 API 33 起才是运行时权限。更低版本检测通知开关,打开通知设置页。悬浮窗
request后立刻check仍是 denied
用户还在设置页。在onShow复查。导入报 UTS 编译失败
使用@/uni_modules/lf-permission/index.js。UTS 代码位于utssdk.pending。nvue 中
plus为 undefined
未等待 plus 就绪。插件内部轮询;业务侧对权限 API 使用await。小程序
authorize失败且无法再弹
状态为permanentlyDenied,调用openSetting。H5
openSetting为 false
浏览器无法打开站点权限页。用文案引导用户到地址栏站点设置。声明了电话权限但业务未用
应用市场上架被拒。无需求不要写入phone/sms等权限。manageExternalStorage上架失败
所有文件访问属敏感特殊权限。无强需求不要使用。后台定位申请失败
Android 10+ 须先具备前台定位,再申请locationBackground。业务用数字码判断权限
使用PermissionStatus字符串比较,例如ret.status === PermissionStatus.GRANTED。
排查
console.log(getPlatform())console.log(awaitcheck('camera'))console.log(listPermissions({onlyCurrent:true}))检查项:
- 当前端
platform与预期是否一致 ret.native是否为当前 API 应申请的权限串- manifest / plist / 小程序是否已声明
status是denied还是permanentlyDenied/serviceOff- 特殊权限是否在
onShow复查 - 引入路径是否为
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接入要求:
- 插件目录为
uni_modules/lf-permission - 从
@/uni_modules/lf-permission/index.js引入 - 按业务声明各端权限与 iOS 用途文案
- 定位场景区分
locationService与location - 特殊权限在设置返回后复查
- 敏感权限无需求不声明
调用示例:
awaitensure('camera',{request:true,openSettingOnDenied:true})问题反馈
插件:https://ext.dcloud.net.cn/plugin?id=29096
邮箱:lingfugroup@gmail.com
