chore:表单弹窗正常
This commit is contained in:
@@ -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) {
|
getMpShare(path) {
|
||||||
//如果没有特别指定 则获取当前页面的路由
|
// 如果没有特别指定 则获取当前页面的路由
|
||||||
if (!path) {
|
if (!path) {
|
||||||
let route = this.getCurrentRoute();
|
let route = this.getCurrentRoute();
|
||||||
path = route.path;
|
path = route.path;
|
||||||
if (path == '/pages_tool/member/index') {
|
if (path == '/pages_tool/member/index') {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
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({
|
Http.sendRequest({
|
||||||
url: "/weapp/api/weapp/share",
|
url: "/weapp/api/weapp/share",
|
||||||
data: {
|
data: {
|
||||||
path: path
|
path: path
|
||||||
},
|
},
|
||||||
success: res => {
|
success: res => {
|
||||||
if (res.code >= 0) {
|
try {
|
||||||
let shareConfig = res.data.data;
|
if (!res || typeof res !== 'object') {
|
||||||
if (shareConfig) {
|
resolve(fallback);
|
||||||
//分享给好友
|
return;
|
||||||
let appMessageData = {
|
}
|
||||||
title: shareConfig.title,
|
if (res.code >= 0) {
|
||||||
path: shareConfig.path,
|
// 响应可能是两种形态:
|
||||||
imageUrl: shareConfig.imageUrl,
|
// A) { code, message, data: { title, path, imageUrl } }
|
||||||
success: res => {
|
// B) { code, message, permission:{...}, data: null }
|
||||||
},
|
// 统一安全解包:外层 payload 可能在 res.data 里,也可能直接在 res 里
|
||||||
fail: 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//分享到朋友圈
|
if (shareConfig) {
|
||||||
let query = '';
|
resolve(buildFromConfig(shareConfig));
|
||||||
if (shareConfig.path.indexOf('?') > 0) {
|
} else {
|
||||||
query = shareConfig.path.split('?')[1];
|
// 后台未配置该路径(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 {
|
} else {
|
||||||
reject(res.data);
|
// 业务 code<0:视为"无可用分享配置",兜底 resolve,不抛错
|
||||||
|
resolve(fallback);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e) {
|
||||||
reject(res.data)
|
// 任何解析异常(响应结构异常/字段缺失/后端改版)一律兜底,绝不 reject
|
||||||
|
console.warn('[UTIL] getMpShare parse error, use fallback:', e && e.message ? e.message : e);
|
||||||
|
resolve(fallback);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
fail: () => {
|
||||||
|
// 网络失败:同样兜底(之前这里没写 fail 回调,Promise 会 hang 住)
|
||||||
|
resolve(fallback);
|
||||||
|
},
|
||||||
|
complete: () => {}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,13 @@
|
|||||||
<button type="primary" size="mini" class="button mini" @click="create()">{{ $lang('common.submit') }}</button>
|
<button type="primary" size="mini" class="button mini" @click="create()">{{ $lang('common.submit') }}</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<ns-empty :text="emptyText" v-else></ns-empty>
|
<!-- 空态:除 ns-empty 文案外,追加一个可手动点的登录按钮——解决一切自动弹窗时序问题后的最后兜底 -->
|
||||||
|
<view v-else>
|
||||||
|
<ns-empty :text="emptyText"></ns-empty>
|
||||||
|
<view v-if="!complete" style="padding: 20rpx 40rpx 40rpx; display: flex; justify-content: center;">
|
||||||
|
<button type="primary" size="mini" @click="forceOpenLogin">点击登录查看表单</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<loading-cover ref="loadingCover"></loading-cover>
|
<loading-cover ref="loadingCover"></loading-cover>
|
||||||
<ns-login ref="login"></ns-login>
|
<ns-login ref="login"></ns-login>
|
||||||
@@ -42,6 +48,8 @@
|
|||||||
complete: false,
|
complete: false,
|
||||||
scroll: true,
|
scroll: true,
|
||||||
urlParams: {},
|
urlParams: {},
|
||||||
|
// 登录后回跳地址(供 ns-login / H5 重定向使用)
|
||||||
|
backUrl: '',
|
||||||
// 行为设置(后端 __form_config__.behavior 下发,含默认值)
|
// 行为设置(后端 __form_config__.behavior 下发,含默认值)
|
||||||
behavior: {},
|
behavior: {},
|
||||||
// 非成功状态的空态文案(表单未开放/需登录/加载失败等)
|
// 非成功状态的空态文案(表单未开放/需登录/加载失败等)
|
||||||
@@ -51,7 +59,9 @@
|
|||||||
// 是否已加载过验证码(避免 onShow 重复刷新导致 session 错乱)
|
// 是否已加载过验证码(避免 onShow 重复刷新导致 session 错乱)
|
||||||
captchaLoaded: false,
|
captchaLoaded: false,
|
||||||
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
|
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
|
||||||
pendingSubmit: null
|
pendingSubmit: null,
|
||||||
|
// onLoad→getData 比 ns-login 挂载早时,用此标志在 mounted 再兜底弹登录(避免 ref 空报错)
|
||||||
|
pendingLoginRequired: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
onShow() {
|
||||||
// 打开是否需要登录由后端 behavior 决定(info 返回 FORM_LOGIN_REQUIRED 时弹登录)
|
// 打开是否需要登录由后端 behavior 决定(info 返回 FORM_LOGIN_REQUIRED 时弹登录)
|
||||||
this.getData();
|
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: {
|
watch: {
|
||||||
storeToken: function(nVal, oVal) {
|
storeToken: function(nVal, oVal) {
|
||||||
if (nVal) {
|
if (nVal) {
|
||||||
@@ -145,29 +221,124 @@
|
|||||||
data: {
|
data: {
|
||||||
form_id: this.id
|
form_id: this.id
|
||||||
},
|
},
|
||||||
success: res => {
|
success: res => { this._handleInfoResponse(res); },
|
||||||
if (res.code == 0 && res.data) {
|
fail: err => { this._handleInfoFailure(err); }
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// ====== 表单 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() {
|
getCaptcha() {
|
||||||
this.captchaLoaded = true;
|
this.captchaLoaded = true;
|
||||||
@@ -285,7 +456,7 @@
|
|||||||
if ((behavior.require_login_submit == 1 || behavior.submit_limit > 0) && !this.storeToken) {
|
if ((behavior.require_login_submit == 1 || behavior.submit_limit > 0) && !this.storeToken) {
|
||||||
// 保存已填写数据,登录成功(MP 弹窗 / H5 回跳)后自动续提交,避免数据丢失
|
// 保存已填写数据,登录成功(MP 弹窗 / H5 回跳)后自动续提交,避免数据丢失
|
||||||
this.savePendingSubmit();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// 验证码必填
|
// 验证码必填
|
||||||
@@ -321,7 +492,7 @@
|
|||||||
if (res.message == 'FORM_LOGIN_REQUIRED') {
|
if (res.message == 'FORM_LOGIN_REQUIRED') {
|
||||||
// 提交时后端仍要求登录(如登录态过期):暂存数据,重登后自动续提交
|
// 提交时后端仍要求登录(如登录态过期):暂存数据,重登后自动续提交
|
||||||
this.savePendingSubmit();
|
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') {
|
} else if (res.message == 'CAPTCHA_ERROR' || res.message == 'CAPTCHA_FAILURE') {
|
||||||
this.$util.showToast({ title: this.$lang('form.form.captchaError') });
|
this.$util.showToast({ title: this.$lang('form.form.captchaError') });
|
||||||
if (behavior.captcha == 1) this.getCaptcha();
|
if (behavior.captcha == 1) this.getCaptcha();
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
</block>
|
</block>
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<ns-empty :text="emptyText" :isIndex="false"></ns-empty>
|
<ns-empty :text="emptyText" :isIndex="false"></ns-empty>
|
||||||
|
<!-- 空态追加手动登录按钮:任何自动弹窗机制失效时的最后兜底,用户一点就开 -->
|
||||||
|
<view v-if="!complete" style="padding: 20rpx 40rpx 40rpx; display: flex; justify-content: center;">
|
||||||
|
<button type="primary" size="mini" @click="forceOpenLogin">点击登录查看表单</button>
|
||||||
|
</view>
|
||||||
</block>
|
</block>
|
||||||
<loading-cover ref="loadingCover"></loading-cover>
|
<loading-cover ref="loadingCover"></loading-cover>
|
||||||
<ns-login ref="login"></ns-login>
|
<ns-login ref="login"></ns-login>
|
||||||
@@ -120,6 +124,8 @@ export default {
|
|||||||
backUrl: '',
|
backUrl: '',
|
||||||
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
|
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
|
||||||
pendingSubmit: null,
|
pendingSubmit: null,
|
||||||
|
// onLoad→getData 比 ns-login 挂载早时,用此标志在 mounted 再兜底弹登录(避免 ref 空报错)
|
||||||
|
pendingLoginRequired: false,
|
||||||
|
|
||||||
// #ifndef H5
|
// #ifndef H5
|
||||||
navbarHeight: 44, // 自绘 navbar 内容区高度(不含状态栏),跨端兜底 44px
|
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
|
// #ifdef H5
|
||||||
if (this.isFromMpWebView || this.hideNavbar) {
|
if (this.isFromMpWebView || this.hideNavbar) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -299,12 +350,25 @@ export default {
|
|||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// 登录后回跳地址:由原始 options 重建(供 FORM_LOGIN_REQUIRED 时 ns-login 使用)
|
// 登录后回跳地址:基于最终状态(this.id / this.uniacid + urlParams + 导航类参数)重建,
|
||||||
|
// 确保 options/scene/H5 href 三个来源的参数都被带到登录回跳 URL 中(尤其 recommend_code)
|
||||||
const backQuery = [];
|
const backQuery = [];
|
||||||
for (let key in options) {
|
if (this.uniacid) backQuery.push('uniacid=' + encodeURIComponent(this.uniacid));
|
||||||
if (key === 'scene') continue;
|
if (this.id) backQuery.push('id=' + encodeURIComponent(this.id));
|
||||||
backQuery.push(key + '=' + encodeURIComponent(options[key]));
|
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.backUrl = '/pages_tool/form/formdata' + (backQuery.length ? '?' + backQuery.join('&') : '');
|
||||||
|
|
||||||
this.getData()
|
this.getData()
|
||||||
@@ -322,6 +386,26 @@ export default {
|
|||||||
onShow() {
|
onShow() {
|
||||||
this.updateNavbarTitle();
|
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: {
|
methods: {
|
||||||
// #ifndef H5
|
// #ifndef H5
|
||||||
// 返回上一页;用 navigateBack,栈深度>1 时由 hasBackStack 控制按钮可见性
|
// 返回上一页;用 navigateBack,栈深度>1 时由 hasBackStack 控制按钮可见性
|
||||||
@@ -344,31 +428,123 @@ export default {
|
|||||||
form_id: this.id,
|
form_id: this.id,
|
||||||
uniacid: this.uniacid
|
uniacid: this.uniacid
|
||||||
},
|
},
|
||||||
success: res => {
|
success: res => { this._handleInfoResponse(res); },
|
||||||
if (res.code == 0 && res.data) {
|
fail: err => { this._handleInfoFailure(err); }
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// ====== 表单 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() {
|
updateNavbarTitle() {
|
||||||
if (!this.detail?.form_name) return;
|
if (!this.detail?.form_name) return;
|
||||||
let formName = this.detail.form_name;
|
let formName = this.detail.form_name;
|
||||||
|
|||||||
Reference in New Issue
Block a user