├── .gitignore
├── README.md
├── config-overrides.js
├── package-lock.json
├── package.json
├── public
├── index.html
└── manifest.json
├── src
├── AsyncForm.interface.ts
├── AsyncForm.less
├── AsyncForm.test.tsx
├── AsyncForm.tsx
├── components
│ └── TabsNav
│ │ ├── index.interface.ts
│ │ ├── index.less
│ │ └── index.tsx
├── index.css
├── index.tsx
├── react-app-env.d.ts
├── serviceWorker.ts
└── utils
│ ├── renderAntd.tsx
│ └── subscribe.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /build
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #### API
2 |
3 | 1. formSchema: object
4 | ```js
5 | {
6 | "title": "normal",
7 | "description": "desc",
8 | "fields": [
9 | {
10 | "field": "username",
11 | "name": "用户名",
12 | },
13 | {
14 | "field": "password",
15 | "name": "密码",
16 | },
17 | ]
18 | }
19 | ```
20 |
21 | 2. formDate: object
22 | ```js
23 | {
24 | "username": "ivliu",
25 | "password": "123456"
26 | }
27 | ```
28 |
29 | 3. callback: (val: any) => void
30 | 回调函数,其中val参数是要提交的值
31 |
32 | 4. submitTxt: string
33 | 提交按钮的文案
34 |
35 | #### 说明
36 |
37 | 下面介绍每个字段的意义以及取值。
38 |
39 | 1. title (string) 标题
40 | 2. description (string) 描述
41 | 3. required (array) 必填字段枚举,数组项可以是字符串或者数组
42 | 1. 字符串:必须字段的field值
43 | 2. *数组: 第一项是必需字段的field值,第二项是自定义警告信息(参照antd),更多项将被忽略
44 | 4. fields (array) 表单字段枚举 #field配置介绍#
45 | 5. *modal (0 | 1) 是否需要展示在modal中,还需研究
46 |
47 |
48 |
49 | ##### field配置介绍
50 |
51 | 1. field (string) 字段名 **非空**
52 | 2. name (string) 标签名称
53 | 3. widget (string) 组件名称 **默认Input**
54 | 4. type (string) 类型 **[string, array, number, boolean, object]**
55 | 5. enum (array) 枚举值,一般配合Select,Radio或者CheckBox使用,数组项是数组
56 | 1. 数组的第一项是对应的提交值,第二项是相应的UI文案
57 | 6. tips (string) 自定义文案提示,多用于palaceholder
58 | 7. tabs: (array) 支持tab切换设置数组类型的field, type是array时应用
59 | 8. by (string) 如果有依赖,定义依赖的field
60 | 9. ref (string | array) 自定义形状,一般用于数组项的自定义,数组项可以是字符串或者数组
61 | 1. 字符串:对应的ref值
62 | 2. 数组:用于有前后依赖时
63 | 10. defaultValue (string | array | number | boolean | object) 默认值
64 | 11. txt (string) 目前作用还未想好,目前是为了兼容CheckBox和Radio不包含enum的时候
65 | 12. *api (string) 针对有网络请求的Select,Radio或者其他组件的情况
66 | 13. *optFields (array) 命名还需改正,对应接口中的字段
67 | 14. *thirdWidget (string) 第三方组件支持
68 |
69 |
70 | * 表示有待调整
71 |
72 | #### Demo(待完善)
73 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { override, addLessLoader, fixBabelImports, disableChunk } = require('customize-cra');
2 |
3 | module.exports = override(
4 | disableChunk(),
5 | addLessLoader({
6 | strictMath: true,
7 | noIeCompat: true,
8 | localIdentName: '[local]--[hash:base64:5]',
9 | }),
10 | fixBabelImports('import', {
11 | libraryName: 'antd',
12 | libraryDirectory: 'es',
13 | style: 'css',
14 | }),
15 | );
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ivliu/async-form-antd",
3 | "version": "0.0.9",
4 | "main": "src/AsyncForm.tsx",
5 | "author": {
6 | "name": "IVLIU"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git@github.com:IVLIU/async-form-antd.git"
11 | },
12 | "description": "通过自定义的formSchema结构,自动生成表单",
13 | "dependencies": {
14 | "@types/jest": "24.0.11",
15 | "@types/lodash": "^4.14.123",
16 | "@types/node": "11.13.0",
17 | "@types/react": "16.8.10",
18 | "@types/react-dom": "16.8.3",
19 | "antd": "^3.16.1",
20 | "babel-plugin-import": "^1.11.0",
21 | "classnames": "^2.2.6",
22 | "customize-cra": "^0.2.12",
23 | "lodash": "^4.17.11",
24 | "react": "^16.8.6",
25 | "react-app-rewired": "^2.1.1",
26 | "react-dom": "^16.8.6",
27 | "react-scripts": "2.1.8",
28 | "typescript": "3.4.1"
29 | },
30 | "scripts": {
31 | "start": "react-app-rewired start",
32 | "build": "react-app-rewired build",
33 | "test": "react-app-rewired test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": "react-app"
38 | },
39 | "browserslist": [
40 | ">0.2%",
41 | "not dead",
42 | "not ie <= 11",
43 | "not op_mini all"
44 | ],
45 | "devDependencies": {
46 | "@types/classnames": "^2.2.7",
47 | "less": "^3.9.0",
48 | "less-loader": "^4.1.0"
49 | },
50 | "license": "MIT"
51 | }
52 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/AsyncForm.interface.ts:
--------------------------------------------------------------------------------
1 | import { FormComponentProps } from 'antd/lib/form';
2 |
3 | export interface IFormSchema {
4 | title: string;
5 | description?: string;
6 | required?: Array;
7 | fields: Array;
8 | // 针对ref做的hack
9 | [key: string]: any;
10 | }
11 |
12 | export interface IField {
13 | field: string;
14 | name?: string;
15 | defaultValue?: any;
16 | enum?: Array<[any, string]>;
17 | type?: 'string' | 'boolean' | 'array' | 'object';
18 | widget?: string;
19 | len?: number;
20 | min?: number;
21 | max?: number;
22 | tips?: string;
23 | tabs?: Array<{title: string, key: number}>;
24 | by?: string;
25 | ref?: string | {[key: string]: string};
26 | [key: string]: any;
27 | }
28 |
29 | export interface IFormItemOption {
30 | initialValue?: string | number | boolean;
31 | rules: Array
32 | }
33 |
34 | export interface IProps extends FormComponentProps {
35 | formSchema: IFormSchema;
36 | formData?: any;
37 | submitTxt?: string;
38 | callback: (data: any) => void;
39 | callbackOfEdit?: () => void;
40 | [key: string]: any;
41 | }
--------------------------------------------------------------------------------
/src/AsyncForm.less:
--------------------------------------------------------------------------------
1 | .af-wrapper, .af-empty__wrapper {
2 | padding: 10px;
3 | background-color: #fff;
4 | }
5 | .af-empty__wrapper {
6 | font-size: 36px;
7 | color: #999;
8 | text-align: center;
9 | }
10 | .af-array__item {
11 | position: relative;
12 | width: 400px;
13 | padding-bottom: 20px;
14 | border-bottom: 1px solid #ddd;
15 | }
16 | .af-array__itemShow{
17 | position: relative;
18 | width: 400px;
19 | padding-bottom: 20px;
20 | height: 80px;
21 | border-bottom: 1px solid #ddd;
22 | }
23 | .af-button {
24 | margin-right: 20px;
25 | }
26 | .af-operation {
27 | position: absolute;
28 | right: 5px;
29 | bottom: 5px;
30 | }
31 | .af-array__item:hover .af-operation {
32 | display: block;
33 | }
34 | .af-empty__wrapper p {
35 | font-size: 14px;
36 | color: #666;
37 | }
38 | .af-empty {
39 | font-size: 14px;
40 | color: #999;
41 | }
42 |
--------------------------------------------------------------------------------
/src/AsyncForm.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import AsyncForm from './AsyncForm';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/AsyncForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, FormEvent, MouseEvent, useState, useRef, useEffect } from 'react';
2 | import { Form, Button, Icon, Tabs, Input, Divider, Modal , message} from 'antd';
3 | import _ from 'lodash';
4 | import renderAntd from './utils/renderAntd';
5 | import ev from './utils/subscribe';
6 | import { IProps, IField, IFormItemOption } from './AsyncForm.interface';
7 | import './AsyncForm.less';
8 |
9 | const { create, Item: FormItem } = Form;
10 | const { TabPane } = Tabs;
11 | // 折叠点击判断
12 | let collapseBoolean = false;
13 |
14 | const AsyncForm: FC = (props) => {
15 | // props
16 | const { formSchema, formData, form, callback, callbackOfEdit, submitTxt } = props;
17 | const { fields } = formSchema;
18 | const { getFieldDecorator, validateFields, getFieldValue } = form;
19 | // state
20 | const [ isShowItem, setIsShowItem] = useState("block")
21 | const [ isShowButton, setIsShowButton ] = useState(false);
22 | const [ isEdit, setIsEdit ] = useState(false);
23 | const [ isTabEdit, setIsTabEdit ] = useState(false);
24 | const [ isHaveFormData, setIsHaveFormData ] = useState(false);
25 | const [ renderOfArrayType, setRenderOfArrayType ] = useState([]);
26 | const [ formatOfArrayType, setFormatOfArrayType ] = useState([]);
27 | const [ tabsActiveKey, setTabsActiveKey ] = useState(0);
28 | const [ tabsFromFormSchema, setTabsFromFormSchema ] = useState>([]);
29 | // ref
30 | const formatRef = useRef<((currentKey: string) => void) | null>(null);
31 | const byRef = useRef('');
32 | const currentTabKeyRef = useRef(0);
33 | const inputRef = useRef(null);
34 | const afWrapperRef = useRef(null);
35 | const renderOfArrayItemKeyRef = useRef([]);
36 | const isEditCallbackRef = useRef<() => void>(() => {
37 | if(!isEdit) {
38 | setIsEdit(true);
39 | }
40 | })
41 | /** NOTE: 维持当前tabs的最大key值 */
42 | const maxKeyRef = useRef(0);
43 | // effects
44 | useEffect(() => {
45 | const handleEnterDown = (e: KeyboardEvent) => {
46 | if(document.activeElement!.tagName==='INPUT') {
47 | if(afWrapperRef.current!.contains(document.activeElement!)) {
48 | isEditCallbackRef.current();
49 | }
50 | }
51 | if(e.keyCode===13) {
52 | e.preventDefault();
53 | }
54 | };
55 | document.addEventListener('keydown',handleEnterDown);
56 | ev.once('edit', () => {
57 | isEditCallbackRef.current();
58 | });
59 | return () => {
60 | document.removeEventListener('keydown', handleEnterDown)
61 | }
62 | }, [])
63 | useEffect(() => {
64 | if(isEdit) {
65 | if(callbackOfEdit) {
66 | callbackOfEdit();
67 | }
68 | }
69 | }, [isEdit]);
70 | useEffect(() => {
71 | if(Object.keys(formData).length>0) {
72 | if(formatOfArrayType.length>0) {
73 | if(formatOfArrayType.length===1) {
74 | const currentArrayTypeData = formData[formatOfArrayType[0]];
75 | const currentTabsData = formData.tabs;
76 | if(Array.isArray(currentArrayTypeData)) {
77 | const renderOfArrayTypeFromFormData = currentArrayTypeData.reduce((prev, current) => {
78 | let currentTabArrayTypeData = [];
79 | if(Array.isArray(current)) {
80 | currentTabArrayTypeData = current.reduce((prev, _, idx) => {
81 | return [...prev, { idx }];
82 | }, []);
83 | }
84 | return [...prev, currentTabArrayTypeData];
85 | }, []);
86 | renderOfArrayItemKeyRef.current = currentArrayTypeData.reduce((prev, current) => {
87 | if(current && Array.isArray(current)) {
88 | return [...prev, current.length];
89 | }
90 | return [...prev, 0];
91 | }, []);
92 | setRenderOfArrayType(renderOfArrayTypeFromFormData);
93 | }
94 | if(currentTabsData) {
95 | setTabsFromFormSchema(currentTabsData);
96 | maxKeyRef.current = currentTabsData.length;
97 | }
98 | }
99 | }
100 | setIsHaveFormData(true);
101 | }
102 | }, [formatOfArrayType])
103 | // render empty fields
104 | if(!fields || fields.length===0) {
105 | return null;
106 | }
107 | // antd
108 | const formItemLayout = {
109 | labelCol: { span: 4 },
110 | // wrapperCol: { span: 8 },
111 | };
112 | const formTailLayout = {
113 | wrapperCol: { offset: 4 },
114 | };
115 | // 配置消息提示信息
116 | message.config({
117 | top: 100,
118 | duration: 2,
119 | maxCount: 1,
120 | });
121 | // function definition
122 | const handleTabStatusEdit: () => void = () => {
123 | const tabsFromFormSchemaClone = _.cloneDeep(tabsFromFormSchema);
124 | if(inputRef.current) {
125 | tabsFromFormSchemaClone[currentTabKeyRef.current].title = inputRef.current.state.value;
126 | setTabsFromFormSchema(tabsFromFormSchemaClone);
127 | }
128 | setIsTabEdit(!isTabEdit);
129 | isEditCallbackRef.current();
130 | }
131 | const handlerAddTabPane:() => void = () => {
132 | const tabsFromFormSchemaClone = _.cloneDeep(tabsFromFormSchema);
133 | tabsFromFormSchemaClone.push({
134 | title:"新菜单",
135 | key:maxKeyRef.current
136 | });
137 | maxKeyRef.current = maxKeyRef.current+1;
138 | setTabsFromFormSchema(tabsFromFormSchemaClone);
139 | isEditCallbackRef.current();
140 | }
141 | const swapArray:(arr:any, index1:number, index2:number) => any[] = (arr,index1,index2) => {
142 | arr[index1] = arr.splice(index2, 1, arr[index1])[0];
143 | return arr;
144 | }
145 | const handlerLeftMoveTabPane:() => void = () => {
146 | const tabsFromFormSchemaClone = _.cloneDeep(tabsFromFormSchema);
147 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
148 | if(+tabsActiveKey - 1<0) {
149 | message.info('已经到底了')
150 | return
151 | }
152 | const newTabsFromFormSchemaClone = swapArray(tabsFromFormSchemaClone,+tabsActiveKey,+tabsActiveKey - 1);
153 | const newRenderOfArrayTypeClone = swapArray(renderOfArrayTypeClone,+tabsActiveKey,+tabsActiveKey - 1);
154 | setTabsFromFormSchema(newTabsFromFormSchemaClone);
155 | setRenderOfArrayType(newRenderOfArrayTypeClone);
156 | setTabsActiveKey(+tabsActiveKey - 1);
157 | isEditCallbackRef.current();
158 | }
159 | const handlerRightMoveTabPane:() => void = () => {
160 | const tabsFromFormSchemaClone = _.cloneDeep(tabsFromFormSchema);
161 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
162 | if(+tabsActiveKey + 1>tabsFromFormSchemaClone.length - 1){
163 | message.info('已经到底了')
164 | return
165 | }
166 | const newTabsFromFormSchemaClone = swapArray(tabsFromFormSchemaClone,+tabsActiveKey,+tabsActiveKey + 1);
167 | const newRenderOfArrayTypeClone = swapArray(renderOfArrayTypeClone,+tabsActiveKey,+tabsActiveKey + 1);
168 | setTabsFromFormSchema(newTabsFromFormSchemaClone);
169 | setRenderOfArrayType(newRenderOfArrayTypeClone);
170 | setTabsActiveKey(+tabsActiveKey + 1);
171 | isEditCallbackRef.current();
172 | }
173 | const handleTabEdit: (targetKey: string | MouseEvent, action: any) => void = (targetKey, action) => {
174 | const tabsFromFormSchemaClone = _.cloneDeep(tabsFromFormSchema);
175 | if(tabsFromFormSchemaClone.length>2 && action==="remove") {
176 | Modal.confirm({
177 | title: '删除确认',
178 | content: '是否删除该项?',
179 | okText: "确定",
180 | cancelText: '取消',
181 | onOk: () => {
182 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
183 | const leftRenderOfArrayType = renderOfArrayTypeClone.slice(0, +targetKey);
184 | const rightRenderOfArrayType = renderOfArrayTypeClone.slice(+targetKey+1);
185 | const leftTabs = tabsFromFormSchemaClone.slice(0, +targetKey);
186 | const rightTabs = tabsFromFormSchemaClone.slice(+targetKey+1);
187 | setTabsFromFormSchema([...leftTabs, ...rightTabs]);
188 | setRenderOfArrayType([...leftRenderOfArrayType, ...rightRenderOfArrayType]);
189 | isEditCallbackRef.current();
190 | }
191 | });
192 | }
193 | }
194 | const handleFormSubmit: (e: FormEvent) => void = (e) => {
195 | e.preventDefault();
196 | validateFields(async (err, val) => {
197 | if(err) {
198 | return
199 | }
200 | if(renderOfArrayType.length===0 || formatOfArrayType.length===0) {
201 | await callback(val);
202 | return;
203 | }
204 | if(!renderOfArrayType.every((rItem) => rItem.length>0)) {
205 | message.warn('菜单栏的数据不能为空');
206 | return;
207 | }
208 | if(!formatRef.current) {
209 | formatRef.current = (cKey) => {
210 | const reg: RegExp = new RegExp(`^${cKey}_.+`);
211 | const keys: string[] = Object.keys(val);
212 | let keysExceptCurrentKey: [string, string][][] = [];
213 | if(!val[cKey]) {
214 | val[cKey] = [];
215 | }
216 | keys.forEach((k) => {
217 | if(reg.test(k)) {
218 | const fieldSet = k.split('_');
219 | const subKey: string = fieldSet[1];
220 | const subIdx: string = fieldSet[2];
221 | if(!keysExceptCurrentKey[+subIdx]) {
222 | keysExceptCurrentKey[+subIdx] = [];
223 | }
224 | if(subKey) {
225 | keysExceptCurrentKey[+subIdx].push([k, subKey]);
226 | }
227 | }
228 | })
229 | keysExceptCurrentKey = keysExceptCurrentKey.filter(Boolean);
230 | renderOfArrayType.forEach((cRenderOfArrayType, idx) => {
231 | let idxOfkey = idx;
232 | if(tabsFromFormSchema.length>0) {
233 | idxOfkey = tabsFromFormSchema[idx].key;
234 | }
235 | cRenderOfArrayType.forEach((cAType, cAIdx) => {
236 | let { idx: subIdx } = cAType;
237 | if(!val[cKey][idx]) {
238 | val[cKey][idx] = [];
239 | }
240 | val[cKey][idx][subIdx] = keysExceptCurrentKey[idxOfkey].reduce((prev, current) => {
241 | const [k, sk] = current;
242 | /** NOTE: antd删除数组会将数组项置为empty,过滤以规避该问题 */
243 | const valArrayKeyWithoutEmpty = val[k].filter(Boolean);
244 | return {[sk]: valArrayKeyWithoutEmpty[cAIdx], ...prev};
245 | }, {});
246 | })
247 | if(!val[cKey][idx]) {
248 | val[cKey][idx] = [];
249 | }
250 | val[cKey][idx] = val[cKey][idx].filter(Boolean);
251 | })
252 | keysExceptCurrentKey.forEach((cKeys) => {
253 | cKeys.forEach((k) => {
254 | const temporaryKey = k[0];
255 | if(temporaryKey) {
256 | delete val[temporaryKey];
257 | }
258 | })
259 | })
260 | }
261 | }
262 | if(formatOfArrayType.length===1) {
263 | formatRef.current(formatOfArrayType[0]);
264 | if(tabsFromFormSchema.length>1) {
265 | /** NOTE: 可以消除tabKey移位后设置initialValue和后续移动错乱的问题 */
266 | val.tabs = tabsFromFormSchema.reduce((prev, current, idx) => {
267 | return [...prev, {...current, key: idx}];
268 | }, [] as Array<{title: string, key: number}>)
269 | }
270 | await callback(val);
271 | formatRef.current = null;
272 | return;
273 | }
274 | formatOfArrayType.forEach((currentKey) => {
275 | /** todo: 需要多个数据处理的问题 */
276 | if(formatRef.current) {
277 | formatRef.current(currentKey);
278 | }
279 | })
280 | if(tabsFromFormSchema.length>1) {
281 | val.tabs = tabsFromFormSchema;
282 | }
283 | await callback(val);
284 | formatRef.current = null;
285 | setIsEdit(false);
286 | })
287 | };
288 | const handleCurrentTabKeyChange: (currentKey: string) => void = (cKey) => {
289 | setTabsActiveKey(+cKey)
290 | currentTabKeyRef.current = +cKey;
291 | }
292 | const handleCollapseTtem:() => void = () => {
293 | collapseBoolean = !collapseBoolean;
294 | collapseBoolean ? setIsShowItem("none") : setIsShowItem("block");
295 | }
296 | const handleArrayItemAdd: () => void = () => {
297 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
298 | const currentTabKey = currentTabKeyRef.current;
299 | collapseBoolean = false;
300 | if(!renderOfArrayTypeClone[currentTabKey]) {
301 | renderOfArrayTypeClone[currentTabKey] = [];
302 | }
303 | if(!renderOfArrayItemKeyRef.current[currentTabKey]) {
304 | renderOfArrayItemKeyRef.current[currentTabKey] = 0;
305 | }
306 | const initialArrayObj = { idx: renderOfArrayItemKeyRef.current[currentTabKey]};
307 | renderOfArrayItemKeyRef.current[currentTabKey] = renderOfArrayItemKeyRef.current[currentTabKey]+1;
308 | renderOfArrayTypeClone[currentTabKey].push(initialArrayObj);
309 | setRenderOfArrayType(renderOfArrayTypeClone);
310 | setIsShowItem("block");
311 | isEditCallbackRef.current();
312 | }
313 | const handleArrayItemUp:(upIdx: number) => void = (uIdx) => {
314 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
315 | const currentTabrenderOfArrayType = renderOfArrayTypeClone[currentTabKeyRef.current];
316 | const temporaryArrayTypeItem = currentTabrenderOfArrayType[uIdx];
317 | currentTabrenderOfArrayType[uIdx] = currentTabrenderOfArrayType[uIdx-1];
318 | currentTabrenderOfArrayType[uIdx-1] = temporaryArrayTypeItem;
319 | renderOfArrayTypeClone[currentTabKeyRef.current] = currentTabrenderOfArrayType;
320 | setRenderOfArrayType(renderOfArrayTypeClone);
321 | isEditCallbackRef.current();
322 | message.success("上移成功")
323 | }
324 | const handleArrayItemDowm:(downIdx: number) => void = (dIdx) => {
325 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
326 | const currentTabrenderOfArrayType = renderOfArrayTypeClone[currentTabKeyRef.current];
327 | const temporaryArrayTypeItem = currentTabrenderOfArrayType[dIdx];
328 | currentTabrenderOfArrayType[dIdx] = currentTabrenderOfArrayType[dIdx+1];
329 | currentTabrenderOfArrayType[dIdx+1] = temporaryArrayTypeItem;
330 | renderOfArrayTypeClone[currentTabKeyRef.current] = currentTabrenderOfArrayType;
331 | setRenderOfArrayType(renderOfArrayTypeClone);
332 | isEditCallbackRef.current();
333 | message.success("下移成功")
334 | }
335 | const handleArrayItemDelete : (deleteIdx: number) => void = (dIdx) => {
336 | Modal.confirm({
337 | title: '删除确认',
338 | content: '是否删除该项?',
339 | okText: "确定",
340 | cancelText: '取消',
341 | onOk: () => {
342 | const renderOfArrayTypeClone = _.cloneDeep(renderOfArrayType);
343 | const currentTabKey = currentTabKeyRef.current;
344 | if(renderOfArrayType[currentTabKey].length===1) {
345 | renderOfArrayTypeClone[currentTabKey] = [];
346 | setRenderOfArrayType(renderOfArrayTypeClone);
347 | return;
348 | }
349 | const left = renderOfArrayTypeClone[currentTabKey].slice(0, dIdx);
350 | const right = renderOfArrayTypeClone[currentTabKey].slice(dIdx+1);
351 | renderOfArrayTypeClone[currentTabKey] = [...left, ...right];
352 | setRenderOfArrayType(renderOfArrayTypeClone);
353 | isEditCallbackRef.current();
354 | message.success("删除成功");
355 | }
356 | })
357 | }
358 | const handleFormItemOptionFormat: (field: IField, currentFormItemField: string, tabKey?: number, arrayIndex?: number, type?: string) => IFormItemOption = (f, cField, tKey, aIdx, type) => {
359 | const { field, name, defaultValue } = f;
360 | const { required } = formSchema;
361 | let initialValue: undefined | string = undefined;
362 | const baseOpt: IFormItemOption = {
363 | rules: [],
364 | };
365 | if(defaultValue) {
366 | initialValue = defaultValue;
367 | }
368 | if((formData[cField] || formData[cField]===0) && isHaveFormData) {
369 | initialValue = formData[cField];
370 | }
371 | if(type && type==="array" && isHaveFormData) {
372 | if(formatOfArrayType.length>0) {
373 | const currentTabFormData = formData[formatOfArrayType[0]] && formData[formatOfArrayType[0]][tKey as number];
374 | if(currentTabFormData) {
375 | const currentFromData = currentTabFormData[aIdx as number];
376 | if(currentFromData) {
377 | initialValue = currentFromData[field];
378 | }
379 | }
380 | }
381 | }
382 | baseOpt.initialValue = initialValue;
383 | if(required && required.indexOf(field)!==-1) {
384 | baseOpt.rules.push({
385 | required: true,
386 | message: `${name || '该字段'}不能为空`,
387 | });
388 | }
389 | return baseOpt;
390 | }
391 | /** NOTE: 因为样式问题暂未应用 */
392 | const handleArrayItemRender: (
393 | filedParam: IField,
394 | keyOfTab?: number,
395 | indexOfTab?: number,
396 | ) => JSX.Element = (f, kOfTab=0, idxOfTab=0) => {
397 | if(!renderOfArrayType[idxOfTab]) {
398 | renderOfArrayType[idxOfTab] = [];
399 | }
400 | if(renderOfArrayType[idxOfTab].length===0) {
401 | return 暂无数据
;
402 | }
403 | return (
404 | <>
405 | {renderOfArrayType[idxOfTab].map((aItem, idxOfRenderArrayType) => {
406 | const { field, by, ref: $ref } = f;
407 | let byVal: string = '';
408 | let $refVal: string = '';
409 | if(typeof $ref==="string") {
410 | $refVal = $ref;
411 | }
412 | if(typeof by==="string") {
413 | byVal = getFieldValue(by);
414 | if(typeof $ref==="object") {
415 | $refVal = $ref[byVal];
416 | byRef.current = $refVal;
417 | }
418 | }
419 | return (
420 |
421 |
422 | {getFieldDecorator(field)(
423 | handleFormRender(
424 | formSchema[$refVal],
425 | true,
426 | {tabKey: kOfTab, arrayIdx: aItem.idx, superField: field}
427 | )
428 | )}
429 |
430 | {idxOfRenderArrayType>0 && (
431 | <>
432 |
handleArrayItemUp(idxOfRenderArrayType)} />
433 |
434 | >
435 | )}
436 | {renderOfArrayType[currentTabKeyRef.current] && idxOfRenderArrayType
438 | handleArrayItemDowm(idxOfRenderArrayType)} />
439 |
440 | >
441 | )}
442 | handleArrayItemDelete(idxOfRenderArrayType)} />
443 |
444 |
445 |
446 | )
447 | })}
448 | >
449 | )
450 | }
451 | const handleFormRender: (
452 | fieldsParam: IField[],
453 | arrayType?: boolean,
454 | arrayTypeOption?: {
455 | tabKey?: number,
456 | arrayIdx: number,
457 | superField: string,
458 | }
459 | ) => JSX.Element = (fp, aType=false, atOpt={tabKey: 0, arrayIdx: 0, superField: ''}) => {
460 | return (
461 | <>
462 | {fp.map((f, idx) => {
463 | const {
464 | field, name, type, tabs,
465 | } = f;
466 | let formItemField = field;
467 | let formItemFieldOption = handleFormItemOptionFormat(f, formItemField);
468 | const operations = (
469 |
470 | handlerAddTabPane()} style={{marginLeft:20}}/>
471 | handlerLeftMoveTabPane()} />
472 | handlerRightMoveTabPane()}/>
473 |
474 | );
475 | if(type==="array") {
476 | if(!isShowButton) {
477 | setIsShowButton(true);
478 | }
479 | if(formatOfArrayType.indexOf(field)===-1) {
480 | // 过度设计,当前交互下仅支持一个类型为array的field
481 | setFormatOfArrayType([...formatOfArrayType, field]);
482 | }
483 | if(tabs && tabsFromFormSchema.length===0) {
484 | setTabsFromFormSchema(tabs);
485 | maxKeyRef.current = tabs.length;
486 | }
487 | return (
488 |
489 |
490 | {tabsFromFormSchema.length>0 ? (
491 |
492 | handleCurrentTabKeyChange(key)}
496 | onEdit={handleTabEdit}
497 | tabBarStyle={{width:510}}
498 | tabBarExtraContent={operations}
499 | activeKey={tabsActiveKey + ""}
500 | >
501 | {tabsFromFormSchema.map(({ title, key }, idxOfTab) => {
502 | if(title.length===0) {
503 | return null;
504 | }
505 | let currentTab = {title}
506 | if(currentTabKeyRef.current===idxOfTab && isTabEdit) {
507 | currentTab = (
508 |
515 | )
516 | }
517 | if(!renderOfArrayType[idxOfTab]) {
518 | renderOfArrayType[idxOfTab] = []
519 | }
520 | return (
521 |
522 | {renderOfArrayType[idxOfTab].length>0 ? renderOfArrayType[idxOfTab] && Array.isArray(renderOfArrayType[idxOfTab]) && renderOfArrayType[idxOfTab].map((aItem, idxOfRenderArrayType) => {
523 | const { by, ref: $ref } = f;
524 | let byVal: string = '';
525 | let $refVal: string = '';
526 | if(typeof $ref==="string") {
527 | $refVal = $ref;
528 | }
529 | if(typeof by==="string") {
530 | byVal = getFieldValue(by);
531 | if(typeof $ref==="object") {
532 | $refVal = $ref[byVal];
533 | byRef.current = $refVal;
534 | }
535 | }
536 | return (
537 |
538 | {/* 通过判断isshowItem来改变class名,从而动态修改样式 */}
539 |
540 | {getFieldDecorator(field)(
541 | handleFormRender(
542 | formSchema[$refVal],
543 | true,
544 | /** NOTE: 维持tabIdx的不变性 */
545 | {tabKey: key, arrayIdx: aItem.idx, superField: field}
546 | )
547 | )}
548 |
549 | {idxOfRenderArrayType>0 && (
550 | <>
551 |
handleArrayItemUp(idxOfRenderArrayType)} />
552 |
553 | >
554 | )}
555 | {renderOfArrayType[currentTabKeyRef.current] && idxOfRenderArrayType
557 | handleArrayItemDowm(idxOfRenderArrayType)} />
558 |
559 | >
560 | )}
561 | handleArrayItemDelete(idxOfRenderArrayType)} />
562 |
563 |
564 |
565 | )
566 | }) : (
567 | 暂无数据
568 | )}
569 |
570 | )
571 | })}
572 |
573 |
574 | ) : (
575 | <>
576 | {renderOfArrayType[0] && Array.isArray(renderOfArrayType[0]) && renderOfArrayType[0].map((aItem, idxOfRenderArrayType) => {
577 | const { field, by, ref: $ref } = f;
578 | let byVal: string = '';
579 | let $refVal: string = '';
580 | if(typeof $ref==="string") {
581 | $refVal = $ref;
582 | }
583 | if(typeof by==="string") {
584 | byVal = getFieldValue(by);
585 | if(typeof $ref==="object") {
586 | $refVal = $ref[byVal];
587 | byRef.current = $refVal;
588 | }
589 | }
590 | return (
591 |
592 |
593 | {getFieldDecorator(field)(
594 | handleFormRender(
595 | formSchema[$refVal],
596 | true,
597 | {tabKey: 0, arrayIdx: aItem.idx, superField: field}
598 | )
599 | )}
600 |
601 | {idxOfRenderArrayType>0 && (
602 | <>
603 |
handleArrayItemUp(idxOfRenderArrayType)} />
604 |
605 | >
606 | )}
607 | {renderOfArrayType[currentTabKeyRef.current] && idxOfRenderArrayType
609 | handleArrayItemDowm(idxOfRenderArrayType)} />
610 |
611 | >
612 | )}
613 | handleArrayItemDelete(idxOfRenderArrayType)} />
614 |
615 |
616 |
617 | )
618 | })}
619 | >
620 | )}
621 |
622 |
623 | )
624 | }
625 | if(aType) {
626 | const { tabKey, arrayIdx, superField} = atOpt;
627 | formItemField = `${superField}_${formItemField}_${tabKey}[${arrayIdx}]`;
628 | formItemFieldOption = handleFormItemOptionFormat(f, formItemField, tabKey, arrayIdx, "array");
629 | }
630 | return (
631 |
632 | {getFieldDecorator(formItemField, formItemFieldOption)(
633 | renderAntd(f,isShowItem)
634 | )}
635 |
636 | )
637 | })}
638 | >
639 | )
640 | };
641 | // render mormal fields
642 | return (
643 |
644 | {/** title或者description的展示 */}
645 |
653 |
654 | )
655 | };
656 |
657 | const AsyncFormWrappedByAntdForm = create({})(AsyncForm);
658 |
659 | export default AsyncFormWrappedByAntdForm;
660 |
--------------------------------------------------------------------------------
/src/components/TabsNav/index.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IProps {
2 | tabs: string[];
3 | onKeyChange: (curentKey: number) => void;
4 | currentKeyAndChildren: {
5 | currentKeyFormSuperComponent: number;
6 | currentChildren: JSX.Element[] | null;
7 | };
8 | [key: string]: any;
9 | }
--------------------------------------------------------------------------------
/src/components/TabsNav/index.less:
--------------------------------------------------------------------------------
1 | .tn-wrapper {
2 | display: flex;
3 | height: 32px;
4 | line-height: 32px;
5 | padding-right: 10px;
6 | background-color: #f3f3f3;
7 | color: #999;
8 | .tn-item {
9 | position: relative;
10 | width: 60px;
11 | text-align: center;
12 | color: #666;
13 | .tn-active__bar {
14 | position: absolute;
15 | bottom: 0;
16 | width: 60px;
17 | height: 3px;
18 | background-color: #18a2ff;
19 | }
20 | }
21 | .tn-item--active {
22 | background-color: #f3f5f9;
23 | color: #18a2ff;
24 | }
25 | .tn-operation {
26 | margin-left: auto;
27 | }
28 | }
29 | .tn-main {
30 | padding: 5px;
31 | }
--------------------------------------------------------------------------------
/src/components/TabsNav/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState, useEffect, useRef } from 'react';
2 | import className from 'classnames';
3 | import { Icon, Divider, Tabs } from 'antd';
4 | import { IProps } from './index.interface';
5 | import './index.less';
6 |
7 | const { TabPane } = Tabs;
8 |
9 | const TabsNav: FC = (props) => {
10 | // props
11 | const { tabs, onKeyChange, currentKeyAndChildren } = props;
12 | const { currentKeyFormSuperComponent, currentChildren } = currentKeyAndChildren;
13 | // function definition
14 | const handleCurrentKeyChange: (currentStringKey: string) => void = (cStrKey) => {
15 | onKeyChange(Number(cStrKey));
16 | }
17 | return (
18 | handleCurrentKeyChange(k)}>
19 | {tabs.map((tab, idx) => {
20 | return (
21 |
22 | {currentKeyFormSuperComponent === idx && currentChildren}
23 |
24 | )
25 | })}
26 |
27 | )
28 | };
29 |
30 | export default TabsNav;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import './index.css';
4 | import AsyncForm from './AsyncForm';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | const formSchema = {
8 | "bigPic":[
9 | {
10 | "field":"doctorIds",
11 | "tips":"请在此处添加医生ID"
12 | },
13 | {
14 | "field":"label",
15 | "tips":"请在此处添加标签文案,用逗号隔开。例如xx,xx,xx"
16 | },
17 | {
18 | "field":"consult",
19 | "tips":"请在此处添加咨询文案"
20 | },
21 | {
22 | "field":"comment",
23 | "tips":"请在此处添加好评文案"
24 | },
25 | {
26 | "widget":"TextArea",
27 | "field":"price",
28 | "tips":"请在此处添加义诊价描述"
29 | }
30 | ],
31 | "description":"多列医生的表单结构设计",
32 | "smallPic":[
33 | {
34 | "field":"doctorIds",
35 | "tips":"请在此处添加医生ID"
36 | },
37 | {
38 | "widget":"TextArea",
39 | "field":"price",
40 | "tips":"请在此处添加义诊价描述"
41 | }
42 | ],
43 | "title":"多列医生",
44 | "fields":[
45 | {
46 | "field":"title",
47 | "max":10,
48 | "name":"标题"
49 | },
50 | {
51 | "widget":"Radio",
52 | "field":"style",
53 | "defaultValue":"0",
54 | "name":"模块样式",
55 | "enum":[
56 | [
57 | "1",
58 | "大图"
59 | ],
60 | [
61 | "0",
62 | "小图"
63 | ]
64 | ]
65 | },
66 | {
67 | "widget":"Radio",
68 | "field":"isShowPrice",
69 | "defaultValue":"0",
70 | "name":"显示价格",
71 | "enum":[
72 | [
73 | "1",
74 | "是"
75 | ],
76 | [
77 | "0",
78 | "否"
79 | ]
80 | ]
81 | },
82 | {
83 | "widget":"Select",
84 | "field":"link",
85 | "name":"跳转页面",
86 | "enum":[
87 | [
88 | "1",
89 | "医生主页"
90 | ],
91 | [
92 | "2",
93 | "图文咨询页"
94 | ]
95 | ],
96 | "tips":"请选择跳转到的页面"
97 | },
98 | {
99 | "ref":{
100 | "0":"smallPic",
101 | "1":"bigPic"
102 | },
103 | "field":"doctors",
104 | "by":"style",
105 | // "tabs":[
106 | // {
107 | // "title": "菜单1",
108 | // "key": 0,
109 | // },
110 | // {
111 | // "title": "菜单2",
112 | // "key": 1,
113 | // },
114 | // ],
115 | "type":"array"
116 | }
117 | ],
118 | "required":[
119 | "title",
120 | "link",
121 | "doctorIds",
122 | ]
123 | };
124 | // {"title":"浮层按钮","required":["content"],"fields":[{"field":"content","name":"模块内容"},{"field":"fontSize","name":"字体大小","widget":"Radio","defaultValue":1,"enum":[[0,"大"],[1,"中"],[2,"小"]]},{"field":"link","name":"跳转链接"},{"field":"background","name":"背景颜色"}]}
125 |
126 |
127 | const formData = {
128 | isShowPrice: "0",
129 | link: "1",
130 | style: "0",
131 | title: "123",
132 | }
133 | // {
134 | // "isShowPrice":"1",
135 | // "doctors":[
136 | // [
137 | // {price: "义诊十元", doctorIds: "123"},
138 | // {price: undefined, doctorIds: "qwer"},
139 | // {price: undefined, doctorIds: "asdf"}
140 | // ],
141 | // [{price: "义诊两元", doctorIds: "456"}],
142 | // [{price: "免费义诊", doctorIds: "789"}],
143 | // [{price: undefined, doctorIds: "000"}],
144 | // ],
145 | // "link":"2",
146 | // "tabs":[
147 | // {
148 | // "title": "妇科",
149 | // "key": 0,
150 | // },
151 | // {
152 | // "title": "儿科",
153 | // "key": 1,
154 | // },
155 | // {
156 | // "title": "内科",
157 | // "key": 2,
158 | // },
159 | // {
160 | // "title": "外科",
161 | // "key": 3,
162 | // },
163 | // ],
164 | // "style":"0",
165 | // "title":"主任"
166 | // }
167 |
168 | render(
169 | console.log('callback', val)}
173 | callbackOfEdit={() => console.log('edit status is true.')}
174 | />,
175 | document.querySelector('#root')
176 | );
177 |
178 | // If you want your app to work offline and load faster, you can change
179 | // unregister() to register() below. Note this comes with some pitfalls.
180 | // Learn more about service workers: https://bit.ly/CRA-PWA
181 | serviceWorker.unregister();
182 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/utils/renderAntd.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input, Radio, Select, Checkbox } from 'antd';
3 | import ev from './subscribe';
4 | import { IField } from '../AsyncForm.interface';
5 |
6 | const { Group: RadioGroup } = Radio;
7 | const { Option: SelectOption } = Select;
8 | const { Group: CheckboxGroup } = Checkbox;
9 |
10 | const handleRadioAndSelectChange: () => void = () => {
11 | ev.emit('edit')
12 | }
13 |
14 | export const renderAntd: (field: IField,isShowItem:string) => JSX.Element | null | undefined = (f,isShowItem) => {
15 | const { name, widget, tips, enum: enumLike } = f;
16 | if (!widget) {
17 | if(f.field === "doctorIds" || f.field === "title"){
18 | return ;
19 | }
20 | return ;
21 | }
22 | switch (widget) {
23 | case 'Radio':
24 | if (!enumLike) {
25 | throw new Error('enum is not define.');
26 | }
27 | return (
28 |
29 | {enumLike.map((el: [any, string], idx: number) => {
30 | const [val, txt] = el;
31 | return (
32 |
33 | {txt}
34 |
35 | );
36 | })}
37 |
38 | );
39 | case 'Select':
40 | if (!enumLike) {
41 | throw new Error('enum is not define.');
42 | }
43 | return (
44 |
58 | );
59 | case 'TextArea':
60 | return
61 | case 'Checkbox':
62 | if (!enumLike) {
63 | throw new Error('enum is not define.');
64 | }
65 | const options = enumLike.reduce((prev, current) => {
66 | const [ value, label ] = current;
67 | return [...prev, {label, value}];
68 | }, [] as {label: string, value: string}[]);
69 | return
70 | default:
71 | return
72 | }
73 | };
74 |
75 | export default renderAntd;
76 |
--------------------------------------------------------------------------------
/src/utils/subscribe.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | const ev: EventEmitter = new EventEmitter();
4 |
5 | export default ev;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "preserve",
21 | "downlevelIteration": true,
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------