refactor(ns-newform.vue): 优化字段样式,支持字段级控件尺寸和样式动态设置

This commit is contained in:
2026-09-04 13:48:00 +08:00
parent 11cc4f6319
commit acab1971ab

View File

@@ -36,7 +36,7 @@
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="textareaStyle"
:style="item._textareaStyle"
></textarea>
</view>
</view>
@@ -54,7 +54,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'box-flex', 'align-right']">
<view :class="['box', 'box-flex', 'align-right']" :style="item._controlBoxStyle">
<text v-if="item.val != ''">{{ item.val }}</text>
<text v-else class="color-tip">{{ item.value.placeholder || $lang('components.ns-newform.selectPlaceholder') }}</text>
<text class="iconfont icon-right"></text>
@@ -70,7 +70,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'check-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]">
<view :class="['box', 'check-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]" :style="item._controlWidthStyle">
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
<label v-for="(v, k) in item.option_lists" :key="k">
<checkbox :value="v.value" :checked="v.checked"></checkbox>
@@ -99,7 +99,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'radio-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]">
<view :class="['box', 'radio-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]" :style="item._controlWidthStyle">
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
<label v-for="(v, k) in item.option_lists" :key="k">
<radio :value="v.value" :checked="item.val == v.value"></radio>
@@ -147,7 +147,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'box-flex', 'align-right']">
<view :class="['box', 'box-flex', 'align-right']" :style="item._controlBoxStyle">
<picker
mode="date"
:value="item.val"
@@ -208,7 +208,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'box-flex', 'align-right']">
<view :class="['box', 'box-flex', 'align-right']" :style="item._controlBoxStyle">
<picker
mode="time"
:value="item.val"
@@ -269,7 +269,7 @@
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'box-flex', 'align-right']">
<view :class="['box', 'box-flex', 'align-right']" :style="item._controlBoxStyle">
<pick-regions
:defaultRegions="item.default_regions"
:selectArr="item.select_arr"
@@ -539,19 +539,23 @@ export default {
const n = parseInt(this.config.textareaLineHeight, 10);
return Number.isFinite(n) && n > 0 ? n : 44;
},
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
// 高度 = rows × lineHeight + padding + border,由 formConfig 动态控制,与 ns-form 同公式
textareaStyle() {
// textarea 基础 inline style 对象(供 textareaStyle 与字段级尺寸合并复用)
textareaBaseStyle() {
const lh = this.$util.rpx(this.textareaLineHeightValue);
const rows = this.textareaRowsValue;
const paddings = this.$util.rpx(24) + this.$util.rpx(2); // padding 12rpx 上下 + border 1rpx 上下
const h = (this.$util.rpx(this.textareaLineHeightValue * (rows - 1)) + lh) + paddings;
return this.$util.objToStyle({
return {
'height': h,
'min-height': h,
'line-height': lh,
'border-radius': this.$util.rpx(this.config.inputRadius)
});
};
},
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
// 高度 = rows × lineHeight + padding + border,由 formConfig 动态控制,与 ns-form 同公式
textareaStyle() {
return this.$util.objToStyle(this.textareaBaseStyle);
},
labelStyle() {
const style = {
@@ -596,10 +600,28 @@ export default {
// 行容器基础上下内边距(原 rowWrapStyle 语义:全局行间距 rowPaddingY;左右恒 0)
const rowPadY = this.$util.rpx(this.config.rowPaddingY);
const clampRpx = (n, min, max) => this.$util.rpx(Math.max(min, Math.min(max, n)));
// 字段级控件尺寸(宽度 / 高度 / 选项间距):'' / null / undefined = 未设置,沿用全局(宽度默认占满 100%)
// clamp 与后端 fieldSizeStyleMixin 一致(width 0..750 / height 0..400 / gap 0..200 rpx)
const sizeRpx = (x, min, max) => {
if (x === undefined || x === null || x === '') return null;
const n = parseFloat(x);
return isNaN(n) ? null : this.$util.rpx(Math.max(min, Math.min(max, n)));
};
this.formData.forEach((item) => {
const itemValue = (item && item.value) || {};
// ---- 字段级控件尺寸 ----
const cW = sizeRpx(itemValue.field_width, 0, 750);
const cH = sizeRpx(itemValue.field_height, 0, 400);
const oG = sizeRpx(itemValue.option_gap, 0, 200);
// 宽度:.box 基础规则是 flex:1,需 flex:none 解除伸缩,否则 inline width 不生效
const ctrlWidthStyle = cW !== null ? { width: cW, 'flex': 'none', 'max-width': '100%' } : {};
// 高度:单行控件(input / picker .uni-input)的 height 与 line-height 均由 --input-height 驱动,
// 在 .box 上覆盖该 CSS 变量即可同时命中 H5(::v-deep input)与小程序的原生元素规则
const ctrlHeightStyle = cH !== null ? { '--input-height': cH } : {};
const ctrlBoxStyle = Object.assign({}, ctrlWidthStyle, ctrlHeightStyle);
// ---- 字段级间距(每个组件独立):外边距直出(允许负值上移贴近相邻组件)----
// clamp 与后端 fieldSpacingStyle 一致(margin -100..400 / padding 0..400 rpx)
const mTop = spNum(itemValue.margin_top), mBottom = spNum(itemValue.margin_bottom),
@@ -638,8 +660,9 @@ export default {
this.$set(item, '_labelStyle', style);
// item 级别 box 对齐方式(与原 getItemBoxAlignStyle 逻辑一致)
// 并入字段级宽度 / 高度(.box 是 flex:1,width 需配合 flex:none 才生效,见 ctrlWidthStyle)
const align = itemValue.input_align;
this.$set(item, '_boxAlignStyle', align ? { 'text-align': align } : {});
this.$set(item, '_boxAlignStyle', Object.assign(align ? { 'text-align': align } : {}, ctrlBoxStyle));
// 必填星号样式:颜色/字号/间距全部预计算 inline(微信小程序/鸿蒙元服务对
// 嵌套 var() fallback 与 .required-after 类 margin 翻转支持不全,动态值不走 CSS 变量链)。
@@ -674,8 +697,19 @@ export default {
optStyle['align-items'] = 'flex-start';
optStyle['gap'] = '20rpx';
}
// 字段级选项间距(option_gap):覆盖默认 gap(wrap 12rpx / vertical 20rpx)
if (oG !== null) optStyle['gap'] = oG;
this.$set(item, '_optionLayoutStyle', optStyle);
// 单行 / 多选行控件(Select、Date、Time、City 的 .box 无 _boxAlignStyle 绑定;
// Checkbox / Radio 只需要宽度,高度无意义)单独落 key,供模板直接绑定
this.$set(item, '_controlBoxStyle', ctrlBoxStyle);
this.$set(item, '_controlWidthStyle', ctrlWidthStyle);
// textarea:字段级高度直出(宽度走 .box 的 _boxAlignStyle;行高沿用 formConfig),
// 同步 min-height 防止被基础规则的 min-height 顶住不缩
this.$set(item, '_textareaStyle', Object.assign({}, this.textareaBaseStyle,
cH !== null ? { 'height': cH, 'min-height': cH } : {}));
// 信息卡片:容器底色 + 空白判定 + 分割线样式(与后台 design.js 的
// getInfoCardContainerStyle / getInfoCardDividerStyle 对齐;数值单位为 px 直出,
// 与编辑器输入单位一致,375 宽屏上与后台 PC 预览视觉一致)