├── .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 | 
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 | 
36 | 
37 |
38 | **pureObject 不存在**
39 |
40 | 
41 | 
42 |
43 | ##### 数组组件
44 |
45 | 数组组件目前有两种 ArrayCard 与 ArrayTable
46 |
47 | **注意,因数组的 items 属性是 ISchema | ISchema[] 所以具体的组件需要拖放在内部的对象中,而不是数组本身**
48 |
49 | 因表格组件的拖拽显示不太直观,所以数组组件在拖拽区域如下图所示
50 |
51 | 
52 |
53 | 而最终预览的形态,则以表格的形式正常显示
54 |
55 | 
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------