├── .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 | 117 | 118 | 125 | 128 | 129 | 130 | 131 | ); 132 | return ( 133 |
134 | 143 |
148 |
149 |
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 |
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 | 17 | { 19 | handleChangeLanguage('zh-CN'); 20 | }} 21 | > 22 | 中文 23 | 24 | { 26 | handleChangeLanguage('en-US'); 27 | }} 28 | > 29 | English 30 | 31 | 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 | 12 | { 14 | handleChangeTheme('default'); 15 | }} 16 | > 17 | Antd 18 | 19 | { 21 | handleChangeTheme('bluebird'); 22 | }} 23 | > 24 | Sula 25 | 26 | 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 |
{ 271 | const values = ctx.form.getFieldsValue(); 272 | updateFieldList(values, idx); 273 | }, 274 | }, 275 | { 276 | type: 'button', 277 | props: { 278 | children: '重置', 279 | }, 280 | action: (ctx) => { 281 | // 因为initialValues使用的fieldList,所以不能用reset 282 | ctx.form.setFieldsValue(initialSingleValue); 283 | updateFieldList(initialSingleValue, idx); 284 | }, 285 | }, 286 | ]} 287 | /> 288 | ); 289 | 290 | const { 291 | hiddenLabel, 292 | span, 293 | offset, 294 | lOffset, 295 | wOffset, 296 | lSpan: itemLSpan, 297 | wSpan: itemWSpan, 298 | textareaProps, 299 | ...restFieldConfig 300 | } = fieldList[idx] || {}; 301 | 302 | return { 303 | name: 'field' + idx, 304 | ...(!hiddenLabel 305 | ? { label: 'field' + idx } 306 | : { label: true, colon: false }), 307 | itemLayout: { 308 | ...(span ? { span } : {}), 309 | ...(isNumber(offset) ? { offset } : {}), 310 | ...(isNumber(itemLSpan) || isNumber(lOffset) 311 | ? { 312 | labelCol: { 313 | span: itemLSpan, 314 | offset: lOffset, 315 | }, 316 | } 317 | : {}), 318 | ...(isNumber(itemWSpan) || isNumber(wOffset) 319 | ? { 320 | wrapperCol: { 321 | span: itemWSpan, 322 | offset: wOffset, 323 | }, 324 | } 325 | : {}), 326 | }, 327 | field: () => ( 328 | 329 |
330 | {/** 加div保证popover显示 */} 331 |