Files
lucky_shop/components-diy/diy-form.vue
ZF sun fae1d77ba5 refactor(diy-form&ns-newform): 重构表单组件样式与配置体系
1.  统一表单样式配置为中心化CSS变量方案,替换原有硬编码样式
2.  新增表单配置合并逻辑,兼容新旧数据格式与平台差异化配置
3.  拆分提交按钮与提示语样式,实现完全可配置化
4.  优化ns-newform组件的样式复用与响应式布局逻辑
5.  修复多端适配与占位符默认值问题
2026-06-11 09:45:09 +08:00

342 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view data-component-name="diy-form" class="diy-from" :class="[formLayoutClass]" :style="[diyFormStyle, cssVars]">
<!-- Banner -->
<view v-if="value.show_banner && value.form_banner" class="form-banner" :style="bannerStyle">
<image :src="$util.img(value.form_banner)" mode="widthFix" class="banner-img" />
</view>
<!-- 提示语 -->
<view v-if="value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value.form_tip }}</view>
<!-- 表单组件 -->
<ns-newform
:data="formComponents"
:customAttr="value"
:mode="formCfg.form_layout === 'vertical' ? 'vertical' : 'horizontal'"
:formConfig="mergedFormConfig"
ref="nsNewform"
></ns-newform>
<!-- 提交按钮(外层 = form-bg + form-padding-x;按钮 = submit-bg + submit-color + input-height + input-radius) -->
<view class="form-submit-wrap" :style="submitWrapStyle">
<view @click.stop="submitForm" class="form-submit" :style="submitStyle">{{ submitText }}</view>
</view>
</view>
</template>
<script>
// 自定义表单
import DiyMinx from './minx.js'
import NsNewform from '@/components/ns-newform/ns-newform.vue'
export default {
name: 'diy-from',
components: { NsNewform },
props: {
value: {
type: Object
}
},
mixins: [DiyMinx],
data() {
return {
formComponents: [] // 转换后的表单组件
};
},
created() {
this.initForm();
},
watch: {
'value': {
deep: true,
handler() {
this.initForm();
}
}
},
computed: {
style() {
var css = '';
css += 'background-color:' + this.value.contentBgColor + ';';
if (this.value.elementAngle === 'round') {
css += 'border-top-left-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;';
css += 'border-top-right-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;';
css += 'border-bottom-left-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;';
css += 'border-bottom-right-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;';
}
return css;
},
// 兼容旧 style 别名(部分页面用 :style="style" 引用)
diyFormStyle() {
return this.style;
},
// 将后端 formConfig 写入 CSS 变量:与后端 design.css :root 对齐,
// 让 banner / tip / submit 与 ns-newform 子组件共享同一套样式变量
cssVars() {
const cfg = this.mergedFormConfig || {};
const rpx = (v) => {
if (v === '' || v === null || v === undefined) return '';
return typeof v === 'number' ? `${v}rpx` : `${v}`;
};
const showDivider = this._coerceBool(cfg.showRowDivider);
return {
'--form-padding-x': rpx(cfg.formPaddingX || 14),
'--banner-padding-x': rpx(cfg.bannerPaddingX || 0),
'--tip-padding-x': rpx(cfg.tipPaddingX || 14),
'--tip-font-size': rpx(cfg.tipFontSize || 24),
'--tip-color': cfg.tipColor || '#666666',
'--tip-bg-color': cfg.tipBgColor || '#ffffff',
'--row-padding-y': rpx(cfg.rowPaddingY || 4),
'--row-min-height': rpx(cfg.rowMinHeight || 60),
'--label-gap': rpx(cfg.labelGap || 16),
'--form-bg-color': cfg.formBgColor || '#ffffff',
'--row-divider-color': cfg.rowDividerColor || '#f0f0f0',
'--input-height': rpx(cfg.inputHeight || 60),
'--input-radius': rpx(cfg.inputRadius || 10),
'--label-fixed-width': rpx(cfg.labelFixedWidth || 180),
'--label-font-size': rpx(cfg.labelFontSize || 28),
'--label-color': cfg.labelColor || '#1f2937',
'--required-color': cfg.requiredColor || '#ef4444',
'--checked-color': cfg.checkedColor || '#ff4544',
'--submit-color': cfg.submitColor || '#ffffff',
'--submit-bg-color': cfg.submitBgColor || '#ff4544',
'--placeholder-color': '#ddd',
'--show-row-divider': showDivider ? '1' : '0'
};
},
// Banner 容器左右内边距(与后端 .form-preview-banner padding-x 对齐)
bannerStyle() {
return {
padding: `0 ${this._num(this.mergedFormConfig && this.mergedFormConfig.bannerPaddingX, 0)}rpx`
};
},
// 提示语样式:padding-x、font-size、color、bg-color(与后端 .form-preview-tip 对齐)
tipStyle() {
const cfg = this.mergedFormConfig || {};
return {
padding: `${this._num(cfg.rowPaddingY, 4)}rpx ${this._num(cfg.tipPaddingX, 14)}rpx`,
'font-size': `${this._num(cfg.tipFontSize, 24)}rpx`,
'color': cfg.tipColor || '#666666',
'background-color': cfg.tipBgColor || 'transparent'
};
},
// 提交按钮外层:与后端 .form-preview-submit 对齐(padding: row-padding-y form-padding-x;background: form-bg-color)
submitWrapStyle() {
const cfg = this.mergedFormConfig || {};
return {
padding: `${this._num(cfg.rowPaddingY, 4)}rpx ${this._num(cfg.formPaddingX, 14)}rpx`,
'background-color': cfg.formBgColor || '#ffffff'
};
},
// 提交按钮样式:颜色 / 背景色 / 高度 / 圆角 / 字号(与后端 .form-preview-submit button 对齐)
submitStyle() {
const cfg = this.mergedFormConfig || {};
return {
color: cfg.submitColor || '#ffffff',
'background-color': cfg.submitBgColor || '#ff4544',
'line-height': `${this._num(cfg.inputHeight, 60)}rpx`,
'border-radius': `${this._num(cfg.inputRadius, 10)}rpx`,
'font-size': `${this._num(cfg.labelFontSize, 28)}rpx`
};
},
// 提交按钮文字:与后端 design.html <button>提交</button> 默认对齐,开放给 formConfig 覆盖
submitText() {
return this.formCfg.submit_text || '提交';
},
formLayoutClass() {
return this.formCfg.form_layout === 'vertical' ? 'form-vertical' : 'form-horizontal';
},
// 后端把表单样式配置统一存放在 data.formConfig 子对象中(与后端 design.js getDefaultFormData().formConfig 字段名一致)。
// 这里做一次归一化:优先取 formConfig;如果历史数据是扁平结构(直接挂在 value 上),也做兼容。
formCfg() {
const v = this.value || {};
if (v.formConfig && typeof v.formConfig === 'object') {
return v.formConfig;
}
return v;
},
// 合并后的表单配置(支持平台差异化)
mergedFormConfig() {
const v = this.value || {};
const fc = this.formCfg;
// 通用全局配置(与后端 design.js getDefaultFormData().formConfig 字段名一致)
const base = {
formPaddingX: fc.form_padding_x,
bannerPaddingX: fc.banner_padding_x,
tipPaddingX: fc.tip_padding_x,
rowPaddingY: fc.row_padding_y,
rowMinHeight: fc.row_min_height,
labelGap: fc.label_gap,
formBgColor: fc.form_bg_color,
rowDividerColor: fc.row_divider_color,
showRowDivider: this._coerceBool(fc.show_row_divider),
inputHeight: fc.input_height,
inputRadius: fc.input_radius,
labelWidthMode: fc.label_width_mode,
labelFixedWidth: fc.label_fixed_width,
labelFontSize: fc.label_font_size,
labelColor: fc.label_color,
requiredColor: fc.required_color,
checkedColor: fc.checked_color,
submitColor: fc.submit_color,
submitBgColor: fc.submit_bg_color,
tipFontSize: fc.tip_font_size,
tipColor: fc.tip_color,
tipBgColor: fc.tip_bg_color
};
// 移除 undefined / 空字符串(保留 0/false)
Object.keys(base).forEach(function(k) {
if (base[k] === undefined || base[k] === null || base[k] === '') {
delete base[k];
}
});
// 平台差异化配置(与后端 formConfig.terminal 字段名对齐)
// 优先级:value.form_config_terminal > value.terminal > value.form_config_platform
const result = Object.assign({}, base);
const termBlock = v.form_config_terminal || v.terminal || v.form_config_platform;
if (termBlock && typeof termBlock === 'object') {
Object.keys(termBlock).forEach(function(platform) {
var src = termBlock[platform];
if (!src) return;
// 嵌套值若为 snake_case(部分序列化路径),在此统一转 camelCase
var translated = {};
Object.keys(src).forEach(function(k) {
var camel = k.replace(/_([a-z])/g, function(_, c) { return c.toUpperCase(); });
translated[camel] = src[k];
});
result[platform] = translated;
});
}
return result;
}
},
methods: {
// 初始化表单 - 将 diy-form 的数据格式转换成 ns-newform 的格式
initForm() {
if (!this.value) {
this.formComponents = [];
return;
}
var components = this.value.formComponents || this.value.form_components || [];
// 转换格式:type -> controller
this.formComponents = (components || []).map(function(item) {
return {
controller: item.type,
value: item.value || {},
val: (item.value && item.value.default) ? item.value.default : '' // 添加初始值
};
});
},
// 数字保底:undefined/空串/非数字时返回兜底值
_num(v, fb) {
if (v === '' || v === null || v === undefined) return fb;
var n = parseInt(v);
return isNaN(n) ? fb : n;
},
// 布尔保底:0/false/'0'/'false' → false;其它有值 → true
_coerceBool(v) {
if (v === false || v === 0 || v === '0' || v === 'false' || v === null || v === undefined) {
return false;
}
return true;
},
// 提交表单
submitForm() {
var self = this;
// 使用 ns-newform 的验证
if (this.$refs.nsNewform && this.$refs.nsNewform.verify) {
var verifyResult = this.$refs.nsNewform.verify();
if (!verifyResult) {
return;
}
}
// 获取表单数据
var submitData = {};
if (this.$refs.nsNewform && this.$refs.nsNewform.formData) {
this.$refs.nsNewform.formData.forEach(function(item, index) {
submitData[index] = item.val;
});
}
// 提交到后端
this.$api.sendRequest({
url: '/form/api/form/create',
data: {
form_id: this.value.form_id || 0,
form_data: JSON.stringify(submitData),
component_id: this.value.id || '',
form_name: this.value.form_name || '默认表单'
},
success: function(res) {
if (res.code === 0) {
uni.showToast({
title: '提交成功',
icon: 'success'
});
// 重置表单
if (self.$refs.nsNewform && self.$refs.nsNewform.setFormData) {
self.$refs.nsNewform.setFormData();
}
} else {
uni.showToast({
title: res.message || '提交失败',
icon: 'none'
});
}
}
});
}
}
};
</script>
<style lang="scss">
.diy-from {
background: #fff;
padding: 0;
border-radius: 10rpx;
.form-banner {
width: 100%;
overflow: hidden;
// 默认左右无边距(与后端 --banner-padding-x 对齐;由内联 bannerStyle 覆盖)
margin: 0;
.banner-img {
width: 100%;
display: block;
}
}
.form-tip {
// 提示语:上下内边距 / 左右内边距 / 字号 / 颜色 / 背景 全部由 tipStyle 内联写入
padding: 0;
text-align: center;
line-height: 1.5;
}
.form-submit-wrap {
// 外层:与后端 .form-preview-submit 对齐(padding-y + form-padding-x + form-bg-color)
// 全部由 submitWrapStyle 内联写入
box-sizing: border-box;
width: 100%;
}
.form-submit {
// 提交按钮:颜色 / 背景色 / 行高 / 圆角 / 字号 全部由 submitStyle 内联写入
display: flex;
align-items: center;
justify-content: center;
width: 100%;
font-weight: 500;
letter-spacing: 1rpx;
transition: opacity 0.2s ease;
box-sizing: border-box;
&:active {
opacity: 0.85;
}
}
}
</style>