Files
lucky_shop/components-diy/diy-form.vue
ZF sun fe9cbcd21d refactor(form, diy-form): 重构表单组件复用逻辑,新增raw模式适配现有表单数据
1. 新增diy-form组件的raw数据源模式,支持直接透传ns-newform格式的表单数据
2. 新增showSubmit配置项,支持隐藏内置提交按钮由父页面自行实现提交逻辑
3. 暴露verify和getFormData方法供父组件调用
4. 替换pages_tool/form/formdata.vue的旧表单组件为diy-form,适配新的调用方式
5. 新增样式穿透修复diy-form默认样式与原有页面风格不一致的问题
2026-06-12 11:09:08 +08:00

356 lines
13 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" :style="diyFormStyle">
<!-- 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)。
showSubmit=false 时由父级页面渲染自己的提交按钮并通过 ref 调用本组件的 verify()/getFormData()。 -->
<view v-if="showSubmit" 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
},
// 数据源模式:
// 'diy'(默认):value.formComponents 是 [{ type, value }],组件内部转换为 ns-newform 期望的 [{ controller, value, val }]
// 'raw' :value.jsonData / value.json_data 已经是 ns-newform 期望的格式,直接透传给 ns-newform
// 用途:让 formdata 等"已是 ns-newform 格式"的页面也能复用本组件的 banner/tip/样式系统
dataMode: {
type: String,
default: 'diy',
validator: (v) => ['diy', 'raw'].includes(v)
},
// 是否渲染内置提交按钮。false 时由父级调用 ref 上的 verify()/getFormData() 自行实现提交
showSubmit: {
type: Boolean,
default: true
}
},
mixins: [DiyMinx],
data() {
return {
formComponents: [] // 转换后的表单组件
};
},
created() {
this.initForm();
},
watch: {
'value': {
deep: true,
handler() {
// raw 模式:formData 由父级透传,跳过重跑 initForm(避免每次父组件 re-render 触发 ns-newform 的深拷贝 initFormData)
if (this.dataMode === 'raw') return;
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;
},
// Banner 容器左右内边距(与后端 .form-preview-banner padding-x 对齐)
bannerStyle() {
return this.$util.objToStyle({
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 this.$util.objToStyle({
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 this.$util.objToStyle({
padding: `${this._num(cfg.rowPaddingY, 4)}rpx ${this._num(cfg.formPaddingX, 14)}rpx`,
'background-color': cfg.formBgColor || '#ffffff'
});
},
// 提交按钮样式:颜色 / 背景色 / 宽度(百分比)/ 上下内边距(控制高度)/ 圆角 / 字号
// 与后端 design.css .form-preview-submit button 对齐:
// - 高度由 --submit-padding-y + 字号决定,避免硬编码 line-height
// - 宽度由 --submit-width-percent 控制(10-100,clamp)
submitStyle() {
const cfg = this.mergedFormConfig || {};
// 宽度百分比:clamp 到 10-100
const widthPct = Math.min(100, Math.max(10, this._num(cfg.submitWidthPercent, 100)));
// 上下内边距:与后端 design.js submitSizeMap 对齐(small/medium/large → 8/16/28 rpx)
const submitPaddingMap = { small: 8, medium: 16, large: 28 };
const submitPaddingY = submitPaddingMap[cfg.submitSize] != null
? submitPaddingMap[cfg.submitSize]
: submitPaddingMap.medium;
return this.$util.objToStyle({
width: widthPct + '%',
padding: `${submitPaddingY}rpx 0`,
color: cfg.submitColor || '#ffffff',
'background-color': cfg.submitBgColor || '#ff4544',
'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 || '提交';
},
// 后端把表单样式配置统一存放在 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,
// 提交按钮尺寸:宽度百分比 + 尺寸预设(与后端 design.js 对齐)
submitWidthPercent: fc.submit_width_percent,
submitSize: fc.submit_size,
// 提示语
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;
}
// raw 模式:value.jsonData / value.json_data 已经是 ns-newform 期望的 [{ controller, value, val }],直接透传
if (this.dataMode === 'raw') {
this.formComponents = this.value.jsonData || this.value.json_data || [];
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 : '' // 添加初始值
};
});
},
// 暴露给父级:触发 ns-newform 校验。校验失败时 ns-newform 内部会 toast;返回 false 表示失败,返回 formData 数组表示成功
verify() {
if (this.$refs.nsNewform && this.$refs.nsNewform.verify) {
return this.$refs.nsNewform.verify();
}
return false;
},
// 暴露给父级:获取 ns-newform 当前的 formData 数组(含 val / option_lists / img_lists 等全部字段)
// 后端 /form/api/form/create 的 form_data 字段约定是 JSON.stringify 整个数组(与 form.vue 保持一致),
// 父级若需要 { index: val } 形式可自行 map
getFormData() {
return (this.$refs.nsNewform && this.$refs.nsNewform.formData) || [];
},
// 数字保底: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;
// 校验(同时返回 formData 数组;校验失败时 ns-newform 内部 toast 提示)
if (!this.verify()) {
return;
}
// 转为 { index: val } 提交格式(后端 /form/api/form/create 期望的紧凑结构)
var submitData = {};
this.getFormData().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 + flex 居中)
// padding/background 由 submitWrapStyle 内联写入
// display: flex + justify-content: center 是让宽度小于 100% 的按钮水平居中的关键
box-sizing: border-box;
width: 100%;
display: flex;
justify-content: center;
}
.form-submit {
// 提交按钮:颜色 / 背景色 / 宽度(百分比)/ 上下内边距(控制高度)/ 圆角 / 字号
// 全部由 submitStyle 内联写入
// 宽度不再写死 100%,由内联 style 的 width: <pct>% 决定;max-width: 100% 防止溢出
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
font-weight: 500;
letter-spacing: 1rpx;
transition: opacity 0.2s ease;
box-sizing: border-box;
&:active {
opacity: 0.85;
}
}
}
</style>