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

795 lines
29 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>
<!-- #ifdef MP-HARMONY -->
<view :style="themeColor" class="harmony-root">
<!-- #endif -->
<!-- #ifndef MP-HARMONY -->
<view :style="themeColor">
<!-- #endif -->
<!--
自绘导航栏(仅在 navigationStyle:custom 的端渲染:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
- pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
- 左侧:hasBackStack 为 true 时显示返回箭头(点击 navigateBack),showHomeBtn=true 时再叠加 Home 胶囊
- 中央:表单标题(form_name)
- 整体高度 = 状态栏高度 + 44px 内容区,由 customNavbarStyle 动态算
H5 端保留系统原生 navbar,不渲染此块
-->
<!-- #ifndef H5 -->
<view class="form-home-navbar" :style="customNavbarStyle">
<view class="form-home-navbar__left">
<view v-if="hasBackStack" class="form-home-navbar__back" @tap="goBack" hover-class="form-home-navbar__btn--hover">
<text class="form-home-navbar__back-icon">‹</text>
</view>
<view v-if="showHomeBtn" class="form-home-capsule" @tap="goHome" hover-class="form-home-capsule--hover">
<text class="iconfont icon-shouye1 form-home-capsule__icon"></text>
</view>
</view>
<view class="form-home-navbar__title">{{ navbarTitle }}</view>
<view class="form-home-navbar__placeholder" />
</view>
<!-- #endif -->
<block v-if="detail">
<!-- 鸿蒙元服务:用原生 scroll-view 包裹表单,替代 CSS overflow 滚动(ArkUI 不识别 WebKit overflow 语义)。
根容器 harmony-root 占满 100vh 并 flex 列布局,navbar 占顶部固定高度,page flex:1 占剩余,scroll-view 在其内滚动。
提交按钮由 diy-form 内部渲染,处于 scroll-view 内,用户可滚到末尾点击。 -->
<!-- #ifdef MP-HARMONY -->
<view class="page harmony-page">
<scroll-view scroll-y="true" :show-scrollbar="false" class="form-scroll">
<view class="system-form-wrap">
<diy-form ref="form" :value="diyValue" dataMode="raw" :showSubmit="!showCaptcha" :onSubmit="handleSubmit" />
<!-- 行为设置:提交验证码开启时,隐藏 diy-form 内置按钮,由本页渲染验证码+提交按钮 -->
<view v-if="showCaptcha" class="form-extra-wrap">
<view class="captcha-wrap">
<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>
<view class="custom-submit" @click="submitFromPage">{{ submitBtnText }}</view>
</view>
</view>
</scroll-view>
</view>
<!-- #endif -->
<!-- #ifndef MP-HARMONY -->
<view class="page">
<view class="system-form-wrap">
<!-- 复用 diy-form:banner / tip / 表单字段 / 提交按钮 全部由其内部渲染,样式(bannerPaddingX/tipColor/submitBgColor 等)由 form_config 控制。
:onSubmit 接管提交逻辑(带 uniacid + showModal);跨 H5 / MP 一致,不依赖 $listeners。
注意:create 重度依赖本组件 this(id/uniacid/memberInfo/$api 等),必须在 created 阶段 bind this,
否则 diy-form 通过 this.onSubmit(formData) 调用时 this 会漂移到子组件实例 -->
<diy-form ref="form" :value="diyValue" dataMode="raw" :showSubmit="!showCaptcha" :onSubmit="handleSubmit" />
<!-- 行为设置:提交验证码开启时,隐藏 diy-form 内置按钮,由本页渲染验证码+提交按钮 -->
<view v-if="showCaptcha" class="form-extra-wrap">
<view class="captcha-wrap">
<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>
<view class="custom-submit" @click="submitFromPage">{{ submitBtnText }}</view>
</view>
</view>
</view>
<!-- #endif -->
</block>
<block v-else>
<ns-empty :text="emptyText" :isIndex="false"></ns-empty>
</block>
<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,
uniacid: 0,
isFromMpWebView: false, // 是否是从微信小程序的WebView跳转过来的
hideNavbar: false, // 查询参数中支持隐藏Navbar
showHomeBtn: false, // 查询参数 show_home=1 时,在系统标题栏左侧显示 Home 按钮
urlParams: {}, // 通用 URL 透传参数收集
handleSubmit: null, // create 的 this 绑定版本,作为 :onSubmit 传给 diy-form(见 created)
// 行为设置(后端 __form_config__.behavior 下发,含默认值)
behavior: {},
// 非成功状态的空态文案(表单未开放/需登录/加载失败等)
formError: '',
// 图形验证码(behavior.captcha=1 时展示,由本页渲染而非 diy-form)
captcha: { id: '', img: '', code: '' },
// 是否已加载过验证码(避免 onShow 重复刷新导致 session 错乱)
captchaLoaded: false,
// 登录后回跳地址(onLoad 时由原始 options 重建)
backUrl: '',
// 登录拦截时暂存待提交数据:组件内存(MP 不丢态)+ 本地存储(H5 登录整页回跳后从存储恢复)
pendingSubmit: null,
// #ifndef H5
navbarHeight: 44, // 自绘 navbar 内容区高度(不含状态栏),跨端兜底 44px
statusBarHeight: 20, // 系统状态栏高度,自绘 navbar 时需要预留顶部空隙
navbarTitle: '', // 自绘 navbar 中央显示的标题
hasBackStack: false, // 自绘 navbar 左上角是否显示返回箭头:页面栈 > 1 时显示
// #endif
// #ifdef H5
formTitle: '', // 查询参数传入的Title
enableSetTitle: false, // 查询参数传入的是否可以强制设置Title
// #endif
}
},
created() {
// 预绑定 this:create 重度依赖本组件 this(id/uniacid/memberInfo/$api/$lang/$util 等)。
// 若直接传 :onSubmit="create",diy-form 通过 this.onSubmit(formData) 调用时,
// 普通函数调用的 this 会指向调用者(子组件实例),导致字段全部读不到。
// bind 后无论子组件如何调用,this 始终是 formdata 实例。
this.handleSubmit = this.create.bind(this);
},
computed: {
// #ifndef H5
// 自绘 navbar 的高度样式
// - 内容区高度 = navbarHeight(兜底 44px)
// - 整体高度 = 状态栏高度 + 内容区高度
// - padding-top 把内容区下移到状态栏之下,避免被刘海/水滴遮挡
customNavbarStyle() {
try {
const sys = uni.getSystemInfoSync();
const sb = sys.statusBarHeight || 0;
const h = this.navbarHeight || 44;
return `height: ${sb + h}px; padding-top: ${sb}px;`;
} catch (e) {
return 'height: 64px; padding-top: 20px;';
}
},
// #endif
// 构造 diy-form raw 模式期望的 value 形态:
// - jsonData:透传给 ns-newform(已是 ns-newform 期望的 [{ controller, value, val }])
// - formConfig:后端返回 camelCase(如 formPaddingX / formBgColor / labelFontSize),
// 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 只在 <block v-if="detail"> 内被消费,无需 null 守卫
diyValue() {
return {
jsonData: this.detail.json_data,
formConfig: this.detail.form_config,
show_banner: this.detail.show_banner,
form_banner: this.detail.form_banner || 'public/uniapp/form/banner.png',
show_tip: this.detail.show_tip,
form_tip: this.detail.form_tip || this.$lang('form.formdata.formTipPlaceholder')
}
},
// 行为设置:提交验证码开启时 diy-form 隐藏内置提交按钮,本页渲染验证码 + 自有提交按钮
showCaptcha() {
return this.behavior.captcha == 1;
},
// 自有提交按钮文案:跟随表单配置的提交按钮文字(form_config 为 camelCase)
submitBtnText() {
return (this.detail && this.detail.form_config && this.detail.form_config.submitText) || this.$lang('common.submit');
},
// 空状态文案:提交成功 / 表单未开放 / 需登录 / 加载失败
emptyText() {
return this.complete ? this.$lang('form.formdata.submitSuccess')
: (this.formError || this.$lang('form.formdata.noFormInfo'));
}
},
onLoad(options) {
console.log("=====表单页面====", options)
if (!options.uniacid) {
uni.showModal({
title: this.$lang('form.formdata.tip'),
content: this.$lang('form.formdata.paramError'),
showCancel: false,
success: () => { }
})
return false
}
uni.setStorageSync('skipPrivacyCheck', true);
// #ifdef H5
this.formTitle = options?.title ?? '';
this.enableSetTitle = options?.enable_set_title == 1;
// #endif
this.uniacid = options.uniacid
this.id = options.id || 0
this.isFromMpWebView = options.from === 'mp-webview';
this.hideNavbar = this.$util.coerceBool(options?.hide_navbar);
// show_home=1 时在系统标题栏左侧显示 Home 按钮,点击回到首页 INDEX_PAGE_URL
// 解析规则统一走 this.$util.coerceBool:支持 '1' / 'true' / 1 / true 四种写法
// 仅在 navigationStyle:custom 端生效(APP / 微信小程序 / 华为快应用 / 鸿蒙元服务);
// H5 保留系统原生 navbar,不支持 show_home,参数被忽略(这是预期行为)
// 注:H5 嵌入微信小程序 webview(from=mp-webview)的场景也属于 H5 范畴,
// 此场景下也没有 Home 入口——后续如需支持,需要单独处理(导航靠宿主小程序接管)
this.showHomeBtn = this.$util.coerceBool(options?.show_home);
// #ifndef H5
// 自绘 navbar 需要用到状态栏高度 / navbar 内容区高度(跨端兜底)
try {
const sys = uni.getSystemInfoSync();
if (sys.statusBarHeight) this.statusBarHeight = sys.statusBarHeight;
// uniapp 在不同端 menuButton / navigationBar 高度略有差异,给个安全兜底即可
if (typeof sys.navbarHeight === 'number' && sys.navbarHeight > 0) {
this.navbarHeight = sys.navbarHeight;
}
} catch (e) { /* keep defaults */ }
// 自绘 navbar 是否显示返回箭头:getCurrentPages().length > 1 即页面有栈
try {
const pages = getCurrentPages ? getCurrentPages() : [];
this.hasBackStack = Array.isArray(pages) && pages.length > 1;
} catch (e) { /* keep default */ }
// #endif
// 通用 URL 参数收集:除业务保留字段外,其余全部透传给后端
// 后端会做卫生过滤并广播事件,供 recommend 等插件消费
const reservedKeys = ['uniacid', 'id', 'scene', 'from', 'hide_navbar', 'show_home', 'title', 'enable_set_title'];
this.urlParams = {};
for (let key in options) {
if (reservedKeys.indexOf(key) !== -1) continue;
this.urlParams[key] = options[key];
}
if (options.scene) {
const scene = decodeURIComponent(options.scene)
const params = scene.split('&')
params.length && params.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];
}
})
}
// #ifdef H5
if (this.isFromMpWebView || this.hideNavbar) {
this.$nextTick(() => {
const navbar = document.querySelector('uni-page-head')
if (navbar) navbar.style.display = 'none'
})
}
// #endif
// 登录后回跳地址:由原始 options 重建(供 FORM_LOGIN_REQUIRED 时 ns-login 使用)
const backQuery = [];
for (let key in options) {
if (key === 'scene') continue;
backQuery.push(key + '=' + encodeURIComponent(options[key]));
}
this.backUrl = '/pages_tool/form/formdata' + (backQuery.length ? '?' + backQuery.join('&') : '');
this.getData()
},
watch: {
storeToken(newVal, oldVal) {
if (newVal) {
this.getData();
// 填写需登录:弹窗登录成功后自动续提交
this.resumeSubmitAfterLogin();
}
}
},
onShow() {
this.updateNavbarTitle();
},
methods: {
// #ifndef H5
// 返回上一页;用 navigateBack,栈深度>1 时由 hasBackStack 控制按钮可见性
goBack() {
uni.navigateBack({ delta: 1 });
},
// #endif
// 返回首页;
// INDEX_PAGE_URL 是 TabBar 根页面,不能用裸 uni.redirectTo(会报"can not redirectTo a tabBar page")
// 必须用 this.$util.redirectTo,内部对 TabBar 页面自动走 uni.switchTab
// (鸿蒙元服务下还会用 reLaunch 兜底,见 common/js/util.js)
// H5 端保留 show_home / goHome(虽然模板里不渲染自绘 navbar,但保留方法调用不报错)
goHome() {
this.$util.redirectTo(this.$util.INDEX_PAGE_URL, {}, 'redirectTo');
},
getData() {
this.$api.sendRequest({
url: '/form/api/form/info',
data: {
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()
}
})
},
updateNavbarTitle() {
if (!this.detail?.form_name) return;
let formName = this.detail.form_name;
// #ifdef H5
if (this.enableSetTitle && this.formTitle) {
formName = this.formTitle;
}
// #endif
// H5 端:系统原生 navbar —— uni.setNavigationBarTitle 改原生标题
// #ifdef H5
uni.setNavigationBarTitle({ title: formName });
// #endif
// 非 H5 端:custom 模式下没有原生 navbar,直接写 data.navbarTitle,
// 由模板里的 .form-home-navbar__title 显示
// #ifndef H5
this.navbarTitle = formName;
// #endif
},
// 图形验证码(点击图片/按钮刷新;提交失败后自动刷新)
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 payload = {
formId: this.id,
uniacid: this.uniacid,
urlParams: this.urlParams,
formData: formData,
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 确保 diy-form 内部 ns-newform 已挂载),避免 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(pending.formData)
},
// 将保存的 formData 回填到 diy-form 内部的 ns-newform
restoreFormData(formData) {
try {
const ns = this.$refs.form && this.$refs.form.$refs && this.$refs.form.$refs.nsNewform
if (ns && Array.isArray(formData)) {
ns.formData = JSON.parse(JSON.stringify(formData))
if (typeof ns.processFormData === 'function') ns.processFormData()
}
} catch (e) {}
},
// 验证码模式下本页自有提交按钮:先走 diy-form 校验与取数,再进 create
submitFromPage() {
if (this.isRepeat) return;
if (!this.$refs.form || !this.$refs.form.verify()) return;
const formData = this.$refs.form.getFormData();
this.create(formData);
},
// 填完跳转:消费统一链接选择器的完整对象(wap_url 外链/页面路径、外部小程序、拨打电话)
doRedirect(link) {
const fallback = () => { this.complete = true; this.detail = null; };
if (!link) return fallback();
if (link.appid) {
// 跳转其他微信小程序(仅小程序端可用;H5 场景直接回退完成态)
// #ifdef MP-WEIXIN
wx.navigateToMiniProgram({ appId: link.appid, path: link.page || '', fail: fallback });
return;
// #endif
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();
}
},
// 由 diy-form 内置提交按钮触发:diy-form handleSubmit 已先做 verify(),
// 仅在通过时才通过 :onSubmit 函数 prop 调用本方法(跨 H5 / MP 一致,不走 $emit / $listeners)。
// 注意:本方法通过 created 中 this.handleSubmit = this.create.bind(this) 预绑定 this,
// 因此内部所有 this.xxx 都指向 formdata 实例,不会漂移到子组件。
create(formData) {
// 防重复提交:console 在拦截之前,避免日志噪声;拦截后立即置位
if (this.isRepeat) return;
const behavior = this.behavior || {};
// 填写需登录 / 限填(后端强制,前端提前拦截优化体验)
if ((behavior.require_login_submit == 1 || behavior.submit_limit > 0) && !this.storeToken) {
// 保存已填写数据,登录成功(MP 弹窗 / H5 回跳)后自动续提交,避免数据丢失
this.savePendingSubmit(formData);
this.$refs.login && this.$refs.login.open(this.backUrl);
return;
}
// 验证码必填
if (behavior.captcha == 1 && !this.captcha.code) {
this.$util.showToast({ title: this.$lang('form.form.captchaRequired') });
return;
}
this.isRepeat = true;
console.log('[FORMDATA-SUBMIT-CAPTCHA]=========================验证码=========================');
const submitCaptchaId = String(this.captcha.id || '');
const submitCaptchaCode = String(this.captcha.code || '').toUpperCase();
console.log('[FORMDATA-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 formData === 'string' ? formData : JSON.stringify(formData),
uniacid: this.uniacid,
url_params: JSON.stringify(this.urlParams),
member_id: this.memberInfo ? this.memberInfo.id : 0,
captcha_id: submitCaptchaId,
captcha_code: submitCaptchaCode
},
success: res => {
this.isRepeat = false;
if (res.code == 0) {
const link = behavior.redirect;
if (link) {
this.$util.showToast({ title: this.$lang('form.formdata.submitSuccess') });
setTimeout(() => this.doRedirect(link), 800);
} else {
this.complete = true
uni.showModal({
title: this.$lang('form.formdata.tip'),
content: this.$lang('form.formdata.submitSuccess'),
showCancel: false,
success: () => { }
})
}
} else {
if (res.message == 'FORM_LOGIN_REQUIRED') {
// 提交时后端仍要求登录(如登录态过期):暂存数据,重登后自动续提交
this.savePendingSubmit(formData)
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()
} else if (res.message == 'FORM_SUBMIT_LIMIT') {
this.$util.showToast({ title: this.$lang('form.form.submitLimit') })
this.complete = true
} else if (res.message == 'FORM_NOT_OPEN') {
this.$util.showToast({ title: this.$lang('form.form.notOpen') })
} else {
this.$util.showToast({
title: res.message
})
}
}
},
fail: () => {
// 网络失败时复位防重复标记,允许用户重试
this.isRepeat = false
}
})
}
}
}
</script>
<style lang="scss" scoped>
/*
* 自绘 navbar(非 H5 端:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
* - pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
* - 整体高度由 inline style 注入:statusBarHeight + 44px
* - 左侧 Home 胶囊 + 中央标题 + 右侧占位,标题过长自动省略
*/
/* #ifndef H5 */
/* --- 自绘 navbar 容器 --- */
.form-home-navbar {
position: relative;
width: 100%;
box-sizing: border-box;
background: #ffffff;
/* 高度由 inline style 通过 customNavbarStyle 注入:statusBarHeight + 44px */
/* padding-top 把内容区下移到状态栏之下,避免被刘海/水滴遮挡 */
/* 浮在表单内容之上,避免被 diy-form 等组件覆盖 */
z-index: 100;
/* 让内容靠 flex 自动落到 padding-top 之下的区域 */
display: flex;
flex-direction: row;
align-items: center;
}
.form-home-navbar__left {
/* 不再用 absolute,跟着 navbar 的 padding-top 下移 */
flex: none;
/* 宽度自适应:返回按钮 56rpx + Home 胶囊 56rpx + 间距 12rpx + 左 padding 16rpx = 最多 ~140rpx */
min-width: 88rpx;
max-width: 200rpx;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 16rpx;
box-sizing: border-box;
gap: 12rpx;
}
.form-home-navbar__back {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
}
.form-home-navbar__back-icon {
font-size: 48rpx;
line-height: 1;
color: #000;
font-weight: 300;
}
.form-home-navbar__btn--hover {
background: rgba(0, 0, 0, 0.06);
}
.form-home-navbar__title {
flex: 1;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 32rpx;
color: #000;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
.form-home-navbar__placeholder {
/* 与 __left 对称,保持标题视觉居中 */
flex: none;
min-width: 88rpx;
max-width: 200rpx;
}
/* --- Home 胶囊主体 --- */
.form-home-capsule {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
}
.form-home-capsule--hover {
background: rgba(0, 0, 0, 0.06);
}
/*
* Home 图标 —— 使用项目统一的 iconfont icon-shouye1(unicode \e02d)
* 与 pages_goods/detail.vue 的首页按钮共用同一个图标,跨端视觉一致
*/
.form-home-capsule__icon {
font-size: 32rpx;
line-height: 1;
color: #000;
display: block;
}
/* #endif */
</style>
<style lang="scss" scoped>
page {
background: #fff;
}
// ===== 非鸿蒙端(H5 / 微信小程序 / App):保留原有 WebKit 滚动 / 安全区处理 =====
// 鸿蒙走下方 MP-HARMONY 专属的 scroll-view 方案,不编译这些 WebKit 属性
/* #ifdef H5 || MP-WEIXIN || APP-PLUS */
.page {
min-height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.system-form-wrap {
overflow: visible;
margin: 0 0 60rpx 0;
padding-bottom: env(safe-area-inset-bottom, 40rpx);
}
/* #endif */
// ===== 鸿蒙元服务:用原生 scroll-view 做滚动容器 =====
// ArkUI 不识别 WebKit 的 overflow / -webkit-overflow-scrolling,必须用 scroll-view 组件(编译为 ArkUI Scroll)。
// 根容器 harmony-root 占满 100vh 并 flex 列布局,navbar 占顶部固定高度,page flex:1 占剩余空间,
// scroll-view 在 page 内 flex:1 获得确定高度后内部滚动。提交按钮由 diy-form 渲染在 scroll-view 内,可滚动到。
/* #ifdef MP-HARMONY */
.harmony-root {
height: 100vh;
display: flex;
flex-direction: column;
}
.harmony-page {
flex: 1;
min-height: 0; // 允许内容收缩,内部 scroll-view 才能拿到确定高度并正确滚动
display: flex;
flex-direction: column;
}
.form-scroll {
flex: 1;
height: 100%;
}
.system-form-wrap {
// 不再用 overflow:hidden 裁剪;提交按钮在 scroll-view 内,滚动可见
overflow: visible;
margin: 0 0 60rpx 0;
padding-bottom: 40rpx; // 鸿蒙不识别 env(safe-area-inset-bottom),固定值兜底
}
/* #endif */
// 穿透 diy-form 包装层:消除其自带的 10rpx 圆角与白色背景,
// 让 form 内容区域保持与原始 ns-newform 接入时一致的视觉
.system-form-wrap ::v-deep .diy-from {
background: transparent;
border-radius: 0;
}
// ===== 行为设置:验证码行 + 自有提交按钮(diy-form 内置按钮被 showSubmit=false 隐藏时接管) =====
.form-extra-wrap {
padding: 0 28rpx 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;
}
}
// 提交按钮:视觉对齐 diy-form 内置按钮(红底白字圆角,medium 内边距)
.custom-submit {
margin-top: 30rpx;
padding: 24rpx 0;
text-align: center;
color: #ffffff;
background-color: #ff4544;
border-radius: 12rpx;
font-size: 30rpx;
}
::v-deep .mescroll-totop {
/* #ifdef H5 */
right: 27rpx !important;
bottom: 120rpx !important;
/* #endif */
/* #ifdef MP */
right: 27rpx !important;
bottom: 180rpx !important;
/* #endif */
}
</style>