diff --git a/docs/form-render/advanced/display.md b/docs/form-render/advanced/display.md index 5f748f63a..31385b8b4 100644 --- a/docs/form-render/advanced/display.md +++ b/docs/form-render/advanced/display.md @@ -373,7 +373,7 @@ export default Demo; ### 主题设置 -对于嵌套类型的表单,我们内置了三种主题,分别为 `collapse | card | tile `, 默认为 `collapse` 主题 +对于嵌套类型的表单,我们内置了四种主题,分别为 `collapse | card | tile | flex`, 默认为 `collapse` 主题 1. 默认样式:`theme: 'collapse'` ,支持`无边框模式: 'collapse:pure'`、`幽灵模式:'collapse:ghost'` @@ -612,3 +612,64 @@ const Demo = () => { export default Demo; ``` + +4. 弹性布局模式:`theme: 'flex'`,支持通过 style 属性配置相关样式 + +```jsx +import React from 'react'; +import Form from '../demo/display'; +const schema = { + type: 'object', + displayType: 'row', + properties: { + objectName2: { + title: '弹性布局', + description: '这是一个对象类型', + type: 'object', + theme: 'flex', + props: { + style: { + flexDirection: 'column', + flexWrap: 'wrap', + margin: '0 0 0 0', + padding: '0 0 0 0', + justifyContent: 'flex-start', + alignItems: 'flex-start', + alignContent: 'flex-start', + }, + }, + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + width: '30%', + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + width: '30%', + }, + date: { + title: '时间选择', + type: 'string', + format: 'date', + width: '30%', + }, + }, + }, + }, +}; + +const Demo = () => { + return ( +
+
+
+ ); +}; + +export default Demo; +``` diff --git a/docs/form-render/api/schema.md b/docs/form-render/api/schema.md index a02637b84..8de6856c2 100644 --- a/docs/form-render/api/schema.md +++ b/docs/form-render/api/schema.md @@ -111,7 +111,7 @@ label 的宽度,数字值单位为 px,也可使用 `'20%'/'2rem'` 等,写 ### theme - 类型:`string` -- 值: `default | card | tile`,详见[这里](/form-render/advanced/display#主题设置) +- 值: `default | card | tile | flex`,详见[这里](/form-render/advanced/display#主题设置) 设置嵌套表单的主题样式 diff --git a/docs/playground/json/simplest.json b/docs/playground/json/simplest.json index 70e3a8617..fe1fe64bb 100644 --- a/docs/playground/json/simplest.json +++ b/docs/playground/json/simplest.json @@ -13,12 +13,22 @@ } ] }, - "select": { - "title": "单选", - "type": "string", - "enum": ["a", "b", "c"], - "enumNames": ["选项1", "选项2", "选项3"], - "widget": "radio" + "$void_1": { + "type": "object", + "title": "void test", + "properties": { + "input2": { + "title": "简单输入框", + "type": "string", + "min": 6, + "rules": [ + { + "pattern": "^[A-Za-z0-9]+$", + "message": "只允许填写英文字母和数字" + } + ] + } + } }, "listName2": { "title": "对象数组", diff --git a/packages/form-render/CHANGELOG.md b/packages/form-render/CHANGELOG.md index 3d1568325..9cf14d91b 100644 --- a/packages/form-render/CHANGELOG.md +++ b/packages/form-render/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.14.0 + +- [+] 为 `object` 类型添加 `flex` 主题 +- [+] 优化 `widgets` 下的组件路径 +- [+] 增加 `void` 支持 +- [!] 修复 `object` 宽度设置 + ## 1.13.21 - [!] 修复 `tableList` 和 `drawerList` 表头无法正常显示的问题 @@ -8,6 +15,10 @@ - [+] 删除 `formData` 中的 `index` +## 1.13.18 + +- [!] 修复 `cardList` 组件无法新增的 bug + ## 1.13.17 - [+] `simpleList` 和 `cardList` 支持自定义新增按钮 diff --git a/packages/form-render/src/form-render-core/src/core/RenderField/ExtendedWidget.js b/packages/form-render/src/form-render-core/src/core/RenderField/ExtendedWidget.js index 4e21e0752..6997411d6 100644 --- a/packages/form-render/src/form-render-core/src/core/RenderField/ExtendedWidget.js +++ b/packages/form-render/src/form-render-core/src/core/RenderField/ExtendedWidget.js @@ -1,4 +1,4 @@ -import React, { Suspense } from 'react'; +import React, { memo, Suspense } from 'react'; import { transformProps } from '../../createWidget'; import { useStore, useTools } from '../../hooks'; import { extraSchemaList, getWidgetName } from '../../mapping'; @@ -162,43 +162,41 @@ const ExtendedWidget = ({ const finalProps = transformProps(widgetProps); return ( }> -
- -
+
); }; -// const areEqual = (prev, current) => { -// if (prev.schema && current.schema) { -// if (prev.schema.$id === '#') { -// return false; -// } -// if (prev.schema.hidden && current.schema.hidden) { -// return true; -// } -// } -// if (prev.readOnly !== current.readOnly) { -// return false; -// } -// if (prev.disabled !== current.disabled) { -// return false; -// } -// if ( -// JSON.stringify(prev.dependValues) !== JSON.stringify(current.dependValues) -// ) { -// return false; -// } -// if (isObjType(prev.schema) && isObjType(current.schema)) { -// return false; -// } -// if ( -// JSON.stringify(prev.value) === JSON.stringify(current.value) && -// JSON.stringify(prev.schema) === JSON.stringify(current.schema) -// ) { -// return true; -// } -// return false; -// }; - -export default ExtendedWidget; +const areEqual = (prev, current) => { + if (prev.schema && current.schema) { + if (prev.schema.$id === '#') { + return false; + } + if (prev.schema.hidden && current.schema.hidden) { + return true; + } + } + if (prev.readOnly !== current.readOnly) { + return false; + } + if (prev.disabled !== current.disabled) { + return false; + } + if ( + JSON.stringify(prev.dependValues) !== JSON.stringify(current.dependValues) + ) { + return false; + } + if (isObjType(prev.schema) && isObjType(current.schema)) { + return false; + } + if ( + JSON.stringify(prev.value) === JSON.stringify(current.value) && + JSON.stringify(prev.schema) === JSON.stringify(current.schema) + ) { + return true; + } + return false; +}; + +export default memo(ExtendedWidget, areEqual); diff --git a/packages/form-render/src/form-render-core/src/core/RenderField/index.js b/packages/form-render/src/form-render-core/src/core/RenderField/index.js index 300e0c431..a7acb6bb2 100644 --- a/packages/form-render/src/form-render-core/src/core/RenderField/index.js +++ b/packages/form-render/src/form-render-core/src/core/RenderField/index.js @@ -232,7 +232,7 @@ const RenderField = props => { ); } else if (isBlockType(_schema)) { return ( -
+
); diff --git a/packages/form-render/src/form-render-core/src/core/index.js b/packages/form-render/src/form-render-core/src/core/index.js index 05eacb5b1..70fe9f737 100644 --- a/packages/form-render/src/form-render-core/src/core/index.js +++ b/packages/form-render/src/form-render-core/src/core/index.js @@ -207,23 +207,26 @@ const CoreRender = ({ if (schema.hidden) { columnStyle.display = 'none'; } - // if (!isComplex) { - // } - if (!isObj) { - if (width) { - columnStyle.width = width; - columnStyle.paddingRight = 8; - } else if (column > 1) { - columnStyle.width = `calc(100% /${column})`; - columnStyle.paddingRight = 8; + + if (width) { + columnStyle.width = width; + } else if (column > 1) { + columnStyle.width = `calc(100% /${column})`; + } + + // 如果传入自定义样式则覆盖使用,object 外层样式使用 schema.style,内层样式使用 schema.props.style + if ('object' === typeof schema?.style) { + columnStyle = { + ...columnStyle, + ...schema.style } } const _labelWidth = isLooselyNumber(effectiveLabelWidth) ? Number(effectiveLabelWidth) : isCssLength(effectiveLabelWidth) - ? effectiveLabelWidth - : 110; // 默认是 110px 的长度 + ? effectiveLabelWidth + : 110; // 默认是 110px 的长度 let labelStyle = { width: _labelWidth }; if (isComplex || _displayType === 'column') { @@ -259,17 +262,15 @@ const CoreRender = ({ }; const objChildren = ( -
- - {item.children} - -
+ + {item.children} + ); const listChildren = ( diff --git a/packages/form-render/src/form-render-core/src/processData.js b/packages/form-render/src/form-render-core/src/processData.js index 3f4709d55..bcf9b4c2e 100644 --- a/packages/form-render/src/form-render-core/src/processData.js +++ b/packages/form-render/src/form-render-core/src/processData.js @@ -6,6 +6,7 @@ import { removeEmptyItemFromList, removeHiddenFromResult, } from './utils'; +import { removeVoidFromResult } from './void'; // 提交前需要先处理formData的逻辑 export const processData = (data, flatten, removeHiddenData) => { let _data = clone(data); @@ -13,6 +14,10 @@ export const processData = (data, flatten, removeHiddenData) => { if (removeHiddenData) { _data = removeHiddenFromResult(data, flatten); } + + // 1.5. 去掉 void 元素 + _data = removeVoidFromResult(_data); + // 2. bind 的处理 _data = transformDataWithBind(_data, flatten); @@ -21,7 +26,6 @@ export const processData = (data, flatten, removeHiddenData) => { // 4. 去掉所有的 undefined _data = cleanEmpty(_data); - return _data; }; diff --git a/packages/form-render/src/form-render-core/src/utils.js b/packages/form-render/src/form-render-core/src/utils.js index eac2dcda0..54a1b8b49 100644 --- a/packages/form-render/src/form-render-core/src/utils.js +++ b/packages/form-render/src/form-render-core/src/utils.js @@ -1,4 +1,5 @@ import { cloneDeep, get, isEmpty, set } from 'lodash-es'; +import { getRealDataPath } from './void'; export function getParamByName(name, url = window.location.href) { name = name.replace(/[\[\]]/g, '\\$&'); @@ -51,7 +52,8 @@ export function getValueByPath(formData, path) { if (path === '#' || !path) { return formData || {}; } else if (typeof path === 'string') { - return get(formData, path); + const realPath = getRealDataPath(path); + return realPath && get(formData, realPath); } else { console.error('path has to be a string'); } @@ -97,7 +99,7 @@ export function getDataPath(id, dataIndex) { _id = _id.replace(/\[\]/, `[${item}]`); }); } - return removeBrackets(_id); + return removeBrackets(getRealDataPath(_id)); } export function isObjType(schema) { diff --git a/packages/form-render/src/form-render-core/src/validator.js b/packages/form-render/src/form-render-core/src/validator.js index b47012086..e83ae49d5 100644 --- a/packages/form-render/src/form-render-core/src/validator.js +++ b/packages/form-render/src/form-render-core/src/validator.js @@ -14,6 +14,7 @@ import { } from './utils'; import { defaultValidateMessages } from './validateMessage'; import { defaultValidateMessagesCN } from './validateMessageCN'; +import { getRealDataFlatten } from './void'; export const parseSchemaExpression = (schema, formData, path) => { if (!isObject(schema)) return schema; @@ -72,6 +73,8 @@ export const validateField = ({ }) => { const paths = getRelatedPaths(path, flatten); // console.log('all relevant paths:', paths); + + flatten = getRealDataFlatten(flatten); const promiseArray = paths.map(path => { const { id, dataIndex } = destructDataPath(path); if (flatten[id] || flatten[`${id}[]`]) { diff --git a/packages/form-render/src/form-render-core/src/void.js b/packages/form-render/src/form-render-core/src/void.js new file mode 100644 index 000000000..b2ddb004a --- /dev/null +++ b/packages/form-render/src/form-render-core/src/void.js @@ -0,0 +1,44 @@ +import { cloneDeep } from 'lodash-es'; +import { dataToKeys } from './utils'; + +// a.b => a.b +// $volid_1.b => b +// $void_1 => undefined +export function getRealDataPath(path) { + if (typeof path !== 'string') { + throw Error(`id ${id} is not a string!!! Something wrong here`); + } + + if (path.match(/[$]void_[^.]+$/)) { + return undefined; + } + + return path.replace(/[$]void_[^.]+./g, ''); +} + +export function getRealDataFlatten(flatten) { + // 处理 $void_xxx + const flatten2 = { ...flatten }; + for (let key of Object.keys(flatten2)) { + const newKey = key.replace(/[$]void_[^.]+\b[.]?/g, ''); + if (newKey !== key) { + const oldValue = flatten2[key]; + delete flatten2[key]; + if (newKey !== '') { + flatten2[newKey] = oldValue; + } + } + } + return flatten2; +} + +export function removeVoidFromResult(data) { + const result = cloneDeep(data); + const keys = dataToKeys(result); + keys.forEach(key => { + if (key.match(/[$]void_[^.]+$/)) { + delete result[key]; + } + }); + return result; +} diff --git a/packages/form-render/src/index.js b/packages/form-render/src/index.js index ae8e0ecf6..bcfb2d8d0 100644 --- a/packages/form-render/src/index.js +++ b/packages/form-render/src/index.js @@ -2,7 +2,7 @@ import { ConfigProvider } from 'antd'; import zhCN from 'antd/lib/locale/zh_CN'; import React from 'react'; import FRCore from './form-render-core/src'; -import { widgets as defaultWidgets } from './widgets/antd'; +import { widgets as defaultWidgets } from './widgets'; export { connectForm, diff --git a/packages/form-render/src/widgets/antd/map.js b/packages/form-render/src/widgets/antd/map.js index a2f669251..20b8e694b 100644 --- a/packages/form-render/src/widgets/antd/map.js +++ b/packages/form-render/src/widgets/antd/map.js @@ -10,6 +10,7 @@ export default function Map({ children, title, schema }) { const { theme: globalTheme, displayType: globalDisplayType } = useStore2(); const theme = schema.theme || globalTheme; + const props = schema?.props || {}; const displayType = schema.displayType || globalDisplayType; useEffect(() => { @@ -39,7 +40,9 @@ export default function Map({ children, title, schema }) {
- {children} +
+ {children} +
); @@ -60,11 +63,18 @@ export default function Map({ children, title, schema }) { } className="fr-theme-card-wrap" > - {children} +
+ {children} +
); } + // 支持自定义弹性布局 + if (theme === 'flex') { + return
{children}
; + } + const toggle = keyList => { if (keyList.length > 0) { setCollapsed(false); @@ -99,7 +109,9 @@ export default function Map({ children, title, schema }) { 'fr-collapse-object-child-column': displayType === 'column', })} > - {children} +
+ {children} +
diff --git a/packages/form-render/src/widgets/antd/html.jsx b/packages/form-render/src/widgets/html.jsx similarity index 100% rename from packages/form-render/src/widgets/antd/html.jsx rename to packages/form-render/src/widgets/html.jsx diff --git a/packages/form-render/src/widgets/antd/index.js b/packages/form-render/src/widgets/index.js similarity index 71% rename from packages/form-render/src/widgets/antd/index.js rename to packages/form-render/src/widgets/index.js index 9998e3e23..59666ee7e 100644 --- a/packages/form-render/src/widgets/antd/index.js +++ b/packages/form-render/src/widgets/index.js @@ -1,21 +1,21 @@ import React from 'react'; import { Checkbox, Input, InputNumber, Rate, Switch, TreeSelect } from 'antd'; -import checkboxes from './checkboxes'; -import color from './color'; -import date from './date'; -import dateRange from './dateRange'; +import checkboxes from './antd/checkboxes'; +import color from './antd/color'; +import date from './antd/date'; +import dateRange from './antd/dateRange'; import Html from './html'; -import ImageInput from './imageInput'; +import ImageInput from './antd/imageInput'; import list from './list'; -import map from './map'; -import multiSelect from './multiSelect'; -import radio from './radio'; -import select from './select'; -import slider from './slider'; -import time from './time'; -import timeRange from './timeRange'; -import upload from './upload'; -import urlInput from './urlInput'; +import map from './antd/map'; +import multiSelect from './antd/multiSelect'; +import radio from './antd/radio'; +import select from './antd/select'; +import slider from './antd/slider'; +import time from './antd/time'; +import timeRange from './antd/timeRange'; +import upload from './antd/upload'; +import urlInput from './antd/urlInput'; // const Cascader = React.lazy(() => import('antd/es/cascader')); diff --git a/packages/form-render/src/widgets/antd/list.jsx b/packages/form-render/src/widgets/list.jsx similarity index 100% rename from packages/form-render/src/widgets/antd/list.jsx rename to packages/form-render/src/widgets/list.jsx diff --git a/packages/table-render/src/components/Search.tsx b/packages/table-render/src/components/Search.tsx index 5ba3c6f4a..a3a0479af 100644 --- a/packages/table-render/src/components/Search.tsx +++ b/packages/table-render/src/components/Search.tsx @@ -119,18 +119,25 @@ const Search: ( }, [_schema]); useEffect(() => { + init(); + }, []); + + async function init() { syncMethods({ searchApi: props.api, syncAfterSearch: props.afterSearch, }); if (!props.hidden && searchOnMount) { + if (typeof searchFormProps.onMount === 'function') { + await searchFormProps.onMount(); + } form.submit(); } // 隐藏search组件时,不会触发form.submit if (props.hidden) { refresh(); } - }, []); + } const btnProps = { searchBtnRender, diff --git a/tools/schema-generator/src/components/Canvas/core/Wrapper.less b/tools/schema-generator/src/components/Canvas/core/Wrapper.less index f3699bb2f..6853c23de 100644 --- a/tools/schema-generator/src/components/Canvas/core/Wrapper.less +++ b/tools/schema-generator/src/components/Canvas/core/Wrapper.less @@ -4,6 +4,17 @@ margin: 0 0 8px 0; } +// 统一编辑区滚动条样式 +.field-wrapper::-webkit-scrollbar { + width: 5px; + background-color: #0002; +} + +.field-wrapper::-webkit-scrollbar-thumb { + background: #0004; + border-radius: 5px; +} + .dnd-container > .field-wrapper { margin: 0; } diff --git a/tools/schema-generator/src/components/Canvas/core/index.jsx b/tools/schema-generator/src/components/Canvas/core/index.jsx index 68a2a6697..e71f70b58 100644 --- a/tools/schema-generator/src/components/Canvas/core/index.jsx +++ b/tools/schema-generator/src/components/Canvas/core/index.jsx @@ -57,18 +57,28 @@ const FR = ({ id = '#', preview, displaySchema }) => { let contentClass = 'fr-content'; let columnStyle = {}; - if (!isComplex && width) { + if (width) { columnStyle = { width, paddingRight: '12px', }; - } else if (!isComplex && column > 1) { + } else if (column > 1) { columnStyle = { width: `calc(100% /${column})`, paddingRight: '12px', }; } + // 如果传入自定义样式则覆盖使用,object 外层样式使用 schema.style,内层样式使用 schema.props.style + // 由于 form-render-core 使用多层 div,而这里只使用一层,所以合并内外层样式 + if('object' === typeof schema?.style || 'object' === typeof schema?.props?.style) { + columnStyle = { + ...columnStyle, + ...(schema?.style || {}), + ...(schema?.props?.style || {}) + } + } + switch (schema.type) { case 'object': if (schema.title) { diff --git a/tools/schema-generator/src/components/Settings/index.less b/tools/schema-generator/src/components/Settings/index.less index 5acdfba79..ea9698b21 100644 --- a/tools/schema-generator/src/components/Settings/index.less +++ b/tools/schema-generator/src/components/Settings/index.less @@ -10,11 +10,11 @@ // 统一左右编辑区滚动条样式 .fr-generator-container .right-layout ::-webkit-scrollbar { width: 5px; - background-color: #0001; + background-color: #0002; } .fr-generator-container .right-layout ::-webkit-scrollbar-thumb { - background: #0003; + background: #0004; border-radius: 5px; } diff --git a/tools/schema-generator/src/components/Sidebar/index.less b/tools/schema-generator/src/components/Sidebar/index.less index ed228856f..87e8ac3d9 100644 --- a/tools/schema-generator/src/components/Sidebar/index.less +++ b/tools/schema-generator/src/components/Sidebar/index.less @@ -8,11 +8,11 @@ // 修复 Windows 滚动条太宽导致两列布局变成一列 .left-layout::-webkit-scrollbar { width: 5px; - background-color: #0001; + background-color: #0002; } .left-layout::-webkit-scrollbar-thumb { - background: #0003; + background: #0004; border-radius: 5px; } diff --git a/tools/schema-generator/src/settings/index.js b/tools/schema-generator/src/settings/index.js index b9434c2bb..f82786c7c 100644 --- a/tools/schema-generator/src/settings/index.js +++ b/tools/schema-generator/src/settings/index.js @@ -553,7 +553,218 @@ export const layouts = [ type: 'object', properties: {}, }, - setting: {}, + setting: { + theme: { + title: '主题', + type: 'string', + enum: [ + "collapse", + "collapse:pure", + "collapse:ghost", + "card", + "tile", + "flex", + ], + enumNames: [ + "默认", + "无框", + "幽灵", + "卡片", + "平铺", + "弹性", + ], + default: 'collapse', + widget: "radio", + }, + props: { + title: '弹性布局', + hidden: '{{"flex" !== formData.theme}}', + type: 'object', + theme: 'tile', + properties: { + style: { + title: '布局样式', + type: 'object', + theme: 'flex', + props: { + style: { + flexDirection: 'column', + } + }, + properties: { + height: { + title: '高度', + description: 'height', + type: 'string', + widget: 'input', + }, + flexDirection: { + title: '布局方向', + description: 'flex-direction', + type: 'string', + enum: [ + 'row', + 'row-reverse', + 'column', + 'column-reverse', + ], + enumNames: [ + '横向', + '横向反转', + '纵向', + '纵向反转', + ], + widget: 'select', + }, + flexWrap: { + title: '换行方式', + description: 'flex-wrap', + type: 'string', + enum: [ + 'wrap', + 'nowrap', + 'wrap-reverse', + ], + enumNames: [ + '换行', + '不换行', + '反向换行', + ], + widget: 'select', + }, + justifyContent: { + title: '对齐方式', + description: 'justify-content', + type: 'string', + enum: [ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + ], + enumNames: [ + '起点对齐', + '终点对齐', + '居中对齐', + '两端对齐', + '相同间距', + ], + widget: 'select', + }, + alignItems: { + title: '轴对齐方式', + description: 'align-items', + type: 'string', + enum: [ + 'flex-start', + 'flex-end', + 'center', + 'baseline', + 'stretch', + ], + enumNames: [ + '起点对齐', + '终点对齐', + '居中对齐', + '基线对齐', + '拉伸铺满', + ], + widget: 'select', + }, + alignContent: { + title: '多轴线对齐', + description: 'align-content', + type: 'string', + enum: [ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + 'stretch', + ], + enumNames: [ + '起点对齐', + '终点对齐', + '居中对齐', + '两端对齐', + '相同间距', + '拉伸铺满', + ], + widget: 'select', + }, + } + }, + } + }, + style: { + title: '元素样式', + type: 'object', + properties: { + background: { + title: '背景', + description: 'background', + type: 'string', + widget: 'color', + }, + margin: { + title: '外边距', + description: 'margin', + type: 'string', + widget: 'input', + }, + padding: { + title: '内边距', + description: 'padding', + type: 'string', + widget: 'input', + }, + borderWidth: { + title: '边框宽', + description: 'border-width', + type: 'string', + widget: 'input', + }, + borderStyle: { + title: '边框样式', + description: 'border-style', + type: 'string', + widget: 'input', + }, + borderColor: { + title: '边框颜色', + description: 'border-color', + type: 'string', + widget: 'color', + }, + borderRadius: { + title: '圆角', + description: 'border-radius', + type: 'string', + widget: 'input', + }, + flex: { + title: '弹性伸缩', + description: 'flex', + type: 'string', + widget: 'input', + }, + order: { + title: '排列顺序', + description: 'order', + type: 'string', + widget: 'input', + }, + alignSelf: { + title: '自身对齐', + description: 'align-self', + type: 'string', + widget: 'input', + }, + } + }, + }, }, { text: '常规列表',