408249787@qq.com 5 ngày trước cách đây
mục cha
commit
51a201adfa

+ 47 - 9
pages/catalog/detail.uvue

@@ -4,6 +4,13 @@ import Loading from '@/components/loading.uvue'
 import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
 import { type SubjectCourseResult, fetchSubjectCourseApp, updateSubjectProgress, queryNextSubjectCourse } from '@/services/subject/course'
 import { router, user } from '@/.cool'
+import {
+  checkPermission,
+  requestPermissions,
+} from '@/uni_modules/m-permission-manager'
+import { useUi } from "@/uni_modules/cool-ui";
+
+const ui = useUi();
 const isLoading = ref(true)
 const showControls = ref(true)
 const showVideo = ref(true)
@@ -209,15 +216,46 @@ function handleTop() {
   progress2.value = 5
 }
 function startRecord() {
-  console.log('开始录音');
-  timer.value = setInterval(() => {
-    timers.value--
-    console.log(timers.value);
-    if (timers.value <= 0) {
-      endRecord()
-    }
-  }, 800)
-  recorderManager.value.start();
+  checkPermission({
+    permission: 'microphone',
+    success(res) {
+      console.log('microphone', res)
+      if (res.granted == true) {
+        console.log('开始录音');
+        timer.value = setInterval(function () {
+          timers.value--
+          if (timers.value <= 0) {
+            endRecord()
+          }
+        }, 1000)
+        recorderManager.value?.start({});
+      } else {
+        ui.showConfirm({
+          title: "获取麦克风权限",
+          message: "本应用仅在你主动触发录音操作时调用麦克风,仅用于临时实时录音,录音内容仅在当前页面本地播放,供你本人收听。你可随时在系统设置中关闭麦克风权限,关闭后将无法使用录音功能,不影响应用其他功能正常使用",
+          callback(action) {
+            if (action === "confirm") {
+              requestPermissions({
+                permissions: ['microphone'],
+                success(res) {
+                  console.log('microphone', res)
+                },
+                fail(err) {
+                  console.error(err.errCode, err.errMsg)
+                },
+              })
+              // 执行删除操作
+            } else {
+              console.log("用户取消操作");
+            }
+          },
+        });
+      }
+    },
+    fail(err) {
+      console.error(err.errCode, err.errMsg)
+    },
+  })
 }
 function endRecord() {
   clearInterval(timer.value)

+ 20 - 0
uni_modules/m-permission-manager/changelog.md

@@ -0,0 +1,20 @@
+## 1.0.3(2026-06-17)
+添加使用说明
+# 更新日志
+
+## 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-manager` Promise 封装

+ 113 - 0
uni_modules/m-permission-manager/index.d.ts

@@ -0,0 +1,113 @@
+/**
+ * m-permission-manager UTS 插件 TypeScript 类型声明
+ */
+
+export type PermissionKey =
+	| 'camera'
+	| 'photoLibrary'
+	| 'storage'
+	| 'readStorage'
+	| 'location'
+	| 'coarseLocation'
+	| 'microphone'
+	| 'contacts'
+	| 'writeContacts'
+	| 'bluetooth'
+	| 'phone'
+	| 'phoneState'
+	| 'sms'
+	| 'readSms'
+	| 'receiveSms'
+	| 'overlay'
+	| 'writeSettings'
+
+export type PermissionStatus =
+	| 'granted'
+	| 'denied'
+	| 'notDetermined'
+	| 'restricted'
+	| 'unsupported'
+
+export interface PermissionCheckResult {
+	permission: PermissionKey
+	status: PermissionStatus
+	granted: boolean
+}
+
+export interface PermissionGrantResult {
+	permission: PermissionKey
+	status: PermissionStatus
+	granted: boolean
+}
+
+export interface CheckPermissionsResult {
+	results: PermissionCheckResult[]
+	allGranted: boolean
+}
+
+export interface RequestPermissionsResult {
+	results: PermissionGrantResult[]
+	allGranted: boolean
+}
+
+export interface PermissionFail {
+	errCode: 9020001 | 9020002 | 9020003 | 9020004 | 9020005 | 9020006
+	errMsg: string
+}
+
+export interface CheckPermissionOptions {
+	permission: PermissionKey
+	success?: (res: PermissionCheckResult) => void
+	fail?: (res: PermissionFail) => void
+	complete?: (res: unknown) => void
+}
+
+export interface CheckPermissionsOptions {
+	permissions: PermissionKey[]
+	success?: (res: CheckPermissionsResult) => void
+	fail?: (res: PermissionFail) => void
+	complete?: (res: unknown) => void
+}
+
+export interface RequestPermissionOptions {
+	permission: PermissionKey
+	success?: (res: PermissionGrantResult) => void
+	fail?: (res: PermissionFail) => void
+	complete?: (res: unknown) => void
+}
+
+export interface RequestPermissionsOptions {
+	permissions: PermissionKey[]
+	success?: (res: RequestPermissionsResult) => void
+	fail?: (res: PermissionFail) => void
+	complete?: (res: unknown) => void
+}
+
+export interface OpenAppSettingsOptions {
+	/** 指定权限类型,各端将尽量跳转到对应设置页 */
+	permission?: PermissionKey
+	success?: (res: PermissionCheckResult) => void
+	fail?: (res: PermissionFail) => void
+	complete?: (res: unknown) => void
+}
+
+export interface GetSupportedPermissionsResult {
+	permissions: PermissionKey[]
+}
+
+export interface BluetoothDemoInfo {
+	available: boolean
+	enabled: boolean
+	stateText: string
+	stateCode: number
+}
+
+export function checkPermissionSync(permission: PermissionKey): PermissionCheckResult
+export function checkPermissionsSync(permissions: PermissionKey[]): CheckPermissionsResult
+export function checkPermission(options: CheckPermissionOptions): void
+export function checkPermissions(options: CheckPermissionsOptions): void
+export function requestPermission(options: RequestPermissionOptions): void
+export function requestPermissions(options: RequestPermissionsOptions): void
+export function openAppSettings(options?: OpenAppSettingsOptions): void
+export function getSupportedPermissionsSync(): GetSupportedPermissionsResult
+export function getBluetoothDemoInfoSync(): BluetoothDemoInfo

+ 44 - 0
uni_modules/m-permission-manager/index.ts

@@ -0,0 +1,44 @@
+/**
+ * App 端编译时模块入口为 uts-proxy,Promise 封装请从 @/utils/permission-manager 引入。
+ */
+// #ifdef APP-ANDROID
+export {
+	checkPermissionSync,
+	checkPermissionsSync,
+	checkPermission,
+	checkPermissions,
+	requestPermission,
+	requestPermissions,
+	openAppSettings,
+	getSupportedPermissionsSync,
+	getBluetoothDemoInfoSync,
+} from './utssdk/app-android/index.uts'
+// #endif
+
+// #ifdef APP-IOS
+export {
+	checkPermissionSync,
+	checkPermissionsSync,
+	checkPermission,
+	checkPermissions,
+	requestPermission,
+	requestPermissions,
+	openAppSettings,
+	getSupportedPermissionsSync,
+	getBluetoothDemoInfoSync,
+} from './utssdk/app-ios/index.uts'
+// #endif
+
+// #ifdef APP-HARMONY
+export {
+	checkPermissionSync,
+	checkPermissionsSync,
+	checkPermission,
+	checkPermissions,
+	requestPermission,
+	requestPermissions,
+	openAppSettings,
+	getSupportedPermissionsSync,
+	getBluetoothDemoInfoSync,
+} from './utssdk/app-harmony/index.uts'
+// #endif

+ 110 - 0
uni_modules/m-permission-manager/package.json

@@ -0,0 +1,110 @@
+{
+	"id": "m-permission-manager",
+	"displayName": "m-permission-manager 跨端权限管理",
+	"version": "1.0.3",
+	"description": "uni-app Vue3 跨端权限管理 UTS 插件,支持 Android / iOS / Harmony,统一 API 检查与申请运行时权限",
+	"keywords": [
+        "权限",
+        "permission",
+        "m-permission-manager",
+        "UTS"
+    ],
+	"repository": "",
+	"engines": {
+		"HBuilderX": "^4.24.0",
+		"uni-app": "^4.0",
+		"uni-app-x": "^4.0"
+	},
+	"dcloudext": {
+		"type": "uts",
+		"sale": {
+			"regular": {
+				"price": "0.00"
+			},
+			"sourcecode": {
+				"price": "0.00"
+			}
+		},
+		"contact": {
+			"qq": ""
+		},
+		"declaration": {
+			"ads": "无",
+			"data": "不上传任何用户数据,仅在本地检查与申请系统权限",
+			"permissions": "Android:宿主 manifest 声明对应 android.permission.*;iOS:宿主 manifest 声明 NSCameraUsageDescription 等 Info.plist 键;Harmony:插件 module.json5 声明 ohos.permission.*,运行时申请"
+		},
+		"npmurl": "",
+		"darkmode": "x",
+		"i18n": "x",
+		"widescreen": "x"
+	},
+	"uni_modules": {
+		"dependencies": [],
+		"encrypt": [],
+		"platforms": {
+			"cloud": {
+				"tcb": "x",
+				"aliyun": "x",
+				"alipay": "x"
+			},
+			"client": {
+				"uni-app": {
+					"vue": {
+						"vue2": "-",
+                        "vue3": "√"
+					},
+					"web": {
+						"safari": "x",
+						"chrome": "x"
+					},
+					"app": {
+						"vue": "-",
+						"nvue": "-",
+						"android": "-",
+						"ios": "-",
+						"harmony": "-"
+					},
+					"mp": {
+						"weixin": "x",
+						"alipay": "x",
+						"toutiao": "x",
+						"baidu": "x",
+						"kuaishou": "x",
+						"jd": "x",
+						"harmony": "x",
+						"qq": "x",
+						"lark": "x",
+						"xhs": "x"
+					},
+					"quickapp": {
+						"huawei": "x",
+						"union": "x"
+					}
+				},
+				"uni-app-x": {
+					"web": {
+						"safari": "x",
+						"chrome": "x"
+					},
+					"app": {
+						"android": {
+							"extVersion": "",
+							"minVersion": "23"
+						},
+						"ios": {
+							"extVersion": "",
+							"minVersion": "14"
+						},
+						"harmony": {
+							"extVersion": "",
+							"minVersion": "12"
+						}
+					},
+					"mp": {
+						"weixin": "x"
+					}
+				}
+			}
+		}
+	}
+}

+ 971 - 0
uni_modules/m-permission-manager/readme.md

@@ -0,0 +1,971 @@
+# m-permission-manager
+
+uni-app Vue3 跨端权限管理 UTS 插件,支持 **Android / iOS / Harmony** 三端,提供统一的权限检查与申请 API。
+
+## 功能
+
+- 统一权限标识,三端通用
+- 同步 / 异步检查权限状态
+- 单个 / 批量申请权限
+- 跳转系统应用设置页
+- 获取当前平台支持的权限列表
+
+## 安装
+
+插件位于 `src/uni_modules/m-permission-manager/`,随项目直接使用,无需额外安装。
+
+## 文档
+
+> 插件市场仅展示本 readme,下方链接为文内锚点,可直接跳转。
+
+| 文档 | 说明 |
+| --- | --- |
+| [使用规范](#使用规范) | 集成约束、推荐流程、审核注意事项 |
+| [权限配置说明](#权限配置说明) | Android / iOS / Harmony 三端权限文件配置详解 |
+| [发布指南](#发布指南) | DCloud 插件市场发布填写参考(插件作者) |
+| [更新日志](#更新日志) | 版本更新记录 |
+
+## 快速开始
+
+### 方式一:Promise 封装(推荐)
+
+```js
+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 风格)
+
+```js
+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)
+  },
+})
+```
+
+## 统一权限标识
+
+| 权限 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()` | 获取当前平台支持的权限列表 |
+
+## 宿主配置
+
+> 完整配置说明见 [权限配置说明](#权限配置说明)。
+
+### Android
+
+在 `manifest.json` → `app-plus.distribute.android.permissions` 中声明所需权限,例如:
+
+```json
+"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\"/>"
+]
+```
+
+Android 13+ 读写媒体文件需使用 `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`;插件会同时校验**设备 API** 与宿主 **targetSdkVersion**,仅当 `targetSdkVersion >= 33` 时才申请新媒体权限,否则回退 `READ_EXTERNAL_STORAGE`(兼容未升级 targetSdk 的自定义基座)。
+
+### iOS
+
+在 `manifest.json` → `app-plus.distribute.ios.privacyDescription` 中配置权限说明,例如:
+
+```json
+"privacyDescription": {
+  "NSCameraUsageDescription": "需要使用相机进行拍照",
+  "NSMicrophoneUsageDescription": "需要使用麦克风进行录音",
+  "NSPhotoLibraryUsageDescription": "需要访问相册以选择图片",
+  "NSLocationWhenInUseUsageDescription": "需要获取位置信息",
+  "NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接周边设备"
+}
+```
+
+> **iOS 蓝牙说明**:蓝牙授权开关出现在「设置 → 本 App → 蓝牙」(需声明 `NSBluetoothAlwaysUsageDescription` 且 App 曾触发过蓝牙权限弹窗)。若 App 设置页无该项,请前往「设置 → 隐私与安全性 → 蓝牙」。系统蓝牙开关在「设置 → 蓝牙」,与应用授权无关。Apple 不提供跳转到「隐私 → 蓝牙」的公开深链,插件会打开 App 设置页并配合引导文案。
+
+### Harmony
+
+插件 `utssdk/app-harmony/module.json5` 已声明鸿蒙端**全部可映射**的 9 项权限:
+
+| 权限 Key | ohos.permission | APL 等级 | 说明 |
+| --- | --- | --- | --- |
+| `camera` | CAMERA | normal | 可直接安装使用 |
+| `microphone` | MICROPHONE | normal | 可直接安装使用 |
+| `location` | LOCATION | normal | 可直接安装使用 |
+| `coarseLocation` | APPROXIMATELY_LOCATION | normal | 可直接安装使用 |
+| `bluetooth` | ACCESS_BLUETOOTH | normal | 可直接安装使用 |
+| `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 签名 |
+
+鸿蒙**不支持**(无系统 API):`phone`、`phoneState`、`sms`、`readSms`、`receiveSms`、`overlay`、`writeSettings`。
+
+#### 配置文件说明
+
+| 文件 | 权限数量 | 用途 |
+| --- | --- | --- |
+| `module.json5` | 5 项(normal) | **默认**,调试证书可直接安装 |
+| `module.normal.json5` | 5 项 | 与 `module.json5` 相同 |
+| `module.full.json5` | 9 项 | 含相册/通讯录等 system_basic 权限,需 ACL 后启用 |
+
+启用完整权限:将 `module.full.json5` 的内容覆盖 `module.json5`,并在 AppGallery Connect 配置 ACL 签名。
+
+#### 安装报 9568289 时
+
+说明当前 `module.json5` 中仍包含未签名的 system_basic 权限(如 `READ_CONTACTS`)。请确认使用的是默认的 5 项 normal 版本;若已手动合并 `module.full.json5`,需先完成 ACL 白名单与 Profile 签名配置。
+
+相册选图场景仍推荐使用系统 **PhotoViewPicker**(`uni.chooseImage`),无需申请媒体读写权限。
+
+#### 系统设置里找不到相册权限?
+
+默认 `module.json5` 为可安装调试包,**未声明** `READ_IMAGEVIDEO` / `WRITE_IMAGEVIDEO`,因此:
+
+- 系统「设置 → 应用 → 本应用 → 权限」中**不会出现**相册/媒体开关
+- 插件会将相册相关权限标记为 **未声明(restricted)**
+- 演示页可直接点 **「选图」** 试用,走系统相册选择器,不依赖该权限
+
+若业务必须申请媒体读写权限:用 `module.full.json5` 覆盖 `module.json5`,在 AppGallery Connect 配置 ACL 并重新签名安装后,系统设置中才会出现对应权限项。
+
+## 错误码
+
+| errCode | 说明 |
+| --- | --- |
+| 9020001 | 当前环境不支持 |
+| 9020002 | 用户拒绝授权 |
+| 9020003 | 权限不支持当前平台 |
+| 9020004 | 打开系统设置失败 |
+| 9020005 | 权限检查失败 |
+| 9020006 | 权限申请失败 |
+
+## 注意事项
+
+1. **H5 不支持**:本插件仅适用于 App(Android / iOS)与 Harmony 端。
+2. **需自定义基座**:UTS 插件修改后需在 HBuilderX 中重新制作自定义基座或云打包。
+3. **永久拒绝**:用户选择「不再询问」后,需引导用户通过 `openAppSettings()` 手动开启。
+4. **特殊权限**:Android 悬浮窗(`overlay`)和系统设置(`writeSettings`)需跳转系统设置页授权,申请结果可能为 `notDetermined`。
+
+
+---
+
+## 使用规范
+
+本文档面向集成本插件的 App 开发者,说明 **必须遵守的约束**、**推荐做法** 和 **上架审核注意事项**。
+
+---
+
+### 1. 基本约束(必须遵守)
+
+#### 1.1 用户主动触发
+
+`requestPermission` / `requestPermissions` **必须由真实用户手势触发**,例如按钮 `click` / `tap`。
+
+```vue
+<!-- ✅ 正确 -->
+<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`,不要一次性申请全部权限。
+
+```js
+// ✅ 审核页
+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,避免向用户展示无效入口。
+
+```js
+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`:
+
+```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 风格)
+
+```js
+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 永久拒绝后的引导
+
+用户选择「不再询问」或多次拒绝后,应引导至系统设置:
+
+```js
+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. 自定义基座与发版
+
+1. 导入或更新插件后,**必须重新制作自定义基座**或云打包
+2. 修改 `manifest.json` 权限或鸿蒙 `module.json5` 后,需重新打包
+3. iOS 修改 `privacyDescription` 后需重新打包,必要时删除旧 App 重装以重新触发授权弹窗
+
+---
+
+### 7. 参考文档
+
+- [readme](#m-permission-manager) — API 与快速开始
+- [权限配置说明](#权限配置说明) — 三端权限文件配置详解
+- [发布指南](#发布指南) — 插件市场发布填写参考
+
+## 权限配置说明
+
+本文档说明集成本插件时,**各端需要修改哪些文件**、**如何配置权限**,以及插件内置配置文件的作用。
+
+---
+
+### 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`
+
+```json
+{
+  "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`
+
+```json
+{
+  "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 设置页并配合引导文案。
+
+---
+
+### 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 中配置权限**(可选,非必须):
+
+1. 先运行一次鸿蒙编译,从 `unpackage/dist/dev/app-harmony/entry/src/main/module.json5` 复制完整文件到 `harmony-configs/entry/src/main/module.json5`
+2. **仅修改** `module.requestPermissions` 节点,其他节点不要删(uni-app 是**文件级覆盖**,不是 JSON 节点合并)
+3. 不要与插件已声明的权限重复
+
+> 集成 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 版本
+
+1. 将 `module.full.json5` 的内容**覆盖** `module.json5`
+2. 在 AppGallery Connect 配置 ACL 白名单
+3. 使用正式 Profile 重新签名打包
+4. 重新制作自定义基座或云打包
+
+#### 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:
+
+```json
+"permissions": [
+  "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+  "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
+]
+```
+
+**iOS** privacyDescription:
+
+```json
+"privacyDescription": {
+  "NSCameraUsageDescription": "需要使用相机",
+  "NSMicrophoneUsageDescription": "需要使用麦克风"
+}
+```
+
+**Harmony**:默认 `module.json5` 已包含,无需修改。
+
+#### 5.2 需要蓝牙
+
+**Android** manifest 增加:
+
+```json
+"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
+"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>"
+```
+
+**iOS** privacyDescription 增加:
+
+```json
+"NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接周边设备"
+```
+
+**Harmony**:默认 `module.json5` 已包含 `ACCESS_BLUETOOTH`。
+
+#### 5.3 需要相册读写(Harmony 完整权限)
+
+1. 用 `module.full.json5` 覆盖 `module.json5`
+2. 配置 ACL 签名
+3. Android 增加 `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`
+4. iOS 增加 `NSPhotoLibraryUsageDescription`
+
+> 若仅需选图不上传,推荐使用 `uni.chooseImage`,三端均无需媒体读写权限。
+
+---
+
+### 6. 配置后必做
+
+1. **重新制作自定义基座**或云打包(UTS 插件 + manifest 变更均须重新打包)
+2. iOS 修改 `privacyDescription` 后建议删除旧 App 重装,以重新触发授权弹窗
+3. Harmony 切换 `module.json5` 后需重新签名安装
+
+---
+
+### 7. 相关文档
+
+- [readme](#m-permission-manager) — API 与权限 Key 对照表
+- [使用规范](#使用规范) — 集成流程与审核注意事项
+- [发布指南](#发布指南) — 插件市场发布填写参考
+
+## 发布指南
+
+面向插件作者,说明如何将本插件提交到 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 代码后已重新制作基座
+
+---
+
+### 提交到插件市场
+
+1. 登录 [DCloud 插件市场](https://ext.dcloud.net.cn/)
+2. 进入 **我的插件** → **发布插件**
+3. 插件类型选择 **UTS 插件**
+4. 上传 `uni_modules/m-permission-manager` 目录(或打包 zip)
+5. 按下方表格填写市场展示信息
+
+---
+
+### 市场后台填写参考
+
+#### 基本信息
+
+| 字段 | 填写内容 |
+| --- | --- |
+| **插件名称** | 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](#m-permission-manager) — 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 张)
+
+1. 权限演示页截图(三端权限列表与状态)
+2. Promise 封装调用代码示例
+3. Android 系统权限弹窗(相机 / 蓝牙)
+4. iOS 隐私授权弹窗
+5. manifest.json 权限配置示例
+6. 三端权限支持对照表
+
+---
+
+### 市场 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()` 展示引导文案。
+
+---
+
+### 版本号规范
+
+遵循 [语义化版本](https://semver.org/lang/zh-CN/):
+
+| 变更类型 | 版本 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,或在插件市场后台配置联系方式。
+
+---
+
+### 参考链接
+
+- [UTS 插件开发文档](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
+- [插件市场发布流程](https://uniapp.dcloud.net.cn/plugin/publish.html)
+- 插件内 [readme](#m-permission-manager)
+- 插件内 [使用规范](#使用规范)
+- 插件内 [权限配置说明](#权限配置说明)
+
+## 更新日志
+
+### 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` 跳转对应系统设置页
+- 新增 [发布指南](#发布指南)、[使用规范](#使用规范)、[权限配置说明](#权限配置说明) 文档
+- readme 补充 iOS 蓝牙说明与 `NSBluetoothAlwaysUsageDescription`
+
+### 1.0.0(2026-05-29)
+
+- 初始版本
+- 支持 Android / iOS / Harmony 三端统一权限检查与申请
+- 提供同步 / 异步 API 及 `@/utils/permission-manager` Promise 封装

+ 4 - 0
uni_modules/m-permission-manager/utssdk/app-android/config.json

@@ -0,0 +1,4 @@
+{
+	"minSdkVersion": 23,
+	"dependencies": []
+}

+ 530 - 0
uni_modules/m-permission-manager/utssdk/app-android/index.uts

@@ -0,0 +1,530 @@
+import Build from 'android.os.Build'
+import Intent from 'android.content.Intent'
+import Settings from 'android.provider.Settings'
+import Uri from 'android.net.Uri'
+import ContextCompat from 'androidx.core.content.ContextCompat'
+import ActivityCompat from 'androidx.core.app.ActivityCompat'
+import PackageManager from 'android.content.pm.PackageManager'
+import BluetoothAdapter from 'android.bluetooth.BluetoothAdapter'
+import { UTSAndroid } from 'io.dcloud.uts'
+import {
+	BluetoothDemoInfo,
+	CheckPermission,
+	CheckPermissionOptions,
+	CheckPermissions,
+	CheckPermissionsOptions,
+	CheckPermissionsResult,
+	CheckPermissionsSync,
+	CheckPermissionSync,
+	GetSupportedPermissionsResult,
+	GetSupportedPermissionsSync,
+	GetBluetoothDemoInfoSync,
+	OpenAppSettings,
+	OpenAppSettingsOptions,
+	PermissionCheckResult,
+	PermissionFail,
+	PermissionKey,
+	PermissionGrantResult,
+	PermissionStatus,
+	RequestPermission,
+	RequestPermissionOptions,
+	RequestPermissions,
+	RequestPermissionsOptions,
+	RequestPermissionsResult,
+} from '../interface.uts'
+import { PermissionFailImpl } from '../unierror'
+
+const ANDROID_API_TIRAMISU : Int = 33
+const ANDROID_API_S : Int = 31
+
+function getAppTargetSdkVersion() : Int {
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		return 0
+	}
+	return activity.getApplicationInfo().targetSdkVersion
+}
+
+/** 设备 API 与宿主 targetSdkVersion 均满足时才使用 READ_MEDIA_*(UTS 运行时强校验 targetSdk) */
+function shouldUseReadMediaPermissions() : boolean {
+	if (Build.VERSION.SDK_INT < ANDROID_API_TIRAMISU) {
+		return false
+	}
+	return getAppTargetSdkVersion() >= ANDROID_API_TIRAMISU
+}
+
+/** 设备 API 与宿主 targetSdkVersion 均满足时才使用 BLUETOOTH_CONNECT / BLUETOOTH_SCAN */
+function shouldUseBluetoothRuntimePermissions() : boolean {
+	if (Build.VERSION.SDK_INT < ANDROID_API_S) {
+		return false
+	}
+	return getAppTargetSdkVersion() >= ANDROID_API_S
+}
+
+const SUPPORTED_PERMISSIONS : PermissionKey[] = [
+	'camera',
+	'photoLibrary',
+	'storage',
+	'readStorage',
+	'location',
+	'coarseLocation',
+	'microphone',
+	'contacts',
+	'writeContacts',
+	'bluetooth',
+	'phone',
+	'phoneState',
+	'sms',
+	'readSms',
+	'receiveSms',
+	'overlay',
+	'writeSettings',
+]
+
+function createCheckResult(permission : PermissionKey, status : PermissionStatus) : PermissionCheckResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+function createRequestResult(permission : PermissionKey, status : PermissionStatus) : PermissionGrantResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+function resolveAndroidPermissions(permission : PermissionKey) : string[] | null {
+	switch (permission) {
+		case 'camera':
+			return ['android.permission.CAMERA']
+		case 'photoLibrary':
+			if (shouldUseReadMediaPermissions()) {
+				return ['android.permission.READ_MEDIA_IMAGES']
+			}
+			return ['android.permission.READ_EXTERNAL_STORAGE']
+		case 'storage':
+			if (shouldUseReadMediaPermissions()) {
+				return ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO']
+			}
+			return ['android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE']
+		case 'readStorage':
+			if (shouldUseReadMediaPermissions()) {
+				return ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO']
+			}
+			return ['android.permission.READ_EXTERNAL_STORAGE']
+		case 'location':
+			return ['android.permission.ACCESS_FINE_LOCATION']
+		case 'coarseLocation':
+			return ['android.permission.ACCESS_COARSE_LOCATION']
+		case 'microphone':
+			return ['android.permission.RECORD_AUDIO']
+		case 'contacts':
+			return ['android.permission.READ_CONTACTS']
+		case 'writeContacts':
+			return ['android.permission.WRITE_CONTACTS']
+		case 'bluetooth':
+			if (shouldUseBluetoothRuntimePermissions()) {
+				return ['android.permission.BLUETOOTH_CONNECT', 'android.permission.BLUETOOTH_SCAN']
+			}
+			return ['android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN']
+		case 'phone':
+			return ['android.permission.CALL_PHONE']
+		case 'phoneState':
+			return ['android.permission.READ_PHONE_STATE']
+		case 'sms':
+			return ['android.permission.SEND_SMS']
+		case 'readSms':
+			return ['android.permission.READ_SMS']
+		case 'receiveSms':
+			return ['android.permission.RECEIVE_SMS']
+		case 'overlay':
+		case 'writeSettings':
+			return []
+		default:
+			return null
+	}
+}
+
+function isPermissionGranted(permission : PermissionKey) : boolean {
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		return false
+	}
+
+	if (permission == 'overlay') {
+		return Settings.canDrawOverlays(activity)
+	}
+	if (permission == 'writeSettings') {
+		return Settings.System.canWrite(activity)
+	}
+
+	const androidPermissions = resolveAndroidPermissions(permission)
+	if (androidPermissions == null) {
+		return false
+	}
+	if (androidPermissions.length == 0) {
+		return false
+	}
+	return UTSAndroid.checkSystemPermissionGranted(activity, androidPermissions)
+}
+
+function resolvePermissionStatus(permission : PermissionKey) : PermissionStatus {
+	const androidPermissions = resolveAndroidPermissions(permission)
+	if (androidPermissions == null) {
+		return 'unsupported'
+	}
+
+	if (permission == 'overlay' || permission == 'writeSettings') {
+		return isPermissionGranted(permission) ? 'granted' : 'notDetermined'
+	}
+
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		return 'denied'
+	}
+
+	let allGranted = true
+	let hasDenied = false
+	for (let i = 0; i < androidPermissions.length; i++) {
+		const androidPermission = androidPermissions[i]
+		const granted = ContextCompat.checkSelfPermission(activity, androidPermission) == PackageManager.PERMISSION_GRANTED
+		if (granted) {
+			continue
+		}
+		allGranted = false
+		if (ActivityCompat.shouldShowRequestPermissionRationale(activity, androidPermission)) {
+			hasDenied = true
+		}
+	}
+	if (allGranted) {
+		return 'granted'
+	}
+	if (hasDenied) {
+		return 'denied'
+	}
+	return 'notDetermined'
+}
+
+function succeedCheckPermission(options : CheckPermissionOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedCheckPermissions(options : CheckPermissionsOptions, res : CheckPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermission(options : RequestPermissionOptions, res : PermissionGrantResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermissions(options : RequestPermissionsOptions, res : RequestPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedOpenAppSettings(options : OpenAppSettingsOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function failRequestPermission(options : RequestPermissionOptions, err : PermissionFail) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFail) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFail) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function requestOverlayPermission(options : RequestPermissionOptions) {
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用'))
+		return
+	}
+	if (Settings.canDrawOverlays(activity)) {
+		succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
+		return
+	}
+	const intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
+	intent.setData(Uri.parse('package:' + activity.getPackageName()))
+	activity.startActivity(intent)
+	succeedRequestPermission(options, createRequestResult(options.permission, 'notDetermined'))
+}
+
+function requestWriteSettingsPermission(options : RequestPermissionOptions) {
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用'))
+		return
+	}
+	if (Settings.System.canWrite(activity)) {
+		succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
+		return
+	}
+	const intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
+	intent.setData(Uri.parse('package:' + activity.getPackageName()))
+	activity.startActivity(intent)
+	succeedRequestPermission(options, createRequestResult(options.permission, 'notDetermined'))
+}
+
+function requestRuntimePermission(options : RequestPermissionOptions) {
+	const activity = UTSAndroid.getUniActivity()
+	const androidPermissions = resolveAndroidPermissions(options.permission)
+	if (activity == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用'))
+		return
+	}
+	if (androidPermissions == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020003))
+		return
+	}
+	if (androidPermissions.length == 0) {
+		failRequestPermission(options, new PermissionFailImpl(9020003))
+		return
+	}
+
+	if (UTSAndroid.checkSystemPermissionGranted(activity, androidPermissions)) {
+		succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
+		return
+	}
+
+	try {
+		UTSAndroid.requestSystemPermission(
+			activity,
+			androidPermissions,
+			(allGranted : boolean, _ : string[]) => {
+				if (allGranted) {
+					succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
+					return
+				}
+				succeedRequestPermission(options, createRequestResult(options.permission, 'denied'))
+			},
+			(_ : boolean, _grantedList : string[]) => {
+				succeedRequestPermission(options, createRequestResult(options.permission, 'denied'))
+			},
+		)
+	} catch (ignored) {
+		failRequestPermission(options, new PermissionFailImpl(9020006, '权限申请失败,请确认 manifest 已声明对应权限且 targetSdkVersion 满足要求'))
+	}
+}
+
+export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult {
+	const status = resolvePermissionStatus(permission)
+	return createCheckResult(permission, status)
+}
+
+export const checkPermissionsSync : CheckPermissionsSync = function (permissions : PermissionKey[]) : CheckPermissionsResult {
+	const results : PermissionCheckResult[] = []
+	let allGranted = true
+	for (let i = 0; i < permissions.length; i++) {
+		const item = checkPermissionSync(permissions[i])
+		results.push(item)
+		if (!item.granted) {
+			allGranted = false
+		}
+	}
+	return {
+		results: results,
+		allGranted: allGranted,
+	}
+}
+
+export const checkPermission : CheckPermission = function (options : CheckPermissionOptions) {
+	const result = checkPermissionSync(options.permission)
+	succeedCheckPermission(options, result)
+}
+
+export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) {
+	const result = checkPermissionsSync(options.permissions)
+	succeedCheckPermissions(options, result)
+}
+
+export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) {
+	if (options.permission == 'overlay') {
+		requestOverlayPermission(options)
+		return
+	}
+	if (options.permission == 'writeSettings') {
+		requestWriteSettingsPermission(options)
+		return
+	}
+	requestRuntimePermission(options)
+}
+
+export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) {
+	const permissions = options.permissions
+	if (permissions.length == 0) {
+		const emptyResult : RequestPermissionsResult = {
+			results: [],
+			allGranted: true,
+		}
+		succeedRequestPermissions(options, emptyResult)
+		return
+	}
+
+	let currentIndex = 0
+	const collectedResults : PermissionGrantResult[] = []
+
+	function requestNext() {
+		if (currentIndex >= permissions.length) {
+			let allGranted = true
+			for (let i = 0; i < collectedResults.length; i++) {
+				if (!collectedResults[i].granted) {
+					allGranted = false
+					break
+				}
+			}
+			const finalResult : RequestPermissionsResult = {
+				results: collectedResults,
+				allGranted: allGranted,
+			}
+			succeedRequestPermissions(options, finalResult)
+			return
+		}
+
+		const permission = permissions[currentIndex]
+		currentIndex += 1
+		requestPermission({
+			permission: permission,
+			success: (res : PermissionGrantResult) => {
+				collectedResults.push(res)
+				requestNext()
+			},
+			fail: (err : PermissionFail) => {
+				collectedResults.push(createRequestResult(permission, 'denied'))
+				failRequestPermissions(options, err)
+			},
+		})
+	}
+
+	requestNext()
+}
+
+export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) {
+	const activity = UTSAndroid.getUniActivity()
+	if (activity == null) {
+		failOpenAppSettings(options, new PermissionFailImpl(9020004, 'Activity 不可用'))
+		return
+	}
+	try {
+		const permission = options.permission
+		let intent : Intent
+		if (permission == 'overlay') {
+			intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
+			intent.setData(Uri.parse('package:' + activity.getPackageName()))
+		} else if (permission == 'writeSettings') {
+			intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
+			intent.setData(Uri.parse('package:' + activity.getPackageName()))
+		} else {
+			intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+			intent.setData(Uri.parse('package:' + activity.getPackageName()))
+		}
+		activity.startActivity(intent)
+		const resultPermission = permission != null ? permission : 'camera'
+		succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted'))
+	} catch (ignored) {
+		failOpenAppSettings(options, new PermissionFailImpl(9020004))
+	}
+}
+
+export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult {
+	return {
+		permissions: SUPPORTED_PERMISSIONS,
+	}
+}
+
+function mapAndroidBluetoothState(state : Int) : string {
+	switch (state) {
+		case BluetoothAdapter.STATE_OFF:
+			return '已关闭'
+		case BluetoothAdapter.STATE_TURNING_ON:
+			return '正在打开'
+		case BluetoothAdapter.STATE_ON:
+			return '已开启'
+		case BluetoothAdapter.STATE_TURNING_OFF:
+			return '正在关闭'
+		default:
+			return '未知'
+	}
+}
+
+export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo {
+	const adapter = BluetoothAdapter.getDefaultAdapter()
+	if (adapter == null) {
+		return {
+			available: false,
+			enabled: false,
+			stateText: '设备不支持蓝牙',
+			stateCode: -1,
+		}
+	}
+	const state = adapter.getState()
+	return {
+		available: true,
+		enabled: state == BluetoothAdapter.STATE_ON,
+		stateText: mapAndroidBluetoothState(state),
+		stateCode: state,
+	}
+}

+ 3 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/config.json

@@ -0,0 +1,3 @@
+{
+	"dependencies": []
+}

+ 508 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/index.uts

@@ -0,0 +1,508 @@
+import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
+import { Permissions, PermissionRequestResult as AtManagerPermissionResult } from '@ohos.abilityAccessCtrl'
+import { BusinessError } from '@ohos.base'
+import common from '@ohos.app.ability.common'
+import Want from '@ohos.app.ability.Want'
+import access from '@ohos.bluetooth.access'
+import bundleManager from '@ohos.bundle.bundleManager'
+import {
+	BluetoothDemoInfo,
+	CheckPermission,
+	CheckPermissionOptions,
+	CheckPermissions,
+	CheckPermissionsOptions,
+	CheckPermissionsResult,
+	CheckPermissionsSync,
+	CheckPermissionSync,
+	GetSupportedPermissionsResult,
+	GetSupportedPermissionsSync,
+	GetBluetoothDemoInfoSync,
+	OpenAppSettings,
+	OpenAppSettingsOptions,
+	PermissionCheckResult,
+	PermissionGrantResult,
+	PermissionKey,
+	PermissionStatus,
+	RequestPermission,
+	RequestPermissionOptions,
+	RequestPermissions,
+	RequestPermissionsOptions,
+	RequestPermissionsResult,
+} from '../interface.uts'
+import { PermissionFailImpl } from '../unierror'
+
+const SUPPORTED_PERMISSIONS : PermissionKey[] = [
+	'camera',
+	'photoLibrary',
+	'storage',
+	'readStorage',
+	'location',
+	'coarseLocation',
+	'microphone',
+	'contacts',
+	'writeContacts',
+	'bluetooth',
+]
+
+/** 鸿蒙无对应系统能力的权限(Android 专属等) */
+const HARMONY_UNSUPPORTED : PermissionKey[] = [
+	'phone',
+	'phoneState',
+	'sms',
+	'readSms',
+	'receiveSms',
+	'overlay',
+	'writeSettings',
+]
+
+function createCheckResult(permission : PermissionKey, status : PermissionStatus) : PermissionCheckResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+function createGrantResult(permission : PermissionKey, status : PermissionStatus) : PermissionGrantResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+/** 需在 module.json5 声明且 ACL 签名的权限 */
+const HARMONY_ACL_PERMISSION_KEYS : PermissionKey[] = [
+	'photoLibrary',
+	'readStorage',
+	'storage',
+	'contacts',
+	'writeContacts',
+]
+
+function isHarmonyAclPermission(permission : PermissionKey) : boolean {
+	for (let i = 0; i < HARMONY_ACL_PERMISSION_KEYS.length; i++) {
+		if (HARMONY_ACL_PERMISSION_KEYS[i] == permission) {
+			return true
+		}
+	}
+	return false
+}
+
+function isHarmonyPermissionDeclared(harmonyPermission : Permissions) : boolean {
+	try {
+		const bundleInfo = bundleManager.getBundleInfoForSelfSync(
+			bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION,
+		)
+		const declared = bundleInfo.reqPermissionDetails
+		for (let i = 0; i < declared.length; i++) {
+			const item = declared[i]
+			if (item.name == harmonyPermission) {
+				return true
+			}
+		}
+		return false
+	} catch (ignored) {
+		return false
+	}
+}
+
+function createAclUndeclaredMessage(permission : PermissionKey) : string {
+	if (permission == 'photoLibrary' || permission == 'readStorage' || permission == 'storage') {
+		return '当前包未声明相册/媒体权限,系统设置无对应开关。选图请用系统相册选择器;完整权限需 module.full.json5 + ACL'
+	}
+	return '当前包未声明该权限,系统设置无对应开关。请使用 module.full.json5 并配置 ACL 后重签安装'
+}
+
+function isHarmonyAclUndeclared(permission : PermissionKey) : boolean {
+	if (!isHarmonyAclPermission(permission)) {
+		return false
+	}
+	const harmonyPermission = resolveHarmonyPermission(permission)
+	if (harmonyPermission == null) {
+		return false
+	}
+	return !isHarmonyPermissionDeclared(harmonyPermission)
+}
+
+function isHarmonyUnsupported(permission : PermissionKey) : boolean {
+	for (let i = 0; i < HARMONY_UNSUPPORTED.length; i++) {
+		if (HARMONY_UNSUPPORTED[i] == permission) {
+			return true
+		}
+	}
+	return false
+}
+
+function resolveHarmonyPermission(permission : PermissionKey) : Permissions | null {
+	switch (permission) {
+		case 'camera':
+			return 'ohos.permission.CAMERA'
+		case 'photoLibrary':
+		case 'readStorage':
+			return 'ohos.permission.READ_IMAGEVIDEO'
+		case 'storage':
+			return 'ohos.permission.WRITE_IMAGEVIDEO'
+		case 'location':
+			return 'ohos.permission.LOCATION'
+		case 'coarseLocation':
+			return 'ohos.permission.APPROXIMATELY_LOCATION'
+		case 'microphone':
+			return 'ohos.permission.MICROPHONE'
+		case 'contacts':
+			return 'ohos.permission.READ_CONTACTS'
+		case 'writeContacts':
+			return 'ohos.permission.WRITE_CONTACTS'
+		case 'bluetooth':
+			return 'ohos.permission.ACCESS_BLUETOOTH'
+		default:
+			return null
+	}
+}
+
+function getAbilityContext() : common.UIAbilityContext | null {
+	try {
+		return getContext() as common.UIAbilityContext
+	} catch (ignored) {
+		return null
+	}
+}
+
+function checkHarmonyPermissionStatus(permission : PermissionKey) : PermissionStatus {
+	if (isHarmonyUnsupported(permission)) {
+		return 'unsupported'
+	}
+
+	const harmonyPermission = resolveHarmonyPermission(permission)
+	if (harmonyPermission == null) {
+		return 'unsupported'
+	}
+
+	if (isHarmonyAclUndeclared(permission)) {
+		return 'restricted'
+	}
+
+	const context = getAbilityContext()
+	if (context == null) {
+		return 'denied'
+	}
+
+	const atManager = abilityAccessCtrl.createAtManager()
+	const tokenId = context.applicationInfo.accessTokenId
+	const status = atManager.checkAccessTokenSync(tokenId, harmonyPermission)
+	if (status == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
+		return 'granted'
+	}
+	return 'notDetermined'
+}
+
+function succeedCheckPermission(options : CheckPermissionOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedCheckPermissions(options : CheckPermissionsOptions, res : CheckPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermission(options : RequestPermissionOptions, res : PermissionGrantResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermissions(options : RequestPermissionsOptions, res : RequestPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function failRequestPermission(options : RequestPermissionOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function succeedOpenAppSettings(options : OpenAppSettingsOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult {
+	return createCheckResult(permission, checkHarmonyPermissionStatus(permission))
+}
+
+export const checkPermissionsSync : CheckPermissionsSync = function (permissions : PermissionKey[]) : CheckPermissionsResult {
+	const results : PermissionCheckResult[] = []
+	let allGranted = true
+	for (let i = 0; i < permissions.length; i++) {
+		const item = checkPermissionSync(permissions[i])
+		results.push(item)
+		if (!item.granted) {
+			allGranted = false
+		}
+	}
+	return {
+		results: results,
+		allGranted: allGranted,
+	}
+}
+
+export const checkPermission : CheckPermission = function (options : CheckPermissionOptions) {
+	succeedCheckPermission(options, checkPermissionSync(options.permission))
+}
+
+export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) {
+	succeedCheckPermissions(options, checkPermissionsSync(options.permissions))
+}
+
+export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) {
+	if (isHarmonyUnsupported(options.permission)) {
+		failRequestPermission(options, new PermissionFailImpl(9020003))
+		return
+	}
+
+	if (isHarmonyAclUndeclared(options.permission)) {
+		failRequestPermission(options, new PermissionFailImpl(9020003, createAclUndeclaredMessage(options.permission)))
+		return
+	}
+
+	const currentStatus = checkHarmonyPermissionStatus(options.permission)
+	if (currentStatus == 'granted') {
+		succeedRequestPermission(options, createGrantResult(options.permission, 'granted'))
+		return
+	}
+
+	const harmonyPermission = resolveHarmonyPermission(options.permission)
+	if (harmonyPermission == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020003))
+		return
+	}
+
+	const context = getAbilityContext()
+	if (context == null) {
+		failRequestPermission(options, new PermissionFailImpl(9020006, 'UIAbilityContext 不可用'))
+		return
+	}
+
+	const atManager = abilityAccessCtrl.createAtManager()
+	const permissionList : Array<Permissions> = [harmonyPermission]
+	atManager.requestPermissionsFromUser(context, permissionList,
+		(err : BusinessError, result : AtManagerPermissionResult) => {
+			if (err != null) {
+				succeedRequestPermission(options, createGrantResult(options.permission, 'denied'))
+				return
+			}
+			if (result.authResults.length > 0
+				&& result.authResults[0] == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
+				succeedRequestPermission(options, createGrantResult(options.permission, 'granted'))
+				return
+			}
+			succeedRequestPermission(options, createGrantResult(options.permission, 'denied'))
+		})
+}
+
+export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) {
+	const permissions = options.permissions
+	if (permissions.length == 0) {
+		const emptyResult : RequestPermissionsResult = {
+			results: [],
+			allGranted: true,
+		}
+		succeedRequestPermissions(options, emptyResult)
+		return
+	}
+
+	const context = getAbilityContext()
+	if (context == null) {
+		failRequestPermissions(options, new PermissionFailImpl(9020006, 'UIAbilityContext 不可用'))
+		return
+	}
+
+	const harmonyPermissions : Array<Permissions> = []
+	const permissionKeys : PermissionKey[] = []
+	for (let i = 0; i < permissions.length; i++) {
+		const key = permissions[i]
+		if (isHarmonyUnsupported(key)) {
+			continue
+		}
+		const harmonyPermission = resolveHarmonyPermission(key)
+		if (harmonyPermission != null) {
+			harmonyPermissions.push(harmonyPermission)
+			permissionKeys.push(key)
+		}
+	}
+
+	if (harmonyPermissions.length == 0) {
+		failRequestPermissions(options, new PermissionFailImpl(9020003))
+		return
+	}
+
+	const atManager = abilityAccessCtrl.createAtManager()
+	atManager.requestPermissionsFromUser(context, harmonyPermissions,
+		(err : BusinessError, result : AtManagerPermissionResult) => {
+			const collectedResults : PermissionGrantResult[] = []
+			let allGranted = true
+
+			if (err != null) {
+				for (let i = 0; i < permissionKeys.length; i++) {
+					collectedResults.push(createGrantResult(permissionKeys[i], 'denied'))
+				}
+				allGranted = false
+			} else {
+				for (let i = 0; i < permissionKeys.length; i++) {
+					const granted = i < result.authResults.length
+						&& result.authResults[i] == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
+					const status : PermissionStatus = granted ? 'granted' : 'denied'
+					collectedResults.push(createGrantResult(permissionKeys[i], status))
+					if (!granted) {
+						allGranted = false
+					}
+				}
+			}
+
+			const finalResult : RequestPermissionsResult = {
+				results: collectedResults,
+				allGranted: allGranted,
+			}
+			succeedRequestPermissions(options, finalResult)
+		})
+}
+
+export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) {
+	const context = getAbilityContext()
+	if (context == null) {
+		failOpenAppSettings(options, new PermissionFailImpl(9020004, 'UIAbilityContext 不可用'))
+		return
+	}
+
+	const wantInfo : Want = {
+		bundleName: 'com.huawei.hmos.settings',
+		abilityName: 'com.huawei.hmos.settings.MainAbility',
+		uri: 'application_info_entry',
+		parameters: {
+			pushParams: context.abilityInfo.bundleName,
+		},
+	}
+
+	context.startAbility(wantInfo).then(() => {
+		const resultPermission = options.permission != null ? options.permission! : 'camera'
+		succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted'))
+	}).catch((_err : BusinessError) => {
+		failOpenAppSettings(options, new PermissionFailImpl(9020004))
+	})
+}
+
+export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult {
+	return {
+		permissions: SUPPORTED_PERMISSIONS,
+	}
+}
+
+function mapHarmonyBluetoothState(state : access.BluetoothState) : string {
+	switch (state) {
+		case access.BluetoothState.STATE_OFF:
+			return '已关闭'
+		case access.BluetoothState.STATE_TURNING_ON:
+			return '正在打开'
+		case access.BluetoothState.STATE_ON:
+			return '已开启'
+		case access.BluetoothState.STATE_TURNING_OFF:
+			return '正在关闭'
+		case access.BluetoothState.STATE_BLE_TURNING_ON:
+			return 'BLE 正在打开'
+		case access.BluetoothState.STATE_BLE_ON:
+			return 'BLE 已开启'
+		case access.BluetoothState.STATE_BLE_TURNING_OFF:
+			return 'BLE 正在关闭'
+		default:
+			return '未知'
+	}
+}
+
+function isHarmonyBluetoothEnabled(state : access.BluetoothState) : boolean {
+	return state == access.BluetoothState.STATE_ON
+		|| state == access.BluetoothState.STATE_BLE_ON
+}
+
+export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo {
+	try {
+		const state = access.getState()
+		return {
+			available: true,
+			enabled: isHarmonyBluetoothEnabled(state),
+			stateText: mapHarmonyBluetoothState(state),
+			stateCode: state as number,
+		}
+	} catch (err) {
+		let message = '读取蓝牙状态失败'
+		if (err != null && typeof err === 'object') {
+			const errObj = err as BusinessError
+			if (errObj.message != null && errObj.message.length > 0) {
+				message = errObj.message
+			}
+		}
+		return {
+			available: false,
+			enabled: false,
+			stateText: message,
+			stateCode: -1,
+		}
+	}
+}

+ 78 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/module.full.json5

@@ -0,0 +1,78 @@
+{
+	"module": {
+		"requestPermissions": [
+			{
+				"name": "ohos.permission.CAMERA",
+				"reason": "$string:permission_camera_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.MICROPHONE",
+				"reason": "$string:permission_microphone_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.LOCATION",
+				"reason": "$string:permission_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.APPROXIMATELY_LOCATION",
+				"reason": "$string:permission_coarse_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.ACCESS_BLUETOOTH",
+				"reason": "$string:permission_bluetooth_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.READ_IMAGEVIDEO",
+				"reason": "$string:permission_read_storage_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.WRITE_IMAGEVIDEO",
+				"reason": "$string:permission_write_storage_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.READ_CONTACTS",
+				"reason": "$string:permission_contacts_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.WRITE_CONTACTS",
+				"reason": "$string:permission_write_contacts_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			}
+		]
+	}
+}

+ 46 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/module.json5

@@ -0,0 +1,46 @@
+{
+	"module": {
+		"requestPermissions": [
+			{
+				"name": "ohos.permission.CAMERA",
+				"reason": "$string:permission_camera_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.MICROPHONE",
+				"reason": "$string:permission_microphone_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.LOCATION",
+				"reason": "$string:permission_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.APPROXIMATELY_LOCATION",
+				"reason": "$string:permission_coarse_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.ACCESS_BLUETOOTH",
+				"reason": "$string:permission_bluetooth_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			}
+		]
+	}
+}

+ 46 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/module.normal.json5

@@ -0,0 +1,46 @@
+{
+	"module": {
+		"requestPermissions": [
+			{
+				"name": "ohos.permission.CAMERA",
+				"reason": "$string:permission_camera_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.MICROPHONE",
+				"reason": "$string:permission_microphone_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.LOCATION",
+				"reason": "$string:permission_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.APPROXIMATELY_LOCATION",
+				"reason": "$string:permission_coarse_location_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			},
+			{
+				"name": "ohos.permission.ACCESS_BLUETOOTH",
+				"reason": "$string:permission_bluetooth_reason",
+				"usedScene": {
+					"abilities": ["EntryAbility"],
+					"when": "inuse"
+				}
+			}
+		]
+	}
+}

+ 40 - 0
uni_modules/m-permission-manager/utssdk/app-harmony/resources/base/element/string.json

@@ -0,0 +1,40 @@
+{
+	"string": [
+		{
+			"name": "permission_camera_reason",
+			"value": "需要使用相机进行拍照或扫码"
+		},
+		{
+			"name": "permission_microphone_reason",
+			"value": "需要使用麦克风进行录音或语音输入"
+		},
+		{
+			"name": "permission_location_reason",
+			"value": "需要获取位置信息以提供定位相关服务"
+		},
+		{
+			"name": "permission_coarse_location_reason",
+			"value": "需要获取大致位置信息以提供定位相关服务"
+		},
+		{
+			"name": "permission_bluetooth_reason",
+			"value": "需要使用蓝牙连接周边设备"
+		},
+		{
+			"name": "permission_read_storage_reason",
+			"value": "需要读取相册或媒体文件"
+		},
+		{
+			"name": "permission_write_storage_reason",
+			"value": "需要保存文件到相册或媒体目录"
+		},
+		{
+			"name": "permission_contacts_reason",
+			"value": "需要读取通讯录以提供联系人相关功能"
+		},
+		{
+			"name": "permission_write_contacts_reason",
+			"value": "需要写入通讯录以保存联系人信息"
+		}
+	]
+}

+ 11 - 0
uni_modules/m-permission-manager/utssdk/app-ios/config.json

@@ -0,0 +1,11 @@
+{
+	"deploymentTarget": "14.0",
+	"frameworks": [
+		"AVFoundation.framework",
+		"Photos.framework",
+		"CoreLocation.framework",
+		"Contacts.framework",
+		"CoreBluetooth.framework"
+	],
+	"dependencies": []
+}

+ 535 - 0
uni_modules/m-permission-manager/utssdk/app-ios/index.uts

@@ -0,0 +1,535 @@
+import { AVCaptureDevice, AVMediaType, AVAuthorizationStatus } from 'AVFoundation'
+import { AVAudioSession } from 'AVFoundation'
+import { PHPhotoLibrary, PHAuthorizationStatus } from 'Photos'
+import { CLLocationManager, CLAuthorizationStatus } from 'CoreLocation'
+import { CNContactStore, CNAuthorizationStatus, CNEntityType } from 'Contacts'
+import { CBManager, CBManagerAuthorization, CBCentralManager, CBManagerState } from 'CoreBluetooth'
+import { UIApplication } from 'UIKit'
+import { DispatchQueue } from 'Dispatch'
+import { NSError, URL } from 'Foundation'
+import {
+	BluetoothDemoInfo,
+	CheckPermission,
+	CheckPermissionOptions,
+	CheckPermissions,
+	CheckPermissionsOptions,
+	CheckPermissionsResult,
+	CheckPermissionsSync,
+	CheckPermissionSync,
+	GetSupportedPermissionsResult,
+	GetSupportedPermissionsSync,
+	GetBluetoothDemoInfoSync,
+	OpenAppSettings,
+	OpenAppSettingsOptions,
+	PermissionCheckResult,
+	PermissionKey,
+	PermissionGrantResult,
+	PermissionStatus,
+	RequestPermission,
+	RequestPermissionOptions,
+	RequestPermissions,
+	RequestPermissionsOptions,
+	RequestPermissionsResult,
+} from '../interface.uts'
+import { PermissionFailImpl } from '../unierror'
+
+const SUPPORTED_PERMISSIONS : PermissionKey[] = [
+	'camera',
+	'photoLibrary',
+	'storage',
+	'readStorage',
+	'location',
+	'coarseLocation',
+	'microphone',
+	'contacts',
+	'writeContacts',
+	'bluetooth',
+]
+
+let locationManager : CLLocationManager | null = null
+let pendingLocationOptions : RequestPermissionOptions | null = null
+
+function createCheckResult(permission : PermissionKey, status : PermissionStatus) : PermissionCheckResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+function createRequestResult(permission : PermissionKey, status : PermissionStatus) : PermissionGrantResult {
+	return {
+		permission: permission,
+		status: status,
+		granted: status == 'granted',
+	}
+}
+
+function isIosUnsupported(permission : PermissionKey) : boolean {
+	return permission == 'phone'
+		|| permission == 'phoneState'
+		|| permission == 'sms'
+		|| permission == 'readSms'
+		|| permission == 'receiveSms'
+		|| permission == 'overlay'
+		|| permission == 'writeSettings'
+}
+
+function mapPhotoStatus(status : PHAuthorizationStatus) : PermissionStatus {
+	if (status == PHAuthorizationStatus.authorized || status == PHAuthorizationStatus.limited) {
+		return 'granted'
+	}
+	if (status == PHAuthorizationStatus.notDetermined) {
+		return 'notDetermined'
+	}
+	if (status == PHAuthorizationStatus.restricted) {
+		return 'restricted'
+	}
+	return 'denied'
+}
+
+function mapLocationStatus(status : CLAuthorizationStatus) : PermissionStatus {
+	if (status == CLAuthorizationStatus.authorizedWhenInUse || status == CLAuthorizationStatus.authorizedAlways) {
+		return 'granted'
+	}
+	if (status == CLAuthorizationStatus.notDetermined) {
+		return 'notDetermined'
+	}
+	if (status == CLAuthorizationStatus.restricted) {
+		return 'restricted'
+	}
+	return 'denied'
+}
+
+function mapContactStatus(status : CNAuthorizationStatus) : PermissionStatus {
+	if (status == CNAuthorizationStatus.authorized) {
+		return 'granted'
+	}
+	if (status == CNAuthorizationStatus.notDetermined) {
+		return 'notDetermined'
+	}
+	if (status == CNAuthorizationStatus.restricted) {
+		return 'restricted'
+	}
+	return 'denied'
+}
+
+function mapBluetoothStatus(status : CBManagerAuthorization) : PermissionStatus {
+	if (status == CBManagerAuthorization.allowedAlways) {
+		return 'granted'
+	}
+	if (status == CBManagerAuthorization.notDetermined) {
+		return 'notDetermined'
+	}
+	if (status == CBManagerAuthorization.restricted) {
+		return 'restricted'
+	}
+	return 'denied'
+}
+
+function resolveCameraPermissionStatus() : PermissionStatus {
+	const status = AVCaptureDevice.authorizationStatus(for = AVMediaType.video)
+	if (status == AVAuthorizationStatus.authorized) {
+		return 'granted'
+	}
+	if (status == AVAuthorizationStatus.notDetermined) {
+		return 'notDetermined'
+	}
+	if (status == AVAuthorizationStatus.restricted) {
+		return 'restricted'
+	}
+	return 'denied'
+}
+
+function resolveMicrophonePermissionStatus() : PermissionStatus {
+	const recordPermission = AVAudioSession.sharedInstance().recordPermission
+	if (recordPermission == AVAudioSession.RecordPermission.granted) {
+		return 'granted'
+	}
+	if (recordPermission == AVAudioSession.RecordPermission.undetermined) {
+		return 'notDetermined'
+	}
+	return 'denied'
+}
+
+function resolvePermissionStatus(permission : PermissionKey) : PermissionStatus {
+	if (isIosUnsupported(permission)) {
+		return 'unsupported'
+	}
+	if (permission == 'camera') {
+		return resolveCameraPermissionStatus()
+	}
+	if (permission == 'photoLibrary' || permission == 'storage' || permission == 'readStorage') {
+		return mapPhotoStatus(PHPhotoLibrary.authorizationStatus())
+	}
+	if (permission == 'location' || permission == 'coarseLocation') {
+		return mapLocationStatus(CLLocationManager.authorizationStatus())
+	}
+	if (permission == 'microphone') {
+		return resolveMicrophonePermissionStatus()
+	}
+	if (permission == 'contacts' || permission == 'writeContacts') {
+		return mapContactStatus(CNContactStore.authorizationStatus(for = CNEntityType.contacts))
+	}
+	if (permission == 'bluetooth') {
+		return mapBluetoothStatus(CBManager.authorization)
+	}
+	return 'unsupported'
+}
+
+function succeedCheckPermission(options : CheckPermissionOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedCheckPermissions(options : CheckPermissionsOptions, res : CheckPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermission(options : RequestPermissionOptions, res : PermissionGrantResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedRequestPermissions(options : RequestPermissionsOptions, res : RequestPermissionsResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function succeedOpenAppSettings(options : OpenAppSettingsOptions, res : PermissionCheckResult) {
+	const successCallback = options.success
+	if (successCallback != null) {
+		successCallback(res)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(res)
+	}
+}
+
+function failRequestPermission(options : RequestPermissionOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFailImpl) {
+	const failCallback = options.fail
+	if (failCallback != null) {
+		failCallback(err)
+	}
+	const completeCallback = options.complete
+	if (completeCallback != null) {
+		completeCallback(err)
+	}
+}
+
+function requestCameraPermission(options : RequestPermissionOptions) {
+	AVCaptureDevice.requestAccess(for = AVMediaType.video, completionHandler = (granted : boolean) => {
+		DispatchQueue.main.async(execute = (): void => {
+			const status : PermissionStatus = granted ? 'granted' : 'denied'
+			succeedRequestPermission(options, createRequestResult(options.permission, status))
+		})
+	})
+}
+
+function requestPhotoPermission(options : RequestPermissionOptions) {
+	PHPhotoLibrary.requestAuthorization((status : PHAuthorizationStatus) => {
+		DispatchQueue.main.async(execute = (): void => {
+			succeedRequestPermission(options, createRequestResult(options.permission, mapPhotoStatus(status)))
+		})
+	})
+}
+
+function requestMicrophonePermission(options : RequestPermissionOptions) {
+	AVAudioSession.sharedInstance().requestRecordPermission((granted : boolean) => {
+		DispatchQueue.main.async(execute = (): void => {
+			const status : PermissionStatus = granted ? 'granted' : 'denied'
+			succeedRequestPermission(options, createRequestResult(options.permission, status))
+		})
+	})
+}
+
+function requestContactsPermission(options : RequestPermissionOptions) {
+	const store = new CNContactStore()
+	store.requestAccess(for = CNEntityType.contacts, completionHandler = (granted : boolean, _ : NSError | null) => {
+		DispatchQueue.main.async(execute = (): void => {
+			const status : PermissionStatus = granted ? 'granted' : 'denied'
+			succeedRequestPermission(options, createRequestResult(options.permission, status))
+		})
+	})
+}
+
+function ensureLocationManager() : CLLocationManager {
+	if (locationManager == null) {
+		locationManager = new CLLocationManager()
+	}
+	return locationManager!
+}
+
+function requestLocationPermission(options : RequestPermissionOptions) {
+	const currentStatus = CLLocationManager.authorizationStatus()
+	if (currentStatus != CLAuthorizationStatus.notDetermined) {
+		succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(currentStatus)))
+		return
+	}
+	pendingLocationOptions = options
+	const manager = ensureLocationManager()
+	manager.requestWhenInUseAuthorization()
+	scheduleLocationPermissionCheck(0)
+}
+
+function scheduleLocationPermissionCheck(attempt : Int) {
+	const maxAttempts : Int = 60
+	DispatchQueue.main.asyncAfter(deadline = DispatchTime.now() + 0.3, execute = (): void => {
+		const options = pendingLocationOptions
+		if (options == null) {
+			return
+		}
+		const authStatus = CLLocationManager.authorizationStatus()
+		if (authStatus == CLAuthorizationStatus.notDetermined) {
+			if (attempt >= maxAttempts) {
+				pendingLocationOptions = null
+				succeedRequestPermission(options, createRequestResult(options.permission, 'denied'))
+				return
+			}
+			scheduleLocationPermissionCheck(attempt + 1)
+			return
+		}
+		pendingLocationOptions = null
+		succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(authStatus)))
+	})
+}
+
+function requestBluetoothPermission(options : RequestPermissionOptions) {
+	const currentStatus = CBManager.authorization
+	if (currentStatus != CBManagerAuthorization.notDetermined) {
+		succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(currentStatus)))
+		return
+	}
+	// 创建 CBCentralManager 实例会触发系统蓝牙权限弹窗
+	const _centralManager = new CBCentralManager()
+	DispatchQueue.main.async(execute = (): void => {
+		succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(CBManager.authorization)))
+	})
+}
+
+export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult {
+	return createCheckResult(permission, resolvePermissionStatus(permission))
+}
+
+export const checkPermissionsSync : CheckPermissionsSync = function (permissions : PermissionKey[]) : CheckPermissionsResult {
+	const results : PermissionCheckResult[] = []
+	let allGranted = true
+	for (let i = 0; i < permissions.length; i++) {
+		const item = checkPermissionSync(permissions[i])
+		results.push(item)
+		if (!item.granted) {
+			allGranted = false
+		}
+	}
+	return {
+		results: results,
+		allGranted: allGranted,
+	}
+}
+
+export const checkPermission : CheckPermission = function (options : CheckPermissionOptions) {
+	succeedCheckPermission(options, checkPermissionSync(options.permission))
+}
+
+export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) {
+	succeedCheckPermissions(options, checkPermissionsSync(options.permissions))
+}
+
+export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) {
+	if (isIosUnsupported(options.permission)) {
+		failRequestPermission(options, new PermissionFailImpl(9020003))
+		return
+	}
+
+	const currentStatus = resolvePermissionStatus(options.permission)
+	if (currentStatus == 'granted') {
+		succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
+		return
+	}
+	if (currentStatus == 'restricted') {
+		succeedRequestPermission(options, createRequestResult(options.permission, 'restricted'))
+		return
+	}
+
+	switch (options.permission) {
+		case 'camera':
+			requestCameraPermission(options)
+			break
+		case 'photoLibrary':
+		case 'storage':
+		case 'readStorage':
+			requestPhotoPermission(options)
+			break
+		case 'location':
+		case 'coarseLocation':
+			requestLocationPermission(options)
+			break
+		case 'microphone':
+			requestMicrophonePermission(options)
+			break
+		case 'contacts':
+		case 'writeContacts':
+			requestContactsPermission(options)
+			break
+		case 'bluetooth':
+			requestBluetoothPermission(options)
+			break
+		default:
+			failRequestPermission(options, new PermissionFailImpl(9020003))
+	}
+}
+
+export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) {
+	const permissions = options.permissions
+	if (permissions == null) {
+		const emptyResult : RequestPermissionsResult = {
+			results: [],
+			allGranted: true,
+		}
+		succeedRequestPermissions(options, emptyResult)
+		return
+	}
+	const permissionList = permissions!
+	if (permissionList.length == 0) {
+		const emptyResult : RequestPermissionsResult = {
+			results: [],
+			allGranted: true,
+		}
+		succeedRequestPermissions(options, emptyResult)
+		return
+	}
+
+	let currentIndex = 0
+	const collectedResults : PermissionGrantResult[] = []
+
+	function requestNext() {
+		if (currentIndex >= permissionList.length) {
+			let allGranted = true
+			for (let i = 0; i < collectedResults.length; i++) {
+				if (!collectedResults[i].granted) {
+					allGranted = false
+					break
+				}
+			}
+			const finalResult : RequestPermissionsResult = {
+				results: collectedResults,
+				allGranted: allGranted,
+			}
+			succeedRequestPermissions(options, finalResult)
+			return
+		}
+
+		const permission = permissionList[currentIndex]
+		currentIndex += 1
+		requestPermission({
+			permission: permission,
+			success: (res : PermissionGrantResult) => {
+				collectedResults.push(res)
+				requestNext()
+			},
+			fail: (err : PermissionFailImpl) => {
+				collectedResults.push(createRequestResult(permission, 'denied'))
+				failRequestPermissions(options, err)
+			},
+		})
+	}
+
+	requestNext()
+}
+
+export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) {
+	const settingsUrl = URL(string = UIApplication.openSettingsURLString)
+	if (settingsUrl == null) {
+		failOpenAppSettings(options, new PermissionFailImpl(9020004))
+		return
+	}
+	DispatchQueue.main.async(execute = (): void => {
+		UIApplication.shared.open(settingsUrl!)
+		const resultPermission = options.permission != null ? options.permission! : 'camera'
+		succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted'))
+	})
+}
+
+export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult {
+	return {
+		permissions: SUPPORTED_PERMISSIONS,
+	}
+}
+
+function mapIOSBluetoothState(state : CBManagerState) : string {
+	switch (state) {
+		case CBManagerState.poweredOn:
+			return '已开启'
+		case CBManagerState.poweredOff:
+			return '已关闭'
+		case CBManagerState.unsupported:
+			return '不支持'
+		case CBManagerState.unauthorized:
+			return '未授权'
+		case CBManagerState.resetting:
+			return '重置中'
+		case CBManagerState.unknown:
+			return '未知'
+		default:
+			return '未知'
+	}
+}
+
+export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo {
+	const manager = new CBCentralManager()
+	const state = manager.state
+	return {
+		available: state != CBManagerState.unsupported,
+		enabled: state == CBManagerState.poweredOn,
+		stateText: mapIOSBluetoothState(state),
+		stateCode: state as number,
+	}
+}

