From 6ffbae0f33428cfcc1769f163e978b63466ccaca Mon Sep 17 00:00:00 2001 From: jing <1683105490@qq.com> Date: Sat, 29 Aug 2026 17:44:26 +0800 Subject: [PATCH] =?UTF-8?q?chore=EF=BC=9A=E8=A1=A8=E5=8D=95=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=AD=A3=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ns-login/ns-login.vue | 81 ++++++++++++++++++++-- components/privacy-popup/privacy-popup.vue | 22 +++--- lang-bundle.json | 1 + lang/en-us/components/ns-login.js | 1 + lang/zh-cn/components/ns-login.js | 1 + pages_tool/form/form.vue | 3 + pages_tool/form/formdata.vue | 17 ++++- 7 files changed, 109 insertions(+), 17 deletions(-) diff --git a/components/ns-login/ns-login.vue b/components/ns-login/ns-login.vue index ab36c2e..9f8f79d 100644 --- a/components/ns-login/ns-login.vue +++ b/components/ns-login/ns-login.vue @@ -325,14 +325,69 @@ export default { // #ifdef MP-WEIXIN - this.getCode(authData => { - this.authLogin(authData, 'authOnlyLogin'); - }, (err) => { - callback && callback(err); + // 真机 __usePrivacyCheck__ 开启时 getPhoneNumber 属于微信隐私接口, + // 必须先完成平台级隐私协议授权,否则点击"授权手机号"按钮会直接报 + // "privacy permission is not authorized or buttonId is invalid"。 + // 这里在登录链路前主动检查并弹隐私弹窗,授权完成后再继续登录。 + this.ensureWxPrivacyAuthorized(() => { + this.getCode(authData => { + this.authLogin(authData, 'authOnlyLogin'); + }, (err) => { + callback && callback(err); + }); }); // #endif }, + // 微信小程序: 确保平台级隐私协议已授权后再执行回调 + ensureWxPrivacyAuthorized(done) { + try { + if (typeof wx === 'undefined' || !wx.getPrivacySetting) { + // 低版本基础库无隐私 API, 直接放行 + done(); + return; + } + wx.getPrivacySetting({ + success: (res) => { + if (!res || !res.needAuthorization) { + // 已授权或无隐私指引配置, 直接放行 + done(); + return; + } + // 需要授权: 优先弹自建隐私弹窗(内含 agreePrivacyAuthorization 按钮, + // 点击同意即完成平台级授权); 用户同意后通过 privacy:agreed 事件继续 + const popup = this.$refs.privacyPopup; + if (popup && typeof popup.showPrivacyPopup === 'function') { + const onAgreed = () => { + uni.$off('privacy:agreed', onAgreed); + uni.$off('privacy:disagreed', onDisagreed); + done(); + }; + const onDisagreed = () => { + uni.$off('privacy:agreed', onAgreed); + uni.$off('privacy:disagreed', onDisagreed); + // 用户拒绝隐私协议: 不继续登录链路(隐私接口也不可用) + }; + uni.$off('privacy:agreed', onAgreed); + uni.$off('privacy:disagreed', onDisagreed); + uni.$on('privacy:agreed', onAgreed); + uni.$on('privacy:disagreed', onDisagreed); + popup.showPrivacyPopup(); + } else if (typeof wx.requirePrivacyAuthorize === 'function') { + // 无 privacy-popup 实例时退回微信官方隐私弹窗 + wx.requirePrivacyAuthorize({ + success: () => done(), + fail: () => { } + }); + } + }, + fail: () => done() + }); + } catch (e) { + done(); + } + }, + // 跳转去登录页 toLoginH5Page(mode = 'navigateTo') { if (this.url) this.$util.redirectTo(this.$util.LOGIN_PAGE_URL, { @@ -614,8 +669,24 @@ export default { } }); } else { + const errMsg = (e.detail && e.detail.errMsg) || ''; + // #ifdef MP-WEIXIN + // 隐私协议未授权时 getPhoneNumber 会直接失败: + // "getPhoneNumber:fail privacy permission is not authorized or buttonId is invalid" + // 此时不再抛原始错误, 引导用户同意隐私弹窗后重新点授权按钮 + if (errMsg.indexOf('privacy') !== -1) { + this.$util.showToast({ + title: this.$lang('components.ns-login.pleaseAgreePrivacy') + }); + const popup = this.$refs.privacyPopup; + if (popup && typeof popup.showPrivacyPopup === 'function') { + popup.showPrivacyPopup(); + } + return; + } + // #endif this.$util.showToast({ - title: e.detail.errMsg + title: errMsg }) } }, diff --git a/components/privacy-popup/privacy-popup.vue b/components/privacy-popup/privacy-popup.vue index 397bf04..37e975d 100644 --- a/components/privacy-popup/privacy-popup.vue +++ b/components/privacy-popup/privacy-popup.vue @@ -281,18 +281,18 @@ export default { // #ifdef MP-WEIXIN if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization((resolve) => { - // 检查是否已同意 - if (!PrivacyManager.needShow()) { - // 已同意, 直接放行 - resolve({ event: 'agree' }) + // 关键: 微信规定 resolve({ event: 'agree' }) 必须携带 buttonId + // (值为页面上 open-type="agreePrivacyAuthorization" 按钮的 id), + // 缺 buttonId 的 agree 会被判定非法, 被拦截的隐私接口(如 getPhoneNumber) + // 直接报 "privacy permission is not authorized or buttonId is invalid"。 + // 所以无论 storage 是否标记过同意, 都必须弹自定义弹窗让用户点同意按钮。 + if (typeof this.privacyAuthorization === 'function') { + // 存 resolve 等用户操作, 由 handleAgree 携带 buttonId='agree-btn' 放行 + this.privacyAuthorization(resolve) } else { - // 需要弹窗, 存 resolve 等用户操作 - if (typeof this.privacyAuthorization === 'function') { - this.privacyAuthorization(resolve) - } else { - // 兜底: 组件未就绪, 直接放行避免 API 卡死 - resolve({ event: 'agree' }) - } + // 兜底: 组件未就绪, disagree 放行避免平台 API 永久挂起 + // (disagree 无需 buttonId, 不会产生非法 resolve) + resolve({ event: 'disagree' }) } }) } diff --git a/lang-bundle.json b/lang-bundle.json index 5348c22..5a26840 100644 --- a/lang-bundle.json +++ b/lang-bundle.json @@ -81,6 +81,7 @@ "loggingIn": "登录中", "loginFailed": "登录失败", "pleaseAuthorizeMobile": "请授权您的手机号码", + "pleaseAgreePrivacy": "请先同意《隐私条款》后重试", "pleaseEnterNickname": "请输入昵称", "avatarPullFailed": "头像拉取失败" }, diff --git a/lang/en-us/components/ns-login.js b/lang/en-us/components/ns-login.js index be6eaef..36998c9 100644 --- a/lang/en-us/components/ns-login.js +++ b/lang/en-us/components/ns-login.js @@ -45,6 +45,7 @@ export const lang = { loggingIn: 'Logging in', loginFailed: 'Login failed', pleaseAuthorizeMobile: 'Please authorize your mobile number', + pleaseAgreePrivacy: 'Please agree to the Privacy Policy and retry', pleaseEnterNickname: 'Please enter nickname', avatarPullFailed: 'Avatar pull failed', } diff --git a/lang/zh-cn/components/ns-login.js b/lang/zh-cn/components/ns-login.js index 3f188ae..34bd36f 100644 --- a/lang/zh-cn/components/ns-login.js +++ b/lang/zh-cn/components/ns-login.js @@ -45,6 +45,7 @@ export const lang = { loggingIn: '登录中', loginFailed: '登录失败', pleaseAuthorizeMobile: '请授权您的手机号码', + pleaseAgreePrivacy: '请先同意《隐私条款》后重试', pleaseEnterNickname: '请输入昵称', avatarPullFailed: '头像拉取失败', } diff --git a/pages_tool/form/form.vue b/pages_tool/form/form.vue index 020973b..b449bef 100644 --- a/pages_tool/form/form.vue +++ b/pages_tool/form/form.vue @@ -251,6 +251,9 @@ this.resumeSubmitAfterLogin(); // 本地缓存一份字段 JSON:下次再打开时如果仍被登录拦截,可作为 skeleton 兜底 try { uni.setStorageSync('form_fields_cache_' + this.id, dataObj.json_data); } catch (e) {} + // 成功分支同样必须关闭 loading 遮罩: + // 之前这里直接 return 跳过了函数底部的 hide(),导致登录后进入表单一直转圈 + if (this.$refs.loadingCover) this.$refs.loadingCover.hide(); return; } diff --git a/pages_tool/form/formdata.vue b/pages_tool/form/formdata.vue index 45420fd..c7536ab 100644 --- a/pages_tool/form/formdata.vue +++ b/pages_tool/form/formdata.vue @@ -170,7 +170,9 @@ export default { // diy-form 内部 mergedFormConfig 同时兼容 snake_case 后再交给 ns-newform // - show_banner / form_banner:交给 diy-form 渲染 banner,样式走 form_config.bannerPaddingX;form_banner 给保底图 // - show_tip / form_tip:交给 diy-form 渲染 tip,样式走 form_config;form_tip 给保底文案 - // 注:本 computed 只在 内被消费,无需 null 守卫 + // 注:小程序端模板引用的 computed 会进入 setData 数据流、每次更新都重新求值, + // 即使消费节点都在 v-if="detail" 内,detail 置空瞬间(提交成功/登录回跳等)也会求值, + // 必须做空值守卫,否则真机抛 Cannot read properties of null (reading 'json_data') 导致渲染中断 diyValue() { // 新视觉默认样式:强制覆盖后端 form_config 中可能存在的旧默认(horizontal / 红按钮 / 灰背景等), // 使表单在前端统一呈现为新设计稿样式;后台仍保留行为类配置(如 captcha / behavior / submitText)。 @@ -195,6 +197,16 @@ export default { submitColor: '#ffffff', submitBgColor: '#3b82f6' }; + if (!this.detail) { + return { + jsonData: null, + formConfig: Object.assign({}, styleDefaults), + show_banner: 0, + form_banner: '', + show_tip: 0, + form_tip: '' + }; + } return { jsonData: this.detail.json_data, formConfig: Object.assign({}, this.detail.form_config || {}, styleDefaults), @@ -459,6 +471,9 @@ export default { this.resumeSubmitAfterLogin(); // 本地缓存:下次再打开时若被登录拦截,可作为 skeleton 数据使用 try { uni.setStorageSync('form_fields_cache_' + this.id, dataObj.json_data); } catch (e) {} + // 成功分支同样必须关闭 loading 遮罩: + // 之前这里直接 return 跳过了函数底部的 hide(),导致登录后进入表单一直转圈 + this.$refs.loadingCover && this.$refs.loadingCover.hide(); return; }