This commit is contained in:
2026-09-05 10:55:36 +08:00

View File

@@ -6,12 +6,17 @@
否则会触发 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 is-card-overlap" :style="bannerStyle + bannerBleedStyle">
<view v-if="value && value.show_banner && value.form_banner" class="form-banner is-card-overlap" :style="bannerStyle + bannerBleedStyle">
<!-- Banner 图片高度策略(与商家端预览 .preview-banner height:auto 对齐):
默认 mode="widthFix":宽度撑满、高度按图片原始比例自适应,图片完整显示不裁剪;
仅显式配置 formConfig.bannerHeight / banner_height 时才固定高度 + aspectFill 居中裁剪。 -->
<view class="banner-img-wrap" :class="{ 'is-fixed-height': hasBannerHeight }" :style="bannerImgWrapStyle">
<image :src="$util.img(value.form_banner)" :mode="bannerImgMode" class="banner-img" />
默认 mode="widthFix":宽度撑满、高度按图片原始比例自适应,图片完整显示不裁剪;
显式配置 bannerHeight/banner_height 或 bannerCropRatio → 切 aspectFill 居中裁剪;
显式配置 bannerCropBox → 切 widthFix + image 内联 transform translate/缩放(手动裁栏框);
裁剪位置(bannerCropPosition):uni-app image 不支持 object-position,
通过 .banner-img-crop 容器的 transform translateY 控制(top/居中/底部)。 -->
<view class="banner-img-wrap" :class="bannerImgWrapClass" :style="bannerImgWrapStyle">
<view class="banner-img-crop" :class="'crop-' + bannerCropPosition">
<image :src="$util.img(value.form_banner)" :mode="bannerImgMode" class="banner-img" :style="bannerImgStyle" />
</view>
</view>
</view>
<!-- 卡片浮起模式:tip / 提交按钮位置可配(仅 card 风格生效)
@@ -71,6 +76,9 @@
import DiyMinx from './minx.js'
import NsNewform from '@/components/ns-newform/ns-newform.vue'
// 横幅裁剪比例白名单(与后端 form.js syncFormConfigToCSS 保持一致)
const BANNER_CROP_RATIOS = { '16:9': 16/9, '4:3': 4/3, '1:1': 1, '3:4': 3/4 }
export default {
name: 'diy-from',
components: { NsNewform },
@@ -154,7 +162,9 @@ export default {
// 商家端预览 .preview-banner 是 max-width:100%; height:auto —— 按图片原始比例完整显示。
// 此前 H5 固定 180rpx + aspectFill 居中裁剪,导致横幅高度不一/图片内容被裁缺失。
// 现在:无显式配置 → widthFix 自适应(与后端 height:auto 等价);
// 显式配置 bannerHeight / banner_height → 固定高度 + aspectFill 裁剪(用户主动选择)。
// 显式配置 bannerHeight / banner_height → 固定高度 + aspectFill 裁剪(用户主动选择);
// 显式配置 bannerCropRatio → 按比例锁高度,比例+高度都设时高度优先(保持数字 rpx 的语义);
// 显式配置 bannerCropBox → 手动裁栏框(widthFix + image transform,最高优先级,独享遮罩)。
hasBannerHeight() {
const cfg = this.mergedFormConfig || {};
const raw = cfg.bannerHeight !== undefined ? cfg.bannerHeight : cfg.banner_height;
@@ -162,7 +172,9 @@ export default {
return !isNaN(n) && n > 0;
},
bannerImgMode() {
return this.hasBannerHeight ? 'aspectFill' : 'widthFix';
// 手动裁栏框优先(widthFix + transform);其次 任一启用裁剪(高度或比例)切 aspectFill;否则 widthFix 原图
if (this.hasManualCrop) return 'widthFix';
return this.hasBannerHeight || this.bannerCropRatio ? 'aspectFill' : 'widthFix';
},
// Banner 图片包裹层高度:仅显式配置 bannerHeight(rpx,clamp 40-1200)时输出;否则为空由图片自适应。
bannerImgWrapStyle() {
@@ -172,6 +184,58 @@ export default {
if (h > 1200) h = 1200;
return this.$util.objToStyle({ height: h + 'rpx' });
},
// 裁剪比例:读双键(camel + snake);合法值 16:9/4:3/1:1/3:4;空 = 不启用
bannerCropRatio() {
const cfg = this.mergedFormConfig || {};
const raw = cfg.bannerCropRatio !== undefined ? cfg.bannerCropRatio : cfg.banner_crop_ratio;
return (raw && BANNER_CROP_RATIOS[raw]) ? raw : '';
},
// 裁剪位置:top/center/bottom(默认 center);通过 CSS object-position 控制(uni-app H5/MP 容器配合 transform 实现)
bannerCropPosition() {
const cfg = this.mergedFormConfig || {};
const raw = cfg.bannerCropPosition !== undefined ? cfg.bannerCropPosition : cfg.banner_crop_position;
return (raw === 'top' || raw === 'bottom') ? raw : 'center';
},
// 手动裁栏框:读双键;返回 {x,y,w,h} 0..1 比例或 null
// 三优先级:手动框 > 比例/位置 > 高度/位置(与后端 syncFormConfigToCSS 对齐)
bannerCropBox() {
const cfg = this.mergedFormConfig || {};
const raw = cfg.bannerCropBox !== undefined ? cfg.bannerCropBox : cfg.banner_crop_box;
if (!raw) return null;
try {
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (obj && typeof obj.x === 'number' && typeof obj.y === 'number' && typeof obj.w === 'number' && typeof obj.h === 'number' && obj.w > 0 && obj.h > 0) {
return obj;
}
} catch (e) { return null; }
return null;
},
// 手动裁栏框激活时:渲染模式切 widthFix(保留全图)+ 内层 img 用 transform 移动/缩放显示裁栏区域
hasManualCrop() { return !!this.bannerCropBox; },
// 手动裁栏框下 image 内联样式:放大到原图尺寸,按 box 坐标 translate 显示
// 用 rpx 转换(MP 原生 rpx / H5 换算 px)
bannerImgStyle() {
if (!this.hasManualCrop) return '';
const b = this.bannerCropBox;
const scaleX = 1 / b.w;
const scaleY = 1 / b.h;
const tx = -b.x * 100;
const ty = -b.y * 100;
return this.$util.objToStyle({
position: 'absolute',
left: 0,
top: 0,
width: (scaleX * 100) + '%',
height: (scaleY * 100) + '%',
maxWidth: 'none',
transform: 'translate(' + tx + '%, ' + ty + '%)',
});
},
// 手动裁栏框下容器需要绝对定位 + 隐藏溢出
bannerImgWrapClass() {
var cls = { 'is-fixed-height': this.hasBannerHeight, 'has-crop': this.bannerImgMode === 'aspectFill', 'has-manual-crop': this.hasManualCrop };
return cls;
},
// 表单风格:优先读显式 form_style(classic / card),未设置时回退到隐式判定(看 show_banner && form_banner)
// - classic:经典风格,平铺无 banner 卡片
// - card :卡片浮起风格,banner + 卡片浮起 + 容器内缩
@@ -442,6 +506,10 @@ export default {
// 配合 mode="aspectFill" 居中裁剪,保证横幅只占页面八分之一左右
bannerHeight: pick('bannerHeight', 'banner_height'),
bannerPaddingX: pick('bannerPaddingX', 'banner_padding_x'),
// 横幅裁剪位置(top/center/bottom)+ 比例(16:9/4:3/1:1/3:4)+ 手动裁栏框 JSON(2026-09-04 方案 C 抽屉配套)
bannerCropPosition: pick('bannerCropPosition', 'banner_crop_position'),
bannerCropRatio: pick('bannerCropRatio', 'banner_crop_ratio'),
bannerCropBox: pick('bannerCropBox', 'banner_crop_box'),
tipPaddingX: pick('tipPaddingX', 'tip_padding_x'),
// 行内上下边距:兼容 row_padding_y / rowPaddingY / row_padding / rowPadding 等键名
rowPaddingY: (function() {
@@ -699,6 +767,59 @@ export default {
height: 100%;
}
}
// 启用 aspectFill 裁剪时,包装一层 .banner-img-crop 承接裁剪位置。
// 重要:uni-app image 在 H5 端固定转译为 <div style="background-image:url; background-size:cover; background-position:center center;"></div>,
// 内层 div 的 background-position 是运行时生成的 inline 样式,普通 CSS 覆盖不了,必须 !important;
// (不能用 .banner-img 的 translateY 实现位置:image 高度被钉死为容器 100% 后 transform 一去一回相互抵消,是零位移)
// - crop-top → background-position: center top(显示图片顶部区域)
// - crop-center → 默认 center center(不覆盖)
// - crop-bottom → background-position: center bottom(显示图片底部区域)
&.has-crop {
.banner-img-crop {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.banner-img {
// 撑满 wrap,让内层 background-image div 拿到确定高度(uni-image H5 端转译依赖)
height: 100%;
position: absolute;
left: 0;
top: 0;
width: 100%;
}
// uni-app H5 编译器会给样式追加 [data-v-xxx](即使未写 scoped),
// 而 uni-image 运行时 JS 生成的内层 div 没有该属性 → 必须 ::v-deep 把属性选择器挪到 .banner-img 上
.banner-img-crop.crop-top .banner-img ::v-deep div {
background-position: center top !important;
}
.banner-img-crop.crop-bottom .banner-img ::v-deep div {
background-position: center bottom !important;
}
}
// 启用手动裁栏框(bannerCropBox 非空)时:image 用 transform 显示裁栏部分,原图按 box 缩放 + 平移
&.has-manual-crop {
.banner-img-crop {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.banner-img {
// 手动框下由 bannerImgStyle 内联 width/height/transform 驱动,不固定 100%
height: auto;
position: absolute;
left: 0;
top: 0;
}
}
}
.banner-img {