import { isString } from '@pisell/utils'; import { nocobaseCpt2PisellCptMap, pisellCptMap, systemFields, formItemMetaMap, } from './constants'; import { parseXRecordSchemaToChildSchemas } from '../data-source-sub-form/meta'; type UiSchema = { title: string; default?: string; enum: { label: string; value: string }[]; 'x-component': string; 'x-component-props': Record; 'x-read-pretty': boolean; 'x-validator'?: string; 'x-display'?: 'visible' | 'hidden' | 'none'; 'x-pattern'?: 'editable' | 'disabled' | 'readPretty'; 'x-data-source'?: { sourceType?: string; type?: string; options?: Array<{ id?: string; label: string | any; // 支持字符串或多语言对象 value: string; color?: string; }>; labelField?: string; valueField?: string; dataSource?: any; extraParams?: Record; }; 'x-record-schema'?: Record | Record[] | string; type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'; }; /** * 将 MultiLanguageText 对象转为平台可解析的 i18n 格式 * MultiLanguageText: { en, 'zh-CN', 'zh-HK', ja, pt, original } * i18n 格式: { type: 'i18n', en, 'zh-CN', 'zh-HK', ... } * * 规则: * - 所有语言字段(含 original)均为空 → 返回 '',避免低代码 i18n 图标被误亮 * - 仅有 original 有值,其它翻译为空 → 返回纯字符串 original,正常显示但不绑定 i18n * - 有任意真实翻译 → 返回 i18n 对象,缺失的语言用 original 兜底 */ const I18N_LANG_KEYS = ['en', 'zh-CN', 'zh-HK', 'ja', 'pt']; const toI18nProp = (value: any): any => { if (!value) return value; if (typeof value === 'string') return value; if (typeof value === 'object' && value.type === 'i18n') return value; if (typeof value === 'object') { const original = value.original || ''; const hasRealTranslation = I18N_LANG_KEYS.some((key) => !!value[key]); if (!hasRealTranslation) { return original; } const i18nResult: Record = { type: 'i18n' }; I18N_LANG_KEYS.forEach((key) => { i18nResult[key] = value[key] || original || ''; }); return i18nResult; } return value; }; // 创建表单组 export const createFormGroup = (children: any[], groupTitle?: any, groupDescription?: any) => { return { children, componentName: 'FormGroup', title: '分组', props: { title: toI18nProp(groupTitle) || 'Group', description: toI18nProp(groupDescription) || '', showTitle: true, showDescription: true, showBackgroundColor: true, backgroundColor: '#F9FAFB', }, }; }; // 创建提交按钮 export const createSubmitButton = (props: any = {}) => { return { componentName: 'SubmitButton', title: '提交', props: { type: 'primary', size: 'large', status: 'normal', childrenString: { type: 'i18n', en: 'Submit', 'zh-CN': '提交', 'zh-HK': '提交', }, confirmConfig: { enable: false, }, toastConfig: { enable: true, showAfterSubmit: true, title: { type: 'i18n', en: 'Submitted successfully', 'zh-CN': '提交成功', 'zh-HK': '提交成功', }, }, refreshData: true, afterSubmitType: 'none', ...props, }, children: [], }; }; // 创建表单项子项 export const createFormItemChildren = (item: { name: string; uiSchema: UiSchema; }) => { let props: Record = item.uiSchema['x-component-props'] || {}; let componentName = item.uiSchema['x-component']; if ( ['Select', 'Radio.Group', 'Checkbox.Group'].includes( item.uiSchema['x-component'] ) && item.uiSchema?.enum?.length > 0 ) { if ( ['Radio.Group', 'Checkbox.Group'].includes(item.uiSchema['x-component']) ) { props.direction = 'horizontal'; } props.options = item?.uiSchema?.enum || []; } if (item.uiSchema['x-read-pretty']) { props.disabled = item.uiSchema['x-read-pretty']; } if (item.uiSchema['x-component'] === 'ColorPicker') { props.defaultValue = item.uiSchema.default; } return { componentName, props, }; }; const modeMap: Record = { add: 'edit', edit: 'edit', view: 'view', }; export const createFormItemSchema = ( item: { name: string; uiSchema: UiSchema; schema?: Record; children?: any[]; _children?: any[]; interface?: string; }, mode: string ) => { const { name, uiSchema } = item; const { title, 'x-component': xComponent, 'x-validator': validator, 'x-read-pretty': readPretty, default: defaultValue, 'x-component-props': xProps, 'x-pattern': xPattern, 'x-display': xDisplay, 'x-description': xDescription, type, } = uiSchema as any; let cptTitle = undefined; // 根据 x-pattern 和 x-display 确定 renderMode // 优先级: x-display > x-pattern > x-read-pretty > x-component-props.renderMode > mode 参数 let renderMode = modeMap[mode] || 'edit'; // 先检查 x-component-props 中是否已有 renderMode if (xProps?.renderMode) { renderMode = xProps.renderMode; } // x-read-pretty 兼容旧逻辑(优先级高于 x-component-props) if (readPretty) { renderMode = 'view'; } // x-pattern 映射到 renderMode(优先级更高) if (xPattern === 'readPretty') { renderMode = 'view'; } else if (xPattern === 'disabled') { renderMode = 'disabled'; } else if (xPattern === 'editable') { renderMode = 'edit'; } // x-display = 'hidden' 优先级最高 if (xDisplay === 'hidden') { renderMode = 'hidden'; } // 构建 props: 先设置默认值,再用 x-component-props 覆盖,最后设置 renderMode let props: Record = { // 默认值 label: toI18nProp(title), name, allowClear: true, placeholder: { type: 'i18n', 'en': 'Enter', 'zh-CN': '请输入', 'zh-HK': '請輸入', }, // x-component-props 中的所有字段直接映射,可以覆盖上面的默认值 ...xProps, // renderMode 最终计算结果(综合了所有来源) renderMode, // 辅助说明文字(来自 registry item 的 description),转为 i18n 格式 ...(xDescription ? { extra: toI18nProp(xDescription) } : {}), }; // 如果 x-component-props 中存在 required 字段,额外添加 requiredobj if (xProps?.required !== undefined) { props.requiredobj = { required: xProps.required, }; } let componentName = xComponent; let childrenSchema: any[] = []; let propertiesSchema: Record = {}; if (item?.interface === 'phone') { cptTitle = '电话号码输入'; componentName = 'Input.Phone'; } if (item?.interface === 'mobile') { cptTitle = '手机号码输入'; componentName = 'Input.Mobile'; } if (xComponent === 'Percent') { cptTitle = '百分比输入'; } if (item?.interface === 'url') { cptTitle = '链接输入'; componentName = 'Input.URL'; } if (item?.interface === 'attachmentUrl') { cptTitle = '多媒体文件上传'; componentName = 'Upload'; } if (item?.interface === 'i18nAttachment') { cptTitle = '多媒体文件上传'; componentName = 'Upload'; props.enableMultilingual = true; } if (validator && validator !== 'integer') { if (validator === 'email') { cptTitle = '电子邮箱输入'; componentName = 'Input.Email'; } props.typeobj = { type: validator, enabled: true, }; } // 如果类型是 integer,设置 precision 为 0,只允许输入整数 if (type === 'integer' && (xComponent === 'InputNumber' || xComponent === 'Percent')) { props.precision = 0; } if (xComponent === 'Input.JSON' && item?.interface !== 'i18nInput') { const jsonChildren = item._children || item.children || []; // 如果有children属性,递归创建子表单项 if (jsonChildren && jsonChildren.length > 0) { props.prefix = name; childrenSchema = jsonChildren.map((child: any) => createFormItemSchema(child, mode) ); // 同时构建 properties 格式 jsonChildren.forEach((child: any) => { const childSchema = createFormItemSchema(child, mode); propertiesSchema[child.name] = childSchema; }); } else { componentName = 'Input.InputJSON'; props.autoSize = { minRows: 4, }; // 根据type类型生成不同的normalize和校验逻辑 const fieldType = type || 'object'; let normalizeValue = ''; let validatorValue = ''; normalizeValue = '(value) => {\n try {\n if (typeof value === \'string\' && value.trim() !== \'\') {\n const trimmedValue = value.trim();\n // 只有以{或[开头才尝试转换\n if (trimmedValue.startsWith(\'{\') || trimmedValue.startsWith(\'[\')) {\n return JSON.parse(trimmedValue);\n }\n }\n } catch(error) {\n console.log(error)\n }\n return value\n}'; validatorValue = "async (value) => {\n try {\n if (typeof value === 'string' && value.trim() !== '') {\n const trimmedValue = value.trim();\n // 确保JSON以{或[开头,以避免纯数字或字符串通过校验\n if (!trimmedValue.startsWith('{') && !trimmedValue.startsWith('[')) {\n return Promise.reject(new Error('JSON必须是对象或数组,请以{或[开头'));\n }\n JSON.parse(trimmedValue);\n }\n } catch (err) {\n return Promise.reject(new Error(err.message));\n }\n}"; props.normalize = { type: 'JSExpression', value: normalizeValue, }; props.getValueProps = { type: 'JSExpression', value: '(value) => {\n try {\n // 使用严格检查:null/undefined不转换,其他对象类型都转换\n if (value !== null && value !== undefined && typeof value === \'object\') {\n return { value: JSON.stringify(value, null, 2) };\n }\n return { value: value };\n } catch(error) {\n console.log(error)\n }\n return {value: value}\n}', }; props.validator = { type: 'JSExpression', value: validatorValue, }; } } if (item?.interface === 'i18nInput') { cptTitle = '多语言输入'; componentName = 'Translation'; } // 注入默认值 props.defaultValue = defaultValue; // 获取选项配置 const dataSourceConfig = (uiSchema as any)?.['x-data-source'] || item?.schema?.['x-data-source']; const recordSchemaConfig = (uiSchema as any)?.['x-record-schema'] || item?.schema?.['x-record-schema']; const isRecordListWrapper = item?.interface === 'RecordListWrapper' || xComponent === 'RecordListWrapper'; if (isRecordListWrapper) { cptTitle = '记录列表包装器'; componentName = 'RecordListWrapper'; props.xRecordSchema = recordSchemaConfig; // 自动将 recordSchemaConfig 解析为子节点 Schema,与 JsonSetter 的 setValue 保持一致, // 避免用户必须在低代码面板手动触发 JsonSetter 才能渲染子表单字段。 if (recordSchemaConfig) { childrenSchema = parseXRecordSchemaToChildSchemas(recordSchemaConfig); } else { childrenSchema = []; } } if ( (['Select', 'Radio.Group', 'Checkbox.Group'].includes(xComponent) || isRecordListWrapper) && (uiSchema?.enum?.length > 0 || dataSourceConfig || isRecordListWrapper) ) { if (['Radio.Group', 'Checkbox.Group'].includes(xComponent)) { props.direction = 'horizontal'; props.placeholder = ""; } else { if (type === 'array') { props.mode = 'multiple'; } props.styleType = 'antd'; props.showSearch = true; props.optionFilterProp = 'label'; props.placeholder = { type: 'i18n', 'en': 'Select an option', 'zh-CN': '请选择一个选项', 'zh-HK': '請選擇一個選項', }; if (props.mode === "multiple") { props.selectAll = true; props.isShowDropdown = true; props.placeholder = { type: 'i18n', 'en': 'Select one or more options', 'zh-CN': '请选择一个或多个选项', 'zh-HK': '請選擇一個或多個選項', }; } } // 统一的选项配置处理 const optionConfig = dataSourceConfig || {}; // 设置 optionSourceType props.optionSourceType = optionConfig?.type || ''; // 根据 sourceType 设置不同的配置 if (props.optionSourceType === 'default') { // 优先使用 optionConfig.options,其次兼容旧的 enum 格式 const rawOptions = optionConfig.options || uiSchema?.enum || []; // 转换多语言格式的 label 为组件可识别的 i18n 格式 props.options = rawOptions.map((option: any) => { const label = option.label; // 如果 label 是多语言对象格式,转换为 i18n 格式 if (label && typeof label === 'object' && !label.type) { return { ...option, label: { type: 'i18n', 'en': label.en || label.original || '', 'zh-CN': label['zh-CN'] || label.original || '', 'zh-HK': label['zh-HK'] || label.original || '', } }; } // 如果已经是 i18n 格式或者是字符串,直接返回 return option; }); } else if (props.optionSourceType === 'api') { // API 类型需要设置 labelField 和 valueField if (optionConfig?.labelField) { props.labelField = optionConfig.labelField; } if (optionConfig?.valueField) { props.valueField = optionConfig.valueField; } if (optionConfig?.dataSource) { props.dataSource = optionConfig.dataSource; } if (optionConfig?.extraParams) { props.extraParams = optionConfig.extraParams; } } } if (xComponent === 'TimePicker') { props.format = 'HH:mm'; props.placeholder = { type: 'i18n', 'en': 'Select a time', 'zh-CN': '请选择时间', 'zh-HK': '請選擇時間', }; props.defaultValue = props.defaultValue || null; } if (xComponent === 'DatePicker') { props.format = props.dateFormat || 'YYYY-MM-DD'; props.showTime = false; props.placeholder = { type: 'i18n', 'en': 'Select a date', 'zh-CN': '请选择日期', 'zh-HK': '請選擇日期', }; props.defaultValue = null; } if (xComponent === 'ColorPicker') { props.defaultValue = defaultValue; } if (xComponent === 'Switch') { props.placeholder = ''; props.allowClear = false; } const _componentName = nocobaseCpt2PisellCptMap[componentName]; // 确定 schema type let schemaType: 'void' | 'string' | 'number' | 'object' | 'array' | 'boolean' = 'string'; if (type) { schemaType = type as any; } else if (xComponent === 'InputNumber' || xComponent === 'Percent') { schemaType = 'number'; } else if (componentName === 'RecordListWrapper') { schemaType = 'array'; } else if (xComponent === 'Input.JSON' || item?.interface === 'i18nInput') { schemaType = 'object'; } else if (['Checkbox.Group', 'Select'].includes(xComponent) && props.mode === 'multiple') { schemaType = 'array'; } else if (xComponent === 'Switch' || xComponent === 'Checkbox') { schemaType = 'boolean'; } // 构建完整的 UI Schema 格式 const schema: any = { // 基础属性 type: schemaType, name: name, title: title, // 组件相关 'x-component': _componentName, 'x-component-props': props, // 包装器 (FormItem 作为 decorator) 'x-decorator': 'FormItem', 'x-decorator-props': { label: title, }, // 默认值 default: defaultValue, // 校验 ...(validator && { 'x-validator': validator }), // children 节点 (兼容旧格式和新格式) ...(childrenSchema.length > 0 && { children: childrenSchema }), ...(Object.keys(propertiesSchema).length > 0 && { properties: propertiesSchema }), // 设计器相关 (如果需要) // 'x-initializer': undefined, // 'x-settings': undefined, // 'x-toolbar': undefined, // 兼容旧格式: 保留 componentName 供旧代码使用 componentName: _componentName, props, }; return schema; }; type FieldOption = { isGroup: boolean; label: string; value: string; isShow: boolean; children: FieldOption[]; }; // 表单schema转换为字段选项 export const formSchema2PisellFieldsOptions = ( formSchema: any, allFields: any[] ) => { if ( !['DataSourceForm', 'FormGroup', 'DataSourceWrapper'].includes( formSchema.componentName ) ) { return []; } const allFieldMap = allFields.reduce((acc, item) => { acc[item.name] = item; return acc; }, {}); let fieldsOptions: FieldOption[] = []; const showFields: any[] = []; const hiddenFields: FieldOption[] = []; if (!formSchema?.children) { return []; } formSchema.children.forEach((item: any) => { // 是基础formItem组件 if (pisellCptMap[item.componentName]) { showFields.push(item.props.name); fieldsOptions.push({ isGroup: false, label: item.props.label || allFieldMap[item.props.name]?.uiSchema?.title || item.props.name, value: item.id, isShow: true, children: [], }); } else if (item.componentName === 'FormGroup') { fieldsOptions.push({ isGroup: true, label: item.props.title, value: item.id, isShow: true, children: item?.children?.map((children: any) => { showFields.push(children.props.name); return { isGroup: false, label: children.props.label || allFieldMap[children.props.name]?.uiSchema?.title || children.props.name, value: children.id, isShow: true, children: [], }; }), }); } }); allFields.forEach((item) => { if (!showFields.includes(item.name)) { hiddenFields.push({ isGroup: false, label: item.uiSchema?.title || allFieldMap[item.name]?.uiSchema?.title || item.name, value: item.name, isShow: false, children: [], }); } }); fieldsOptions = [ ...fieldsOptions, { isGroup: true, label: 'Hidden', value: 'hidden', isShow: true, children: hiddenFields, }, ]; return fieldsOptions; }; // 字段转换为schema export const fields2ChildrenSchemas = (fields: any[], mode: string) => { return fields .filter( (field: any) => field.uiSchema && !field.uiSchema['x-read-pretty'] && !systemFields.includes(field.name) ) .map((field: any) => { return createFormItemSchema(field, mode); }) .filter((item: any) => !!item.componentName); }; // 设置变量 export const setVariables = ( target: any, { label, value, }: { label: string; value: any; } ) => { const variables = target.getProps().getPropValue('_variables'); target.getProps().setPropValue('_variables', { ...variables, [label]: value, }); }; // 批量设置变量 export const setBatchVariables = ( target: any, variables: Array<{ /** * currentFields 当前表单字段列表 * dataSourceTitle 数据源标题 * dataSourceValue 数据源值 * cptValuesVariables 组件值变量 */ label: | 'currentFields' | 'dataSourceTitle' | 'dataSourceValue' | 'cptValuesVariables' | 'currentValueVariables' | 'dataSourceVariables'; value: any; }> ) => { const existingVariables = target.getProps().getPropValue('_variables') || {}; const newVariables = variables.reduce( (acc, { label, value }) => ({ ...acc, [label]: value, }), existingVariables ); target.getProps().setPropValue('_variables', newVariables); }; // 获取当前节点所有变量 export const getAllVariables = (target: any) => { return target.getProps().getPropValue('_variables'); }; // 获取当前节点单个变量 export const getVariable = (target: any, label: string) => { return target.getProps().getPropValue('_variables')?.[label]; }; // 清除当前节点所有变量 export const clearVariables = (target: any) => { return target.getProps().setPropValue('_variables', null); }; type VariablesItemType = { componentName: string; title: string; id: string; variables: Record; }; // 获取当前节点父节点所有变量 const getParentVariables = (target: any) => { let parent = target.node; const variables: VariablesItemType[] = []; while (parent) { const { componentName, title, id } = parent; const _variables = parent.getProps().getPropValue('_variables'); const strTitle = isString(title) ? title : title['zh-CN']; if (_variables) { variables.push({ componentName, title: strTitle, id, variables: _variables, }); } parent = parent.parent; } return variables; }; // 获取数据源变量 export const getDataSourceVariables = (target: any) => { const variables = getParentVariables(target); return variables ?.map((item: VariablesItemType) => { return { label: item.title, value: item.id, children: item.variables.dataSourceVariables, isVariable: true, }; }) .filter((item) => item?.children?.length); }; export const getCurrentValueVariables = (target: any) => { const ds = target?.getProps()?.getPropValue('_variables')?.dataSourceValue; if (ds?.fromSettingMeta || ds?.fromDeviceSettingMeta) { return []; } const variables = getParentVariables(target); const currentDataSourceTitle = getVariable(target, 'dataSourceTitle'); const currentValueVariables: any[] = [ { label: currentDataSourceTitle, value: 'server', }, ]; variables.forEach((item: VariablesItemType) => { if (item.variables?.currentValueVariables?.length) { currentValueVariables.push({ label: item.title, value: item.id, children: item.variables.currentValueVariables, isVariable: true, }); } }); return currentValueVariables; }; export const getCptValuesVariables = (target: any) => { const variables = getParentVariables(target); return variables ?.map((item: VariablesItemType) => { return { label: `${item.title}-${item?.variables?.dataSourceTitle || ''}`, value: item.id, children: item.variables.cptValuesVariables, isVariable: true, }; }) .filter((item) => item?.children?.length); }; export const getDataSourceValue = (target: any) => { const variables = getParentVariables(target); return variables?.find((item) => item.variables.dataSourceValue)?.variables .dataSourceValue; };