├── .editorconfig
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .umirc.js
├── README.md
├── mock
├── .gitkeep
└── index.js
├── package.json
└── src
├── app.js
├── components
├── configButton
│ └── index.js
├── configTree
│ ├── component
│ │ ├── compactArrayView.js
│ │ └── compactObjectView.js
│ ├── index.js
│ ├── index.less
│ ├── parser
│ │ ├── highlight.js
│ │ └── index.js
│ ├── render
│ │ ├── iconRender.js
│ │ └── nameRender.js
│ └── treeNode.js
├── drawer
│ └── index.js
├── guide
│ ├── index.js
│ └── index.less
├── jsonEditor
│ ├── index.js
│ ├── index.less
│ ├── lint
│ │ ├── basicType.js
│ │ └── index.js
│ └── suggestions
│ │ ├── actions.js
│ │ ├── dependency.js
│ │ ├── index.js
│ │ └── request.js
├── jsonEditorDrawer
│ └── index.js
├── jsonFormTemp
│ ├── component
│ │ └── formDrawer.js
│ └── index.js
├── jsonTableTemp
│ ├── component
│ │ └── tableDrawer.js
│ └── index.js
├── languageSwitch
│ ├── index.js
│ └── index.less
├── pluginTree
│ ├── index.js
│ ├── index.less
│ └── sulaconfig
│ │ ├── actions.js
│ │ ├── columns.js
│ │ ├── fieldPlugins.js
│ │ └── renderPlugins.js
├── styleSelect
│ ├── index.js
│ └── index.less
├── themeSwitch
│ ├── index.js
│ └── index.less
├── tipsSwitch
│ └── index.js
└── tipsWrapper
│ └── index.js
├── global.js
├── layout
├── index.js
├── index.less
└── themeContext.js
├── locales
├── en-US.js
└── zh-CN.js
├── menus.js
├── pages
├── exception
│ └── 404.js
├── form
│ ├── card.js
│ ├── horizontal.js
│ ├── media.js
│ ├── nestedcard.js
│ ├── stepform.js
│ └── vertical.js
├── list
│ ├── advancedsearch.js
│ ├── basic.js
│ ├── nopagination.js
│ ├── singlesearch.js
│ └── stepquerytable.js
├── sulalayout
│ ├── components
│ │ ├── index.js
│ │ └── style.less
│ └── form.js
└── sulaplugin
│ ├── form.js
│ └── table.js
├── routes.js
└── utils
├── getFormMode.js
└── serialize.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /npm-debug.log*
6 | /yarn-error.log
7 | /yarn.lock
8 | /package-lock.json
9 |
10 | # production
11 | /dist
12 |
13 | # misc
14 | .DS_Store
15 |
16 | # umi
17 | .umi
18 | .umi-production
19 | .umi-test
20 | /.env.local
21 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | **/*.ejs
4 | **/*.html
5 | package.json
6 | .umi
7 | .umi-production
8 | .umi-test
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 80,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.umirc.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'umi';
2 |
3 | export default defineConfig({
4 | sula: {},
5 | hash: true,
6 | history: {
7 | type: 'hash',
8 | },
9 | locale: {
10 | default: 'en-US',
11 | antd: true,
12 | },
13 | nodeModulesTransform: {
14 | type: 'none',
15 | },
16 | routes: [
17 | {
18 | name: 'Home',
19 | path: '/',
20 | component: '../layout',
21 | routes: [
22 | {
23 | component: './exception/404',
24 | },
25 | ],
26 | },
27 | ],
28 | });
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # umi project
2 |
3 | ## Getting Started
4 |
5 | Install dependencies,
6 |
7 | ```bash
8 | $ yarn install
9 | ```
10 |
11 | Start the dev server,
12 |
13 | ```bash
14 | $ yarn start
15 | ```
16 |
17 | 🍳 Let's cook it!
18 |
--------------------------------------------------------------------------------
/mock/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/sula-cooker/5d98874b7804c71eea7cd69a56d1cb3f2adbdcab/mock/.gitkeep
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | import { Random, mock } from 'mockjs';
2 | import moment from 'moment';
3 |
4 | const status = ['dispatching', 'success', 'warning'];
5 |
6 | const level = ['High', 'Medium', 'Low'];
7 |
8 | const recipientName = ['Lucy', 'Lily', 'Jack', 'Mocy'];
9 |
10 | const recipientTime = ['morning', 'afternoon', 'night'];
11 |
12 | const priceProject = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];
13 |
14 | const SERIAL = 'SERIAL_NUMBER_';
15 |
16 | const success = {
17 | success: true,
18 | code: 200,
19 | message: 'success',
20 | description: 'success',
21 | };
22 |
23 | const faild = {
24 | success: false,
25 | code: 404,
26 | message: 'faild',
27 | description: 'Page not found',
28 | };
29 |
30 | let dataSource = [];
31 | for (let i = 0; i < 200; i += 1) {
32 | dataSource.push({
33 | id: i + '',
34 | name: Random.name(),
35 | senderName: Random.name(),
36 | senderNumber: Random.id(),
37 | senderAddress: Random.sentence(2, 3),
38 | recipientName: Random.pick(recipientName),
39 | recipientNumber: Random.id(),
40 | recipientAddress: Random.sentence(2, 3),
41 | recipientTime: Random.pick(recipientTime),
42 | time: [Random.date('yyyy-MM-dd'), Random.date('yyyy-MM-dd')],
43 | priceProject: Random.pick(priceProject),
44 | address: Random.city(true),
45 | status: Random.pick(status),
46 | level: Random.pick(level),
47 | description: Random.sentence(3, 4),
48 | times: Random.natural(),
49 | createTime: Random.date('MM-dd HH:mm:ss'),
50 | ruler: [[{ type: 'price', comparator: 'lt', value: '100' }]],
51 | });
52 | }
53 |
54 | let maxId = -1;
55 | dataSource.forEach(({ id }) => {
56 | if (id > maxId) {
57 | maxId = id;
58 | }
59 | });
60 |
61 | function getPagingData(
62 | { current, pageSize },
63 | filters = {},
64 | { order, columnKey } = {},
65 | nopag,
66 | ) {
67 | let filteredDataSource = dataSource;
68 | if (Object.keys(filters).length) {
69 | filteredDataSource = filteredDataSource.filter((row) => {
70 | const isMatched = Object.keys(filters).every((key) => {
71 | const filterValue = filters[key];
72 | const cellValue = row[key];
73 | if (filterValue === null) {
74 | return true;
75 | }
76 | if (Array.isArray(filterValue)) {
77 | if (filterValue.length === 0) {
78 | return true;
79 | }
80 | if (typeof cellValue === 'string') {
81 | return filterValue.includes(cellValue);
82 | }
83 | if (Array.isArray(cellValue) && cellValue.length) {
84 | return (
85 | moment(filterValue[0]).valueOf() <=
86 | moment(cellValue[0]).valueOf() &&
87 | moment(cellValue[0]).valueOf() <=
88 | moment(filterValue[1]).valueOf() &&
89 | moment(filterValue[0]).valueOf() <=
90 | moment(cellValue[1]).valueOf() &&
91 | moment(cellValue[1]).valueOf() <= moment(filterValue[1]).valueOf()
92 | );
93 | }
94 | return true;
95 | }
96 | if (typeof cellValue === 'number' || typeof cellValue === 'boolean') {
97 | return filterValue === cellValue;
98 | }
99 | if (typeof cellValue !== 'number' && !cellValue) {
100 | return true;
101 | }
102 |
103 | if (key === 'id') {
104 | return `${SERIAL}${cellValue}`.includes(filterValue);
105 | }
106 |
107 | return cellValue.includes(filterValue);
108 | });
109 |
110 | return isMatched;
111 | });
112 | }
113 |
114 | if (order) {
115 | filteredDataSource.sort((a, b) => {
116 | return order === 'ascend'
117 | ? a[columnKey] - b[columnKey]
118 | : b[columnKey] - a[columnKey];
119 | });
120 | }
121 |
122 | const pageData = [];
123 | const start = (current - 1) * pageSize;
124 | let end = current * pageSize;
125 |
126 | if (end > filteredDataSource.length) {
127 | end = filteredDataSource.length;
128 | }
129 | for (let i = start; i < end; i += 1) {
130 | pageData.push(filteredDataSource[i]);
131 | }
132 |
133 | if (nopag) {
134 | return {
135 | ...success,
136 | data: dataSource.slice(0, 20),
137 | };
138 | }
139 |
140 | return {
141 | ...success,
142 | data: {
143 | list: pageData,
144 | total: filteredDataSource.length,
145 | pageSize,
146 | current,
147 | },
148 | };
149 | }
150 |
151 | const listApi = (body, nopag) => {
152 | const { filters, pageSize, current, sorter } = body;
153 | return getPagingData({ current, pageSize }, filters, sorter, nopag);
154 | };
155 |
156 | const addApi = (body) => {
157 | const { name, time = [], ...restReq } = body;
158 | dataSource.forEach(({ id }) => {
159 | if (Number(id) > Number(maxId)) {
160 | maxId = id;
161 | }
162 | });
163 | dataSource.unshift({
164 | id: String(maxId * 1 + 1),
165 | status: Random.pick(status),
166 | time: time.map((v) => moment(v).format('YYYY-MM-DD')),
167 | ...restReq,
168 | });
169 | return success;
170 | };
171 |
172 | const deleteApi = ({ rowKeys }) => {
173 | const selectedRowKeys = Array.isArray(rowKeys) ? rowKeys : [rowKeys];
174 | selectedRowKeys.forEach((id) => {
175 | dataSource = dataSource.filter((v) => v.id != id);
176 | });
177 | return success;
178 | };
179 |
180 | const detailApi = (body) => {
181 | const { id } = body;
182 | const data = dataSource.find((v) => v.id == id);
183 | return {
184 | ...success,
185 | data,
186 | };
187 | };
188 |
189 | const getList = (data) => ({
190 | ...success,
191 | data: data.map((v) => ({ text: v, value: v })),
192 | });
193 |
194 | const getPlugins = () => ({
195 | ...success,
196 | data: {
197 | id: 123454321,
198 | input: 'sula',
199 | autocomplete: 'sula',
200 | textarea: 'sula-sula',
201 | inputnumber: 123,
202 | rate: 2,
203 | slider: 10,
204 | switch: true,
205 | checkboxgroup: ['sula'],
206 | radiogroup: 'sula',
207 | select: 'sula',
208 | treeselect: '0-0-1',
209 | cascader: ['zhejiang', 'hangzhou', 'xihu'],
210 | transfer: ['0', '1'],
211 | timepicker: '2019-12-16T13:08:31.001Z',
212 | datepicker: '2019-12-17T11:06:30.005Z',
213 | rangepicker: ['2019-12-16T11:06:30.009Z', '2019-12-19T11:06:30.009Z'],
214 | upload: [
215 | {
216 | uid: 'rc-upload-1576589336277-4',
217 | lastModified: 1576318435446,
218 | lastModifiedDate: '2019-12-14T10:13:55.446Z',
219 | name: 'scatter-simple.html',
220 | size: 1823,
221 | type: 'text/html',
222 | percent: 0,
223 | originFileObj: {
224 | uid: 'rc-upload-1576589336277-4',
225 | },
226 | },
227 | ],
228 | },
229 | });
230 |
231 | function logInfo(req, data) {
232 | const { url, type, body } = req;
233 | const jsonBody = JSON.parse(body);
234 |
235 | console.log(
236 | `%c request: %c ${type} ${url}`,
237 | 'color:#f80;font-weight:bold;',
238 | 'color:#f00;',
239 | );
240 | console.log('%c params:', 'color:#f80;font-weight:bold;', jsonBody);
241 | console.log('%c response:', 'color:#f80;font-weight:bold;', data);
242 | console.log('');
243 | }
244 |
245 | mock('/api/manage/list.json', 'post', function (req) {
246 | const { body } = req;
247 | const data = listApi(JSON.parse(body));
248 | logInfo(req, data);
249 | return data;
250 | });
251 |
252 | mock('/api/manage/listnopag.json', 'post', function (req) {
253 | const { body } = req;
254 | const data = listApi(JSON.parse(body), true);
255 | return data;
256 | });
257 |
258 | mock('/api/manage/add.json', 'post', function (req) {
259 | const { body } = req;
260 | const data = addApi(JSON.parse(body));
261 | logInfo(req, data);
262 | return data;
263 | });
264 |
265 | mock('/api/manage/delete.json', 'post', function (req) {
266 | const { body } = req;
267 | const data = deleteApi(JSON.parse(body));
268 | logInfo(req, data);
269 | return data;
270 | });
271 |
272 | mock('/api/manage/detail.json', 'post', function (req) {
273 | const { body } = req;
274 | const data = detailApi(JSON.parse(body));
275 | logInfo(req, data);
276 | return data;
277 | });
278 |
279 | mock('/api/manage/statusList.json', function (req) {
280 | const data = getList(status);
281 | logInfo(req, data);
282 | return data;
283 | });
284 | mock('/api/manage/priceList.json', function (req) {
285 | const data = getList(priceProject);
286 | logInfo(req, data);
287 | return data;
288 | });
289 | mock('/api/manage/recipientList.json', function (req) {
290 | const data = getList(recipientName);
291 | logInfo(req, data);
292 | return data;
293 | });
294 | mock('/api/manage/plugins.json', 'post', function (req) {
295 | const data = getPlugins();
296 | logInfo(req, data);
297 | return data;
298 | });
299 |
300 | mock('/api/techuiplugin.json', 'post', function (req) {
301 | const data = {
302 | ...success,
303 | data: {
304 | checkcard: true,
305 | checkcardgroup: ['B'],
306 | colorpicker: ['#FF86B7', '#5B8FF9'],
307 | inputamount: { amount: 11, currency: 'Rmb' },
308 | sliderinput: 0.39,
309 | tagfilter: ['cat9'],
310 | lightfilter: [1, '23'],
311 | },
312 | };
313 | logInfo(req, data);
314 | return data;
315 | });
316 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Sula",
3 | "private": true,
4 | "description": "sula cooker",
5 | "scripts": {
6 | "start": "umi dev",
7 | "build": "umi build",
8 | "predeploy": "npm run build",
9 | "deploy": "now deploy ./dist -n cook --prod"
10 | },
11 | "dependencies": {
12 | "@monaco-editor/react": "^3.3.0",
13 | "@sula/nav": "1.0.0-alpha.2",
14 | "antd": "^4.2.5",
15 | "sula": "^1.0.0-beta.6"
16 | },
17 | "devDependencies": {
18 | "@sula/templates": "^1.0.3",
19 | "@umijs/preset-react": "^1.5.19",
20 | "acorn": "^7.2.0",
21 | "acorn-walk": "^7.1.1",
22 | "ast-types": "^0.13.3",
23 | "copy-to-clipboard": "^3.3.1",
24 | "mockjs": "^1.1.0",
25 | "umi": "^3.2.14",
26 | "umi-plugin-sula": "^1.0.0-beta.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import extraRoutes from '@/routes';
2 |
3 | export function patchRoutes({ routes }) {
4 | routes[0].routes = extraRoutes.concat(routes[0].routes);
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/configButton/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Affix, Button } from 'antd';
3 | import { SettingOutlined } from '@ant-design/icons';
4 | import ThemeContext from '@/layout/themeContext';
5 | import { Guide } from '@/components/guide';
6 |
7 | export default props => {
8 | const { onClick } = props;
9 | const theme = React.useContext(ThemeContext);
10 |
11 | if (theme.hiddenCustomControls) {
12 | return null;
13 | }
14 |
15 | const style = { position: 'fixed', top: 360, right: 24, zIndex: 1001 };
16 |
17 | return (
18 |
19 |
25 | }
28 | type="primary"
29 | onClick={onClick}
30 | />
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/configTree/component/compactArrayView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const prefixCls = 'tree-compact';
4 |
5 | export default class CompactArrayView extends React.Component {
6 | shouldComponentUpdate(nextProps) {
7 | return nextProps.array.length !== this.props.array.length;
8 | }
9 |
10 | render() {
11 | let { array } = this.props;
12 | let count = array.length;
13 |
14 | if (count === 0) {
15 | return {'[ ]'};
16 | }
17 | return (
18 |
19 | {'['}
20 | {count + ' element' + (count > 1 ? 's' : '')}
21 | {']'}
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/configTree/component/compactObjectView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const prefixCls = 'tree-compact';
4 |
5 | export default class CompactObjectView extends React.Component {
6 | shouldComponentUpdate(nextProps) {
7 | const nextLength = Object.keys(nextProps.object).length;
8 | const prevLength = Object.keys(this.props.object).length;
9 | return nextLength !== prevLength;
10 | }
11 |
12 | render() {
13 | let keys = this.props.object.map(v => v.name);
14 | let count = keys.length;
15 |
16 | if (count === 0) {
17 | return {'{ }'};
18 | }
19 | if (count > 2) {
20 | keys = keys.slice(0, 2).concat([`... +${count - 2}`]);
21 | }
22 | return (
23 |
24 | {'{'}
25 | {keys.join(', ')}
26 | {'}'}
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/configTree/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import castArray from 'lodash/castArray';
3 | import createTreeData, { getConfigNode } from './parser';
4 | import { createHighLightTreeData, clearActiveNode } from './parser/highlight';
5 | import TreeNode from './treeNode';
6 | import iconRender from './render/iconRender';
7 | import nameRender from './render/nameRender';
8 |
9 | import './index.less';
10 |
11 | export { iconRender, nameRender };
12 |
13 | function parseData(data) {
14 | let res;
15 | try {
16 | res = createTreeData(getConfigNode(data));
17 | } catch (e) {}
18 |
19 | return res;
20 | }
21 |
22 | export default props => {
23 | const {
24 | data,
25 | onToggle,
26 | onSelect,
27 | nameRender,
28 | iconRender,
29 | contextMenuRender,
30 | style,
31 | level = 0,
32 | currentLine,
33 | } = props;
34 | const [treeData, setTreeData] = useState(parseData(data));
35 | const [activeLine, setActiveLine] = useState(-1);
36 |
37 | useEffect(() => {
38 | setTreeData(parseData(data));
39 | }, [data]);
40 |
41 | useEffect(() => {
42 | if (!treeData || !treeData[0]) {
43 | setActiveLine(-1);
44 | } else {
45 | setActiveLine(currentLine);
46 | }
47 | }, [treeData, currentLine]);
48 |
49 | useEffect(() => {
50 | setTimeout(() => {
51 | setTreeData(createHighLightTreeData(treeData, activeLine));
52 | });
53 | }, [activeLine]);
54 |
55 | const handleSelect = (node, hasChildren) => {
56 | const noActiveData = clearActiveNode(treeData);
57 | setTreeData(noActiveData);
58 | onSelect(node, hasChildren);
59 | };
60 |
61 | return (
62 |
63 | {castArray(treeData).map((node, index) => {
64 | if (!node) return;
65 |
66 | return (
67 |
78 | );
79 | })}
80 |
81 | );
82 | };
83 |
--------------------------------------------------------------------------------
/src/components/configTree/index.less:
--------------------------------------------------------------------------------
1 | // hover background
2 | @hover-background: #1990ffab;
3 |
4 | .si-tree {
5 | font-size: 14px;
6 |
7 | &-node {
8 | &-block {
9 | display: flex;
10 | align-items: center;
11 | padding: 2px 4px;
12 | cursor: pointer;
13 | user-select: none;
14 | &:hover {
15 | background-color: @hover-background;
16 | color: #fff;
17 | }
18 |
19 | &-actived {
20 | background-color: @hover-background;
21 | color: #fff;
22 | }
23 | }
24 |
25 | &-switcher {
26 | margin-right: 2px;
27 | }
28 |
29 | &-icon {
30 | margin-right: 4px;
31 | display: flex;
32 | }
33 |
34 | &-name {
35 | }
36 | }
37 | }
38 |
39 | .tree-compact {
40 | margin-left: 4px;
41 | font-size: 13px;
42 | font-style: italic;
43 | color: #49aa19;
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/configTree/parser/highlight.js:
--------------------------------------------------------------------------------
1 | const isHightLight = (hightLightData, highLightLine) => {
2 | const { children, loc = [0, 0, 0, 0] } = hightLightData;
3 | const [lineStart, , lineEnd] = loc;
4 | if (children) {
5 | return isActiveObj(highLightLine, lineStart, lineEnd);
6 | }
7 | return lineStart <= highLightLine && lineEnd >= highLightLine;
8 | };
9 |
10 | const hasHighLightChildren = (hightLightData = {}, highLightLine) => {
11 | const { children, loc = [0, 0, 0, 0] } = hightLightData;
12 | const [lineStart, , lineEnd] = loc;
13 | if (children) {
14 | return (
15 | isActiveObj(highLightLine, lineStart, lineEnd) ||
16 | children.some(v => hasHighLightChildren(v, highLightLine))
17 | );
18 | }
19 | return lineStart <= highLightLine && lineEnd >= highLightLine;
20 | };
21 |
22 | function isActiveObj(highLightLine, start, end) {
23 | return highLightLine === start || highLightLine === end;
24 | }
25 |
26 | function createHighLightTreeData(treeData, highLightLine) {
27 | const data = Array.isArray(treeData) ? [...treeData] : [treeData];
28 |
29 | data.forEach(node => {
30 | if (!node) return;
31 | node.toggled = hasHighLightChildren(node, highLightLine);
32 | node.active = isHightLight(node, highLightLine);
33 | if (node.children) {
34 | if (node.active) {
35 | node.children = clearActiveNode(node.children);
36 | } else {
37 | node.children = createHighLightTreeData(node.children, highLightLine);
38 | }
39 | }
40 | });
41 |
42 | return data;
43 | }
44 |
45 | function clearActiveNode(treeData) {
46 | const data = Array.isArray(treeData) ? [...treeData] : [treeData];
47 | data.forEach(node => {
48 | if (!node) return;
49 | node.active = false;
50 | if (node.children) {
51 | node.children = clearActiveNode(node.children);
52 | }
53 | });
54 | return data;
55 | }
56 |
57 | export { createHighLightTreeData, clearActiveNode };
58 |
--------------------------------------------------------------------------------
/src/components/configTree/parser/index.js:
--------------------------------------------------------------------------------
1 | import { parse } from '@babel/parser';
2 | import { visit } from 'ast-types';
3 |
4 | function getConfigNode(data) {
5 | let configNode;
6 |
7 | visit(parse(data), {
8 | visitIdentifier: path => {
9 | if (configNode) {
10 | return false;
11 | }
12 | const name = path.getValueProperty('name');
13 | if (name === 'config') {
14 | const node = path.parentPath.getValueProperty('init');
15 | if (node) {
16 | configNode = node;
17 | }
18 | }
19 | return false;
20 | },
21 | });
22 |
23 | return configNode;
24 | }
25 |
26 | export { getConfigNode };
27 |
28 | function locToPositon(data) {
29 | const { start, end } = data;
30 | const { line: startLine, column: startCol } = start;
31 | const { line: endLine, column: endCol } = end;
32 | const position = [startLine, startCol + 1, endLine, endCol + 1];
33 | return position;
34 | }
35 |
36 | export default function createTreeData(data) {
37 | if (!data) return;
38 |
39 | const { loc, properties = [] } = data;
40 | const astTree = properties.map(node => {
41 | if (!node) return;
42 | const { key, value, loc, type } = node;
43 | // 直接使用value类型 字符串 数值 布尔值
44 | const valueType = [
45 | 'StringLiteral',
46 | 'NumericLiteral',
47 | 'BooleanLiteral',
48 | 'ConditionalExpression',
49 | 'MemberExpression',
50 | ];
51 | const objectType = ['ObjectExpression'];
52 | const arrayType = ['ArrayExpression'];
53 | const funcType = ['ArrowFunctionExpression', 'FunctionExpression'];
54 | const paramsType = ['Identifier', 'ThisExpression'];
55 |
56 | const position = locToPositon(loc);
57 |
58 | const name = key?.name || key?.value;
59 |
60 | // 方法的快捷表示法
61 | if (type === 'ObjectMethod') {
62 | return {
63 | name,
64 | value: null,
65 | loc: position,
66 | };
67 | }
68 |
69 | if (valueType.includes(value.type)) {
70 | return {
71 | name,
72 | value: value.value,
73 | loc: position,
74 | };
75 | }
76 |
77 | if (objectType.includes(value.type)) {
78 | return {
79 | name,
80 | value: null,
81 | loc: position,
82 | children: createTreeData(value),
83 | type: 'object',
84 | };
85 | }
86 |
87 | if (arrayType.includes(value.type)) {
88 | return {
89 | name,
90 | value: null,
91 | loc: position,
92 | children: value.elements.map((v, idx) => {
93 | return {
94 | name: idx,
95 | value: null,
96 | type: 'object',
97 | loc: locToPositon(v.loc),
98 | children: createTreeData(v),
99 | };
100 | }),
101 | type: 'array',
102 | };
103 | }
104 |
105 | if (funcType.includes(value.type)) {
106 | return {
107 | name,
108 | value: null,
109 | loc: position,
110 | };
111 | }
112 |
113 | if (paramsType.includes(value.type)) {
114 | return {
115 | name,
116 | value: null,
117 | loc: position,
118 | };
119 | }
120 | });
121 |
122 | return astTree;
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/configTree/render/iconRender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CompactArrayView from '../component/compactArrayView';
3 | import CompactObjectView from '../component/compactObjectView';
4 |
5 | function iconRender(data) {
6 | const { name, type, children, toggled } = data;
7 | if (toggled) {
8 | return name;
9 | }
10 | if (type === 'array') {
11 | return (
12 |
13 | {name}
14 |
15 |
16 | );
17 | }
18 | if (type === 'object') {
19 | return (
20 |
21 | {name}
22 |
23 |
24 | );
25 | }
26 | return name;
27 | }
28 |
29 | export default iconRender;
30 |
--------------------------------------------------------------------------------
/src/components/configTree/render/nameRender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default data => {
4 | const { type, name } = data;
5 |
6 | const pluginTypes = ['render', 'action', 'type', 'container', 'validator'];
7 | if (pluginTypes.includes(name)) {
8 | return ;
9 | }
10 |
11 | if (name === 'fields' || name === 'columns') {
12 | return (
13 |
14 | );
15 | }
16 |
17 | if (typeof name === 'number' && type === 'object') {
18 | return ;
19 | }
20 |
21 | return
;
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/configTree/treeNode.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import isArray from 'lodash/isArray';
3 | import cx from 'classnames';
4 |
5 | function TreeNode(props) {
6 | const {
7 | data,
8 | nameRender,
9 | iconRender,
10 | level,
11 | contextMenuRender,
12 | activeLine,
13 | } = props;
14 | const [nodeData, setNodeData] = useState(data);
15 | const isDirectory = !!nodeData.children;
16 |
17 | const handleClick = () => {
18 | const { onToggle, onSelect } = props;
19 | const newNodeData = { ...nodeData };
20 | // newNodeData.active = true;
21 | if (newNodeData.children) {
22 | newNodeData.toggled = !nodeData.toggled;
23 | }
24 | setNodeData(newNodeData);
25 | if (onSelect) {
26 | onSelect(newNodeData, !!nodeData.children);
27 | }
28 | };
29 |
30 | useEffect(() => {
31 | setNodeData(data);
32 | }, [activeLine]);
33 |
34 | const renderChildren = level => {
35 | const {
36 | onToggle,
37 | onSelect,
38 | nameRender,
39 | iconRender,
40 | contextMenuRender,
41 | } = props;
42 |
43 | if (nodeData.loading) {
44 | return loading...;
45 | }
46 |
47 | let children = nodeData.children;
48 | if (!isArray(children)) {
49 | children = children ? [children] : [];
50 | }
51 |
52 | return (
53 |
54 | {children.map((child, index) => (
55 |
66 | ))}
67 |
68 | );
69 | };
70 |
71 | let blockNode = (
72 |
79 |
85 | {isDirectory ? (
86 |
92 | ) : (
93 |
94 | )}
95 | {iconRender ? (
96 |
{iconRender(nodeData)}
97 | ) : null}
98 |
99 | {nameRender ? nameRender(nodeData) : nodeData.name}
100 |
101 |
102 | );
103 |
104 | if (contextMenuRender) {
105 | blockNode = contextMenuRender(nodeData, blockNode);
106 | }
107 |
108 | return (
109 |
110 | {blockNode}
111 | {nodeData.toggled ? renderChildren(level + 1) : null}
112 |
113 | );
114 | }
115 |
116 | export default TreeNode;
117 |
--------------------------------------------------------------------------------
/src/components/drawer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Drawer } from 'antd';
3 |
4 | export default props => {
5 | const { children, ...restProps } = props;
6 |
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/guide/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 |
3 | import style from './index.less';
4 | import { Space, Button, Popover } from 'antd';
5 | import ThemeContext, { GUIDE, DONE } from '@/layout/themeContext';
6 |
7 | function getElementLeft(element) {
8 | let actualLeft = element.offsetLeft;
9 | let current = element.offsetParent;
10 |
11 | while (current !== null) {
12 | actualLeft += current.offsetLeft;
13 | current = current.offsetParent;
14 | }
15 |
16 | return actualLeft;
17 | }
18 | function getElementTop(element) {
19 | let actualTop = element.offsetTop;
20 | let current = element.offsetParent;
21 |
22 | while (current !== null) {
23 | actualTop += current.offsetTop;
24 | current = current.offsetParent;
25 | }
26 |
27 | return actualTop;
28 | }
29 |
30 | export default props => {
31 | const { children } = props;
32 | const containerRef = useRef();
33 | const [visible, setVisible] = useState(true);
34 | const [guideNodes, setGuideNodes] = useState([]);
35 | const [activeIdx, setActiveIdx] = useState(0);
36 | const theme = React.useContext(ThemeContext);
37 |
38 | useEffect(() => {
39 | setTimeout(() => {
40 | setGuideNodes(markDom());
41 | });
42 | window.addEventListener('resize', onWindowResize, false);
43 | return () => {
44 | window.removeEventListener('resize', onWindowResize, false);
45 | };
46 | }, []);
47 |
48 | function onWindowResize() {
49 | setGuideNodes(markDom());
50 | }
51 |
52 | const markDom = () => {
53 | const nodeList = [...document.querySelectorAll('[data-guide-step]')].sort(
54 | (p, c) => p.dataset.guideStep - c.dataset.guideStep,
55 | );
56 |
57 | const dots = nodeList.map(node => {
58 | const height = node.clientHeight || node.offsetHeight;
59 | const width = node.clientWidth || node.offsetWidth;
60 | const left = getElementLeft(node);
61 | const top = getElementTop(node);
62 |
63 | return {
64 | left: left - 10,
65 | top: top - 10,
66 | width: width + 20,
67 | height: height + 20,
68 | tips: node.getAttribute('data-guide-tips'),
69 | step: node.getAttribute('data-guide-step'),
70 | snapshot: node.getAttribute('data-guide-snapshot'),
71 | };
72 | });
73 |
74 | return dots;
75 | };
76 |
77 | function closeGuideTips() {
78 | theme.toggleGuideTips(true);
79 | localStorage.setItem(GUIDE, DONE);
80 | }
81 |
82 | const onSkip = () => {
83 | setActiveIdx(-1);
84 | closeGuideTips();
85 | };
86 |
87 | const onNext = () => {
88 | setActiveIdx(activeIdx + 1);
89 | if (activeIdx === guideNodes.length - 1) {
90 | closeGuideTips();
91 | }
92 | };
93 |
94 | const onPrev = () => {
95 | setActiveIdx(activeIdx - 1);
96 | };
97 |
98 | useEffect(() => {
99 | setVisible(activeIdx > -1 && activeIdx < guideNodes.length);
100 | }, [activeIdx, guideNodes]);
101 |
102 | const renderShallow = () => {
103 | return (
104 |
105 |
106 | {guideNodes.map((node, idx, { length }) => {
107 | const { tips, step, snapshot, ...nodeStyle } = node;
108 | const content = (
109 |
110 |
111 |

112 |
113 |
114 |
117 |
118 |
125 |
128 |
129 |
130 |
131 | );
132 | return (
133 |
150 | );
151 | })}
152 |
153 | );
154 | };
155 |
156 | return (
157 |
158 | {children}
159 | {visible && renderShallow()}
160 |
161 | );
162 | };
163 |
164 | export function Guide(props) {
165 | const {
166 | children,
167 | step,
168 | tips = '',
169 | snapshot = 'https://img.alicdn.com/tfs/TB1_e8_H1L2gK0jSZPhXXahvXXa-696-272.png',
170 | ...rest
171 | } = props;
172 | const theme = React.useContext(ThemeContext);
173 |
174 | if (theme.hiddenGuideTips) {
175 | return children;
176 | }
177 |
178 | return (
179 |
186 | {children}
187 |
188 | );
189 | }
190 |
--------------------------------------------------------------------------------
/src/components/guide/index.less:
--------------------------------------------------------------------------------
1 | .activeNode {
2 | position: absolute;
3 | z-index: 10000;
4 | border: 1px solid #000;
5 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.4);
6 | border-radius: 4px;
7 | }
8 |
9 | .shadow {
10 | position: fixed;
11 | left: 0;
12 | right: 0;
13 | top: 0;
14 | bottom: 0;
15 | background-color: #000;
16 | opacity: 0.5;
17 | z-index: 100;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import {
3 | Typography,
4 | Button,
5 | message,
6 | Popconfirm,
7 | Tooltip,
8 | Row,
9 | Col,
10 | Space,
11 | } from 'antd';
12 | import copy from 'copy-to-clipboard';
13 | import Editor, { monaco } from '@monaco-editor/react';
14 | import serialize, { deserialize } from '@/utils/serialize';
15 | import {
16 | BulbOutlined,
17 | PlayCircleOutlined,
18 | DeleteOutlined,
19 | CopyOutlined,
20 | FullscreenOutlined,
21 | FullscreenExitOutlined,
22 | } from '@ant-design/icons';
23 | import validateSulaConfig from './lint';
24 | import * as acorn from 'acorn';
25 | import * as walk from 'acorn-walk';
26 | import ConfigTree, { iconRender, nameRender } from '@/components/configTree';
27 | import registerSuggestions from './suggestions';
28 | import style from './index.less';
29 |
30 | const { Title } = Typography;
31 | const STRTEMP = 'const config = ';
32 |
33 | const getEditorValue = data => {
34 | return `${STRTEMP}${serialize(data, { space: 2, unsafe: true })}`;
35 | };
36 |
37 | let isRegister;
38 | let decorations = [];
39 |
40 | export default props => {
41 | const monacoRef = React.useRef(null);
42 | const {
43 | onRun,
44 | value,
45 | type,
46 | onFullScreen = () => {},
47 | shallowHeight = 0,
48 | } = props;
49 |
50 | const editorRef = React.useRef(null);
51 | const [highLightLine, setHighLightLine] = React.useState();
52 | const [isFull, setFull] = React.useState(false);
53 |
54 | const finalValue = getEditorValue(value);
55 |
56 | const [treeData, setTreeData] = React.useState(finalValue);
57 |
58 | const handleCopy = () => {
59 | const jsonEditorValue = editorRef.current.getValue();
60 | const val = serialize(jsonEditorValue, { space: 2 });
61 | copy(deserialize(val).slice(STRTEMP.length));
62 | message.success('JSON Schema Copied 🎉');
63 | };
64 |
65 | const formatDocument = () => {
66 | setTimeout(() => {
67 | editorRef.current &&
68 | editorRef.current.getAction('editor.action.formatDocument').run();
69 | }, 300);
70 | };
71 |
72 | monaco.init().then(ref => {
73 | monacoRef.current = ref;
74 | if (!isRegister) {
75 | registerSuggestions(ref);
76 | isRegister = true;
77 | }
78 | });
79 |
80 | const onEditorDidMount = (_monaco, editor) => {
81 | editorRef.current = editor;
82 | const model = editor.getModel();
83 |
84 | formatDocument();
85 |
86 | editor.onKeyDown(e => {
87 | if (e.shiftKey) {
88 | editorRef.current &&
89 | editorRef.current.trigger(
90 | 'auto completion',
91 | 'editor.action.triggerSuggest',
92 | );
93 | }
94 | });
95 |
96 | editor.onDidChangeCursorPosition(e => {
97 | const lineCount = editor.getModel().getLineCount();
98 | if (e.position.lineNumber === 1) {
99 | editor.setPosition({
100 | lineNumber: 2,
101 | column: 1,
102 | });
103 | } else if (e.position.lineNumber === lineCount) {
104 | editor.setPosition({
105 | lineNumber: lineCount - 1,
106 | column: 1,
107 | });
108 | }
109 | });
110 |
111 | editor.onMouseDown(event => {
112 | clearDecorations();
113 | onEditorLineChange(event.target.position);
114 | });
115 |
116 | editor.onKeyDown(event => {
117 | setTimeout(() => {
118 | const position = editor.getPosition();
119 | onEditorLineChange(position);
120 | setTreeData(editor.getValue());
121 | });
122 | });
123 |
124 | editor.onDidChangeModelContent(() => {
125 | const content = editor.getValue();
126 | const markers = monacoRef.current.editor.getModelMarkers();
127 |
128 | if (markers.length > 0) {
129 | markers.forEach(marker => {
130 | monacoRef.current.editor.setModelMarkers(
131 | editor.getModel(),
132 | marker.owner,
133 | [],
134 | );
135 | });
136 | }
137 | const ast = acorn.parse(content, { locations: true });
138 | walk.full(ast, node => {
139 | // if (node?.loc?.start?.column < 5) {
140 | validateSulaConfig(node, monacoRef.current, editor);
141 | // }
142 | });
143 | });
144 | };
145 |
146 | function onEditorLineChange(position) {
147 | const { lineNumber } = position || {};
148 | setHighLightLine(lineNumber);
149 | }
150 |
151 | // console.log(`点击字段: ${clickType}, 点击字段所属字段: ${type}, 提示: ${suggestionType}`);
152 |
153 | const onClickRun = () => {
154 | const value = editorRef.current.getValue().slice(STRTEMP.length);
155 |
156 | try {
157 | const res = deserialize(value);
158 | onRun(res);
159 | formatDocument();
160 | setFull(false);
161 | message.success(`Run Success 🎉`);
162 | } catch (e) {
163 | message.error(`JSON 格式错误`);
164 | }
165 | };
166 |
167 | const onClickDelete = () => {
168 | editorRef.current.setValue(`${STRTEMP}{\n\n}`);
169 | };
170 |
171 | function clearDecorations() {
172 | if (decorations.length) {
173 | decorations = editorRef.current.deltaDecorations(decorations, []);
174 | }
175 | }
176 |
177 | function setDecorations(range) {
178 | const [startLineNumber, startColumn, endLineNumber, endColumn] = range;
179 | const newDecorations = editorRef.current.deltaDecorations(
180 | [],
181 | [
182 | {
183 | range: {
184 | endColumn,
185 | endLineNumber,
186 | startColumn,
187 | startLineNumber,
188 | },
189 | options: {
190 | className: 'si-editor-highlight',
191 | },
192 | },
193 | ],
194 | );
195 | decorations = decorations.concat(newDecorations);
196 | }
197 |
198 | const handleSelect = (node, hasChildren) => {
199 | const { loc, name } = node;
200 | clearDecorations();
201 | // 高亮
202 | if (hasChildren) {
203 | const [line, startColumn] = loc;
204 | const keyPosition = [line, startColumn, line, 100];
205 | setDecorations(keyPosition);
206 | } else {
207 | setDecorations(loc);
208 | }
209 | editorRef.current.revealLine(loc[0]); // 行跳转
210 | };
211 |
212 | const handleFullScreen = () => {
213 | setFull(true);
214 | };
215 |
216 | const handleExitFullScreen = () => {
217 | setFull(false);
218 | };
219 |
220 | useEffect(() => {
221 | editorRef?.current?.layout();
222 | onFullScreen && onFullScreen(isFull);
223 | }, [isFull]);
224 |
225 | useEffect(() => {
226 | editorRef?.current?.layout();
227 | }, [shallowHeight]);
228 |
229 | const hasConfigTree = type !== 'editor';
230 |
231 | return (
232 |
233 |
234 |
235 | 代码展示
236 |
237 |
238 |
241 |
242 |
243 |
244 | }
249 | onClick={onClickRun}
250 | shape="circle"
251 | />
252 |
253 |
254 |
255 | }
258 | size="small"
259 | shape="circle"
260 | />
261 |
262 |
263 |
264 | }
267 | onClick={handleCopy}
268 | style={{
269 | border: 'none',
270 | }}
271 | shape="circle"
272 | size="small"
273 | />
274 |
275 |
276 |
277 | }
280 | onClick={handleExitFullScreen}
281 | style={{
282 | border: 'none',
283 | display: !isFull ? 'none' : '',
284 | }}
285 | shape="circle"
286 | size="small"
287 | />
288 |
289 |
290 | }
293 | onClick={handleFullScreen}
294 | style={{
295 | border: 'none',
296 | display: isFull ? 'none' : '',
297 | }}
298 | shape="circle"
299 | size="small"
300 | />
301 |
302 |
303 |
304 |
305 |
306 |
307 |
317 |
318 | {hasConfigTree && (
319 |
320 | 属性节点树
321 |
333 |
334 | )}
335 |
336 |
337 | );
338 | };
339 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/index.less:
--------------------------------------------------------------------------------
1 | :global {
2 | .si-editor-highlight {
3 | background: #1990ffab;
4 | }
5 | }
6 |
7 | .title {
8 | color: #1890ff;
9 | padding: 12px;
10 | font-weight: bold;
11 | }
12 |
13 | .editorWrapper {
14 | position: fixed;
15 | left: 0;
16 | right: 0;
17 | top: 0;
18 | bottom: 0;
19 | padding: 24px;
20 | background-color: #fff;
21 | z-index: 10000;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/lint/basicType.js:
--------------------------------------------------------------------------------
1 | import { isString, isNumber, isBoolean, lowerCase } from 'lodash';
2 |
3 | const getData = data => data.value || data;
4 |
5 | /**
6 | *
7 | * 其他类型暂时忽略
8 | */
9 |
10 | const isIgnoreType = node => {
11 | return getData(node).type === 'ConditionalExpression';
12 | };
13 |
14 | /**
15 | * 基础类型
16 | */
17 | export const isStringType = node => {
18 | if (isIgnoreType(node)) {
19 | return true;
20 | }
21 | return isString(getData(node).value);
22 | };
23 |
24 | export const isArrayType = node => {
25 | if (isIgnoreType(node)) {
26 | return true;
27 | }
28 | return getData(node).type === 'ArrayExpression';
29 | };
30 |
31 | export const isNumberType = node => {
32 | if (isIgnoreType(node)) {
33 | return true;
34 | }
35 | return isNumber(getData(node).value);
36 | };
37 |
38 | export const isBooleanType = node => {
39 | if (isIgnoreType(node)) {
40 | return true;
41 | }
42 | return isBoolean(getData(node).value);
43 | };
44 |
45 | export const isObjectType = node => {
46 | if (isIgnoreType(node)) {
47 | return true;
48 | }
49 | return getData(node).type === 'ObjectExpression';
50 | };
51 |
52 | export const isEmptyObjectType = node => {
53 | if (isIgnoreType(node)) {
54 | return true;
55 | }
56 | return isObjectType(node) && !getData(node).value;
57 | };
58 |
59 | export const isFunctionType = node => {
60 | if (isIgnoreType(node)) {
61 | return true;
62 | }
63 | return (
64 | getData(node).type === 'ArrowFunctionExpression' ||
65 | getData(node).type === 'FunctionExpression' ||
66 | getData(node).type === 'ObjectMethod'
67 | );
68 | };
69 |
70 | /**
71 | * field
72 | */
73 |
74 | export const isNameType = node => {
75 | return isStringType(node) || isNumberType(node) || isArrayType(node);
76 | };
77 |
78 | export const isFieldRenderType = node => {
79 | return isStringType(node) || isObjectType(node) || isFunctionType(node);
80 | };
81 |
82 | export const isLayoutType = node => {
83 | if (isIgnoreType(node)) {
84 | return true;
85 | }
86 | return ['vertical', 'horizontal', 'inline'].indexOf(getData(node).value) > -1;
87 | };
88 |
89 | export const isModeType = node => {
90 | // console.log(getv(node).value);
91 | if (isIgnoreType(node)) {
92 | return true;
93 | }
94 | return ['view', 'create', 'edit'].indexOf(getData(node).value) > -1;
95 | };
96 |
97 | /**
98 | * 通用
99 | */
100 | export const isFetchType = node => {
101 | const { properties } = getData(node);
102 | if (!properties.length) return false;
103 | return properties.some(item => {
104 | const { key, value } = item || {};
105 | if ((key.name || key.value) === 'url') {
106 | return true;
107 | }
108 | });
109 | };
110 |
111 | export const isMethodType = node => {
112 | if (isIgnoreType(node)) {
113 | return true;
114 | }
115 | return ['post', 'get', 'delete'].indexOf(lowerCase(getData(node).value)) > -1;
116 | };
117 |
118 | /**
119 | * table
120 | */
121 |
122 | export const isColumnRender = node => {
123 | if (isIgnoreType(node)) {
124 | return true;
125 | }
126 | return (
127 | isStringType(node) ||
128 | isObjectType(node) ||
129 | isArrayType(node) ||
130 | isFunctionType(node)
131 | );
132 | };
133 |
134 | /**
135 | * action
136 | */
137 | export const isActionType = node => {
138 | if (isIgnoreType(node)) {
139 | return true;
140 | }
141 | return (
142 | isStringType(node) ||
143 | isObjectType(node) ||
144 | isArrayType(node) ||
145 | isFunctionType(node)
146 | );
147 | };
148 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/lint/index.js:
--------------------------------------------------------------------------------
1 | import * as basicTypes from './basicType';
2 | let monaco;
3 | let editor;
4 |
5 | const {
6 | isArrayType,
7 | isStringType,
8 | isNumberType,
9 | isBooleanType,
10 | isNameType,
11 | isFieldRenderType,
12 | isLayoutType,
13 | isObjectType,
14 | isMethodType,
15 | isFunctionType,
16 | isModeType,
17 | isColumnRender,
18 | isActionType,
19 | } = basicTypes;
20 |
21 | const errorMarks = (node, message = '类型错误') => {
22 | const name = node?.key?.name || node?.key?.value || node.value || '';
23 | const { loc } = node;
24 |
25 | monaco.editor.setModelMarkers(editor.getModel(), 'propFunc', [
26 | {
27 | startLineNumber: loc.start.line,
28 | startColumn: loc.start.column + 1,
29 | endLineNumber: loc.end.line,
30 | endColumn: loc.end.column + 1,
31 | message: `${name} ${message}`,
32 | severity: monaco.MarkerSeverity.Error,
33 | },
34 | ]);
35 | };
36 |
37 | function requiredValidate(field, name) {
38 | const nameList = Array.isArray(name) ? name : [name];
39 | const { properties } = field;
40 | if (!properties) return;
41 | if (
42 | !properties.some(item => {
43 | const itemName = item?.key?.name || item?.key?.value || item.value;
44 | return nameList.some(v => itemName === v);
45 | })
46 | ) {
47 | errorMarks(field, `缺少必填项${name}`);
48 | }
49 | }
50 |
51 | export default (data, _monaco, _editor) => {
52 | if (!data) return;
53 | monaco = _monaco;
54 | editor = _editor;
55 | const { properties = [] } = data;
56 |
57 | properties.forEach(node => {
58 | const { key } = node;
59 | const name = key?.name || key?.value;
60 | if (node?.loc?.start?.column < 5) {
61 | // 仅对最外层校验,内部校验直接处理外部字段
62 | validate(node, name);
63 | }
64 | });
65 | };
66 |
67 | function validate(node, name) {
68 | /**
69 | * fields字段校验
70 | */
71 | const fieldTypes = ['fields'];
72 | if (fieldTypes.includes(name)) {
73 | if (!isArrayType(node)) {
74 | errorMarks(node, '应为数组类型');
75 | return;
76 | }
77 | const { elements } = node.value || {};
78 | elements.forEach(field => {
79 | if (!isObjectType(field)) {
80 | errorMarks(field, '应为对象类型');
81 | return;
82 | }
83 |
84 | const { properties } = field;
85 | if (!properties) return;
86 | if (
87 | !properties.some(item => {
88 | const itemName = item?.key?.name || item?.key?.value || item.value;
89 | return itemName === 'fields';
90 | })
91 | ) {
92 | requiredValidate(field, 'name');
93 | requiredValidate(field, ['field', 'render']);
94 | }
95 |
96 | properties.forEach(item => {
97 | const itemName = item?.key?.name || item?.key?.value || item.value;
98 | if (itemName === 'name' && !isNameType(item)) {
99 | errorMarks(item);
100 | return;
101 | }
102 | if (itemName === 'label') {
103 | if (!isStringType(item)) {
104 | errorMarks(item);
105 | return;
106 | }
107 | }
108 | if (itemName === 'field') {
109 | if (!isFieldRenderType(item)) {
110 | errorMarks(item);
111 | return;
112 | }
113 | if (isObjectType(item)) {
114 | const { properties: renderProperties } = item.value;
115 | if (!renderProperties) return;
116 | renderProperties.forEach(renderItem => {
117 | const renderItemName =
118 | renderItem.key.name || renderItem.key.value;
119 | if (renderItemName === 'type') {
120 | if (!isStringType(renderItem) && !isFunctionType(renderItem)) {
121 | errorMarks(renderItem);
122 | return;
123 | }
124 | }
125 | });
126 | }
127 | }
128 |
129 | if (itemName === 'valuePropName' && !isStringType(item)) {
130 | errorMarks(item);
131 | return;
132 | }
133 | if (itemName === 'rules' && !isArrayType(item)) {
134 | errorMarks(item);
135 | return;
136 | }
137 | if (itemName === 'wrapFormItem' && !isBooleanType(item)) {
138 | errorMarks(item);
139 | return;
140 | }
141 | if (itemName === 'initialDisabled' && !isBooleanType(item)) {
142 | errorMarks(item);
143 | return;
144 | }
145 | if (itemName === 'initialVisible' && !isBooleanType(item)) {
146 | errorMarks(item);
147 | return;
148 | }
149 | if (itemName === 'dependency') {
150 | if (!isObjectType(item)) {
151 | errorMarks(item);
152 | return;
153 | }
154 | const { properties: depProperties } = item.value;
155 | if (!depProperties) return;
156 | depProperties.forEach(depItem => {
157 | const depName = depItem.key.name || depItem.key.value;
158 | const depType = ['value', 'source', 'disabled', 'visible'];
159 | if (!depType.includes(depName)) {
160 | errorMarks(
161 | depType,
162 | 'dependency 只可以包含value source disabled visible四种类型',
163 | );
164 | return;
165 | }
166 | if (!isObjectType(depItem)) {
167 | errorMarks(depItem);
168 | return;
169 | }
170 | const { properties: depItemProperties } = depItem.value;
171 | if (!depItemProperties) return;
172 | depItemProperties.forEach(depNode => {
173 | const depNodeName = depNode.key.name || depNode.key.value;
174 | const allowDepTypes = [
175 | 'relates',
176 | 'inputs',
177 | 'cases',
178 | 'ignores',
179 | 'type',
180 | 'output',
181 | 'defaultOutput',
182 | ];
183 | if (!allowDepTypes.includes(depNodeName)) {
184 | errorMarks(depNode, '多余类型');
185 | return;
186 | }
187 |
188 | if (depNodeName === 'relates') {
189 | if (!isArrayType(depNode)) {
190 | errorMarks(depNode);
191 | return;
192 | }
193 | }
194 | if (depNodeName === 'inputs') {
195 | if (!isArrayType(depNode)) {
196 | errorMarks(depNode);
197 | return;
198 | }
199 | }
200 | if (depNodeName === 'cases') {
201 | if (!isArrayType(depNode)) {
202 | errorMarks(depNode);
203 | return;
204 | }
205 | }
206 | if (depNodeName === 'ignores') {
207 | if (!isArrayType(depNode)) {
208 | errorMarks(depNode);
209 | return;
210 | }
211 | }
212 | });
213 | });
214 | }
215 |
216 | if (itemName === 'render' && !isFieldRenderType(item)) {
217 | errorMarks(item);
218 | return;
219 | }
220 | });
221 | });
222 | }
223 |
224 | /**
225 | * layout
226 | */
227 | if (name === 'layout') {
228 | if (!isLayoutType(node)) {
229 | errorMarks(node);
230 | return;
231 | }
232 | }
233 |
234 | /**
235 | * 请求校验
236 | */
237 | const fetchType = [
238 | 'remoteSource',
239 | 'remoteDataSource',
240 | 'remoteValues',
241 | 'submit',
242 | ];
243 | if (fetchType.includes(name)) {
244 | if (!isObjectType(node)) {
245 | errorMarks(node, '应为对象类型');
246 | return;
247 | }
248 | requiredValidate(node.value, 'url');
249 | const { properties } = node.value;
250 | if (!properties) return;
251 | properties.forEach(item => {
252 | const itemName = item.key.name || item.key.value;
253 | if (itemName === 'url' && !isStringType(item)) {
254 | errorMarks(item);
255 | return;
256 | }
257 | if (itemName === 'method' && !isMethodType(item)) {
258 | errorMarks(item);
259 | return;
260 | }
261 | if (itemName === 'params' && !isObjectType(item)) {
262 | errorMarks(item);
263 | return;
264 | }
265 | if (itemName === 'extraParams' && !isObjectType(item)) {
266 | errorMarks(item);
267 | return;
268 | }
269 | if (itemName === 'convertParams' && !isFunctionType(item)) {
270 | errorMarks(item);
271 | return;
272 | }
273 | if (itemName === 'converter' && !isFunctionType(item)) {
274 | errorMarks(item);
275 | return;
276 | }
277 | });
278 | }
279 |
280 | /**
281 | * columns
282 | */
283 | if (name === 'columns') {
284 | if (!isArrayType(node)) {
285 | errorMarks(node);
286 | return;
287 | }
288 |
289 | const { elements } = node.value || {};
290 | elements.forEach(column => {
291 | if (!isObjectType(column)) {
292 | errorMarks(column, '应为对象类型');
293 | return;
294 | }
295 |
296 | const { properties } = column;
297 | if (!properties) return;
298 | properties.forEach(item => {
299 | const itemName = item?.key?.name || item?.key?.value || item.value;
300 | if (itemName === 'key') {
301 | if (!isStringType(item)) {
302 | errorMarks(item, '应为字符串类型');
303 | return;
304 | }
305 | }
306 | if (itemName === 'title') {
307 | if (!isStringType(item)) {
308 | errorMarks(item, '应为字符串类型');
309 | return;
310 | }
311 | }
312 | if (itemName === 'render') {
313 | if (!isColumnRender(item)) {
314 | errorMarks(item);
315 | return;
316 | }
317 | }
318 | });
319 | });
320 | }
321 |
322 | /**
323 | * mode
324 | */
325 |
326 | if (name === 'mode') {
327 | if (!isModeType(node)) {
328 | errorMarks(node, 'mode可选类型为view create edit');
329 | return;
330 | }
331 | }
332 |
333 | /**
334 | * action
335 | */
336 | if (name === 'actionsRender' || name === 'leftActionsRender') {
337 | if (!isActionType(node)) {
338 | errorMarks(node);
339 | return;
340 | }
341 | }
342 |
343 | /**
344 | * formItem
345 | */
346 | if (name === 'itemLayout') {
347 | if (!isObjectType(node)) {
348 | errorMarks(node);
349 | return;
350 | }
351 |
352 | const { properties } = node.value;
353 | properties.forEach(item => {
354 | const itemName = item?.key?.name || item?.key?.value || item.value;
355 | if (itemName === 'span') {
356 | if (!isNumberType(item)) {
357 | errorMarks(item);
358 | return;
359 | }
360 | }
361 | if (itemName === 'gutter') {
362 | if (!isNumberType(item)) {
363 | errorMarks(item);
364 | return;
365 | }
366 | }
367 | if (itemName === 'offset') {
368 | if (!isNumberType(item)) {
369 | errorMarks(item);
370 | return;
371 | }
372 | }
373 |
374 | if (itemName === 'labelCol') {
375 | if (!isObjectType(item)) {
376 | errorMarks(item);
377 | return;
378 | }
379 | }
380 | if (itemName === 'wrapperCol') {
381 | if (!isObjectType(item)) {
382 | errorMarks(item);
383 | return;
384 | }
385 | }
386 | });
387 | }
388 | }
389 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/suggestions/actions.js:
--------------------------------------------------------------------------------
1 | export default monaco => [
2 | {
3 | type: 'action',
4 | label: 'back',
5 | detail: '返回上一级',
6 | kind: monaco.languages.CompletionItemKind.Property,
7 | insertTextRules:
8 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
9 | insertText: "'back'",
10 | documentation: {
11 | value: ``,
12 | },
13 | },
14 | {
15 | type: 'action',
16 | label: 'request',
17 | detail: '请求',
18 | kind: monaco.languages.CompletionItemKind.Property,
19 | insertTextRules:
20 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
21 | insertText:
22 | '{\n type: "request",\n url: "${1:\u002Fsula.json}",\n method: "${2:get}",\n},',
23 | documentation: {
24 | value: ``,
25 | },
26 | },
27 | {
28 | type: 'action',
29 | label: 'forward',
30 | detail: '前进一级',
31 | kind: monaco.languages.CompletionItemKind.Property,
32 | insertTextRules:
33 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
34 | insertText: "'forward'",
35 | documentation: {
36 | value: ``,
37 | },
38 | },
39 | {
40 | type: 'action',
41 | label: 'modalform',
42 | detail: '弹框表单插件',
43 | kind: monaco.languages.CompletionItemKind.Property,
44 | insertTextRules:
45 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
46 | insertText:
47 | '{\n type: "modalform",\n title: "Title",\n mode: "edit",\n fields: [\n {\n name: "input",\n label: "input",\n field: {\n type: "input",\n props: {\n placeholder: "请输入"\n }\n },\n rules: [\n {\n required: true,\n message: "请输入"\n }\n ]\n }\n ],\n remoteValues: {\n url: "\u002Fdetail.json",\n method: "post",\n params: {\n id: 1,\n },\n },\n submit: {\n url: "\u002Fadd.json",\n method: "post"\n },\n},',
48 | documentation: {
49 | value: `
50 | 属性名 | 描述 | 类型
51 | ---|:--:|---:
52 | fields | 表单配置 | -
53 | title | 弹框标题 | -
54 | mode | 表单模式 | -
55 | remoteValues | 远程表单值的请求配置 | -
56 | submit | 提交表单数据的请求配置 | -
57 | `,
58 | },
59 | },
60 | {
61 | type: 'action',
62 | label: 'drawerform',
63 | detail: '弹框表单插件',
64 | kind: monaco.languages.CompletionItemKind.Property,
65 | insertTextRules:
66 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
67 | insertText:
68 | '{\n type: "drawerform",\n title: "Title",\n mode: "edit",\n fields: [\n {\n name: "input",\n label: "input",\n field: {\n type: "input",\n props: {\n placeholder: "请输入"\n }\n },\n rules: [\n {\n required: true,\n message: "请输入"\n }\n ]\n }\n ],\n remoteValues: {\n url: "\u002Fdetail.json",\n method: "post",\n params: {\n id: 1,\n },\n },\n submit: {\n url: "\u002Fadd.json",\n method: "post"\n },\n},',
69 | documentation: {
70 | value: `
71 | 属性名 | 描述 | 类型
72 | ---|:--:|---:
73 | fields | 表单配置 | -
74 | title | 抽屉标题 | -
75 | mode | 表单模式 | -
76 | remoteValues | 远程表单值的请求配置 | -
77 | submit | 提交表单数据的请求配置 | -
78 | `,
79 | },
80 | },
81 | {
82 | type: 'action',
83 | label: 'refreshtable',
84 | detail: '刷新表格',
85 | kind: monaco.languages.CompletionItemKind.Property,
86 | insertTextRules:
87 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
88 | insertText: "'refreshtable'",
89 | documentation: {
90 | value: ``,
91 | },
92 | },
93 | {
94 | type: 'action',
95 | label: 'resettable',
96 | detail: '重置表格',
97 | kind: monaco.languages.CompletionItemKind.Property,
98 | insertTextRules:
99 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
100 | insertText: "'resettable'",
101 | documentation: {
102 | value: ``,
103 | },
104 | },
105 | {
106 | type: 'action',
107 | label: 'route',
108 | detail: '路由跳转',
109 | kind: monaco.languages.CompletionItemKind.Property,
110 | insertTextRules:
111 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
112 | insertText:
113 | '{\n type: "route",\n path: "${1:\u002Fcreate}",\n params: {\n "${2:mode}": "${3:create}"\n },\n},',
114 | documentation: {
115 | value: ``,
116 | },
117 | },
118 | ];
119 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/suggestions/dependency.js:
--------------------------------------------------------------------------------
1 | export default monaco => [
2 | {
3 | type: 'dependency',
4 | label: 'dependency',
5 | detail: '级联插件',
6 | kind: monaco.languages.CompletionItemKind.Property,
7 | insertTextRules:
8 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
9 | insertText:
10 | 'dependency: {\n ${1:value}: {\n relates: ["${2:relateName}"],\n inputs: [["${3:relateValue}"]],\n output: ${4:"outputValue"},\n ignores: [[${5:"ignoreValue"}]],\n defaultOutput: ${6:"value"},\n },\n},',
11 | documentation: {
12 | value: `### 表单级联配置 [文档](https://doc.sula.now.sh/zh/plugin/form-dependency.html)
13 | #### 可选value visible source disabled 配置
14 | * **relates** 受哪项表单影响
15 | * **inputs** relates数组对应的表单值
16 | * **output** 匹配到inputs时 输出值
17 | * **ignores** relates忽略的值
18 | * **defaultOutput** 匹配ignores或未匹配到inputs时,输出值
19 | `,
20 | },
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/suggestions/index.js:
--------------------------------------------------------------------------------
1 | import dependency from './dependency';
2 | import actions from './actions';
3 | import request from './request';
4 |
5 | function registerSuggestions(monaco) {
6 | monaco.languages.registerCompletionItemProvider('javascript', {
7 | // @ts-ignore
8 | provideCompletionItems(model, position) {
9 | // 其他提示
10 | return {
11 | suggestions: [
12 | ...dependency(monaco),
13 | ...actions(monaco),
14 | ...request(monaco),
15 | ],
16 | };
17 | },
18 | });
19 | }
20 |
21 | export default registerSuggestions;
22 |
--------------------------------------------------------------------------------
/src/components/jsonEditor/suggestions/request.js:
--------------------------------------------------------------------------------
1 | export default monaco => [
2 | {
3 | type: 'fetch',
4 | label: 'remoteDataSource',
5 | detail: '远程表单值',
6 | kind: monaco.languages.CompletionItemKind.Property,
7 | insertTextRules:
8 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
9 | insertText:
10 | "remoteDataSource: {\n url: '${1:\u002Fsula.json}',\n method: '${2:GET}',\n params: {\n '${3:name}': '${4:sula}'\n },\n convertParams({ params }) {\n return params;\n },\n converter({ data }) {\n return data;\n },\n},",
11 | documentation: {
12 | value: `
13 | 属性名 | 描述 | 类型
14 | ---|:--:|---:
15 | url | 请求地址 |string
16 | method| 请求方法 |'post' 'get'
17 | params | 请求参数 | object
18 | convertParams | 请求参数转换方法 | (ctx, config) => params
19 | converter | 返回参数转换方法 | (ctx, config) => any
20 | `,
21 | },
22 | },
23 | {
24 | type: 'fetch',
25 | label: 'remoteSource',
26 | detail: '远程数据源',
27 | kind: monaco.languages.CompletionItemKind.Property,
28 | insertTextRules:
29 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
30 | insertText:
31 | "remoteSource: {\n url: '${1:\u002Fsula.json}',\n method: '${2:GET}',\n params: {\n '${3:name}': '${4:sula}'\n },\n convertParams({ params }) {\n return params\n },\n converter({ data }) {\n return data;\n },\n},",
32 | documentation: {
33 | value: `
34 | 属性名 | 描述 | 类型
35 | ---|:--:|---:
36 | url | 请求地址 |string
37 | method| 请求方法 |'post' 'get'
38 | params | 请求参数 | object
39 | convertParams | 请求参数转换方法 | (ctx, config) => params
40 | converter | 返回参数转换方法 | (ctx, config) => any
41 | `,
42 | },
43 | },
44 | {
45 | type: 'fetch',
46 | label: 'remoteValues',
47 | detail: '远程数据值',
48 | kind: monaco.languages.CompletionItemKind.Property,
49 | insertTextRules:
50 | monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
51 | insertText:
52 | "remoteValues: {\n url: '${1:\u002Fsula.json}',\n method: '${2:GET}',\n params: {\n '${3:name}': '${4:sula}'\n },\n convertParams({ params }) {\n return params\n },\n converter({ data }) {\n return data;\n },\n},",
53 | documentation: {
54 | value: `
55 | 属性名 | 描述 | 类型
56 | ---|:--:|---:
57 | url | 请求地址 |string
58 | method| 请求方法 |'post' 'get'
59 | params | 请求参数 | object
60 | convertParams | 请求参数转换方法 | (ctx, config) => params
61 | converter | 返回参数转换方法 | (ctx, config) => any
62 | `,
63 | },
64 | },
65 | ];
66 |
--------------------------------------------------------------------------------
/src/components/jsonEditorDrawer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Drawer from '@/components/drawer';
3 | import JsonEditor from '@/components/jsonEditor';
4 | import PluginTree from '@/components/pluginTree';
5 |
6 | export default props => {
7 | const { visible, onClose, value, onRun, clickType } = props;
8 | const [pluginType, setPluginType] = React.useState(clickType);
9 | const [editorValue, setEditorValue] = React.useState(value);
10 | const [isFull, setFull] = React.useState(false);
11 |
12 | React.useEffect(() => {
13 | setPluginType(clickType);
14 | }, [clickType]);
15 |
16 | React.useEffect(() => {
17 | setEditorValue(value);
18 | }, [value]);
19 |
20 | const isColumnsType = clickType === 'columns';
21 |
22 | const handleChangeEditorValue = (code, isAction) => {
23 | if (isColumnsType || isAction) {
24 | setEditorValue({
25 | ...editorValue,
26 | ...code,
27 | });
28 | return;
29 | }
30 |
31 | const { name, label, action } = editorValue;
32 | setEditorValue({
33 | ...(name ? { name } : {}),
34 | ...(label ? { label } : {}),
35 | ...code,
36 | ...(action ? { action } : {}),
37 | });
38 | };
39 |
40 | return (
41 |
42 |
43 |
49 |
50 |
58 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/jsonFormTemp/component/formDrawer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Typography, message, Radio, Space } from 'antd';
3 | import ConfigButton from '@/components/configButton';
4 | import Drawer from '@/components/drawer';
5 | import Editor from '@/components/jsonEditor';
6 | import StyleSelect from '@/components/styleSelect';
7 |
8 | const { Title } = Typography;
9 |
10 | const formStyleLists = [
11 | {
12 | type: '卡片表单',
13 | img: 'https://img.alicdn.com/tfs/TB15HnIqAL0gK0jSZFtXXXQCXXa-40-32.svg',
14 | url: '/form/card',
15 | },
16 | {
17 | type: '嵌套卡片表单',
18 | img: 'https://img.alicdn.com/tfs/TB1XSzOqBr0gK0jSZFnXXbRRXXa-40-32.svg',
19 | url: '/form/nestedcard',
20 | },
21 | {
22 | type: '多列布局',
23 | img: 'https://img.alicdn.com/tfs/TB1CwzLqrr1gK0jSZFDXXb9yVXa-40-40.svg',
24 | url: '/form/vertical',
25 | },
26 | {
27 | type: '单列布局',
28 | img: 'https://img.alicdn.com/tfs/TB1fgrPqxD1gK0jSZFyXXciOVXa-40-40.svg',
29 | url: '/form/horizontal',
30 | },
31 | {
32 | type: '分步表单',
33 | img: 'https://img.alicdn.com/tfs/TB1maTYqAT2gK0jSZPcXXcKkpXa-40-32.svg',
34 | url: '/form/stepform',
35 | },
36 | {
37 | type: '响应式布局',
38 | img: 'https://gw.alicdn.com/tfs/TB1ZNSmGoT1gK0jSZFhXXaAtVXa-53-46.svg',
39 | url: '/form/media',
40 | },
41 | ];
42 |
43 | export default props => {
44 | const {
45 | width = 600,
46 | id,
47 | visible,
48 | onClick,
49 | onClose,
50 | onRun,
51 | code: defaultCode,
52 | iconVisible,
53 | mode = 'create',
54 | changeMode,
55 | direction,
56 | isWizard,
57 | changeDirection,
58 | actionsPosition,
59 | changeActionsPosition,
60 | } = props;
61 | const [code, setCode] = useState(defaultCode);
62 | const [isDetail, setIsDetail] = useState(false);
63 | const styleRef = React.useRef(null);
64 |
65 | const onClickRun = value => {
66 | try {
67 | setCode(value);
68 | } catch (e) {
69 | message.error('JSON 格式错误');
70 | }
71 | };
72 |
73 | const onModeChange = e => {
74 | const { value } = e.target;
75 | changeMode(value);
76 | };
77 |
78 | const onDirectionChange = e => {
79 | const { value } = e.target;
80 | changeDirection(value);
81 | };
82 |
83 | const onActionsPositionChange = e => {
84 | const { value } = e.target;
85 | changeActionsPosition(value);
86 | };
87 |
88 | React.useEffect(() => {
89 | const { hash } = window.location;
90 | hash.includes('detail') && setIsDetail(true);
91 | }, []);
92 |
93 | React.useEffect(() => {
94 | onRun(code);
95 | }, [code]);
96 |
97 | const height =
98 | styleRef?.current?.clientHeight || styleRef?.current?.offsetHeight;
99 |
100 | return (
101 |
102 |
103 |
104 |
105 | {!isDetail && (
106 |
107 |
表单模式
108 |
113 | 新建
114 | 编辑
115 | 查看
116 |
117 |
118 | )}
119 |
120 |
127 |
128 | {isWizard && (
129 |
130 |
分布表单方向
131 |
136 | 横向
137 | 纵向
138 |
139 |
140 | )}
141 |
142 | {!isWizard && (
143 |
144 |
表单按钮位置
145 |
150 | 默认
151 | 居中
152 | 右侧
153 | 底部
154 |
155 |
156 | )}
157 |
158 |
159 |
160 |
170 |
171 |
172 | {!visible && !iconVisible &&
}
173 |
174 | );
175 | };
176 |
--------------------------------------------------------------------------------
/src/components/jsonFormTemp/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { CreateForm, StepForm } from 'sula';
3 | import { merge, set, unset, get } from 'lodash';
4 | import { triggerRenderPlugin } from 'sula/es/rope/triggerPlugin';
5 | import getFormMode from '@/utils/getFormMode';
6 | import FormDrawer from './component/formDrawer';
7 | import ControlDrawer from '@/components/jsonEditorDrawer';
8 | import TipsWrapper from '@/components/tipsWrapper';
9 |
10 | export default (props) => {
11 | const {
12 | history,
13 | location,
14 | match,
15 | staticContext,
16 | computedMatch,
17 | route,
18 | children,
19 | routes,
20 | ...config
21 | } = props;
22 | const [formDrawerVisible, setFormDrawerVisible] = useState(false);
23 | const [visible, setVisible] = useState(false);
24 | const [code, setCode] = useState(config); // 全局代码,透传给Form组件 纯json
25 | const [mode, setMode] = useState(getFormMode(props));
26 | const [direction, setDirection] = useState('horizontal');
27 | const [actionsPosition, setActionsPosition] = useState(
28 | config.actionsPosition,
29 | );
30 | const [flag, setFlag] = useState([0]);
31 | const [controlValue, setControlValue] = useState({}); // 局部控制jsonEditor的value
32 | const [key, setKey] = useState(0);
33 | const [clickType, setClickType] = useState('');
34 | const [init, setInit] = useState(false);
35 |
36 | function deleteItem(path) {
37 | const finalCode = { ...code };
38 | unset(finalCode, path);
39 | setCode(finalCode);
40 | setKey(key + 1);
41 | }
42 |
43 | function addItem(name, path, position = 'left') {
44 | let finalCode = { ...code };
45 | const nodePath = path.slice(0, -1);
46 | const node = get(finalCode, nodePath);
47 | const idx = node.length + 1;
48 |
49 | let defaultCode;
50 | switch (name) {
51 | case 'fields':
52 | defaultCode = {
53 | name: 'input' + idx,
54 | label: 'input' + idx,
55 | field: 'input',
56 | };
57 | break;
58 |
59 | case 'actionsRender':
60 | defaultCode = {
61 | type: 'button',
62 | props: {
63 | children: '按钮' + idx,
64 | type: 'primary',
65 | },
66 | };
67 | break;
68 | default:
69 | return;
70 | }
71 |
72 | node.splice(
73 | position === 'left' ? path[path.length - 1] : path[path.length - 1] + 1,
74 | 0,
75 | defaultCode,
76 | );
77 | finalCode = set(finalCode, nodePath, node);
78 |
79 | setCode(finalCode);
80 | setKey(key + 1);
81 | }
82 |
83 | const isWizard = !!(code && code.steps);
84 |
85 | const getLabel = (data, path) => {
86 | return (
87 | {
91 | setControlValue(data);
92 | setFlag(path);
93 | setVisible(true);
94 | setClickType('form');
95 | }}
96 | onDelete={() => {
97 | deleteItem(path);
98 | }}
99 | onAddBefore={() => {
100 | addItem('fields', path);
101 | }}
102 | onAddAfter={() => {
103 | addItem('fields', path, 'right');
104 | }}
105 | >
106 | {data.label}
107 |
108 | );
109 | };
110 |
111 | const getLabelFields = (data, arr = []) => {
112 | if (!data) return [];
113 | return data.map((v, idx) => {
114 | const { fields, steps } = v;
115 | if (fields) {
116 | return {
117 | ...v,
118 | fields: getLabelFields(fields, [...arr, idx, 'fields']),
119 | };
120 | }
121 |
122 | if (steps) {
123 | return {
124 | ...v,
125 | steps: getLabelFields(steps, [...arr, idx, 'steps']),
126 | };
127 | }
128 |
129 | return {
130 | ...v,
131 | label: getLabel(v, [...arr, idx]),
132 | };
133 | });
134 | };
135 |
136 | const getFieldsConfig = (data) => {
137 | const { steps, fields } = data;
138 | return isWizard
139 | ? { steps: getLabelFields(steps, ['steps']) }
140 | : { fields: getLabelFields(fields, ['fields']) };
141 | };
142 |
143 | const [labelCode, setLabelCode] = useState({
144 | ...code,
145 | ...getFieldsConfig(config),
146 | });
147 |
148 | useEffect(() => {
149 | setInit(true);
150 | }, []);
151 |
152 | useEffect(() => {
153 | const newLabelCode = { ...code };
154 | setLabelCode({ ...newLabelCode, ...getFieldsConfig(code) });
155 | if (init) {
156 | setKey(key + 1);
157 | }
158 | }, [code]);
159 |
160 | // 给个靠后点默认id,防止前面删掉后无数据
161 | const { id = 19 } = props.match.params;
162 |
163 | const handleDo = (val) => {
164 | setCode(val);
165 | setFormDrawerVisible(false);
166 | };
167 |
168 | const remoteValues = {
169 | params: {
170 | id,
171 | },
172 | };
173 |
174 | const Comp = isWizard ? StepForm : CreateForm;
175 |
176 | const onRun = (val) => {
177 | const { name: oldName } = controlValue;
178 | const { name, ...restVal } = { ...val };
179 | const newVal = oldName ? { name: oldName, ...restVal } : val;
180 |
181 | let finalCode = { ...code };
182 | finalCode = set(finalCode, flag, newVal);
183 | setCode(finalCode);
184 |
185 | setVisible(false);
186 | setKey(key + 1);
187 | };
188 |
189 | const onModeChange = (mode) => {
190 | setMode(mode);
191 | setFormDrawerVisible(false);
192 | setKey(key + 1);
193 | };
194 |
195 | const onDirectionChange = (direction) => {
196 | setDirection(direction);
197 | setFormDrawerVisible(false);
198 | setKey(key + 1);
199 | };
200 |
201 | const onActionsPositionChange = (position) => {
202 | setActionsPosition(position);
203 | setFormDrawerVisible(false);
204 | setKey(key + 1);
205 | };
206 |
207 | const onControlTipClick = (data, namePath) => {
208 | setControlValue(data);
209 | setFlag(namePath);
210 | setVisible(true);
211 | setClickType('actions');
212 | };
213 |
214 | const getClickItem = (data, name) => {
215 | const finalActions = data.map((action, idx) => {
216 | return {
217 | type: (ctx) => {
218 | const children = triggerRenderPlugin(ctx, action);
219 | const path = [name, idx];
220 | return (
221 | onControlTipClick(action, path)}
223 | onDelete={() => {
224 | deleteItem(path);
225 | }}
226 | onAddBefore={() => {
227 | addItem('actionsRender', path);
228 | }}
229 | onAddAfter={() => {
230 | addItem('actionsRender', path, 'right');
231 | }}
232 | >
233 | {children}
234 |
235 | );
236 | },
237 | };
238 | });
239 |
240 | return finalActions;
241 | };
242 |
243 | const getActionConfig = (data) => {
244 | const { actionsRender, ...restProps } = { ...data };
245 | if (!actionsRender) return data;
246 | return {
247 | ...restProps,
248 | actionsRender: getClickItem(actionsRender, 'actionsRender'),
249 | };
250 | };
251 |
252 | const wizardDirection = isWizard ? { direction } : {};
253 |
254 | const finalConfig = merge(
255 | getActionConfig(labelCode),
256 | { remoteValues },
257 | wizardDirection,
258 | );
259 |
260 | return (
261 |
262 |
268 | setFormDrawerVisible(true)}
275 | onClose={() => setFormDrawerVisible(false)}
276 | onRun={handleDo}
277 | code={code}
278 | changeMode={onModeChange}
279 | changeDirection={onDirectionChange}
280 | isWizard={isWizard}
281 | actionsPosition={actionsPosition}
282 | changeActionsPosition={onActionsPositionChange}
283 | width="900px"
284 | />
285 | {
289 | setVisible(false);
290 | }}
291 | value={controlValue}
292 | onRun={onRun}
293 | />
294 |
295 | );
296 | };
297 |
--------------------------------------------------------------------------------
/src/components/jsonTableTemp/component/tableDrawer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { message } from 'antd';
3 | import Drawer from '@/components/drawer';
4 | import ConfigButton from '@/components/configButton';
5 | import StyleSelect from '@/components/styleSelect';
6 | import Editor from '@/components/jsonEditor';
7 |
8 | const tableStyleList = [
9 | {
10 | type: '快速搜索',
11 | img: 'https://img.alicdn.com/tfs/TB1al1JqHr1gK0jSZR0XXbP8XXa-40-40.svg',
12 | url: '/list/singlesearch',
13 | },
14 | {
15 | type: '高级搜索',
16 | img: 'https://img.alicdn.com/tfs/TB1QgKIqQL0gK0jSZFtXXXQCXXa-40-40.svg',
17 | url: '/list/advancedsearch',
18 | },
19 | {
20 | type: '一般搜索',
21 | img: 'https://img.alicdn.com/tfs/TB1xHCPqRr0gK0jSZFnXXbRRXXa-40-40.svg',
22 | url: '/list/basic',
23 | },
24 | {
25 | type: '无分页表格',
26 | img: 'https://img.alicdn.com/tfs/TB1txuMqQL0gK0jSZFxXXXWHVXa-40-40.svg',
27 | url: '/list/nopagination',
28 | },
29 | {
30 | type: '分步查询表格',
31 | img: 'https://img.alicdn.com/tfs/TB10IOmGkL0gK0jSZFAXXcA9pXa-53-46.svg',
32 | url: '/list/stepquerytable',
33 | },
34 | ];
35 |
36 | export default React.memo(props => {
37 | const {
38 | width = 600,
39 | visible,
40 | onClick,
41 | onClose,
42 | onRun,
43 | code: defaultCode,
44 | iconVisible,
45 | } = props;
46 | const [code, setCode] = useState(defaultCode);
47 | const [init, setInit] = useState(false);
48 | const [jsonEditorVal, setJsonEditorVal] = useState(defaultCode);
49 | const styleRef = React.useRef(null);
50 |
51 | React.useEffect(() => {
52 | setInit(true);
53 | }, []);
54 |
55 | const onClickRun = value => {
56 | setJsonEditorVal(value);
57 | try {
58 | setCode(value);
59 | } catch (e) {
60 | message.error('JSON 格式错误');
61 | }
62 | };
63 |
64 | React.useEffect(() => {
65 | if (init) {
66 | onRun(code);
67 | }
68 | }, [code]);
69 |
70 | const height =
71 | styleRef?.current?.clientHeight || styleRef?.current?.offsetHeight;
72 |
73 | return (
74 |
75 |
76 |
77 |
82 |
83 |
84 |
90 |
91 |
92 | {!visible && !iconVisible &&
}
93 |
94 | );
95 | });
96 |
--------------------------------------------------------------------------------
/src/components/jsonTableTemp/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Table, QueryTable, StepQueryTable } from 'sula';
3 | import {
4 | get,
5 | set,
6 | isEmpty,
7 | cloneDeep,
8 | assign,
9 | isArray,
10 | isNumber,
11 | isPlainObject,
12 | unset,
13 | } from 'lodash';
14 | import TableDrawer from './component/tableDrawer';
15 | import ControlJsonDrawer from '@/components/jsonEditorDrawer';
16 | import TipsWrapper from '@/components/tipsWrapper';
17 | import { triggerRenderPlugin } from 'sula/es/rope/triggerPlugin';
18 | import { Guide } from '@/components/guide';
19 |
20 | export default props => {
21 | const [tableDrawerVisible, setTableDrawerVisible] = useState(false);
22 | const [visible, setVisible] = useState(false);
23 | const [code, setCode] = useState(props);
24 | const [flag, setFlag] = useState([]);
25 | const [clickCode, setClickCode] = useState();
26 | const [clickType, setClickType] = useState('');
27 | const [key, setKey] = useState(0);
28 |
29 | const onControlTipClick = (data, namePath) => {
30 | setClickCode(data);
31 | setFlag(namePath);
32 | setVisible(true);
33 | setClickType('actions');
34 | };
35 |
36 | function deleteItem(name, idx) {
37 | const finalCode = { ...code };
38 | finalCode[name].splice(idx, 1);
39 | setCode(finalCode);
40 | setKey(key + 1);
41 | }
42 |
43 | function addItem(name, index, position = 'left') {
44 | const finalCode = { ...code };
45 | const idx = finalCode[name].length + 1;
46 | let defaultCode;
47 | switch (name) {
48 | case 'fields':
49 | defaultCode = {
50 | name: 'input' + idx,
51 | label: 'input' + idx,
52 | field: 'input',
53 | };
54 | break;
55 |
56 | case 'actionsRender':
57 | case 'leftActionsRender':
58 | defaultCode = {
59 | type: 'button',
60 | props: {
61 | children: '按钮' + idx,
62 | type: 'primary',
63 | },
64 | };
65 | break;
66 | case 'columns':
67 | defaultCode = {
68 | key: 'column' + idx,
69 | title: 'column' + idx,
70 | };
71 | break;
72 | default:
73 | return;
74 | }
75 | finalCode[name].splice(
76 | position === 'left' ? index : index + 1,
77 | 0,
78 | defaultCode,
79 | );
80 | setCode(finalCode);
81 | setKey(key + 1);
82 | }
83 |
84 | function addActionItem(path, index, position = 'left') {
85 | let finalCode = { ...code };
86 | let render = get(finalCode, path);
87 | render = isArray(render) ? render : [render];
88 | render.splice(position === 'left' ? index : index + 1, 0, {
89 | type: 'icon',
90 | props: {
91 | type: 'edit',
92 | },
93 | });
94 | finalCode = set(finalCode, path, render);
95 | setCode(finalCode);
96 | setKey(key + 1);
97 | }
98 |
99 | const getLabelFields = (data, name) => {
100 | if (isEmpty(data)) return;
101 | return data.map((field, index) => {
102 | const labelRender = (
103 | {
107 | setClickCode({ ...field });
108 | setFlag([name, index]);
109 | setVisible(true);
110 | setClickType('form');
111 | }}
112 | onAddBefore={() => {
113 | addItem(name, index);
114 | }}
115 | onAddAfter={() => {
116 | addItem(name, index, 'right');
117 | }}
118 | onDelete={() => {
119 | deleteItem(name, index);
120 | }}
121 | >
122 | {field.label}
123 |
124 | );
125 | return {
126 | ...field,
127 | label: index ? (
128 | labelRender
129 | ) : (
130 |
131 | {labelRender}
132 |
133 | ),
134 | };
135 | });
136 | };
137 |
138 | const getClickItem = (data, name) => {
139 | if (isEmpty(data)) return;
140 | const finalActions = data.map((action, idx) => {
141 | return {
142 | type: ctx => {
143 | const children = triggerRenderPlugin(ctx, action);
144 | const actionRender = (
145 | onControlTipClick(action, [name, idx])}
147 | onDelete={() => {
148 | deleteItem(name, idx);
149 | }}
150 | onAddBefore={() => {
151 | addItem(name, idx);
152 | }}
153 | onAddAfter={() => {
154 | addItem(name, idx, 'right');
155 | }}
156 | >
157 | {children}
158 |
159 | );
160 |
161 | if (idx) {
162 | return actionRender;
163 | }
164 | return (
165 |
166 | {actionRender}
167 |
168 | );
169 | },
170 | };
171 | });
172 |
173 | return finalActions;
174 | };
175 |
176 | const getCellRender = (data, name, index) => {
177 | if (!data) return;
178 |
179 | if (isPlainObject(data)) {
180 | return (ctx = {}) => {
181 | const children = triggerRenderPlugin(ctx, data) || '';
182 | const idx = isNumber(index)
183 | ? [...name, 'render', index]
184 | : [...name, 'render'];
185 | return (
186 | onControlTipClick(data, idx)}
188 | onDelete={() => {
189 | const finalCode = { ...code };
190 | unset(finalCode, idx);
191 | setCode(finalCode);
192 | setKey(key + 1);
193 | }}
194 | onAddBefore={() => {
195 | addActionItem([...name, 'render'], index);
196 | }}
197 | onAddAfter={() => {
198 | addActionItem([...name, 'render'], index, 'right');
199 | }}
200 | >
201 | {children}
202 |
203 | );
204 | };
205 | }
206 |
207 | if (isArray(data)) {
208 | return data.map((o, idx) => getCellRender(o, name, idx));
209 | }
210 |
211 | return data;
212 | };
213 |
214 | const getLabelColumns = (data, name) => {
215 | if (isEmpty(data)) return;
216 | return data.map((column, idx) => {
217 | const { title, render } = column;
218 | const titleRender = (
219 | {
222 | setClickCode(column);
223 | setFlag([name, idx]);
224 | setVisible(true);
225 | setClickType('columns');
226 | }}
227 | onDelete={() => {
228 | deleteItem(name, idx);
229 | }}
230 | onAddBefore={() => {
231 | addItem(name, idx);
232 | }}
233 | onAddAfter={() => {
234 | addItem(name, idx, 'right');
235 | }}
236 | >
237 | {title}
238 |
239 | );
240 | return {
241 | ...column,
242 | title: idx ? (
243 | titleRender
244 | ) : (
245 |
246 | {titleRender}
247 |
248 | ),
249 | render: getCellRender(render, ['columns', idx]),
250 | };
251 | });
252 | };
253 |
254 | const getActionConfig = data => {
255 | const {
256 | columns = [],
257 | actionsRender = [],
258 | fields,
259 | leftActionsRender = [],
260 | ...restProps
261 | } = {
262 | ...data,
263 | };
264 | return {
265 | ...restProps,
266 | columns: getLabelColumns(columns, 'columns'),
267 | fields: getLabelFields(fields, 'fields'),
268 | actionsRender: getClickItem(actionsRender, 'actionsRender'),
269 | leftActionsRender: getClickItem(leftActionsRender, 'leftActionsRender'),
270 | };
271 | };
272 |
273 | const handleDo = val => {
274 | setCode(val);
275 | setTableDrawerVisible(false);
276 | setKey(key + 1);
277 | };
278 |
279 | const onRun = val => {
280 | const oldName = get(code, [...flag, 'name']);
281 | const { name, ...resVal } = val;
282 | const newVal = oldName ? { name: oldName, ...resVal } : val;
283 | setCode(set({ ...code }, flag, newVal));
284 | setKey(key + 1);
285 | setVisible(false);
286 | };
287 |
288 | const { initialPaging = {} } = code;
289 | const { pagination } = initialPaging;
290 |
291 | let Comp = QueryTable;
292 | if (initialPaging === false || pagination === false) {
293 | Comp = Table;
294 | } else if (code.steps) {
295 | Comp = StepQueryTable;
296 | }
297 |
298 | const finalCode = getActionConfig(code);
299 | const { remoteDataSource } = finalCode;
300 | // 防止模板内部代码改变remoteDataSource
301 | const finalDataSource = cloneDeep(remoteDataSource);
302 |
303 | return (
304 |
305 |
309 |
setTableDrawerVisible(true)}
312 | onClose={() => setTableDrawerVisible(false)}
313 | onRun={handleDo}
314 | code={code}
315 | iconVisible={visible}
316 | width="900"
317 | key={'config' + key}
318 | />
319 | setVisible(false)}
322 | onRun={onRun}
323 | value={clickCode}
324 | onSelectCode={onRun}
325 | clickType={clickType}
326 | />
327 |
328 | );
329 | };
330 |
--------------------------------------------------------------------------------
/src/components/languageSwitch/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, Menu, Tooltip } from 'antd';
3 | import { getLocale } from 'umi';
4 |
5 | import styles from './index.less';
6 |
7 | export default props => {
8 | const { handleChangeLanguage } = props;
9 |
10 | const localeUrl = {
11 | en: 'https://img.alicdn.com/tfs/TB1GdeYri_1gK0jSZFqXXcpaXXa-24-24.png',
12 | zh: 'https://img.alicdn.com/tfs/TB1AQ50reH2gK0jSZJnXXaT1FXa-24-24.png',
13 | };
14 |
15 | const menu = (
16 |
32 | );
33 |
34 | const logoUrl = getLocale() === 'en-US' ? localeUrl.en : localeUrl.zh;
35 |
36 | return (
37 |
38 |
39 |
40 |

41 |
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/languageSwitch/index.less:
--------------------------------------------------------------------------------
1 | .localeLogo {
2 | overflow: hidden;
3 | border-radius: 50%;
4 | width: 36px;
5 | height: 36px;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | transition: all 0.3s;
10 | &:hover {
11 | background-color: #eef9ff;
12 | }
13 | img {
14 | border-radius: 50%;
15 | width: 26px;
16 | height: 26px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/pluginTree/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Input, Tabs, Tooltip } from 'antd';
3 | import formConfig from './sulaconfig/fieldPlugins';
4 | import renderConfig from './sulaconfig/renderPlugins';
5 | import columnsConfig from './sulaconfig/columns';
6 | import actionsConfig from './sulaconfig/actions';
7 |
8 | import style from './index.less';
9 |
10 | const sulaPluginConfig = {
11 | form: formConfig, // 对应field变更,name label不变
12 | actions: renderConfig, // actions配置,对应render类型变更,action属性不变
13 | columns: columnsConfig, // columns配置,只变更render属性
14 | // actionsConfig // 只变更配置中的action属性
15 | };
16 |
17 | const { Search } = Input;
18 | const { Meta } = Card;
19 | const { TabPane } = Tabs;
20 |
21 | export default props => {
22 | const {
23 | onClickTypeChange = () => {},
24 | onEditorValueChange = () => {},
25 | type = 'form',
26 | hasField,
27 | isColumnsType,
28 | } = props;
29 |
30 | const [plugins = [], setPlugins] = React.useState(sulaPluginConfig[type]);
31 | const [actionPlugins = [], setActionPlugins] = React.useState(actionsConfig);
32 |
33 | const onChange = e => {
34 | const val = e.target.value;
35 | const newPlugins = sulaPluginConfig[type].filter(v => v.type.includes(val));
36 | setPlugins(newPlugins);
37 | setActionPlugins(actionsConfig.filter(v => v.type.includes(val)));
38 | };
39 |
40 | const tablePaneRender = (data, isAction = false) => {
41 | return (
42 |
43 | {data.map(plugin => {
44 | return (
45 |
52 | );
53 | })}
54 |
55 | );
56 | };
57 |
58 | return (
59 |
60 |
65 |
66 |
67 | {tablePaneRender(plugins)}
68 |
69 | {!hasField && !isColumnsType && (
70 |
71 | {tablePaneRender(actionPlugins, true)}
72 |
73 | )}
74 |
75 |
76 | );
77 | };
78 |
79 | function PluginCard(props) {
80 | const {
81 | plugin = {},
82 | onClickTypeChange,
83 | onEditorValueChange,
84 | isAction = false,
85 | } = props;
86 | const {
87 | avatar = 'https://img.alicdn.com/tfs/TB1BQlopUY1gK0jSZFCXXcwqXXa-56-50.svg',
88 | type = '',
89 | desc = '',
90 | code = '',
91 | } = plugin;
92 | return (
93 | {
97 | onClickTypeChange(type);
98 | onEditorValueChange(code, isAction);
99 | }}
100 | >
101 |
104 |
105 |
106 | }
107 | title={type}
108 | description={
109 |
110 |
{desc}
111 | {/*
112 | "question-circle"
113 | */}
114 |
115 | }
116 | />
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/pluginTree/index.less:
--------------------------------------------------------------------------------
1 | .card {
2 | flex-basis: 100%;
3 | margin-bottom: 8px;
4 | cursor: pointer;
5 | transition: all 0.3s;
6 | &:hover {
7 | border-color: rgb(64, 137, 247);
8 | .icon {
9 | display: block;
10 | }
11 | }
12 | }
13 |
14 | .icon {
15 | position: absolute;
16 | right: 10px;
17 | top: 15px;
18 | display: none;
19 | z-index: 99;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/pluginTree/sulaconfig/actions.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | type: 'refreshtable',
4 | desc: '刷新表格',
5 | code: {
6 | action: 'refreshtable',
7 | },
8 | },
9 | {
10 | type: 'resettable',
11 | desc: '重置表格',
12 | code: {
13 | action: 'resettable',
14 | },
15 | },
16 | {
17 | type: 'request',
18 | desc: '请求',
19 | code: {
20 | action: {
21 | type: 'request',
22 | url: '/api/manage/list.json',
23 | method: 'post',
24 | },
25 | },
26 | },
27 | {
28 | type: 'back',
29 | desc: '后退',
30 | code: {
31 | action: 'back',
32 | },
33 | },
34 | {
35 | type: 'forward',
36 | desc: '前进',
37 | code: {
38 | action: 'forward',
39 | },
40 | },
41 | {
42 | type: 'route',
43 | desc: '路由到指定页',
44 | code: {
45 | action: {
46 | type: 'route',
47 | path: '/form/card/create',
48 | },
49 | },
50 | },
51 | // form
52 | {
53 | type: 'validateFields',
54 | desc: '表单校验',
55 | code: {
56 | action: {
57 | type: 'validateFields',
58 | args: ['id'],
59 | },
60 | },
61 | },
62 | {
63 | type: 'validateGroupFields',
64 | desc: '表单组校验',
65 | code: {
66 | action: {
67 | type: 'validateGroupFields',
68 | args: ['group'],
69 | },
70 | },
71 | },
72 | {
73 | type: 'validateQueryFields',
74 | desc: '搜索列表校验',
75 | code: {
76 | action: {
77 | type: 'validateQueryFields',
78 | args: ['id'],
79 | },
80 | },
81 | },
82 | {
83 | type: 'resetFields',
84 | desc: '重置表单',
85 | code: {
86 | action: 'resetFields',
87 | },
88 | },
89 |
90 | // modalform
91 | {
92 | type: 'modalform',
93 | desc: '弹框表单',
94 | code: {
95 | action: {
96 | type: 'modalform',
97 | title: 'title',
98 | mode: 'edit',
99 | fields: [
100 | {
101 | name: 'input',
102 | label: 'input',
103 | field: 'input',
104 | },
105 | ],
106 | remoteValues: {
107 | url: '/api/manage/detail.json',
108 | method: 'post',
109 | params: {
110 | id: '#{record.id}',
111 | },
112 | },
113 | submit: {
114 | url: '/api/manage/add.json',
115 | method: 'post',
116 | },
117 | },
118 | },
119 | },
120 | {
121 | type: 'drawerform',
122 | desc: '抽屉表单',
123 | code: {
124 | action: {
125 | type: 'drawerform',
126 | title: 'title',
127 | mode: 'edit',
128 | fields: [
129 | {
130 | name: 'input',
131 | label: 'input',
132 | field: 'input',
133 | },
134 | ],
135 | remoteValues: {
136 | url: '/api/manage/detail.json',
137 | method: 'post',
138 | params: {
139 | id: '#{record.id}',
140 | },
141 | },
142 | submit: {
143 | url: '/api/manage/add.json',
144 | method: 'post',
145 | },
146 | },
147 | },
148 | },
149 | ];
150 |
--------------------------------------------------------------------------------
/src/components/pluginTree/sulaconfig/columns.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | type: 'textlink',
4 | desc: '文本链接',
5 | code: {
6 | render: {
7 | type: 'button',
8 | props: {
9 | children: '#{text}',
10 | type: 'link',
11 | size: 'small',
12 | link: '#/form/card/view/#{text}',
13 | style: { padding: 0 },
14 | },
15 | },
16 | },
17 | },
18 | {
19 | type: 'tag',
20 | desc: '标签',
21 | code: {
22 | key: 'status',
23 | title: '标签插件',
24 | render: [
25 | {
26 | type: 'tag',
27 | props: {
28 | type: 'primary',
29 | children: 'primary',
30 | },
31 | },
32 | {
33 | type: 'tag',
34 | props: {
35 | children: '#f50',
36 | color: '#f50',
37 | },
38 | },
39 | ],
40 | },
41 | },
42 | {
43 | type: 'process',
44 | desc: '进度条',
45 | code: {
46 | key: 'process',
47 | title: '进度条插件',
48 | render: [
49 | {
50 | type: 'progress',
51 | props: {
52 | percent: 30,
53 | status: 'active',
54 | },
55 | },
56 | ],
57 | },
58 | },
59 | {
60 | type: 'operator',
61 | desc: '图标操作组',
62 | code: {
63 | key: 'operator',
64 | title: 'Operator',
65 | render: [
66 | {
67 | type: 'icon',
68 | props: {
69 | type: 'edit',
70 | },
71 | action: {
72 | type: 'route',
73 | path: '/form/card/edit/#{record.id}',
74 | },
75 | },
76 | {
77 | type: 'icon',
78 | props: {
79 | type: 'delete',
80 | },
81 | tooltip: 'Delete',
82 | confirm: 'Are you sure to delete?',
83 | visible: '#{record.id % 3 === 0}',
84 | action: [
85 | {
86 | type: 'request',
87 | url: '/api/manage/delete.json',
88 | method: 'POST',
89 | params: {
90 | rowKeys: '#{record.id}',
91 | },
92 | },
93 | 'refreshtable',
94 | ],
95 | },
96 | ],
97 | },
98 | },
99 | ];
100 |
--------------------------------------------------------------------------------
/src/components/pluginTree/sulaconfig/fieldPlugins.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | type: 'input',
4 | desc: '输入框组件',
5 | code: {
6 | field: {
7 | type: 'input',
8 | props: {
9 | placeholder: 'Please input',
10 | },
11 | },
12 | rules: [{ required: true, message: 'Please input' }],
13 | },
14 | },
15 | {
16 | type: 'inputnumber',
17 | desc: '数字输入框',
18 | code: {
19 | field: {
20 | type: 'inputnumber',
21 | props: {
22 | placeholder: 'Please input',
23 | },
24 | },
25 | },
26 | rules: [{ required: true, message: 'Please input' }],
27 | },
28 | {
29 | type: 'select',
30 | desc: '选择器',
31 | code: {
32 | field: {
33 | type: 'select',
34 | props: {
35 | placeholder: 'Please select',
36 | },
37 | },
38 | initialSource: [
39 | { text: 'Sula', value: 'sula' },
40 | { text: 'Antd', value: 'antd', disabled: true },
41 | ],
42 | rules: [{ required: true, message: 'Please select' }],
43 | },
44 | },
45 | {
46 | type: 'textarea',
47 | desc: '文本域',
48 | code: {
49 | field: {
50 | type: 'textarea',
51 | props: {
52 | placeholder: 'Please input',
53 | },
54 | },
55 | },
56 | rules: [{ required: true, message: 'Please input' }],
57 | },
58 | {
59 | type: 'switch',
60 | desc: '开关',
61 | code: {
62 | field: {
63 | type: 'switch',
64 | props: {
65 | checkedChildren: 'On',
66 | unCheckedChildren: 'Off',
67 | },
68 | },
69 | valuePropName: 'checked',
70 | },
71 | },
72 | {
73 | type: 'checkbox',
74 | desc: '多选框',
75 | code: {
76 | field: {
77 | type: 'checkbox',
78 | },
79 | valuePropName: 'checked',
80 | },
81 | },
82 | {
83 | type: 'checkboxgroup',
84 | desc: '多选框组',
85 | code: {
86 | field: {
87 | type: 'checkboxgroup',
88 | },
89 | initialSource: [
90 | { text: 'Sula', value: 'sula' },
91 | { text: 'Antd', value: 'antd', disabled: true },
92 | { text: 'Umi', value: 'umi' },
93 | ],
94 | rules: [{ required: true, message: 'Please select' }],
95 | },
96 | },
97 | {
98 | type: 'radio',
99 | desc: '单选框',
100 | code: {
101 | field: {
102 | type: 'radio',
103 | },
104 | valuePropName: 'checked',
105 | },
106 | },
107 | {
108 | type: 'radiogroup',
109 | desc: '单选框组',
110 | code: {
111 | field: {
112 | type: 'radiogroup',
113 | },
114 | initialSource: [
115 | { text: 'Sula', value: 'sula' },
116 | { text: 'Antd', value: 'antd', disabled: true },
117 | ],
118 | rules: [{ required: true, message: 'Please select' }],
119 | },
120 | },
121 | {
122 | type: 'password',
123 | desc: '密码输入框',
124 | code: {
125 | field: {
126 | type: 'password',
127 | props: {
128 | placeholder: 'Please input',
129 | },
130 | },
131 | },
132 | rules: [{ required: true, message: 'Please input' }],
133 | },
134 |
135 | {
136 | type: 'rate',
137 | desc: '评分组件',
138 | code: {
139 | field: {
140 | type: 'rate',
141 | props: {
142 | placeholder: 'Please input',
143 | },
144 | },
145 | },
146 | },
147 | {
148 | type: 'slider',
149 | desc: '滑动输入条',
150 | code: {
151 | field: {
152 | type: 'slider',
153 | props: {
154 | min: 0,
155 | max: 200,
156 | },
157 | },
158 | },
159 | },
160 | {
161 | type: 'cascader',
162 | desc: '级联选择器',
163 | code: {
164 | field: {
165 | type: 'cascader',
166 | props: {
167 | placeholder: 'Please select',
168 | },
169 | },
170 | initialSource: [
171 | {
172 | text: 'Sula',
173 | value: 'sula',
174 | children: [
175 | {
176 | text: 'Sula-1',
177 | value: 'sula-1',
178 | children: [
179 | {
180 | text: 'Sula-1-1',
181 | value: 'sula-1-1',
182 | },
183 | ],
184 | },
185 | { text: 'Sula-2', value: 'sula-2' },
186 | ],
187 | },
188 | { text: 'Antd', value: 'antd', disabled: true },
189 | ],
190 | rules: [{ required: true, message: 'Please select' }],
191 | },
192 | },
193 | {
194 | type: 'datepicker',
195 | desc: '日期选择器',
196 | code: {
197 | field: {
198 | type: 'datepicker',
199 | props: {
200 | placeholder: 'Please select',
201 | },
202 | },
203 | rules: [{ required: true, message: 'Please select' }],
204 | },
205 | },
206 | {
207 | type: 'rangepicker',
208 | desc: '日期范围选择',
209 | code: {
210 | field: {
211 | type: 'rangepicker',
212 | props: {
213 | placeholder: 'Please select',
214 | },
215 | },
216 | rules: [{ required: true, message: 'Please select' }],
217 | },
218 | },
219 | {
220 | type: 'timepicker',
221 | desc: '时间选择器',
222 | code: {
223 | field: {
224 | type: 'timepicker',
225 | props: {
226 | placeholder: 'Please select',
227 | },
228 | },
229 | rules: [{ required: true, message: 'Please select' }],
230 | },
231 | },
232 | ];
233 |
--------------------------------------------------------------------------------
/src/components/pluginTree/sulaconfig/renderPlugins.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | type: 'button',
4 | desc: '按钮',
5 | code: {
6 | type: 'button',
7 | props: {
8 | children: 'button',
9 | type: 'primary',
10 | },
11 | },
12 | },
13 | {
14 | type: 'icon',
15 | desc: '图标',
16 | code: {
17 | type: 'icon',
18 | props: {
19 | type: 'edit',
20 | },
21 | },
22 | },
23 | {
24 | type: 'text',
25 | desc: '文本',
26 | code: {
27 | type: 'text',
28 | props: {
29 | children: 'text',
30 | },
31 | },
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------
/src/components/styleSelect/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Tooltip } from 'antd';
3 | import { history } from 'umi';
4 | import style from './index.less';
5 |
6 | const { Title } = Typography;
7 |
8 | export default props => {
9 | const { title, data, mode, id, ...restProps } = props;
10 |
11 | const { hash } = window.location;
12 | return (
13 |
14 |
{title}
15 | {data.map(({ img, type, url, isView }) => {
16 | if (isView && mode !== 'view') {
17 | return null;
18 | }
19 |
20 | const isActive = hash.includes(url);
21 |
22 | return (
23 |
24 | {
27 | const finalUrl =
28 | mode && mode !== 'create' ? `${url}/${mode}/${id}` : url;
29 | history.push(finalUrl);
30 | }}
31 | >
32 |
33 |
34 |
35 | );
36 | })}
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/styleSelect/index.less:
--------------------------------------------------------------------------------
1 | .card {
2 | cursor: pointer;
3 | display: inline-block;
4 | margin-right: 8px;
5 | }
6 |
7 | .activeCard {
8 | box-shadow: 0px 0px 10px #1890ff;
9 | border-radius: 3px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/themeSwitch/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, Menu, Tooltip } from 'antd';
3 | import { SkinOutlined } from '@ant-design/icons';
4 |
5 | import styles from './index.less';
6 |
7 | export default props => {
8 | const { handleChangeTheme } = props;
9 |
10 | const menu = (
11 |
27 | );
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/themeSwitch/index.less:
--------------------------------------------------------------------------------
1 | .themeLogo {
2 | overflow: hidden;
3 | border-radius: 50%;
4 | width: 36px;
5 | height: 36px;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | transition: all 0.3s;
10 | &:hover {
11 | background-color: #eef9ff;
12 | }
13 | img {
14 | border-radius: 50%;
15 | width: 26px;
16 | height: 26px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/tipsSwitch/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip } from 'antd';
3 | import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
4 |
5 | export default props => {
6 | const [view, setView] = React.useState(false);
7 | const { onClick } = props;
8 |
9 | const handleClick = () => {
10 | setView(view => !view);
11 | onClick(view);
12 | };
13 |
14 | return (
15 |
16 | {view ? (
17 |
18 | ) : (
19 |
23 | )}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/tipsWrapper/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Popover, Space, Popconfirm, Button } from 'antd';
3 | import ThemeContext from '@/layout/themeContext';
4 |
5 | export default props => {
6 | const {
7 | title = '',
8 | onSet,
9 | onAddBefore,
10 | onAddAfter,
11 | onDelete,
12 | children,
13 | ...restProps
14 | } = props;
15 | const [visible, setVisible] = React.useState(false);
16 |
17 | const theme = React.useContext(ThemeContext);
18 |
19 | if (theme.hiddenCustomControls) {
20 | return children;
21 | }
22 |
23 | const getTitle = (title = '') => {
24 | return 点击按钮配置{title && ` ${title} `};
25 | };
26 |
27 | const content = (
28 |
29 |
30 |
39 | {onDelete && (
40 |
41 |
44 |
45 | )}
46 |
47 |
48 |
49 | {onAddBefore && (
50 |
53 | )}
54 | {onAddAfter && (
55 |
58 | )}
59 |
60 |
61 | );
62 |
63 | return (
64 | {
72 | setVisible(!visible);
73 | }}
74 | >
75 | {children}
76 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | import { Icon } from 'sula';
2 | import {
3 | EditOutlined,
4 | DeleteOutlined,
5 | PlusOutlined,
6 | RedoOutlined,
7 | EyeOutlined,
8 | } from '@ant-design/icons';
9 | import Mock from 'mockjs';
10 |
11 | import 'antd/dist/antd.min.css';
12 | import '../mock';
13 |
14 | Mock.setup({
15 | timeout: '1000-2000',
16 | });
17 |
18 | Icon.iconRegister({
19 | edit: {
20 | outlined: EditOutlined,
21 | },
22 | delete: {
23 | outlined: DeleteOutlined,
24 | },
25 | plus: {
26 | outlined: PlusOutlined,
27 | },
28 | redo: {
29 | outlined: RedoOutlined,
30 | },
31 | eye: {
32 | outlined: EyeOutlined,
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/src/layout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Layout, Space, Tooltip } from 'antd';
3 | import {
4 | DingtalkOutlined,
5 | AlipayCircleOutlined,
6 | GithubOutlined,
7 | } from '@ant-design/icons';
8 | import Nav from '@sula/nav';
9 | import { ConfigProvider } from 'sula';
10 | import routes from '@/routes';
11 | import menus from '@/menus';
12 | import TipsSwitch from '@/components/tipsSwitch';
13 | import LanguageSwitch from '@/components/languageSwitch';
14 | import ThemeSwitch from '@/components/themeSwitch';
15 | import GuideWrapper from '@/components/guide';
16 | import { setLocale, getLocale, history } from 'umi';
17 | import ThemeContext, { GUIDE, DONE } from './themeContext';
18 |
19 | import zhCN from 'sula/es/localereceiver/zh_CN';
20 | import enUS from 'sula/es/localereceiver/en_US';
21 |
22 | import styles from './index.less';
23 |
24 | export default class LayoutComponent extends React.Component {
25 | state = {
26 | hiddenCustomControls: false,
27 | locale: getLocale(),
28 | theme: 'bluebird',
29 | hiddenGuideTips: localStorage.getItem(GUIDE) === DONE,
30 | };
31 |
32 | componentDidMount() {
33 | Nav.start({
34 | siderConfig: {
35 | menus,
36 | },
37 | getUserInfo: () => {
38 | return Promise.resolve({
39 | operatorName: 'Sula',
40 | });
41 | },
42 | userInfoViewVisible: false,
43 | getTopMenus: () => {
44 | return Promise.resolve([
45 | {
46 | name: 'DingDing',
47 | icon: ,
48 | },
49 | {
50 | name: 'Alipay',
51 | icon: ,
52 | },
53 | ]);
54 | },
55 | navRightExtra: this.navRightExtraRender(),
56 | breadcrumbVisible: true, // 是否显示面包屑
57 | routes, // 路由信息(项目路由配置信息)
58 | });
59 | }
60 |
61 | handleChangeLanguage = lang => {
62 | setLocale(lang);
63 | this.setState({
64 | locale: lang,
65 | });
66 | };
67 |
68 | handleChangeTheme = theme => {
69 | this.setState({
70 | theme,
71 | });
72 | };
73 |
74 | toggleQuestion = () => {
75 | this.setState({
76 | hiddenCustomControls: !this.state.hiddenCustomControls,
77 | });
78 | };
79 |
80 | navRightExtraRender = () => {
81 | return (
82 |
83 |
87 | {/* */}
88 |
89 |
90 | {
92 | window.open('https://github.com/umijs/sula');
93 | }}
94 | className={styles.github}
95 | />
96 |
97 |
98 | );
99 | };
100 |
101 | toggleGuideTips = visible => {
102 | this.setState({
103 | hiddenGuideTips: visible,
104 | });
105 | };
106 |
107 | render() {
108 | const { children } = this.props;
109 | const { hiddenCustomControls, hiddenGuideTips, locale, theme } = this.state;
110 |
111 | return (
112 |
119 |
120 |
121 |
122 |
127 | {children}
128 |
129 |
130 |
131 |
132 |
133 | );
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/layout/index.less:
--------------------------------------------------------------------------------
1 | @import (reference) '~antd/lib/style/themes/default';
2 |
3 | .wrapper {
4 | background: #f1f2f6;
5 | padding: 16px 24px;
6 | min-height: calc(100vh - 48px - 64px);
7 | }
8 |
9 | .navExtra {
10 | display: flex;
11 | color: rgb(64, 137, 247);
12 | user-select: none;
13 | margin-right: 16px;
14 | }
15 |
16 | .github {
17 | color: #24292f;
18 | font-size: 24px;
19 | }
20 |
--------------------------------------------------------------------------------
/src/layout/themeContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const theme = {
4 | hiddenCustomControls: false,
5 | hiddenGuideTips: false,
6 | toggleGuideTips: () => {},
7 | };
8 |
9 | const ThemeContext = React.createContext(theme);
10 |
11 | export default ThemeContext;
12 |
13 | export const GUIDE = 'sula-guide';
14 | export const DONE = 'sula-guide-done-01';
15 |
--------------------------------------------------------------------------------
/src/locales/en-US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | WELCOME_TO_UMI_WORLD: "{name}, welcome to umi's world",
3 | /** index */
4 | delete: 'Delete',
5 | create: 'Create',
6 | refresh: 'Refresh',
7 | reset: 'Reset',
8 |
9 | /** list */
10 | list_id: 'ID',
11 | list_info: 'Info',
12 | list_sender: 'Sender Name',
13 | list_recipient: 'Recipient Name',
14 | list_time: 'Time',
15 | list_description: 'Description',
16 | list_status: 'Status',
17 | list_price: 'Price Protection',
18 | list_operator: 'Operator',
19 | list_placeholder_search_id: 'Search Id',
20 | list_placeholder_search_status: 'Search status',
21 | list_placeholder_search_sender: 'Search sender name',
22 | list_placeholder_search_recipient: 'Search recipient name',
23 | list_confirm_delete: 'Are you sure to delete?',
24 | list_confirm_refresh: 'Are you sure to refresh?',
25 | list_tooltip_refresh: 'Refresh',
26 | list_tooltip_view: 'View',
27 | list_tooltip_edit: 'Edit',
28 | list_tooltip_delete: 'Delete',
29 | list_title: 'Basic Infomation',
30 |
31 | /** form */
32 | form_title_sender: 'Sender',
33 | form_title_recipient: 'Recipient',
34 | form_title_basic: 'Basic',
35 | form_title_wrapper: 'Wrapper',
36 | form_title_submit: 'Submit',
37 | form_title_other: 'Other',
38 | form_title_tab1: 'Tab1',
39 | form_title_tab2: 'Tab2',
40 | form_title_tab3: 'Tab3',
41 | form_title_tab4: 'Tab4',
42 | form_title_step1: 'Step1',
43 | form_title_step2: 'Step2',
44 | form_title_step3: 'Step3',
45 | form_title_step4: 'Step4',
46 | form_label_sender_name: 'Sender Name',
47 | form_label_sender_secrecy: 'Secercy',
48 | form_label_sender_number: 'Sender Number',
49 | form_label_sender_address: 'Sender Address',
50 | form_label_recipient_name: 'Recipient Name',
51 | form_label_recipient_time: 'Recipient Time',
52 | form_label_recipient_number: 'Recipient Number',
53 | form_label_recipient_address: 'Recipient Address',
54 | form_label_delivery_time: 'Delicery Time',
55 | form_label_price_protection: 'Price Protection',
56 | form_label_basic_description: 'Description',
57 | form_label_ruler: 'Ruler',
58 | form_label_description: 'Description',
59 | form_placeholder_sender_name: 'Please input sender name',
60 | form_placeholder_sender_number: 'Please input sender number',
61 | form_placeholder_sender_address: 'Please input sender address',
62 | form_placeholder_recipient_name: 'Please input recipient name',
63 | form_placeholder_recipient_number: 'Please input recipient number',
64 | form_placeholder_recipient_address: 'Please input recipient address',
65 | form_placeholder_basic_description: 'Please input description',
66 | form_placeholder_start_time: 'Start time',
67 | form_placeholder_end_time: 'End time',
68 | form_placeholder_price_protection: 'Please select price protection',
69 | form_placeholder_description: 'Please input description',
70 | form_validator_input: 'Please input',
71 |
72 | /** plugins */
73 | plugins_title_table: 'Table Plugins',
74 | plugins_title_text: 'Text',
75 | plugins_title_number: 'Number',
76 | plugins_title_select: 'Select',
77 | plugins_title_time: 'Time',
78 | plugins_title_field: 'Field',
79 | plugins_title_other: 'Other',
80 | plugins_placeholder_input: 'Add Something...',
81 | plugins_title_columnmerge: 'columnmerge plugin',
82 | plugins_title_operationgroup: 'operationgroup plugin',
83 | plugins_title_modalform: 'modalform plugin',
84 | plugins_title_drawerform: 'drawerform plugin',
85 | plugins_title_actions: 'action plugin',
86 | plugins_title_tag: 'tag plugin',
87 | plugins_title_badge: 'badge plugin',
88 | plugins_title_progress: 'progress plugin',
89 | };
90 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /** index */
3 | delete: '删除',
4 | create: '新增',
5 | refresh: '刷新',
6 | reset: '重置',
7 |
8 | /** list */
9 | list_id: 'ID',
10 | list_info: '信息',
11 | list_sender: '发送人',
12 | list_recipient: '接收人',
13 | list_time: '时间',
14 | list_description: '描述',
15 | list_status: '状态',
16 | list_price: '保护价格',
17 | list_operator: '操作',
18 | list_placeholder_search_id: '搜索Id',
19 | list_placeholder_search_status: '搜索状态',
20 | list_placeholder_search_sender: '搜索发送人',
21 | list_placeholder_search_recipient: '搜索接收人',
22 | list_confirm_delete: '确定要删除吗?',
23 | list_confirm_refresh: '确定要刷新吗?',
24 | list_tooltip_refresh: '刷新',
25 | list_tooltip_view: '查看',
26 | list_tooltip_edit: '编辑',
27 | list_tooltip_delete: '删除',
28 | list_title: '基本信息',
29 |
30 | /** form */
31 | form_title_sender: '发送',
32 | form_title_recipient: '接收',
33 | form_title_basic: '基础',
34 | form_title_wrapper: '嵌套',
35 | form_title_submit: '提交',
36 | form_title_other: '其他',
37 | form_title_tab1: '标签1',
38 | form_title_tab2: '标签2',
39 | form_title_tab3: '标签3',
40 | form_title_tab4: '标签4',
41 | form_title_step1: '步骤1',
42 | form_title_step2: '步骤2',
43 | form_title_step3: '步骤3',
44 | form_title_step4: '步骤4',
45 | form_label_sender_name: '发送人姓名',
46 | form_label_sender_secrecy: '是否保密',
47 | form_label_sender_number: '发送人号码',
48 | form_label_sender_address: '发送人地址',
49 | form_label_recipient_name: '接收人姓名',
50 | form_label_recipient_time: '接收时间',
51 | form_label_recipient_number: '接收人号码',
52 | form_label_recipient_address: '接收人地址',
53 | form_label_delivery_time: '送货时间',
54 | form_label_price_protection: '价格保护',
55 | form_label_basic_description: '其他信息',
56 | form_label_ruler: '规则',
57 | form_label_description: '描述',
58 | form_placeholder_sender_name: '请输入发送人姓名',
59 | form_placeholder_sender_number: '请输入发送人号码',
60 | form_placeholder_sender_address: '请输入发送人地址',
61 | form_placeholder_recipient_name: '请选择接收人姓名',
62 | form_placeholder_recipient_number: '请输入接收人号码',
63 | form_placeholder_recipient_address: '请输入接收人地址',
64 | form_placeholder_basic_description: '请输入其他信息',
65 | form_placeholder_start_time: '开始时间',
66 | form_placeholder_end_time: '结束时间',
67 | form_placeholder_price_protection: '请选择保护价格',
68 | form_placeholder_description: '请输入其他描述',
69 | form_validator_input: '该项为必填项',
70 |
71 | /** plugins */
72 | plugins_title_table: '表格插件',
73 | plugins_title_text: '文本输入',
74 | plugins_title_number: '数字输入',
75 | plugins_title_select: '选择输入',
76 | plugins_title_time: '时间输入',
77 | plugins_title_field: '文件输入',
78 | plugins_title_other: '其他',
79 | plugins_placeholder_input: '请输入',
80 | plugins_title_columnmerge: '列合并插件',
81 | plugins_title_operationgroup: '列合并插件',
82 | plugins_title_modalform: '弹框表单插件',
83 | plugins_title_drawerform: '抽屉表单插件',
84 | plugins_title_actions: '行为插件',
85 | plugins_title_tag: '标签插件',
86 | plugins_title_badge: '徽标数插件',
87 | plugins_title_progress: '进度条插件',
88 | };
89 |
--------------------------------------------------------------------------------
/src/menus.js:
--------------------------------------------------------------------------------
1 | import {
2 | BulbOutlined,
3 | TableOutlined,
4 | FormOutlined,
5 | LayoutOutlined,
6 | } from '@ant-design/icons';
7 |
8 | export default [
9 | {
10 | name: 'List',
11 | icon: ,
12 | hasChildren: true,
13 | children: [
14 | {
15 | name: 'Table',
16 | link: '#/list/basic',
17 | },
18 | {
19 | name: 'SingleSearch',
20 | link: '#/list/singlesearch',
21 | },
22 | {
23 | name: 'AdvancedSearch',
24 | link: '#/list/advancedsearch',
25 | },
26 | {
27 | name: 'StepQueryTable',
28 | link: '#/list/stepquerytable',
29 | },
30 | {
31 | name: 'NoPagination',
32 | link: '#/list/nopagination',
33 | },
34 | ],
35 | },
36 | {
37 | name: 'Form',
38 | icon: ,
39 | hasChildren: true,
40 | children: [
41 | {
42 | name: 'Card',
43 | link: '#/form/card',
44 | },
45 | {
46 | name: 'NestedCard',
47 | link: '#/form/nestedcard',
48 | },
49 | {
50 | name: 'Vertical',
51 | link: '#/form/vertical',
52 | },
53 | {
54 | name: 'Media',
55 | link: '#/form/media',
56 | },
57 | {
58 | name: 'Horizontal',
59 | link: '#/form/horizontal',
60 | },
61 | {
62 | name: 'StepForm',
63 | link: '#/form/stepform',
64 | },
65 | ],
66 | },
67 | {
68 | name: 'Form Layout',
69 | icon: ,
70 | hasChildren: true,
71 | children: [
72 | {
73 | name: 'form',
74 | link: '#/layout/form',
75 | },
76 | ],
77 | },
78 | {
79 | name: 'Plugin',
80 | icon: ,
81 | hasChildren: true,
82 | children: [
83 | {
84 | name: 'table',
85 | link: '#/plugin/table',
86 | },
87 | {
88 | name: 'form',
89 | link: '#/plugin/form',
90 | },
91 | ],
92 | },
93 | ];
94 |
--------------------------------------------------------------------------------
/src/pages/exception/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'umi';
3 | import { Result, Button } from 'antd';
4 |
5 | export default () => (
6 |
12 |
13 |
14 | }
15 | />
16 | );
17 |
--------------------------------------------------------------------------------
/src/pages/form/card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateForm from '@/components/jsonFormTemp';
3 | import { cardConfig as config } from '@sula/templates';
4 |
5 | export default (props) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/form/horizontal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateForm from '@/components/jsonFormTemp';
3 | import { horizontalConfig as config } from '@sula/templates';
4 |
5 | export default (props) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/form/media.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateForm from '@/components/jsonFormTemp';
3 | import { mediaConfig as config } from '@sula/templates';
4 |
5 | export default (props) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/form/nestedcard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateForm from '@/components/jsonFormTemp';
3 | import { nestcardConfig as config } from '@sula/templates';
4 |
5 | export default (props) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/form/stepform.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StepForm from '@/components/jsonFormTemp';
3 | import { Card } from 'sula';
4 | import { stepformConfig as config } from '@sula/templates';
5 |
6 | export default (props) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/pages/form/vertical.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateForm from '@/components/jsonFormTemp';
3 | import { verticalConfig as config } from '@sula/templates';
4 |
5 | export default (props) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/list/advancedsearch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QueryTable from '@/components/jsonTableTemp';
3 | import { advancesearchConfig as config } from '@sula/templates';
4 |
5 | export default () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/list/basic.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QueryTable from '@/components/jsonTableTemp';
3 | import { useIntl } from 'umi';
4 |
5 | export default () => {
6 | // 国际化配置
7 | const inti = useIntl();
8 |
9 | const config = {
10 | layout: 'vertical',
11 | rowKey: 'id',
12 | columns: [
13 | {
14 | key: 'id',
15 | title: inti.formatMessage({ id: 'list_id' }),
16 | sorter: true,
17 | render: {
18 | type: 'button',
19 | props: {
20 | type: 'link',
21 | size: 'small',
22 | children: 'SERIAL_NUMBER_#{text}',
23 | href: '#/form/card/view/#{text}',
24 | style: {
25 | padding: 0,
26 | },
27 | },
28 | },
29 | },
30 | {
31 | key: 'senderName',
32 | title: inti.formatMessage({ id: 'list_sender' }),
33 | filterRender: 'search',
34 | },
35 | {
36 | key: 'recipientName',
37 | title: inti.formatMessage({ id: 'list_recipient' }),
38 | filters: [
39 | { text: 'Jack', value: 'Jack' },
40 | { text: 'Lucy', value: 'Lucy' },
41 | { text: 'Lily', value: 'lily' },
42 | { text: 'Mocy', value: 'Mocy' },
43 | ],
44 | },
45 | {
46 | key: 'status',
47 | title: inti.formatMessage({ id: 'list_status' }),
48 | render: {
49 | type: 'tag',
50 | props: {
51 | children: '#{text}',
52 | color:
53 | '#{text === "dispatching" ? "#2db7f5" : text === "success" ? "#87d068" : "#f50"}',
54 | },
55 | },
56 | },
57 | {
58 | key: 'operator',
59 | title: inti.formatMessage({ id: 'list_operator' }),
60 | render: [
61 | {
62 | type: 'icon',
63 | props: {
64 | type: 'edit',
65 | },
66 | action: {
67 | type: 'route',
68 | path: '/form/card/edit/#{record.id}',
69 | },
70 | },
71 | {
72 | type: 'icon',
73 | props: {
74 | type: 'delete',
75 | },
76 | tooltip: inti.formatMessage({ id: 'list_tooltip_delete' }),
77 | confirm: inti.formatMessage({ id: 'list_confirm_delete' }),
78 | visible: '#{record.id % 3 === 0}',
79 | action: [
80 | {
81 | type: 'request',
82 | url: '/api/manage/delete.json',
83 | method: 'POST',
84 | params: {
85 | rowKeys: '#{record.id}',
86 | },
87 | },
88 | 'refreshtable',
89 | ],
90 | },
91 | ],
92 | },
93 | ],
94 | actionsRender: [
95 | {
96 | type: 'button',
97 | props: {
98 | children: inti.formatMessage({ id: 'create' }),
99 | type: 'primary',
100 | },
101 | action: {
102 | type: 'route',
103 | path: '/form/card/create',
104 | },
105 | },
106 | ],
107 | fields: [
108 | {
109 | name: 'id',
110 | label: inti.formatMessage({ id: 'list_id' }),
111 | field: {
112 | type: 'input',
113 | props: {
114 | placeholder: inti.formatMessage({
115 | id: 'list_placeholder_search_id',
116 | }),
117 | },
118 | },
119 | },
120 | {
121 | name: 'senderName',
122 | label: inti.formatMessage({ id: 'form_label_sender_name' }),
123 | field: {
124 | type: 'input',
125 | props: {
126 | placeholder: inti.formatMessage({
127 | id: 'form_placeholder_sender_name',
128 | }),
129 | },
130 | },
131 | },
132 | {
133 | name: 'recipientName',
134 | label: inti.formatMessage({ id: 'form_placeholder_recipient_name' }),
135 | field: {
136 | type: 'select',
137 | props: {
138 | mode: 'multiple',
139 | allowClear: true,
140 | placeholder: inti.formatMessage({
141 | id: 'form_placeholder_recipient_name',
142 | }),
143 | },
144 | },
145 | remoteSource: {
146 | url: '/api/manage/recipientList.json',
147 | },
148 | },
149 | ],
150 | remoteDataSource: {
151 | url: '/api/manage/list.json',
152 | method: 'post',
153 | },
154 | };
155 |
156 | return ;
157 | };
158 |
--------------------------------------------------------------------------------
/src/pages/list/nopagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QueryTable from '@/components/jsonTableTemp';
3 | import { nopaginationConfig as config } from '@sula/templates';
4 |
5 | export default () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/list/singlesearch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QueryTable from '@/components/jsonTableTemp';
3 | import { singlesearchConfig as config } from '@sula/templates';
4 |
5 | export default () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/list/stepquerytable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StepQueryTable from '@/components/jsonTableTemp';
3 | import { stepquerytableConfig as config } from '@sula/templates';
4 |
5 | export default () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/sulalayout/components/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Radio, Input } from 'antd';
3 |
4 | export function RadioGroup(props) {
5 | const { source = [], ...rest } = props;
6 | return (
7 |
8 | {source.map((ele) => (
9 |
10 | {ele}
11 |
12 | ))}
13 |
14 | );
15 | }
16 |
17 | export function TextArea(props) {
18 | const [height, setHeight] = useState(32);
19 |
20 | return (
21 |
22 | {
24 | setHeight(height);
25 | }}
26 | style={{ height, width: '100%' }}
27 | value={`height: ${height}`}
28 | {...props}
29 | />
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/sulalayout/components/style.less:
--------------------------------------------------------------------------------
1 | :global {
2 | .react-resizable {
3 | position: relative;
4 | }
5 | .react-resizable-handle {
6 | position: absolute;
7 | width: 20px;
8 | height: 20px;
9 | background-repeat: no-repeat;
10 | background-origin: content-box;
11 | box-sizing: border-box;
12 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
13 | background-position: bottom right;
14 | padding: 0 3px 3px 0;
15 | }
16 | .react-resizable-handle-sw {
17 | bottom: 0;
18 | left: 0;
19 | cursor: sw-resize;
20 | transform: rotate(90deg);
21 | }
22 | .react-resizable-handle-se {
23 | bottom: 0;
24 | right: 0;
25 | cursor: se-resize;
26 | }
27 | .react-resizable-handle-nw {
28 | top: 0;
29 | left: 0;
30 | cursor: nw-resize;
31 | transform: rotate(180deg);
32 | }
33 | .react-resizable-handle-ne {
34 | top: 0;
35 | right: 0;
36 | cursor: ne-resize;
37 | transform: rotate(270deg);
38 | }
39 | .react-resizable-handle-w,
40 | .react-resizable-handle-e {
41 | top: 50%;
42 | margin-top: -10px;
43 | cursor: ew-resize;
44 | }
45 | .react-resizable-handle-w {
46 | left: 0;
47 | transform: rotate(135deg);
48 | }
49 | .react-resizable-handle-e {
50 | right: 0;
51 | transform: rotate(315deg);
52 | }
53 | .react-resizable-handle-n,
54 | .react-resizable-handle-s {
55 | left: 50%;
56 | margin-left: -10px;
57 | cursor: ns-resize;
58 | }
59 | .react-resizable-handle-n {
60 | top: 0;
61 | transform: rotate(225deg);
62 | }
63 | .react-resizable-handle-s {
64 | bottom: 0;
65 | transform: rotate(45deg);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/sulalayout/form.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Form } from 'sula';
3 | import { message, Popover, Collapse, Space, Button, Col } from 'antd';
4 | import copy from 'copy-to-clipboard';
5 | import { isEmpty, isNumber } from 'lodash';
6 | import { TextArea, RadioGroup } from './components';
7 |
8 | const initialLayoutValue = {
9 | fieldCount: 3,
10 | layout: 'horizontal',
11 | iSpan: 0,
12 | cols: 3,
13 | };
14 |
15 | const initialSingleValue = {
16 | hiddenLabel: false,
17 | span: undefined,
18 | wSpan: undefined,
19 | offset: undefined,
20 | lOffset: undefined,
21 | wOffset: undefined,
22 | showOffsetSetting: false,
23 | };
24 |
25 | export default () => {
26 | const [fieldList, setFieldList] = useState([]);
27 | const [layoutConfig, setLayoutConfig] = useState(initialLayoutValue);
28 |
29 | const layoutFormRef = React.useRef();
30 |
31 | const formLayoutConfig = {
32 | ref: layoutFormRef,
33 | container: 'card',
34 | onValuesChange: (value, values) => {
35 | setLayoutConfig(values);
36 | },
37 | initialValues: initialLayoutValue,
38 | itemLayout: {
39 | span: 12,
40 | },
41 | fields: [
42 | {
43 | name: 'fieldCount',
44 | label: '表单项数量',
45 | field: {
46 | type: 'inputnumber',
47 | props: {
48 | min: 1,
49 | max: 8,
50 | },
51 | },
52 | },
53 | {
54 | name: 'layout',
55 | label: '布局',
56 | field: () => {
57 | return ;
58 | },
59 | },
60 | {
61 | name: 'cols',
62 | label: 'cols(多列布局)',
63 | field: {
64 | type: 'slider',
65 | props: {
66 | style: { width: '80%' },
67 | min: 0,
68 | max: 4,
69 | dots: true,
70 | },
71 | },
72 | },
73 | {
74 | name: 'iSpan',
75 | label: '每项表单占位栅格数',
76 | field: {
77 | type: 'slider',
78 | props: {
79 | style: { width: '80%' },
80 | min: 0,
81 | max: 24,
82 | dots: true,
83 | step: 2,
84 | },
85 | },
86 | dependency: {
87 | value: {
88 | relates: [['cols']],
89 | inputs: ['*'],
90 | output: 0,
91 | },
92 | },
93 | },
94 | {
95 | name: 'lSpan',
96 | label: '标题占位格数',
97 | field: {
98 | type: 'slider',
99 | props: {
100 | style: { width: '80%' },
101 | min: 0,
102 | max: 24,
103 | dots: true,
104 | step: 2,
105 | },
106 | },
107 | },
108 | {
109 | name: 'wSpan',
110 | label: '表单控件占位格数',
111 | field: {
112 | type: 'slider',
113 | props: {
114 | style: { width: '80%' },
115 | min: 0,
116 | max: 24,
117 | dots: true,
118 | step: 2,
119 | },
120 | },
121 | },
122 | {
123 | label: true,
124 | colon: false,
125 | render: [
126 | {
127 | type: 'button',
128 | props: {
129 | children: '获取配置',
130 | onClick: onCopy,
131 | type: 'primary',
132 | },
133 | },
134 | {
135 | type: 'button',
136 | props: {
137 | children: '重置',
138 | onClick: () => {},
139 | },
140 | },
141 | ],
142 | },
143 | ],
144 | };
145 |
146 | const { iSpan, lSpan, wSpan, fieldCount, layout, cols } = layoutConfig;
147 |
148 | const fields = new Array(fieldCount).fill(0).map((v, idx) => {
149 | const content = (
150 |