refactor(ns-newform): 优化表单样式计算与渲染逻辑

1. 移除模板内动态调用的样式方法,改用预计算的响应式样式属性
2. 重构行分割线实现,改用border-bottom替代伪元素提升兼容性
3. 新增兜底样式解决小程序占位符样式失效问题
4. 统一表单样式的计算时机与响应式更新逻辑
This commit is contained in:
2026-06-11 16:29:14 +08:00
parent 7c7e67df4a
commit f11146013e

View File

@@ -4,11 +4,11 @@
<!-- 文本输入框 -->
<view v-if="item.controller == 'Text'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]" :style="rowCellStyle">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'align-left']" :style="getItemBoxAlignStyle(item)">
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
<input
type="text"
:placeholder="item.value.placeholder || '请输入提示语'"
@@ -22,11 +22,11 @@
<!-- 多行文本输入框 -->
<view v-if="item.controller == 'Textarea'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'flex-box', 'textarea', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'align-left']" :style="getItemBoxAlignStyle(item)">
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
<textarea
:placeholder="item.value.placeholder || '请输入提示语'"
placeholder-class="placeholder color-tip"
@@ -44,7 +44,7 @@
@change="pickerChange($event, index)"
>
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -60,7 +60,7 @@
<!-- 复选框 -->
<view v-if="item.controller == 'Checkbox'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -88,7 +88,7 @@
<!-- 单选框 -->
<view v-if="item.controller == 'Radio'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -116,11 +116,11 @@
<!-- 图片上传 -->
<view v-if="item.controller == 'Img'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'img-boxs', 'align-left']" :style="getItemBoxAlignStyle(item)">
<view :class="['box', 'img-boxs', 'align-left']" :style="item._boxAlignStyle">
<view v-for="(v, k) in item.img_lists" :key="k" class="img-box" @tap="uploadImg(index)">
<image :src="$util.img(v)" mode="aspectFill" show-menu-by-longpress></image>
<text class="iconfont icon-guanbi" @tap.stop="delImg(k, index)"></text>
@@ -135,7 +135,7 @@
<!-- 日期选择器 -->
<view v-if="item.controller == 'Date'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -157,11 +157,11 @@
<!-- 日期范围选择器 -->
<view v-if="item.controller == 'Datelimit'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'date-boxs', 'align-center']" :style="getItemBoxAlignStyle(item)">
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
<view class="date-box">
<picker
mode="date"
@@ -196,7 +196,7 @@
<!-- 时间选择器 -->
<view v-if="item.controller == 'Time'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -218,11 +218,11 @@
<!-- 时间范围选择器 -->
<view v-if="item.controller == 'Timelimit'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'date-boxs', 'align-center']" :style="getItemBoxAlignStyle(item)">
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
<view class="date-box">
<picker
mode="time"
@@ -257,7 +257,7 @@
<!-- 城市选择器 -->
<view v-if="item.controller == 'City'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'box-flex', { 'vertical-layout': mode === 'vertical' }]">
<view class="name" :style="[nameWidthStyle, getItemLabelStyle(item), nameStyle]">
<view class="name" :style="item._labelStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
@@ -571,26 +571,39 @@ export default {
}
return v;
},
// 组件级 label 样式:通过 CSS 变量覆盖,驱动子元素 .tit / .required
getItemLabelStyle(item) {
const override = (item && item.value) || {};
const style = {};
if (override.label_font_size !== undefined && override.label_font_size !== null && override.label_font_size !== '') {
let fs = override.label_font_size;
if (typeof fs === 'number') fs = `${fs}rpx`;
else if (typeof fs === 'string' && !fs.endsWith('rpx') && !fs.endsWith('px')) fs = `${fs}rpx`;
style['--item-label-font-size'] = fs;
}
if (override.label_color) {
style['--item-label-color'] = override.label_color;
}
return style;
},
// 组件级输入框对齐方式:left / center / right
getItemBoxAlignStyle(item) {
const align = item && item.value && item.value.input_align;
if (!align) return {};
return { 'text-align': align };
// 预计算每项的样式(写到 item._labelStyle / item._boxAlignStyle),
// 避免在模板里以方法形式调用:微信小程序 WXML 不支持 :style="getXxx(item)" 语法。
// 触发时机:processFormData 末尾 / calculateMaxLabelWidth 末尾 / config 变化 / mode 变化。
recomputeItemStyles() {
if (!this.formData || this.formData.length === 0) return;
const widthStyle = this.nameWidthStyle || {};
const nameMarginStyle = this.nameStyle || {};
this.formData.forEach((item) => {
const itemValue = (item && item.value) || {};
const style = Object.assign({}, widthStyle);
// item 级别 label 字号 / 颜色覆盖(与原 getItemLabelStyle 逻辑一致)
if (itemValue.label_font_size !== undefined && itemValue.label_font_size !== null && itemValue.label_font_size !== '') {
let fs = itemValue.label_font_size;
if (typeof fs === 'number') fs = `${fs}rpx`;
else if (typeof fs === 'string' && !fs.endsWith('rpx') && !fs.endsWith('px')) fs = `${fs}rpx`;
style['--item-label-font-size'] = fs;
}
if (itemValue.label_color) {
style['--item-label-color'] = itemValue.label_color;
}
// 合并全局的 nameStyle(margin-right)
Object.assign(style, nameMarginStyle);
// $set 包裹以确保新属性是响应式的(Vue 2 对动态加的属性需要 $set)
this.$set(item, '_labelStyle', style);
// item 级别 box 对齐方式(与原 getItemBoxAlignStyle 逻辑一致)
const align = itemValue.input_align;
this.$set(item, '_boxAlignStyle', align ? { 'text-align': align } : {});
});
},
// 容器样式(提供给外部 diy-form 使用)
getFormWrapStyle() {
@@ -615,6 +628,8 @@ export default {
// 与后端公式一致:chars * (fontSize/2) * 1.1 + 12,单位 px → 转 rpx(×2)
const widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
this.maxLabelWidth = Math.round(widthPx * 2);
// 宽度变了,需要把全局 nameWidthStyle 重新合到每项 _labelStyle 里
this.recomputeItemStyles();
},
initFormData() {
@@ -759,8 +774,11 @@ export default {
}
}
});
// 数据处理完后再算每项的 _labelStyle / _boxAlignStyle(依赖 nameWidthStyle,而 maxLabelWidth 此时可能还没算)
// 这里先算一次;calculateMaxLabelWidth 完成后会再算一次(带正确的 maxLabelWidth)
this.recomputeItemStyles();
},
verify() {
for (let i = 0; i < this.formData.length; i++) {
let item = this.formData[i];
@@ -1011,8 +1029,8 @@ export default {
// 与后端 form-preview-wrap.no-divider-all 对齐:整组隐藏所有行分割线
&.no-divider-all {
.order-wrap::after {
display: none !important;
.order-wrap {
border-bottom: 0;
}
}
}
@@ -1022,31 +1040,18 @@ export default {
transition: all 0.3s ease;
padding: 0;
background: var(--form-bg-color);
&::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1rpx;
background: var(--row-divider-color, #f0f0f0);
}
box-sizing: border-box;
// 行分割线:与后端 design.css .form-preview-wrap .form-gorup 对齐
// 用 border-bottom 而非 ::after 伪元素(亚像素不可见 + 多端兼容性差)
border-bottom: 2rpx solid var(--row-divider-color, #f0f0f0);
&:last-child {
&::after {
display: none;
}
border-bottom: 0;
}
// 后端 show_row_divider = 0:隐藏所有行分割线
&.no-divider {
&::after {
display: none !important;
}
&:last-child::after {
display: none !important;
}
border-bottom: 0;
}
.order-cell {
@@ -1383,4 +1388,22 @@ export default {
}
}
}
</style>
<!--
非 scoped 兜底样式:
1) .placeholder / .color-tip 走的是 mp/小程序原生的占位符元素(placeholder-class),
scoped 编译后的 [data-v-xxx] 属性选择器匹配不到,必须在全局作用域定义。
2) 与 common/css/main.scss 的 .color-tip 协同,确保占位符在 H5 / mp / 鸿蒙 各端都可显;
若页面未引入 main.scss,本兜底也能保证 Select/Date/Time 等 .color-tip 提示文字可见。
-->
<style lang="scss">
.placeholder {
color: #909399;
font-size: 28rpx;
}
.color-tip {
color: #909399;
}
</style>