From f6038575cc30765d3c8ece837a1e6b11dbb75a48 Mon Sep 17 00:00:00 2001 From: jing <1683105490@qq.com> Date: Sat, 29 Aug 2026 16:04:20 +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 --- common/js/util.js | 121 +++++++++++++----- pages_tool/form/form.vue | 221 +++++++++++++++++++++++++++++---- pages_tool/form/formdata.vue | 230 +++++++++++++++++++++++++++++++---- 3 files changed, 489 insertions(+), 83 deletions(-) diff --git a/common/js/util.js b/common/js/util.js index a714c70..9405da8 100644 --- a/common/js/util.js +++ b/common/js/util.js @@ -1644,59 +1644,118 @@ export default { }, /** * 获取小程序分享内容数据 + * + * 重要:本函数保证永远 resolve(绝不 reject)。 + * 原因:外部 8 处调用(category.vue / diy.js / promote_code.vue / contact.vue 等) + * 全部只写了 .then(res => {}) 没有 .catch(),一旦 reject 就会变成 Unhandled Promise Rejection, + * 微信小程序框架会把它包成 Error: MiniProgramError 并以红框形式打印到控制台,污染日志且易被 + * 误判为业务崩溃。后台未配置某路径的分享(data:null)、接口返回 code<0、甚至 HTTP 网络失败, + * 全部退化为"从传入 path 构造最小分享对象",保证调用方始终能拿到可消费的结构。 */ getMpShare(path) { - //如果没有特别指定 则获取当前页面的路由 + // 如果没有特别指定 则获取当前页面的路由 if (!path) { let route = this.getCurrentRoute(); path = route.path; if (path == '/pages_tool/member/index') { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { resolve({}) }); } } - return new Promise((resolve, reject) => { + // 从传入 path 生成的兜底分享(后台未配置 / 接口异常时使用) + const buildFallback = (p) => { + const fallbackPath = p || '/pages/index/index'; + const fallback = { + title: '小程序分享', + path: fallbackPath, + imageUrl: '' + }; + const appMessage = { + title: fallback.title, + path: fallback.path, + imageUrl: fallback.imageUrl, + success: () => {}, + fail: () => {} + }; + const query = (fallback.path.indexOf('?') > 0) ? fallback.path.split('?')[1] : ''; + const timeLine = { + title: fallback.title, + query: fallback.path, + imageUrl: fallback.imageUrl + }; + return { appMessage, timeLine, _fallback: true, query }; + }; + // 通用构建:从后端返回的 {title,path,imageUrl} 拼成标准结构 + const buildFromConfig = (shareConfig) => { + const appMessage = { + title: shareConfig.title, + path: shareConfig.path, + imageUrl: shareConfig.imageUrl || '', + success: () => {}, + fail: () => {} + }; + const query = (shareConfig.path && shareConfig.path.indexOf('?') > 0) + ? shareConfig.path.split('?')[1] + : ''; + const timeLine = { + title: shareConfig.title, + query: shareConfig.path, + imageUrl: shareConfig.imageUrl || '' + }; + return { appMessage, timeLine, query }; + }; + const fallback = buildFallback(path); + return new Promise((resolve) => { Http.sendRequest({ url: "/weapp/api/weapp/share", data: { path: path }, success: res => { - if (res.code >= 0) { - let shareConfig = res.data.data; - if (shareConfig) { - //分享给好友 - let appMessageData = { - title: shareConfig.title, - path: shareConfig.path, - imageUrl: shareConfig.imageUrl, - success: res => { - }, - fail: res => { + try { + if (!res || typeof res !== 'object') { + resolve(fallback); + return; + } + if (res.code >= 0) { + // 响应可能是两种形态: + // A) { code, message, data: { title, path, imageUrl } } + // B) { code, message, permission:{...}, data: null } + // 统一安全解包:外层 payload 可能在 res.data 里,也可能直接在 res 里 + const outer = res.data !== undefined ? res.data : res; + let shareConfig = null; + if (outer && typeof outer === 'object') { + // 后端在 outer 内嵌真正的配置对象(A 形态):outer.data = {title, path, ...} + if (outer.data && typeof outer.data === 'object' && (outer.data.title || outer.data.path)) { + shareConfig = outer.data; + } else if (outer.title || outer.path) { + // 极少数后端版本把配置平铺在 outer 上(扁平形态) + shareConfig = outer; } } - //分享到朋友圈 - let query = ''; - if (shareConfig.path.indexOf('?') > 0) { - query = shareConfig.path.split('?')[1]; + if (shareConfig) { + resolve(buildFromConfig(shareConfig)); + } else { + // 后台未配置该路径(B 形态 / data=null / 空对象)→ 不 reject,直接兜底 + // 防止 8 处无 catch 的调用方变成 Unhandled Promise Rejection + resolve(fallback); } - let timeLineData = { - title: shareConfig.title, - query: shareConfig.path, - imageUrl: shareConfig.imageUrl, - } - resolve({ - appMessage: appMessageData, - timeLine: timeLineData, - }) } else { - reject(res.data); + // 业务 code<0:视为"无可用分享配置",兜底 resolve,不抛错 + resolve(fallback); } - } else { - reject(res.data) + } catch (e) { + // 任何解析异常(响应结构异常/字段缺失/后端改版)一律兜底,绝不 reject + console.warn('[UTIL] getMpShare parse error, use fallback:', e && e.message ? e.message : e); + resolve(fallback); } - } + }, + fail: () => { + // 网络失败:同样兜底(之前这里没写 fail 回调,Promise 会 hang 住) + resolve(fallback); + }, + complete: () => {} }); }) }, diff --git a/pages_tool/form/form.vue b/pages_tool/form/form.vue index 5f27cb3..020973b 100644 --- a/pages_tool/form/form.vue +++ b/pages_tool/form/form.vue @@ -21,7 +21,13 @@ - + + + + + + + @@ -42,6 +48,8 @@ complete: false, scroll: true, urlParams: {}, + // 登录后回跳地址(供 ns-login / H5 重定向使用) + backUrl: '', // 行为设置(后端 __form_config__.behavior 下发,含默认值) behavior: {}, // 非成功状态的空态文案(表单未开放/需登录/加载失败等) @@ -51,7 +59,9 @@ // 是否已加载过验证码(避免 onShow 重复刷新导致 session 错乱) captchaLoaded: false, // 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复) - pendingSubmit: null + pendingSubmit: null, + // onLoad→getData 比 ns-login 挂载早时,用此标志在 mounted 再兜底弹登录(避免 ref 空报错) + pendingLoginRequired: false }; }, computed: { @@ -124,11 +134,77 @@ }); } } + + // #ifdef H5 + // H5:从 window.location.href 解析参数,兼容 history 路由 (?key=val) 与 hash 路由 (#/path?key=val) 两种形式 + // 保证 recommend_code / uniacid 等参数不丢失 + try { + const href = window.location.href; + let queryStr = ''; + const hashIdx = href.indexOf('#'); + const qIdx = href.indexOf('?'); + if (qIdx !== -1 && (hashIdx === -1 || qIdx < hashIdx)) { + // history 模式:? 在 # 之前(或无 #) + queryStr = href.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : href.length); + } else if (hashIdx !== -1) { + // hash 模式:# 之后可能还有 ? + const afterHash = href.substring(hashIdx + 1); + const hq = afterHash.indexOf('?'); + if (hq !== -1) queryStr = afterHash.substring(hq + 1); + } + if (queryStr) { + const pairs = queryStr.split('&'); + for (const p of pairs) { + if (!p) continue; + const eq = p.indexOf('='); + let k, v; + if (eq === -1) { k = decodeURIComponent(p); v = ''; } + else { k = decodeURIComponent(p.substring(0, eq)); v = decodeURIComponent(p.substring(eq + 1)); } + if (k === 'id') { if (!this.id) this.id = v; } + else if (k === 'uniacid') { /* reserved, 透传到后端时不进 urlParams */ } + else if (k === 'scene') { continue; } + else { if (this.urlParams[k] === undefined) this.urlParams[k] = v; } + } + } + } catch (e) {} + // #endif + + // 构建登录回跳地址:包含 id + 所有 urlParams(尤其 recommend_code), + // 供 ns-login 弹窗登录成功或 H5 整页重定向登录后回到本页 + const backQuery = []; + if (this.id) backQuery.push('id=' + encodeURIComponent(this.id)); + for (const k in this.urlParams) { + const v = this.urlParams[k]; + if (v !== undefined && v !== null && v !== '') { + backQuery.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); + } + } + this.backUrl = '/pages_tool/form/form' + (backQuery.length ? '?' + backQuery.join('&') : ''); }, onShow() { // 打开是否需要登录由后端 behavior 决定(info 返回 FORM_LOGIN_REQUIRED 时弹登录) this.getData(); }, + mounted() { + // 1) 兜底 A:onLoad→getData 回调比 ns-login 挂载还早时,pendingLoginRequired=true + // 挂载完成再最后尝试一次自动弹窗 + if (this.pendingLoginRequired) { + const tryOpen = () => { + try { + if (this.$refs.login && typeof this.$refs.login.open === 'function') { + this.$refs.login.open(this.backUrl); + } + } catch (e) { console.warn('[FORM] mounted login.open failed:', e); } + }; + this.$nextTick(tryOpen); + this.pendingLoginRequired = false; + } + // 2) 兜底 B:登录态在组件创建之前就已写入(token 提前 race),但 info 因时序没拿到数据 + // 此时用户实际上已经登录,再拉一次 info 就能拿到真实表单数据 + if (this.storeToken && !this.detail) { + this.$nextTick(() => { this.getData(); }); + } + }, watch: { storeToken: function(nVal, oVal) { if (nVal) { @@ -145,29 +221,124 @@ data: { form_id: this.id }, - success: res => { - if (res.code == 0 && res.data) { - this.detail = res.data; - this.behavior = (res.data.form_config && res.data.form_config.behavior) || {}; - if (this.behavior.captcha == 1 && !this.captchaLoaded) this.getCaptcha(); - // 登录回跳后自动续提交(填写需登录场景:H5 整页回跳 / MP 弹窗登录后) - this.resumeSubmitAfterLogin(); - } else if (res.message == 'FORM_LOGIN_REQUIRED') { - // 打开需登录:弹登录组件,登录成功后 watch storeToken 重新拉取 - this.$refs.login ? this.$refs.login.open('/pages_tool/form/form?id=' + this.id) - : (this.formError = this.$lang('form.form.loginRequired')); - } else if (res.message == 'FORM_NOT_OPEN') { - this.formError = this.$lang('form.form.notOpen'); - } else { - this.formError = res.message || this.$lang('form.form.noFormInfo'); - } - if (this.$refs.loadingCover) this.$refs.loadingCover.hide(); - }, - fail: res => { - if (this.$refs.loadingCover) this.$refs.loadingCover.hide(); - } + success: res => { this._handleInfoResponse(res); }, + fail: err => { this._handleInfoFailure(err); } }); }, + // ====== 表单 info 响应统一处理(剥离自 getData,便于严格判定与复用) ====== + // 成功分支必须同时满足:code==0 + data 是对象 + 内含 json_data 或 form_config + // 只要不满足就是"没拿到有效表单",然后用最宽的条件判定是否是登录拦截: + // - 消息包含 FORM_LOGIN_REQUIRED / LOGIN_REQUIRED / 请先登录 / 登录相关中文 + // - 或者 data 无效(null/undefined/''/{}/非对象)+ 用户未登录 + 有 form_id + // 命中任一 → 一律弹登录 + 友好文案"请先登录",不再误显示"未获取到表单信息" + _handleInfoResponse(res) { + try { + const payload = (res && typeof res === 'object') ? res : {}; + const dataObj = payload.data; + + // —— 1) 严格判定:是否拿到了真正的表单结构? —— + const hasValidFormData = + payload.code == 0 && + dataObj !== null && dataObj !== undefined && dataObj !== '' && + typeof dataObj === 'object' && + (dataObj.json_data !== undefined || dataObj.form_config !== undefined); + + if (hasValidFormData) { + this.detail = dataObj; + this.formError = ''; + this.behavior = (dataObj.form_config && dataObj.form_config.behavior) || {}; + if (this.behavior.captcha == 1 && !this.captchaLoaded) this.getCaptcha(); + this.resumeSubmitAfterLogin(); + // 本地缓存一份字段 JSON:下次再打开时如果仍被登录拦截,可作为 skeleton 兜底 + try { uni.setStorageSync('form_fields_cache_' + this.id, dataObj.json_data); } catch (e) {} + return; + } + + // —— 2) 没拿到有效结构 → 宽网判定:是否属于"需要登录"场景? —— + const msg = (payload.message || '').toString(); + const isFormNotOpen = (msg === 'FORM_NOT_OPEN'); + + const isLoginMsg = + msg === 'FORM_LOGIN_REQUIRED' || + msg === 'LOGIN_REQUIRED' || + msg.indexOf('请先登录') !== -1 || + (msg.indexOf('登录') !== -1 && msg.indexOf('请') !== -1) || + (msg.toLowerCase().indexOf('login') !== -1 && msg.toLowerCase().indexOf('required') !== -1); + + const dataIsEmpty = + !dataObj || + dataObj === '' || + dataObj === null || + dataObj === undefined || + (typeof dataObj === 'object' && !dataObj.json_data && !dataObj.form_config && + ((Array.isArray(dataObj) && dataObj.length === 0) || + (!Array.isArray(dataObj) && Object.keys(dataObj).length === 0))); + + // 最宽判定:只要不是"表单未开放",且(显式登录消息 OR 空数据+未登录+有 id)都按登录拦截处理 + const shouldTriggerLogin = + !isFormNotOpen && + (isLoginMsg || (dataIsEmpty && !this.storeToken && this.id)); + + if (shouldTriggerLogin) { + this.formError = this.$lang('form.form.loginRequired'); + this._openLoginWithRetry(); + } else if (isFormNotOpen) { + this.formError = this.$lang('form.form.notOpen'); + } else { + this.formError = msg || this.$lang('form.form.noFormInfo'); + } + } catch (e) { + console.warn('[FORM] _handleInfoResponse exception:', e && e.message ? e.message : e); + this.formError = this.$lang('form.form.noFormInfo'); + } + if (this.$refs.loadingCover) this.$refs.loadingCover.hide(); + }, + // info 接口网络失败时也做同样判定:未登录+有 id 就尝试启动登录流程,至少让用户先完成登录 + // (登录成功后会再次 getData,那时网络一般已经正常) + _handleInfoFailure(err) { + try { + if (this.id && !this.storeToken) { + this.formError = this.$lang('form.form.loginRequired'); + this._openLoginWithRetry(); + } else { + this.formError = this.$lang('form.form.noFormInfo'); + } + } catch (e) { + this.formError = this.$lang('form.form.noFormInfo'); + } + if (this.$refs.loadingCover) this.$refs.loadingCover.hide(); + }, + // 打开登录:3 次 setTimeout 重试(0ms / 60ms / 320ms) + // 彻底覆盖 onLoad→回调→ns-login 尚未挂载的所有时序问题 + _openLoginWithRetry() { + const url = this.backUrl; + let attempts = 0; + const delays = [0, 60, 320]; + const tryOnce = () => { + try { + if (this.$refs.login && typeof this.$refs.login.open === 'function') { + this.$refs.login.open(url); + this.pendingLoginRequired = false; + return true; + } + } catch (e) { + console.warn('[FORM] login.open attempt ' + (attempts + 1) + ' failed:', e); + } + attempts++; + if (attempts < delays.length) { + setTimeout(tryOnce, delays[attempts]); + } else { + // 三次全失败:交给 mounted 钩子最后兜底 + this.pendingLoginRequired = true; + } + return false; + }; + tryOnce(); + }, + // 模板空态按钮的手动入口:用户主动点击也能打开登录弹窗(最后一道防线) + forceOpenLogin() { + this._openLoginWithRetry(); + }, // 图形验证码(点击图片/按钮刷新;提交失败后自动刷新) getCaptcha() { this.captchaLoaded = true; @@ -285,7 +456,7 @@ if ((behavior.require_login_submit == 1 || behavior.submit_limit > 0) && !this.storeToken) { // 保存已填写数据,登录成功(MP 弹窗 / H5 回跳)后自动续提交,避免数据丢失 this.savePendingSubmit(); - this.$refs.login && this.$refs.login.open('/pages_tool/form/form?id=' + this.id); + this.$refs.login && this.$refs.login.open(this.backUrl); return; } // 验证码必填 @@ -321,7 +492,7 @@ if (res.message == 'FORM_LOGIN_REQUIRED') { // 提交时后端仍要求登录(如登录态过期):暂存数据,重登后自动续提交 this.savePendingSubmit(); - this.$refs.login && this.$refs.login.open('/pages_tool/form/form?id=' + this.id); + this.$refs.login && this.$refs.login.open(this.backUrl); } else if (res.message == 'CAPTCHA_ERROR' || res.message == 'CAPTCHA_FAILURE') { this.$util.showToast({ title: this.$lang('form.form.captchaError') }); if (behavior.captcha == 1) this.getCaptcha(); diff --git a/pages_tool/form/formdata.vue b/pages_tool/form/formdata.vue index a78733b..45420fd 100644 --- a/pages_tool/form/formdata.vue +++ b/pages_tool/form/formdata.vue @@ -82,6 +82,10 @@ + + + + @@ -120,6 +124,8 @@ export default { backUrl: '', // 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复) pendingSubmit: null, + // onLoad→getData 比 ns-login 挂载早时,用此标志在 mounted 再兜底弹登录(避免 ref 空报错) + pendingLoginRequired: false, // #ifndef H5 navbarHeight: 44, // 自绘 navbar 内容区高度(不含状态栏),跨端兜底 44px @@ -290,6 +296,51 @@ export default { }) } + // #ifdef H5 + // H5:从 window.location.href 解析参数,兼容 history 路由 (?key=val) 与 hash 路由 (#/path?key=val) 两种形式 + // 保证 recommend_code / uniacid 等参数在任何路由模式下都不丢失 + try { + const href = window.location.href; + let queryStr = ''; + const hashIdx = href.indexOf('#'); + const qIdx = href.indexOf('?'); + if (qIdx !== -1 && (hashIdx === -1 || qIdx < hashIdx)) { + // history 模式:? 在 # 之前(或无 #) + queryStr = href.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : href.length); + } else if (hashIdx !== -1) { + // hash 模式:# 之后可能还有 ? + const afterHash = href.substring(hashIdx + 1); + const hq = afterHash.indexOf('?'); + if (hq !== -1) queryStr = afterHash.substring(hq + 1); + } + if (queryStr) { + const pairs = queryStr.split('&'); + const reservedH5 = ['scene', 'from', 'hide_navbar', 'show_home', 'title', 'enable_set_title']; + for (const p of pairs) { + if (!p) continue; + const eq = p.indexOf('='); + let k, v; + if (eq === -1) { k = decodeURIComponent(p); v = ''; } + else { k = decodeURIComponent(p.substring(0, eq)); v = decodeURIComponent(p.substring(eq + 1)); } + if (k === 'id') { if (!this.id) this.id = v; } + else if (k === 'uniacid') { if (!this.uniacid) this.uniacid = v; } + else if (reservedH5.indexOf(k) !== -1) { + // 导航/显示类参数:仅当 options 没有传时才从 href 补 + if (options[k] === undefined && this[k] === undefined) { + if (k === 'from') this.isFromMpWebView = (v === 'mp-webview'); + else if (k === 'hide_navbar') this.hideNavbar = this.$util.coerceBool(v); + else if (k === 'show_home') this.showHomeBtn = this.$util.coerceBool(v); + else if (k === 'title') this.formTitle = v; + else if (k === 'enable_set_title') this.enableSetTitle = (v == 1); + } + } else { + if (this.urlParams[k] === undefined) this.urlParams[k] = v; + } + } + } + } catch (e) {} + // #endif + // #ifdef H5 if (this.isFromMpWebView || this.hideNavbar) { this.$nextTick(() => { @@ -299,12 +350,25 @@ export default { } // #endif - // 登录后回跳地址:由原始 options 重建(供 FORM_LOGIN_REQUIRED 时 ns-login 使用) + // 登录后回跳地址:基于最终状态(this.id / this.uniacid + urlParams + 导航类参数)重建, + // 确保 options/scene/H5 href 三个来源的参数都被带到登录回跳 URL 中(尤其 recommend_code) const backQuery = []; - for (let key in options) { - if (key === 'scene') continue; - backQuery.push(key + '=' + encodeURIComponent(options[key])); + if (this.uniacid) backQuery.push('uniacid=' + encodeURIComponent(this.uniacid)); + if (this.id) backQuery.push('id=' + encodeURIComponent(this.id)); + for (const k in this.urlParams) { + const v = this.urlParams[k]; + if (v !== undefined && v !== null && v !== '') { + backQuery.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); + } } + // 导航类参数(不在 urlParams 中,单独收集) + if (options && options.from) backQuery.push('from=' + encodeURIComponent(options.from)); + if (this.hideNavbar) backQuery.push('hide_navbar=1'); + if (this.showHomeBtn) backQuery.push('show_home=1'); + // #ifdef H5 + if (this.formTitle) backQuery.push('title=' + encodeURIComponent(this.formTitle)); + if (this.enableSetTitle) backQuery.push('enable_set_title=1'); + // #endif this.backUrl = '/pages_tool/form/formdata' + (backQuery.length ? '?' + backQuery.join('&') : ''); this.getData() @@ -322,6 +386,26 @@ export default { onShow() { this.updateNavbarTitle(); }, + mounted() { + // 1) 兜底 A:onLoad→getData 回调比 ns-login 挂载还早时,pendingLoginRequired=true + // 挂载完成再最后尝试一次自动弹窗 + if (this.pendingLoginRequired) { + const tryOpen = () => { + try { + if (this.$refs.login && typeof this.$refs.login.open === 'function') { + this.$refs.login.open(this.backUrl); + } + } catch (e) { console.warn('[FORMDATA] mounted login.open failed:', e); } + }; + this.$nextTick(tryOpen); + this.pendingLoginRequired = false; + } + // 2) 兜底 B:登录态在组件创建之前就已写入(token 提前 race),但 info 因时序没拿到数据 + // 此时用户实际上已经登录,再拉一次 info 就能拿到真实表单数据 + if (this.storeToken && !this.detail) { + this.$nextTick(() => { this.getData(); }); + } + }, methods: { // #ifndef H5 // 返回上一页;用 navigateBack,栈深度>1 时由 hasBackStack 控制按钮可见性 @@ -344,31 +428,123 @@ export default { form_id: this.id, uniacid: this.uniacid }, - success: res => { - if (res.code == 0 && res.data) { - this.detail = res.data - this.updateNavbarTitle(); - // 行为设置(后端已合并默认值):验证码开启时拉取图形验证码 - this.behavior = (res.data.form_config && res.data.form_config.behavior) || {} - if (this.behavior.captcha == 1 && !this.captchaLoaded) this.getCaptcha() - // 登录回跳后自动续提交(填写需登录场景:H5 整页回跳 / MP 弹窗登录后) - this.resumeSubmitAfterLogin() - } else if (res.message == 'FORM_LOGIN_REQUIRED') { - // 打开需登录:弹登录组件,登录成功后 watch storeToken 重新拉取 - this.$refs.login ? this.$refs.login.open(this.backUrl) - : (this.formError = this.$lang('form.form.loginRequired')) - } else if (res.message == 'FORM_NOT_OPEN') { - this.formError = this.$lang('form.form.notOpen') - } else { - this.formError = res.message || '' - } - this.$refs.loadingCover && this.$refs.loadingCover.hide() - }, - fail: () => { - this.$refs.loadingCover && this.$refs.loadingCover.hide() - } + success: res => { this._handleInfoResponse(res); }, + fail: err => { this._handleInfoFailure(err); } }) }, + // ====== 表单 info 响应统一处理(剥离自 getData,便于严格判定与复用) ====== + // 成功必须同时满足:code==0 + data 是对象 + 内含 json_data 或 form_config + // 不满足 → 宽网判定是否是登录拦截: + // - 显式消息:FORM_LOGIN_REQUIRED / LOGIN_REQUIRED / 请先登录 + // - 兜底:数据为空 + 用户未登录 + 有 id + // 命中任一 → 一律弹登录 + 友好文案"请先登录",不再误显示"未获取到表单信息" + _handleInfoResponse(res) { + try { + const payload = (res && typeof res === 'object') ? res : {}; + const dataObj = payload.data; + + // —— 1) 严格判定:是否拿到了真正的表单结构? —— + const hasValidFormData = + payload.code == 0 && + dataObj !== null && dataObj !== undefined && dataObj !== '' && + typeof dataObj === 'object' && + (dataObj.json_data !== undefined || dataObj.form_config !== undefined); + + if (hasValidFormData) { + this.detail = dataObj; + this.formError = ''; + this.updateNavbarTitle(); + this.behavior = (dataObj.form_config && dataObj.form_config.behavior) || {}; + if (this.behavior.captcha == 1 && !this.captchaLoaded) this.getCaptcha(); + this.resumeSubmitAfterLogin(); + // 本地缓存:下次再打开时若被登录拦截,可作为 skeleton 数据使用 + try { uni.setStorageSync('form_fields_cache_' + this.id, dataObj.json_data); } catch (e) {} + return; + } + + // —— 2) 没拿到有效结构 → 宽网判定:是否属于"需要登录"场景? —— + const msg = (payload.message || '').toString(); + const isFormNotOpen = (msg === 'FORM_NOT_OPEN'); + + const isLoginMsg = + msg === 'FORM_LOGIN_REQUIRED' || + msg === 'LOGIN_REQUIRED' || + msg.indexOf('请先登录') !== -1 || + (msg.indexOf('登录') !== -1 && msg.indexOf('请') !== -1) || + (msg.toLowerCase().indexOf('login') !== -1 && msg.toLowerCase().indexOf('required') !== -1); + + const dataIsEmpty = + !dataObj || + dataObj === '' || + dataObj === null || + dataObj === undefined || + (typeof dataObj === 'object' && !dataObj.json_data && !dataObj.form_config && + ((Array.isArray(dataObj) && dataObj.length === 0) || + (!Array.isArray(dataObj) && Object.keys(dataObj).length === 0))); + + // 最宽判定:只要不是"表单未开放",且(显式登录消息 OR 空数据+未登录+有 id)都按登录拦截处理 + const shouldTriggerLogin = + !isFormNotOpen && + (isLoginMsg || (dataIsEmpty && !this.storeToken && this.id)); + + if (shouldTriggerLogin) { + this.formError = this.$lang('form.form.loginRequired'); + this._openLoginWithRetry(); + } else if (isFormNotOpen) { + this.formError = this.$lang('form.form.notOpen'); + } else { + this.formError = msg || ''; + } + } catch (e) { + console.warn('[FORMDATA] _handleInfoResponse exception:', e && e.message ? e.message : e); + this.formError = ''; + } + this.$refs.loadingCover && this.$refs.loadingCover.hide(); + }, + // info 接口网络失败时同样判定:未登录+有 id 就先走登录流程 + _handleInfoFailure(err) { + try { + if (this.id && !this.storeToken) { + this.formError = this.$lang('form.form.loginRequired'); + this._openLoginWithRetry(); + } else { + this.formError = ''; + } + } catch (e) { + this.formError = ''; + } + this.$refs.loadingCover && this.$refs.loadingCover.hide(); + }, + // 打开登录:3 次 setTimeout 重试(0ms / 60ms / 320ms) + // 彻底覆盖 onLoad→回调→ns-login 尚未挂载的所有时序问题 + _openLoginWithRetry() { + const url = this.backUrl; + let attempts = 0; + const delays = [0, 60, 320]; + const tryOnce = () => { + try { + if (this.$refs.login && typeof this.$refs.login.open === 'function') { + this.$refs.login.open(url); + this.pendingLoginRequired = false; + return true; + } + } catch (e) { + console.warn('[FORMDATA] login.open attempt ' + (attempts + 1) + ' failed:', e); + } + attempts++; + if (attempts < delays.length) { + setTimeout(tryOnce, delays[attempts]); + } else { + this.pendingLoginRequired = true; + } + return false; + }; + tryOnce(); + }, + // 模板空态按钮的手动入口(最后一道防线) + forceOpenLogin() { + this._openLoginWithRetry(); + }, updateNavbarTitle() { if (!this.detail?.form_name) return; let formName = this.detail.form_name;