├── .gitignore ├── .prettierrc ├── README.md ├── config-overrides.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── markdown │ ├── array-p.png │ ├── array.png │ ├── formCard-p.png │ ├── formCard.png │ ├── preview.png │ ├── pureObj-p.png │ └── pureObj.png └── robots.txt ├── src ├── App.css ├── App.tsx ├── components │ ├── core │ │ ├── baseComponent │ │ │ ├── arrayComponent │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── builtInComponent │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── index.tsx │ │ │ └── layoutComponent │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ ├── container │ │ │ ├── ashcan │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── editSchema │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── preview │ │ │ │ └── index.tsx │ │ ├── context │ │ │ └── index.ts │ │ ├── index.tsx │ │ └── propertiesEdit │ │ │ ├── index.tsx │ │ │ └── schema │ │ │ └── index.ts │ ├── customComponent │ │ └── custom-monaco-editor.tsx │ └── draggable │ │ ├── customHooks.ts │ │ ├── draggable.d.ts │ │ ├── index.tsx │ │ ├── nested.scss │ │ └── nested.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts └── utils │ ├── __tests__ │ └── transform.test.ts │ ├── index.ts │ └── transform.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 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 90, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ], 11 | "semi": false, 12 | "useTabs": true, 13 | "bracketSpacing": true, 14 | "insertPragma": true 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Formily 可视化 schema 编辑器 4 | 5 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/preview.png) 6 | 7 | ### 预览 8 | 9 | 可以直接点击[本链接](https://guard-233.github.io/formily-generator/)在线使用 10 | 11 | ### clone 并使用 12 | 13 | ```bash 14 | git clone https://github.com/Guard-233/formily-generator.git 15 | cd ./formily-generator 16 | yarn start 17 | ``` 18 | 19 | **注意,使用本工具之前,最好先了解[Formily](https://formilyjs.org)的使用方式** 20 | 21 | ### 界面说明 22 | 23 | #### 左侧 24 | 25 | 左侧主要是可拖动的组件,分为基础组件,布局组件,数组组件 26 | 27 | 在此主要说明布局组件与数组组件的使用方式 28 | 29 | ##### 布局组件 30 | 31 | 所有的布局组件除了 pureObject 之外均为样式组件,如果想让数据结构与样式同意,则需要给布局组件内部嵌套一层 pureObject。 32 | 33 | **pureObject 存在** 34 | 35 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/pureObj.png) 36 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/pureObj-p.png) 37 | 38 | **pureObject 不存在** 39 | 40 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/formCard.png) 41 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/formCard-p.png) 42 | 43 | ##### 数组组件 44 | 45 | 数组组件目前有两种 ArrayCard 与 ArrayTable 46 | 47 | **注意,因数组的 items 属性是 ISchema | ISchema[] 所以具体的组件需要拖放在内部的对象中,而不是数组本身** 48 | 49 | 因表格组件的拖拽显示不太直观,所以数组组件在拖拽区域如下图所示 50 | 51 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/array.png) 52 | 53 | 而最终预览的形态,则以表格的形式正常显示 54 | 55 | ![preview](https://raw.githubusercontent.com/Guard-233/formily-generator/master/public/markdown/array-p.png) 56 | 57 | #### 中部 58 | 59 | 中部区域为组件可拖放的区域,需要注意的是,中部区域的组件预览效果与最终效果有出入,请以点击预览按钮后的页面为准 60 | 61 | #### 右侧 62 | 63 | 点击中部区域的任意一个以拖放的组件,都可以在右侧进行相应的属性编辑,具体可编辑的属性,可以看[Formily](https://formilyjs.org)的官方文档 64 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | /** 2 | * /* config-overrides.js 3 | * 4 | * @format 5 | */ 6 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin') 7 | 8 | module.exports = function override(config, env) { 9 | //do stuff with the webpack config... 10 | console.log(config) 11 | 12 | const { plugins } = config 13 | 14 | return { 15 | ...config, 16 | plugins: [ 17 | ...plugins, 18 | new MonacoWebpackPlugin({ 19 | languages: ['json', 'javascript'] 20 | }) 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedom", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.2.1", 7 | "@formily/antd": "1.2.7", 8 | "@formily/antd-components": "1.2.7", 9 | "@formily/printer": "^1.2.7", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.3.2", 12 | "@testing-library/user-event": "^7.1.2", 13 | "@types/jest": "^24.0.0", 14 | "@types/node": "^12.0.0", 15 | "@types/react": "^16.9.0", 16 | "@types/react-dom": "^16.9.0", 17 | "antd": "4.0.0", 18 | "lodash": "^4.17.19", 19 | "node-sass": "^4.14.1", 20 | "react": "^16.13.1", 21 | "react-app-rewired": "^2.1.6", 22 | "react-dom": "^16.13.1", 23 | "react-monaco-editor": "^0.39.1", 24 | "react-scripts": "3.4.1", 25 | "react-sortablejs": "^2.0.11", 26 | "styled-components": "^5.1.1", 27 | "typescript": "~3.7.2", 28 | "utility-types": "^3.10.0" 29 | }, 30 | "scripts": { 31 | "predeploy": "npm run build", 32 | "deploy": "gh-pages -d build", 33 | "start": "react-app-rewired start", 34 | "build": "react-app-rewired build", 35 | "test": "react-scripts test --env=jsdom", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@types/lodash": "^4.14.157", 55 | "@types/sortablejs": "^1.10.4", 56 | "gh-pages": "^3.1.0", 57 | "monaco-editor-webpack-plugin": "^1.9.0" 58 | }, 59 | "homepage": "https://guard-233.github.io/formily-generator" 60 | } 61 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/markdown/array-p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/array-p.png -------------------------------------------------------------------------------- /public/markdown/array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/array.png -------------------------------------------------------------------------------- /public/markdown/formCard-p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/formCard-p.png -------------------------------------------------------------------------------- /public/markdown/formCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/formCard.png -------------------------------------------------------------------------------- /public/markdown/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/preview.png -------------------------------------------------------------------------------- /public/markdown/pureObj-p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/pureObj-p.png -------------------------------------------------------------------------------- /public/markdown/pureObj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guard-233/formily-generator/aa2b0a1fbd0d34229f1b7ab80d01de29ef7669de/public/markdown/pureObj.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useEffect } from 'react' 4 | import { Core } from './components/core' 5 | 6 | const App = () => { 7 | useEffect(() => { 8 | console.log('first: '); 9 | 10 | }, []) 11 | 12 | 13 | return 14 | } 15 | 16 | export default App 17 | 18 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/arrayComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { schema } from "./schema"; 3 | import { ReactDraggable } from "../../../draggable/index"; 4 | import { IDraggableList } from "../../../draggable/draggable"; 5 | import { FormilyToDraggable } from "../../../../utils/transform"; 6 | import { GenNonDuplicateID } from "../../../../utils"; 7 | 8 | export const ArrayComponent = () => { 9 | const [list, setList] = useState( 10 | FormilyToDraggable(schema) 11 | ); 12 | 13 | return ( 14 | ({ ...item, id: item.id + "-" + GenNonDuplicateID() })} 24 | > 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/arrayComponent/schema.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ISchema } from '@formily/antd' 4 | export const schema: ISchema = { 5 | type: 'object', 6 | properties: { 7 | arrayTable: { 8 | title: '数组table', 9 | maxItems: 3, 10 | type: 'array', 11 | 'x-component': 'arraytable', 12 | 'x-component-props': { 13 | // operationsWidth: 300, 14 | }, 15 | items: [ 16 | { 17 | type: 'object', 18 | properties: { 19 | // aa: { 20 | // 'x-component': 'input', 21 | // description: 'hello world', 22 | // title: '字段1' 23 | // }, 24 | // bb: { 25 | // 'x-component': 'input', 26 | // title: '字段2' 27 | // } 28 | } 29 | } 30 | // { 31 | // type: 'object', 32 | // properties: { 33 | // cc: { 34 | // 'x-component': 'input', 35 | // description: 'hello world', 36 | // title: '字段3' 37 | // }, 38 | // dd: { 39 | // 'x-component': 'input', 40 | // title: '字段4' 41 | // } 42 | // } 43 | // } 44 | ] 45 | }, 46 | arrayCards: { 47 | title: '数组Cards', 48 | maxItems: 3, 49 | type: 'array', 50 | 'x-component': 'arraycards', 51 | items: [ 52 | { 53 | type: 'object', 54 | properties: { 55 | // aa: { 56 | // 'x-component': 'input', 57 | // description: 'hello world', 58 | // title: '字段1' 59 | // }, 60 | // bb: { 61 | // 'x-component': 'input', 62 | // title: '字段2' 63 | // } 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/builtInComponent/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import { schema } from './schema' 5 | import { ReactDraggable } from '../../../draggable/index' 6 | import { IDraggableList } from '../../../draggable/draggable' 7 | import { FormilyToDraggable } from '../../../../utils/transform' 8 | import { GenNonDuplicateID } from '../../../../utils' 9 | 10 | export const BuiltInComponent = () => { 11 | const [list, setList] = useState(FormilyToDraggable(schema)) 12 | 13 | return ( 14 | ({ ...item, id: item.id + '-' + GenNonDuplicateID() })} 25 | > 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/builtInComponent/schema.ts: -------------------------------------------------------------------------------- 1 | import { ISchema } from "@formily/antd"; 2 | 3 | export const schema: ISchema = { 4 | type: "object", 5 | properties: { 6 | // import { Input } from '@formily/antd-components' 7 | string: { 8 | title: "String", 9 | "x-component": "Input", 10 | "x-component-props": { 11 | placeholder: "input", 12 | }, 13 | type: "string", 14 | }, 15 | // import { Select } from '@formily/antd-components' 16 | select: { 17 | title: "Object Select", 18 | "x-component": "Select", 19 | enum: [ 20 | { label: "One", value: "1" }, 21 | { label: "Two", value: "2" }, 22 | { label: "Three", value: "3" }, 23 | { label: "Four", value: "4" }, 24 | ], 25 | "x-component-props": { 26 | placeholder: "select", 27 | }, 28 | }, 29 | // components={{ 30 | // TextArea: Input.TextArea 31 | // }} 32 | textarea: { 33 | title: "String", 34 | "x-component": "TextArea", 35 | "x-component-props": { 36 | placeholder: "textarea", 37 | }, 38 | }, 39 | // import { Password } from '@formily/antd-components' 40 | Password: { 41 | title: "Password", 42 | "x-component": "Password", 43 | "x-component-props": { 44 | placeholder: "Password", 45 | }, 46 | }, 47 | // import { NumberPicker } from '@formily/antd-components' 48 | NumberPicker: { 49 | title: "NumberPicker", 50 | "x-component": "NumberPicker", 51 | }, 52 | // import { Switch } from '@formily/antd-components' 53 | Switch: { 54 | title: "Switch", 55 | "x-component": "Switch", 56 | }, 57 | // import { DatePicker } from '@formily/antd-components' 58 | DatePicker: { 59 | title: "DatePicker", 60 | "x-component": "DatePicker", 61 | "x-component-props": { 62 | format: "YYYY-MM-DD HH:mm:ss", 63 | }, 64 | }, 65 | // RangePicker: DatePicker.RangePicker 66 | "[start,end]": { 67 | title: "RangePicker", 68 | "x-component": "RangePicker", 69 | }, 70 | // WeekPicker: DatePicker.WeekPicker 71 | WeekPicker: { 72 | title: "WeekPicker", 73 | "x-component": "WeekPicker", 74 | }, 75 | // MonthPicker: DatePicker.MonthPicker 76 | MonthPicker: { 77 | title: "MonthPicker", 78 | "x-component": "MonthPicker", 79 | }, 80 | // YearPicker: DatePicker.YearPicker 81 | YearPicker: { 82 | title: "YearPicker", 83 | "x-component": "YearPicker", 84 | }, 85 | // import { TimePicker } from '@formily/antd-components' 86 | TimePicker: { 87 | title: "TimePicker", 88 | "x-component": "TimePicker", 89 | "x-component-props": { 90 | format: "YYYY-MM-DD HH:mm:ss", 91 | }, 92 | }, 93 | // import { Range } from '@formily/antd-components' 94 | Range: { 95 | title: "Range", 96 | "x-component": "Range", 97 | "x-component-props": { 98 | min: 0, 99 | max: 1024, 100 | marks: [0, 1024], 101 | }, 102 | }, 103 | // import { Upload } from '@formily/antd-components' 104 | upload: { 105 | title: "Card Upload", 106 | "x-component": "Upload", 107 | "x-component-props": { 108 | listType: "card", 109 | }, 110 | }, 111 | // import { Checkbox } from '@formily/antd-components' 112 | checkbox: { 113 | title: "Object Checkbox", 114 | "x-component": "CheckboxGroup", 115 | enum: [ 116 | { label: "One", value: "1" }, 117 | { label: "Two", value: "2" }, 118 | { label: "Three", value: "3" }, 119 | { label: "Four", value: "4" }, 120 | ], 121 | }, 122 | // import { Radio } from '@formily/antd-components' 123 | radio: { 124 | title: "Object Radio", 125 | "x-component": "RadioGroup", 126 | enum: [ 127 | { label: "One", value: "1" }, 128 | { label: "Two", value: "2" }, 129 | { label: "Three", value: "3" }, 130 | { label: "Four", value: "4" }, 131 | ], 132 | }, 133 | // import { Rating } from '@formily/antd-components' 134 | rating: { 135 | title: "Rating", 136 | "x-component": "Rating", 137 | "x-component-props": { 138 | allowHalf: true, 139 | }, 140 | }, 141 | // import { Transfer } from '@formily/antd-components' 142 | transfer: { 143 | title: "Transfer", 144 | "x-component": "Transfer", 145 | enum: [ 146 | { label: "One", value: "1", key: "1" }, 147 | { label: "Two", value: "2", key: "2" }, 148 | { label: "Three", value: "3", key: "3" }, 149 | { label: "Four", value: "4", key: "4" }, 150 | ], 151 | "x-component-props": { 152 | showSearch: true, 153 | render: (record: { label: any }) => record.label, 154 | }, 155 | }, 156 | }, 157 | }; 158 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { BuiltInComponent } from './builtInComponent' 5 | import { ArrayComponent } from './arrayComponent' 6 | import { LayoutComponent } from './layoutComponent' 7 | import { PageHeader } from 'antd' 8 | 9 | export const BaseComponent = () => { 10 | return ( 11 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/layoutComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { schema } from "./schema"; 3 | import { ReactDraggable } from "../../../draggable/index"; 4 | import { IDraggableList } from "../../../draggable/draggable"; 5 | import { FormilyToDraggable } from "../../../../utils/transform"; 6 | import { GenNonDuplicateID } from "../../../../utils"; 7 | 8 | export const LayoutComponent = () => { 9 | const [list, setList] = useState( 10 | FormilyToDraggable(schema) 11 | ); 12 | 13 | return ( 14 | ({ ...item, id: item.id + "-" + GenNonDuplicateID() })} 24 | > 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/core/baseComponent/layoutComponent/schema.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ISchema } from '@formily/antd' 4 | export const schema: ISchema = { 5 | type: 'object', 6 | properties: { 7 | formCards: { 8 | title: 'formcards', 9 | type: 'object', 10 | 'x-component': 'card', 11 | 'x-component-props': { 12 | title: 'formCards' 13 | }, 14 | properties: {} 15 | }, 16 | formblock: { 17 | title: 'formblock', 18 | type: 'object', 19 | 'x-component': 'block', 20 | 'x-component-props': { 21 | title: 'formblock' 22 | }, 23 | properties: {} 24 | }, 25 | tabs: { 26 | type: 'object', 27 | 'x-component': 'tab', 28 | 'x-component-props': { 29 | defaultActiveKey: 'tab-1' 30 | }, 31 | properties: { 32 | 'tab-1': { 33 | type: 'object', 34 | 'x-component': 'tabpane', 35 | 'x-component-props': { 36 | tab: '选项1' 37 | }, 38 | properties: {} 39 | }, 40 | 'tab-2': { 41 | type: 'object', 42 | 'x-component': 'tabpane', 43 | 'x-component-props': { 44 | tab: '选项2' 45 | }, 46 | properties: {} 47 | } 48 | } 49 | }, 50 | FormMegaLayout: { 51 | 'x-component': 'mega-layout', 52 | 'x-component-props': { 53 | labelCol: 4 54 | }, 55 | type: 'object', 56 | properties: {} 57 | }, 58 | pureObject: { 59 | type: 'object', 60 | 'x-component': '', 61 | properties: {} 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/core/container/ashcan/index.scss: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | .deleteActive { 4 | background-color: red($color: #000000); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/core/container/ashcan/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import { DeleteOutlined } from '@ant-design/icons' 5 | import { ReactSortable } from 'react-sortablejs' 6 | import { Avatar, Popover } from 'antd' 7 | import { IDraggableList } from '../../../draggable/draggable' 8 | import './index.scss' 9 | 10 | export const Ashcan = () => { 11 | const [list] = useState([{ id: 'delete' }]) 12 | 13 | return ( 14 | list} 18 | ghostClass="deleteActive" 19 | > 20 | {list.map((item) => { 21 | return ( 22 | 23 | } 32 | /> 33 | 34 | ) 35 | })} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/core/container/editSchema/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { ISchema } from '@formily/antd' 4 | import MonacoEditor from 'react-monaco-editor' 5 | 6 | import { Modal, Button } from 'antd' 7 | import { IDraggableList } from '../../../draggable/draggable' 8 | import { FormilyToDraggable } from '../../../../utils/transform' 9 | import { ActiveItem } from '../../context' 10 | 11 | export const EditSchema = (props: { 12 | schema: ISchema 13 | setSchema: React.Dispatch> 14 | }) => { 15 | const { schema, setSchema } = props 16 | 17 | const [state, setState] = useState({ 18 | visible: false 19 | }) 20 | 21 | const [code, setCode] = useState(JSON.stringify(schema, undefined, 2)) 22 | 23 | const { changeActive } = useContext(ActiveItem) 24 | 25 | useEffect(() => { 26 | setCode(JSON.stringify(schema, undefined, 2)) 27 | }, [schema]) 28 | 29 | const handleOk = (e: any) => { 30 | setSchema(FormilyToDraggable(JSON.parse(code))) 31 | 32 | changeActive({ 33 | list: { 34 | id: 'schema', 35 | properties: [], 36 | type: 'object' 37 | }, 38 | setList: () => {} 39 | }) 40 | 41 | setState({ 42 | visible: false 43 | }) 44 | } 45 | 46 | const handleCancel = (e: any) => { 47 | setState({ 48 | visible: false 49 | }) 50 | } 51 | 52 | return ( 53 |
54 | 64 | 76 | 84 | 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/components/core/container/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState, useEffect } from 'react' 4 | import { ReactDraggable } from '../../draggable' 5 | import { IDraggableList } from '../../draggable/draggable' 6 | import { GenNonDuplicateID } from '../../../utils' 7 | import { Preview } from './preview' 8 | import { EditSchema } from './editSchema' 9 | import { DraggableToFormily } from '../../../utils/transform' 10 | import { Layout, Tag } from 'antd' 11 | import { Ashcan } from './ashcan' 12 | 13 | const { Content, Header } = Layout 14 | 15 | class ErrorBoundary extends React.Component< 16 | any, 17 | { 18 | hasError: boolean 19 | } 20 | > { 21 | constructor(props: Readonly<{}>) { 22 | super(props) 23 | this.state = { hasError: false } 24 | } 25 | 26 | static getDerivedStateFromError() { 27 | // Update state so the next render will show the fallback UI. 28 | return { hasError: true } 29 | } 30 | 31 | componentDidCatch(error: any, info: any) { 32 | // You can also log the error to an error reporting service 33 | // logErrorToMyService(error, info); 34 | } 35 | 36 | render() { 37 | if (this.state.hasError) { 38 | // You can render any custom fallback UI 39 | return

Something went wrong.

40 | } 41 | 42 | return this.props.children 43 | } 44 | } 45 | 46 | export const Container = () => { 47 | const [list, setList] = useState>([ 48 | { 49 | type: 'object', 50 | id: 'schema' 51 | } 52 | ]) 53 | 54 | const tag = 请拖拽至下方框体内部 55 | 56 | useEffect(() => { 57 | if (list.length > 1) { 58 | setList( 59 | list.filter((item) => { 60 | return item.id === 'schema' 61 | }) 62 | ) 63 | } 64 | }, [list]) 65 | 66 | return ( 67 | 74 |
81 | 82 | 83 |
84 | 89 | 90 | {list.length > 1 ? tag : ''} 91 | ({ 100 | ...item, 101 | id: item.id + GenNonDuplicateID() 102 | })} 103 | > 104 | 105 | 106 | 107 |
108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /src/components/core/container/preview/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import { SchemaForm, ISchema, FormButtonGroup, Submit, Reset } from '@formily/antd' 5 | import { Modal, Button } from 'antd' 6 | import { 7 | Input, 8 | Select, 9 | Password, 10 | NumberPicker, 11 | Switch, 12 | DatePicker, 13 | TimePicker, 14 | Range, 15 | Upload, 16 | Checkbox, 17 | Radio, 18 | Rating, 19 | Transfer, 20 | FormCard, 21 | ArrayTable, 22 | ArrayCards 23 | } from '@formily/antd-components' 24 | 25 | const BuiltInComponents = { 26 | Input, 27 | Select, 28 | TextArea: Input.TextArea, 29 | Password, 30 | NumberPicker, 31 | Switch, 32 | DatePicker, 33 | RangePicker: DatePicker.RangePicker, 34 | WeekPicker: DatePicker.WeekPicker, 35 | MonthPicker: DatePicker.MonthPicker, 36 | YearPicker: DatePicker.YearPicker, 37 | TimePicker, 38 | Range, 39 | Upload, 40 | Checkbox, 41 | CheckboxGroup: Checkbox.Group, 42 | Radio, 43 | RadioGroup: Radio.Group, 44 | Rating, 45 | Transfer 46 | } 47 | 48 | const LayoutComponents = { 49 | FormCard 50 | } 51 | 52 | const ArrayComponents = { 53 | ArrayTable, 54 | ArrayCards 55 | } 56 | 57 | export const Preview = (props: ISchema) => { 58 | const [state, setState] = useState({ 59 | visible: false 60 | }) 61 | 62 | const handleOk = (e: any) => { 63 | setState({ 64 | visible: false 65 | }) 66 | } 67 | 68 | const handleCancel = (e: any) => { 69 | setState({ 70 | visible: false 71 | }) 72 | } 73 | 74 | return ( 75 |
76 | 86 | 99 | { 107 | console.log(value) 108 | }} 109 | > 110 | 111 | 提交重置​ 112 | 113 | 114 | 115 |
116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /src/components/core/context/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { createContext } from 'react' 4 | import { IDraggableList } from '../../draggable/draggable.d' 5 | 6 | const active = { 7 | list: { 8 | id: 'schema' 9 | }, 10 | setList: (IDraggableList: IDraggableList) => {} 11 | } 12 | 13 | const changeActive: any = () => { 14 | return {} 15 | } 16 | 17 | export const ActiveItem = createContext<{ 18 | active: { 19 | list: IDraggableList 20 | setList(IDraggableList: IDraggableList): void 21 | } 22 | changeActive: React.Dispatch< 23 | React.SetStateAction<{ 24 | list: IDraggableList 25 | setList(IDraggableList: IDraggableList): void 26 | }> 27 | > 28 | }>({ 29 | active, 30 | changeActive 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/core/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import { BaseComponent } from './baseComponent' 5 | import { Container } from './container' 6 | import { PropertiesEdit } from './propertiesEdit' 7 | import { ActiveItem } from './context' 8 | import { IDraggableList } from '../draggable/draggable.d' 9 | 10 | export const Core = () => { 11 | const [active, setActive] = useState<{ 12 | list: IDraggableList 13 | setList(IDraggableList: IDraggableList): void 14 | }>({ 15 | list: { 16 | id: 'schema', 17 | properties: [], 18 | type: 'object' 19 | }, 20 | setList: (list: IDraggableList) => {} 21 | }) 22 | 23 | return ( 24 |
30 | 36 | 37 | 38 | 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/core/propertiesEdit/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useContext, useState } from 'react' 4 | import { SchemaForm, LifeCycleTypes, FormSpy } from '@formily/antd' 5 | import { 6 | Input, 7 | Select, 8 | Password, 9 | NumberPicker, 10 | Switch, 11 | DatePicker, 12 | TimePicker, 13 | Range, 14 | Upload, 15 | Checkbox, 16 | Radio, 17 | Rating, 18 | Transfer, 19 | FormCard, 20 | ArrayTable, 21 | ArrayCards, 22 | FormMegaLayout 23 | } from '@formily/antd-components' 24 | import { schema } from './schema' 25 | import { ActiveItem } from '../context' 26 | import { CustomMonacoEditor } from '../../customComponent/custom-monaco-editor' 27 | 28 | const BuiltInComponents = { 29 | Input, 30 | Select, 31 | TextArea: Input.TextArea, 32 | Password, 33 | NumberPicker, 34 | Switch, 35 | DatePicker, 36 | RangePicker: DatePicker.RangePicker, 37 | WeekPicker: DatePicker.WeekPicker, 38 | MonthPicker: DatePicker.MonthPicker, 39 | YearPicker: DatePicker.YearPicker, 40 | TimePicker, 41 | Range, 42 | Upload, 43 | Checkbox, 44 | CheckboxGroup: Checkbox.Group, 45 | Radio, 46 | RadioGroup: Radio.Group, 47 | Rating, 48 | Transfer 49 | } 50 | 51 | const LayoutComponents = { 52 | FormCard, 53 | FormMegaLayout 54 | } 55 | 56 | const ArrayComponents = { 57 | ArrayTable, 58 | ArrayCards 59 | } 60 | 61 | export const PropertiesEdit = () => { 62 | const { active } = useContext(ActiveItem) 63 | 64 | const { list, setList } = active 65 | 66 | const [id, setId] = useState('') 67 | 68 | return ( 69 |
77 | { 79 | setList(value) 80 | }} 81 | value={list} 82 | components={{ 83 | ...BuiltInComponents, 84 | ...LayoutComponents, 85 | ...ArrayComponents, 86 | CustomMonacoEditor 87 | }} 88 | schema={schema} 89 | > 90 | {/* 91 | 提交 92 | */} 93 | { 96 | if (id !== action.payload.values.id) { 97 | setId(action.payload.values.id) 98 | } 99 | 100 | if (list.id === id) { 101 | setList(action.payload.values) 102 | return state 103 | } 104 | }} 105 | > 106 | {({ form: spyForm, state }) => { 107 | return
{JSON.stringify(state)}
108 | }} 109 |
110 |
111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/components/core/propertiesEdit/schema/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ISchema } from '@formily/antd' 4 | export const schema: ISchema = { 5 | type: 'object', 6 | properties: { 7 | card: { 8 | type: 'object', 9 | 'x-component': 'tab', 10 | 'x-component-props': { 11 | defaultActiveKey: 'base-config' 12 | }, 13 | properties: { 14 | 'base-config': { 15 | type: 'object', 16 | 'x-component': 'tabpane', 17 | 'x-component-props': { 18 | tab: '基础配置' 19 | }, 20 | properties: { 21 | label: { 22 | type: 'object', 23 | 'x-component': 'mega-layout', 24 | 'x-component-props': { 25 | labelCol: 8, 26 | wrapperCol: 16, 27 | full: true, 28 | labelAlign: 'left' 29 | }, 30 | properties: { 31 | id: { 32 | 'x-component': 'Input', 33 | type: 'string', 34 | title: 'id', 35 | description: '字段的key值' 36 | }, 37 | type: { 38 | 'x-component': 'Input', 39 | type: 'string', 40 | title: 'type', 41 | default: 'string', 42 | description: '字段类型' 43 | }, 44 | DEFAULT_NO_NAME: { 45 | 'x-component': 'mega-layout', 46 | 'x-component-props': { 47 | labelCol: 8, 48 | wrapperCol: 24, 49 | full: true, 50 | labelAlign: 'top' 51 | }, 52 | properties: { 53 | default: { 54 | 'x-component': 'CustomMonacoEditor', 55 | type: 'string', 56 | title: 'default', 57 | description: '字段默认值' 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | 'component-config': { 66 | type: 'object', 67 | 'x-component': 'tabpane', 68 | 'x-component-props': { 69 | tab: '组件配置' 70 | }, 71 | properties: { 72 | label: { 73 | type: 'object', 74 | 'x-component': 'mega-layout', 75 | 'x-component-props': { 76 | labelCol: 8, 77 | wrapperCol: 16, 78 | full: true, 79 | labelAlign: 'left' 80 | }, 81 | properties: { 82 | title: { 83 | 'x-component': 'Input', 84 | type: 'string', 85 | title: 'title', 86 | description: '字段标题' 87 | }, 88 | description: { 89 | 'x-component': 'Input', 90 | type: 'string', 91 | title: 'description', 92 | description: '字段描述' 93 | }, 94 | READONLY_NO_NAME: { 95 | 'x-component': 'mega-layout', 96 | 'x-component-props': { 97 | labelCol: 8, 98 | wrapperCol: 16, 99 | full: false, 100 | labelAlign: 'left' 101 | }, 102 | properties: { 103 | readOnly: { 104 | type: 'boolean', 105 | 'x-component': 'Switch', 106 | title: 'readOnly', 107 | description: '是否只读与 editable 一致' 108 | }, 109 | // writeOnly: { 110 | // type: 'boolean', 111 | // 'x-component': 'Switch', 112 | // title: 'writeOnly' 113 | // }, 114 | // editable 字段是否可编辑 boolean 115 | editable: { 116 | type: 'boolean', 117 | title: 'editable', 118 | description: '字段是否可编辑', 119 | 'x-component': 'Switch' 120 | }, 121 | // visible 字段是否可见(数据+样式) boolean 122 | visible: { 123 | type: 'boolean', 124 | title: 'visible', 125 | description: '字段是否可见(数据+样式)', 126 | 'x-component': 'Switch' 127 | }, 128 | // display 字段样式是否可见 boolean 129 | display: { 130 | type: 'boolean', 131 | title: 'display', 132 | description: '字段样式是否可见', 133 | 'x-component': 'Switch' 134 | } 135 | } 136 | }, 137 | ENUM_NO_NAME: { 138 | 'x-component': 'mega-layout', 139 | 'x-component-props': { 140 | labelCol: 8, 141 | wrapperCol: 24, 142 | full: true, 143 | labelAlign: 'top' 144 | }, 145 | properties: { 146 | // enum: { 147 | // type: 'array', 148 | // 'x-component': 'arraytable', 149 | // title: 'enum', 150 | // items: { 151 | // type: 'object', 152 | // properties: { 153 | // label: { 154 | // title: 'label', 155 | // 'x-component': 'input', 156 | // default: '' 157 | // }, 158 | // value: { 159 | // title: 'value', 160 | // 'x-component': 'input', 161 | // default: '' 162 | // } 163 | // } 164 | // }, 165 | // description: '某些组件的枚举值' 166 | // }, 167 | ENUM_NO_NAME: { 168 | 'x-component': 'mega-layout', 169 | 'x-component-props': { 170 | labelCol: 8, 171 | wrapperCol: 24, 172 | full: true, 173 | labelAlign: 'top' 174 | }, 175 | properties: { 176 | enum: { 177 | 'x-component': 'CustomMonacoEditor', 178 | title: 'enum', 179 | description: '某些组件的枚举值' 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | // maxItems 最大条目数 number 186 | maxItems: { 187 | 'x-component': 'NumberPicker', 188 | type: 'number', 189 | title: 'maxItems', 190 | description: '最大条目数' 191 | }, 192 | // minItems 最小条目数 number 193 | minItems: { 194 | 'x-component': 'NumberPicker', 195 | type: 'number', 196 | title: 'minItems', 197 | description: '最小条目数' 198 | }, 199 | // maxProperties 最大属性数量 number 200 | maxProperties: { 201 | 'x-component': 'NumberPicker', 202 | type: 'number', 203 | title: 'maxProperties', 204 | description: '最大属性数量' 205 | }, 206 | // minProperties 最小属性数量 number 207 | minProperties: { 208 | 'x-component': 'NumberPicker', 209 | type: 'number', 210 | title: 'minProperties', 211 | description: '最小属性数量' 212 | } 213 | } 214 | } 215 | } 216 | }, 217 | 'rules-config': { 218 | type: 'object', 219 | 'x-component': 'tabpane', 220 | 'x-component-props': { 221 | tab: '校验配置' 222 | }, 223 | properties: { 224 | label: { 225 | type: 'object', 226 | 'x-component': 'mega-layout', 227 | 'x-component-props': { 228 | labelCol: 8, 229 | wrapperCol: 16, 230 | full: true, 231 | labelAlign: 'left' 232 | }, 233 | properties: { 234 | CONST_NO_NAME: { 235 | 'x-component': 'mega-layout', 236 | 'x-component-props': { 237 | labelCol: 8, 238 | wrapperCol: 24, 239 | full: true, 240 | labelAlign: 'top' 241 | }, 242 | properties: { 243 | const: { 244 | 'x-component': 'CustomMonacoEditor', 245 | title: 'const', 246 | description: '校验字段值是否与 const 的值相等' 247 | } 248 | } 249 | }, 250 | // validate 251 | multipleOf: { 252 | 'x-component': 'NumberPicker', 253 | type: 'number', 254 | title: 'multipleOf', 255 | description: '校验字段值是否可被 multipleOf 的值整除' 256 | }, 257 | // maximum 校验最大值(大于) number 258 | maximum: { 259 | 'x-component': 'NumberPicker', 260 | type: 'number', 261 | title: 'maximum', 262 | description: '校验最大值(大于)' 263 | }, 264 | // exclusiveMaximum 校验最大值(大于等于) number 265 | exclusiveMaximum: { 266 | 'x-component': 'NumberPicker', 267 | type: 'number', 268 | title: 'exclusiveMaximum', 269 | description: '校验最大值(大于等于)' 270 | }, 271 | // minimum 校验最小值(小于) number 272 | minimum: { 273 | 'x-component': 'NumberPicker', 274 | type: 'number', 275 | title: 'minimum', 276 | description: '校验最小值(小于)' 277 | }, 278 | // exclusiveMinimum 最小值(小于等于) number 279 | exclusiveMinimum: { 280 | 'x-component': 'NumberPicker', 281 | type: 'number', 282 | title: 'exclusiveMinimum', 283 | description: '最小值(小于等于)' 284 | }, 285 | // maxLength 校验最大长度 number 286 | maxLength: { 287 | 'x-component': 'NumberPicker', 288 | type: 'number', 289 | title: 'maxLength', 290 | description: '校验最大长度' 291 | }, 292 | // minLength 校验最小长度 number 293 | minLength: { 294 | 'x-component': 'NumberPicker', 295 | type: 'number', 296 | title: 'minLength', 297 | description: '校验最小长度' 298 | }, 299 | // pattern 正则校验规则 string | RegExp 300 | pattern: { 301 | 'x-component': 'Input', 302 | type: 'string', 303 | title: 'pattern', 304 | description: '正则校验规则' 305 | }, 306 | // uniqueItems 是否校验重复 boolean 307 | uniqueItems: { 308 | type: 'boolean', 309 | title: 'uniqueItems', 310 | description: '是否校验重复', 311 | 'x-component': 'RadioGroup', 312 | enum: [ 313 | { 314 | label: 'true', 315 | value: true 316 | }, 317 | { 318 | label: 'false', 319 | value: false 320 | } 321 | ] 322 | }, 323 | // required 必填 boolean 324 | required: { 325 | type: 'boolean', 326 | title: 'required', 327 | description: '是否必填', 328 | 'x-component': 'RadioGroup', 329 | enum: [ 330 | { 331 | label: 'true', 332 | value: true 333 | }, 334 | { 335 | label: 'false', 336 | value: false 337 | } 338 | ] 339 | }, 340 | // format 正则规则类型,详细类型可以往后看 InternalFormats !!! 341 | format: { 342 | 'x-component': 'Select', 343 | 'x-component-props': { 344 | mode: 'tags' 345 | }, 346 | title: 'format', 347 | description: '正则规则类型', 348 | enum: [ 349 | 'url', 350 | 'email', 351 | 'ipv6', 352 | 'ipv4', 353 | 'idcard', 354 | 'taodomain', 355 | 'qq', 356 | 'phone', 357 | 'money', 358 | 'zh', 359 | 'date', 360 | 'zip' 361 | ] 362 | }, 363 | // triggerType 字段校验时机 "onChange" | "onBlur" 364 | triggerType: { 365 | 'x-component': 'Input', 366 | type: 'string', 367 | title: 'triggerType', 368 | description: '字段校验时机', 369 | enum: [ 370 | { label: 'onChange', value: 'onChange' }, 371 | { label: 'onBlur', value: 'onBlur' } 372 | ], 373 | 'x-component-props': { 374 | placeholder: '字段校验时机' 375 | } 376 | } 377 | } 378 | } 379 | } 380 | }, 381 | 'x-config': { 382 | type: 'object', 383 | 'x-component': 'tabpane', 384 | 'x-component-props': { 385 | tab: 'x扩展配置' 386 | }, 387 | properties: { 388 | label: { 389 | type: 'object', 390 | 'x-component': 'mega-layout', 391 | 'x-component-props': { 392 | labelCol: 8, 393 | wrapperCol: 16, 394 | full: true, 395 | labelAlign: 'left' 396 | }, 397 | properties: { 398 | // x-component 字段 UI 组件名称,大小写不敏感 string 399 | 'x-component': { 400 | 'x-component': 'Input', 401 | type: 'string', 402 | title: 'x-component' 403 | }, 404 | // x-index 字段顺序 number 405 | 'x-index': { 406 | 'x-component': 'NumberPicker', 407 | type: 'number', 408 | title: 'x-index', 409 | description: '字段顺序' 410 | }, 411 | // x-component-props 字段 UI 组件属性 {} 412 | X_COMPONENT_PROPS_NO_NAME: { 413 | 'x-component': 'mega-layout', 414 | 'x-component-props': { 415 | labelCol: 8, 416 | wrapperCol: 24, 417 | full: true, 418 | labelAlign: 'top' 419 | }, 420 | properties: { 421 | 'x-component-props': { 422 | 'x-component': 'CustomMonacoEditor', 423 | title: 'x-component-props', 424 | description: '字段 UI 组件属性' 425 | } 426 | } 427 | }, 428 | // x-props 字段扩展属性 { [name: string]: any } !!! 429 | X_PROPS_NO_NAME: { 430 | 'x-component': 'mega-layout', 431 | 'x-component-props': { 432 | labelCol: 8, 433 | wrapperCol: 24, 434 | full: true, 435 | labelAlign: 'top' 436 | }, 437 | properties: { 438 | 'x-props': { 439 | 'x-component': 'CustomMonacoEditor', 440 | type: 'string', 441 | title: 'x-props', 442 | description: '字段扩展属性' 443 | } 444 | } 445 | }, 446 | // x-rules 字段校验规则,详细描述可以往后看 ValidatePatternRules 447 | X_RULES_NO_NAME: { 448 | 'x-component': 'mega-layout', 449 | 'x-component-props': { 450 | labelCol: 8, 451 | wrapperCol: 24, 452 | full: true, 453 | labelAlign: 'top' 454 | }, 455 | properties: { 456 | 'x-rules': { 457 | 'x-component': 'CustomMonacoEditor', 458 | title: 'x-rules', 459 | description: '字段校验规则' 460 | } 461 | } 462 | }, 463 | // x-linkages 字段间联动协议,详细描述可以往后看 Array<{ target: FormPathPattern, type: string, [key: string]: any }> 464 | X_LINKAGES_NO_NAME: { 465 | 'x-component': 'mega-layout', 466 | 'x-component-props': { 467 | labelCol: 8, 468 | wrapperCol: 24, 469 | full: true, 470 | labelAlign: 'top' 471 | }, 472 | properties: { 473 | 'x-linkages': { 474 | 'x-component': 'CustomMonacoEditor', 475 | type: 'string', 476 | title: 'x-linkages', 477 | description: '字段间联动协议,详细描述可以往后看' 478 | } 479 | } 480 | }, 481 | X_MEGA_PROPS_NO_NAME: { 482 | 'x-component': 'mega-layout', 483 | 'x-component-props': { 484 | labelCol: 8, 485 | wrapperCol: 24, 486 | full: true, 487 | labelAlign: 'top' 488 | }, 489 | properties: { 490 | 'x-mega-props': { 491 | 'x-component': 'CustomMonacoEditor', 492 | type: 'string', 493 | title: 'x-mega-props', 494 | description: '字段布局属性' 495 | } 496 | } 497 | } 498 | } 499 | } 500 | } 501 | } 502 | } 503 | } 504 | // label: { 505 | // type: "object", 506 | // "x-component": "mega-layout", 507 | // "x-component-props": { 508 | // labelAlign: "top", 509 | // }, 510 | // properties: { 511 | 512 | // // properties 对象属性 {[key : string]:Schema} !!! 513 | // // properties: { 514 | // // "x-component": "Input", 515 | // // type: "string", 516 | // // title: "properties", 517 | // // description: "正则规则类型,详细类型可以往后看", 518 | // // }, 519 | // // items 数组描述 Schema | Schema[] !!! 520 | // // items: { 521 | // // "x-component": "Input", 522 | // // type: "string", 523 | // // title: "items", 524 | // // description: "数组描述", 525 | // // }, 526 | // // additionalItems 额外数组元素描述 Schema !!! 527 | // additionalItems: { 528 | // "x-component": "Input", 529 | // type: "string", 530 | // title: "additionalItems", 531 | // description: "额外数组元素描述", 532 | // }, 533 | // // patternProperties 动态匹配对象的某个属性的 Schema {[key : string]:Schema} !!! 534 | // patternProperties: { 535 | // "x-component": "Input", 536 | // type: "string", 537 | // title: "patternProperties", 538 | // description: "动态匹配对象的某个属性的 Schema", 539 | // }, 540 | // // additionalProperties 匹配对象额外属性的 Schema Schema !!! 541 | // additionalProperties: { 542 | // "x-component": "Input", 543 | // type: "string", 544 | // title: "additionalProperties", 545 | // description: "匹配对象额外属性的 Schema", 546 | // }, 547 | // }, 548 | // }, 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /src/components/customComponent/custom-monaco-editor.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState, useEffect } from 'react' 4 | import MonacoEditor from 'react-monaco-editor' 5 | import { ISchemaFieldComponentProps } from '@formily/antd' 6 | import { Modal, Button, Tag } from 'antd' 7 | 8 | const CustomMonacoEditor = (props: ISchemaFieldComponentProps) => { 9 | let { value = '' } = props 10 | 11 | const [state, setState] = useState({ 12 | visible: false 13 | }) 14 | 15 | const handleOk = (e: any) => { 16 | onValueChange(code) 17 | 18 | setState({ 19 | visible: false 20 | }) 21 | } 22 | 23 | const handleCancel = (e: any) => { 24 | setState({ 25 | visible: false 26 | }) 27 | } 28 | 29 | const onValueChange = (value: any) => { 30 | const [firstStr] = value 31 | 32 | let result: any = value 33 | 34 | if (firstStr === '"') { 35 | result = value.slice(1, -1) 36 | } else if (firstStr === '{' || firstStr === '[') { 37 | result = JSON.parse(value) 38 | } else { 39 | result = Number(value) 40 | } 41 | 42 | props.onChange(result) 43 | } 44 | 45 | /** 46 | * 大编辑器与小编辑器共用的代码 47 | */ 48 | const [code, setCode] = useState(value || '') 49 | 50 | const [syncFlag, setSyncFlag] = useState(true) 51 | 52 | const ReSetCode = (newValue: any) => { 53 | setSyncFlag(false) 54 | setCode(newValue) 55 | } 56 | 57 | useEffect(() => { 58 | setCode(JSON.stringify(value, undefined, 2)) 59 | }, [value]) 60 | 61 | return ( 62 |
67 | 76 |
82 | 88 | {'数据未同步,请点击“同步”按钮'} 89 | 90 | 100 | 109 |
110 | 122 | 130 | 131 |
132 | ) 133 | } 134 | 135 | CustomMonacoEditor.CostomMonacoEditor = true 136 | 137 | export { CustomMonacoEditor } 138 | -------------------------------------------------------------------------------- /src/components/draggable/customHooks.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { useState } from 'react' 4 | export const useActiveTab = () => { 5 | const [activetab, setActiveTab] = useState('') 6 | } 7 | -------------------------------------------------------------------------------- /src/components/draggable/draggable.d.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ReactSortableProps } from 'react-sortablejs' 4 | import { ISchema } from '@formily/antd' 5 | 6 | export type SetList = ReactSortableProps['setList'] 7 | 8 | type OmitISchemaKey = 9 | | 'properties' 10 | | 'patternProperties' 11 | | 'items' 12 | | 'additionalItems' 13 | | 'additionalProperties' 14 | 15 | type ExcluedISchema = Omit 16 | 17 | export interface IDraggableList extends ExcluedISchema { 18 | id: string | number 19 | properties?: IDraggableList[] 20 | patternProperties?: IDraggableList[] 21 | items?: IDraggableList | IDraggableList[] 22 | additionalItems?: IDraggableList 23 | additionalProperties?: IDraggableList 24 | } 25 | 26 | export interface IReactDraggableProps extends ReactSortableProps { 27 | allowActive?: boolean 28 | } 29 | 30 | export interface INestedProps extends ReactSortableProps { 31 | allowActive?: boolean 32 | } 33 | -------------------------------------------------------------------------------- /src/components/draggable/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { IReactDraggableProps } from './draggable' 5 | import { Nested } from './nested' 6 | 7 | export const ReactDraggable = (props: IReactDraggableProps) => { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/components/draggable/nested.scss: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | .wrapper { 4 | width: 400px; 5 | height: 60px; 6 | p { 7 | width: 10%; 8 | float: left; 9 | } 10 | .nested { 11 | border: 1px solid black; 12 | width: 70%; 13 | height: 100%; 14 | position: relative; 15 | margin-left: 20px; 16 | float: left; 17 | } 18 | 19 | .commontype { 20 | border: 1px dashed black; 21 | width: 70%; 22 | height: 60%; 23 | position: relative; 24 | margin-left: 20px; 25 | float: left; 26 | } 27 | } 28 | .active-target { 29 | border: 1px solid lightblue; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/draggable/nested.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useContext } from 'react' 4 | import { ReactSortable } from 'react-sortablejs' 5 | import { INestedProps, IDraggableList } from './draggable' 6 | import { 7 | SchemaForm, 8 | SchemaMarkupField as Field, 9 | ISchemaFieldComponentProps 10 | } from '@formily/antd' 11 | import { DraggableToFormily } from '../../utils/transform' 12 | import { merge } from 'lodash' 13 | import { 14 | Input, 15 | Select, 16 | Password, 17 | NumberPicker, 18 | Switch, 19 | DatePicker, 20 | TimePicker, 21 | Range, 22 | Upload, 23 | Checkbox, 24 | Radio, 25 | Rating, 26 | Transfer, 27 | FormCard, 28 | FormBlock, 29 | FormMegaLayout, 30 | FormTab, 31 | ArrayCards, 32 | ArrayTable 33 | } from '@formily/antd-components' 34 | import { Card, Form, Tabs } from 'antd' 35 | import { cloneDeep } from 'lodash' 36 | import { ActiveItem } from '../core/context' 37 | import 'antd/dist/antd.css' 38 | import './nested.scss' 39 | import { GenNonDuplicateID } from '../../utils' 40 | 41 | const { TabPane } = Tabs 42 | 43 | // 内置的非布局组件 44 | const BuiltInComponents = { 45 | Input, 46 | Select, 47 | TextArea: Input.TextArea, 48 | Password, 49 | NumberPicker, 50 | Switch, 51 | DatePicker, 52 | RangePicker: DatePicker.RangePicker, 53 | WeekPicker: DatePicker.WeekPicker, 54 | MonthPicker: DatePicker.MonthPicker, 55 | YearPicker: DatePicker.YearPicker, 56 | TimePicker, 57 | Range, 58 | Upload, 59 | Checkbox, 60 | CheckboxGroup: Checkbox.Group, 61 | Radio, 62 | RadioGroup: Radio.Group, 63 | Rating, 64 | Transfer 65 | } 66 | 67 | // 布局组件 68 | const LayoutComponents = { 69 | FormCard, 70 | FormBlock, 71 | FormMegaLayout, 72 | FormTab 73 | } 74 | 75 | const ArrayComponent = { 76 | ArrayCards, 77 | ArrayTable 78 | } 79 | 80 | /** 81 | * 合并后的组件 82 | * @todo 是否可以考虑进行动态引入 83 | */ 84 | const margeComponents = { 85 | ...BuiltInComponents, 86 | ...LayoutComponents, 87 | ...ArrayComponent 88 | } 89 | 90 | interface INested { 91 | (props: INestedProps): JSX.Element 92 | (props: { value: INestedProps }): JSX.Element 93 | } 94 | 95 | export const isInestedProps = (value: any): value is INestedProps => { 96 | return !value.value 97 | } 98 | 99 | const NestedObject = (props: ISchemaFieldComponentProps) => { 100 | const { list, setList } = props.value 101 | 102 | return ( 103 | 113 | ) 114 | } 115 | 116 | /** 117 | * 项目核心函数式组件,用于递归渲染 118 | * @param props 119 | */ 120 | export const Nested: INested = (args: any) => { 121 | let props: INestedProps = args 122 | 123 | if (!isInestedProps(args)) { 124 | props = args.value 125 | } 126 | 127 | const { list, setList } = props 128 | 129 | /** 130 | * 重写setList,使其对于递归组件也有效果 131 | * @param item 132 | */ 133 | const nestedSetList = (item: IDraggableList) => { 134 | return (newState: IDraggableList | IDraggableList[]) => { 135 | setList( 136 | list.map((t) => { 137 | if (t.id === item.id) { 138 | if (t.type === 'object') { 139 | return { 140 | ...item, 141 | id: item.id, 142 | properties: cloneDeep(newState) as IDraggableList[] 143 | } 144 | } else if (item.type === 'array') { 145 | return { 146 | ...item, 147 | id: item.id, 148 | items: cloneDeep(newState) as IDraggableList[] 149 | } 150 | } else { 151 | return newState as IDraggableList 152 | } 153 | } 154 | return t 155 | }), 156 | null, 157 | { 158 | dragging: null 159 | } 160 | ) 161 | } 162 | } 163 | 164 | const { changeActive } = useContext(ActiveItem) 165 | 166 | const ObjOnclick = (item: IDraggableList, list: IDraggableList[]) => { 167 | return (event: React.MouseEvent) => { 168 | event.stopPropagation() 169 | 170 | if (props.allowActive) { 171 | changeActive({ 172 | list: item, 173 | setList: (newState) => { 174 | setList( 175 | list.map((card) => { 176 | if (card.id === newState.id) { 177 | return newState 178 | } 179 | return card 180 | }), 181 | null, 182 | { 183 | dragging: null 184 | } 185 | ) 186 | } 187 | }) 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * 目前的默认渲染机制 194 | * @param item 195 | */ 196 | const DefaultRender = (item: IDraggableList) => { 197 | if (item.id === 'formCards' || item.id === 'schema') { 198 | debugger 199 | } 200 | 201 | return ( 202 | 203 | { 207 | event.stopPropagation() 208 | 209 | if (props.allowActive) { 210 | changeActive({ 211 | list: item, 212 | setList: (newState) => { 213 | if (!/\{\{ *\}\}/.test(JSON.stringify(newState))) { 214 | nestedSetList(item)(newState) 215 | } 216 | } 217 | }) 218 | } 219 | }} 220 | schema={{ 221 | type: 'object', 222 | properties: { [item.id]: DraggableToFormily([item]) } 223 | }} 224 | > 225 | 226 | ) 227 | } 228 | 229 | /** 230 | * 对于对象的渲染机制 231 | * @param item 232 | * @todo 目前样式上之对于Cards有效 233 | */ 234 | const ObjectRender = (item: IDraggableList) => { 235 | let ObjRenderCom: any = FormCard 236 | 237 | switch (item['x-component']?.toLocaleLowerCase()) { 238 | case 'block': 239 | ObjRenderCom = FormBlock 240 | break 241 | case 'tab': 242 | ObjRenderCom = Tabs 243 | break 244 | case 'tabpane': 245 | ObjRenderCom = TabPane 246 | break 247 | case 'mega-layout': 248 | ObjRenderCom = FormMegaLayout 249 | } 250 | 251 | switch (item['x-component']?.toLocaleLowerCase()) { 252 | case 'block': 253 | return ( 254 | 255 | 266 | 267 | 268 | 269 | 270 | 271 | ) 272 | case 'mega-layout': 273 | case '': 274 | return ( 275 | 276 |
283 |
{item['x-component'] || 'pureObject'}
284 | 289 |
290 |
291 | ) 292 | case 'tab': 293 | let active: string | undefined = 'tab-1' 294 | 295 | console.log(active) 296 | 297 | const activeObj = { 298 | add(activeKey: string | React.MouseEvent) { 299 | const id = GenNonDuplicateID() 300 | 301 | setList( 302 | list.map((i) => { 303 | if (i.id === item.id) { 304 | return { 305 | ...item, 306 | properties: [ 307 | ...item.properties, 308 | { 309 | id: 'schema' + id, 310 | type: 'object', 311 | 'x-component': 'tabpane', 312 | 'x-component-props': { 313 | tab: '选项' + id 314 | }, 315 | properties: [] 316 | } 317 | ] 318 | } 319 | } 320 | return i 321 | }), 322 | null, 323 | { dragging: null } 324 | ) 325 | }, 326 | remove(activeKey: string | React.MouseEvent) { 327 | setList( 328 | list.map((i) => { 329 | if (i.id === item.id) { 330 | return { 331 | ...item, 332 | properties: item.properties?.filter((j) => { 333 | return j.id !== activeKey 334 | }) 335 | } 336 | } 337 | return i 338 | }), 339 | null, 340 | { dragging: null } 341 | ) 342 | } 343 | } 344 | 345 | const onChange = (activeKey: string) => { 346 | active = activeKey 347 | } 348 | 349 | const onEdit = ( 350 | targetKey: string | React.MouseEvent, 351 | action: 'add' | 'remove' 352 | ) => { 353 | activeObj[action](targetKey) 354 | } 355 | 356 | return ( 357 | 358 |
359 | 366 | {item.properties?.map((tabp) => { 367 | return ( 368 | 369 | { 373 | setList( 374 | list.map((i) => { 375 | if (item.id === i.id) { 376 | return { 377 | ...item, 378 | properties: item.properties!.map((t) => { 379 | if (t.id === tabp.id) { 380 | return { 381 | ...tabp, 382 | id: tabp.id, 383 | properties: cloneDeep( 384 | newState 385 | ) as IDraggableList[] 386 | } 387 | } 388 | return t 389 | }) 390 | } 391 | } 392 | return i 393 | }), 394 | null, 395 | { dragging: null } 396 | ) 397 | }} 398 | > 399 | 400 | ) 401 | })} 402 | 403 |
404 |
405 | ) 406 | default: 407 | return ( 408 | 409 |
410 | 411 | 416 | 417 |
418 |
419 | ) 420 | } 421 | } 422 | 423 | /** 424 | * 对于数组的渲染机制 425 | * @param item 426 | * @todo 暂未完善 427 | */ 428 | const ArrayRender = (item: IDraggableList) => { 429 | return ( 430 | 435 | 440 | 441 | ) 442 | } 443 | 444 | return ( 445 | 460 | {list.map((item: IDraggableList, index: number) => { 461 | switch (item.type) { 462 | case 'object': 463 | return ObjectRender(item) 464 | case 'array': 465 | return ArrayRender(item) 466 | default: 467 | return DefaultRender(item) 468 | } 469 | })} 470 | 471 | ) 472 | } 473 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "@formily/printer"; 4 | -------------------------------------------------------------------------------- /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.0/8 are 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.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 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/utils/__tests__/transform.test.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ISchema } from '@formily/antd' 4 | /** @format */ 5 | 6 | import { DraggableToFormily, FormilyToDraggable } from '../transform' 7 | 8 | it('Sortable list data transform to formily schema', () => { 9 | const obj: ISchema = { 10 | type: 'object', 11 | properties: { 12 | key: { 13 | type: 'string' 14 | }, 15 | key1: { 16 | type: 'object', 17 | properties: { 18 | key2: { 19 | type: 'array', 20 | items: [ 21 | { 22 | type: 'object', 23 | properties: { 24 | key3: { 25 | type: 'string' 26 | } 27 | } 28 | }, 29 | { 30 | type: 'object', 31 | properties: { 32 | key4: { 33 | type: 'string' 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | const arr = [ 45 | { 46 | id: 'schema', 47 | type: 'object', 48 | properties: [ 49 | { 50 | id: 'key', 51 | type: 'string' 52 | }, 53 | { 54 | id: 'key1', 55 | type: 'object', 56 | properties: [ 57 | { 58 | id: 'key2', 59 | type: 'array', 60 | items: [ 61 | { 62 | type: 'object', 63 | id: 'schema', 64 | properties: [ 65 | { 66 | id: 'key3', 67 | type: 'string' 68 | } 69 | ] 70 | }, 71 | { 72 | type: 'object', 73 | id: 'schema', 74 | properties: [ 75 | { 76 | id: 'key4', 77 | type: 'string' 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | ] 84 | } 85 | ] 86 | } 87 | ] 88 | 89 | expect( 90 | DraggableToFormily([ 91 | { 92 | title: 'task 3', 93 | id: 2, 94 | properties: [ 95 | { 96 | title: 'task 4', 97 | properties: [], 98 | id: 3 99 | } 100 | ] 101 | } 102 | ]) 103 | ).toEqual({ 104 | title: 'task 3', 105 | properties: { '3': { properties: {}, title: 'task 4' } } 106 | }) 107 | 108 | expect(FormilyToDraggable(obj)).toEqual(arr) 109 | 110 | expect(DraggableToFormily(arr)).toEqual(obj) 111 | }) 112 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于创建随机id 3 | * 4 | * @format 5 | * @param randomLength 随机数长度控制 6 | */ 7 | 8 | export const GenNonDuplicateID = (randomLength: number = 16) => { 9 | return Number(Math.random().toString().substr(3, randomLength) + Date.now()).toString(36) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/transform.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { cloneDeep } from 'lodash' 4 | import { IDraggableList } from '../components/draggable/draggable' 5 | import { ISchema } from '@formily/antd' 6 | import { GenNonDuplicateID } from '../utils' 7 | 8 | /** 9 | * 将 Draggable(ReactSortable) 的数据格式转换为 formily 所需的数据格式 10 | * @param list 传入 IDrIDraggableList[] 格式的数据 11 | */ 12 | export const DraggableToFormily = (list: IDraggableList[]): ISchema => { 13 | const arrayToObject = ( 14 | properties: IDraggableList[] 15 | ): { 16 | [key: string]: ISchema 17 | } => { 18 | return properties.reduce((propertiesPre, PropertiesNow) => { 19 | return { 20 | ...propertiesPre, 21 | [PropertiesNow.id]: DraggableToFormily([PropertiesNow]) 22 | } 23 | }, {}) 24 | } 25 | 26 | const Schema = list.reduce((pre, now) => { 27 | const DeepPropertise: ISchema = Object.create(null) 28 | 29 | Object.assign(DeepPropertise, now) 30 | 31 | if (now.properties) { 32 | DeepPropertise.properties = arrayToObject(now.properties) 33 | } 34 | 35 | if (now.patternProperties) { 36 | DeepPropertise.patternProperties = arrayToObject(now.patternProperties) 37 | } 38 | 39 | if (now.items) { 40 | if (!Array.isArray(now.items)) { 41 | now.items = [now.items] 42 | } 43 | DeepPropertise.items = now.items.map((item) => DraggableToFormily([item])) 44 | } 45 | 46 | delete (DeepPropertise as { id: string }).id 47 | 48 | return { 49 | ...pre, 50 | ...DeepPropertise 51 | } 52 | }, {}) 53 | 54 | return Schema 55 | } 56 | 57 | /** 58 | * 将 formily 的数据格式转换为 Draggable(ReactSortable) 所需的数据格式 59 | * @param vSchema 传入 ISchema 格式的数据 60 | * @param key 可控制ISchema数据格式的第一个对象转换为 Draggable(ReactSortable) 格式时的id名 61 | */ 62 | export const FormilyToDraggable = (vSchema: ISchema, key?: string): IDraggableList[] => { 63 | const schema = { 64 | [key || 'schema' + GenNonDuplicateID()]: cloneDeep(vSchema) 65 | } 66 | const keys = Object.keys(schema) 67 | 68 | const ObjectToArray = (properties: { [key: string]: ISchema }) => { 69 | return Object.keys(properties).map((propertiesKey) => { 70 | return FormilyToDraggable(properties[propertiesKey], propertiesKey)[0] 71 | }) 72 | } 73 | 74 | const result = keys.reduce((pre: Array, nowKey: string) => { 75 | const value: any = schema[nowKey] 76 | 77 | if (value.properties) { 78 | value.properties = ObjectToArray(value.properties) 79 | } 80 | 81 | if (value.patternProperties) { 82 | value.patternProperties = ObjectToArray(value.patternProperties) 83 | } 84 | 85 | if (value.items) { 86 | if (typeof value.items === 'object' && !Array.isArray(value.items)) { 87 | value.items = [value.items] 88 | } 89 | value.items = value.items.map( 90 | (item: ISchema | { [key: string]: ISchema }) => FormilyToDraggable(item)[0] 91 | ) 92 | } 93 | 94 | return [ 95 | ...pre, 96 | { 97 | id: nowKey, 98 | ...value 99 | } 100 | ] 101 | }, []) 102 | 103 | return result 104 | } 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "suppressImplicitAnyIndexErrors": true, 18 | "downlevelIteration": true 19 | }, 20 | "include": ["src", "src/utils"] 21 | } 22 | --------------------------------------------------------------------------------