chore:表单样式更新

This commit is contained in:
2026-08-28 11:58:23 +08:00
parent e693e4273c
commit ecb0789780
3 changed files with 186 additions and 105 deletions

View File

@@ -237,9 +237,33 @@ export default {
delete base[k];
}
});
// 前端视觉默认:强制覆盖后端 formConfig 中可能存在的旧默认(horizontal / 红按钮 / 灰背景等),
// 与 formdata.vue 的 styleDefaults 保持一致,使所有 diy-form(含 diy-group 内的表单)统一呈现新设计稿样式。
// 后端仍保留行为/文案类配置(behavior / submitText / banner / tip 等),本合并仅覆盖视觉字段。
const styleDefaults = {
formLayout: 'vertical',
formPaddingX: 28,
rowPaddingY: 24,
labelGap: 20,
labelFontSize: 32,
labelFontWeight: 700,
labelColor: '#1f2937',
inputHeight: 84,
inputRadius: 16,
inputBgColor: '#ffffff',
inputBorderColor: '#e5e7eb',
inputFocusBorderColor: '#3b82f6',
inputFocusBgColor: '#ffffff',
placeholderColor: '#9ca3af',
placeholderFontSize: 28,
checkedColor: '#3b82f6',
showRowDivider: false,
submitColor: '#ffffff',
submitBgColor: '#3b82f6'
};
// 平台差异化配置(terminal 块在 formConfig 子对象内)
// 优先级:v.formConfig.terminal > v.form_config_terminal > v.terminal > v.form_config_platform
const result = Object.assign({}, base);
const result = Object.assign({}, base, styleDefaults);
const termBlock = (v.formConfig && v.formConfig.terminal) || v.form_config_terminal || v.terminal || v.form_config_platform;
if (termBlock && typeof termBlock === 'object') {
Object.keys(termBlock).forEach(function(platform) {

View File

@@ -4,8 +4,8 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Text'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box">
<input
@@ -22,8 +22,8 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Textarea'">
<view class="order-cell flex-box textarea" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box">
<!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
@@ -48,20 +48,20 @@
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box">
<view :class="['box', 'box-flex']">
<text v-if="item.val != ''">{{ item.val }}</text>
<text v-else class="color-tip">{{ $lang('components.nsForm.selectPlaceholder') }}</text>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</view>
</picker>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Checkbox'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view :class="['box', 'check-group-box', 'option-layout-' + (item.value.option_layout || 'wrap')]">
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
@@ -80,8 +80,8 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Radio'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view :class="['box', 'radio-group-box', 'option-layout-' + (item.value.option_layout || 'wrap')]">
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
@@ -100,8 +100,8 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Img'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box img-boxs">
<view class="img-box" v-for="(v, k) in item.img_lists" :key="k" @click="uploadImg(index)">
@@ -116,23 +116,23 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Date'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box box-flex">
<view :class="['box', 'box-flex']">
<picker mode="date" :value="item.val" @change="bindDateChange($event, index)">
<view class="uni-input" :class="{ 'color-tip': !item.val }">{{ item.val ? item.val : item.value.placeholder }}</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Datelimit'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box date-boxs">
<view class="date-box">
@@ -157,23 +157,23 @@
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Time'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box box-flex">
<view :class="['box', 'box-flex']">
<picker mode="time" :value="item.val" @change="bindTimeChange($event, index)">
<view class="uni-input" :class="{ 'color-tip': !item.val }">{{ item.val ? item.val : item.value.placeholder }}</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Timelimit'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box date-boxs">
<view class="date-box">
@@ -196,21 +196,21 @@
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'City'">
<view class="order-cell box-flex" :style="rowCellStyle">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
<text class="tit">{{ item.value.title }}</text>
</view>
<view class="box">
<view :class="['box', 'box-flex']">
<pick-regions :default-regions="item.default_regions" :select-arr="item.select_arr" @getRegions="handleGetRegions($event, index)">
<view class="select-address " :class="{ empty: !item.val, 'color-tip': !item.val }">
{{ item.val ? item.val : (item.select_arr == '2' ? $lang('components.nsForm.selectProvinceCity') : $lang('components.nsForm.selectProvinceCityDistrict')) }}
</view>
</pick-regions>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</view>
</block>
</view>
</template>
@@ -263,22 +263,22 @@
config() {
// 1) 内置默认值(最低优先级,与重构前的硬编码值一一对应,确保未传 formConfig 时零回归)
const defaults = {
formPaddingX: 0,
formPaddingX: 28,
formBgColor: '#ffffff',
rowPaddingY: 20,
rowPaddingY: 24,
rowMinHeight: 60,
labelGap: 16,
inputHeight: 60,
inputRadius: 0,
labelWidthMode: 'fixed', // 旧版 width: 80vw 用 fixed 180rpx 等价替代
labelGap: 20,
inputHeight: 84,
inputRadius: 16,
labelWidthMode: 'auto', // 与 ns-newform 对齐:标签在上、输入框在下,整行宽度
labelFixedWidth: 180,
labelFontSize: 28,
labelFontSize: 32,
labelColor: '#1f2937',
labelFontWeight: 600,
labelFontWeight: 700,
labelAutoMaxWidth: 200,
requiredColor: '#ef4444',
checkedColor: '#ff4544',
placeholderColor: '#909399',
checkedColor: '#3b82f6',
placeholderColor: '#9ca3af',
placeholderFontSize: 28,
// textarea 显示行数 / 行高(与 ns-newform 对齐)
// 改用 :rows + line-height 驱动高度:rows 给原生 <textarea>(H5/微信原生层/鸿蒙 ArkUI 都识别),
@@ -404,13 +404,10 @@
'min-height': this.$util.rpx(this.config.rowMinHeight)
});
},
// 标签宽度:根据 labelWidthMode 决定;fixed 用 --label-fixed-width,auto 用 maxLabelWidth
// 标签宽度:垂直布局下标签整行显示,宽度交由 CSS(.name { width:100% })控制;
// 字号 / 颜色 / 字重已由 .form-wrap 注入的 --label-* 变量统一驱动,无需 inline 注入。
nameWidthStyle() {
const cfg = this.config;
if (cfg.labelWidthMode === 'fixed') {
return { width: this.$util.rpx(cfg.labelFixedWidth) };
}
return this.maxLabelWidth ? { width: this.$util.rpx(this.maxLabelWidth) } : { width: 'auto' };
return {};
},
// 输入框样式(input / textarea 共用)
inputStyle() {
@@ -948,31 +945,31 @@
后端 formConfig 一律以 rpx 传递,前端按 rpx 写入;
仅 H5 端需要在 JS computed(formWrapStyle 等)里通过 _rpx() 转 px。 */
.form-wrap {
--form-padding-x: 0rpx;
--form-padding-x: 28rpx;
--form-bg-color: #ffffff;
--row-padding-y: 20rpx;
--row-padding-y: 24rpx;
--row-min-height: 60rpx;
--label-gap: 16rpx;
--input-height: 60rpx;
--input-radius: 0rpx;
--label-gap: 20rpx;
--input-height: 84rpx;
--input-radius: 16rpx;
--label-fixed-width: 180rpx;
--label-font-size: 28rpx;
--label-font-size: 32rpx;
--label-color: #1f2937;
--label-font-weight: 600;
--label-font-weight: 700;
--required-color: #ef4444;
--placeholder-font-size: 28rpx;
--checked-color: #ff4544;
--placeholder-color: #909399;
--checked-color: #3b82f6;
--placeholder-color: #9ca3af;
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
--textarea-rows: 4;
--textarea-line-height: 44rpx;
--textarea-line-height: 48rpx;
// 输入框视觉增强(与 ns-newform 对齐)
--input-border-color: #e5e7eb;
--input-bg-color: #f9fafb;
--input-focus-border-color: #ff4544;
--input-bg-color: #ffffff;
--input-focus-border-color: #3b82f6;
--input-focus-bg-color: #ffffff;
--row-divider-color: #f0f0f0;
--show-row-divider: 1;
--show-row-divider: 0;
}
.order-wrap {
@@ -990,7 +987,8 @@
.order-cell {
display: flex;
align-items: center;
flex-direction: column; // 与 ns-newform vertical 对齐:标签在上、输入框在下
align-items: flex-start;
background: #fff;
position: relative;
min-height: var(--row-min-height, 60rpx);
@@ -1023,18 +1021,23 @@
}
.name {
width: var(--label-fixed-width, 180rpx);
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: var(--label-gap, 16rpx);
display: flex;
align-items: center;
min-height: var(--input-height, 60rpx);
line-height: 1.5;
margin-bottom: var(--label-gap, 20rpx);
margin-right: 0;
}
.tit {
text-align: left;
font-size: var(--label-font-size, $font-size-base);
color: var(--label-color, #1f2937);
font-weight: var(--label-font-weight, 600);
font-weight: var(--label-font-weight, 700);
text {
font-size: var(--label-font-size, $font-size-base);
@@ -1044,15 +1047,17 @@
.required {
color: var(--required-color, #ef4444);
font-size: var(--label-font-size, 28rpx);
margin-left: 4rpx;
width: 14rpx;
margin-right: 6rpx;
font-weight: bold;
text-align: left;
display: inline-block;
vertical-align: middle;
}
.box {
flex: 1;
padding: 0 10rpx;
width: 100%;
padding: 0;
line-height: inherit;
text-align: left;
@@ -1064,26 +1069,27 @@
color: var(--label-color, #1f2937);
// 显式 height / line-height / min-height 同步覆盖 WebView 默认与鸿蒙原生 TextInput 的固有高度,
// 避免 placeholder / 文字下半部分被裁切
line-height: var(--input-height, 60rpx);
height: var(--input-height, 60rpx);
min-height: var(--input-height, 60rpx);
border-radius: var(--input-radius, 0rpx);
line-height: var(--input-height, 84rpx);
height: var(--input-height, 84rpx);
min-height: var(--input-height, 84rpx);
border-radius: var(--input-radius, 16rpx);
box-sizing: border-box;
padding: 0 12rpx;
width: 100%;
padding: 0 24rpx;
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
background: var(--input-bg-color, #ffffff);
transition: all 0.3s ease;
&:focus {
background: var(--input-focus-bg-color, #ffffff);
border-color: var(--input-focus-border-color, #ff4544);
border-color: var(--input-focus-border-color, #3b82f6);
}
// 原生 placeholder 样式(WebView / H5 生效;小程序/鸿蒙的 placeholder
// 通过 placeholder-class="placeholder" 钩子走 .placeholder 规则)
&::placeholder {
color: var(--placeholder-color, #909399);
color: var(--placeholder-color, #9ca3af);
font-size: var(--placeholder-font-size, 28rpx);
}
}
@@ -1091,9 +1097,9 @@
// placeholder-class 钩子:在鸿蒙原生 TextInput / WebView 中都生效,
// 限定在 .form-wrap 内避免污染全局 .placeholder / .color-tip
.placeholder {
color: var(--placeholder-color, #909399);
color: var(--placeholder-color, #9ca3af);
font-size: var(--placeholder-font-size, 28rpx);
line-height: var(--input-height, 60rpx);
line-height: var(--input-height, 84rpx);
}
::v-deep textarea,
@@ -1101,36 +1107,36 @@
font-size: var(--label-font-size, $font-size-base);
width: 100%;
color: var(--label-color, #1f2937);
// 行高由 formConfig.textareaLineHeight(兜底 44rpx)控制;
// 行高由 formConfig.textareaLineHeight(兜底 48rpx)控制;
// 高度 = rows × line-height + padding + border,所有值都从 CSS 变量读取,
// 真正做到了"rows 改 → 高度自动改"。
line-height: var(--textarea-line-height, 44rpx);
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
line-height: var(--textarea-line-height, 48rpx);
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
text-align: left;
border-radius: var(--input-radius, 0rpx);
border-radius: var(--input-radius, 16rpx);
box-sizing: border-box;
padding: 12rpx;
padding: 12rpx 24rpx;
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
background: var(--input-bg-color, #ffffff);
transition: all 0.3s ease;
&:focus {
background: var(--input-focus-bg-color, #ffffff);
border-color: var(--input-focus-border-color, #ff4544);
border-color: var(--input-focus-border-color, #3b82f6);
}
}
.uni-input {
font-size: var(--label-font-size, $font-size-base);
color: var(--label-color, #1f2937);
line-height: var(--input-height, 60rpx);
min-height: var(--input-height, 60rpx);
line-height: var(--input-height, 84rpx);
min-height: var(--input-height, 84rpx);
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
border-radius: var(--input-radius, 0rpx);
background: var(--input-bg-color, #ffffff);
border-radius: var(--input-radius, 16rpx);
transition: all 0.3s ease;
}
@@ -1140,7 +1146,29 @@
}
.color-tip {
color: var(--placeholder-color, #909399);
color: var(--placeholder-color, #9ca3af);
}
// 让原生 picker / 自定义 pick-regions 撑满父容器(纵向布局下 label 在上、控件在下)
picker,
.pick-regions {
display: block;
width: 100%;
min-width: 0;
}
// 在 box-flex(带右侧箭头)场景下,picker 不应占满,留出右侧箭头空间
&.box-flex {
picker,
.pick-regions {
width: auto;
}
.iconfont.icon-right {
flex-shrink: 0;
margin-left: 8rpx;
line-height: 1;
}
}
checkbox-group {
@@ -1245,8 +1273,9 @@
line-height: 1;
.iconfont {
font-size: 32rpx;
font-size: 40rpx;
margin-right: 10rpx;
color: #d1d5db;
}
}
}
@@ -1262,8 +1291,9 @@
line-height: 1;
.iconfont {
font-size: 32rpx;
font-size: 40rpx;
margin-right: 10rpx;
color: #d1d5db;
}
}
}

View File

@@ -6,7 +6,7 @@
</view>
<view class="system-form-wrap">
<view class="form-title">{{ $lang('form.form.title') }}</view>
<ns-form :data="detail.json_data" :formConfig="detail.form_config" ref="form"></ns-form>
<ns-form :data="detail.json_data" :formConfig="mergedFormConfig" ref="form"></ns-form>
<!-- 行为设置:提交验证码 -->
<view class="captcha-wrap" v-if="behavior.captcha == 1">
<input class="captcha-input" type="text" v-model="captcha.code" :placeholder="$lang('form.form.captchaPlaceholder')" />
@@ -55,6 +55,33 @@
};
},
computed: {
// 前端视觉默认:强制覆盖后端 form_config 中可能存在的旧默认(horizontal / 红按钮 / 灰背景等),
// 与 diy-form 路径(formdata.vue 的 styleDefaults)保持一致,使系统表单页也统一呈现新设计稿样式。
// 后端仍保留行为类配置(behavior / submitText 等),本合并仅覆盖视觉字段。
mergedFormConfig() {
const styleDefaults = {
formLayout: 'vertical',
formPaddingX: 28,
rowPaddingY: 24,
labelGap: 20,
labelFontSize: 32,
labelFontWeight: 700,
labelColor: '#1f2937',
inputHeight: 84,
inputRadius: 16,
inputBgColor: '#ffffff',
inputBorderColor: '#e5e7eb',
inputFocusBorderColor: '#3b82f6',
inputFocusBgColor: '#ffffff',
placeholderColor: '#9ca3af',
placeholderFontSize: 28,
checkedColor: '#3b82f6',
showRowDivider: false,
submitColor: '#ffffff',
submitBgColor: '#3b82f6'
};
return Object.assign({}, this.detail ? (this.detail.form_config || {}) : {}, styleDefaults);
},
emptyText() {
return this.complete ? this.$lang('form.form.submitSuccess') : (this.formError || this.$lang('form.form.noFormInfo'));
}