13404089107
2026-04-29 c31a16f4dc175ef1a58d56ad0da0913d9da31349
feat: 添加iOS隐私权限说明及统一权限确认逻辑

- 在manifest.json中添加iOS隐私权限使用描述,以满足App Store审核要求
- 创建统一的权限确认工具函数,在涉及相机、相册操作前向用户说明权限用途
- 在扫码邀请码、上传头像、营业执照、客服发送图片、保存二维码等场景添加权限确认
- 修复极光推送通知处理中的硬编码值问题
1个文件已添加
8个文件已修改
133 ■■■■■ 已修改文件
traditionalApp/App.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/clinicInfo/clinicInfo.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/courseDetail/courseDetail.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/customerService/customerService.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/inviteUser/inviteUser.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/personalInfo/personalInfo.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/register/register.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/permission.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue
@@ -21,19 +21,18 @@
        jpushModule.initJPushService();
        // 监听是否连接成功
        jpushModule.addConnectEventListener(result => {
            // let connectEnable = result.connectEnable;
            // console.log(connectEnable, '极光连接成功');
            console.log('qweqweqwe', result);
            let connectEnable = result.connectEnable;
            console.log(connectEnable, '极光连接成功');
            if (uni.getStorageSync('token')) {
            }
        });
        jpushModule.addNotificationListener(result => {
            plus.runtime.setBadgeNumber(res.data);
            jpushModule.setBadge(res.data)
            // getUnreadCount().then((res) => {
            plus.runtime.setBadgeNumber('111111');
            jpushModule.setBadge('2222222222222')
            // });
        });
        // #endif
