This commit is contained in:
2026-09-01 16:29:29 +08:00
4 changed files with 220 additions and 25 deletions

View File

@@ -6,24 +6,44 @@
否则会触发 MP 编译器注入的 propsToData 观察者(对应编译产物里
r=function(){var o=this.value.show_banner...this.$mp.data...})
报 "Cannot read properties of undefined (reading 'show_banner')"。 -->
<view v-if="value && value.show_banner && value.form_banner" class="form-banner" :style="bannerStyle">
<view v-if="value && value.show_banner && value.form_banner" class="form-banner is-card-overlap" :style="bannerStyle + bannerBleedStyle">
<image :src="$util.img(value.form_banner)" mode="widthFix" class="banner-img" />
</view>
<!-- 提示语 -->
<view v-if="value && value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value && value.form_tip }}</view>
<!-- 表单组件 -->
<ns-newform
:data="formComponents"
:customAttr="value"
:mode="mergedFormConfig.formLayout || 'vertical'"
:formConfig="mergedFormConfig"
ref="nsNewform"
></ns-newform>
<!-- 提交按钮(外层 = form-bg + form-padding-x;按钮 = submit-bg + submit-color + input-height + input-radius)。
showSubmit=false 时由父级页面渲染自己的提交按钮并通过 ref 调用本组件的 verify()/getFormData()。 -->
<view v-if="showSubmit" class="form-submit-wrap" :style="submitWrapStyle">
<view @click.stop="handleSubmit" class="form-submit" :style="submitStyle">{{ submitText }}</view>
<!-- 卡片容器:仅在后台配置了 banner 图片时启用,让表单浮在 banner 之上(参考左图布局)。
无 banner 时不渲染 .form-card,保持原有平铺行为,零外溢。 -->
<view v-if="hasBannerMode" class="form-card" :style="cardWrapStyle">
<!-- 提示语 -->
<view v-if="value && value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value && value.form_tip }}</view>
<!-- 表单组件 -->
<ns-newform
:data="formComponents"
:customAttr="value"
:mode="mergedFormConfig.formLayout || 'vertical'"
:formConfig="mergedFormConfig"
ref="nsNewform"
></ns-newform>
<!-- 提交按钮(外层 = form-bg + form-padding-x;按钮 = submit-bg + submit-color + input-height + input-radius)。
showSubmit=false 时由父级页面渲染自己的提交按钮并通过 ref 调用本组件的 verify()/getFormData()。 -->
<view v-if="showSubmit" class="form-submit-wrap" :style="submitWrapStyle">
<view @click.stop="handleSubmit" class="form-submit" :style="submitStyle">{{ submitText }}</view>
</view>
</view>
<!-- 无 banner 时仍按旧布局直接渲染 tip / ns-newform / submit,避免影响其他使用方(diy-group / diy-article 等) -->
<template v-else>
<!-- 提示语 -->
<view v-if="value && value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value && value.form_tip }}</view>
<!-- 表单组件 -->
<ns-newform
:data="formComponents"
:customAttr="value"
:mode="mergedFormConfig.formLayout || 'vertical'"
:formConfig="mergedFormConfig"
ref="nsNewform"
></ns-newform>
<view v-if="showSubmit" class="form-submit-wrap" :style="submitWrapStyle">
<view @click.stop="handleSubmit" class="form-submit" :style="submitStyle">{{ submitText }}</view>
</view>
</template>
</view>
</template>
@@ -58,6 +78,14 @@ export default {
type: Boolean,
default: true
},
// 父级容器左右内边距(rpx)。卡片模式下让 banner 图片突破这个内边距左右铺满(如 system-form-wrap 的 padding-x)。
// 仅在 hasBannerMode=true 时生效;默认 0(不铺满)。
// 引入原因:formdata.vue 的 .system-form-wrap 默认 padding: 0 50rpx,banner 图片被左右各 50rpx 收缩,
// 后台配 form_banner 后图片应跨越屏幕宽度,而不是被容器内缩。
containerInset: {
type: Number,
default: 0
},
// 父级可选地接管提交:传入函数 prop 时,verify 通过后由它处理 formData;
// 不传时走组件内置 defaultSubmit(兜底)。default 必须是 null,不能写成函数返回 null
onSubmit: {
@@ -108,6 +136,52 @@ export default {
padding: `0 ${this._num(this.mergedFormConfig && this.mergedFormConfig.bannerPaddingX, 0)}rpx`
});
},
// 后台配了 banner 图片时启用卡片浮起布局(与参考设计左图一致);无 banner 走原平铺,零外溢
hasBannerMode() {
const v = this.value || {};
return !!(v.show_banner && v.form_banner);
},
// 卡片容器内联样式:maring-top(负偏移让卡片向上压住 banner 底部)+ border-radius + padding-x + box-shadow + background。
// 数值由 formConfig 走 camelCase/snake_case 双读,缺省走默认值,使后端零配置时也能看到卡片浮起效果。
// 关键:卡片必须自带白底 + z-index(见 SCSS .form-card)—— 只有卡片自己的背景才能"盖住"banner 图片底部,
// 父级容器(如 formdata.vue 的 .system-form-wrap)是 banner 的祖先,其背景会被 banner 覆盖,无法形成浮起效果。
cardWrapStyle() {
const cfg = this.mergedFormConfig || {};
// 父级传了 containerInset(即 formdata.vue 场景),外层 system-form-wrap 已给左右 padding-x,这里默认 0 不重复
// 其他场景(diy-group / diy-article 等未传 containerInset),回退到 bannerPaddingX 或 28rpx 保证卡片模式视觉正常
const parentInset = this._num(this.containerInset, 0);
const defaultSidePad = parentInset > 0 ? 0 : this._num(cfg.bannerPaddingX, 28);
const sidePad = this._num(cfg.cardSidePad, defaultSidePad);
// overlap:负值,让卡片向上"压"住 banner 底部(与设计图一致)
const overlap = -Math.abs(this._num(cfg.cardOverlap, 48));
// 卡片圆角:缺省 24rpx(浮起卡片的圆角,比外层卡片的 16rpx 更明显,突出浮起感)
const radius = this._num(cfg.cardRadius, 24);
// 阴影:缺省中等阴影(与设计方案一致)—— 由本容器承担浮起感
const shadow = cfg.cardShadow || '0 8rpx 24rpx rgba(0,0,0,0.08)';
// 卡片底色:纯白,与设计方案一致;可由后台 formConfig.cardBgColor 覆盖
const bgColor = cfg.cardBgColor || '#ffffff';
return this.$util.objToStyle({
marginTop: overlap + 'rpx',
paddingLeft: sidePad + 'rpx',
paddingRight: sidePad + 'rpx',
borderRadius: radius + 'rpx',
boxShadow: shadow,
backgroundColor: bgColor
});
},
// Banner 容器破壳样式:仅在卡片模式下 + 父级传了 containerInset>0 时启用,让 banner 突破父容器 padding 跨越屏幕宽度。
// 计算逻辑:负 margin-left/right + 等量扩宽 width,使 banner 元素左/右边界对齐到屏幕边缘。
// 父级未传 containerInset 或传 0 时返回空样式,banner 保持原尺寸(零外溢)。
bannerBleedStyle() {
const inset = this._num(this.containerInset, 0);
if (!this.hasBannerMode || inset <= 0) return '';
const expand = inset * 2;
return this.$util.objToStyle({
marginLeft: -inset + 'rpx',
marginRight: -inset + 'rpx',
width: 'calc(100% + ' + expand + 'rpx)'
});
},
// 提示语样式:padding-x、font-size、color、bg-color、border-bottom(与后端 .form-preview-tip 对齐)
tipStyle() {
const cfg = this.mergedFormConfig || {};
@@ -229,7 +303,13 @@ export default {
tipFontSize: pick('tipFontSize', 'tip_font_size'),
tipColor: pick('tipColor', 'tip_color'),
tipBgColor: pick('tipBgColor', 'tip_bg_color'),
tipBorderColor: pick('tipBorderColor', 'tip_border_color')
tipBorderColor: pick('tipBorderColor', 'tip_border_color'),
// 卡片浮起(仅 show_banner && form_banner 启用)
cardOverlap: pick('cardOverlap', 'card_overlap'),
cardRadius: pick('cardRadius', 'card_radius'),
cardShadow: pick('cardShadow', 'card_shadow'),
cardSidePad: pick('cardSidePad', 'card_side_pad'),
cardBgColor: pick('cardBgColor', 'card_bg_color')
};
// 移除 undefined / 空字符串(保留 0/false)
Object.keys(base).forEach(function(k) {
@@ -392,12 +472,37 @@ export default {
// 默认左右无边距(与后端 --banner-padding-x 对齐;由内联 bannerStyle 覆盖)
margin: 0;
// banner 图片作为卡片上方的"底图":建立层叠上下文,让 .form-card 通过 z-index 浮在其上。
// 仅在卡片浮起模式(.is-card-overlap)启用,不影响其它使用方(diy-group 等)的旧布局。
&.is-card-overlap {
position: relative;
z-index: 1;
}
.banner-img {
width: 100%;
display: block;
}
}
// 卡片容器:浮在 banner 图片上方(参考设计左图)。
// 仅在 hasBannerMode=true 时渲染(见 template v-if);
// 阴影 + margin-top(负偏移)+ 圆角 + 白底 全部由 cardWrapStyle 内联写入(读取后台 formConfig.card*)。
// 关键:卡片必须自带白底并 z-index 高于 banner,才能真正"盖住"banner 图片底部形成浮起;
// 父级容器(formdata.vue 的 .system-form-wrap)虽也是白底,但它是 banner 的祖先、背景会被 banner 覆盖,
// 因此本容器的白底不可省略(与 .system-form-wrap 同色,视觉上从卡片向下自然延续到页面底部)。
.form-card {
// 定位上下文 + 高于 banner 的层级:让卡片盖住 banner 底部(banner z-index:1)
position: relative;
z-index: 2;
// 兜底白底 / 圆角 / 阴影:与后端未配置时内联写入的默认值一致;后台配置时由内联 cardWrapStyle 覆盖
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
// 兜底 padding-bottom:让卡片与后面内容(如推荐码、其它 diy 组件)有间距
padding-bottom: 24rpx;
}
.form-tip {
// 提示语:上下内边距 / 左右内边距 / 字号 / 颜色 / 背景 全部由 tipStyle 内联写入
padding: 0;

View File

@@ -137,8 +137,8 @@ export default {
},
computed: {
warpCss() {
var obj = '';
obj += 'background-color:' + this.value.componentBgColor + ';';
var obj = '';
obj += 'background-color:' + this.value.componentBgColor + ';';
if (this.value.componentAngle == 'round') {
obj += 'border-top-left-radius:' + this.value.topAroundRadius * 2 + 'rpx;';
obj += 'border-top-right-radius:' + this.value.topAroundRadius * 2 + 'rpx;';

View File

@@ -120,7 +120,7 @@
</view>
</view>
<!-- 分享推荐链接按钮:深蓝渐变,左侧图标 + 文字,右侧“复制链接”小按钮(独立于白色卡片外) -->
<button v-if="stylePreset === 'gradient' && value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:24rpx auto 0 auto;width:calc(100% - 28rpx);text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
<button v-if="stylePreset === 'gradient' && value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:24rpx auto 20rpx auto;width:calc(100% - 28rpx);text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
<view style="display:flex;align-items:center;justify-content:space-between;width:100%;height:100rpx;background:linear-gradient(90deg,#3B82F6 0%,#1E40AF 100%);border-radius:20rpx;padding:0 24rpx;box-shadow:0 4rpx 16rpx rgba(29,78,216,0.35);box-sizing:border-box;">
<view style="display:flex;align-items:center;gap:12rpx;">
<text style="font-size:36rpx;color:#FFFFFF;">📤</text>
@@ -131,7 +131,7 @@
</view>
</view>
</button>
<view v-else-if="stylePreset === 'gradient' && value.showShareButton !== false" @tap="onShareClick" style="margin:24rpx auto 0 auto;width:calc(100% - 28rpx);">
<view v-else-if="stylePreset === 'gradient' && value.showShareButton !== false" @tap="onShareClick" style="margin:24rpx auto 20rpx auto;width:calc(100% - 28rpx);">
<view style="display:flex;align-items:center;justify-content:space-between;width:100%;height:100rpx;background:linear-gradient(90deg,#3B82F6 0%,#1E40AF 100%);border-radius:20rpx;padding:0 24rpx;box-shadow:0 4rpx 16rpx rgba(29,78,216,0.35);box-sizing:border-box;">
<view style="display:flex;align-items:center;gap:12rpx;">
<text style="font-size:36rpx;color:#FFFFFF;">📤</text>

View File

@@ -5,6 +5,10 @@
<!-- #ifndef MP-HARMONY -->
<view :style="themeColor">
<!-- #endif -->
<!-- 顶部图条:位于 navbar 之下、表单卡片之上。宽度 calc(100% - 2*containerInset) 水平居中,
留下与卡片等宽的左右对称留白(默认容器内缩 50rpx ≈ 屏宽 6.7%,即内容宽 87%)。
图片由后台 form_config.bg_image / bgImage 下发;未配置时不渲染,保持原来的纯白布局。 -->
<view v-if="bgImage" class="formdata-bg" :style="bgLayerStyle"></view>
<!--
自绘导航栏(仅在 navigationStyle:custom 的端渲染:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
- pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
@@ -34,8 +38,8 @@
<!-- #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" />
<view class="system-form-wrap" :style="containerStyle">
<diy-form ref="form" :value="diyValue" dataMode="raw" :showSubmit="!showCaptcha" :onSubmit="handleSubmit" :container-inset="containerInset" />
<!-- 行为设置:提交验证码开启时,隐藏 diy-form 内置按钮,由本页渲染验证码+提交按钮 -->
<view v-if="showCaptcha" class="form-extra-wrap">
<view class="captcha-wrap">
@@ -56,12 +60,12 @@
<!-- #endif -->
<!-- #ifndef MP-HARMONY -->
<view class="page">
<view class="system-form-wrap">
<view class="system-form-wrap" :style="containerStyle">
<!-- 复用 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 ref="form" :value="diyValue" dataMode="raw" :showSubmit="!showCaptcha" :onSubmit="handleSubmit" :container-inset="containerInset" />
<!-- 行为设置:提交验证码开启时,隐藏 diy-form 内置按钮,由本页渲染验证码+提交按钮 -->
<view v-if="showCaptcha" class="form-extra-wrap">
<view class="captcha-wrap">
@@ -210,12 +214,60 @@ export default {
return {
jsonData: this.detail.json_data,
formConfig: Object.assign({}, this.detail.form_config || {}, styleDefaults),
show_banner: this.detail.show_banner,
// 全屏底图模式下隐藏卡片内 banner,避免与底图重复(见 hideInnerBanner)
show_banner: this.hideInnerBanner ? 0 : 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')
}
},
// ===== 容器左右内缩(默认开启,不依赖 bgImage) =====
// 图条图片:后台 form_config.bg_image / bgImage 下发(兼容 snake_case / camelCase)。
// 未配置时不渲染图条,但表单卡片本身已有内缩效果(见 .system-form-wrap 默认 padding)
bgImage() {
if (!this.detail) return '';
const fc = this.detail.form_config || {};
const raw = fc.bgImage || fc.bg_image || fc.backgroundImage || fc.background_image || '';
return raw ? this.$util.img(raw) : '';
},
// 容器左右内缩(rpx)。后台 form_config.container_inset / containerInset 可调;
// 默认 50rpx,在 750rpx 屏宽下左右各留 50rpx ≈ 6.7%,内容宽 ≈ 87%,
// 与参考图一致;CSS .system-form-wrap 中也写了这个默认 padding(防 H5 computed 未及时就绪时也生效)
containerInset() {
const fc = (this.detail && this.detail.form_config) || {};
const raw = fc.containerInset !== undefined ? fc.containerInset : fc.container_inset;
const n = parseInt(raw);
return isNaN(n) ? 50 : Math.min(150, Math.max(0, n));
},
// 顶部图条高度(rpx)。后台 form_config.bg_image_height 可调;clamp 80-600
bgImageHeight() {
const fc = (this.detail && this.detail.form_config) || {};
const raw = fc.bgImageHeight !== undefined ? fc.bgImageHeight : fc.bg_image_height;
const n = parseInt(raw);
return isNaN(n) ? 200 : Math.min(600, Math.max(80, n));
},
// 图条与表单卡片之间的间距(rpx)。后台 form_config.card_gap 可调;clamp 0-80
cardGap() {
const fc = (this.detail && this.detail.form_config) || {};
const raw = fc.cardGap !== undefined ? fc.cardGap : fc.card_gap;
const n = parseInt(raw);
return isNaN(n) ? 24 : Math.min(80, Math.max(0, n));
},
// 图条内联样式:水平居中(width=100%-2*inset + 左右各 inset 边距),高度由配置控制
bgLayerStyle() {
const inset = this.containerInset;
return 'width:calc(100% - ' + (inset * 2) + 'rpx);margin:0 ' + inset + 'rpx;height:' + this.bgImageHeight + 'rpx;background-image:url(' + this.bgImage + ');';
},
// 表单容器内联样式:覆盖 .system-form-wrap 默认 padding,使后台可配。始终返回有效样式(含默认值),
// 不再依赖 bgImage 开关 —— 表单默认就有左右内缩居中(不需后台任何配置)
containerStyle() {
return 'padding-left:' + this.containerInset + 'rpx;padding-right:' + this.containerInset + 'rpx;';
},
// 图条模式下隐藏 diy-form 卡片顶部的 banner,避免与图条重复
// 如需保留卡片内 banner:把下面这行改成 `return false;` 即可
hideInnerBanner() {
return !!this.bgImage;
},
// 行为设置:提交验证码开启时 diy-form 隐藏内置提交按钮,本页渲染验证码 + 自有提交按钮
showCaptcha() {
return this.behavior.captcha == 1;
@@ -905,16 +957,40 @@ page {
// ===== 非鸿蒙端(H5 / 微信小程序 / App):保留原有 WebKit 滚动 / 安全区处理 =====
// 鸿蒙走下方 MP-HARMONY 专属的 scroll-view 方案,不编译这些 WebKit 属性
/*
* ===== 顶部图条 =====
* 位于 navbar 之下、表单卡片之上。宽度由内联 bgLayerStyle 注入(calc(100% - 2*inset)),
* 与下方表单卡片等宽、左右各留 inset 对称留白。圆角 + overflow 防止图片溢出。
* 未配置 form_config.bg_image 时该节点不渲染(v-if="bgImage"),页面保持原来的纯白样式。
*/
.formdata-bg {
position: relative;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 16rpx;
overflow: hidden;
box-sizing: border-box;
}
/* #ifdef H5 || MP-WEIXIN || APP-PLUS */
.page {
min-height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
/* 页面容器不再垫白底 —— 只有红色框区域(.system-form-wrap 表单卡片)保留白底,
图条上下、左右留白等区域透出页面默认底色 */
}
.system-form-wrap {
overflow: visible;
margin: 0 0 60rpx 0;
/* ===== 表单卡片:唯一保留白底的区域 =====
87% 宽居中 + 16rpx 圆角 + 白底,对应红色框范围(从图条底部到页面底部) */
padding: 0 50rpx;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 16rpx;
padding-bottom: env(safe-area-inset-bottom, 40rpx);
}
/* #endif */
@@ -934,6 +1010,7 @@ page {
min-height: 0; // 允许内容收缩,内部 scroll-view 才能拿到确定高度并正确滚动
display: flex;
flex-direction: column;
/* 鸿蒙端与 .page 保持一致:去掉白底,只有 .system-form-wrap 是白底卡片 */
}
.form-scroll {
flex: 1;
@@ -943,10 +1020,23 @@ page {
// 不再用 overflow:hidden 裁剪;提交按钮在 scroll-view 内,滚动可见
overflow: visible;
margin: 0 0 60rpx 0;
// ===== 表单卡片:默认就是「白底圆角、左右内缩居中」(参考图效果),不依赖后台配置 =====
padding: 0 50rpx;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 16rpx;
padding-bottom: 40rpx; // 鸿蒙不识别 env(safe-area-inset-bottom),固定值兜底
}
/* #endif */
/*
* ===== .form-card 兼容占位(旧模板/旧调用残留) =====
* 旧实现用 .form-card 做条件类加强化,现已将白底/圆角合并到 .system-form-wrap 默认样式中。
* 保留此类名是为了不破坏潜在的 :class="formCardClass" 引用(已从模板移除,但旧页面缓存可能用到);
* 这里只做 noop,不影响任何视觉。
*/
.form-card {}
// 穿透 diy-form 包装层:消除其自带的 10rpx 圆角与白色背景,
// 让 form 内容区域保持与原始 ns-newform 接入时一致的视觉
.system-form-wrap ::v-deep .diy-from {