更新记录
1.0.5(2026-07-10) 下载此版本
- readme 定位演示案例:补充 iOS 系统定位(wgs84)降级策略与 manifest Geolocation 配置说明
- 使用规范文档引用说明更新
1.0.4(2026-07-07) 下载此版本
- readme 添加使用案例
1.0.3(2026-06-17) 下载此版本
添加使用说明
查看更多平台兼容性
uni-app(4.0)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| - | √ | × | × | - | - | - | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(4.0)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | 6.0 | 14 | 12 | × |
m-permission-manager
uni-app Vue3 跨端权限管理 UTS 插件,支持 Android / iOS / Harmony 三端,提供统一的权限检查与申请 API。
功能
- 统一权限标识,三端通用
- 同步 / 异步检查权限状态
- 单个 / 批量申请权限
- 跳转系统应用设置页
- 获取当前平台支持的权限列表
安装
插件位于 src/uni_modules/m-permission-manager/,随项目直接使用,无需额外安装。
快速开始
方式一:Promise 封装(推荐)
import {
checkPermission,
requestPermission,
requestPermissions,
openAppSettings,
getSupportedPermissions,
} from '@/utils/permission-manager'
// 检查相机权限
const result = await checkPermission('camera')
console.log(result.granted, result.status)
// 申请相机权限
const granted = await requestPermission('camera')
if (!granted) {
await openAppSettings()
}
// 批量申请
const batch = await requestPermissions(['camera', 'microphone', 'location'])
console.log(batch.allGranted, batch.results)
方式二:直接调用 UTS API(callback 风格)
import { requestPermission } from '@/uni_modules/m-permission-manager'
requestPermission({
permission: 'camera',
success(res) {
console.log(res.granted)
},
fail(err) {
console.error(err.errCode, err.errMsg)
},
})
使用案例
以下为本项目完整示例代码,与仓库中对应文件保持一致。集成时推荐将 Promise 封装复制到 @/utils/permission-manager.js,业务页面参考权限演示页逻辑。
Promise 封装 — @/utils/permission-manager.js
/* eslint-disable no-unreachable -- uni-app 条件编译在 ESLint 中会产生误报 */
/**
* App 端权限管理封装(Android / iOS / Harmony)。
*/
// #ifdef APP-PLUS || APP-HARMONY
import {
checkPermissionSync as checkPermissionSyncUts,
checkPermissionsSync as checkPermissionsSyncUts,
checkPermission as checkPermissionUts,
checkPermissions as checkPermissionsUts,
requestPermission as requestPermissionUts,
requestPermissions as requestPermissionsUts,
openAppSettings as openAppSettingsUts,
getSupportedPermissionsSync,
getBluetoothDemoInfoSync as getBluetoothDemoInfoSyncUts,
} from '@/uni_modules/m-permission-manager'
// #endif
const ERROR_MESSAGES = {
9020001: '当前环境不支持权限管理',
9020002: '用户拒绝授权',
9020003: '权限不支持当前平台',
9020004: '打开系统设置失败',
9020005: '权限检查失败',
9020006: '权限申请失败',
}
export class PermissionError extends Error {
constructor(detail = {}) {
const code = detail.errCode || detail.code || 'UNKNOWN'
const message = detail.errMsg || detail.message || ERROR_MESSAGES[code] || '权限操作失败'
super(message)
this.name = 'PermissionError'
this.code = typeof code === 'number' ? code : String(code)
this.detail = detail
}
}
function callUtsApi(api, options = {}) {
return new Promise((resolve, reject) => {
api({
...options,
success: resolve,
fail: (err) => reject(new PermissionError(err)),
})
})
}
function notSupported() {
return Promise.reject(new PermissionError({ errCode: 9020001, errMsg: '当前平台不支持权限管理' }))
}
/**
* 同步检查单个权限
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
*/
export function checkPermissionSync(permission) {
// #ifdef APP-PLUS || APP-HARMONY
return checkPermissionSyncUts(permission)
// #endif
return { permission, status: 'unsupported', granted: false }
}
/**
* 同步检查多个权限
* @param {import('@/uni_modules/m-permission-manager').PermissionKey[]} permissions
*/
export function checkPermissionsSync(permissions) {
// #ifdef APP-PLUS || APP-HARMONY
return checkPermissionsSyncUts(permissions)
// #endif
return {
results: permissions.map((p) => ({ permission: p, status: 'unsupported', granted: false })),
allGranted: false,
}
}
/**
* 检查单个权限(Promise)
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
*/
export function checkPermission(permission) {
// #ifdef APP-PLUS || APP-HARMONY
return callUtsApi(checkPermissionUts, { permission })
// #endif
return notSupported()
}
/**
* 检查多个权限(Promise)
* @param {import('@/uni_modules/m-permission-manager').PermissionKey[]} permissions
*/
export function checkPermissions(permissions) {
// #ifdef APP-PLUS || APP-HARMONY
return callUtsApi(checkPermissionsUts, { permissions })
// #endif
return notSupported()
}
/**
* 申请单个权限(Promise),返回是否已授权
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
*/
export async function requestPermission(permission) {
// #ifdef APP-PLUS || APP-HARMONY
const result = await callUtsApi(requestPermissionUts, { permission })
return result.granted
// #endif
await notSupported()
return false
}
/**
* 申请多个权限(Promise)
* @param {import('@/uni_modules/m-permission-manager').PermissionKey[]} permissions
*/
export function requestPermissions(permissions) {
// #ifdef APP-PLUS || APP-HARMONY
return callUtsApi(requestPermissionsUts, { permissions })
// #endif
return notSupported()
}
/**
* 打开系统应用设置页
* @param {import('@/uni_modules/m-permission-manager').OpenAppSettingsOptions} [options]
*/
export function openAppSettings(options = {}) {
// #ifdef APP-PLUS || APP-HARMONY
return callUtsApi(openAppSettingsUts, options)
// #endif
return notSupported()
}
/**
* 按权限类型打开对应系统设置页
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
*/
export function openPermissionSettings(permission) {
return openAppSettings({ permission })
}
/**
* 获取跳转系统设置前的引导文案(按平台与权限类型)
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
* @param {string} [label]
*/
export function getPermissionSettingsGuide(permission, label = permission) {
let platform = ''
try {
platform = uni.getSystemInfoSync().platform || ''
} catch {
platform = ''
}
if (permission === 'bluetooth') {
// #ifdef APP-PLUS || APP-HARMONY
const btInfo = getBluetoothDemoInfoSync()
if (btInfo.available && !btInfo.enabled) {
if (platform === 'ios') {
return `系统蓝牙未开启。请前往「设置 → 蓝牙」打开开关,再返回 App 重试「${label}」。`
}
return `系统蓝牙未开启。请前往「设置 → 蓝牙」打开开关,再返回 App 重试「${label}」。`
}
// #endif
if (platform === 'ios') {
return '请在「设置 → 本 App → 蓝牙」开启授权;若未看到该选项,请前往「设置 → 隐私与安全性 → 蓝牙」找到本 App 并开启。'
}
return `请在系统设置的应用权限页中开启「${label}」。`
}
if (permission === 'overlay') {
return '请前往「显示在其他应用上层」权限页,允许本 App 显示悬浮窗。'
}
if (permission === 'writeSettings') {
return '请前往「修改系统设置」权限页,允许本 App 修改系统设置。'
}
return `请在系统设置中手动开启「${label}」权限。`
}
/**
* 获取当前平台支持的权限列表
*/
export function getSupportedPermissions() {
// #ifdef APP-PLUS || APP-HARMONY
return getSupportedPermissionsSync().permissions
// #endif
return []
}
/**
* 读取系统蓝牙开关状态(演示用)
* @returns {import('@/uni_modules/m-permission-manager').BluetoothDemoInfo}
*/
export function getBluetoothDemoInfoSync() {
// #ifdef APP-PLUS || APP-HARMONY
const api = resolveUtsApi('getBluetoothDemoInfoSync', getBluetoothDemoInfoSyncUts)
if (typeof api === 'function') {
return api()
}
return {
available: false,
enabled: false,
stateText: '蓝牙演示 API 不可用,请重新制作自定义基座',
stateCode: -1,
}
// #endif
return {
available: false,
enabled: false,
stateText: '当前平台不支持',
stateCode: -1,
}
}
function resolveUtsApi(name, fallback) {
if (typeof uni !== 'undefined' && typeof uni.requireUTSPlugin === 'function') {
const proxy = uni.requireUTSPlugin('uni_modules/m-permission-manager')
const api = proxy?.[name]
if (typeof api === 'function') {
return api
}
}
return fallback
}
/**
* 检查并申请权限,未授权时可选跳转设置
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} permission
* @param {{ openSettings?: boolean }} [options]
*/
export async function ensurePermission(permission, options = {}) {
const checked = await checkPermission(permission)
if (checked.granted) {
return true
}
const granted = await requestPermission(permission)
if (granted) {
return true
}
if (options.openSettings) {
await openPermissionSettings(permission)
}
return false
}
权限演示页 — src/pages/permission/index.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import {
checkPermissionsSync,
checkPermissions,
checkPermissionSync,
checkPermission,
requestPermission,
requestPermissions,
openAppSettings,
openPermissionSettings,
getPermissionSettingsGuide,
getSupportedPermissions,
PermissionError,
} from '@/utils/permission-manager'
import { requireAuth } from '@/utils/guard'
import {
hasPermissionDemo,
getPermissionDemoLabel,
runPermissionDemo,
} from '@/utils/permission-demos'
import logger from '@/utils/logger'
const PERMISSION_META = [
{ key: 'camera', label: '相机' },
{ key: 'photoLibrary', label: '相册' },
{ key: 'storage', label: '存储(写)' },
{ key: 'readStorage', label: '存储(读)' },
{ key: 'location', label: '精确定位' },
{ key: 'coarseLocation', label: '大致定位' },
{ key: 'microphone', label: '麦克风' },
{ key: 'contacts', label: '通讯录(读)' },
{ key: 'writeContacts', label: '通讯录(写)' },
{ key: 'bluetooth', label: '蓝牙' },
{ key: 'phone', label: '拨打电话' },
{ key: 'phoneState', label: '电话状态' },
{ key: 'sms', label: '发送短信' },
{ key: 'readSms', label: '读取短信' },
{ key: 'receiveSms', label: '接收短信' },
{ key: 'overlay', label: '悬浮窗' },
{ key: 'writeSettings', label: '系统设置' },
]
const COMMON_PERMISSIONS = ['camera', 'microphone', 'location']
const HARMONY_ACL_KEYS = ['photoLibrary', 'readStorage', 'storage', 'contacts', 'writeContacts']
const HARMONY_ALBUM_DEMO_KEYS = ['photoLibrary', 'readStorage', 'storage']
const STATUS_LABELS = {
granted: '已授权',
denied: '已拒绝',
notDetermined: '未请求',
restricted: '受限制',
unsupported: '不支持',
unknown: '未知',
}
const isAppRuntime = ref(false)
const isHarmonyRuntime = ref(false)
const platformText = ref('未知')
const refreshing = ref(false)
const batchRequesting = ref(false)
const openingSettings = ref(false)
const lastLog = ref('')
const permissionList = ref([])
const demoLoadingKey = ref('')
const demoResult = ref(null)
const listRefreshKey = ref(0)
const collapsedSections = ref({
granted: false,
pending: false,
denied: true,
unsupported: true,
})
const grantedList = computed(() => permissionList.value.filter((item) => item.status === 'granted'))
const pendingList = computed(() => permissionList.value.filter((item) => item.status === 'notDetermined'))
const deniedList = computed(() => permissionList.value.filter((item) => item.status === 'denied' || item.status === 'restricted'))
const unsupportedList = computed(() => permissionList.value.filter((item) => item.status === 'unsupported'))
const permissionSections = computed(() => [
{
key: 'granted',
title: '已申请',
items: grantedList.value,
emptyText: '暂无已授权权限',
},
{
key: 'pending',
title: '未申请',
items: pendingList.value,
emptyText: '全部权限均已处理',
},
{
key: 'denied',
title: '已拒绝',
items: deniedList.value,
emptyText: '暂无被拒绝权限',
},
])
function isSectionCollapsed(key) {
return collapsedSections.value[key] === true
}
function toggleSection(key) {
collapsedSections.value = {
...collapsedSections.value,
[key]: !collapsedSections.value[key],
}
}
function detectRuntime() {
// #ifdef APP-PLUS || APP-HARMONY
isAppRuntime.value = true
// #endif
// #ifdef H5
isAppRuntime.value = false
// #endif
// #ifdef APP-HARMONY
isHarmonyRuntime.value = true
// #endif
// #ifndef APP-HARMONY
isHarmonyRuntime.value = false
// #endif
try {
const info = uni.getSystemInfoSync()
// #ifdef APP-HARMONY
platformText.value = `Harmony · ${info.osVersion || ''}`
// #endif
// #ifdef APP-PLUS
platformText.value = `${info.platform} · ${info.system || ''}`
// #endif
// #ifdef H5
platformText.value = `H5 · ${info.browserName || '浏览器'}`
// #endif
} catch {
platformText.value = '未知'
}
}
function applyPermissionResults(results) {
const supported = isAppRuntime.value ? getSupportedPermissions() : []
const resultMap = new Map()
results.forEach((item) => {
resultMap.set(item.permission, item.status)
})
permissionList.value = PERMISSION_META.map((item) => {
const supportedOnPlatform = supported.length === 0 || supported.includes(item.key)
const prev = permissionList.value.find((p) => p.key === item.key)
const status = supportedOnPlatform
? (resultMap.get(item.key) || 'notDetermined')
: 'unsupported'
return {
...item,
status,
loading: prev?.loading ?? false,
}
})
listRefreshKey.value += 1
}
function buildPermissionList() {
if (!isAppRuntime.value) {
permissionList.value = PERMISSION_META.map((item) => ({
...item,
status: 'unsupported',
loading: false,
}))
listRefreshKey.value += 1
return
}
const keys = PERMISSION_META.map((item) => item.key)
const synced = checkPermissionsSync(keys)
applyPermissionResults(synced.results)
}
function expandSectionsAfterRefresh() {
collapsedSections.value = {
granted: grantedList.value.length === 0,
pending: pendingList.value.length === 0,
denied: deniedList.value.length === 0,
unsupported: true,
}
}
function summarizePermissionStatus() {
const granted = grantedList.value.length
const pending = pendingList.value.length
const denied = deniedList.value.length
const unsupported = unsupportedList.value.length
return { granted, pending, denied, unsupported }
}
function statusLabel(status) {
if (status === 'restricted' && isHarmonyRuntime.value) {
return '未声明'
}
return STATUS_LABELS[status] || STATUS_LABELS.unknown
}
function isHarmonyAclKey(key) {
return HARMONY_ACL_KEYS.includes(key)
}
function isHarmonyAlbumDemoKey(key) {
return HARMONY_ALBUM_DEMO_KEYS.includes(key)
}
function getHarmonyAclHint(key) {
if (isHarmonyAlbumDemoKey(key)) {
return '当前包未声明媒体读写权限,系统设置无开关。可直接「选图」试用。'
}
return '当前包未声明该权限,系统设置无开关。需 module.full.json5 + ACL 后重装。'
}
function canShowRequest(item) {
if (item.status === 'granted' || item.status === 'unsupported') {
return false
}
if (isHarmonyRuntime.value && item.status === 'restricted' && isHarmonyAclKey(item.key)) {
return false
}
return true
}
function canShowDemo(item) {
if (!hasPermissionDemo(item.key)) {
return false
}
if (item.status === 'granted') {
return true
}
if (isHarmonyRuntime.value && isHarmonyAlbumDemoKey(item.key)) {
return true
}
return false
}
function showHarmonyAclGuide(key) {
const isAlbum = isHarmonyAlbumDemoKey(key)
uni.showModal({
title: '鸿蒙受限权限说明',
content: isAlbum
? `${getPermissionLabel(key)} 属于 system_basic 权限。当前调试包为可安装版本,未在 module.json5 中声明,因此系统「设置 → 应用 → 权限」里不会出现相册开关。\n\n可直接使用「选图」演示(系统 PhotoViewPicker,无需 READ_IMAGEVIDEO)。\n\n若必须走权限申请:用 module.full.json5 覆盖 module.json5,并在 AppGallery Connect 配置 ACL 后重签安装。`
: `${getPermissionLabel(key)} 需 ACL 签名且须在 module.json5 声明。当前包未声明,系统设置无对应开关。`,
confirmText: isAlbum ? '选图试用' : '知道了',
cancelText: '关闭',
success: (res) => {
if (res.confirm && isAlbum) {
handleDemo(key)
}
},
})
}
function statusClass(status) {
if (status === 'notDetermined') {
return 'status-not-determined'
}
return `status-${status}`
}
function setLog(message) {
lastLog.value = `[${formatTime()}] ${message}`
}
function formatTime() {
const now = new Date()
const pad = (n) => String(n).padStart(2, '0')
return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
function updateItemStatus(key, status) {
permissionList.value = permissionList.value.map((item) => {
if (item.key !== key) return item
return { ...item, status }
})
}
function setItemLoading(key, loading) {
permissionList.value = permissionList.value.map((item) => {
if (item.key !== key) return item
return { ...item, loading }
})
}
function getPermissionLabel(key) {
return PERMISSION_META.find((item) => item.key === key)?.label || key
}
function showError(error, fallback) {
const message = error instanceof PermissionError ? error.message : fallback
uni.showToast({ title: message, icon: 'none' })
setLog(message)
}
async function handleRefreshAll() {
if (!isAppRuntime.value) return
refreshing.value = true
try {
const keys = PERMISSION_META.map((item) => item.key)
const result = await checkPermissions(keys)
applyPermissionResults(result.results)
expandSectionsAfterRefresh()
const summary = summarizePermissionStatus()
const message = `已刷新:${summary.granted} 已授权,${summary.pending} 未申请,${summary.denied} 已拒绝`
setLog(message)
uni.showToast({
title: '刷新完成',
icon: 'success',
})
} catch (error) {
logger.log('handleRefreshAll error', error)
showError(error, '刷新失败')
} finally {
refreshing.value = false
}
}
async function handleBatchRequest() {
if (!isAppRuntime.value) return
batchRequesting.value = true
try {
const result = await requestPermissions(COMMON_PERMISSIONS)
result.results.forEach((item) => {
updateItemStatus(item.permission, item.status)
})
const names = COMMON_PERMISSIONS.map(getPermissionLabel).join('、')
const message = result.allGranted
? `常用权限已全部授权:${names}`
: '部分权限未授权,请查看列表状态'
setLog(message)
uni.showToast({
title: result.allGranted ? '全部已授权' : '部分未授权',
icon: result.allGranted ? 'success' : 'none',
})
} catch (error) {
showError(error, '批量申请失败')
} finally {
batchRequesting.value = false
}
}
async function handleOpenSettings(permission) {
if (!isAppRuntime.value) return
openingSettings.value = true
try {
if (permission) {
await openPermissionSettings(permission)
setLog(`已跳转「${getPermissionLabel(permission)}」相关系统设置`)
} else {
await openAppSettings()
setLog('已跳转系统应用设置')
}
} catch (error) {
showError(error, '打开设置失败')
} finally {
openingSettings.value = false
}
}
async function handleCheck(key) {
if (!isAppRuntime.value) return
setItemLoading(key, true)
try {
const result = await checkPermission(key)
updateItemStatus(key, result.status)
setLog(`${getPermissionLabel(key)}:${statusLabel(result.status)}`)
} catch (error) {
showError(error, '检查失败')
} finally {
setItemLoading(key, false)
}
}
function offerDemoAfterGrant(key) {
if (!hasPermissionDemo(key)) return
uni.showModal({
title: '授权成功',
content: `是否立即体验「${getPermissionLabel(key)}」功能?`,
confirmText: getPermissionDemoLabel(key),
cancelText: '稍后',
success: (res) => {
if (res.confirm) handleDemo(key)
},
})
}
async function handleDemo(key) {
if (!isAppRuntime.value || demoLoadingKey.value) return
demoLoadingKey.value = key
try {
const result = await runPermissionDemo(key)
demoResult.value = result
const logMessage = `${result.title}:${result.message}`
setLog(logMessage)
uni.showToast({ title: result.message, icon: 'success' })
} catch (error) {
const message = error?.errMsg || error?.message || '演示失败'
if (message !== '已取消') {
uni.showToast({ title: message, icon: 'none' })
setLog(`${getPermissionLabel(key)} 演示:${message}`)
}
} finally {
demoLoadingKey.value = ''
}
}
async function handleRequest(key) {
if (!isAppRuntime.value) return
const current = checkPermissionSync(key)
updateItemStatus(key, current.status)
if (current.status === 'restricted' && isHarmonyRuntime.value && isHarmonyAclKey(key)) {
showHarmonyAclGuide(key)
return
}
setItemLoading(key, true)
try {
const granted = await requestPermission(key)
const status = granted ? 'granted' : 'denied'
updateItemStatus(key, status)
const message = `${getPermissionLabel(key)}:${granted ? '申请成功' : '用户拒绝'}`
setLog(message)
uni.showToast({
title: granted ? '已授权' : '未授权',
icon: granted ? 'success' : 'none',
})
if (granted) {
offerDemoAfterGrant(key)
} else if (!(isHarmonyRuntime.value && isHarmonyAclKey(key))) {
const label = getPermissionLabel(key)
uni.showModal({
title: '权限未授予',
content: getPermissionSettingsGuide(key, label),
confirmText: '去设置',
cancelText: '取消',
success: (res) => {
if (res.confirm) handleOpenSettings(key)
},
})
}
} catch (error) {
logger.log('handleRequest error', error)
if (isHarmonyRuntime.value && isHarmonyAclKey(key)) {
showHarmonyAclGuide(key)
setLog(error instanceof PermissionError ? error.message : '申请失败')
} else {
showError(error, '申请失败')
}
} finally {
setItemLoading(key, false)
}
}
onMounted(() => {
detectRuntime()
buildPermissionList()
})
onShow(() => {
if (!requireAuth()) return
detectRuntime()
buildPermissionList()
expandSectionsAfterRefresh()
})
</script>
授权后能力试用 — src/utils/permission-demos.js
/**
* 各权限授权后的使用演示(App / Harmony)。
*/
import { getBluetoothDemoInfoSync } from '@/utils/permission-manager'
function getAppPlusRuntime() {
if (typeof globalThis === 'undefined') return undefined
return globalThis['plus']
}
function uniAsync(method, options = {}) {
const fn = uni[method]
if (typeof fn !== 'function') {
return Promise.reject(new Error(`uni.${method} 在当前平台不可用`))
}
return new Promise((resolve, reject) => {
fn({
...options,
success: resolve,
fail: reject,
})
})
}
function confirmAction(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
success: (res) => resolve(!!res.confirm),
})
})
}
/** @typedef {{ title: string, message: string, image?: string, detail?: string }} PermissionDemoResult */
/** @type {Record<string, { label: string, run: () => Promise<PermissionDemoResult> }>} */
const DEMO_REGISTRY = {
camera: {
label: '拍照',
run: async () => {
const res = await uniAsync('chooseImage', {
count: 1,
sourceType: ['camera'],
sizeType: ['compressed'],
})
const image = res.tempFilePaths?.[0]
return {
title: '相机',
message: '拍照成功',
image,
detail: image,
}
},
},
photoLibrary: {
label: '选图',
run: pickImageFromAlbum,
},
readStorage: {
label: '选图',
run: pickImageFromAlbum,
},
storage: {
label: '保存相册',
run: async () => {
const pick = await uniAsync('chooseImage', {
count: 1,
sourceType: ['album'],
sizeType: ['compressed'],
})
const filePath = pick.tempFilePaths?.[0]
if (!filePath) {
throw new Error('未选择图片')
}
await uniAsync('saveImageToPhotosAlbum', { filePath })
return {
title: '存储(写)',
message: '图片已保存到相册',
image: filePath,
}
},
},
location: {
label: '定位',
run: getLocationDemo,
},
coarseLocation: {
label: '定位',
run: getLocationDemo,
},
microphone: {
label: '录音',
run: recordAudioDemo,
},
contacts: {
label: '读联系人',
run: readContactsDemo,
},
writeContacts: {
label: '新建联系人',
run: async () => {
await uniAsync('addPhoneContact', {
firstName: '权限',
lastName: 'Demo',
mobilePhoneNumber: '***',
organization: 'uni_app_demo',
})
return {
title: '通讯录(写)',
message: '已打开新建联系人界面',
}
},
},
bluetooth: {
label: '蓝牙',
run: bluetoothDemo,
},
phone: {
label: '拨号',
run: async () => {
const ok = await confirmAction('拨号演示', '将打开系统拨号界面,号码:10086')
if (!ok) {
throw new Error('已取消')
}
await uniAsync('makePhoneCall', { phoneNumber: '10086' })
return {
title: '拨打电话',
message: '已打开拨号界面',
}
},
},
phoneState: {
label: '设备信息',
run: async () => {
const info = uni.getSystemInfoSync()
const detail = [
`品牌:${info.brand || '-'}`,
`型号:${info.model || '-'}`,
`系统:${info.system || info.osName || '-'}`,
`平台:${info.uniPlatform || info.platform || '-'}`,
].join('\n')
return {
title: '电话状态',
message: '已读取设备信息',
detail,
}
},
},
sms: {
label: '发短信',
run: sendSmsDemo,
},
readSms: {
label: '说明',
run: async () => infoDemo('读取短信', '读取短信为后台权限,需监听系统短信广播,本页仅演示授权流程。'),
},
receiveSms: {
label: '说明',
run: async () => infoDemo('接收短信', '接收短信为后台权限,需注册 BroadcastReceiver,本页仅演示授权流程。'),
},
overlay: {
label: '去设置',
run: async () => {
const { openAppSettings } = await import('@/utils/permission-manager')
await openAppSettings()
return infoDemo('悬浮窗', '请在系统设置中手动开启「显示在其他应用上层」权限。')
},
},
writeSettings: {
label: '去设置',
run: async () => {
const { openAppSettings } = await import('@/utils/permission-manager')
await openAppSettings()
return infoDemo('系统设置', '请在系统设置中手动开启「修改系统设置」权限。')
},
},
}
async function pickImageFromAlbum() {
const res = await uniAsync('chooseImage', {
count: 1,
sourceType: ['album'],
sizeType: ['compressed'],
})
const image = res.tempFilePaths?.[0]
const result = {
title: '相册',
message: '选图成功',
image,
detail: image,
}
// #ifdef APP-HARMONY
result.detail = `${image || ''}\n通过系统相册选择器选图,无需 READ_IMAGEVIDEO 权限`.trim()
// #endif
return result
}
async function getLocationDemo() {
const res = await uniAsync('getLocation', {
type: 'gcj02',
geocode: true,
isHighAccuracy: true,
highAccuracyExpireTime: 3000,
})
const region = formatLocationAddress(res.address)
const coordsText = formatLocationCoords(res)
const detailLines = []
if (region) detailLines.push(region)
detailLines.push(coordsText)
return {
title: '定位',
message: region || coordsText,
detail: detailLines.join('\n'),
}
}
function formatLocationCoords(res) {
const lat = Number(res.latitude)
const lng = Number(res.longitude)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
return '未能读取有效坐标'
}
const accuracy = res.accuracy !== null && res.accuracy !== undefined && Number.isFinite(Number(res.accuracy))
? `,精度 ${Math.round(Number(res.accuracy))}m`
: ''
return `纬度 ${lat.toFixed(6)},经度 ${lng.toFixed(6)}${accuracy}`
}
function isValidRegionText(text) {
if (!text || typeof text !== 'string') return false
const trimmed = text.trim()
if (!trimmed) return false
if (/^[??.\s_-]+$/.test(trimmed)) return false
if (/^(unknown|null|undefined|n\/a)$/i.test(trimmed)) return false
return true
}
function formatLocationAddress(address) {
if (!address) return ''
if (typeof address === 'string') {
const text = address.trim()
return isValidRegionText(text) ? text : ''
}
if (typeof address !== 'object') {
const text = String(address).trim()
return isValidRegionText(text) ? text : ''
}
const parts = [
address.country,
address.province,
address.city,
address.district,
address.street,
address.streetNum || address.streetNumber,
address.poiName,
].filter((item) => item !== null && item !== undefined && String(item).trim() !== '')
const unique = []
for (let i = 0; i < parts.length; i++) {
const part = String(parts[i]).trim()
if (!isValidRegionText(part)) continue
if (unique.length === 0 || unique[unique.length - 1] !== part) {
unique.push(part)
}
}
const merged = unique.join('')
|| address.formatted
|| address.fullAddress
|| address.address
|| ''
return isValidRegionText(String(merged).trim()) ? String(merged).trim() : ''
}
async function recordAudioDemo() {
const recorder = uni.getRecorderManager()
return new Promise((resolve, reject) => {
let timer = null
recorder.onStop((res) => {
if (timer) clearTimeout(timer)
const seconds = Math.max(1, Math.round((res.duration || 2000) / 1000))
resolve({
title: '麦克风',
message: `录音完成(约 ${seconds} 秒)`,
detail: res.tempFilePath,
})
})
recorder.onError((err) => {
if (timer) clearTimeout(timer)
reject(err)
})
recorder.start({ duration: 5000, format: 'mp3' })
uni.showToast({ title: '正在录音…', icon: 'none', duration: 1500 })
timer = setTimeout(() => recorder.stop(), 2000)
})
}
async function readContactsDemo() {
// #ifdef APP-PLUS
const appPlus = getAppPlusRuntime()
if (appPlus?.contacts) {
return new Promise((resolve, reject) => {
appPlus.contacts.getAddressBook(
appPlus.contacts.ADDRESSBOOK_PHONE,
(addressbook) => {
addressbook.find(
['displayName', 'phoneNumbers'],
(contacts) => {
const count = contacts?.length ?? 0
const preview = count > 0
? contacts.slice(0, 3).map((c) => c.displayName || '未命名').join('、')
: '无联系人'
resolve({
title: '通讯录(读)',
message: `共 ${count} 个联系人`,
detail: count > 0 ? `示例:${preview}${count > 3 ? '…' : ''}` : preview,
})
},
reject,
{ multiple: true },
)
},
reject,
)
})
}
// #endif
return infoDemo(
'通讯录(读)',
'当前平台暂无统一读取 API。Android 可使用 plus.contacts;鸿蒙建议使用系统 ContactPicker。',
)
}
async function bluetoothDemo() {
if (typeof uni.openBluetoothAdapter === 'function') {
await uniAsync('openBluetoothAdapter')
try {
const state = await uniAsync('getBluetoothAdapterState')
const parts = []
if (state.available === false) parts.push('蓝牙不可用')
else parts.push('蓝牙已开启')
if (state.discovering) parts.push('正在扫描')
if (state.connected) parts.push('已连接设备')
return {
title: '蓝牙',
message: parts.join(',') || '蓝牙适配器正常',
detail: JSON.stringify(state, null, 2),
}
} finally {
if (typeof uni.closeBluetoothAdapter === 'function') {
uni.closeBluetoothAdapter({})
}
}
}
return formatBluetoothDemoInfo(getBluetoothDemoInfoSync())
}
function formatBluetoothDemoInfo(info) {
if (!info.available) {
return {
title: '蓝牙',
message: info.stateText || '设备不支持蓝牙',
detail: `状态码:${info.stateCode}`,
}
}
const permissionText = info.enabled ? '蓝牙已开启,权限可用' : '蓝牙未开启,请在系统设置中打开'
return {
title: '蓝牙',
message: `蓝牙${info.stateText}`,
detail: `${permissionText}\n状态码:${info.stateCode}`,
}
}
async function sendSmsDemo() {
// #ifdef APP-PLUS
const appPlus = getAppPlusRuntime()
if (appPlus?.messaging) {
const ok = await confirmAction('短信演示', '将打开系统短信界面(不会自动发送)')
if (!ok) throw new Error('已取消')
const msg = appPlus.messaging.createMessage(appPlus.messaging.TYPE_SMS)
msg.to = ['10086']
msg.body = 'uni_app_demo 权限测试'
appPlus.messaging.sendMessage(msg)
return {
title: '发送短信',
message: '已打开短信编辑界面',
}
}
// #endif
return infoDemo('发送短信', '当前平台请使用系统短信应用或原生能力发送短信。')
}
function infoDemo(title, message) {
return Promise.resolve({ title, message })
}
/**
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} key
*/
export function hasPermissionDemo(key) {
return key in DEMO_REGISTRY
}
/**
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} key
*/
export function getPermissionDemoLabel(key) {
return DEMO_REGISTRY[key]?.label || '试用'
}
/**
* @param {import('@/uni_modules/m-permission-manager').PermissionKey} key
* @returns {Promise<PermissionDemoResult>}
*/
export async function runPermissionDemo(key) {
const entry = DEMO_REGISTRY[key]
if (!entry) {
throw new Error('暂无该权限的使用演示')
}
uni.showLoading({ title: '演示中…', mask: true })
try {
return await entry.run()
} finally {
uni.hideLoading()
}
}
统一权限标识
| 权限 Key | 说明 | Android | iOS | Harmony |
|---|---|---|---|---|
camera |
相机 | ✅ | ✅ | ✅ |
photoLibrary |
相册 | ✅ | ✅ | ✅ |
storage |
存储(写) | ✅ | ✅* | ✅ |
readStorage |
存储(读) | ✅ | ✅* | ✅ |
location |
精确定位 | ✅ | ✅ | ✅ |
coarseLocation |
大致定位 | ✅ | ✅* | ✅ |
microphone |
麦克风 | ✅ | ✅ | ✅ |
contacts |
通讯录(读) | ✅ | ✅ | ✅ |
writeContacts |
通讯录(写) | ✅ | ✅* | ✅ |
bluetooth |
蓝牙 | ✅ | ✅ | ✅ |
phone |
拨打电话 | ✅ | ❌ | ❌ |
phoneState |
电话状态 | ✅ | ❌ | ❌ |
sms |
发送短信 | ✅ | ❌ | ❌ |
readSms |
读取短信 | ✅ | ❌ | ❌ |
receiveSms |
接收短信 | ✅ | ❌ | ❌ |
overlay |
悬浮窗 | ✅ | ❌ | ❌ |
writeSettings |
系统设置 | ✅ | ❌ | ❌ |
*iOS 部分权限映射到相近的系统权限(如 storage → 相册)
权限状态
| status | 说明 |
|---|---|
granted |
已授权 |
denied |
已拒绝 |
notDetermined |
尚未请求 |
restricted |
受限制(如家长控制) |
unsupported |
当前平台不支持 |
API
| 方法 | 说明 |
|---|---|
checkPermissionSync(key) |
同步检查单个权限 |
checkPermissionsSync(keys[]) |
同步检查多个权限 |
checkPermission(options) |
异步检查单个权限 |
checkPermissions(options) |
异步检查多个权限 |
requestPermission(options) |
申请单个权限 |
requestPermissions(options) |
申请多个权限 |
openAppSettings(options?) |
打开系统应用设置 |
openPermissionSettings(key) |
按权限类型打开对应设置页(封装层) |
getSupportedPermissionsSync() |
获取当前平台支持的权限列表 |
使用规范
本文档面向集成本插件的 App 开发者,说明 必须遵守的约束、推荐做法 和 上架审核注意事项。
1. 基本约束(必须遵守)
1.1 用户主动触发
requestPermission / requestPermissions 必须由真实用户手势触发,例如按钮 click / tap。
<!-- ✅ 正确 -->
<button @click="handleRequestCamera">申请相机权限</button>
<!-- ❌ 错误:页面 onLoad / onShow 自动批量申请 -->
<script setup>
onMounted(async () => {
await requestPermissions(['camera', 'microphone', 'location']) // 体验差,可能被系统限制
})
</script>
原因:Android / iOS / Harmony 运行时权限弹窗均面向用户明确操作;冷启动批量申请易触发拒绝,且不符合各应用商店审核惯例。
1.2 先声明、后申请
仅安装插件不够,必须在各端配置文件中声明所需权限,否则:
| 平台 | 未声明后果 |
|---|---|
| Android | SecurityException、申请无弹窗或直接失败 |
| iOS | 崩溃或无授权弹窗 |
| Harmony | 系统设置无对应开关,插件返回 restricted 或 9020003 |
详细配置见 权限配置说明。
1.3 按需申请
只申请业务实际使用的权限。在功能入口处按需 requestPermission,不要一次性申请全部权限。
// ✅ 审核页
async function handleRequestCamera() {
const granted = await requestPermission('camera')
if (!granted) {
uni.showModal({
title: '需要相机权限',
content: getPermissionSettingsGuide('camera', '相机'),
confirmText: '去设置',
success: (res) => {
if (res.confirm) openPermissionSettings('camera')
},
})
return
}
// 继续业务逻辑
}
1.4 平台差异处理
调用前可通过 getSupportedPermissions() 过滤当前平台不支持的 Key,避免向用户展示无效入口。
const supported = getSupportedPermissions()
const canUseOverlay = supported.includes('overlay') // 仅 Android 为 true
2. 推荐集成流程
进入功能页
↓
checkPermission(key) ──→ granted → 执行业务
↓ notDetermined / denied
用户点击「申请权限」
↓
requestPermission(key)
↓
系统弹窗 ──→ 拒绝 → getPermissionSettingsGuide + openPermissionSettings
↓ 同意
granted → 执行业务
2.1 Promise 封装(推荐)
项目可复用 @/utils/permission-manager.js:
import {
checkPermission,
requestPermission,
requestPermissions,
openPermissionSettings,
getPermissionSettingsGuide,
ensurePermission,
getSupportedPermissions,
} from '@/utils/permission-manager'
| 方法 | 用途 |
|---|---|
checkPermission(key) |
检查状态,返回 { granted, status } |
requestPermission(key) |
申请权限,返回 boolean |
requestPermissions(keys[]) |
批量申请,返回 { allGranted, results } |
openPermissionSettings(key) |
按权限类型跳转系统设置 |
getPermissionSettingsGuide(key, label) |
获取跳转前的引导文案 |
ensurePermission(key, { openSettings }) |
检查 → 申请 → 可选跳转设置 |
2.2 直接调用 UTS API(callback 风格)
import { requestPermission } from '@/uni_modules/m-permission-manager'
requestPermission({
permission: 'camera',
success(res) {
if (res.granted) { /* ... */ }
},
fail(err) {
console.error(err.errCode, err.errMsg)
},
})
2.3 永久拒绝后的引导
用户选择「不再询问」或多次拒绝后,应引导至系统设置:
const granted = await requestPermission('bluetooth')
if (!granted) {
uni.showModal({
title: '蓝牙权限未授予',
content: getPermissionSettingsGuide('bluetooth', '蓝牙'),
confirmText: '去设置',
success: (res) => {
if (res.confirm) openPermissionSettings('bluetooth')
},
})
}
各端跳转行为:
| 平台 | openPermissionSettings 行为 |
|---|---|
| Android 普通权限 | 应用详情 → 权限列表 |
Android overlay |
悬浮窗权限设置页 |
Android writeSettings |
修改系统设置权限页 |
| iOS | 本 App 设置页(需 manifest 声明对应 UsageDescription) |
| Harmony | 系统设置 → 应用详情 |
3. 各端特别注意
3.1 Android
- Android 13+:相册 / 存储自动映射
READ_MEDIA_IMAGES/READ_MEDIA_VIDEO - Android 12+:蓝牙自动映射
BLUETOOTH_CONNECT/BLUETOOTH_SCAN - 特殊权限:
overlay、writeSettings需跳转系统设置页,申请结果可能为notDetermined,返回后需再次checkPermission
3.2 iOS
- 必须在
privacyDescription中配置对应NS*UsageDescription,否则无弹窗 storage/readStorage映射到相册权限(PHPhotoLibrary)coarseLocation/location均映射到定位授权- 蓝牙:需
NSBluetoothAlwaysUsageDescription;系统蓝牙开关在「设置 → 蓝牙」,App 授权在 App 设置页或「隐私与安全性 → 蓝牙」
3.3 Harmony
- 默认
module.json5含 5 项 normal 权限,调试包可直接安装 - 相册 / 通讯录为
system_basic,需 ACL 签名 +module.full.json5 - 相册选图推荐
uni.chooseImage(系统 PhotoViewPicker),无需媒体读写权限 - 未声明的 ACL 权限:检查返回
restricted,申请返回 9020003
4. 错误码与用户提示
| errCode | 含义 | 建议提示 |
|---|---|---|
| 9020001 | 当前环境不支持 | 请在 App 端使用 |
| 9020002 | 用户拒绝授权 | 您拒绝了授权,可在设置中重新开启 |
| 9020003 | 权限不支持当前平台 | 当前平台不支持该权限 |
| 9020004 | 打开系统设置失败 | 无法打开系统设置,请手动前往 |
| 9020005 | 权限检查失败 | 权限检查失败,请重试 |
| 9020006 | 权限申请失败 | 权限申请失败,请重试 |
5. 上架审核注意事项
| 市场 | 注意点 |
|---|---|
| App Store | 每个 NS*UsageDescription 文案需与真实功能一致;不要申请未使用的权限 |
| Google Play | 需在 Data safety 中声明权限用途;Android 13+ 媒体权限需按类型声明 |
| 华为 / 鸿蒙 | 说明 ohos.permission.* 用途;system_basic 权限需 ACL 白名单 |
| 国内 Android 市场 | 隐私合规检测会扫描 manifest 权限列表,确保与隐私政策一致 |
6. 自定义基座与发版
- 导入或更新插件后,必须重新制作自定义基座或云打包
- 修改
manifest.json权限或鸿蒙module.json5后,需重新打包 - iOS 修改
privacyDescription后需重新打包,必要时删除旧 App 重装以重新触发授权弹窗
权限配置说明
本文档说明集成本插件时,各端需要修改哪些文件、如何配置权限,以及插件内置配置文件的作用。
1. 配置总览
| 平台 | 配置文件 | 配置方 | 说明 |
|---|---|---|---|
| Android | 宿主 manifest.json |
集成者 | 声明 android.permission.* |
| iOS | 宿主 manifest.json |
集成者 | 声明 NS*UsageDescription |
| Harmony | 插件 utssdk/app-harmony/module.json5(可选:harmony-configs/entry/src/main/module.json5) |
插件内置为主,集成者可切换 full 版或宿主覆盖 |
插件负责运行时检查与申请;Android / iOS 的静态声明必须由宿主项目在 manifest 中完成。Harmony 权限由插件 module 合并进宿主包,集成者按需切换 normal / full 版本。
2. Android 配置
2.1 配置文件
路径:项目根目录 manifest.json
节点:app-plus.distribute.android.permissions
{
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>",
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>",
"<uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\"/>"
]
}
}
}
}
2.2 统一 Key 与 Android 权限映射
| 插件 Key | Android 权限 | 备注 |
|---|---|---|
camera |
CAMERA |
|
photoLibrary |
READ_MEDIA_IMAGES(API 33+)或 READ_EXTERNAL_STORAGE |
插件按 SDK 自动选择 |
storage |
READ_MEDIA_IMAGES + READ_MEDIA_VIDEO(API 33+)或 WRITE/READ_EXTERNAL_STORAGE |
|
readStorage |
同 photoLibrary / 媒体读权限 |
|
location |
ACCESS_FINE_LOCATION |
|
coarseLocation |
ACCESS_COARSE_LOCATION |
|
microphone |
RECORD_AUDIO |
|
contacts |
READ_CONTACTS |
|
writeContacts |
WRITE_CONTACTS |
|
bluetooth |
BLUETOOTH_CONNECT + BLUETOOTH_SCAN(API 31+)或 BLUETOOTH + BLUETOOTH_ADMIN |
|
phone |
CALL_PHONE |
|
phoneState |
READ_PHONE_STATE |
|
sms |
SEND_SMS |
|
readSms |
READ_SMS |
|
receiveSms |
RECEIVE_SMS |
|
overlay |
无 manifest 条目 | 通过 Settings.canDrawOverlays 检查 |
writeSettings |
无 manifest 条目 | 通过 Settings.System.canWrite 检查 |
2.3 注意
- 只声明业务使用的权限,避免过度申请
- Android 13+ 媒体权限使用
READ_MEDIA_*;插件仅在 targetSdkVersion >= 33 时动态申请新媒体权限,否则回退READ_EXTERNAL_STORAGE(兼容 targetSdk 偏低的自定义基座) overlay、writeSettings需在 manifest 声明对应 capability 后,通过插件跳转系统设置页授权
3. iOS 配置
3.1 配置文件
路径:项目根目录 manifest.json
节点:app-plus.distribute.ios.privacyDescription
{
"app-plus": {
"distribute": {
"ios": {
"privacyDescription": {
"NSCameraUsageDescription": "需要使用相机进行拍照",
"NSMicrophoneUsageDescription": "需要使用麦克风进行录音",
"NSPhotoLibraryUsageDescription": "需要访问相册以选择图片",
"NSPhotoLibraryAddUsageDescription": "需要保存图片到相册",
"NSLocationWhenInUseUsageDescription": "需要获取位置信息",
"NSContactsUsageDescription": "需要访问通讯录",
"NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接周边设备"
}
}
}
}
}
3.2 统一 Key 与 Info.plist 键映射
| 插件 Key | Info.plist 键 | 备注 |
|---|---|---|
camera |
NSCameraUsageDescription |
|
photoLibrary |
NSPhotoLibraryUsageDescription |
|
storage |
NSPhotoLibraryUsageDescription 或 NSPhotoLibraryAddUsageDescription |
映射到相册 |
readStorage |
NSPhotoLibraryUsageDescription |
映射到相册 |
location |
NSLocationWhenInUseUsageDescription |
|
coarseLocation |
NSLocationWhenInUseUsageDescription |
与精确定位共用 |
microphone |
NSMicrophoneUsageDescription |
|
contacts |
NSContactsUsageDescription |
|
writeContacts |
NSContactsUsageDescription |
读写共用 |
bluetooth |
NSBluetoothAlwaysUsageDescription |
必须配置,否则 App 设置页无蓝牙开关 |
3.3 iOS 蓝牙特别说明
iOS 蓝牙涉及两个独立概念:
| 概念 | 设置路径 | 配置要求 |
|---|---|---|
| 系统蓝牙开关 | 设置 → 蓝牙 | 无需 manifest,用户手动打开 |
| App 蓝牙授权 | 设置 → 本 App → 蓝牙,或 设置 → 隐私与安全性 → 蓝牙 | 需 NSBluetoothAlwaysUsageDescription + 重新打包 + App 曾触发弹窗 |
Apple 不提供跳转到「隐私 → 蓝牙」的公开深链;插件 openPermissionSettings('bluetooth') 会打开 App 设置页并配合引导文案。
3.4 iOS 定位特别说明
除 NSLocationWhenInUseUsageDescription 外,宿主还需在 manifest 启用定位模块:
{
"app-plus": {
"modules": { "Geolocation": {} },
"distribute": {
"sdkConfigs": {
"geolocation": {
"system": { "__platform__": ["ios", "android"] }
}
}
}
}
}
- 仅配置系统定位时,
uni.getLocation在 iOS 上应使用type: 'wgs84';gcj02需接入高德/腾讯定位 SDK - 高精度 GPS 在室内或冷启动时易超时,演示代码建议降级为普通
wgs84网络定位 - 修改 manifest 定位模块后需重新制作自定义基座或云打包
4. Harmony 配置
4.1 harmony-configs 要不要配?
使用 m-permission-manager 时,一般不需要在 harmony-configs/ 里重复声明权限。
鸿蒙端权限有两种配置路径:
| 方式 | 配置文件 | 适用场景 |
|---|---|---|
| UTS 插件声明(推荐) | uni_modules/m-permission-manager/utssdk/app-harmony/module.json5 |
插件打包为鸿蒙子模块(HAR),权限对整个 App 生效 |
| 宿主工程覆盖 | harmony-configs/entry/src/main/module.json5 |
插件未覆盖的权限、或希望在宿主侧集中管理 |
鸿蒙官方规则:已在子模块中声明的权限,主工程无需重复添加,权限在整个应用中生效。
m-permission-manager 作为 UTS 插件,其 module.json5 会在编译时拷贝为插件子模块的 src/main/module.json5,因此默认情况下宿主不必再配。
当前项目的 harmony-configs/ 主要用于:
| 文件 | 用途 |
|---|---|
AppScope/app.json5 |
包名、版本、图标(HX 4.31+ 也可在 manifest.json → app-harmony 图形界面配置) |
build-profile.json5 |
签名证书(.cer / .p12 / .p7b) |
entry/build-profile.json5 |
release 混淆等构建选项 |
entry/obfuscation-rules.txt |
混淆规则 |
若要在 harmony-configs 中配置权限(可选,非必须):
- 先运行一次鸿蒙编译,从
unpackage/dist/dev/app-harmony/entry/src/main/module.json5复制完整文件到harmony-configs/entry/src/main/module.json5 - 仅修改
module.requestPermissions节点,其他节点不要删(uni-app 是文件级覆盖,不是 JSON 节点合并) - 不要与插件已声明的权限重复
集成 m-permission-manager 时,优先改插件内的
module.json5/module.full.json5,而不是 harmony-configs。
4.2 插件内置文件
目录:uni_modules/m-permission-manager/utssdk/app-harmony/
| 文件 | 权限数量 | APL 等级 | 用途 |
|---|---|---|---|
module.json5 |
5 项 | normal | 默认生效,调试证书可直接安装 |
module.normal.json5 |
5 项 | normal | 与 module.json5 相同,备份参考 |
module.full.json5 |
9 项 | normal + system_basic | 含相册 / 通讯录,需 ACL 签名 |
权限说明文案:resources/base/element/string.json
4.3 默认 module.json5(5 项 normal)
| 插件 Key | ohos.permission | 说明 |
|---|---|---|
camera |
CAMERA |
|
microphone |
MICROPHONE |
|
location |
LOCATION |
|
coarseLocation |
APPROXIMATELY_LOCATION |
|
bluetooth |
ACCESS_BLUETOOTH |
4.4 module.full.json5 额外 4 项(system_basic,需 ACL)
| 插件 Key | ohos.permission | APL | 说明 |
|---|---|---|---|
photoLibrary / readStorage |
READ_IMAGEVIDEO |
system_basic | 需 ACL |
storage |
WRITE_IMAGEVIDEO |
system_basic | 需 ACL |
contacts |
READ_CONTACTS |
system_basic | 需 ACL |
writeContacts |
WRITE_CONTACTS |
system_basic | 需 ACL |
4.5 如何切换 full 版本
- 将
module.full.json5的内容覆盖module.json5 - 在 AppGallery Connect 配置 ACL 白名单
- 使用正式 Profile 重新签名打包
- 重新制作自定义基座或云打包
4.6 未声明 ACL 权限时的插件行为
| 场景 | 检查 status | 申请结果 |
|---|---|---|
| 相册 / 通讯录等未在 module.json5 声明 | restricted |
9020003,附未声明说明 |
| 系统设置 | 无对应开关 | 引导使用 uni.chooseImage 或切换 full 版本 |
4.7 安装报 9568289
说明 module.json5 中含有未签名的 system_basic 权限。请确认使用默认 5 项 normal 版本;若已合并 full 版本,需先完成 ACL 与 Profile 签名。
4.8 鸿蒙不支持的功能
以下插件 Key 在鸿蒙返回 unsupported:phone、phoneState、sms、readSms、receiveSms、overlay、writeSettings。
5. 配置示例(按业务场景)
5.1 仅需相机 + 麦克风
Android manifest:
"permissions": [
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
]
iOS privacyDescription:
"privacyDescription": {
"NSCameraUsageDescription": "需要使用相机",
"NSMicrophoneUsageDescription": "需要使用麦克风"
}
Harmony:默认 module.json5 已包含,无需修改。
5.2 需要蓝牙
Android manifest 增加:
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>"
iOS privacyDescription 增加:
"NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接周边设备"
Harmony:默认 module.json5 已包含 ACCESS_BLUETOOTH。
5.3 需要相册读写(Harmony 完整权限)
- 用
module.full.json5覆盖module.json5 - 配置 ACL 签名
- Android 增加
READ_MEDIA_IMAGES/READ_MEDIA_VIDEO - iOS 增加
NSPhotoLibraryUsageDescription
若仅需选图不上传,推荐使用
uni.chooseImage,三端均无需媒体读写权限。
6. 配置后必做
- 重新制作自定义基座或云打包(UTS 插件 + manifest 变更均须重新打包)
- iOS 修改
privacyDescription后建议删除旧 App 重装,以重新触发授权弹窗 - Harmony 切换
module.json5后需重新签名安装
错误码
| errCode | 说明 |
|---|---|
| 9020001 | 当前环境不支持 |
| 9020002 | 用户拒绝授权 |
| 9020003 | 权限不支持当前平台 |
| 9020004 | 打开系统设置失败 |
| 9020005 | 权限检查失败 |
| 9020006 | 权限申请失败 |
更新日志
1.0.3(2026-06-17)
- readme 合并全部文档内容,适配插件市场单页展示
1.0.2(2026-06-17)
-
修复 Android:相册/存储/蓝牙申请失败(
targetSdkVersion与READ_MEDIA_*/BLUETOOTH_*不匹配) -
权限映射同时校验设备 API 与宿主
targetSdkVersion,自定义基座 targetSdk < 33 时回退旧版权限1.0.1(2026-06-17)
-
新增
openPermissionSettings(permission)按权限类型跳转系统设置 -
新增
getPermissionSettingsGuide(permission, label)引导文案 -
Android:
overlay/writeSettings跳转对应系统设置页 -
新增《发布指南.md》《使用规范.md》《权限配置说明.md》
-
readme 补充 iOS 蓝牙说明与
NSBluetoothAlwaysUsageDescription
1.0.0(2026-05-29)
- 初始版本
- 支持 Android / iOS / Harmony 三端统一权限检查与申请
- 提供同步 / 异步 API 及
@/utils/permission-managerPromise 封装
发布指南
面向插件作者,说明如何将本插件提交到 DCloud 插件市场,以及后台各字段的填写参考。
发布前检查清单
代码与文档
- [ ]
package.json中id、version、description、keywords已填写 - [ ]
dcloudext.type为uts - [ ]
dcloudext.declaration中 ads / data / permissions 已如实填写 - [ ]
uni_modules.platforms中各端支持情况准确(vue3=y,H5/小程序=x) - [ ]
readme.md完整(安装、API、权限、FAQ) - [ ]
使用规范.md、权限配置说明.md、changelog.md已更新 - [ ]
index.d.ts与index.js导出一致
功能验证
- [ ] Android 真机:相机 / 麦克风 / 定位 / 蓝牙申请与检查
- [ ] Android 12+ 真机:蓝牙
BLUETOOTH_CONNECT/BLUETOOTH_SCAN - [ ] Android 13+ 真机:相册
READ_MEDIA_IMAGES自动映射 - [ ] Android 真机:悬浮窗(
overlay)、修改系统设置(writeSettings)跳转设置页 - [ ] iOS 14+ 真机:相机 / 麦克风 / 定位 / 蓝牙 / 相册
- [ ] iOS 真机:拒绝后
openPermissionSettings('bluetooth')引导文案正确 - [ ] Harmony API 12+ 真机:5 项 normal 权限申请(默认
module.json5) - [ ] Harmony 真机:相册 ACL 未声明时返回
restricted/ 9020003 - [ ] 拒绝授权时引导跳转系统设置
- [ ] H5 环境明确不可用(9020001)
打包验证
- [ ] 自定义基座可正常运行
- [ ] 云打包 / 本地打包正式包可正常运行
- [ ] 修改 UTS 代码后已重新制作基座
提交到插件市场
- 登录 DCloud 插件市场
- 进入 我的插件 → 发布插件
- 插件类型选择 UTS 插件
- 上传
uni_modules/m-permission-manager目录(或打包 zip) - 按下方表格填写市场展示信息
市场后台填写参考
基本信息
| 字段 | 填写内容 |
|---|---|
| 插件名称 | m-permission-manager 跨端权限管理 |
| 插件 ID | m-permission-manager |
| 插件类型 | UTS 插件 |
| 价格 | 免费(或按实际定价) |
| 版本号 | 与 package.json → version 一致,如 1.0.0 |
标题(建议)
m-permission-manager - uni-app 跨端权限管理(Android/iOS/鸿蒙)
一句话描述
UTS 权限插件,Android / iOS / Harmony 三端统一 API,检查与申请运行时权限,支持跳转系统设置。
详细描述(可直接粘贴)
m-permission-manager 是 uni-app Vue3 跨端权限管理 UTS 插件,支持 Android、iOS、Harmony 三端。
【核心能力】
· 17 种统一权限 Key,三端通用命名
· 同步 / 异步检查权限状态
· 单个 / 批量申请权限
· 按权限类型跳转系统设置页
· 获取当前平台支持的权限列表
【平台支持】
· Android:17 项权限(含悬浮窗、短信、电话等 Android 专属)
· iOS:10 项通用权限(相机、相册、定位、麦克风、蓝牙、通讯录等)
· Harmony:10 项通用权限(默认 5 项 normal 可直接安装,相册/通讯录需 ACL)
【集成方式】
1. 导入 uni_modules 插件
2. 在宿主 manifest.json(Android/iOS)及鸿蒙 module.json5 中声明所需权限
3. 使用 @/utils/permission-manager Promise 封装或直接调用 UTS API
详细权限配置见插件内权限配置说明,集成规范见使用规范。
标签 / 关键词
权限, permission, 运行时权限, 蓝牙, 相机, 定位, Android, iOS, Harmony, 鸿蒙, UTS
使用说明(市场「使用说明」字段)
可直接粘贴以下内容,或填写插件 readme 链接:
【快速开始】
1. 从插件市场导入 m-permission-manager 到项目 uni_modules 目录
2. 在 manifest.json 中声明宿主所需权限(Android permissions / iOS privacyDescription)
鸿蒙端见插件 utssdk/app-harmony/module.json5,详见权限配置说明
3. 重新制作自定义基座或云打包(UTS 插件不支持标准基座)
4. 在代码中调用:
import {
checkPermission,
requestPermission,
openPermissionSettings,
} from '@/utils/permission-manager'
const result = await checkPermission('camera')
const granted = await requestPermission('camera')
if (!granted) {
await openPermissionSettings('camera')
}
【重要说明】
· 仅支持 App 端(Android / iOS / Harmony),不支持 H5 与小程序
· 插件负责运行时检查与申请,宿主仍需在 manifest 中声明静态权限
· iOS 蓝牙需在 privacyDescription 中配置 NSBluetoothAlwaysUsageDescription
· 鸿蒙相册/通讯录为 system_basic 权限,默认调试包未声明,需 ACL 签名后启用
完整文档:
· readme — API 与快速开始
· 使用规范 — 集成约束与推荐流程
· 权限配置说明 — 三端权限文件配置详解
权限与隐私声明(dcloudext.declaration 对应项)
| 字段 | 填写内容 |
|---|---|
| 是否含广告 | 无 |
| 是否采集数据 | 不上传任何用户数据,仅在本地检查与申请系统权限 |
| 权限说明 | Android:宿主 manifest 声明对应 android.permission.;iOS:宿主 manifest 声明 NSCameraUsageDescription 等 Info.plist 键;Harmony:插件 module.json5 声明 ohos.permission.,运行时申请 |
平台支持(与 package.json 一致)
| 平台 | 支持情况 |
|---|---|
| uni-app Vue3 | ✅ |
| uni-app Vue2 | ❌ |
| H5 | ❌ |
| 小程序 | ❌ |
| Android App | ✅(minSdk 23) |
| iOS App | ✅(min iOS 14) |
| Harmony App | ✅(API 12+) |
截图建议(4–6 张)
- 权限演示页截图(三端权限列表与状态)
- Promise 封装调用代码示例
- Android 系统权限弹窗(相机 / 蓝牙)
- iOS 隐私授权弹窗
- manifest.json 权限配置示例
- 三端权限支持对照表
市场 FAQ 预填
Q:为什么必须自定义基座?
A:本插件为 UTS 原生插件,标准基座不包含。需制作含本插件的自定义基座或云打包。
Q:H5 能用吗?
A:不支持。仅 App 端(Android / iOS / Harmony)。
Q:安装插件后还需要配置权限吗?
A:需要。Android / iOS 必须在宿主 manifest.json 中声明静态权限与隐私说明;鸿蒙由插件 module.json5 合并,但 ACL 权限需额外签名。详见权限配置说明。
Q:三端权限 Key 一样吗?
A:统一使用 camera、microphone、location、bluetooth 等 Key,插件内部映射到各平台原生权限。部分 Key 仅 Android 支持(如 overlay、sms),调用时会返回 unsupported。
Q:iOS 蓝牙在 App 设置里找不到?
A:需在 manifest 配置 NSBluetoothAlwaysUsageDescription 并重新打包,且 App 需曾触发蓝牙权限弹窗。若 App 设置页无该项,请前往「设置 → 隐私与安全性 → 蓝牙」。系统蓝牙开关在「设置 → 蓝牙」,与应用授权无关。
Q:鸿蒙相册权限申请失败?
A:默认 module.json5 仅含 5 项 normal 权限,未声明相册读写。相册选图推荐 uni.chooseImage;若必须申请媒体权限,需用 module.full.json5 覆盖并配置 ACL 签名。
Q:用户永久拒绝后怎么办?
A:调用 openPermissionSettings(permissionKey) 跳转对应系统设置页,配合 getPermissionSettingsGuide() 展示引导文案。
版本号规范
遵循 语义化版本:
| 变更类型 | 版本 bump | 示例 |
|---|---|---|
| 不兼容 API 变更 | major | 2.0.0 |
| 新增功能(兼容) | minor | 1.1.0 |
| Bug 修复 | patch | 1.0.1 |
每次发版同步更新 package.json → version 和 changelog.md。
联系方式
发布前请在 package.json → dcloudext.contact.qq 填写维护者 QQ,或在插件市场后台配置联系方式。

收藏人数:
下载插件并导入HBuilderX
赞赏(1)
下载 298
赞赏 0
下载 12474227
赞赏 1936
赞赏
京公网安备:11010802035340号