import { IPublicTypeComponentMetadata, IPublicTypeSnippet } from '@alilc/lowcode-types' const CustomFormMeta: IPublicTypeComponentMetadata = { group: '低代码组件', componentName: 'CustomForm', title: '自定义表单', docUrl: '', screenshot: '', devMode: 'proCode', category: '信息输入', npm: { package: '@dckj-npm/dc-material', version: '0.1.359', exportName: 'CustomForm', main: 'src/index.tsx', destructuring: true, subName: '', }, configure: { component: { isContainer: true, }, props: [ // ───────────────────────────────────────────────────────────────────── // 分组一:全局布局 // ───────────────────────────────────────────────────────────────────── { type: 'group', title: '全局布局', name: 'layoutGroup', display: 'accordion', items: [ { title: { label: '列数', tip: '表单的整体列数,所有表单项按此列数排列。\n单列适合简单表单,多列适合信息密集的编辑页面。', }, name: 'columns', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '一列', value: 1 }, { title: '二列', value: 2 }, { title: '三列', value: 3 }, { title: '四列', value: 4 }, ], }, initialValue: 1, }, }, { title: { label: '标签位置', tip: '表单标签相对于输入框的位置:\n· 顶部(top):标签在输入框上方,适合移动端或字段较多时\n· 左侧(left):标签在输入框左侧,传统表单样式,配合「标签宽度」使用\n· 内嵌(inset):标签显示在输入框内部,输入时标签浮动到上方', }, name: 'labelAlign', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '顶部', value: 'top' }, { title: '左侧', value: 'left' }, { title: '内嵌', value: 'inset' }, ], }, initialValue: 'top', }, extraProps: { setValue: (target: any, value: string) => { if (value === 'left') { target.getProps().setPropValue('labelCol', { fixedSpan: 4 }) } else { target.getProps().setPropValue('labelCol', null) } target.getProps().setPropValue('labelAlign', value) }, }, }, { title: { label: '标签宽度', tip: '当标签位置为「左侧」时,设置标签列的固定宽度(格宫列数,1~24)。\nJSON 示例:{ "fixedSpan": 6 } 意为标签占 6 格。\n不填则自动宽度。常用值:4(短标签)、6(中等标签)、8(长标签)', }, name: 'labelCol.fixedSpan', condition: (target: any) => target.getProps().getPropValue('labelAlign') === 'left', setter: { componentName: 'NumberSetter', props: { min: 1, max: 12 }, initialValue: 4, }, }, { title: { label: '行间距', tip: '每行表单项之间的上下间距(像素)。\n默认 0。建议设为 8~16px 让表单更舒适。', }, name: '!rowGap', extraProps: { getValue: (target: any) => { const sp = target.getProps().getPropValue('spacing') return Array.isArray(sp) ? (sp[0] ?? 0) : 0 }, setValue: (target: any, value: number) => { const sp = target.getProps().getPropValue('spacing') const colGap = Array.isArray(sp) ? (sp[1] ?? 16) : 16 target.getProps().setPropValue('spacing', [value, colGap]) }, }, setter: { componentName: 'NumberSetter', props: { min: 0, max: 100 }, initialValue: 0, }, }, { title: { label: '列间距', tip: '多列表单中各列之间的左右间距(像素)。\n默认 16px。单列表单中此设置无明显效果。', }, name: '!colGap', extraProps: { getValue: (target: any) => { const sp = target.getProps().getPropValue('spacing') return Array.isArray(sp) ? (sp[1] ?? 16) : 16 }, setValue: (target: any, value: number) => { const sp = target.getProps().getPropValue('spacing') const rowGap = Array.isArray(sp) ? (sp[0] ?? 0) : 0 target.getProps().setPropValue('spacing', [rowGap, value]) }, }, setter: { componentName: 'NumberSetter', props: { min: 0, max: 100 }, initialValue: 16, }, }, { title: { label: '组件宽度占满', tip: '全局控制所有表单项的输入组件是否撑满容器宽度(100%)。\n开启后相当为每个表单项设置 fullWidth=true。\n单个表单项的「宽度占满」设置优先级更高,可覆盖此全局值。', }, name: 'fullWidth', setter: { componentName: 'BoolSetter', initialValue: true, }, }, { title: { label: '表单状态', tip: '编辑态:正常填写表单。\n只读态:所有输入框变为纯文本显示,空值显示"—"。\n常用于"查看详情"场景,绑定变量可动态切换编辑/只读。', }, name: '!status', extraProps: { getValue: (target: any) => { return target.getProps().getPropValue('isPreview') ? 'readonly' : 'editable' }, setValue: (target: any, value: string) => { target.getProps().setPropValue('isPreview', value === 'readonly') }, }, setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '编辑', value: 'editable' }, { title: '只读', value: 'readonly' }, ], }, initialValue: 'editable', }, }, { title: { label: '空态文案', tip: '当没有任何表单项时展示的提示文字。', }, name: 'emptyContent', setter: { componentName: 'StringSetter', isRequired: false, initialValue: '添加表单项', }, }, ], }, // ───────────────────────────────────────────────────────────────────── // 分组二:表单项配置 // ───────────────────────────────────────────────────────────────────── { title: { label: '表单项', tip: '配置表单中每一行的字段。\n每个表单项需要填写「字段名」和「标题」,其余可选。\n字段名(field)是提交数据时的 key,需要与后端接口字段名保持一致。', }, name: 'formItems', setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ // ── 基础 { title: { label: '字段名', tip: '必填。提交表单时该字段的 key 名称,需与后端接口字段名一致。\n例如:user_name、phone、banquet_date', }, name: 'field', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, { title: { label: '标题', tip: '表单项左侧(或上方)显示的文字标签。\n例如:姓名、手机号、宴请日期', }, name: 'label', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, { title: { label: '必填', tip: '开启后标题旁显示红色星号,并在提交时自动校验该字段不能为空。', }, name: 'required', setter: { componentName: 'BoolSetter', isRequired: false, initialValue: false, }, }, // ── 组件类型 { title: { label: '组件类型', tip: '选择该字段使用哪种输入控件:\n· 输入框(Input):单行文本,最常用\n· 多行文本(TextArea):适合备注、描述\n· 下拉选择(Select):从预设选项中选一个\n· 单选组(RadioGroup):横排单选按钮\n· 复选组(CheckboxGroup):多选框\n· 数字输入(NumberPicker):带加减按钮的数字\n· 日期选择(DatePicker):日历弹出选日期\n· 上传(Upload):文件/图片上传', }, name: 'componentType', important: true, display: 'inline', setter: { componentName: 'SelectSetter', props: { options: [ { label: '输入框 (Input)', value: 'Input' }, { label: '多行文本 (TextArea)', value: 'TextArea' }, { label: '下拉选择 (Select)', value: 'Select' }, { label: '单选组 (RadioGroup)', value: 'RadioGroup' }, { label: '复选组 (CheckboxGroup)', value: 'CheckboxGroup' }, { label: '数字输入 (NumberPicker)', value: 'NumberPicker' }, { label: '日期选择 (DatePicker)', value: 'DatePicker' }, { label: '日期时间 (DateTimePicker)', value: 'DateTimePicker' }, { label: '上传 (Upload)', value: 'Upload' }, ], }, initialValue: 'Input', }, }, // ── 输入提示(仅对支持 placeholder 的组件显示) { title: { label: '输入提示', tip: '显示在输入框内的灰色提示文字(placeholder)。\n例如:请输入姓名、请选择日期', }, name: 'placeholder', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return !['Upload', 'RadioGroup', 'CheckboxGroup'].includes(t) }, setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, // ── 默认值 { title: { label: '默认值', tip: '该字段的初始默认值,表单打开时自动填入。\n· 文本类型填写字符串,例如:待处理\n· 数字类型填写数字,例如:0\n· 日期类型填写格式化字符串,例如:2026-01-01\n· 下拉/单选填写选项的 value 值,例如:male\n· 复选框填写数组,例如:["a","b"]\n\n优先级高于"全局初始值"(initialValues)中同名字段。', }, name: 'initialValue', setter: { componentName: 'MixedSetter', props: { setters: [ { componentName: 'StringSetter', title: '字符串' }, { componentName: 'NumberSetter', title: '数字' }, { componentName: 'BoolSetter', title: '布尔' }, { componentName: 'JsonSetter', title: 'JSON(数组/对象)' }, ], }, isRequired: false, }, }, // ── 选项(下拉/单选/复选) { title: { label: '选项列表', tip: '仅在组件类型为「下拉选择」「单选组」「复选组」时生效。\n每个选项需配置:\n· 名称(label):界面上显示的文字,例如:男\n· 值(value):提交时实际传给后端的值,例如:male', }, name: 'options', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType') return ['Select','RadioGroup','CheckboxGroup'].includes(t) }, setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ { title: { label: '名称', tip: '选项显示的文字' }, name: 'label', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, { title: { label: '值', tip: '提交时传给后端的实际值' }, name: 'value', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, ], }, }, }, }, initialValue: [], }, }, // ── 动态选项绑定(阶段二) { title: { label: '动态选项绑定', tip: '仅在组件类型为「下拉选择」「单选组」「复选组」时生效。\n绑定后优先级高于上方「选项列表」(静态配置)。\n支持绑定页面变量或数据源返回的数组,数组格式为 [{label, value}, ...]。\n常见用法:将套餐列表、城市列表等接口数据直接绑定到选项。\n若接口返回的数据字段名不是 label/value,请配合下方「选项显示字段名」和「选项值字段名」使用。', }, name: 'optionsBind', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType') return ['Select','RadioGroup','CheckboxGroup'].includes(t) }, setter: { componentName: 'SetterFormVariable', props: { attributes: [ { label: '选项数据', value: 'optionsBind', children: [ { label: '显示文字', isRequire: true, value: 'label' }, { label: '选项值', isRequire: true, value: 'value' }, ], }, ], }, }, }, // ── 动态选项字段映射(当数据源字段名不是 label/value 时使用) { title: { label: '选项显示字段名', tip: '当「动态选项绑定」的数据源字段名不是 label 时填写。\n例如后端返回 {package_name, id, ...},想用 package_name 做显示文字,则填 "package_name"。\n不填则默认使用 label 字段。', }, name: 'optionsLabelKey', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType') return ['Select','RadioGroup','CheckboxGroup'].includes(t) }, setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, { title: { label: '选项值字段名', tip: '当「动态选项绑定」的数据源字段名不是 value 时填写。\n例如后端返回 {package_name, id, ...}:\n· 想用 id 做存储值 → 填 "id"\n· 想用 package_name 做存储值 → 填 "package_name"\n不填则默认使用 value 字段。\n配置后,字段联动规则的「匹配字段名 matchKey」会自动默认使用此字段名,无需重复配置。', }, name: 'optionsValueKey', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType') return ['Select','RadioGroup','CheckboxGroup'].includes(t) }, setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, // ── 布局 { title: { label: '列跨度', tip: '该表单项横向占几列(不超过表单总列数)。\n例如总列数为 4,设置为 2 则占一半宽度,设置为 4 则独占一行。', }, name: 'columnSpan', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '1格', value: 1 }, { title: '2格', value: 2 }, { title: '3格', value: 3 }, { title: '4格', value: 4 }, ], }, initialValue: 1, }, }, { title: { label: '尺寸', tip: '单个表单项的组件尺寸,优先级高于表单全局 size。\n不设置时继承表单全局尺寸(默认 medium)。', }, name: 'size', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '小', value: 'small' }, { title: '中', value: 'medium' }, { title: '大', value: 'large' }, ], }, }, }, { title: { label: '宽度占满', tip: '开启后该表单项的输入组件宽度为 100%,撑满所在列的宽度。\n默认开启,通常不需要关闭。', }, name: 'fullWidth', setter: { componentName: 'BoolSetter', initialValue: true, }, }, // ── 提示信息 { title: { label: '错误提示', tip: '校验失败时显示的自定义错误文字,涵盖必填为空与正则/格式不匹配两种情况。\n不填则由组件自动生成,例如"xxx 不能为空"或"格式不正确"。\n示例:请输入正确的 11 位手机号', }, name: '!helpMsg', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('help') || target.parent?.getPropValue?.('formItemProps')?.requiredMessage || '', setValue: (target: any, value: string) => { target.parent?.setPropValue?.('help', value || undefined) const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (!value) { delete fp.requiredMessage } else { fp.requiredMessage = value } target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, { title: { label: '补充说明', tip: '始终显示在输入框下方的灰色提示文字,与错误提示共存,用于告知用户填写要求或格式。\n例如:手机号用于接收预订通知短信', }, name: 'extra', setter: { componentName: 'StringSetter', isRequired: false, initialValue: '', }, }, // ── 组件属性(结构化,按组件类型分组) // ── 通用:禁用 { title: { label: '禁用', tip: '开启后该输入组件变为禁用状态,用户无法编辑。', }, name: '!disabled', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return !['Upload'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.disabled, setValue: (target: any, value: boolean) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.disabled = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, // ── Input:最大字符数 / 只读 { title: { label: '最大字符数', tip: '输入框最多允许输入的字符数。\n超出后无法继续输入。配合「显示计数」使用效果更好。', }, name: '!maxLength', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.maxLength, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') { delete cp.maxLength } else { cp.maxLength = value } target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter', props: { min: 1 } }, }, { title: { label: '显示计数', tip: '开启后在输入框右下角显示"已输入/最大字符数"计数器。\n需配合「最大字符数」使用。', }, name: '!hasLimitHint', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.hasLimitHint, setValue: (target: any, value: boolean) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.hasLimitHint = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, { title: { label: '只读', tip: '开启后输入框内容不可编辑(与禁用不同,只读不改变样式)。\n常用于"自动计算填入"的字段,如套餐价格。', }, name: '!readOnly', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.readOnly, setValue: (target: any, value: boolean) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.readOnly = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, // ── TextArea:行数 { title: { label: '显示行数', tip: '多行文本框默认显示的行数(高度)。默认 4 行。', }, name: '!rows', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'TextArea', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.rows, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.rows = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter', props: { min: 1, max: 20 }, initialValue: 4 }, }, // ── NumberPicker:范围 / 步长 / 精度 { title: { label: '最小值', tip: '数字输入框允许输入的最小值。', }, name: '!min', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.min, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete cp.min else cp.min = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter' }, }, { title: { label: '最大值', tip: '数字输入框允许输入的最大值。', }, name: '!max', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.max, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete cp.max else cp.max = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter' }, }, { title: { label: '步长', tip: '每次点击加减按钮时变化的数量。默认 1。', }, name: '!step', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.step, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete cp.step else cp.step = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter', props: { min: 0 }, initialValue: 1 }, }, { title: { label: '小数位数', tip: '数字保留几位小数。默认不限制(整数)。', }, name: '!precision', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.precision, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete cp.precision else cp.precision = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter', props: { min: 0, max: 10 } }, }, // ── Select:可搜索 / 可清除 / 多选模式 { title: { label: '可搜索', tip: '开启后下拉框支持输入关键字过滤选项。', }, name: '!showSearch', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Select', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.showSearch, setValue: (target: any, value: boolean) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.showSearch = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, { title: { label: '可清除', tip: '开启后选择框右侧显示清除图标,点击可清空已选值。', }, name: '!hasClear', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Select', 'DatePicker', 'DateTimePicker'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.hasClear, setValue: (target: any, value: boolean) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.hasClear = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, { title: { label: '多选模式', tip: '设置下拉框的选择模式:\n· 单选(single):只能选一个选项(默认)\n· 多选(multiple):可以选多个,以标签形式展示\n· 标签输入(tag):可以选多个,也支持直接输入新标签', }, name: '!mode', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Select', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.mode || 'single', setValue: (target: any, value: string) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.mode = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '单选', value: 'single' }, { title: '多选', value: 'multiple' }, { title: '标签', value: 'tag' }, ], }, initialValue: 'single', }, }, // ── DatePicker / DateTimePicker:日期格式 { title: { label: '日期格式', tip: '日期显示和提交的格式字符串。\n常用:\n· 仅日期:YYYY-MM-DD(默认)\n· 日期+时间:YYYY-MM-DD HH:mm\n· 年月:YYYY-MM', }, name: '!dateFormat', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['DatePicker', 'DateTimePicker'].includes(t) }, extraProps: { getValue: (target: any) => { const t = target.parent?.getPropValue?.('componentType') return target.parent?.getPropValue?.('componentProps')?.format || (t === 'DateTimePicker' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD') }, setValue: (target: any, value: string) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } cp.format = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'StringSetter', initialValue: 'YYYY-MM-DD' }, }, // ── Upload:文件类型 / 数量限制 { title: { label: '允许的文件类型', tip: '限制可上传的文件类型(传给 input[accept])。\n例如:image/* 只允许图片,.pdf 只允许 PDF,不填则不限制。', }, name: '!accept', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Upload', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.accept, setValue: (target: any, value: string) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (!value) delete cp.accept else cp.accept = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'StringSetter' }, }, { title: { label: '最多上传数量', tip: '允许上传的最多文件数量,超出后禁止继续上传。不填则不限制。', }, name: '!limit', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Upload', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.limit, setValue: (target: any, value: number) => { const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete cp.limit else cp.limit = value target.parent?.setPropValue?.('componentProps', cp) }, }, setter: { componentName: 'NumberSetter', props: { min: 1 } }, }, // ── 表单项控制 { title: { label: '自动校验', tip: '开启后每次字段值变化时自动触发校验,不需等到提交才验证。\n适合需要即时反馈的场景(如手机号格式)。', }, name: '!autoValidate', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.autoValidate, setValue: (target: any, value: boolean) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } fp.autoValidate = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, { title: { label: '隐藏冒号', tip: '开启后在标题文字后面不显示冒号。', }, name: '!colon', extraProps: { getValue: (target: any) => { const v = target.parent?.getPropValue?.('formItemProps')?.colon return v === false }, setValue: (target: any, value: boolean) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } fp.colon = !value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'BoolSetter', initialValue: false }, }, // ── 表单校验 // 最小长度(Input / TextArea / CheckboxGroup) { title: { label: '最少字符数', tip: 'Input/TextArea:输入字符不能少于此数量。\nCheckboxGroup:至少要选几项。', }, name: '!minLength', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea', 'CheckboxGroup'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.minLength, setValue: (target: any, value: number) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete fp.minLength else fp.minLength = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'NumberSetter', props: { min: 0 } }, }, // 最大长度(Input / TextArea / CheckboxGroup / Select multiple) { title: { label: '最多字符数(校验)', tip: 'Input/TextArea:输入字符不能超过此数量(校验维度,与组件层的最大字符数独立)。\nCheckboxGroup:最多能选几项。', }, name: '!maxLengthValidate', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea', 'CheckboxGroup'].includes(t) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.maxLength, setValue: (target: any, value: number) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete fp.maxLength else fp.maxLength = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'NumberSetter', props: { min: 0 } }, }, // 最小/最大值(NumberPicker) { title: { label: '最小数值(校验)', tip: '数字字段的最小合法值(校验维度)。', }, name: '!minValue', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.min, setValue: (target: any, value: number) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete fp.min else fp.min = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'NumberSetter' }, }, { title: { label: '最大数值(校验)', tip: '数字字段的最大合法值(校验维度)。', }, name: '!maxValue', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.max, setValue: (target: any, value: number) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (value === undefined || value === null || (value as any) === '') delete fp.max else fp.max = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'NumberSetter' }, }, // 正则校验(Input / TextArea)—— 提供预置规则 + 自定义 { title: { label: '正则校验', tip: '选择常用的格式校验规则,或选"自定义"后填写自己的正则表达式(无需前后加斜杠)。\n常用示例:\n· 手机号:^1[3-9]\\d{9}$\n· 邮箱:^[\\w.]+@[\\w.]+\\.[a-z]{2,}$\n· 正整数:^\\d+$\n· 身份证号:^\\d{17}[\\dXx]$', }, name: '!patternPreset', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea'].includes(t) }, extraProps: { getValue: (target: any) => { const pattern = target.parent?.getPropValue?.('formItemProps')?.pattern const presets: Record = { '^1[3-9]\\d{9}$': 'phone', '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$': 'email', '^\\d+$': 'integer', '^\\d+(\\.\\d+)?$': 'number', '^\\d{17}[\\dXx]$': 'idcard', '^[\\u4e00-\\u9fa5]+$': 'chinese', } return pattern ? (presets[pattern] || '__custom__') : '' }, setValue: (target: any, value: string) => { const patternMap: Record = { phone: '^1[3-9]\\d{9}$', email: '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$', integer: '^\\d+$', number: '^\\d+(\\.\\d+)?$', idcard: '^\\d{17}[\\dXx]$', chinese: '^[\\u4e00-\\u9fa5]+$', } const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (!value || value === '__none__') { delete fp.pattern } else if (value === '__custom__') { // 自定义时不修改 pattern,等用户在下方输入框填写 } else { fp.pattern = patternMap[value] } target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'SelectSetter', props: { options: [ { label: '不校验', value: '__none__' }, { label: '手机号(11位)', value: 'phone' }, { label: '邮箱地址', value: 'email' }, { label: '整数', value: 'integer' }, { label: '数字(含小数)', value: 'number' }, { label: '身份证号(15/18位)', value: 'idcard' }, { label: '纯中文', value: 'chinese' }, { label: '自定义正则…', value: '__custom__' }, ], }, initialValue: '__none__', }, }, { title: { label: '自定义正则', tip: '填写自定义正则表达式(无需前后加斜杠),例如:^1[3-9]\\d{9}$', }, name: '!pattern', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' if (!['Input', 'TextArea'].includes(t)) return false const preset = target.parent?.getPropValue?.('formItemProps')?.__patternPreset // 当预置选择了"自定义"时显示,或者 pattern 已有值但不匹配任何预置时也显示 const pattern = target.parent?.getPropValue?.('formItemProps')?.pattern const builtinPatterns = ['^1[3-9]\\d{9}$', '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$', '^\\d+$', '^\\d+(\\.\\d+)?$', '^\\d{17}[\\dXx]$', '^[\\u4e00-\\u9fa5]+$'] return preset === '__custom__' || (!!pattern && !builtinPatterns.includes(pattern)) }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.pattern, setValue: (target: any, value: string) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (!value) delete fp.pattern else fp.pattern = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'StringSetter' }, }, { title: { label: '正则校验失败提示', tip: '正则不匹配时显示的错误提示文字。', }, name: '!patternMessage', condition: (target: any) => { const t = target.parent?.getPropValue?.('componentType') || 'Input' return ['Input', 'TextArea'].includes(t) && !!target.parent?.getPropValue?.('formItemProps')?.pattern }, extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.patternMessage, setValue: (target: any, value: string) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (!value) delete fp.patternMessage else fp.patternMessage = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'StringSetter' }, }, // 格式校验(仅 Input) { title: { label: '格式校验', tip: '快速选择常用格式进行校验:\n· number:必须是数字\n· email:必须是邮箱格式\n· url:必须是网址格式\n· tel:必须是电话格式', }, name: '!format', condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Input', extraProps: { getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.format, setValue: (target: any, value: string) => { const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) } if (!value) delete fp.format else fp.format = value target.parent?.setPropValue?.('formItemProps', fp) }, }, setter: { componentName: 'SelectSetter', props: { options: [ { label: '不限制', value: '' }, { label: '数字 (number)', value: 'number' }, { label: '邮箱 (email)', value: 'email' }, { label: '网址 (url)', value: 'url' }, { label: '电话 (tel)', value: 'tel' }, ], }, initialValue: '', }, }, ], }, }, }, }, initialValue: [], }, }, // ───────────────────────────────────────────────────────────────────── // 字段初始化 // ───────────────────────────────────────────────────────────────────── { title: { label: '字段初始化', tip: '批量配置表单字段的初始值。\n\n· 字段名:与「表单项」中的「字段名」保持一致,例如 guest_count、user_name\n· 值类型:\n 固定值 — 直接填写静态默认值(字符串/数字/布尔/JSON)\n 变量绑定 — 绑定页面 state 或接口返回的动态变量\n\n💡 设置单个字段默认值时,也可在「表单项」列表中直接配置「默认值」,更快捷。', }, name: 'initialValues', extraProps: { getValue: (target: any) => { const iv = target.getProps().getPropValue('initialValues') if (!iv) return [] if (Array.isArray(iv)) { // 不过滤 field 为空的行,允许新添加的空行显示出来供用户填写 // 仅过滤非对象的元素(防御性兼容旧脏数据) return iv .filter((row: any) => row && typeof row === 'object') .map((row: any) => ({ field: typeof row.field === 'string' ? row.field.trim() : '', valueType: row.valueType === 'variable' ? 'variable' : 'fixed', value: row.value, })) } // 对象格式:key 不可能为空字符串,保留原有过滤 return Object.entries(iv as Record).map(([field, value]) => ({ field: String(field).trim(), valueType: 'fixed', value, })).filter((row) => row.field) }, setValue: (target: any, rows: any[]) => { if (!Array.isArray(rows)) return // 不过滤 field 为空的行:用户点击"添加一项"时新行 field 为空字符串, // 若此处过滤掉,ArraySetter 重读 getValue 后新行消失,表现为"添加无效"。 // 运行时 normalizeInitialValues 已忽略 field 为空的行,不影响实际行为。 const cleanedRows = rows .filter((row: any) => row && typeof row === 'object') .map((row: any) => ({ field: typeof row.field === 'string' ? row.field.trim() : '', valueType: row.valueType === 'variable' ? 'variable' : 'fixed', value: row.value, })) target.getProps().setPropValue('initialValues', cleanedRows) }, }, setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ { title: { label: '字段名', tip: '与「表单项」中的「字段名」保持一致,例如:guest_count、user_name', }, name: 'field', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, { title: { label: '值类型', tip: '· 固定值:静态初始值,例如"待处理"、10、true\n· 变量绑定:绑定页面 state 或动态数据,表单打开时从变量取值', }, name: 'valueType', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '固定值', value: 'fixed' }, { title: '变量绑定', value: 'variable' }, ], }, initialValue: 'fixed', }, }, { title: { label: '初始值', tip: '该字段的默认值。\n· 文本类填字符串,例如:待处理\n· 数字类填数字,例如:10\n· 下拉/单选填选项的 value 值,例如:male\n· 复选框填 JSON 数组,例如:["a","b"]', }, name: 'value', condition: (target: any) => (target.parent?.getPropValue?.('valueType') || 'fixed') === 'fixed', setter: { componentName: 'MixedSetter', props: { setters: [ { componentName: 'StringSetter', title: '字符串' }, { componentName: 'NumberSetter', title: '数字' }, { componentName: 'BoolSetter', title: '布尔' }, { componentName: 'JsonSetter', title: 'JSON(数组/对象)' }, ], }, }, }, { title: { label: '变量绑定', tip: '绑定页面 state 或外部传入的变量,表单打开时从该变量读取初始值。\n例如:绑定 state.selectedPackage 可将上一步选择的套餐自动填入。', }, name: 'value', condition: (target: any) => target.parent?.getPropValue?.('valueType') === 'variable', setter: { componentName: 'VariableSetter', }, extraProps: { supportVariable: true, }, }, ], }, }, initialValue: () => ({ field: '', valueType: 'fixed', value: '' }), }, }, initialValue: [], }, }, // ───────────────────────────────────────────────────────────────────── // 计算字段 // ───────────────────────────────────────────────────────────────────── { title: { label: '计算字段', tip: '声明由多个表单字段自动拼接/计算出的衍生字段,每行一条规则。\n\n工作原理:每次任意字段值变化时自动重新计算,结果写入目标字段,提交时自动携带。\n\n表达式写法:用 {字段名} 引用表单中其他字段的当前值\n 例如:{package_name},{package_price}元,预约日期:{banquet_date}\n\n配置步骤:\n ① 目标字段名 = 要写入的字段名,例如:order_desc\n ② 表达式 = 输入模板文字,用 {字段名} 插入字段值\n ③ 保存后切换套餐/填值即可看到目标字段自动更新\n\n无需编写任何 JS 逻辑,配置即生效。', }, name: 'computedFields', extraProps: { getValue: (target: any) => { const cf = target.getProps().getPropValue('computedFields') if (!cf) return [] if (Array.isArray(cf)) return cf return Object.entries(cf as Record).map(([targetField, template]) => ({ targetField, template })) }, setValue: (target: any, value: any[]) => { if (!Array.isArray(value)) return target.getProps().setPropValue('computedFields', value.filter(Boolean)) }, }, setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ { title: { label: '目标字段名', tip: '计算结果写入的字段名,提交时携带此字段。\n建议取语义化名称,例如:order_desc、booking_summary\n该字段不需要出现在上方「表单项」中,提交时会自动追加到表单值里。', }, name: 'targetField', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' }, }, { title: { label: '表达式', tip: '用 {字段名} 引用表单字段的当前值,支持任意文字拼接。\n\n示例:{package_name},{guest_count}人,预订日期:{banquet_date}\n\n操作:先查看「表单项」中的字段名,在此处输入拼接模板,用 {字段名} 插入对应字段值,固定文字直接输入。', }, name: 'template', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' }, }, ], }, }, initialValue: () => ({ targetField: '', template: '' }), }, }, initialValue: [], }, }, // ───────────────────────────────────────────────────────────────────── // 字段联动 // ───────────────────────────────────────────────────────────────────── { title: { label: '字段联动', tip: '配置"某字段变化 → 自动回填其他字段"的联动规则。\n\n【简单模式】适用于:选某个值 → 直接回填固定值或绑定变量\n 例如:选"豪华套餐" → 回填价格=9800\n · 监听字段:值发生变化时触发检查,填字段名,例如 package_name\n · 触发值:留空=任何变化都触发;填具体值=精确匹配才触发\n · 回填字段:要被自动填入值的目标字段名\n · 回填方式:固定值 或 变量(从 state 中选取)\n\n【数据源模式】适用于:根据所选值从一个列表中查找匹配项,自动回填该项的多个字段\n 典型场景:下拉框绑定了 state.packages(含 name/price/count),选择套餐名后自动回填价格和人数\n 监听字段 = package_name\n 数据源 = state.packages(与 package_name 下拉框的动态选项相同)\n 匹配字段 = value(Select 存储的是选项的 value 字段)\n 回填映射 = {"package_price":"price","guest_count":"count"}(JSON 格式)\n 注意:数据源中的每个对象需要有 matchKey 字段(用于匹配监听字段的值)以及各回填字段对应的属性。', }, name: 'fieldLinkage', setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ // ── 通用:监听字段 ────────────────────────────────────── { title: { label: '监听字段', tip: '当此字段值变化时触发联动检查,填表单项的字段名(field),例如:package_name\n可从上方「表单项」列表中查看各字段名。', }, name: 'watchField', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' }, }, // ── 模式切换 ──────────────────────────────────────────── { title: { label: '联动模式', tip: '· 简单:监听字段变化 → 直接设置某个字段的值(固定值或变量)\n· 数据源:监听字段变化 → 在指定数据源中查找匹配项 → 批量回填多个字段\n (适合"选套餐 → 联动回填价格、人数"等场景)', }, name: 'linkageMode', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '简单', value: 'simple' }, { title: '数据源', value: 'dataSource' }, ], }, initialValue: 'simple', }, }, // ── 简单模式:触发值 ──────────────────────────────────── { title: { label: '触发值', tip: '监听字段的值等于此值时才触发回填。\n· 留空 = 该字段任何变化都触发\n· 填具体值 = 精确匹配后触发,例如:豪华套餐、1、true\n\n多个触发值:添加多条相同监听字段的规则,每条配置不同触发值。', }, name: 'matchValue', condition: (target: any) => (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple', setter: { componentName: 'StringSetter', initialValue: '' }, }, // ── 简单模式:回填字段 ────────────────────────────────── { title: { label: '回填字段', tip: '触发条件满足后,自动填入值的目标字段名(field),例如:package_price\n可从上方「表单项」列表中查看各字段名。', }, name: 'fillField', condition: (target: any) => (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple', setter: { componentName: 'StringSetter', initialValue: '' }, }, // ── 简单模式:回填方式 ────────────────────────────────── { title: { label: '回填方式', tip: '· 固定值:直接填写静态回填值,例如 9800、"待处理"\n· 变量:回填值来自页面 state(适合动态数据场景)', }, name: 'fillValueType', condition: (target: any) => (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '固定值', value: 'fixed' }, { title: '变量', value: 'variable' }, ], }, initialValue: 'fixed', }, }, // ── 简单模式:回填值(固定)──────────────────────────── { title: { label: '回填值', tip: '触发条件满足后填入「回填字段」的具体值。\n· 字符串示例:豪华套餐、待处理\n· 数字示例:9800、10\n如需动态值,请将「回填方式」切换为「变量」。', }, name: 'fillValue', condition: (target: any) => (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple' && (target.parent?.getPropValue?.('fillValueType') || 'fixed') === 'fixed', setter: { componentName: 'MixedSetter', props: { setters: [ { componentName: 'StringSetter', title: '字符串' }, { componentName: 'NumberSetter', title: '数字' }, { componentName: 'BoolSetter', title: '布尔' }, ], }, }, }, // ── 简单模式:回填值(变量)──────────────────────────── { title: { label: '回填变量', tip: '从页面 state 中选取变量作为回填值,表单联动时实时取该变量的当前值填入目标字段。\n\n如果 state 中没有现成变量,但回填值需要来自某个下拉框选项对象的字段(如选套餐后取价格),请改用「数据源」模式。', }, name: 'fillValue', condition: (target: any) => (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple' && target.parent?.getPropValue?.('fillValueType') === 'variable', setter: { componentName: 'VariableSetter' }, extraProps: { supportVariable: true }, }, // ── 数据源模式:数据源 ────────────────────────────────── { title: { label: '数据源', tip: '⚠️ 必须通过右侧「变量绑定」按钮选择变量,不能直接输入文本(如 "this.state.xxx"),否则运行时会报错。\n\n请选取一个数组变量作为查找来源。\n\n典型配置:\n1. 将 package_name 字段的「动态选项绑定」绑定到 state.packages\n2. 此处也选择同一个 state.packages\n\n数据格式要求(每个对象必须同时包含用于匹配的字段和用于回填的字段):\n state.packages = [\n {value:"A", label:"豪华套餐", price:9800, count:10},\n {value:"B", label:"标准套餐", price:5800, count:8}\n ]\n其中 value/label 供下拉框显示用,price/count 供联动回填用。', }, name: 'dataSource', condition: (target: any) => target.parent?.getPropValue?.('linkageMode') === 'dataSource', setter: { componentName: 'VariableSetter' }, extraProps: { supportVariable: true }, }, // ── 数据源模式:匹配字段 ──────────────────────────────── { title: { label: '匹配字段', tip: '在数据源的每个对象中,用哪个字段的值与监听字段的当前值进行比对。\n\n下拉框(Select)默认存储选项的 value 字段,此处填 value 即可。\n如果下拉框存储的是 label(名称),则填 label。\n\n示例:state.packages = [{value:"A", label:"豪华套餐", price:9800, count:10}, ...]\n则此处填 value(与 package_name 字段存储的选项 value 对应)。\n\n注意:匹配字段的值类型需与下拉框存储的值类型一致(如都为字符串,或都为数字)。', }, name: 'matchKey', condition: (target: any) => target.parent?.getPropValue?.('linkageMode') === 'dataSource', setter: { componentName: 'StringSetter', initialValue: 'value' }, }, // ── 数据源模式:回填映射 ──────────────────────────────── { title: { label: '回填映射', tip: '找到匹配项后,将数据源对象中的哪些字段值填入表单的哪些字段。\n\nJSON 格式:{ "表单字段名": "数据源字段名", ... }\n\n示例(state.packages 中每项有 price 和 count 属性):\n {"package_price":"price","guest_count":"count"}\n\n效果:匹配到套餐后,自动将 price 回填到 package_price 字段,count 回填到 guest_count 字段。\n\n注意:数据源字段名(value 部分)必须是数据源对象中实际存在的属性名。', }, name: 'fillFields', condition: (target: any) => target.parent?.getPropValue?.('linkageMode') === 'dataSource', setter: { componentName: 'JsonSetter', initialValue: {}, }, }, ], }, }, initialValue: () => ({ watchField: '', linkageMode: 'simple', matchValue: '', fillField: '', fillValueType: 'fixed', fillValue: '' }), }, }, initialValue: [], }, }, // ───────────────────────────────────────────────────────────────────── // 提交参数配置 // ───────────────────────────────────────────────────────────────────── { title: { label: '提交参数配置', tip: '控制表单提交时如何将字段值构建为请求参数,支持三种模式:\n\n① 直接透传(passthrough,默认)\n字段名与接口参数名完全一致时无需配置——表单字段值直接作为请求 body,零配置即可工作。\n\n② 字段重命名(rename)\n表单字段名与接口参数名不一致时使用,按规则将指定字段重命名,其余字段仍按原名透传。\n例如:表单字段 user_name → 接口参数 username\n\n③ 动态表单格式(itemList)\n后端接口接收 itemList:[{columnName,columnValue},...] 格式时使用,适合 /dynamicFormTableRecord/ 等接口。\n例如:{user_name:"张三"} → [{columnName:"user_name",columnValue:"张三"}]', }, name: 'submitMapping', setter: { componentName: 'ObjectSetter', props: { config: { items: [ // ── 模式选择 ────────────────────────────────────────────── { title: { label: '参数构建模式', tip: '选择提交时参数的构建方式:\n· 直接透传:字段名与接口参数名一致,直接发送;最简单,首选。\n· 字段重命名:字段名与接口参数名不一致,在下方按需配置重命名规则即可。\n· 动态表单格式:接口接收 itemList:[{columnName,columnValue}] 格式,需完整配置字段映射列表。', }, name: 'mode', setter: { componentName: 'SelectSetter', props: { options: [ { label: '① 直接透传(字段名与接口参数名一致)', value: 'passthrough' }, { label: '② 字段重命名(字段名与接口参数名不一致)', value: 'rename' }, { label: '③ 动态表单格式(itemList 格式)', value: 'itemList' }, ], }, initialValue: 'passthrough', }, }, // ── rename 模式:字段重命名映射 ─────────────────────────── { title: { label: '字段重命名规则', tip: '每行配置一条重命名规则,将表单字段名映射到接口参数名。\n· 来源字段(表单字段名):填写表单项中 field 的值,例如 user_name\n· 目标参数名(接口参数名):接口实际接收的参数名,例如 username\n\n未在此处列出的字段,将按原名透传到请求参数中。\n不需要填写表单中所有字段,只填需要重命名的字段即可。', }, name: 'fieldRenameMap', condition: (target: any) => target.parent?.getPropValue?.('mode') === 'rename', setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ { title: { label: '来源字段(表单字段名)', tip: '表单项中 field 的值,即当前使用的字段名,例如:user_name', }, name: 'fromField', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, { title: { label: '目标参数名(接口参数名)', tip: '接口实际接收的参数名,例如:username', }, name: 'toField', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, ], }, }, initialValue: () => ({ fromField: '', toField: '' }), }, }, initialValue: [], }, }, // ── itemList 模式:目标字段名 ────────────────────────────── { title: { label: '目标字段名(itemList 写入位置)', tip: '组装后的 [{columnName,columnValue},...] 数组写入 finalValues 的字段名,默认 itemList。\n需与数据源静态 options.params 中预设的字段名一致,提交时该值会被组装结果覆盖。\n通常保持默认值 itemList 即可。', }, name: 'targetField', condition: (target: any) => target.parent?.getPropValue?.('mode') === 'itemList', setter: { componentName: 'StringSetter', initialValue: 'itemList', }, }, // ── itemList 模式:字段映射列表 ──────────────────────────── { title: { label: '字段映射列表', tip: '每行定义一条映射规则,决定哪些字段进入 itemList,以及每项的 columnName。\n\n· 接口字段名(columnName):dynamicFormTableRecord 表的字段标识,与后端 schema 一致。\n· 来源字段(field):从哪个表单字段取值,填写表单项的 field 名,例如 user_name。\n· 模板(template):用 {字段名} 引用并拼接多个表单字段,与 field 二选一。\n 例如:{banquet_date} {banquet_time} → "2026-03-12 18:00"(将日期和时间合并为一个值)\n\n来源字段与模板只需填一项,两者都填时 field 优先。\n未在此处列出的表单字段不会进入 itemList(也不会出现在请求参数里)。', }, name: 'items', condition: (target: any) => target.parent?.getPropValue?.('mode') === 'itemList', setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ { title: { label: '接口字段名(columnName)', tip: 'dynamicFormTableRecord 中的字段标识,例如:user_name、banquet_time、guest_count', }, name: 'columnName', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '', }, }, { title: { label: '来源字段', tip: '从哪个表单字段取值,填写表单项的 field 名,例如:user_name\n与「模板」二选一,只需填一项。', }, name: 'field', setter: { componentName: 'StringSetter', initialValue: '', }, }, { title: { label: '模板', tip: '用 {字段名} 引用并拼接多个字段值,例如:{banquet_date} {banquet_time}\n与「来源字段」二选一,只需填一项。', }, name: 'template', setter: { componentName: 'StringSetter', initialValue: '', }, }, ], }, }, initialValue: () => ({ columnName: '', field: '' }), }, }, initialValue: [], }, }, ], }, }, }, }, // ───────────────────────────────────────────────────────────────────── // 分组六:操作按钮 // ───────────────────────────────────────────────────────────────────── { type: 'group', title: '操作按钮', name: 'actionGroup', display: 'accordion', items: [ // ── 对齐方式 ────────────────────────────────────────────────────── { title: { label: '对齐方式', tip: '表单底部操作按钮区域的水平对齐方式。\n· 居左:按钮靠左排列\n· 居中:按钮居中排列(默认)\n· 居右:按钮靠右排列', }, name: 'operationAlign', setter: { componentName: 'RadioGroupSetter', props: { options: [ { title: '居左', value: 'left' }, { title: '居中', value: 'center' }, { title: '居右', value: 'right' }, ], }, initialValue: 'center', }, }, // ── 按钮列表 ────────────────────────────────────────────────────── { title: { label: '按钮列表', tip: '配置表单底部的操作按钮。\n\n默认包含「立即预订」(提交,primary 样式)和「重置」(重置,normal 样式)两个按钮,可按需增删改。\n\n字段说明:\n· 按钮文字:按钮显示的文字,例如:立即预订、保存草稿、取消\n· 操作类型:\n 提交表单 → 校验通过后收集字段值 → 调用「提交数据源(Fetch)」接口 → 触发 onSubmit 事件\n 重置表单 → 清空所有字段值,触发 onReset 事件\n 自定义 → 直接触发「点击事件」,不操作表单\n· 按钮样式:主要(primary 蓝色)/ 次要(secondary)/ 普通(normal 灰色)\n· 提交数据源(Fetch):仅「提交表单」类型生效。点击后弹出 Fetch 数据源配置面板,选择接口地址和请求参数,请求参数支持绑定到表单字段值,提交时自动携带表单数据发起请求。\n· 提交时校验:关闭后跳过必填校验直接提交(适合草稿保存)\n· 点击事件:仅「自定义」类型生效\n\n不配置任何按钮时表单不显示按钮区域。', }, name: 'operations', setter: { componentName: 'ArraySetter', props: { itemSetter: { componentName: 'ObjectSetter', props: { config: { items: [ // ── 按钮文字 { title: { label: '按钮文字', tip: '按钮上显示的文字,例如:立即预订、保存草稿、取消', }, name: 'text', important: true, display: 'inline', setter: { componentName: 'StringSetter', isRequired: true, initialValue: '立即预订', }, }, // ── 操作类型 { title: { label: '操作类型', tip: '· 提交表单:校验 → 收集字段值 → 调用「提交数据源(Fetch)」 → 触发 onSubmit 事件\n· 重置表单:清空所有字段,触发 onReset 事件\n· 自定义:直接调用「点击事件」函数,不涉及表单操作', }, name: 'action', important: true, display: 'inline', setter: { componentName: 'SelectSetter', props: { options: [ { label: '提交表单', value: 'submit' }, { label: '重置表单', value: 'reset' }, { label: '自定义', value: 'custom' }, ], }, initialValue: 'submit', }, extraProps: { setValue: (target: any, value: string) => { // 根据操作类型自动填充默认文字(仅当文字为空时) const textMap: Record = { submit: '立即预订', reset: '重置', custom: '按钮', } if (!target.parent?.getPropValue?.('text')) { target.parent?.setPropValue?.('text', textMap[value] || '按钮') } // 根据操作类型自动设置默认样式 if (!target.parent?.getPropValue?.('type')) { const typeMap: Record = { submit: 'primary', reset: 'normal', custom: 'normal', } target.parent?.setPropValue?.('type', typeMap[value] || 'normal') } target.parent?.setPropValue?.('action', value) }, }, }, // ── 按钮样式 { title: { label: '按钮样式', tip: '· 主要(primary):蓝色高亮,适合提交等主操作\n· 次要(secondary):灰白色,适合次要操作\n· 普通(normal):普通灰色样式,适合重置等辅助操作', }, name: 'type', setter: { componentName: 'SelectSetter', props: { options: [ { label: '主要 (primary)', value: 'primary' }, { label: '次要 (secondary)', value: 'secondary' }, { label: '普通 (normal)', value: 'normal' }, ], }, initialValue: 'normal', }, }, // ── 提交时校验(仅 submit) { title: { label: '提交时校验', tip: '开启后点击此按钮时先执行必填校验,校验不通过则不触发提交。\n关闭后跳过校验,直接提交(适合"草稿保存"等场景)。\n仅对「提交表单」类型按钮生效。', }, name: 'submitValidate', condition: (target: any) => target.parent?.getPropValue?.('action') === 'submit', setter: { componentName: 'BoolSetter', initialValue: true, }, }, // ── 提交数据源(Fetch,仅 submit) { title: { label: '提交数据源(Fetch)', tip: '绑定页面级 Fetch 数据源,点击提交按钮校验通过后自动调用该数据源发起请求。\n\n【配置步骤】\n1. 在页面左侧「数据源」面板新建一个 fetch 类型数据源\n · 填写接口地址(URI)、请求方式(POST/PUT)、鉴权 Headers\n · isInit 设为 false(提交时才触发,不需要页面初始化时自动加载)\n2. 在此处点击「变量」按钮,选择 this.dataSourceMap.<数据源ID>\n\n【参数说明】\n表单提交时会将所有字段值(含 computedFields / submitMapping 处理后的 itemList)\n作为覆盖参数传入 .load(values),与数据源静态 params 合并,无需手写 onSubmit。\n\n【多按钮场景】\n每个「提交表单」按钮可绑定独立的数据源,如"保存草稿"绑定草稿接口,"正式提交"绑定提交接口。\n\n仅对「提交表单」类型按钮生效。', }, name: 'dataSource', condition: (target: any) => target.parent?.getPropValue?.('action') === 'submit', setter: { componentName: 'MixedSetter', props: { setters: [ { componentName: 'VariableSetter', title: '绑定数据源变量', }, { componentName: 'ExpressionSetter', title: '表达式', }, ], }, }, }, // ── 点击事件(仅 custom) { title: { label: '点击事件', tip: '自定义按钮被点击时调用的函数,例如:跳转页面、打开弹窗。\n仅对「自定义」类型按钮生效。', }, name: 'onClick', condition: (target: any) => target.parent?.getPropValue?.('action') === 'custom', setter: { componentName: 'FunctionSetter', }, extraProps: { supportVariable: true, }, }, ], }, }, initialValue: () => ({ text: '立即预订', action: 'submit', type: 'primary', submitValidate: true }), }, }, initialValue: [ { text: '立即预订', action: 'submit', type: 'primary', submitValidate: true }, { text: '重置', action: 'reset', type: 'normal' }, ], }, }, ], }, ], supports: { style: true, events: [ { name: 'onReset', template: "onReset(\${extParams}){\n// \u70b9\u51fb\u91cd\u7f6e\u6309\u94ae\u540e\u89e6\u53d1\n// \u53ef\u5728\u6b64\u5904\u540c\u6b65\u9875\u9762 state \u6216\u6267\u884c\u5176\u4ed6\u6e05\u7a7a\u903b\u8f91\nconsole.log('[CustomForm] onReset');\n}", }, { name: 'onChange', template: "onChange(field, value, allValues,\${extParams}){\n// 任意字段值变化时触发\n// field = 变化的字段名(字符串)\n// value = 该字段的最新值\n// allValues = 当前所有字段值(含 computedFields 计算字段)\n// 常见用法:将字段值同步到页面 state\n// this.setState({ [field]: value })\nconsole.log('[CustomForm] onChange', field, value, allValues);\n}", }, { name: 'onSubmit', template: "onSubmit(values, errors, field,\${extParams}){\n// 提交成功(校验通过)时触发\n// values = 所有字段的当前值(含 computedFields 计算字段)\n// 无需再手动组装字段,直接使用 values 对象即可\nconsole.log('[CustomForm] onSubmit', values);\n}", }, { name: 'onSubmitFailed', template: "onSubmitFailed(values, errors, field,\${extParams}){\n// 提交失败(校验未通过)时触发\n// errors = 校验错误信息对象,key 为字段名\nconsole.log('[CustomForm] onSubmitFailed', errors);\n}", }, ], }, }, } const snippets: IPublicTypeSnippet[] = [ { title: '自定义表单(完整示例)', screenshot: '', schema: { componentName: 'CustomForm', props: { columns: 2, initialValues: [ { field: 'guest_count', valueType: 'fixed', value: 10 }, ], computedFields: [ { targetField: 'order_desc', template: '{package_name},{package_price}元,预订日期:{banquet_date},人数:{guest_count}人' }, ], formItems: [ { field: 'banquet_date', label: '宴请日期', required: true, placeholder: '请选择日期时间', componentType: 'DateTimePicker', componentProps: { format: 'YYYY-MM-DD HH:mm' }, }, { field: 'guest_count', label: '宴请人数', required: true, placeholder: '请输入人数', componentType: 'NumberPicker', }, { field: 'package_name', label: '套餐名称', required: true, componentType: 'Select', placeholder: '请选择套餐', options: [], }, { field: 'package_price', label: '套餐价格', componentType: 'Input', componentProps: { readOnly: true }, extra: '选择套餐后自动填入', }, { field: 'user_name', label: '预订人姓名', required: true, placeholder: '请输入姓名', componentType: 'Input', }, { field: 'user_mobile', label: '联系方式', required: true, placeholder: '请输入手机号', componentType: 'Input', }, { field: 'memo', label: '备注', placeholder: '请输入备注(选填)', componentType: 'TextArea', columnSpan: 2, }, ], fieldLinkage: [ { watchField: 'package_name', linkageMode: 'simple', matchValue: '豪华套餐', fillField: 'package_price', fillValueType: 'fixed', fillValue: 9800 }, ], operations: [ { text: '立即预订', action: 'submit', type: 'primary', submitValidate: true }, { text: '重置', action: 'reset', type: 'normal' }, ], }, }, }, ] export default { ...CustomFormMeta, snippets, }