Files
lucky_shop/pages_tool/form/form.vue
2026-08-25 17:39:03 +08:00

406 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>