├── .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 |
handleFormSubmit(e)}> 646 | {handleFormRender(fields)} 647 | 648 | {isShowButton && } 649 | {isShowButton && renderOfArrayType.length>0 && } 650 | 651 | 652 |
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 | --------------------------------------------------------------------------------