1. 对齐ns-form和ns-newform的textarea配置参数,新增textareaRows和textareaLineHeight配置 2. 移除平台分支判断,所有平台统一使用:rows+行高计算高度的方案 3. 适配鸿蒙ArkUI原生TextArea,通过:rows控制行数,JS动态计算高度 4. 移除textarea硬编码高度,改为通过formConfig动态控制
413 lines
17 KiB
Vue
413 lines
17 KiB
Vue
<template>
|
||
<view data-component-name="diy-form" class="diy-from" :style="diyFormStyle">
|
||
<!-- Banner -->
|
||
<!-- 鸿蒙元服务运行时在首次 _render 时 value 可能还是 undefined,
|
||
下面所有 value.xxx 模板表达式都必须用 `value && value.xxx` 守护,
|
||
否则会触发 MP 编译器注入的 propsToData 观察者(对应编译产物里
|
||
r=function(){var o=this.value.show_banner...this.$mp.data...})
|
||
报 "Cannot read properties of undefined (reading 'show_banner')"。 -->
|
||
<view v-if="value && 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 && value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value && value.form_tip }}</view>
|
||
<!-- 表单组件 -->
|
||
<ns-newform
|
||
:data="formComponents"
|
||
:customAttr="value"
|
||
:mode="mergedFormConfig.formLayout || '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="handleSubmit" class="form-submit" :style="submitStyle">{{ submitText }}</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 自定义表单
|
||
// 提交接管三档(不依赖 $listeners / 事件机制,跨 H5 / MP / 鸿蒙一致):
|
||
// 1) 默认 → showSubmit=true + 不传 onSubmit → 内置按钮 + 内置 defaultSubmit
|
||
// 2) 复用按钮、自定义逻辑 → showSubmit=true + :onSubmit="fn" → 内置按钮 + 父级 fn
|
||
// 3) 完全自定义 UI → showSubmit=false + :onSubmit="fn" 或 ref.verify()/getFormData() → 父级全控
|
||
// 注:type: Function prop 在 uni-app MP 端走 Vue runtime 注入,机制与 ns-navbar.customBack 一致
|
||
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
|
||
},
|
||
// 父级可选地接管提交:传入函数 prop 时,verify 通过后由它处理 formData;
|
||
// 不传时走组件内置 defaultSubmit(兜底)。default 必须是 null,不能写成函数返回 null
|
||
onSubmit: {
|
||
type: Function,
|
||
default: null
|
||
}
|
||
},
|
||
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() {
|
||
// 鸿蒙元服务运行时首次 _render 时 this.value 可能还是 undefined,必须全程加护
|
||
var css = '';
|
||
var v = this.value || {};
|
||
css += 'background-color:' + (v.contentBgColor || '') + ';';
|
||
if (v.elementAngle === 'round') {
|
||
css += 'border-top-left-radius:' + (v.topElementAroundRadius || 0) * 2 + 'rpx;';
|
||
css += 'border-top-right-radius:' + (v.topElementAroundRadius || 0) * 2 + 'rpx;';
|
||
css += 'border-bottom-left-radius:' + (v.bottomElementAroundRadius || 0) * 2 + 'rpx;';
|
||
css += 'border-bottom-right-radius:' + (v.bottomElementAroundRadius || 0) * 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、border-bottom(与后端 .form-preview-tip 对齐)
|
||
tipStyle() {
|
||
const cfg = this.mergedFormConfig || {};
|
||
const style = {
|
||
padding: `${this._num(cfg.rowPaddingY, 4)}rpx ${this._num(cfg.tipPaddingX, 14)}rpx`,
|
||
'font-size': `${this._num(cfg.tipFontSize, 32)}rpx`,
|
||
'color': cfg.tipColor || '#000000',
|
||
'background-color': cfg.tipBgColor || 'transparent'
|
||
};
|
||
// 仅在配置了 tipBorderColor 时添加底线,避免 transparent 占位仍增加 2rpx 高度
|
||
if (cfg.tipBorderColor) {
|
||
style['border-bottom'] = `2rpx solid ${cfg.tipBorderColor}`;
|
||
}
|
||
return this.$util.objToStyle(style);
|
||
},
|
||
// 提交按钮外层:与后端 .form-preview-submit 对齐(margin-top + padding: row-padding-y form-padding-x;background: form-bg-color)
|
||
submitWrapStyle() {
|
||
const cfg = this.mergedFormConfig || {};
|
||
return this.$util.objToStyle({
|
||
marginTop: `${this._num(cfg.submitMarginTop, 0)}rpx`,
|
||
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 对齐(mini/small/medium/large/huge → 0/4/12/24/40 rpx)
|
||
const submitPaddingMap = { mini: 0, small: 4, medium: 12, large: 24, huge: 40 };
|
||
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 覆盖
|
||
// 走 mergedFormConfig(兼容 camelCase / snake_case),让 formConfig.submitText 能正确覆盖
|
||
submitText() {
|
||
return this.mergedFormConfig.submitText || '提交';
|
||
},
|
||
// 父级是否接管提交(集中判断,便于以后替换机制;避免多个调用点各自 typeof 校验)
|
||
hasCustomSubmit() {
|
||
return typeof this.onSubmit === 'function';
|
||
},
|
||
// 后端把表单样式配置统一存放在 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;
|
||
// 后端实际返回 camelCase(如 formPaddingX / formBgColor / labelFontSize),
|
||
// 但旧版 design.js / 部分序列化路径可能用 snake_case(form_padding_x)。这里同时读两种写法,camelCase 优先
|
||
const self = this;
|
||
const pick = function(camel, snake) {
|
||
var v1 = fc[camel];
|
||
if (v1 !== undefined && v1 !== null && v1 !== '') return v1;
|
||
return fc[snake];
|
||
};
|
||
// 通用全局配置(与后端 design.js getDefaultFormData().formConfig 字段名一致)
|
||
const base = {
|
||
formPaddingX: pick('formPaddingX', 'form_padding_x'),
|
||
bannerPaddingX: pick('bannerPaddingX', 'banner_padding_x'),
|
||
tipPaddingX: pick('tipPaddingX', 'tip_padding_x'),
|
||
rowPaddingY: pick('rowPaddingY', 'row_padding_y'),
|
||
rowMinHeight: pick('rowMinHeight', 'row_min_height'),
|
||
labelGap: pick('labelGap', 'label_gap'),
|
||
formBgColor: pick('formBgColor', 'form_bg_color'),
|
||
rowDividerColor: pick('rowDividerColor', 'row_divider_color'),
|
||
showRowDivider: self._coerceBool(pick('showRowDivider', 'show_row_divider')),
|
||
inputHeight: pick('inputHeight', 'input_height'),
|
||
inputRadius: pick('inputRadius', 'input_radius'),
|
||
formLayout: pick('formLayout', 'form_layout'),
|
||
labelWidthMode: pick('labelWidthMode', 'label_width_mode'),
|
||
labelFixedWidth: pick('labelFixedWidth', 'label_fixed_width'),
|
||
labelFontSize: pick('labelFontSize', 'label_font_size'),
|
||
labelColor: pick('labelColor', 'label_color'),
|
||
labelFontWeight: pick('labelFontWeight', 'label_font_weight'),
|
||
labelAutoMaxWidth: pick('labelAutoMaxWidth', 'label_auto_max_width'),
|
||
requiredColor: pick('requiredColor', 'required_color'),
|
||
checkedColor: pick('checkedColor', 'checked_color'),
|
||
// 占位符:遗漏会被 ns-newform SCSS 默认值回退覆盖
|
||
placeholderColor: pick('placeholderColor', 'placeholder_color'),
|
||
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
|
||
// textarea 显示行数 / 行高(透传给 ns-newform,与 ns-form 保持一致)
|
||
// 不传时 ns-newform 走自己的默认 4 行 / 44rpx
|
||
textareaRows: pick('textareaRows', 'textarea_rows'),
|
||
textareaLineHeight: pick('textareaLineHeight', 'textarea_line_height'),
|
||
// 输入框视觉增强(与 ns-newform 对齐)
|
||
inputBorderColor: pick('inputBorderColor', 'input_border_color'),
|
||
inputBgColor: pick('inputBgColor', 'input_bg_color'),
|
||
inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'),
|
||
inputFocusBgColor: pick('inputFocusBgColor', 'input_focus_bg_color'),
|
||
submitText: pick('submitText', 'submit_text'),
|
||
submitColor: pick('submitColor', 'submit_color'),
|
||
submitBgColor: pick('submitBgColor', 'submit_bg_color'),
|
||
// 提交按钮尺寸:宽度百分比 + 尺寸预设(与后端 design.js 对齐)
|
||
submitWidthPercent: pick('submitWidthPercent', 'submit_width_percent'),
|
||
submitSize: pick('submitSize', 'submit_size'),
|
||
submitMarginTop: pick('submitMarginTop', 'submit_margin_top'),
|
||
// 提示语
|
||
tipFontSize: pick('tipFontSize', 'tip_font_size'),
|
||
tipColor: pick('tipColor', 'tip_color'),
|
||
tipBgColor: pick('tipBgColor', 'tip_bg_color'),
|
||
tipBorderColor: pick('tipBorderColor', 'tip_border_color')
|
||
};
|
||
// 移除 undefined / 空字符串(保留 0/false)
|
||
Object.keys(base).forEach(function(k) {
|
||
if (base[k] === undefined || base[k] === null || base[k] === '') {
|
||
delete base[k];
|
||
}
|
||
});
|
||
// 平台差异化配置(terminal 块在 formConfig 子对象内)
|
||
// 优先级:v.formConfig.terminal > v.form_config_terminal > v.terminal > v.form_config_platform
|
||
const result = Object.assign({}, base);
|
||
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) {
|
||
var src = termBlock[platform];
|
||
if (!src) return;
|
||
// 嵌套值若为 snake_case(部分序列化路径),在此统一转 camelCase
|
||
var translated = {};
|
||
Object.keys(src).forEach(function(k) {
|
||
translated[self.$util.snakeToCamel(k)] = 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 等全部字段)
|
||
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;
|
||
},
|
||
|
||
// 统一提交入口:内置按钮 / 父级 ref 调用都走这里
|
||
// 不依赖 $listeners / 事件机制(MP 端 $listeners 为 undefined),跨端一致
|
||
handleSubmit() {
|
||
// 校验(同时返回 formData 数组;校验失败时 ns-newform 内部 toast 提示)
|
||
if (!this.verify()) {
|
||
return;
|
||
}
|
||
const formData = this.getFormData();
|
||
// 父级显式接管:function prop 在 H5 / MP / 鸿蒙一致可用
|
||
if (this.hasCustomSubmit) {
|
||
return this.onSubmit(formData);
|
||
}
|
||
// 兜底:内置 sendRequest 流程
|
||
this.defaultSubmit(formData);
|
||
},
|
||
|
||
// 公共兜底实现:默认的 /form/api/form/create 流程;
|
||
// 父级若需"先自定义、失败回退",可经 ref.defaultSubmit(formData) 调用
|
||
defaultSubmit(submitFormData) {
|
||
var self = this;
|
||
this.$api.sendRequest({
|
||
url: '/form/api/form/create',
|
||
data: {
|
||
form_id: this.value.form_id || 0,
|
||
form_data: typeof submitFormData === 'string' ? submitFormData : JSON.stringify(submitFormData),
|
||
component_id: this.value.id || '',
|
||
form_name: this.value.form_name || '默认表单'
|
||
},
|
||
success: function(res) {
|
||
if (res.code === 0) {
|
||
uni.showToast({
|
||
title: self.$lang('submitSuccess'),
|
||
icon: 'success'
|
||
});
|
||
// 重置表单
|
||
if (self.$refs.nsNewform && self.$refs.nsNewform.setFormData) {
|
||
self.$refs.nsNewform.setFormData();
|
||
}
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || self.$lang('submitFail'),
|
||
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 对齐(margin-top + padding-y + form-padding-x + form-bg-color + flex 居中)
|
||
// margin-top/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>
|