diff --git a/README.md b/README.md index 4b25f6e82..61b422204 100755 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ## 变化 -- FormRender 已升级到 XRender 1.0 版本,并提供开箱即用的 Form/Table/Chart 解决方案,原版本可见 **旧文档** 。 +- FormRender 已升级到 XRender 1.0 版本,并提供开箱即用的 Form/Table/Chart 解决方案,原版本可见旧文档。 - FormRender 0.x 版本直接升级到 1.0 需修改 API 才能正常运行,可见 [0.x 到 1.x](https://x-render.gitee.io/form-render/migrate),很推荐升级。 ## 谁在使用? @@ -125,7 +125,7 @@ ## 贡献 -想贡献代码、解 BUG 或者提高文档可读性?非常欢迎一起参与进来,在提交 MR 前阅读一下 [Contributing Guide](https://github.com/alibaba/form-render/blob/master/CONTRIBUTING.md)。 +想贡献代码、解 BUG 或者提高文档可读性?非常欢迎一起参与进来,在提交 PR 前阅读一下 [Contributing Guide](https://github.com/alibaba/form-render/blob/master/CONTRIBUTING.md)。 感谢给 XRender 贡献代码的你们: diff --git a/docs/form-render/.test/test-1.0.5.md b/docs/form-render/.test/test-1.0.5.md new file mode 100644 index 000000000..2634c9dcd --- /dev/null +++ b/docs/form-render/.test/test-1.0.5.md @@ -0,0 +1,105 @@ +```jsx +import React, { useEffect } from 'react'; +import { Button } from 'antd'; +import FormRender, { useForm } from 'form-render'; + +const schema = { + displayType: 'row', + type: 'object', + properties: { + input1: { + title: '简单输入框简单输入框', + description: 'sdfdsgfshfghfgdh', + type: 'string', + required: true, + rules: [ + { + required: true, + message: 'ete', + }, + ], + }, + input2: { + title: '简单输入框2', + type: 'boolean', + }, + input3: { + title: '简单输入框3', + type: 'string', + required: true, + }, + image: { + title: '图片展示', + type: 'string', + format: 'image', + }, + checkboxes: { + title: '多选', + description: '下拉多选', + type: 'array', + items: { + type: 'string', + }, + enum: ['A', 'B', 'C', 'D'], + enumNames: ['杭州', '武汉', '湖州', '贵阳'], + widget: 'checkboxes', + default: null, + }, + multiSelect: { + title: '多选', + description: '下拉多选', + type: 'array', + items: { + type: 'string', + }, + enum: ['A', 'B', 'C', 'D'], + enumNames: ['杭州', '武汉', '湖州', '贵阳'], + widget: 'multiSelect', + default: null, + }, + }, +}; + +const Demo = () => { + const form = useForm(); + useEffect(() => { + form.setValues({ a: 1, b: 2, c: { x: { y: [{ z: 'sdf' }] } } }); + }, []); + const onFinish = (formData, errorFields) => { + if (errorFields.length > 0) { + alert( + 'errorFields:' + + JSON.stringify(errorFields) + + '\nformData:' + + JSON.stringify(formData, null, 2) + ); + } else { + alert('formData:' + JSON.stringify(formData, null, 2)); + } + }; + + return ( +
+ + +
+ ); +}; + +export default Demo; +``` + +label, descIcon +入参 +随便传入什么值,都会透传,不会被 schema 截取 +null 值在多选里的展示 + +title 的布局需要重新写一下 diff --git a/docs/form-render/.test/test-bind.md b/docs/form-render/.test/test-bind.md new file mode 100644 index 000000000..15a0ad085 --- /dev/null +++ b/docs/form-render/.test/test-bind.md @@ -0,0 +1,69 @@ +```jsx +import React, { useEffect } from 'react'; +import { Button, Modal } from 'antd'; +import FormRender, { useForm } from 'form-render'; + +const delay = ms => new Promise(res => setTimeout(res, ms)); + +const schema = { + type: 'object', + properties: { + inputName: { + bind: 'ttt', + title: '简单输入框', + type: 'string', + format: 'image', + }, + listName: { + bind: 'a.x', + title: '对象数组', + description: '对象数组嵌套功能', + type: 'array', + items: { + type: 'object', + properties: { + inputName2: { + title: '复杂输入框', + description: '英文或数字组合', + type: 'string', + }, + selectName: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + widget: 'radio', + }, + }, + }, + }, + }, +}; + +const Demo = () => { + const form = useForm(); + + useEffect(() => { + form.setValues({ ttt: '234', a: { x: [{ inputName2: 'hello' }] } }); + }, []); + + const onFinish = (formData, errorFields) => { + if (errorFields.length > 0) { + alert('errorFields:' + JSON.stringify(errorFields)); + } else { + alert('formData:' + JSON.stringify(formData, null, 2)); + } + }; + + return ( +
+ + +
+ ); +}; + +export default Demo; +``` diff --git a/docs/form-render/.test/test-time.md b/docs/form-render/.test/test-time.md new file mode 100644 index 000000000..13898a399 --- /dev/null +++ b/docs/form-render/.test/test-time.md @@ -0,0 +1,87 @@ +```jsx +import React from 'react'; +import { Button } from 'antd'; +import FormRender, { useForm } from 'form-render'; + +const schema = { + displayType: 'row', + labelWidth: 120, + type: 'object', + properties: { + input1: { + title: '简单输入框', + description: + 'sdfasdgdsgfdsgfsdfgssdgdsgfdsgfsdfgssdgdsgfdsgfsdfgsdfgsdfgfghfghfghfgh', + type: 'string', + required: true, + }, + input2: { + title: '简单输入框2', + type: 'boolean', + }, + daaa: { + title: 'daa', + type: 'string', + format: 'date', + props: { + showTime: true, + format: 'dateTime', + }, + }, + daaa2: { + title: 'daa2', + type: 'string', + format: 'time', + }, + daaa3: { + title: 'daa3', + type: 'range', + format: 'date', + }, + input3: { + title: '图片', + type: 'string', + format: 'image', + required: true, + }, + input4: { + title: 'url', + type: 'string', + format: 'url', + required: true, + }, + }, +}; + +const Demo = () => { + const form = useForm(); + const onFinish = (formData, errorFields) => { + if (errorFields.length > 0) { + alert( + 'errorFields:' + + JSON.stringify(errorFields) + + '\nformData:' + + JSON.stringify(formData, null, 2) + ); + } else { + alert('formData:' + JSON.stringify(formData, null, 2)); + } + }; + + return ( +
+ + +
+ ); +}; + +export default Demo; +``` diff --git a/docs/form-render/.test/validation.md b/docs/form-render/.test/validation.md new file mode 100644 index 000000000..b9a1423ab --- /dev/null +++ b/docs/form-render/.test/validation.md @@ -0,0 +1,9 @@ +--- +order: 5 +group: + order: 3 + title: 高级用法 +toc: false +--- + +# 表单校验 diff --git a/docs/form-render/advanced/widget.md b/docs/form-render/advanced/widget.md index e0d0e47a4..e48cfd339 100644 --- a/docs/form-render/advanced/widget.md +++ b/docs/form-render/advanced/widget.md @@ -174,14 +174,14 @@ export const mapping = { object: 'map', html: 'html', 'string:upload': 'upload', - 'string:date': 'date', 'string:url': 'url', 'string:dateTime': 'date', - 'string:time': 'date', + 'string:date': 'date', + 'string:time': 'time', 'string:textarea': 'textarea', 'string:color': 'color', 'string:image': 'imageInput', - 'range:time': 'dateRange', + 'range:time': 'timeRange', 'range:date': 'dateRange', 'range:dateTime': 'dateRange', '*?enum': 'radio', diff --git a/docs/form-render/index.md b/docs/form-render/index.md index 596851055..eddc957df 100644 --- a/docs/form-render/index.md +++ b/docs/form-render/index.md @@ -196,7 +196,7 @@ export default Demo; import Form, { useForm, connectForm } from 'form-render'; ``` -#### \
(常用 props) +### \ (常用 props) | 参数 | 描述 | 类型 | 是否必填 | 默认值 | | ------------ | -------------------------------------------------------------- | ------------------------------------------------------------------ | -------- | ---------- | @@ -207,18 +207,35 @@ import Form, { useForm, connectForm } from 'form-render'; | displayType | 表单元素与 label 同行 or 分两行展示, inline 则整个展示自然顺排 | `string('column' / 'row' / 'inline')` | 否 | 'column' | | widgets | 自定义组件,当内置组件无法满足时使用 | `object` | 否 | {} | -#### \ (不常用 props) +### \ (不常用 props) -| 参数 | 描述 | 类型 | 默认值 | -| -------------- | ---------------------------------------------------------------- | ------------------- | ------ | -| column | 一行展示多少列 | `number` | 1 | -| mapping | schema 与组件的映射关系表,当内置的表不满足时使用 | `object` | {} | -| debug | 开启 debug 模式,提供更多信息 | `boolean` | false | -| locale | 展示语言,目前只支持中文、英文 | `string('cn'/'en')` | 'cn' | -| configProvider | antd 的 configProvider,配置透传 | `object` | - | -| debounceInput | 是否开启输入时使用快照模式。仅建议在表单巨大且表达式非常多时开启 | `boolean` | false | +| 参数 | 描述 | 类型 | 默认值 | +| ---------------- | ---------------------------------------------------------------- | ------------------- | ------ | +| column | 一行展示多少列 | `number` | 1 | +| mapping | schema 与组件的映射关系表,当内置的表不满足时使用 | `object` | {} | +| debug | 开启 debug 模式,时时显示表单内部状态 | `boolean` | false | +| debugCss | 用于 css 问题的调整,显示 css 布局提示线 | `boolean` | false | +| locale | 展示语言,目前只支持中文、英文 | `string('cn'/'en')` | 'cn' | +| configProvider | antd 的 configProvider,配置透传 | `object` | - | +| debounceInput | 是否开启输入时使用快照模式。仅建议在表单巨大且表达式非常多时开启 | `boolean` | false | +| validateMessages | 修改默认的校验提示信息。详见下 | `object` | {} | -#### useForm / connectForm +#### validateMessages + +`Form` 为验证提供了[默认的错误提示信息](https://github.com/alibaba/x-render/blob/master/packages/form-render/src/validateMessageCN.js),你可以通过配置 `validateMessages` 属性,修改对应的提示模板。一种常见的使用方式,是配置国际化提示信息: + +```js +const validateMessages = { + required: '${title}是必选字段', + // ... +}; + +; +``` + +目前可以用的转义字段为 `${title}`/`${min}`/`${max}`/`${len}`/`${pattern}`, 如果有更多需求请提 [issue](https://github.com/alibaba/x-render/issues/new/choose) + +### useForm / connectForm `useForm` / `connectForm` 用于创建表单实例,所有对表单的外部操作和回调函数全挂在其生产的实例上,例如表单提交是 `form.submit`。注意 `useForm` 是 hooks,而 `connectForm` 是高阶组件,所以前者只能在函数组件使用,后者可用于 class 组件。两者无其他区别。使用时需要创建实例,并通过 props 挂钩到与其对应的表单上: diff --git a/docs/form-render/schema.md b/docs/form-render/schema.md index 60da0a2e9..d20cc82f9 100644 --- a/docs/form-render/schema.md +++ b/docs/form-render/schema.md @@ -208,6 +208,7 @@ export default () => ; 1. 当服务端接口获取的字段与你希望的表单展示结构不同时,可以通过 bind 字段绑定的方式指明表单的某个字段对应的是外部数据的另一个字段。详细例子见 [“表单与外界的交互”](/form-render/advanced/form-methods) 的例 3 2. 用户并不希望纯展示的字段也出现在表单中,此时,使用 bind: `false` 可避免字段在提交时出现 +3. 注意:请不要 bind 一个数组结构下面的字段,目前没有对此进行处理,所以会无效(在未来这个限制会解除) #### min diff --git a/docs/index.md b/docs/index.md index f79e40099..013965d0c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,9 +19,12 @@ features: title: ChartRender desc: 傻瓜式的图表绘制库 footer: Please feel free to use and contribute to the development. -translateHelp: FormRender 已升级到 XRender 1.0 版本,并提供开箱即用的 Form/Table/Chart 解决方案,原版本可见 旧文档。 --- + + FormRender 已升级到 v1.x 版本,并对外提供中后台开箱即用 XRender 表单 / 表格 / 图表方案,如需使用老版本(v0.x),请点击右上角 旧文档 + + ## 谁在使用? @@ -104,7 +107,7 @@ translateHelp: FormRender 已升级到 XRender 1.0 版本,并提供开箱即 ## 贡献 -想贡献代码、解 BUG 或者提高文档可读性?非常欢迎一起参与进来,在提交 MR 前阅读一下 [Contributing Guide](https://github.com/alibaba/form-render/blob/master/CONTRIBUTING.md)。 +想贡献代码、解 BUG 或者提高文档可读性?非常欢迎一起参与进来,在提交 PR 前阅读一下 [Contributing Guide](https://github.com/alibaba/form-render/blob/master/CONTRIBUTING.md)。 感谢给 XRender 贡献代码的你们: diff --git a/docs/tools/generator/index.md b/docs/tools/generator/index.md index 2982caef9..f9a0fc89a 100644 --- a/docs/tools/generator/index.md +++ b/docs/tools/generator/index.md @@ -69,7 +69,7 @@ export default Demo; | 事件名 | 说明 | 回调参数 | | --- | --- | --- | | onChange | 表单 data 变化回调 | 表单的 data | -| onSchameChange | 表单 schema 变化回调 | 导出的 schema | +| onSchemaChange | 表单 schema 变化回调 | 导出的 schema | ### Methods diff --git a/packages/chart-render/CHANGELOG.md b/packages/chart-render/CHANGELOG.md index fff866e1e..1c13dab6f 100644 --- a/packages/chart-render/CHANGELOG.md +++ b/packages/chart-render/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +### 0.1.7 + +- [+] 折线图单指标双维度 增加百分数支持 + +### 0.1.6 + +- [+] 折线图单指标单维度 增加百分数支持 + ### 0.1.5 - [+] 折线图增加双指标 双维度 支持 diff --git a/packages/chart-render/package.json b/packages/chart-render/package.json index eb80f945e..77ac3ddf9 100644 --- a/packages/chart-render/package.json +++ b/packages/chart-render/package.json @@ -1,6 +1,6 @@ { "name": "chart-render", - "version": "0.1.5", + "version": "0.1.7", "scripts": { "build": "father-build", "prepare": "npm run build", diff --git a/packages/chart-render/src/components/Column/index.tsx b/packages/chart-render/src/components/Column/index.tsx index 77de7bb6b..4c0867690 100644 --- a/packages/chart-render/src/components/Column/index.tsx +++ b/packages/chart-render/src/components/Column/index.tsx @@ -14,7 +14,7 @@ export interface ICRColumnProps /** * 是否倒置,倒置后柱形图会表现成条形图 */ - inverted: boolean; + inverted?: boolean; }; export function generateConfig(meta: ICommonProps['meta'], data: ICommonProps['data']): ColumnConfig { diff --git a/packages/chart-render/src/components/Line/index.tsx b/packages/chart-render/src/components/Line/index.tsx index 4577b7c52..a59eebb94 100644 --- a/packages/chart-render/src/components/Line/index.tsx +++ b/packages/chart-render/src/components/Line/index.tsx @@ -3,41 +3,81 @@ import { Area, Line, DualAxes } from '@ant-design/charts'; import { AreaConfig } from '@ant-design/charts/es/Area'; import { LineConfig } from '@ant-design/charts/es/line'; import { DualAxesConfig } from '@ant-design/charts/es/dualAxes'; -import { ICommonProps } from '../../utils/types'; -import { splitMeta } from '../../utils'; +import { ICommonProps, IMetaItem } from '../../utils/types'; +import { splitMeta, strip } from '../../utils'; import ErrorTemplate from '../ErrorTemplate'; -export interface ICRLineProps extends ICommonProps, Omit { +export interface ILine extends ICommonProps, Omit { /** * 以面积图展示,默认 `false` * - 注意面积图默认堆叠展示,如不需要可以传入 `isStack={false}` 覆盖 * - 开启面积图后方可使用 `areaStyle` `startOnZero` `isPercent` 属性 */ - withArea: boolean; -}; + withArea?: boolean; +} +export interface IArea extends ICommonProps, Omit {}; +export interface IDualAxes extends ICommonProps, Omit {}; export function generateConfig(meta: ICommonProps['meta'], data: ICommonProps['data']): AreaConfig | LineConfig | DualAxesConfig { const { metaDim, metaInd } = splitMeta(meta); if (metaInd.length === 1 && metaDim.length === 1) { // case 1: 单指标、单维度 => 维度作为 x 轴,指标作为 y 轴 - const xField = metaDim.shift()?.id as string; - const yField = metaInd.shift()?.id as string; + const xFieldMeta = metaDim.shift() as IMetaItem; + const yFieldMeta = metaInd.shift() as IMetaItem; + const xField = xFieldMeta.id; + const yField = yFieldMeta.id; return { data, xField, yField, + yAxis: { + label: { + formatter: v => { + return yFieldMeta.isRate ? `${strip(100 * Number(v))}%` : v; + }, + }, + }, + tooltip: { + formatter: ({ [xField]: type, [yField]: value }) => { + return { + name: yFieldMeta.name, + value: yFieldMeta.isRate ? `${strip(100 * Number(value))}%` : value, + } + }, + }, meta: { [yField]: { alias: meta.find(({ id }) => id === yField)?.name } }, }; } else if (metaInd.length === 1 && metaDim.length === 2) { // case 2: 单指标、双维度 => 第一维度作为 x 轴,指标作为 y 轴,第二维度作为 系列 + const xFieldMeta = metaDim.shift() as IMetaItem; + const yFieldMeta = metaInd.shift() as IMetaItem; + const seriesFieldMeta = metaDim.shift() as IMetaItem; + const xField = xFieldMeta.id; + const yField = yFieldMeta.id; + const seriesField = seriesFieldMeta.id; return { data, - xField: metaDim.shift()?.id as string, - yField: metaInd.shift()?.id as string, - seriesField: metaDim.shift()?.id, + xField, + yField, + seriesField, + yAxis: { + label: { + formatter: v => { + return yFieldMeta.isRate ? `${strip(100 * Number(v))}%` : v; + }, + }, + }, + tooltip: { + formatter: ({ [seriesField]: type, [yField]: value }) => { + return { + name: type, + value: yFieldMeta.isRate ? `${strip(100 * Number(value))}%` : value, + } + }, + }, }; } else if (metaInd.length === 2 && metaDim.length === 2) { // case 3: 双指标、双维度 => 第一维度作为 x 轴,第二维度作为 系列,第一指标作为左 y 轴,第二指标作为右 y 轴 @@ -79,11 +119,12 @@ export function generateConfig(meta: ICommonProps['meta'], data: ICommonProps['d return { data }; }; -const CRLine: React.FC = ({ +const CRLine: React.FC = ({ className, style, meta = [], data = [], + // @ts-ignore withArea, ...props }) => { diff --git a/packages/form-render/.fatherrc.js b/packages/form-render/.fatherrc.js index 0cca75b8a..e1f5c6f1a 100644 --- a/packages/form-render/.fatherrc.js +++ b/packages/form-render/.fatherrc.js @@ -1,11 +1,8 @@ import copy from 'rollup-plugin-copy'; export default { - entry: ['src/index.js'], esm: 'rollup', cjs: 'rollup', - lessInBabelMode: true, - lessInRollupMode: {}, extraRollupPlugins: [ copy({ targets: [{ src: 'src/index.d.ts', dest: 'dist/' }], diff --git a/packages/form-render/CHANGELOG.md b/packages/form-render/CHANGELOG.md index a67490f54..65e7a686c 100644 --- a/packages/form-render/CHANGELOG.md +++ b/packages/form-render/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +### 1.0.5 + +- [+] 新增 `validateMessages` 字段,用于覆盖默认的校验信息,详见[文档](https://x-render.gitee.io/form-render#validatemessages) ([#306](https://github.com/alibaba/x-render/issues/306)) +- [!] rules 字段无法生效的问题 ([#305](https://github.com/alibaba/x-render/issues/305)) +- [!] 修复了下拉多选框在 value = null 时会展示一个空标签的问题 +- [!] 说明(description)的 tooltip 展示气泡位置确保对齐 + +### 1.0.4 + +- [+] 新增时间区间组件 timeRange。通过`{type: 'range', format: 'time'}` 渲染 +- [!] 完善 ts 声明文件。([#302](https://github.com/alibaba/x-render/pull/302)) +- [!] fix 了 rules 校验和 image 校验的冲突 +- [!] 修复了 tooltip、checkbox 等样式的问题 + +### 1.0.3 + +- [!] fix 列表的 bind 无效的问题 +- [!] fix format: image 未校验的问题 +- [!] 默认列表样式微调 + ### 1.0.2 - [+] 新增默认列表展示 card list。widget: list0 diff --git a/packages/form-render/package.json b/packages/form-render/package.json index 96d3c7822..dacbc4dbe 100644 --- a/packages/form-render/package.json +++ b/packages/form-render/package.json @@ -1,6 +1,6 @@ { "name": "form-render", - "version": "1.0.2", + "version": "1.0.5", "description": "通过 JSON Schema 生成标准 Form,常用于自定义搭建配置界面生成", "repository": { "type": "git", diff --git a/packages/form-render/src/core/RenderChildren/list.less b/packages/form-render/src/core/RenderChildren/list.less index 7fd658331..3e6bfdafd 100644 --- a/packages/form-render/src/core/RenderChildren/list.less +++ b/packages/form-render/src/core/RenderChildren/list.less @@ -5,7 +5,7 @@ .fr-card-item { border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 4px; - padding: 8px 8px 4px 8px; + padding: 8px 12px 4px 12px; margin-bottom: 8px; position: relative; display: flex; diff --git a/packages/form-render/src/core/RenderField/ExtendedWidget.js b/packages/form-render/src/core/RenderField/ExtendedWidget.js index 88a576d3f..c8aa420a1 100644 --- a/packages/form-render/src/core/RenderField/ExtendedWidget.js +++ b/packages/form-render/src/core/RenderField/ExtendedWidget.js @@ -58,7 +58,7 @@ const ExtendedWidget = ({ ...schema.props, }; - ['title', 'placeholder', 'disabled'].forEach(key => { + ['title', 'placeholder', 'disabled', 'format'].forEach(key => { if (schema[key]) { widgetProps[key] = schema[key]; } diff --git a/packages/form-render/src/core/index.js b/packages/form-render/src/core/index.js index 019ee3b02..066c19549 100644 --- a/packages/form-render/src/core/index.js +++ b/packages/form-render/src/core/index.js @@ -18,6 +18,7 @@ const Core = ({ dataIndex = [], // 数据来源是数组的第几个index,上层每有一个list,就push一个index hideTitle = false, hideValidation = false, + debugCss, ...rest }) => { // console.log(''); @@ -186,7 +187,10 @@ const Core = ({ // TODO: list 也要算进去 return ( -
+
{isObjType && objChildren} {isList && listChildren} diff --git a/packages/form-render/src/index.d.ts b/packages/form-render/src/index.d.ts index 193a7c933..b69b110d1 100644 --- a/packages/form-render/src/index.d.ts +++ b/packages/form-render/src/index.d.ts @@ -52,9 +52,14 @@ export interface FRProps { labelWidth?: string; /** antd的全局config */ configProvider?: any; - theme?: any; + theme?: string | number; + /** 覆盖默认的校验信息 */ + validateMessages?: any; flatten?: any; + /** 显示当前表单内部状态 */ debug?: boolean; + /** 显示css布局提示线 */ + debugCss?: boolean; locale?: string; column?: number; debounceInput?: boolean; @@ -69,6 +74,6 @@ declare const FR: React.FC; export declare function useForm(params?: FormParams): FormInstance; -export declare function connectForm(): any; +export declare function connectForm(component: React.FC): any; export default FR; diff --git a/packages/form-render/src/index.js b/packages/form-render/src/index.js index 6743dd8fe..8773df8fa 100644 --- a/packages/form-render/src/index.js +++ b/packages/form-render/src/index.js @@ -34,11 +34,13 @@ function App({ schema, flatten: _flatten, debug, + debugCss, locale = 'cn', // 'cn'/'en' debounceInput = false, size, configProvider, theme, + validateMessages, ...rest }) { try { @@ -67,7 +69,7 @@ function App({ ]); useEffect(() => { - syncStuff({ schema, flatten, beforeFinish, locale }); + syncStuff({ schema, flatten, beforeFinish, locale, validateMessages }); }, [JSON.stringify(_flatten), JSON.stringify(schema)]); // 组件destroy的时候,destroy form,因为useForm可能在上层,所以不一定会跟着destroy @@ -145,7 +147,7 @@ function App({
{'isSubmitting:' + JSON.stringify(form.isSubmitting)}
) : null} - +
diff --git a/packages/form-render/src/index.less b/packages/form-render/src/index.less index 7b6f6e67c..5b4bf8474 100644 --- a/packages/form-render/src/index.less +++ b/packages/form-render/src/index.less @@ -7,7 +7,7 @@ padding: 0; color: rgba(0, 0, 0, 0.85); line-height: 1.5715; - /* font-variant: tabular-nums; */ + margin-bottom: 0; } .fr-field-column { @@ -280,13 +280,14 @@ .fr-tooltip-container { position: absolute; width: 160px; - left: 50%; white-space: initial !important; - bottom: 30px; - text-align: center; + top: 0; + left: 0; + transform: translateY(-120%); + text-align: left; background: #2b222a; padding: 4px; - margin-left: -77px; + margin-left: -69px; border-radius: 4px; color: #efefef; font-size: 13px; diff --git a/packages/form-render/src/mapping.js b/packages/form-render/src/mapping.js index a2af46c9e..101bebb14 100644 --- a/packages/form-render/src/mapping.js +++ b/packages/form-render/src/mapping.js @@ -8,14 +8,14 @@ export const mapping = { object: 'map', html: 'html', 'string:upload': 'upload', - 'string:date': 'date', 'string:url': 'url', 'string:dateTime': 'date', - 'string:time': 'date', + 'string:date': 'date', + 'string:time': 'time', 'string:textarea': 'textarea', 'string:color': 'color', 'string:image': 'imageInput', - 'range:time': 'dateRange', + 'range:time': 'timeRange', 'range:date': 'dateRange', 'range:dateTime': 'dateRange', '*?enum': 'radio', diff --git a/packages/form-render/src/processData.js b/packages/form-render/src/processData.js index 48bca2f86..26767a3e7 100644 --- a/packages/form-render/src/processData.js +++ b/packages/form-render/src/processData.js @@ -24,52 +24,42 @@ export const transformDataWithBind = (data, flatten) => { Object.keys(flatten).forEach(key => { const bind = flatten[key] && flatten[key].schema && flatten[key].schema.bind; + const _key = key.replace('[]', ''); if (bind === false) { - unbindKeys.push(key); + unbindKeys.push(_key); } else if (typeof bind === 'string') { - bindKeys.push({ key, bind }); + bindKeys.push({ key: _key, bind }); } else if (isMultiBind(bind)) { - bindArrKeys.push({ key, bind }); + bindArrKeys.push({ key: _key, bind }); } }); const handleBindData = formData => { unbindKeys.forEach(key => { - if (key.indexOf('[]') === -1) { - unset(formData, key); // TODO: 光remove了一个key,如果遇到remove了那个key上层的object为空了,object是不是也要去掉。。。不过感觉是伪需求 - } else { - // const keys = key.split('[]').filter(k => !!k); - // TODO: list里的元素要bind,贼复杂,而且基本上用不到,之后写吧 - } + unset(formData, key); // TODO: 光remove了一个key,如果遇到remove了那个key上层的object为空了,object是不是也要去掉。。。不过感觉是伪需求 }); bindKeys.forEach(item => { const { key, bind } = item; - if (key.indexOf('[]') === -1) { - let temp = get(formData, key); - // 如果已经有值了,要和原来的值合并,而不是覆盖 - const oldVal = get(formData, bind); - if (isObject(oldVal)) { - temp = { ...oldVal, ...temp }; - } - set(formData, bind, temp); - unset(formData, key); - } else { + let temp = get(formData, key); + // 如果已经有值了,要和原来的值合并,而不是覆盖 + const oldVal = get(formData, bind); + if (isObject(oldVal)) { + temp = { ...oldVal, ...temp }; } + set(formData, bind, temp); + unset(formData, key); }); bindArrKeys.forEach(item => { const { key, bind } = item; - if (key.indexOf('[]') === -1) { - const temp = get(formData, key); - if (Array.isArray(temp)) { - temp.forEach((t, i) => { - if (bind[i]) { - set(formData, bind[i], t); - } - }); - } - unset(formData, key); - } else { + const temp = get(formData, key); + if (Array.isArray(temp)) { + temp.forEach((t, i) => { + if (bind[i]) { + set(formData, bind[i], t); + } + }); } + unset(formData, key); }); }; handleBindData(_data); @@ -90,10 +80,11 @@ export const transformDataWithBind2 = (data, flatten) => { Object.keys(flatten).forEach(key => { const bind = flatten[key] && flatten[key].schema && flatten[key].schema.bind; + const _key = key.replace('[]', ''); if (typeof bind === 'string') { - bindKeys.push({ key, bind }); + bindKeys.push({ key: _key, bind }); } else if (isMultiBind(bind)) { - bindArrKeys.push({ key, bind }); + bindArrKeys.push({ key: _key, bind }); } }); diff --git a/packages/form-render/src/useForm.js b/packages/form-render/src/useForm.js index 7fab857f4..890f87672 100644 --- a/packages/form-render/src/useForm.js +++ b/packages/form-render/src/useForm.js @@ -35,6 +35,7 @@ export const useForm = props => { const clickSubmit = useRef(false); // 点击submit的那一下,不要执行useEffect里的validate const beforeFinishRef = useRef(); const localeRef = useRef('cn'); + const validateMessagesRef = useRef(); const _data = useRef({}); // 用ref是为了破除闭包的影响 const _touchedKeys = useRef([]); // 用ref是为了破除闭包的影响 @@ -98,6 +99,7 @@ export const useForm = props => { isRequired: true, touchedKeys: _touchedKeys.current, locale: localeRef.current, + validateMessages: validateMessagesRef.current, }).then(res => { const oldFormatErrors = res.map(item => item.name); _onValidate(oldFormatErrors); @@ -118,6 +120,7 @@ export const useForm = props => { isRequired: allTouched, touchedKeys: _touchedKeys.current, locale: localeRef.current, + validateMessages: validateMessagesRef.current, }).then(res => { _setErrors(res); }); @@ -142,11 +145,18 @@ export const useForm = props => { // { name: 'a.b.c', errors: ['Please input your Password!', 'something else is wrong'] }, // ] - const syncStuff = ({ schema, flatten, beforeFinish, locale }) => { + const syncStuff = ({ + schema, + flatten, + beforeFinish, + locale, + validateMessages, + }) => { schemaRef.current = schema; flattenRef.current = flatten; beforeFinishRef.current = beforeFinish; localeRef.current = locale; + validateMessagesRef.current = validateMessages; }; // TODO: 外部校验的error要和本地的合并么? @@ -193,6 +203,7 @@ export const useForm = props => { touchedKeys: [], isRequired: true, locale: localeRef.current, + validateMessages: validateMessagesRef.current, }) .then(errors => { // 如果有错误,也不停止校验和提交,在onFinish里让用户自己搞 diff --git a/packages/form-render/src/utils.js b/packages/form-render/src/utils.js index b7731987e..8bfbd2a5b 100644 --- a/packages/form-render/src/utils.js +++ b/packages/form-render/src/utils.js @@ -612,8 +612,9 @@ export const removeEmptyItemFromList = formData => { return result; }; -export const getDscriptorFromSchema = ({ schema, isRequired = true }) => { +export const getDescriptorFromSchema = ({ schema, isRequired = true }) => { let result = {}; + let singleResult = {}; if (isObjType(schema)) { result.type = 'object'; if (isRequired && schema.required === true) { @@ -626,7 +627,10 @@ export const getDscriptorFromSchema = ({ schema, isRequired = true }) => { if (Array.isArray(schema.required) && schema.required.indexOf(key) > -1) { item.required = true; } - result.fields[key] = getDscriptorFromSchema({ schema: item, isRequired }); + result.fields[key] = getDescriptorFromSchema({ + schema: item, + isRequired, + }); }); } else if (isListType(schema)) { result.type = 'array'; @@ -640,15 +644,13 @@ export const getDscriptorFromSchema = ({ schema, isRequired = true }) => { if (Array.isArray(schema.required) && schema.required.indexOf(key) > -1) { item.required = true; } - result.defaultField.fields[key] = getDscriptorFromSchema({ + result.defaultField.fields[key] = getDescriptorFromSchema({ schema: item, isRequired, }); }); } else { - // if (schema.type) { - // result.type = schema.type; - // } + // 单个的逻辑 const processRule = item => { if (schema.type) return { ...item, type: schema.type }; if (item.pattern && typeof item.pattern === 'string') { @@ -657,27 +659,16 @@ export const getDscriptorFromSchema = ({ schema, isRequired = true }) => { return item; }; const { required, ...rest } = schema; - if (isRequired && schema.required === true) { - rest.required = true; - } - if (schema.rules) { - if (Array.isArray(schema.rules)) { - const _rules = schema.rules.map(item => { - return processRule(item); - }); - result = [rest, ..._rules]; - } else if (isObject(schema.rules)) { - result = [rest, processRule(schema.rules)]; - } else { - result = rest; + + ['type', 'pattern', 'min', 'max', 'len'].forEach(key => { + if (Object.keys(rest).indexOf(key) > -1) { + singleResult[key] = rest[key]; } - } else { - result = rest; - // TODO1: 补齐 - } + }); + switch (schema.type) { case 'range': - result.type = 'array'; + singleResult.type = 'array'; break; default: break; @@ -685,14 +676,63 @@ export const getDscriptorFromSchema = ({ schema, isRequired = true }) => { switch (schema.format) { case 'email': case 'url': - result.type = schema.format; - break; - case 'image': - // TODO1: 补齐 + singleResult.type = schema.format; break; default: break; } + + let requiredRule; + if (isRequired && schema.required === true) { + requiredRule = { required: true }; + } + + if (schema.rules) { + if (Array.isArray(schema.rules)) { + const _rules = []; + schema.rules.forEach(item => { + if (item.required === true) { + if (isRequired) { + requiredRule = item; + } + } else { + _rules.push(processRule(item)); + } + }); + result = [singleResult, ..._rules]; + } else if (isObject(schema.rules)) { + // TODO: 规范上不允许rules是object,省一点事儿 + result = [singleResult, processRule(schema.rules)]; + } else { + result = singleResult; + } + } else { + result = singleResult; + } + + if (requiredRule) { + if (Array.isArray(result)) { + result.push(requiredRule); + } else if (isObject(result)) { + result = [result, requiredRule]; + } + } + + if (schema.format === 'image') { + const imgValidator = { + validator: (rule, value) => { + const pattern = /([/|.|w|s|-])*.(jpg|gif|png|bmp|apng|webp|jpeg|json)/; + if (value === undefined) return true; + return !!pattern.exec(value) || isUrl(value); + }, + message: '${title}的类型不是image', + }; + if (Array.isArray(result)) { + result.push(imgValidator); + } else if (isObject(result)) { + result = [result, imgValidator]; + } + } } return result; }; @@ -733,7 +773,7 @@ export const formatPathFromValidator = err => { // }, // }; // path = 'x.y' -// return true +// return {required: true, message?: 'xxxx'} export const isPathRequired = (path, schema) => { let pathArr = path.split('.'); while (pathArr.length > 0) { @@ -749,7 +789,19 @@ export const isPathRequired = (path, schema) => { if (childSchema) { return isPathRequired(rest.join('.'), childSchema); } - return !!schema.required; // 是否要这么干 TODO1: 意味着已经处理过了 + + // 单个的逻辑 + let result = { required: false }; + if (schema.required === true) { + result.required = true; + } + if (schema.rules) { + const requiredItem = schema.rules.find(item => item.required); + if (requiredItem) { + result = requiredItem; + } + } + return result; } }; @@ -859,7 +911,7 @@ export const updateSchemaToNewVersion = schema => { const updateSingleSchema = schema => { try { let _schema = clone(schema); - _schema.rules = []; + _schema.rules = _schema.rules || []; _schema.props = _schema.props || {}; if (_schema['ui:options']) { _schema.props = _schema['ui:options']; diff --git a/packages/form-render/src/validator.js b/packages/form-render/src/validator.js index b7cec4ef5..46fec0f5a 100644 --- a/packages/form-render/src/validator.js +++ b/packages/form-render/src/validator.js @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { - getDscriptorFromSchema, + getDescriptorFromSchema, formatPathFromValidator, isPathRequired, generateDataSkeleton, @@ -18,23 +18,27 @@ export const validateAll = ({ isRequired = true, touchedKeys = [], locale = 'cn', + validateMessages = {}, }) => { if (Object.keys(schema).length === 0) return Promise.resolve(); - const descriptor = getDscriptorFromSchema({ + const descriptor = getDescriptorFromSchema({ schema, isRequired, }).fields; - // console.log(descriptor, '&&&& descriptor'); + // console.log(descriptor, '&&&& descriptor', formData); let touchVerifyList = []; // 如果是最后的校验,所有key都touch了,就不用算这个了 + // 因为要整个构建validator在list的情况太复杂了,所以required单独拿出来处理,但是这边有不少单独处理逻辑,例如message if (!isRequired) { touchedKeys.forEach(key => { const keyRequired = isPathRequired(key, schema); const val = get(formData, key); - if (!val && keyRequired) { - touchVerifyList.push({ name: key, error: ['${title}必填'] }); + if (!val && keyRequired.required) { + const _message = + keyRequired.message || validateMessages.required || '${title}必填'; + touchVerifyList.push({ name: key, error: [_message] }); } }); } @@ -49,7 +53,8 @@ export const validateAll = ({ } catch (error) { return Promise.resolve([]); } - const messageFeed = locale === 'en' ? en : cn; + let messageFeed = locale === 'en' ? en : cn; + merge(messageFeed, validateMessages); validator.messages(messageFeed); return validator .validate(formData || {}) diff --git a/packages/form-render/src/widgets/antd/date.js b/packages/form-render/src/widgets/antd/date.js index e161d21d3..8e130cf99 100644 --- a/packages/form-render/src/widgets/antd/date.js +++ b/packages/form-render/src/widgets/antd/date.js @@ -1,18 +1,13 @@ import React from 'react'; import moment from 'moment'; -import { DatePicker, TimePicker } from 'antd'; +import { DatePicker } from 'antd'; import { getFormat } from '../../utils'; // TODO: 不要使用moment,使用dayjs -export default p => { - let { format = 'dateTime' } = p.schema; - if (p.options && p.options.format) { - format = p.options.format; - } - const DateComponent = format === 'time' ? TimePicker : DatePicker; +export default ({ onChange, format, value, style, ...rest }) => { const dateFormat = getFormat(format); // week的时候会返回 2020-31周 quarter会返回 2020-Q2 需要处理之后才能被 moment - let _value = p.value || ''; + let _value = value || undefined; if (typeof _value === 'string') { if (format === 'week') { _value = _value.substring(0, _value.length - 1); @@ -25,17 +20,14 @@ export default p => { _value = moment(_value, dateFormat); } - const placeholderObj = p.description ? { placeholder: p.description } : {}; - - const onChange = (value, string) => p.onChange(string); + const handleChange = (value, string) => { + onChange(string); + }; - const dateParams = { - ...placeholderObj, - ...p.options, + let dateParams = { value: _value, - style: { width: '100%' }, - disabled: p.disabled || p.readOnly, - onChange, + style: { width: '100%', ...style }, + onChange: handleChange, }; // TODO: format是在options里自定义的情况,是否要判断一下要不要showTime @@ -47,5 +39,7 @@ export default p => { dateParams.picker = format; } - return ; + dateParams = { ...dateParams, ...rest }; + + return ; }; diff --git a/packages/form-render/src/widgets/antd/dateRange.js b/packages/form-render/src/widgets/antd/dateRange.js index f4891327a..7e78e8877 100644 --- a/packages/form-render/src/widgets/antd/dateRange.js +++ b/packages/form-render/src/widgets/antd/dateRange.js @@ -3,37 +3,14 @@ * 日历多选组件 */ import React from 'react'; -import { DatePicker, TimePicker } from 'antd'; +import { DatePicker } from 'antd'; import moment from 'moment'; import { getFormat } from '../../utils'; +const { RangePicker } = DatePicker; -const { RangePicker: DateRange } = DatePicker; -const { RangePicker: TimeRange } = TimePicker; - -export default function dateRange(p) { - const { format = 'dateTime' } = p && p.schema; - const onChange = (value, string) => p.onChange(string); - const RangeComponent = format === 'time' ? TimeRange : DateRange; - const hocProps = { ...p, onChange, RangeComponent }; - return ; -} - -const RangeHoc = ({ - onChange, - RangeComponent, - value, - schema = {}, - options, - disabled, - readOnly, -}) => { - let { format = 'dateTime' } = schema; - if (options && options.format) { - format = options.format; - } +const DateRange = ({ onChange, format, value, style, ...rest }) => { const dateFormat = getFormat(format); let [start, end] = Array.isArray(value) ? value : []; - // week的时候会返回 2020-31周 quarter会返回 2020-Q2 需要处理之后才能被 moment if (typeof start === 'string' && typeof end === 'string') { if (format === 'week') { @@ -49,18 +26,28 @@ const RangeHoc = ({ const _value = start && end ? [moment(start, dateFormat), moment(end, dateFormat)] : []; - const dateParams = { - ...options, + const handleChange = (value, stringList) => { + onChange(stringList); + }; + + let dateParams = { value: _value, - style: { width: '100%' }, - showTime: format === 'dateTime', - disabled: disabled || readOnly, - onChange, + style: { width: '100%', ...style }, + onChange: handleChange, }; + // TODO: format是在options里自定义的情况,是否要判断一下要不要showTime + if (format === 'dateTime') { + dateParams.showTime = true; + } + if (['week', 'month', 'quarter', 'year'].indexOf(format) > -1) { dateParams.picker = format; } - return ; + dateParams = { ...dateParams, ...rest }; + + return ; }; + +export default DateRange; diff --git a/packages/form-render/src/widgets/antd/index.js b/packages/form-render/src/widgets/antd/index.js index cc4fc5143..1fa510e88 100644 --- a/packages/form-render/src/widgets/antd/index.js +++ b/packages/form-render/src/widgets/antd/index.js @@ -1,7 +1,9 @@ import checkboxes from './checkboxes'; import color from './color'; import date from './date'; +import time from './time'; import dateRange from './dateRange'; +import timeRange from './timeRange'; import list from './list'; import map from './map'; import multiSelect from './multiSelect'; @@ -37,6 +39,11 @@ const FrTextArea = createWidget(({ autoSize }) => ({ autoSize: autoSize ? autoSize : { minRows: 3 }, }))(TextArea); +// TODO: 这个如果 size small可能会有问题 +const FrCheckbox = ({ style, ...rest }) => ( + +); + const FrTreeSelect = ({ style, ...rest }) => ( ); @@ -47,11 +54,13 @@ const FrCascader = ({ style, ...rest }) => ( export const widgets = { input: Input, - checkbox: Checkbox, + checkbox: FrCheckbox, checkboxes, // checkbox多选 color, date, + time, dateRange, + timeRange, imageInput: ImageInput, url: urlInput, list, diff --git a/packages/form-render/src/widgets/antd/multiSelect.js b/packages/form-render/src/widgets/antd/multiSelect.js index 799e5c4fe..a2aa8aecb 100644 --- a/packages/form-render/src/widgets/antd/multiSelect.js +++ b/packages/form-render/src/widgets/antd/multiSelect.js @@ -1,9 +1,8 @@ import React from 'react'; import { Select } from 'antd'; -import { createWidget } from '../../createWidget'; import { getArray } from '../../utils'; -const mapProps = ({ schema, style, options: _options }) => { +const MultiSelect = ({ schema, value, style, options: _options, ...rest }) => { let options; // 如果已经有外部注入的options了,内部的schema就会被忽略 if (_options && Array.isArray(_options)) { @@ -20,13 +19,16 @@ const mapProps = ({ schema, style, options: _options }) => { }); } - return { + const selectProps = { options, mode: 'multiple', style: { width: '100%', ...style }, + ...rest, }; -}; -const Component = createWidget(mapProps)(Select); + const _value = Array.isArray(value) ? value : undefined; + + return