traditionalApp/manifest.json
@@ -53,6 +53,11 @@
            /* ios打包配置 */
            "ios": {
                "dSYMs": false,
                "privacyDescription": {
                    "NSCameraUsageDescription": "需要使用相机拍摄照片 / 视频,用于扫码自动填充邀请码、拍摄照片作为头像",
                    "NSPhotoLibraryUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码",
                    "NSPhotoLibraryAddUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码"
                },
                "capabilities": {
                    "entitlements": {
                        "com.apple.developer.associated-domains": [
@@ -334,4 +339,4 @@
            "modules": {}
        }
    }
}
}
traditionalApp/pages/clinicInfo/clinicInfo.vue
@@ -81,6 +81,7 @@
    updateClinicInfo,
    uploadClinicLicense,
} from './service.js'
import { confirmMediaForLicense } from '@/utils/permission.js'
interface RegionItem {
    label?: string
@@ -277,12 +278,17 @@
/**
 * 选择营业执照图片,仅缓存本地路径等待提交前上传。
 */
function uploadLicense() {
async function uploadLicense() {
    if (uploadingLicense.value) {
        return
    }
    const confirmed = await confirmMediaForLicense()
    if (!confirmed) {
        return
    }
    uni.chooseImage({
        count: 1,
        sourceType: ['album', 'camera'],
        success: (res) => {
            const filePath = res.tempFilePaths && res.tempFilePaths.length > 0 ? res.tempFilePaths[0] : ''
            if (!filePath) {
traditionalApp/pages/courseDetail/courseDetail.vue
@@ -205,7 +205,7 @@
                    <view v-if="!loading && allVideos.length === 0" class="empty-text">暂无课程章节</view>
                </view>
            </template>
            <view v-if="loading" class="loading-text">加载中...</view>
            <view v-if="isReadonlyDisplayMode" class="open-app-banner" @tap="openApp">
                <view class="app-logo-placeholder"></view>
traditionalApp/pages/customerService/customerService.vue
@@ -79,6 +79,7 @@
<script setup lang="ts">
    import { ref, onMounted, nextTick } from 'vue';
    import { confirmMediaForCustomerService } from '@/utils/permission.js';
    const statusBarHeight = ref(20);
    const inputText = ref('');
@@ -165,7 +166,11 @@
        }, 1000);
    };
    const chooseImage = () => {
    const chooseImage = async () => {
        const confirmed = await confirmMediaForCustomerService();
        if (!confirmed) {
            return;
        }
        uni.chooseImage({
            count: 1,
            sizeType: ['compressed'],
traditionalApp/pages/inviteUser/inviteUser.vue
@@ -72,6 +72,7 @@
    import { onLoad, onShow } from '@dcloudio/uni-app'
    import { getMyShareQrcode } from './service'
    import { normalizeOptionalText, saveImage } from '@/utils/utils.js'
    import { confirmAlbumForInviteQrCode } from '@/utils/permission.js'
    interface ShareBoundUserItem {
        id: number | null
@@ -151,7 +152,7 @@
    /**
     * 保存当前二维码图片到相册。
     */
    function saveQRCode() {
    async function saveQRCode() {
        const saveTarget = qrcodeData.value.qrCodeImageUrl || qrcodeData.value.qrCodeDataUrl
        if (!saveTarget) {
            uni.showToast({
@@ -160,6 +161,10 @@
            })
            return
        }
        const confirmed = await confirmAlbumForInviteQrCode()
        if (!confirmed) {
            return
        }
        saveImage(saveTarget)
    }
traditionalApp/pages/personalInfo/personalInfo.vue
@@ -48,6 +48,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { getSettingsProfile, updateSettingsProfile, uploadSettingsAvatar } from '../settings/service.js'
import { cacheSettingsProfile, isSuccessResponse, maskPhone, normalizeSettingsProfile } from '../settings/utils.js'
import { confirmMediaForAvatar } from '@/utils/permission.js'
const statusBarHeight = ref(20)
const saving = ref(false)
@@ -98,10 +99,14 @@
/**
 * 选择头像并上传到后端,成功后只更新页面临时状态。
 */
function chooseAvatar() {
async function chooseAvatar() {
    if (uploadingAvatar.value) {
        return
    }
    const confirmed = await confirmMediaForAvatar()
    if (!confirmed) {
        return
    }
    uni.chooseImage({
        count: 1,
        sizeType: ['compressed'],
traditionalApp/pages/register/register.vue
@@ -176,6 +176,7 @@
    validateInviteBindTicket
} from './service.js'
import { extractBindTicket, extractInviteCode, normalizeOptionalText } from '@/utils/utils.js'
import { confirmCameraForInviteCodeScan, confirmMediaForLicense } from '@/utils/permission.js'
interface RegionItem {
    label?: string
@@ -345,7 +346,11 @@
/**
 * 扫码填充邀请码,支持从邀请下载页 URL 中提取推广码。
 */
const scanCode = () => {
const scanCode = async () => {
    const confirmed = await confirmCameraForInviteCodeScan()
    if (!confirmed) {
        return
    }
    uni.scanCode({
        success: (res) => {
            const inviteCode = normalizeInviteCode(res.result)
@@ -404,12 +409,17 @@
/**
 * 选择营业执照图片,仅缓存本地路径等待提交前上传。
 */
const uploadLicense = () => {
const uploadLicense = async () => {
    if (uploadingLicense.value) {
        return
    }
    const confirmed = await confirmMediaForLicense()
    if (!confirmed) {
        return
    }
    uni.chooseImage({
        count: 1,
        sourceType: ['album', 'camera'],
        success: (res) => {
            const filePath = res.tempFilePaths && res.tempFilePaths.length > 0 ? res.tempFilePaths[0] : ''
            if (!filePath) {
traditionalApp/utils/permission.js
New file
@@ -0,0 +1,70 @@
const showPermissionPurposeModal = ({ title = '权限使用说明', content = '' }) => {
    return new Promise((resolve) => {
        uni.showModal({
            title,
            content,
            confirmText: '继续',
            cancelText: '取消',
            success: (res) => {
                resolve(!!res.confirm)
            },
            fail: () => {
                resolve(false)
            }
        })
    })
}
/**
 * 扫码前说明相机权限用途。
 */
export const confirmCameraForInviteCodeScan = () => {
    return showPermissionPurposeModal({
        content: '相机:需要使用相机拍摄照片 / 视频,用于扫码自动填充邀请码。'
    })
}
/**
 * 选择或拍摄头像前说明相机、相册权限用途。
 */
export const confirmMediaForAvatar = () => {
    return showPermissionPurposeModal({
        content: [
            '相机:需要使用相机拍摄照片 / 视频,用于拍摄照片作为头像。',
            '相册:需要访问手机相册,用于选择图片 / 视频上传、更换个人头像。'
        ].join('\n')
    })
}
/**
 * 选择或拍摄营业执照前说明相机、相册权限用途。
 */
export const confirmMediaForLicense = () => {
    return showPermissionPurposeModal({
        content: [
            '相机:需要使用相机拍摄照片 / 视频,用于拍摄营业执照照片并上传。',
            '相册:需要访问手机相册,用于选择营业执照图片 / 视频上传。'
        ].join('\n')
    })
}
/**
 * 客服发送图片前说明相机、相册权限用途。
 */
export const confirmMediaForCustomerService = () => {
    return showPermissionPurposeModal({
        content: [
            '相机:需要使用相机拍摄照片 / 视频,用于拍摄图片发送给客服。',
            '相册:需要访问手机相册,用于选择图片 / 视频发送给客服。'
        ].join('\n')
    })
}
/**
 * 保存二维码前说明相册权限用途。
 */
export const confirmAlbumForInviteQrCode = () => {
    return showPermissionPurposeModal({
        content: '相册:需要访问手机相册,用于保存邀请二维码到本地相册。'
    })
}