+ 125 - 0
uni_modules/m-permission-manager/utssdk/interface.uts

@@ -0,0 +1,125 @@
+/**
+ * m-permission-manager 跨平台权限管理接口
+ */
+
+/** 统一权限标识,三端通用 */
+export type PermissionKey =
+	| 'camera'
+	| 'photoLibrary'
+	| 'storage'
+	| 'readStorage'
+	| 'location'
+	| 'coarseLocation'
+	| 'microphone'
+	| 'contacts'
+	| 'writeContacts'
+	| 'bluetooth'
+	| 'phone'
+	| 'phoneState'
+	| 'sms'
+	| 'readSms'
+	| 'receiveSms'
+	| 'overlay'
+	| 'writeSettings'
+
+export type PermissionStatus =
+	| 'granted'
+	| 'denied'
+	| 'notDetermined'
+	| 'restricted'
+	| 'unsupported'
+
+export type PermissionCheckResult = {
+	permission : PermissionKey
+	status : PermissionStatus
+	granted : boolean
+}
+
+export type PermissionGrantResult = {
+	permission : PermissionKey
+	status : PermissionStatus
+	granted : boolean
+}
+
+export type CheckPermissionsResult = {
+	results : PermissionCheckResult[]
+	allGranted : boolean
+}
+
+export type RequestPermissionsResult = {
+	results : PermissionGrantResult[]
+	allGranted : boolean
+}
+
+export type CheckPermissionOptions = {
+	permission : PermissionKey
+	success ?: (res : PermissionCheckResult) => void
+	fail ?: (res : PermissionFail) => void
+	complete ?: (res : any) => void
+}
+
+export type CheckPermissionsOptions = {
+	permissions : PermissionKey[]
+	success ?: (res : CheckPermissionsResult) => void
+	fail ?: (res : PermissionFail) => void
+	complete ?: (res : any) => void
+}
+
+export type RequestPermissionOptions = {
+	permission : PermissionKey
+	success ?: (res : PermissionGrantResult) => void
+	fail ?: (res : PermissionFail) => void
+	complete ?: (res : any) => void
+}
+
+export type RequestPermissionsOptions = {
+	permissions : PermissionKey[]
+	success ?: (res : RequestPermissionsResult) => void
+	fail ?: (res : PermissionFail) => void
+	complete ?: (res : any) => void
+}
+
+export type OpenAppSettingsOptions = {
+	/** 指定权限类型,各端将尽量跳转到对应设置页 */
+	permission ?: PermissionKey
+	success ?: (res : PermissionCheckResult) => void
+	fail ?: (res : PermissionFail) => void
+	complete ?: (res : any) => void
+}
+
+export type GetSupportedPermissionsResult = {
+	permissions : PermissionKey[]
+}
+
+/**
+ * 9020001 当前环境不支持
+ * 9020002 用户拒绝授权
+ * 9020003 权限不支持当前平台
+ * 9020004 打开系统设置失败
+ * 9020005 权限检查失败
+ * 9020006 权限申请失败
+ */
+export type PermissionErrorCode = 9020001 | 9020002 | 9020003 | 9020004 | 9020005 | 9020006
+
+export interface PermissionFail extends IUniError {
+	errCode : PermissionErrorCode
+}
+
+export type CheckPermissionSync = (permission : PermissionKey) => PermissionCheckResult
+export type CheckPermissionsSync = (permissions : PermissionKey[]) => CheckPermissionsResult
+export type CheckPermission = (options : CheckPermissionOptions) => void
+export type CheckPermissions = (options : CheckPermissionsOptions) => void
+export type RequestPermission = (options : RequestPermissionOptions) => void
+export type RequestPermissions = (options : RequestPermissionsOptions) => void
+export type OpenAppSettings = (options : OpenAppSettingsOptions) => void
+export type GetSupportedPermissionsSync = () => GetSupportedPermissionsResult
+
+/** 蓝牙演示信息(读取系统蓝牙开关状态) */
+export type BluetoothDemoInfo = {
+	available : boolean
+	enabled : boolean
+	stateText : string
+	stateCode : number
+}
+
+export type GetBluetoothDemoInfoSync = () => BluetoothDemoInfo

