406 lines
18 KiB
Vue
406 lines
18 KiB
Vue
<template>
|
||
<view :style="themeColor">
|
||
<view class="page" v-if="detail">
|
||
<view class="form-banner">
|
||
<image :src="$util.img('public/uniapp/form/banner.png')" mode="widthFix"></image>
|
||
</view>
|
||
<view class="system-form-wrap">
|
||
<view class="form-title">{{ $lang('form.form.title') }}</view>
|
||
<ns-form :data="detail.json_data" :formConfig="detail.form_config" ref="form"></ns-form>
|
||
<!-- 行为设置:提交验证码 -->
|
||
<view class="captcha-wrap" v-if="behavior.captcha == 1">
|
||
<input class="captcha-input" type="text" v-model="captcha.code" :placeholder="$lang('form.form.captchaPlaceholder')" />
|
||
<!-- #ifdef H5 -->
|
||
<img v-if="captcha.img" class="captcha-img" :src="captcha.img" @click="getCaptcha()">
|
||
<!-- #endif -->
|
||
<!-- #ifndef H5 -->
|
||
<image v-if="captcha.img" class="captcha-img" :src="captcha.img" mode="aspectFill" @click="getCaptcha()"></image>
|
||
<!-- #endif -->
|
||
<view class="captcha-btn" @click="getCaptcha()">{{ captcha.img ? '换一张' : '获取验证码' }}</view>
|
||
</view>
|
||
<button type="primary" size="mini" class="button mini" @click="create()">{{ $lang('common.submit') }}</button>
|
||
</view>
|
||
</view>
|
||
<ns-empty :text="emptyText" v-else></ns-empty>
|
||
|
||
<loading-cover ref="loadingCover"></loading-cover>
|
||
<ns-login ref="login"></ns-login>
|
||
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import scroll from '@/common/js/scroll-view.js';
|
||
|
||
export default {
|
||
mixins: [scroll],
|
||
data() {
|
||
return {
|
||
id: 0,
|
||
detail: null,
|
||
isRepeat: false,
|
||
complete: false,
|
||
scroll: true,
|
||
urlParams: {},
|
||
// 行为设置(后端 __form_config__.behavior 下发,含默认值)
|
||
behavior: {},
|
||
// 非成功状态的空态文案(表单未开放/需登录/加载失败等)
|
||
formError: '',
|
||
// 图形验证码(behavior.captcha=1 时展示)
|
||
captcha: { id: '', img: '', code: '' },
|
||
// 是否已加载过验证码(避免 onShow 重复刷新导致 session 错乱)
|
||
captchaLoaded: false,
|
||
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
|
||
pendingSubmit: null
|
||
};
|
||
},
|
||
computed: {
|
||
emptyText() {
|
||
return this.complete ? this.$lang('form.form.submitSuccess') : (this.formError || this.$lang('form.form.noFormInfo'));
|
||
}
|
||
},
|
||
onLoad(data) {
|
||
// #ifdef MP-ALIPAY
|
||
let options = my.getLaunchOptionsSync();
|
||
options.query && Object.assign(data, options.query)
|
||
// #endif
|
||
|
||
this.id = data.id || 0;
|
||
|
||
// 通用 URL 参数收集:除 id/scene 外的所有参数都透传给后端
|
||
// 后端会做卫生过滤并广播事件,供 recommend 等插件消费
|
||
const reservedKeys = ['uniacid', 'id', 'scene'];
|
||
this.urlParams = {};
|
||
for (let key in data) {
|
||
if (reservedKeys.indexOf(key) !== -1) continue;
|
||
this.urlParams[key] = data[key];
|
||
}
|
||
|
||
if (data.scene) {
|
||
var sceneParams = decodeURIComponent(data.scene);
|
||
sceneParams = sceneParams.split('&');
|
||
if (sceneParams.length) {
|
||
sceneParams.forEach(item => {
|
||
if (item.indexOf('id') != -1) this.id = item.split('-')[1];
|
||
// scene 中也可能携带其他参数
|
||
let pair = item.split('=');
|
||
if (pair.length === 2 && pair[0] !== 'id') {
|
||
this.urlParams[pair[0]] = pair[1];
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
onShow() {
|
||
// 打开是否需要登录由后端 behavior 决定(info 返回 FORM_LOGIN_REQUIRED 时弹登录)
|
||
this.getData();
|
||
},
|
||
watch: {
|
||
storeToken: function(nVal, oVal) {
|
||
if (nVal) {
|
||
this.getData();
|
||
// 填写需登录:弹窗登录成功后自动续提交
|
||
this.resumeSubmitAfterLogin();
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
getData() {
|
||
this.$api.sendRequest({
|
||
url: '/form/api/form/info',
|
||
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();
|
||
}
|
||
});
|
||
},
|
||
// 图形验证码(点击图片/按钮刷新;提交失败后自动刷新)
|
||
getCaptcha() {
|
||
this.captchaLoaded = true;
|
||
this.$api.sendRequest({
|
||
url: '/api/captcha/captcha',
|
||
data: {
|
||
captcha_id: this.captcha.id
|
||
},
|
||
success: res => {
|
||
if (res.code == 0 && res.data) {
|
||
this.captcha.id = res.data.id;
|
||
this.captcha.img = res.data.img ? res.data.img.replace(/\r\n/g, '') : '';
|
||
this.captcha.code = '';
|
||
}
|
||
}
|
||
});
|
||
},
|
||
// 登录拦截时保存待提交数据(组件内存 + 本地存储,H5 登录会整页回跳)
|
||
savePendingSubmit(formData) {
|
||
const data = formData || (this.$refs.form && this.$refs.form.formData) || null;
|
||
const payload = {
|
||
formId: this.id,
|
||
urlParams: this.urlParams,
|
||
formData: data,
|
||
captchaId: this.captcha.id,
|
||
captchaCode: this.captcha.code,
|
||
ts: Date.now()
|
||
};
|
||
this.pendingSubmit = payload;
|
||
try { uni.setStorageSync('form_pending_submit', payload); } catch (e) {}
|
||
// H5 整页回跳标记:仅当确由登录拦截触发时才允许自动续提交,避免陈旧数据被误提交
|
||
try { uni.setStorageSync('form_pending_from_login', 1); } catch (e) {}
|
||
},
|
||
// 仅在已登录时取回并消费待提交数据(未登录 / 非登录回跳 / 过期不消费,避免误提交);取后清除
|
||
takePendingSubmit() {
|
||
let payload = this.pendingSubmit;
|
||
const fromStorage = !payload;
|
||
if (fromStorage) {
|
||
try { payload = uni.getStorageSync('form_pending_submit'); } catch (e) {}
|
||
}
|
||
if (!payload || !payload.formData) return null;
|
||
if (!this.storeToken) return null;
|
||
// H5 存储路径:必须是因登录拦截触发的回跳,否则视为陈旧数据,清理后不提交
|
||
if (fromStorage) {
|
||
let fromLogin = false;
|
||
try { fromLogin = uni.getStorageSync('form_pending_from_login'); } catch (e) {}
|
||
if (!fromLogin) {
|
||
this.pendingSubmit = null;
|
||
try { uni.removeStorageSync('form_pending_submit'); } catch (e) {}
|
||
return null;
|
||
}
|
||
}
|
||
// 仅消费 10 分钟内的待提交,避免极旧数据被自动提交
|
||
if (payload.ts && Date.now() - payload.ts > 10 * 60 * 1000) {
|
||
this.pendingSubmit = null;
|
||
try { uni.removeStorageSync('form_pending_submit'); } catch (e) {}
|
||
try { uni.removeStorageSync('form_pending_from_login'); } catch (e) {}
|
||
return null;
|
||
}
|
||
this.pendingSubmit = null;
|
||
try { uni.removeStorageSync('form_pending_submit'); } catch (e) {}
|
||
try { uni.removeStorageSync('form_pending_from_login'); } catch (e) {}
|
||
return payload;
|
||
},
|
||
// 登录成功后:回填已填数据并自动续提交
|
||
resumeSubmitAfterLogin() {
|
||
const pending = this.takePendingSubmit();
|
||
if (!pending) return;
|
||
// 回填表单字段(nextTick 确保 ns-form 已挂载),避免 H5 整页回跳后好友看到空白表单
|
||
this.$nextTick(() => this.restoreFormData(pending.formData));
|
||
// 验证码:MP 登录不丢态可直接提交;H5 回跳后验证码失效,需重新获取并让用户重填
|
||
if (this.behavior.captcha == 1 && !this.captcha.code) {
|
||
this.getCaptcha();
|
||
this.$util.showToast({ title: '请重新输入验证码后提交' });
|
||
return;
|
||
}
|
||
this.create();
|
||
},
|
||
// 将保存的 formData 回填到 ns-form
|
||
restoreFormData(formData) {
|
||
try {
|
||
const form = this.$refs.form;
|
||
if (form && Array.isArray(formData)) {
|
||
form.formData = JSON.parse(JSON.stringify(formData));
|
||
if (typeof form.processFormData === 'function') form.processFormData();
|
||
}
|
||
} catch (e) {}
|
||
},
|
||
// 填完跳转:消费统一链接选择器的完整对象(wap_url 外链/页面路径、外部小程序、拨打电话)
|
||
doRedirect(link) {
|
||
const fallback = () => { this.complete = true; this.detail = null; };
|
||
if (!link) return fallback();
|
||
if (link.appid) {
|
||
// 跳转其他微信小程序
|
||
wx.navigateToMiniProgram({ appId: link.appid, path: link.page || '', fail: fallback });
|
||
} else if (link.mobile) {
|
||
uni.makePhoneCall({ phoneNumber: String(link.mobile), fail: fallback });
|
||
fallback();
|
||
} else if (link.wap_url) {
|
||
if (/^https?:\/\//i.test(link.wap_url)) {
|
||
uni.navigateTo({ url: '/pages_tool/webview/webview?src=' + encodeURIComponent(link.wap_url), fail: fallback });
|
||
} else {
|
||
uni.navigateTo({ url: link.wap_url, fail: fallback });
|
||
}
|
||
} else {
|
||
fallback();
|
||
}
|
||
},
|
||
create() {
|
||
if (this.$refs.form.verify()) {
|
||
if (this.isRepeat) return;
|
||
const behavior = this.behavior || {};
|
||
|
||
// 填写需登录 / 限填(后端强制,前端提前拦截优化体验)
|
||
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);
|
||
return;
|
||
}
|
||
// 验证码必填
|
||
if (behavior.captcha == 1 && !this.captcha.code) {
|
||
this.$util.showToast({ title: this.$lang('form.form.captchaRequired') });
|
||
return;
|
||
}
|
||
|
||
this.isRepeat = true;
|
||
const submitCaptchaId = String(this.captcha.id || '');
|
||
const submitCaptchaCode = String(this.captcha.code || '').toUpperCase();
|
||
console.log('[FORM-SUBMIT-CAPTCHA]', { captcha_id: submitCaptchaId, captcha_code: submitCaptchaCode, raw_code: this.captcha.code });
|
||
this.$api.sendRequest({
|
||
url: '/form/api/form/create',
|
||
data: {
|
||
form_id: this.id,
|
||
form_data: typeof this.$refs.form.formData === 'string' ? this.$refs.form.formData : JSON.stringify(this.$refs.form.formData),
|
||
url_params: JSON.stringify(this.urlParams),
|
||
member_id: this.memberInfo ? this.memberInfo.id : 0,
|
||
captcha_id: submitCaptchaId,
|
||
captcha_code: submitCaptchaCode
|
||
},
|
||
success: res => {
|
||
if (res.code == 0) {
|
||
this.$util.showToast({ title: this.$lang('form.form.submitSuccess') });
|
||
const link = behavior.redirect;
|
||
setTimeout(() => {
|
||
link ? this.doRedirect(link)
|
||
: (this.complete = true, this.detail = null);
|
||
}, link ? 800 : 1500);
|
||
} else {
|
||
this.isRepeat = false;
|
||
if (res.message == 'FORM_LOGIN_REQUIRED') {
|
||
// 提交时后端仍要求登录(如登录态过期):暂存数据,重登后自动续提交
|
||
this.savePendingSubmit();
|
||
this.$refs.login && this.$refs.login.open('/pages_tool/form/form?id=' + this.id);
|
||
} 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();
|
||
} else if (res.message == 'FORM_SUBMIT_LIMIT') {
|
||
this.$util.showToast({ title: this.$lang('form.form.submitLimit') });
|
||
this.complete = true;
|
||
this.detail = null;
|
||
} else if (res.message == 'FORM_NOT_OPEN') {
|
||
this.$util.showToast({ title: this.$lang('form.form.notOpen') });
|
||
} else {
|
||
this.$util.showToast({ title: res.message });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
},
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
|
||
.form-banner {
|
||
width: 100vw;
|
||
line-height: 1;
|
||
|
||
image {
|
||
width: 100%;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
.system-form-wrap {
|
||
background: $color-bg;
|
||
border-radius: 32rpx;
|
||
overflow: hidden;
|
||
margin: 0 0 60rpx 0;
|
||
padding: 0 26rpx;
|
||
transform: translateY(-40rpx);
|
||
|
||
.form-title {
|
||
line-height: 100rpx;
|
||
padding-top: 20rpx;
|
||
}
|
||
|
||
.captcha-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 16rpx 20rpx;
|
||
margin-top: 24rpx;
|
||
|
||
.captcha-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
font-size: 28rpx;
|
||
border: 2rpx solid #e5e7eb;
|
||
border-radius: 12rpx;
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.captcha-img {
|
||
width: 200rpx;
|
||
height: 72rpx;
|
||
margin-left: 20rpx;
|
||
border-radius: 12rpx;
|
||
border: 2rpx solid #e5e7eb;
|
||
}
|
||
|
||
.captcha-btn {
|
||
margin-left: 20rpx;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 26rpx;
|
||
color: #ff4544;
|
||
background: #fff;
|
||
border: 2rpx solid #ff4544;
|
||
border-radius: 12rpx;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.button {
|
||
height: 80rpx;
|
||
line-height: 80rpx !important;
|
||
margin-top: 30rpx !important;
|
||
width: 100%;
|
||
border-radius: 80rpx;
|
||
}
|
||
|
||
::v-deep .form-wrap {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
border-radius: 32rpx;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
::v-deep .mescroll-totop {
|
||
/* #ifdef H5 */
|
||
right: 27rpx !important;
|
||
bottom: 120rpx !important;
|
||
/* #endif */
|
||
/* #ifdef MP */
|
||
right: 27rpx !important;
|
||
bottom: 180rpx !important;
|
||
/* #endif */
|
||
}
|
||
</style>
|