+ 23 - 0
uni_modules/m-permission-manager/utssdk/unierror.uts

@@ -0,0 +1,23 @@
+import { PermissionErrorCode, PermissionFail } from './interface.uts'
+
+export const UniErrorSubject = 'm-permission-manager'
+
+export const PermissionUniErrors : Map<PermissionErrorCode, string> = new Map([
+	[9020001, '当前环境不支持权限管理'],
+	[9020002, '用户拒绝授权'],
+	[9020003, '权限不支持当前平台'],
+	[9020004, '打开系统设置失败'],
+	[9020005, '权限检查失败'],
+	[9020006, '权限申请失败'],
+])
+
+export class PermissionFailImpl extends UniError implements PermissionFail {
+	override errCode : PermissionErrorCode
+
+	constructor(errCode : PermissionErrorCode, errMsg : string = '') {
+		super()
+		this.errSubject = UniErrorSubject
+		this.errCode = errCode
+		this.errMsg = errMsg.length > 0 ? errMsg : (PermissionUniErrors.get(errCode) ?? '')
+	}
+}

+ 220 - 0
uni_modules/m-permission-manager/使用规范.md

@@ -0,0 +1,220 @@
+# m-permission-manager 使用规范
+
+本文档面向集成本插件的 App 开发者,说明 **必须遵守的约束**、**推荐做法** 和 **上架审核注意事项**。
+
+---
+
+## 1. 基本约束(必须遵守)
+
+### 1.1 用户主动触发
+
+`requestPermission` / `requestPermissions` **必须由真实用户手势触发**,例如按钮 `click` / `tap`。
+
+```vue
+<!-- ✅ 正确 -->
+<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 |
+
+详细配置见 [权限配置说明.md](./权限配置说明.md)。
+
+### 1.3 按需申请
+
+只申请业务实际使用的权限。在功能入口处按需 `requestPermission`,不要一次性申请全部权限。
+
+```js
+// ✅ 审核页
+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,避免向用户展示无效入口。
+
+```js
+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`:
+
+```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 风格)
+
+```js
+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 永久拒绝后的引导
+
+用户选择「不再询问」或多次拒绝后,应引导至系统设置:
+
+```js
+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. 自定义基座与发版
+
+1. 导入或更新插件后,**必须重新制作自定义基座**或云打包
+2. 修改 `manifest.json` 权限或鸿蒙 `module.json5` 后,需重新打包
+3. iOS 修改 `privacyDescription` 后需重新打包,必要时删除旧 App 重装以重新触发授权弹窗
+
+---
+
+## 7. 参考文档
+
+- [readme.md](./readme.md) — API 与快速开始
+- [权限配置说明.md](./权限配置说明.md) — 三端权限文件配置详解
+- [发布指南.md](./发布指南.md) — 插件市场发布填写参考

+ 227 - 0
uni_modules/m-permission-manager/发布指南.md

@@ -0,0 +1,227 @@
+# m-permission-manager 插件发布指南
+
+面向插件作者,说明如何将本插件提交到 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 代码后已重新制作基座
+
+---
+
+## 提交到插件市场
+
+1. 登录 [DCloud 插件市场](https://ext.dcloud.net.cn/)
+2. 进入 **我的插件** → **发布插件**
+3. 插件类型选择 **UTS 插件**
+4. 上传 `uni_modules/m-permission-manager` 目录(或打包 zip)
+5. 按下方表格填写市场展示信息
+
+---
+
+## 市场后台填写参考
+
+### 基本信息
+
+| 字段 | 填写内容 |
+| --- | --- |
+| **插件名称** | 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
+
+详细权限配置见插件内《权限配置说明.md》,集成规范见《使用规范.md》。
+```
+
+### 标签 / 关键词
+
+```
+权限, permission, 运行时权限, 蓝牙, 相机, 定位, Android, iOS, Harmony, 鸿蒙, UTS
+```
+
+### 使用说明(市场「使用说明」字段)
+
+可直接粘贴以下内容,或填写插件 readme 链接:
+
+```
+【快速开始】
+
+1. 从插件市场导入 m-permission-manager 到项目 uni_modules 目录
+
+2. 在 manifest.json 中声明宿主所需权限(Android permissions / iOS privacyDescription)
+   鸿蒙端见插件 utssdk/app-harmony/module.json5,详见《权限配置说明.md》
+
+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.md — API 与快速开始
+· 使用规范.md — 集成约束与推荐流程
+· 权限配置说明.md — 三端权限文件配置详解
+```
+
+### 权限与隐私声明(`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 张)
+
+1. 权限演示页截图(三端权限列表与状态)
+2. Promise 封装调用代码示例
+3. Android 系统权限弹窗(相机 / 蓝牙)
+4. iOS 隐私授权弹窗
+5. manifest.json 权限配置示例
+6. 三端权限支持对照表
+
+---
+
+## 市场 FAQ 预填
+
+**Q:为什么必须自定义基座?**  
+A:本插件为 UTS 原生插件,标准基座不包含。需制作含本插件的自定义基座或云打包。
+
+**Q:H5 能用吗?**  
+A:不支持。仅 App 端(Android / iOS / Harmony)。
+
+**Q:安装插件后还需要配置权限吗?**  
+A:需要。Android / iOS 必须在宿主 `manifest.json` 中声明静态权限与隐私说明;鸿蒙由插件 `module.json5` 合并,但 ACL 权限需额外签名。详见《权限配置说明.md》。
+
+**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()` 展示引导文案。
+
+---
+
+## 版本号规范
+
+遵循 [语义化版本](https://semver.org/lang/zh-CN/):
+
+| 变更类型 | 版本 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,或在插件市场后台配置联系方式。
+
+---
+
+## 参考链接
+
+- [UTS 插件开发文档](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
+- [插件市场发布流程](https://uniapp.dcloud.net.cn/plugin/publish.html)
+- 插件内 [readme.md](./readme.md)
+- 插件内 [使用规范.md](./使用规范.md)
+- 插件内 [权限配置说明.md](./权限配置说明.md)

+ 285 - 0
uni_modules/m-permission-manager/权限配置说明.md

@@ -0,0 +1,285 @@
+# m-permission-manager 权限配置说明
+
+本文档说明集成本插件时,**各端需要修改哪些文件**、**如何配置权限**,以及插件内置配置文件的作用。
+
+---
+
+## 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`
+
+```json
+{
+  "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`
+
+```json
+{
+  "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 设置页并配合引导文案。
+
+---
+
+## 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 中配置权限**(可选,非必须):
+
+1. 先运行一次鸿蒙编译,从 `unpackage/dist/dev/app-harmony/entry/src/main/module.json5` 复制完整文件到 `harmony-configs/entry/src/main/module.json5`
+2. **仅修改** `module.requestPermissions` 节点,其他节点不要删(uni-app 是**文件级覆盖**,不是 JSON 节点合并)
+3. 不要与插件已声明的权限重复
+
+> 集成 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 版本
+
+1. 将 `module.full.json5` 的内容**覆盖** `module.json5`
+2. 在 AppGallery Connect 配置 ACL 白名单
+3. 使用正式 Profile 重新签名打包
+4. 重新制作自定义基座或云打包
+
+### 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:
+
+```json
+"permissions": [
+  "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+  "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
+]
+```
+
+**iOS** privacyDescription:
+
+```json
+"privacyDescription": {
+  "NSCameraUsageDescription": "需要使用相机",
+  "NSMicrophoneUsageDescription": "需要使用麦克风"
+}
+```
+
+**Harmony**:默认 `module.json5` 已包含,无需修改。
+
+### 5.2 需要蓝牙
+
+**Android** manifest 增加:
+
+```json
+"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
+"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>"
+```
+
+**iOS** privacyDescription 增加:
+
+```json
+"NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接周边设备"
+```
+
+**Harmony**:默认 `module.json5` 已包含 `ACCESS_BLUETOOTH`。
+
+### 5.3 需要相册读写(Harmony 完整权限)
+
+1. 用 `module.full.json5` 覆盖 `module.json5`
+2. 配置 ACL 签名
+3. Android 增加 `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`
+4. iOS 增加 `NSPhotoLibraryUsageDescription`
+
+> 若仅需选图不上传,推荐使用 `uni.chooseImage`,三端均无需媒体读写权限。
+
+---
+
+## 6. 配置后必做
+
+1. **重新制作自定义基座**或云打包(UTS 插件 + manifest 变更均须重新打包)
+2. iOS 修改 `privacyDescription` 后建议删除旧 App 重装,以重新触发授权弹窗
+3. Harmony 切换 `module.json5` 后需重新签名安装
+
+---
+
+## 7. 相关文档
+
+- [readme.md](./readme.md) — API 与权限 Key 对照表
+- [使用规范.md](./使用规范.md) — 集成流程与审核注意事项
+- [发布指南.md](./发布指南.md) — 插件市场发布填写参考