├── .editorconfig ├── .fatherrc.ts ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── README.md ├── example ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── config │ ├── config.ts │ └── routeConfig.ts ├── mock │ └── route.ts ├── package.json ├── src │ ├── constant.ts │ ├── layouts │ │ ├── BlankLayout │ │ │ └── index.tsx │ │ └── LoginLayout │ │ │ └── index.tsx │ ├── pages │ │ ├── 404.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── login │ │ │ └── index.tsx │ └── services │ │ └── api-lock.json ├── tsconfig.json └── typing.d.ts ├── interfaces ├── api.ts ├── common.ts └── screen.ts ├── package.json ├── src ├── api │ └── index.tsx ├── constantConfig.ts ├── index.ts ├── manage │ ├── index.ts │ └── templates │ │ ├── formActionMethods.ts │ │ ├── formActionMethodsModal.ts │ │ ├── index.ts │ │ ├── longDetail.ts │ │ ├── longDetailModal.ts │ │ ├── longForm.ts │ │ ├── longFormModal.ts │ │ ├── shortDetail.ts │ │ ├── shortDetailModal.ts │ │ ├── shortForm.ts │ │ ├── shortFormModal.ts │ │ ├── table.ts │ │ └── util.ts ├── screen │ ├── index.ts │ └── templates │ │ ├── generateCol.ts │ │ ├── generateLayout.ts │ │ ├── generateRow.ts │ │ └── generateScreen.ts └── utils │ ├── index.ts │ ├── removeUnusedImport.ts │ ├── writeNewMenu.ts │ └── writeNewRoute.ts ├── tsconfig.json ├── ui ├── components │ ├── ConfigActions │ │ └── index.tsx │ ├── FormItemConfig │ │ ├── index.module.less │ │ └── index.tsx │ ├── FormItemConfigDrawer │ │ ├── index.tsx │ │ └── props.ts │ ├── FormItemsDrawer │ │ └── index.tsx │ └── Title │ │ ├── index.module.less │ │ └── index.tsx ├── hooks │ ├── useCard.ts │ ├── useConfig.ts │ ├── useConfigVisible.ts │ ├── useFormItem.ts │ ├── useScreen.ts │ └── useTable.ts ├── index.tsx ├── pages │ ├── manage │ │ ├── Context.ts │ │ ├── components │ │ │ ├── ConstantConfigAction │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── Dashboard │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── ExportActions │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── ImportAction │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── PathMenuAction │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── TemplateList │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── template.json │ │ │ ├── content │ │ │ │ ├── LongDetailContent │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── LongDetailModalContent │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── LongFormContent │ │ │ │ │ └── index.tsx │ │ │ │ ├── LongFormModalContent │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShortDetailContent │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShortDetailModalContent │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShortFormContent │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShortFormModalContent │ │ │ │ │ └── index.tsx │ │ │ │ └── TableContent │ │ │ │ │ ├── TitleWithActions │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ └── drawers │ │ │ │ ├── ApiConfigDrawer │ │ │ │ └── index.tsx │ │ │ │ ├── CardConfigDrawer │ │ │ │ └── index.tsx │ │ │ │ ├── ShortFormConfigDrawer │ │ │ │ └── index.tsx │ │ │ │ ├── TableColumnConfigDrawer │ │ │ │ └── index.tsx │ │ │ │ └── TableConfigDrawer │ │ │ │ └── index.tsx │ │ ├── index.module.less │ │ └── index.tsx │ ├── mobile │ │ ├── Context.ts │ │ └── index.tsx │ └── screen │ │ ├── ColConfigDrawer │ │ ├── index.tsx │ │ └── props.ts │ │ ├── Context.ts │ │ ├── Dashboard │ │ ├── index.module.less │ │ └── index.tsx │ │ ├── ScreenConfigDrawer │ │ └── index.tsx │ │ ├── helper.tsx │ │ ├── index.tsx │ │ ├── preview.ts │ │ └── structure.ts └── utils │ └── index.ts └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'prod'; 2 | 3 | export default [ 4 | { 5 | target: 'node', 6 | cjs: { type: 'babel', lazy: true }, 7 | disableTypeCheck: true, 8 | extraBabelPlugins: [ 9 | [ 10 | 'babel-plugin-import', 11 | { libraryName: 'antd', libraryDirectory: 'es', style: true }, 12 | 'antd', 13 | ], 14 | ], 15 | }, 16 | { 17 | entry: 'ui/index.tsx', 18 | umd: { 19 | name: 'tasks', 20 | minFile: isProd, 21 | sourcemap: !isProd, 22 | }, 23 | extraExternals: ['antd', 'react', 'react-dom', 'xterm'], 24 | typescriptOpts: { 25 | check: false, 26 | globals: { 27 | antd: 'window.antd', 28 | react: 'window.React', 29 | 'react-dom': 'window.ReactDOM', 30 | }, 31 | }, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .changelog 3 | .umi 4 | .umi-test 5 | .umi-production 6 | .DS_Store 7 | /lib 8 | dist 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | package.json 3 | 4 | # fixtures 5 | **/fixtures/** 6 | 7 | # templates 8 | **/templates/** 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "proseWrap": "never" 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to plugin 2 | 3 | ## Set up 4 | 5 | Install dev deps after git clone the repo. 6 | 7 | ```bash 8 | # npm is not allowed. 9 | $ yarn 10 | ``` 11 | 12 | ## Build 13 | 14 | Transform with babel and rollup. 15 | 16 | ```bash 17 | $ yarn build 18 | 19 | # Build and monitor file changes 20 | $ yarn build --watch 21 | 22 | ``` 23 | 24 | ## Dev Plugin 25 | 26 | ```bash 27 | # This Step must only be executed in Build 28 | $ yarn dev 29 | ``` 30 | 31 | ## Debug 32 | 33 | TODO 34 | 35 | ## Test 36 | 37 | ```bash 38 | $ yarn test 39 | ``` 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # umi-plugin-page-creator 2 | 3 | [![NPM version](https://img.shields.io/npm/v/umi-plugin-page-creator.svg?style=flat)](https://npmjs.org/package/umi-plugin-page-creator) [![NPM downloads](http://img.shields.io/npm/dm/umi-plugin-page-creator.svg?style=flat)](https://npmjs.org/package/umi-plugin-page-creator) 4 | 5 | ## Install 6 | 7 | ```bash 8 | # or npm install 9 | $ yarn add umi-plugin-page-creator -D 10 | ``` 11 | 12 | ## Development UI 13 | 14 | UI mini start: 15 | 16 | ```bash 17 | $ yarn watch 18 | $ yarn start 19 | ``` 20 | 21 | 22 | 23 | ## Usage 24 | 25 | Umi会自动识别以umi-plugin-*开头的插件,并自动加载。不需要额外的配置 26 | 27 | ## LICENSE 28 | 29 | MIT 30 | -------------------------------------------------------------------------------- /example/.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 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | ESLINT=1 3 | -------------------------------------------------------------------------------- /example/.eslintignore: -------------------------------------------------------------------------------- 1 | /scripts 2 | **/node_modules/** 3 | _scripts 4 | /lambda/mock/** 5 | _test_ 6 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module", // Allows for the use of imports 6 | ecmaFeatures: { 7 | jsx: true // Allows for the parsing of JSX 8 | } 9 | }, 10 | settings: { 11 | react: { 12 | version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 13 | } 14 | }, 15 | extends: [ 16 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 17 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 18 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 19 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 20 | ], 21 | plugins: ['import'], 22 | rules: { 23 | 'prettier/prettier': 'off', 24 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 25 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 26 | 'import/newline-after-import': 'error', 27 | 'react/display-name': 'off', 28 | '@typescript-eslint/explicit-module-boundary-types': 'off', 29 | 'react/prop-types': 'off', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /.umi 2 | /.umi-production 3 | /dist 4 | /node_modules 5 | /yarn.lock 6 | -------------------------------------------------------------------------------- /example/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | umi-block.json 24 | -------------------------------------------------------------------------------- /example/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /example/config/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 11:07:13 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-07 11:22:12 8 | */ 9 | import { defineConfig } from 'umi'; 10 | import routeConfig from './routeConfig'; 11 | 12 | export default defineConfig({ 13 | presets: [require.resolve('@umijs/preset-ui')], 14 | plugins: [require.resolve('../../lib')], 15 | routes: routeConfig, 16 | }); 17 | -------------------------------------------------------------------------------- /example/config/routeConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 11:41:44 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-20 12:04:03 8 | */ 9 | export default [ 10 | { 11 | path: '/user', 12 | component: '../layouts/LoginLayout', 13 | routes: [ 14 | { 15 | path: '/user/login', 16 | component: './login', 17 | title: '登录', 18 | }, 19 | ], 20 | }, 21 | { 22 | path: '/', 23 | component: '../layouts/BlankLayout', 24 | routes: [ 25 | { 26 | path: '/homepage', 27 | component: './index', 28 | }, 29 | { 30 | path: '/', 31 | redirect: '/homepage', 32 | }, 33 | { 34 | path: '*', 35 | component: './404', 36 | }, 37 | ], 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /example/mock/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2019-10-27 16:26:45 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-20 11:10:41 8 | */ 9 | export default { 10 | '/resource': { 11 | success: true, 12 | data: [], 13 | code: 20000, 14 | message: '成功', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "umi dev", 8 | "eslint:fix": "eslint --fix --ext .ts,.tsx src/" 9 | }, 10 | "dependencies": { 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^16.9.35", 16 | "@types/react-dom": "^16.9.8", 17 | "@typescript-eslint/eslint-plugin": "^3.1.0", 18 | "@typescript-eslint/parser": "^3.1.0", 19 | "eslint": "^7.1.0", 20 | "eslint-config-prettier": "^6.11.0", 21 | "eslint-plugin-import": "^2.20.2", 22 | "eslint-plugin-prettier": "^3.1.3", 23 | "eslint-plugin-react": "^7.20.0", 24 | "prettier": "^2.0.5", 25 | "typescript": "^3.8.3" 26 | }, 27 | "engines": { 28 | "node": ">=8.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/src/constant.ts: -------------------------------------------------------------------------------- 1 | import test from 'ttt'; 2 | 3 | const a = 123; 4 | const b = 555; 5 | -------------------------------------------------------------------------------- /example/src/layouts/BlankLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BlankLayout: React.FC = (props) => ( 4 |
5 |
layout header
6 | {props.children} 7 |
8 | ); 9 | 10 | export default BlankLayout; 11 | -------------------------------------------------------------------------------- /example/src/layouts/LoginLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginLayout: React.FC = (props) => ( 4 |
5 |
login header
6 | {props.children} 7 |
8 | ); 9 | 10 | export default LoginLayout; 11 | -------------------------------------------------------------------------------- /example/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>
404
; 4 | -------------------------------------------------------------------------------- /example/src/pages/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | html, 3 | #root { 4 | height: 100%; 5 | margin: 0; 6 | } 7 | 8 | a { 9 | color: #1890ff; 10 | cursor: pointer; 11 | } 12 | 13 | .normal { 14 | background: #ddd; 15 | height: 100vh; 16 | padding: 48px; 17 | } 18 | -------------------------------------------------------------------------------- /example/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.css'; 3 | 4 | export default () =>
Hello Umi!
; 5 | -------------------------------------------------------------------------------- /example/src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>
login page
; 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "allowJs": true, 12 | "lib": ["esnext", "dom"], 13 | "noEmit": true, 14 | "strict": true, 15 | "experimentalDecorators": true, 16 | "allowSyntheticDefaultImports": true, 17 | "isolatedModules": false, 18 | "forceConsistentCasingInFileNames": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noImplicitAny": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "strictNullChecks": true, 25 | "suppressImplicitAnyIndexErrors": true, 26 | "downlevelIteration": true, 27 | "resolveJsonModule": true, 28 | "strictPropertyInitialization": true, 29 | "skipLibCheck": true, 30 | "paths": { 31 | "@/*": ["src/*"], 32 | "@@/*": ["src/.umi/*"] 33 | } 34 | }, 35 | "include": ["./src/**/*", "./typings.d.ts"], 36 | } 37 | -------------------------------------------------------------------------------- /example/typing.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | -------------------------------------------------------------------------------- /interfaces/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-18 21:18:11 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-22 15:29:38 8 | */ 9 | 10 | /** api-lock.json的结构 */ 11 | export interface ApiJSON { 12 | name: string; 13 | mods: ApiMod[]; 14 | baseClasses: ApiClass[]; 15 | } 16 | 17 | /** 模块 */ 18 | export interface ApiMod { 19 | name: string; 20 | description: string; 21 | interfaces: ApiInterface[]; 22 | } 23 | 24 | /** 模块下的接口 */ 25 | export interface ApiInterface { 26 | consumes: string[]; 27 | description: string; 28 | name: string; 29 | method: string; 30 | path: string; 31 | response: DataType; 32 | parameters: ApiProperty[]; 33 | } 34 | 35 | export interface DataType { 36 | typeArgs: DataType[]; 37 | typeName: string; 38 | isDefsType: boolean; 39 | } 40 | 41 | export interface ApiProperty { 42 | in?: string; 43 | description: string; 44 | name: string; 45 | required: boolean; 46 | dataType: DataType; 47 | } 48 | 49 | export interface ApiClass { 50 | name: string; 51 | properties: ApiProperty[]; 52 | description?: string; 53 | required: boolean; 54 | } 55 | 56 | export interface BaseClass { 57 | name: string; 58 | dbId: string; 59 | description: string; 60 | properties: { 61 | label: string; 62 | value: string; 63 | required: boolean; 64 | }[]; 65 | } 66 | -------------------------------------------------------------------------------- /interfaces/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 21:58:39 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-09 17:03:40 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { ColumnType } from 'antd/lib/table'; 11 | 12 | export type FormItemType = 13 | | 'input' 14 | | 'cascader' 15 | | 'date' 16 | | 'range' 17 | | 'time' 18 | | 'number' 19 | | 'radio' 20 | | 'checkbox' 21 | | 'switch' 22 | | 'slider' 23 | | 'select' 24 | | 'treeselect' 25 | | 'upload' 26 | | 'rate' 27 | | 'password' 28 | | 'textarea'; 29 | 30 | /** 适用于input/date/time/number */ 31 | export type FormItemProps = { 32 | type: FormItemType; 33 | name: string; 34 | label: string; 35 | placeholder?: string | [string, string]; 36 | [key: string]: unknown; 37 | }; 38 | 39 | export type TemplateType = 40 | | 'short-form' 41 | | 'long-form' 42 | | 'one-column-form-modal' 43 | | 'two-columns-form-modal' 44 | | 'short-detail' 45 | | 'long-detail' 46 | | 'one-column-detail-modal' 47 | | 'two-columns-detail-modal' 48 | | 'table'; 49 | 50 | export type ShortFormConfig = { 51 | title: string; 52 | }; 53 | 54 | export type Template = { 55 | name: string; 56 | type: TemplateType; 57 | image: string; 58 | }; 59 | 60 | export type AjaxResponse = { 61 | success: boolean; 62 | data?: T; 63 | message: string; 64 | }; 65 | 66 | export type CardItemProps = { 67 | title: string; 68 | formItems: FormItemProps[]; 69 | }; 70 | 71 | /** 导出/导入的json格式 */ 72 | export type ConfigProps = { 73 | formConfig?: Store; 74 | formItems?: FormItemProps[]; 75 | initialFetch?: string[]; 76 | submitFetch?: string[]; 77 | cards?: CardItemProps[]; 78 | tableConfig?: Store; 79 | columns?: ColumnType[]; 80 | }; 81 | 82 | /** 83 | * 表字段验证配置 84 | */ 85 | export interface TableVerificationRuleList { 86 | appVerificationRuleCode: string; 87 | tableCode: string; 88 | tableName: string; 89 | tableFieldCode: string; 90 | isRequired: boolean; 91 | requiredMaxLength: number; 92 | requiredMinLength: number; 93 | fieldName: string; 94 | fieldType: string; 95 | pattern: string; 96 | } 97 | -------------------------------------------------------------------------------- /interfaces/screen.ts: -------------------------------------------------------------------------------- 1 | import { ColSize } from 'antd/lib/col'; 2 | 3 | export interface ScreenConfig { 4 | title: string; 5 | titleStyle: string; 6 | gutter: number; 7 | left: { 8 | xs: ColSize; 9 | sm: ColSize; 10 | md: ColSize; 11 | lg: ColSize; 12 | xl: ColSize; 13 | xxl: ColSize; 14 | rows: ScreenRow[]; 15 | }; 16 | center: { 17 | xs: ColSize; 18 | sm: ColSize; 19 | md: ColSize; 20 | lg: ColSize; 21 | xl: ColSize; 22 | xxl: ColSize; 23 | rows: ScreenRow[]; 24 | }; 25 | right: { 26 | xs: ColSize; 27 | sm: ColSize; 28 | md: ColSize; 29 | lg: ColSize; 30 | xl: ColSize; 31 | xxl: ColSize; 32 | rows: ScreenRow[]; 33 | }; 34 | } 35 | 36 | export interface ScreenRow { 37 | height: number; 38 | cols: ScreenCol[]; 39 | } 40 | 41 | export interface ScreenCol { 42 | xs?: ColSize; 43 | sm?: ColSize; 44 | md?: ColSize; 45 | lg?: ColSize; 46 | xl?: ColSize; 47 | xxl?: ColSize; 48 | type: ColType; 49 | chartConfig: ChartConfig; 50 | } 51 | 52 | export type LayoutType = 'left' | 'center' | 'right'; 53 | export type ColType = 54 | | 'custom' 55 | | 'bar' 56 | | 'groupBar' 57 | | 'rangeBar' 58 | | 'barLine' 59 | | 'groupBarLine' 60 | | 'column' 61 | | 'groupColumn' 62 | | 'rangeColumn' 63 | | 'circle' 64 | | 'rose' 65 | | 'line' 66 | | 'wave' 67 | | 'radar' 68 | | 'circleStackBar' 69 | | 'scatter' 70 | | 'stackArea' 71 | | 'stackBar' 72 | | 'stackRose' 73 | | 'waterfall' 74 | | 'map' 75 | | 'table' 76 | | 'rank'; 77 | 78 | export interface ChartConfig { 79 | [key: string]: unknown; 80 | } 81 | 82 | export interface ScreenColConfig { 83 | layoutType: LayoutType; 84 | rowIndex: number; 85 | colIndex: number; 86 | config: Partial; 87 | } 88 | 89 | export interface ScreenConfigPayload { 90 | title: string; 91 | titleStyle: object; 92 | gutter: number; 93 | layout: ScreenConfigPayloadLayout[]; 94 | } 95 | 96 | export interface ScreenConfigPayloadLayout { 97 | name: string; 98 | xs?: ColSize; 99 | sm?: ColSize; 100 | md?: ColSize; 101 | lg?: ColSize; 102 | xl?: ColSize; 103 | xxl?: ColSize; 104 | rows: ScreenConfigPayloadRow[]; 105 | } 106 | 107 | export interface ScreenConfigPayloadRow { 108 | name: string; 109 | height: number; 110 | cols: ScreenConfigPayloadCol[]; 111 | } 112 | 113 | export interface ScreenConfigPayloadCol { 114 | name: string; 115 | xs?: ColSize; 116 | sm?: ColSize; 117 | md?: ColSize; 118 | lg?: ColSize; 119 | xl?: ColSize; 120 | xxl?: ColSize; 121 | type: ColType; 122 | chartConfig: ChartConfig; 123 | } 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umi-plugin-page-creator", 3 | "version": "1.7.3", 4 | "description": "", 5 | "authors": { 6 | "name": "chenjie", 7 | "email": "chenjie@thundersdata.com" 8 | }, 9 | "repository": "/umi-plugin-page-creator", 10 | "main": "lib/index.js", 11 | "scripts": { 12 | "start": "cross-env UMI_UI=1 APP_ROOT=example umi dev", 13 | "watch": "father-build --watch", 14 | "build": "rm -rf dist lib && cross-env NODE_ENV=prod father-build", 15 | "ui:build": "ui-build", 16 | "prettier": "prettier --parser=typescript --write '{src,ui}/**/*.ts?(x)'", 17 | "test": "umi-test", 18 | "test:coverage": "umi-test --coverage", 19 | "test:update": "umi-test --updateSnapshot" 20 | }, 21 | "lint-staged": { 22 | "*.ts?(x)": [ 23 | "git add" 24 | ] 25 | }, 26 | "devDependencies": { 27 | "@testing-library/react": "^9.4.0", 28 | "@testing-library/react-hooks": "^3.2.1", 29 | "@types/classnames": "^2.2.10", 30 | "@types/codemirror": "^0.0.95", 31 | "@types/faker": "^4.1.11", 32 | "@types/jest": "^25.1.3", 33 | "@types/node": "^13.7.7", 34 | "@umijs/fabric": "^2.0.8", 35 | "@umijs/preset-ui": "^2.1.13", 36 | "@umijs/test": "^3.0.10", 37 | "@umijs/test-utils": "^1.0.0", 38 | "@umijs/ui-builder": "^0.0.5", 39 | "@umijs/ui-types": "^2.0.0-beta.2", 40 | "antd": "^4.2.0", 41 | "body-parser": "^1.18.2", 42 | "classnames": "^2.2.6", 43 | "codemirror": "^5.54.0", 44 | "copy-to-clipboard": "^3.3.1", 45 | "cross-env": "^6.0.3", 46 | "express": "^4.15.3", 47 | "faker": "^4.1.0", 48 | "father-build": "^1.17.2", 49 | "immer": "^6.0.5", 50 | "lerna": "^3.20.2", 51 | "lint-staged": "^10.0.8", 52 | "npm-run-all": "^4.1.5", 53 | "pify": "^5.0.0", 54 | "puppeteer": "^1.20.0", 55 | "query-string": "^6.11.1", 56 | "react": "^16.12.0", 57 | "react-codemirror2": "^7.2.0", 58 | "react-dom": "^16.12.0", 59 | "react-test-renderer": "^16.9.0", 60 | "test-umi-plugin": "^0.1.0", 61 | "typescript": "^3.9.2", 62 | "umi": "^3.1.0", 63 | "use-immer": "^0.4.0", 64 | "yorkie": "^2.0.0" 65 | }, 66 | "gitHooks": { 67 | "pre-commit": "lint-staged" 68 | }, 69 | "files": [ 70 | "dist", 71 | "lib" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /src/api/index.tsx: -------------------------------------------------------------------------------- 1 | import { IApi } from 'umi'; 2 | import { existsSync } from 'fs'; 3 | import { ApiJSON } from '../../interfaces/api'; 4 | 5 | /** 6 | * 根据项目根目录下的services/api-lock.json,生成一个接口和声明的关联文件。 7 | * 在UI侧选择某个接口, 8 | */ 9 | export default function(api: IApi) { 10 | const jsonPath = api.paths.absSrcPath + '/services/api-lock.json'; 11 | if (!existsSync(jsonPath)) { 12 | return { 13 | databases: null, 14 | // mods: null, 15 | baseClasses: null, 16 | }; 17 | } 18 | const apiJson: ApiJSON[] = require(jsonPath); 19 | 20 | const databases = apiJson.map(db => ({ 21 | label: db.name, 22 | value: db.name, 23 | children: db.mods.map(mod => ({ 24 | label: mod.description, 25 | value: mod.name, 26 | children: mod.interfaces.map(({ name, response, description, method, parameters }) => { 27 | // 提交表单数据时的DTO 28 | const paramsName = parameters.find(param => param.in === 'body')?.dataType.typeName; 29 | // 获取数据时的DTO 30 | let responseName; 31 | if (response.typeArgs.length > 0) { 32 | responseName = response.typeArgs.find(arg => arg.isDefsType)?.typeName; 33 | } else { 34 | if (response.isDefsType) { 35 | responseName = response.typeName; 36 | } 37 | } 38 | const value = `${name}-${paramsName}-${responseName}`; 39 | return { 40 | label: `${description}(${method})`, 41 | value, 42 | }; 43 | }), 44 | })), 45 | })); 46 | 47 | const mods = apiJson.reduce( 48 | (accu, curr) => 49 | accu.concat( 50 | curr.mods.map(mod => ({ 51 | name: mod.name, 52 | description: mod.description, 53 | dbId: curr.name, 54 | })) as [], 55 | ), 56 | [], 57 | ); 58 | 59 | const baseClasses = apiJson.reduce( 60 | (accu, curr) => 61 | accu.concat( 62 | curr.baseClasses.map(mod => ({ 63 | name: mod.name, 64 | dbId: curr.name, 65 | description: mod.description || '', 66 | properties: mod.properties.map(prop => ({ 67 | label: prop.description, 68 | value: prop.name, 69 | required: prop.required, 70 | })), 71 | })) as [], 72 | ), 73 | [], 74 | ); 75 | 76 | return { 77 | databases, 78 | // mods, 79 | baseClasses, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/constantConfig.ts: -------------------------------------------------------------------------------- 1 | import { IApi } from 'umi'; 2 | import { existsSync, writeFileSync, readFileSync } from 'fs'; 3 | import * as types from '@babel/types'; 4 | import * as parser from '@babel/parser'; 5 | import generate from '@babel/generator'; 6 | 7 | /** 8 | * 加载常量文件的内容 9 | * @param api 10 | */ 11 | export function getConstantConfig(api: IApi) { 12 | const constantFilePath = api.paths.absSrcPath + '/constant.ts'; 13 | if (!existsSync(constantFilePath)) { 14 | writeFileSync(constantFilePath, '', 'utf-8'); 15 | return ''; 16 | } else { 17 | const ast: types.File = parser.parse(readFileSync(constantFilePath, 'utf-8'), { 18 | sourceType: 'module', 19 | plugins: ['typescript'], 20 | }); 21 | return generate(ast, {}).code; 22 | } 23 | } 24 | 25 | /** 26 | * 保存修改之后的常量配置 27 | * @param api 28 | * @param code 29 | */ 30 | export function saveConstantConfig(api: IApi, code: string) { 31 | const constantFilePath = api.paths.absSrcPath + '/constant.ts'; 32 | writeFileSync(constantFilePath, code, 'utf-8'); 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 10:38:23 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-22 17:21:04 8 | */ 9 | // ref: 10 | // - https://umijs.org/plugin/develop.html 11 | import { IApi } from '@umijs/types'; 12 | import { join } from 'path'; 13 | 14 | import generatePage from './manage'; 15 | import generateApi from './api'; 16 | import generateScreen from './screen'; 17 | import {getConstantConfig, saveConstantConfig} from './constantConfig' 18 | 19 | export default function(api: IApi) { 20 | let mods = []; 21 | 22 | // @ts-ignore 23 | api.addUIPlugin(() => join(__dirname, '../dist/index.umd.js')); 24 | 25 | // @ts-ignore 26 | api.onUISocket(({ action, failure, success }) => { 27 | const { type, payload = {} } = action; 28 | if (type.includes('screen')) { 29 | const result = generateScreen(payload, api); 30 | if (result) { 31 | success({ success: true, message: '恭喜你,文件创建成功' }); 32 | } else { 33 | failure({ success: false, message: '对不起,文件创建失败' }); 34 | } 35 | } else if (type.includes('apiGenerator')) { 36 | const { databases, baseClasses } = generateApi(api); 37 | if (databases === null) { 38 | failure({ 39 | success: false, 40 | databases: null, 41 | }); 42 | } else { 43 | success({ 44 | success: true, 45 | databases, 46 | baseClasses, 47 | }); 48 | } 49 | } else if (type.includes('constant')) { 50 | if (type === 'org.umi-plugin-page-creator.constantLoad') { 51 | const code = getConstantConfig(api); 52 | success({ 53 | success: true, 54 | data: code, 55 | }) 56 | } else if (type === 'org.umi-plugin-page-creator.constantSave') { 57 | saveConstantConfig(api, payload.code); 58 | success({ 59 | success: true, 60 | message: '常量配置保存成功' 61 | }); 62 | } 63 | } else { 64 | const result = generatePage(payload, type, api); 65 | if (result) { 66 | success({ success: true, message: '恭喜你,文件创建成功' }); 67 | } else { 68 | failure({ success: false, message: '对不起,目录已存在' }); 69 | } 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/manage/templates/formActionMethods.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成table与form连接的一些方法组件(非modal类型) 3 | * @公司: thundersdata 4 | * @作者: 廖军 5 | * @Date: 2020-10-10 16:15:04 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-27 14:08:34 8 | */ 9 | 10 | export interface Payload { 11 | initialFetch?: string[]; 12 | pageName: string; 13 | generateDetail: boolean; 14 | } 15 | 16 | export default ({ pageName, initialFetch = [], generateDetail }: Payload) => { 17 | const apiStr = initialFetch.length === 3 ? 18 | `API.${initialFetch[0]}.${initialFetch[1]}` : 'API.recruitment.person'; 19 | return ` 20 | import React, { forwardRef } from 'react'; 21 | import { Store } from 'antd/es/form/interface'; 22 | import { history } from 'umi'; 23 | import { useRequest } from 'ahooks'; 24 | import { message } from 'antd'; 25 | 26 | export interface FormActionMethodsInstance { 27 | onAdd?: () => void; 28 | onDelete?: (row: Store) => void; 29 | onEdit?: (row: Store) => void; 30 | onPreview?: (row: Store) => void; 31 | onDeleteBatch?: (ids: number[]) => void; 32 | } 33 | 34 | export default forwardRef void }>(({ reload }, ref) => { 35 | const formActionRef = ref as React.MutableRefObject; 36 | const { run: handleDelete } = useRequest(${apiStr}.remove.fetch, { 37 | manual: true, 38 | onSuccess: () => { 39 | message.success('删除成功'); 40 | reload && reload(); 41 | }, 42 | }); 43 | const { run: handleDeleteBatch } = useRequest(${apiStr}.deleteBatch, { 44 | manual: true, 45 | onSuccess: () => { 46 | message.success('批量删除成功'); 47 | reload && reload(); 48 | }, 49 | }); 50 | 51 | /** 新增 */ 52 | formActionRef.current.onAdd = () => history.push('/${pageName}/edit'); 53 | 54 | /** 删除 */ 55 | formActionRef.current.onDelete = (row: Store) => handleDelete(row.id); 56 | 57 | /** 编辑 */ 58 | formActionRef.current.onEdit = (row: Store) => history.push(\`/${pageName}/edit?id=\${row.id}\`); 59 | 60 | ${generateDetail ? `/** 查看 */ 61 | formActionRef.current.onPreview = (row: Store) => history.push(\`/${pageName}/detail?id=\${row.id}\`);` : ''} 62 | 63 | /** 批量删除 */ 64 | formActionRef.current.onDeleteBatch = (ids: number[]) => handleDeleteBatch(ids); 65 | 66 | return <>; 67 | }); 68 | `; 69 | } -------------------------------------------------------------------------------- /src/manage/templates/formActionMethodsModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成table与form连接的一些方法组件(modal类型) 3 | * @公司: thundersdata 4 | * @作者: 廖军 5 | * @Date: 2020-10-10 16:26:10 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-27 14:09:49 8 | */ 9 | 10 | export interface Payload { 11 | initialFetch?: string[]; 12 | generateDetail: boolean; 13 | } 14 | 15 | export default ({ initialFetch = [], generateDetail }: Payload) => { 16 | const apiStr = initialFetch.length === 3 ? 17 | `API.${initialFetch[0]}.${initialFetch[1]}` : 'API.recruitment.person'; 18 | return ` 19 | import React, { forwardRef } from 'react'; 20 | import { Store } from 'antd/es/form/interface'; 21 | import { useRequest } from 'ahooks'; 22 | import { message } from 'antd'; 23 | import { useImmer } from 'use-immer'; 24 | import Edit from '../Edit'; 25 | ${generateDetail ? `import Detail from '../Detail';` : ''} 26 | 27 | export interface FormActionMethodsInstance { 28 | onAdd?: () => void; 29 | onDelete?: (row: Store) => void; 30 | onEdit?: (row: Store) => void; 31 | onPreview?: (row: Store) => void; 32 | onDeleteBatch?: (ids: number[]) => void; 33 | } 34 | 35 | export default forwardRef void }>(({ reload }, ref) => { 36 | const formActionRef = ref as React.MutableRefObject; 37 | const [editModalConfig, setEditModalConfig] = useImmer<{ 38 | visible: boolean; 39 | formData: Store; 40 | loading: boolean; 41 | }>({ 42 | visible: false, 43 | formData: {}, 44 | loading: false, 45 | }); 46 | ${generateDetail ? `const [detailModalConfig, setDetailModalConfig] = useImmer<{ 47 | visible: boolean; 48 | formData: Store; 49 | loading: boolean; 50 | }>({ 51 | visible: false, 52 | formData: {}, 53 | loading: false, 54 | });` : ''} 55 | 56 | const { run: handleDelete } = useRequest(${apiStr}.remove.fetch, { 57 | manual: true, 58 | onSuccess: () => { 59 | message.success('删除成功'); 60 | reload && reload(); 61 | }, 62 | }); 63 | 64 | const { run: handleDeleteBatch } = useRequest(${apiStr}.deleteBatch, { 65 | manual: true, 66 | onSuccess: () => { 67 | message.success('批量删除成功'); 68 | reload && reload(); 69 | }, 70 | }); 71 | 72 | /** 新增 */ 73 | formActionRef.current.onAdd = () => 74 | setEditModalConfig(config => { 75 | config.visible = true; 76 | config.formData = {}; 77 | }); 78 | 79 | /** 删除 */ 80 | formActionRef.current.onDelete = (row: Store) => handleDelete(row.id); 81 | 82 | /** 编辑 */ 83 | formActionRef.current.onEdit = (row: Store) => 84 | setEditModalConfig(config => { 85 | config.visible = true; 86 | config.formData = row; 87 | }); 88 | 89 | ${generateDetail ? `/** 查看 */ 90 | formActionRef.current.onPreview = (row: Store) => 91 | setDetailModalConfig(config => { 92 | config.visible = true; 93 | config.formData = row; 94 | });` : ''} 95 | 96 | /** 批量删除 */ 97 | formActionRef.current.onDeleteBatch = (ids: number[]) => handleDeleteBatch(ids); 98 | 99 | return ( 100 | <> 101 | 106 | setEditModalConfig(config => { 107 | config.visible = false; 108 | config.loading = false; 109 | config.formData = {}; 110 | }) 111 | } 112 | reload={reload} 113 | /> 114 | ${generateDetail ? ` 119 | setDetailModalConfig(config => { 120 | config.visible = false; 121 | config.loading = false; 122 | config.formData = {}; 123 | }) 124 | } 125 | />` : ''} 126 | 127 | ); 128 | }); 129 | `; 130 | } -------------------------------------------------------------------------------- /src/manage/templates/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 14:06:48 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-10 16:50:07 8 | */ 9 | import generateShortFormCode from './shortForm'; 10 | import generateLongFormCode from './longForm'; 11 | import generateShortDetailCode from './shortDetail'; 12 | import generateLongDetailCode from './longDetail'; 13 | import generateTableCode from './table'; 14 | import generateShortFormModalCode from './shortFormModal'; 15 | import generateLongFormModalCode from './longFormModal'; 16 | import generateShortDetailModalCode from './shortDetailModal'; 17 | import generateLongDetailModalCode from './longDetailModal'; 18 | import generateFormActionMethodsCode from './formActionMethods'; 19 | import generateFormActionMethodsModalCode from './formActionMethodsModal'; 20 | 21 | export { 22 | generateShortFormCode, 23 | generateLongFormCode, 24 | generateShortDetailCode, 25 | generateLongDetailCode, 26 | generateTableCode, 27 | generateShortFormModalCode, 28 | generateLongFormModalCode, 29 | generateShortDetailModalCode, 30 | generateLongDetailModalCode, 31 | generateFormActionMethodsCode, 32 | generateFormActionMethodsModalCode, 33 | }; 34 | -------------------------------------------------------------------------------- /src/manage/templates/longDetail.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成长详情页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 16:05:30 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-09 15:38:34 8 | */ 9 | import { transformFormItemLines, generateBreadcrumbs } from './util'; 10 | import { CardItemProps } from '../../../interfaces/common'; 11 | 12 | export interface Payload { 13 | cards: CardItemProps[]; 14 | initialFetch?: string[]; 15 | menu: string; 16 | } 17 | 18 | export default function generateLongFormCode(payload: Payload): string { 19 | if (payload && payload.cards) { 20 | const { cards = [], initialFetch, menu } = payload; 21 | 22 | const breadcrumbs = generateBreadcrumbs(menu); 23 | 24 | const code = ` 25 | import React from 'react'; 26 | import { 27 | Form, 28 | Card, 29 | Row, 30 | Col, 31 | Spin, 32 | } from 'antd'; 33 | import { history } from 'umi'; 34 | import { useRequest } from 'ahooks'; 35 | import Title from '@/components/Title'; 36 | import DetailValue from '@/components/DetailValue'; 37 | ${breadcrumbs.length > 1 && `import CustomBreadcrumb from '@/components/CustomBreadcrumb';`} 38 | console.log('emptyline'); 39 | const colLayout = { 40 | lg: { 41 | span: 8 42 | }, 43 | md: { 44 | span: 12 45 | }, 46 | sm: { 47 | span: 24 48 | } 49 | } 50 | console.log('emptyline'); 51 | export default () => { 52 | const [form] = Form.useForm(); 53 | const { id } = history.location.query; 54 | console.log('emptyline'); 55 | const { loading } = useRequest(API.${initialFetch && initialFetch.length === 3 ? `${initialFetch[0]}.${initialFetch[1]}.${ 56 | initialFetch[2].split('-')[0] 57 | }.fetch({ id })` : `recruitment.person.getPerson.fetch( 58 | { personCode: id }, 59 | )`}, { 60 | ready: !!id, 61 | onSuccess: data => { 62 | const values = { 63 | ...data 64 | }; 65 | form.setFieldsValue(values); 66 | } 67 | }); 68 | console.log('emptyline'); 69 | return ( 70 | 71 | 72 |
73 | ${cards 74 | .map(card => { 75 | const { title = '', formItems = [] } = card; 76 | const cols = 3; 77 | // 把formItems分成3列 78 | const formItemLines = transformFormItemLines(formItems, cols); 79 | 80 | return ` 81 | } style={{ marginBottom: 16 }}> 82 | ${formItemLines 83 | .map(line => { 84 | return ` 85 | 86 | ${line 87 | .map(formItem => { 88 | const { label, name, type, ...restProps } = formItem; 89 | 90 | return ` 91 | 92 | 96 | 97 | 98 | 99 | `; 100 | }) 101 | .join('')} 102 | 103 | `; 104 | }) 105 | .join('')} 106 | 107 | `; 108 | }) 109 | .join('')} 110 |
111 |
112 | ); 113 | }; 114 | `; 115 | return code; 116 | } 117 | return ''; 118 | } 119 | -------------------------------------------------------------------------------- /src/manage/templates/longDetailModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成大表单页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 16:05:30 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-15 19:33:41 8 | */ 9 | import { transformFormItemLines } from './util'; 10 | import { FormItemProps } from '../../../interfaces/common'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | 13 | export interface Payload { 14 | formConfig: Store; 15 | formItems: FormItemProps[]; 16 | } 17 | 18 | export default function generateLongDetailModalCode(payload: Payload): string { 19 | if (payload && payload.formConfig && payload.formItems) { 20 | const { formConfig, formItems = [] } = payload; 21 | 22 | const cols = 2; 23 | // 把formItems分成2列 24 | const formItemLines = transformFormItemLines(formItems, cols); 25 | 26 | const code = ` 27 | import React, { useEffect } from 'react'; 28 | import { 29 | Modal, 30 | Form, 31 | Row, 32 | Col, 33 | Spin, 34 | Input, 35 | DatePicker, 36 | TimePicker, 37 | Cascader, 38 | InputNumber, 39 | Radio, 40 | Checkbox, 41 | Switch, 42 | Slider, 43 | Select, 44 | TreeSelect, 45 | Upload, 46 | Rate, 47 | } from 'antd'; 48 | import { FormInstance } from 'antd/lib/form'; 49 | import { Store } from 'antd/es/form/interface'; 50 | import { isEmpty } from 'lodash-es'; 51 | import DetailValue from '@/components/DetailValue'; 52 | console.log('emptyline'); 53 | const layout = { 54 | labelCol: { span: 8 }, 55 | wrapperCol: { span: 16 }, 56 | }; 57 | console.log('emptyline'); 58 | export default ({ 59 | visible, 60 | toggleVisible, 61 | formData, 62 | loading, 63 | }: { 64 | visible: boolean; 65 | toggleVisible: () => void; 66 | formData: Store; 67 | loading: boolean; 68 | }) => { 69 | const [form] = Form.useForm(); 70 | console.log('emptyline'); 71 | useEffect(() => { 72 | if (!isEmpty(formData)) { 73 | form.setFieldsValue(formData); 74 | } 75 | }, [formData]); 76 | console.log('emptyline'); 77 | return ( 78 | 87 | 88 |
89 | ${formItemLines 90 | .map(line => { 91 | return ` 92 | 93 | ${line 94 | .map(formItem => { 95 | return ` 96 | 97 | 101 | 102 | 103 | 104 | `; 105 | }) 106 | .join('')} 107 | 108 | `; 109 | }) 110 | .join('')} 111 |
112 |
113 |
114 | ); 115 | }; 116 | `; 117 | return code; 118 | } 119 | return ''; 120 | } 121 | -------------------------------------------------------------------------------- /src/manage/templates/longForm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成大表单页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 16:05:30 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-10 10:15:36 8 | */ 9 | import { createFormComponentsByType, transformFormItemLines, generateRules, generateBreadcrumbs } from './util'; 10 | import { CardItemProps, FormItemProps } from '../../../interfaces/common'; 11 | import { getPageNameByPath } from '..'; 12 | 13 | export interface Payload { 14 | cards: CardItemProps[]; 15 | initialFetch?: string[]; 16 | submitFetch?: string[]; 17 | menu: string; 18 | path: string; 19 | } 20 | 21 | export default function generateLongFormCode(payload: Payload): string { 22 | if (payload && payload.cards) { 23 | const { cards = [], initialFetch, submitFetch, menu } = payload; 24 | const formItems = cards.reduce((acc, curr) => acc.concat(curr.formItems), [] as FormItemProps[]); 25 | const hasUploadItem = formItems.find(item => item.type === 'upload'); 26 | 27 | const breadcrumbs = generateBreadcrumbs(menu); 28 | 29 | const code = ` 30 | import React ${hasUploadItem ? ', { useState }' : ''} from 'react'; 31 | import { 32 | Form, 33 | Button, 34 | Card, 35 | Row, 36 | Col, 37 | Input, 38 | DatePicker, 39 | TimePicker, 40 | Cascader, 41 | InputNumber, 42 | Radio, 43 | Checkbox, 44 | Switch, 45 | Slider, 46 | Select, 47 | TreeSelect, 48 | Upload, 49 | Rate, 50 | message, 51 | Spin, 52 | } from 'antd'; 53 | import { history } from 'umi'; 54 | import { useRequest } from 'ahooks'; 55 | import { Store } from 'antd/es/form/interface'; 56 | import Title from '@/components/Title'; 57 | import useSpinning from '@/hooks/useSpinning'; 58 | import FooterToolbar from '@/components/FooterToolbar'; 59 | ${breadcrumbs.length > 1 && `import CustomBreadcrumb from '@/components/CustomBreadcrumb';`} 60 | import { getVerificationRules } from '@/pages/${getPageNameByPath(payload.path)}/validators'; 61 | console.log('emptyline'); 62 | const colLayout = { 63 | lg: { 64 | span: 8 65 | }, 66 | md: { 67 | span: 12 68 | }, 69 | sm: { 70 | span: 24 71 | } 72 | } 73 | console.log('emptyline'); 74 | export default () => { 75 | const [form] = Form.useForm(); 76 | const { tip, setTip } = useSpinning(); 77 | ${hasUploadItem ? `const [submitBtnDisabled, setSubmitBtnDisabled] = useState(false);` : ''} 78 | console.log('emptyline'); 79 | const { id } = history.location.query; 80 | console.log('emptyline'); 81 | const fetchDetail = () => { 82 | if (id) { 83 | setTip('加载详情中,请稍候...'); 84 | return API.${initialFetch && initialFetch.length === 3 ? `${initialFetch[0]}.${initialFetch[1]}.${ 85 | initialFetch[2].split('-')[0] 86 | }.fetch({ id })` : `recruitment.person.getPerson.fetch( 87 | { personCode: id }, 88 | )`}; 89 | } 90 | return Promise.resolve(false); 91 | }; 92 | console.log('emptyline'); 93 | const { loading } = useRequest(fetchDetail, { 94 | refreshDeps: [id], 95 | onSuccess: data => { 96 | const values = { 97 | ...data 98 | }; 99 | form.setFieldsValue(values); 100 | } 101 | }); 102 | console.log('emptyline'); 103 | const submit = (values: Store) => { 104 | setSpinning(true); 105 | setTip('数据保存中,请稍候...'); 106 | console.log('emptyline'); 107 | const payload = { 108 | ...values, 109 | }; 110 | console.log('emptyline'); 111 | return API.${submitFetch && submitFetch.length === 3 ? `${submitFetch[0]}.${submitFetch[1]}.${ 112 | submitFetch[2].split('-')[0] 113 | }` : 'recruitment.person.addPerson'}.fetch(payload); 114 | }; 115 | console.log('emptyline'); 116 | const { run: handleFinish, loading: submitting } = useRequest(submit, { 117 | manual: true, 118 | onSuccess: () => { 119 | message.success('保存成功'); 120 | }, 121 | }); 122 | console.log('emptyline'); 123 | return ( 124 | 125 | 126 |
127 | ${cards 128 | .map(card => { 129 | const { title = '', formItems = [] } = card; 130 | const cols = 3; 131 | // 把formItems分成3列 132 | const formItemLines = transformFormItemLines(formItems, cols); 133 | 134 | return ` 135 | } style={{ marginBottom: 16 }}> 136 | ${formItemLines 137 | .map(line => { 138 | return ` 139 | 140 | ${line 141 | .map(formItem => { 142 | const { 143 | label, 144 | name, 145 | type, 146 | required = false, 147 | customRules = '', 148 | ...restProps 149 | } = formItem; 150 | const rules = generateRules(customRules as string, required as boolean); 151 | return ` 152 | 153 | { 161 | if (Array.isArray(e)) { 162 | return e; 163 | } 164 | return e && e.fileList; 165 | }}` : ''} 166 | > 167 | ${createFormComponentsByType(type, restProps)} 168 | 169 | 170 | `; 171 | }) 172 | .join('')} 173 | 174 | `; 175 | }) 176 | .join('')} 177 | 178 | `; 179 | }) 180 | .join('')} 181 | 182 | 190 | 191 |
192 |
193 | ); 194 | }; 195 | `; 196 | return code; 197 | } 198 | return ''; 199 | } 200 | -------------------------------------------------------------------------------- /src/manage/templates/longFormModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成大表单页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 16:05:30 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-10 17:40:45 8 | */ 9 | import { createFormComponentsByType, transformFormItemLines, generateRules } from './util'; 10 | import { FormItemProps } from '../../../interfaces/common'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | import { getPageNameByPath } from '..'; 13 | 14 | export interface Payload { 15 | formConfig: Store; 16 | formItems: FormItemProps[]; 17 | submitFetch?: string[]; 18 | fromTable: boolean; 19 | path: string; 20 | } 21 | 22 | export default function generateLongFormModalCode(payload: Payload): string { 23 | if (payload && payload.formConfig && payload.formItems) { 24 | const { formConfig, formItems = [], submitFetch, fromTable } = payload; 25 | 26 | const cols = 2; 27 | // 把formItems分成2列 28 | const formItemLines = transformFormItemLines(formItems, cols); 29 | const item = formItems.find(item => item.type === 'upload'); 30 | 31 | const code = ` 32 | import React, { useEffect ${item ? ', useState' : ''} } from 'react'; 33 | import { 34 | Modal, 35 | Button, 36 | Form, 37 | Row, 38 | Col, 39 | Spin, 40 | Input, 41 | DatePicker, 42 | TimePicker, 43 | Cascader, 44 | InputNumber, 45 | Radio, 46 | Checkbox, 47 | Switch, 48 | Slider, 49 | Select, 50 | TreeSelect, 51 | Upload, 52 | Rate, 53 | message, 54 | } from 'antd'; 55 | import { FormInstance } from 'antd/lib/form'; 56 | import { Store } from 'antd/es/form/interface'; 57 | import { isEmpty } from 'lodash-es'; 58 | import { useRequest } from 'ahooks'; 59 | import useSpinning from '@/hooks/useSpinning'; 60 | import { getVerificationRules } from '@/pages/${getPageNameByPath(payload.path)}/validators'; 61 | console.log('emptyline'); 62 | const twoColumnsLayout = { 63 | labelCol: { span: 8 }, 64 | wrapperCol: { span: 16 }, 65 | }; 66 | console.log('emptyline'); 67 | export default ({ 68 | visible, 69 | toggleVisible, 70 | formData, 71 | loading, 72 | ${fromTable ? `reload,` : ''} 73 | }: { 74 | visible: boolean; 75 | toggleVisible: () => void; 76 | formData: Store; 77 | loading: boolean; 78 | ${fromTable ? `reload?: () => void;` : ''} 79 | }) => { 80 | const [form] = Form.useForm(); 81 | const { tip, setTip } = useSpinning(); 82 | ${item ? `const [submitBtnDisabled, setSubmitBtnDisabled] = useState(false);` : ''} 83 | console.log('emptyline'); 84 | useEffect(() => { 85 | if (!isEmpty(formData)) { 86 | form.setFieldsValue(formData); 87 | } 88 | }, [formData]); 89 | console.log('emptyline'); 90 | const handleCancel = () => { 91 | toggleVisible(); 92 | form.resetFields(); 93 | }; 94 | console.log('emptyline'); 95 | const submit = (values: Store) => { 96 | setTip('数据保存中,请稍候...'); 97 | console.log('emptyline'); 98 | const payload = { 99 | ...values, 100 | }; 101 | console.log('emptyline'); 102 | return API.${submitFetch && submitFetch.length === 3 ? `${submitFetch[0]}.${submitFetch[1]}.${ 103 | submitFetch[2].split('-')[0] 104 | }` : 'recruitment.person.addPerson'}.fetch(payload); 105 | }; 106 | console.log('emptyline'); 107 | const { run: handleFinish, loading: submitting } = useRequest(submit, { 108 | manual: true, 109 | onSuccess: () => { 110 | message.success('保存成功'); 111 | form.resetFields(); 112 | ${fromTable ? `reload && reload();` : ''} 113 | } 114 | }); 115 | console.log('emptyline'); 116 | return ( 117 | form.submit()} 126 | onCancel={handleCancel} 127 | confirmLoading={submitting} 128 | > 129 | 130 |
131 | ${formItemLines 132 | .map(line => { 133 | return ` 134 | 135 | ${line 136 | .map(formItem => { 137 | const { 138 | label, 139 | name, 140 | type, 141 | required = false, 142 | customRules = '', 143 | ...restProps 144 | } = formItem; 145 | const rules = generateRules(customRules as string, required as boolean); 146 | return ` 147 | 148 | { 156 | if (Array.isArray(e)) { 157 | return e; 158 | } 159 | return e && e.fileList; 160 | }}` : ''} 161 | > 162 | ${createFormComponentsByType(type, restProps)} 163 | 164 | 165 | `; 166 | }) 167 | .join('')} 168 | 169 | `; 170 | }) 171 | .join('')} 172 |
173 |
174 |
175 | ); 176 | }; 177 | `; 178 | return code; 179 | } 180 | return ''; 181 | } 182 | -------------------------------------------------------------------------------- /src/manage/templates/shortDetail.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成短详情页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 14:04:41 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-09 15:39:08 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { FormItemProps } from '../../../interfaces/common'; 11 | import { generateBreadcrumbs } from './util'; 12 | 13 | export interface Payload { 14 | formConfig: Store; 15 | formItems: FormItemProps[]; 16 | initialFetch?: string[]; 17 | menu: string; 18 | } 19 | 20 | export default function generateShortDetail(payload: Payload): string { 21 | if (payload && payload.formConfig && payload.formItems) { 22 | const { formConfig, formItems, initialFetch, menu } = payload; 23 | 24 | const breadcrumbs = generateBreadcrumbs(menu); 25 | 26 | const code = ` 27 | import React from 'react'; 28 | import { Card, Form, Spin } from 'antd'; 29 | import { history } from 'umi'; 30 | import { useRequest } from 'ahooks'; 31 | import Title from '@/components/Title'; 32 | import DetailValue from '@/components/DetailValue'; 33 | ${breadcrumbs.length > 1 && `import CustomBreadcrumb from '@/components/CustomBreadcrumb';`} 34 | console.log('emptyline'); 35 | const formItemLayout = { 36 | labelCol: { 37 | xs: { span: 24 }, 38 | sm: { span: 7 }, 39 | }, 40 | wrapperCol: { 41 | xs: { span: 24 }, 42 | sm: { span: 12 }, 43 | md: { span: 10 }, 44 | }, 45 | }; 46 | console.log('emptyline'); 47 | export default () => { 48 | const [form] = Form.useForm(); 49 | const { id } = history.location.query; 50 | console.log('emptyline'); 51 | const { loading } = useRequest(API.${initialFetch && initialFetch.length === 3 ? `${initialFetch[0]}.${initialFetch[1]}.${ 52 | initialFetch[2].split('-')[0] 53 | }.fetch({ id })` : `recruitment.person.getPerson.fetch( 54 | { personCode: id }, 55 | )`}, { 56 | ready: !!id, 57 | onSuccess: data => { 58 | const values = { 59 | ...data 60 | } 61 | form.setFieldsValue(values); 62 | } 63 | }); 64 | console.log('emptyline'); 65 | return ( 66 | 67 | 68 |
69 | } style={{ marginBottom: 16 }}> 70 | ${formItems 71 | .map(item => { 72 | const { label, name, detailItemType } = item; 73 | return ` 78 | 79 | `; 80 | }) 81 | .join('')} 82 | 83 |
84 |
85 | ); 86 | }; 87 | `; 88 | return code; 89 | } 90 | return ''; 91 | } 92 | -------------------------------------------------------------------------------- /src/manage/templates/shortDetailModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成单列详情弹窗 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 14:04:41 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-19 11:39:50 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { FormItemProps } from '../../../interfaces/common'; 11 | 12 | export interface Payload { 13 | formConfig: Store; 14 | formItems: FormItemProps[]; 15 | } 16 | 17 | export default function generateShortDetailModalCode(payload: Payload): string { 18 | if (payload && payload.formConfig && payload.formItems) { 19 | const { formConfig, formItems } = payload; 20 | 21 | const code = ` 22 | import React, { useEffect } from 'react'; 23 | import { 24 | Button, 25 | Modal, 26 | Form, 27 | Spin, 28 | Input, 29 | DatePicker, 30 | TimePicker, 31 | Cascader, 32 | InputNumber, 33 | Radio, 34 | Checkbox, 35 | Switch, 36 | Slider, 37 | Select, 38 | TreeSelect, 39 | Upload, 40 | Rate, 41 | } from 'antd'; 42 | import { isEmpty } from 'lodash-es'; 43 | import { FormInstance } from 'antd/lib/form'; 44 | import { Store } from 'antd/es/form/interface'; 45 | import DetailValue from '@/components/DetailValue'; 46 | console.log('emptyline'); 47 | const layout = { 48 | labelCol: { span: 5 }, 49 | wrapperCol: { span: 18 }, 50 | }; 51 | console.log('emptyline'); 52 | export default ({ 53 | visible, 54 | toggleVisible, 55 | formData, 56 | loading, 57 | }: { 58 | visible: boolean; 59 | toggleVisible: () => void; 60 | formData: Store; 61 | loading: boolean; 62 | }) => { 63 | const [form] = Form.useForm(); 64 | console.log('emptyline'); 65 | useEffect(() => { 66 | if (!isEmpty(formData)) { 67 | form.setFieldsValue(formData); 68 | } 69 | }, [formData]); 70 | console.log('emptyline'); 71 | return ( 72 | 80 | 81 |
82 | ${formItems 83 | .map(item => { 84 | const { 85 | label, 86 | name, 87 | detailItemType, 88 | } = item; 89 | return ` 93 | 94 | `; 95 | }) 96 | .join('')} 97 |
98 |
99 |
100 | ); 101 | }; 102 | `; 103 | return code; 104 | } 105 | return ''; 106 | } 107 | -------------------------------------------------------------------------------- /src/manage/templates/shortForm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成短表单 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 14:04:41 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-10 10:10:28 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { createFormComponentsByType, generateRules, generateBreadcrumbs } from './util'; 11 | import { FormItemProps } from '../../../interfaces/common'; 12 | import { getPageNameByPath } from '..'; 13 | 14 | export interface Payload { 15 | formConfig: Store; 16 | formItems: FormItemProps[]; 17 | initialFetch?: string[]; 18 | submitFetch?: string[]; 19 | menu: string; 20 | path: string; 21 | } 22 | 23 | export default function generateShortFormCode(payload: Payload): string { 24 | if (payload && payload.formConfig && payload.formItems) { 25 | const { formConfig, formItems, initialFetch, submitFetch, menu, } = payload; 26 | const hasUploadItem = formItems.find(item => item.type === 'upload'); 27 | 28 | const breadcrumbs = generateBreadcrumbs(menu); 29 | 30 | const code = ` 31 | import React ${hasUploadItem ? ', { useState }' : ''} from 'react'; 32 | import { 33 | Form, 34 | Button, 35 | Card, 36 | Input, 37 | DatePicker, 38 | TimePicker, 39 | Cascader, 40 | InputNumber, 41 | Radio, 42 | Checkbox, 43 | Switch, 44 | Slider, 45 | Select, 46 | TreeSelect, 47 | Upload, 48 | Rate, 49 | message, 50 | Spin, 51 | } from 'antd'; 52 | import { Store } from 'antd/es/form/interface'; 53 | import { history } from 'umi'; 54 | import { useRequest } from 'ahooks'; 55 | import Title from '@/components/Title'; 56 | import useSpinning from '@/hooks/useSpinning'; 57 | ${breadcrumbs.length > 1 && `import CustomBreadcrumb from '@/components/CustomBreadcrumb';`} 58 | import { getVerificationRules } from '@/pages/${getPageNameByPath(payload.path)}/validators'; 59 | console.log('emptyline'); 60 | const formItemLayout = { 61 | labelCol: { 62 | xs: { span: 24 }, 63 | sm: { span: 7 }, 64 | }, 65 | wrapperCol: { 66 | xs: { span: 24 }, 67 | sm: { span: 12 }, 68 | md: { span: 10 }, 69 | }, 70 | }; 71 | console.log('emptyline'); 72 | const submitFormLayout = { 73 | wrapperCol: { 74 | xs: { span: 24, offset: 0 }, 75 | sm: { span: 10, offset: 7 }, 76 | }, 77 | }; 78 | console.log('emptyline'); 79 | export default () => { 80 | const [form] = Form.useForm(); 81 | const { tip, setTip } = useSpinning(); 82 | ${hasUploadItem ? `const [submitBtnDisabled, setSubmitBtnDisabled] = useState(false);` : ''} 83 | console.log('emptyline'); 84 | const { id } = history.location.query; 85 | console.log('emptyline'); 86 | const fetchDetail = () => { 87 | if (id) { 88 | setTip('加载详情中,请稍候...'); 89 | return API.${initialFetch && initialFetch.length === 3 ? `${initialFetch[0]}.${initialFetch[1]}.${ 90 | initialFetch[2].split('-')[0] 91 | }.fetch({ id })` : `recruitment.person.getPerson.fetch( 92 | { personCode: id }, 93 | )`}; 94 | } 95 | return Promise.resolve(false); 96 | }; 97 | console.log('emptyline'); 98 | const { loading } = useRequest(fetchDetail, { 99 | refreshDeps: [id], 100 | onSuccess: data => { 101 | const values = { 102 | ...data 103 | }; 104 | form.setFieldsValue(values); 105 | }, 106 | }); 107 | console.log('emptyline'); 108 | const submit = (values: Store) => { 109 | setTip('数据保存中,请稍候...'); 110 | console.log('emptyline'); 111 | const payload = { 112 | ...values, 113 | }; 114 | console.log('emptyline'); 115 | return API.${submitFetch && submitFetch.length === 3 ? `${submitFetch[0]}.${submitFetch[1]}.${ 116 | submitFetch[2].split('-')[0] 117 | }` : 'recruitment.person.addPerson'}.fetch(payload); 118 | }; 119 | console.log('emptyline'); 120 | const { run: handleFinish, loading: submitting } = useRequest(submit, { 121 | manual: true, 122 | onSuccess: () => { 123 | message.success('保存成功'); 124 | }, 125 | }); 126 | console.log('emptyline'); 127 | return ( 128 | 129 | 130 | }> 131 |
132 | ${formItems 133 | .map(item => { 134 | const { 135 | label, 136 | name, 137 | type, 138 | required = false, 139 | customRules = '', 140 | ...restProps 141 | } = item; 142 | const rules = generateRules(customRules as string, required as boolean); 143 | return ` { 152 | if (Array.isArray(e)) { 153 | return e; 154 | } 155 | return e && e.fileList; 156 | }}` : ''} 157 | > 158 | ${createFormComponentsByType(type, restProps)} 159 | `; 160 | }) 161 | .join('')} 162 | 163 | 166 | 167 |
168 |
169 |
170 | ); 171 | }; 172 | `; 173 | return code; 174 | } 175 | return ''; 176 | } 177 | -------------------------------------------------------------------------------- /src/manage/templates/shortFormModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成单列表单弹窗 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-07 14:04:41 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-10 17:41:18 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { createFormComponentsByType, generateRules } from './util'; 11 | import { FormItemProps } from '../../../interfaces/common'; 12 | import { getPageNameByPath } from '..'; 13 | 14 | export interface Payload { 15 | formConfig: Store; 16 | formItems: FormItemProps[]; 17 | submitFetch?: string[]; 18 | fromTable: boolean; 19 | path: string; 20 | } 21 | 22 | export default function generateShortFormModalCode(payload: Payload): string { 23 | if (payload && payload.formConfig && payload.formItems) { 24 | const { formConfig, formItems, submitFetch, fromTable } = payload; 25 | const item = formItems.find(item => item.type === 'upload'); 26 | 27 | const code = ` 28 | import React, { useEffect ${item ? ', useState' : ''} } from 'react'; 29 | import { 30 | Button, 31 | Modal, 32 | Form, 33 | Spin, 34 | Input, 35 | DatePicker, 36 | TimePicker, 37 | Cascader, 38 | InputNumber, 39 | Radio, 40 | Checkbox, 41 | Switch, 42 | Slider, 43 | Select, 44 | TreeSelect, 45 | Upload, 46 | Rate, 47 | message, 48 | } from 'antd'; 49 | import { isEmpty } from 'lodash-es'; 50 | import { FormInstance } from 'antd/lib/form'; 51 | import { Store } from 'antd/es/form/interface'; 52 | import { useRequest } from 'ahooks'; 53 | import useSpinning from '@/hooks/useSpinning'; 54 | import { getVerificationRules } from '@/pages/${getPageNameByPath(payload.path)}/validators'; 55 | console.log('emptyline'); 56 | const formLayout = { 57 | labelCol: { span: 6 }, 58 | wrapperCol: { span: 17 }, 59 | }; 60 | console.log('emptyline'); 61 | export default ({ 62 | visible, 63 | toggleVisible, 64 | formData, 65 | loading, 66 | ${fromTable ? `reload,` : ''} 67 | }: { 68 | visible: boolean; 69 | toggleVisible: () => void; 70 | formData: Store; 71 | loading: boolean; 72 | ${fromTable ? `reload?: () => void;` : ''} 73 | }) => { 74 | const [form] = Form.useForm(); 75 | const { tip, setTip } = useSpinning(); 76 | ${item ? `const [submitBtnDisabled, setSubmitBtnDisabled] = useState(false);` : ''} 77 | console.log('emptyline'); 78 | useEffect(() => { 79 | if (!isEmpty(formData)) { 80 | form.setFieldsValue(formData); 81 | } 82 | }, [formData]); 83 | console.log('emptyline'); 84 | const handleCancel = () => { 85 | toggleVisible(); 86 | form.resetFields(); 87 | } 88 | console.log('emptyline'); 89 | const submit = (values: Store) => { 90 | setTip('数据保存中,请稍候...'); 91 | console.log('emptyline'); 92 | const payload = { 93 | ...values, 94 | }; 95 | console.log('emptyline'); 96 | return API.${submitFetch && submitFetch.length === 3 ? `${submitFetch[0]}.${submitFetch[1]}.${ 97 | submitFetch[2].split('-')[0] 98 | }` : 'recruitment.person.addPerson'}.fetch(payload); 99 | }; 100 | console.log('emptyline'); 101 | const { run: handleFinish, loading: submitting } = useRequest(submit, { 102 | manual: true, 103 | onSuccess: () => { 104 | message.success('保存成功'); 105 | form.resetFields(); 106 | ${fromTable ? `reload && reload();` : ''} 107 | }, 108 | }); 109 | console.log('emptyline'); 110 | return ( 111 | form.submit()} 119 | onCancel={handleCancel} 120 | confirmLoading={submitting} 121 | > 122 | 123 |
124 | ${formItems 125 | .map(item => { 126 | const { 127 | label, 128 | name, 129 | type, 130 | required = false, 131 | customRules = '', 132 | ...restProps 133 | } = item; 134 | const rules = generateRules(customRules as string, required as boolean); 135 | return ` { 143 | if (Array.isArray(e)) { 144 | return e; 145 | } 146 | return e && e.fileList; 147 | }}` : ''} 148 | > 149 | ${createFormComponentsByType(type, restProps)} 150 | `; 151 | }) 152 | .join('')} 153 |
154 |
155 |
156 | ); 157 | }; 158 | `; 159 | return code; 160 | } 161 | return ''; 162 | } 163 | -------------------------------------------------------------------------------- /src/manage/templates/table.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 生成表格页面 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 16:14:11 6 | * @LastEditors: 廖军 7 | * @LastEditTime: 2020-10-27 14:07:26 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { ColumnType } from 'antd/lib/table'; 11 | import { generateBreadcrumbs } from './util'; 12 | 13 | export interface Payload { 14 | tableConfig: Store; 15 | columns: ColumnType[]; 16 | initialFetch?: string[]; 17 | menu: string; 18 | pageName: string; 19 | } 20 | 21 | export default function generateTable(payload: Payload): string { 22 | if (payload && payload.tableConfig && payload.columns) { 23 | const { tableConfig, columns, initialFetch, menu, pageName } = payload; 24 | 25 | const breadcrumbs = generateBreadcrumbs(menu); 26 | const columnsStrings = columns.map(config => JSON.stringify(config)); 27 | 28 | const code = ` 29 | import React, { useRef } from 'react'; 30 | import { message, Popconfirm, Button } from 'antd'; 31 | import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; 32 | import json from '@/utils/json'; 33 | import { initialPagination } from '@/constant'; 34 | ${breadcrumbs.length > 1 && `import CustomBreadcrumb from '@/components/CustomBreadcrumb';`} 35 | import LinkButtons from '@/components/LinkButtons'; 36 | import { Store } from 'antd/es/form/interface'; 37 | import FormActionMethods, { FormActionMethodsInstance } from '@/pages/${pageName}/components/FormActionMethods'; 38 | console.log('emptyline'); 39 | export default () => { 40 | const actionRef = useRef(); 41 | const formActionRef = useRef({}); 42 | const columns: ProColumns<${initialFetch && initialFetch.length === 3 ? `defs.${initialFetch[0]}.${initialFetch[2].split('-')[2]}` : 'defs.recruitment.PersonResultDTO'}>[] = [${[...columnsStrings, ` 43 | { 44 | title: '操作', 45 | dataIndex: 'id', 46 | align: 'left', 47 | copyable: false, 48 | valueType: 'text', 49 | hideInSearch: true, 50 | render: (_, row) => ( 51 | formActionRef.current?.onPreview!(row), 57 | }, 58 | { 59 | name: '编辑', 60 | key: 'edit', 61 | onClick: () => formActionRef.current?.onEdit!(row), 62 | }, 63 | { 64 | name: '删除', 65 | key: 'delete', 66 | onClick: () => formActionRef.current?.onDelete!(row), 67 | }, 68 | ]} 69 | /> 70 | ), 71 | } 72 | `].join(',')}]; 73 | console.log('emptyline'); 74 | return ( 75 | <> 76 | 77 | { 80 | delete rest._timestamp; 81 | const { 82 | list, 83 | page, 84 | total, 85 | } = await ${initialFetch && initialFetch.length === 3 ? `API.${initialFetch[0]}.${initialFetch[1]}.${initialFetch[2].split('-')[0]}` : 'API.recruitment.person.queryPerson'}.fetch( 86 | json.removeEmpty({ 87 | ...rest, 88 | page: current || initialPagination.page, 89 | pageSize: rest?.pageSize || initialPagination.pageSize, 90 | }) 91 | ); 92 | return { 93 | success: true, 94 | data: list || [], 95 | page, 96 | total, 97 | }; 98 | }} 99 | columns={columns} 100 | rowKey="${tableConfig.rowKey}" 101 | headerTitle="${tableConfig.headerTitle}" 102 | rowSelection={{}} 103 | toolBarRender={(_, { selectedRowKeys = [] }) => [ 104 | ...(selectedRowKeys?.length > 0 105 | ? [ 106 | formActionRef.current?.onDeleteBatch!(selectedRowKeys.map(id => +id))} 111 | okText="是" 112 | cancelText="否" 113 | > 114 | 115 | , 116 | ] 117 | : []), 118 | , 125 | ]} 126 | /> 127 | 128 | 129 | ); 130 | } 131 | `; 132 | return code; 133 | } 134 | return ''; 135 | } 136 | -------------------------------------------------------------------------------- /src/manage/templates/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-08 13:56:59 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-19 23:31:55 8 | */ 9 | import { LabeledValue } from 'antd/lib/select'; 10 | import { FormItemType, FormItemProps } from '../../../interfaces/common'; 11 | import { Rule } from 'antd/lib/form'; 12 | 13 | export function createFormComponentsByType( 14 | type: FormItemType, 15 | props: Omit, 16 | ) { 17 | const propsStr = generatePropsStr(props); 18 | switch (type) { 19 | case 'input': 20 | default: 21 | return ` 22 | 23 | `; 24 | case 'cascader': 25 | return ``; 26 | 27 | case 'checkbox': 28 | const { options: checkboxOptions, ...checkboxProps } = props; 29 | return ``; 37 | 38 | case 'date': 39 | return ``; 40 | 41 | case 'number': 42 | return ``; 43 | 44 | case 'password': 45 | return ``; 46 | 47 | case 'radio': 48 | const { options: radioOptions, ...radioProps } = props; 49 | return ``; 55 | 56 | case 'range': 57 | return ``; 58 | 59 | case 'rate': 60 | return ``; 61 | 62 | case 'select': 63 | const { options: selectOptions, ...selectProps } = props; 64 | return ``; 72 | 73 | case 'slider': 74 | return ``; 75 | 76 | case 'switch': 77 | return ``; 78 | 79 | case 'textarea': 80 | return ``; 81 | 82 | case 'time': 83 | return ``; 84 | 85 | case 'treeselect': 86 | return ``; 87 | 88 | case 'upload': 89 | return ` { 90 | if (info.file.status === 'uploading') { 91 | setSubmitBtnDisabled(true); 92 | } else { 93 | setSubmitBtnDisabled(false); 94 | } 95 | }} ${propsStr}>`; 96 | } 97 | } 98 | 99 | /** 100 | * 把属性打平成字符串 101 | */ 102 | export function generatePropsStr(props: object): string { 103 | const result = `${Object.entries(props) 104 | .map(item => { 105 | const [key, value] = item; 106 | if (typeof value === 'number' || typeof value === 'bigint') { 107 | return `${key}={${value}}`; 108 | } else if (typeof value === 'boolean') { 109 | return value ? `${key}` : ``; 110 | } else if (typeof value === 'string') { 111 | return `${key}="${value}"`; 112 | } else if (typeof value === 'function') { 113 | return `${key}={${value.toString()}}`; 114 | } else if (typeof value === 'object') { 115 | return `${key}={${JSON.stringify(value)}}`; 116 | } 117 | 118 | return ''; 119 | }) 120 | .join(' ')}`; 121 | 122 | return result; 123 | } 124 | 125 | /** 126 | * 将一个数组按照指定的列拆分成N个二维数组 127 | * @param formItems 128 | * @param cols 129 | */ 130 | export function transformFormItemLines(formItems: FormItemProps[], cols = 3) { 131 | let lineNum = 132 | formItems.length % cols === 0 133 | ? formItems.length / cols 134 | : Math.floor(formItems.length / cols + 1); 135 | let res = []; 136 | for (let i = 0; i < lineNum; i++) { 137 | let temp = formItems.slice(i * cols, i * cols + cols); 138 | res.push(temp); 139 | } 140 | return res; 141 | } 142 | 143 | export function generateRules(customRules?: string, required?: boolean) { 144 | const rules: Rule[] = [{ 145 | whitespace: true, 146 | }]; 147 | try { 148 | if (customRules) { 149 | const _rules = eval((customRules as string).replace(/\u21b5/g, '')); 150 | if (Array.isArray(_rules)) { 151 | rules.push(..._rules); 152 | } 153 | } 154 | } catch (error) { 155 | console.error(error.message); 156 | } 157 | 158 | if (required) { 159 | const requiredObj = rules.find(item => Object.keys(item).includes('required')); 160 | if (!requiredObj || requiredObj['required'] === false) { 161 | rules.push({ 162 | required: true, 163 | }); 164 | } 165 | } 166 | 167 | return JSON.stringify(rules); 168 | } 169 | 170 | /** 171 | * 生成面包屑 172 | * @param menu 173 | * @param path 174 | */ 175 | export function generateBreadcrumbs(menu: string) { 176 | const menus = menu ? menu.split('/') : []; 177 | const breadcrumbs = []; 178 | 179 | for (let _menu of menus) { 180 | breadcrumbs.push({ 181 | breadcrumbName: _menu, 182 | }); 183 | } 184 | 185 | return JSON.stringify(breadcrumbs); 186 | } 187 | -------------------------------------------------------------------------------- /src/screen/index.ts: -------------------------------------------------------------------------------- 1 | import { IApi } from 'umi'; 2 | import { existsSync, mkdirSync } from 'fs'; 3 | import { ScreenConfigPayload } from '../../interfaces/screen'; 4 | import generateScreen from './templates/generateScreen'; 5 | import generateLayout from './templates/generateLayout'; 6 | import generateRow from './templates/generateRow'; 7 | import generateCol from './templates/generateCol'; 8 | 9 | /** 10 | * 生成大屏的一系列文件 11 | * @param payload 12 | * @param api 13 | */ 14 | export default function(payload: ScreenConfigPayload, api: IApi) { 15 | const componentsPath = api.paths.absPagesPath + '/components'; 16 | if (!existsSync(componentsPath)) { 17 | mkdirSync(componentsPath); 18 | } 19 | 20 | // 生成大屏的配置 21 | generateScreen(api.paths.absPagesPath!, payload); 22 | 23 | const { gutter, layout } = payload; 24 | 25 | // 生成布局组件(Left/Center/Right) 26 | layout.forEach((item) => { 27 | const { 28 | name: layoutName, 29 | rows, 30 | } = item; 31 | 32 | const layoutPath = componentsPath + '/' + layoutName; 33 | if (!existsSync(layoutPath)) { 34 | mkdirSync(layoutPath); // <- 创建布局的文件夹 35 | } 36 | generateLayout(layoutPath, item); 37 | 38 | rows.forEach(row => { 39 | const { name: rowName, cols } = row; 40 | 41 | const rowPath = layoutPath + '/' + rowName; 42 | if (!existsSync(rowPath)) { 43 | mkdirSync(rowPath); // <- 创建行的文件夹 44 | } 45 | generateRow(rowPath, gutter, row); 46 | 47 | cols.forEach(col => { 48 | const { name: colName } = col; 49 | 50 | const colPath = rowPath + '/' + colName; 51 | if (!existsSync(colPath)) { 52 | mkdirSync(colPath); 53 | } 54 | generateCol(colPath, col); 55 | }); 56 | }); 57 | }); 58 | 59 | return true; 60 | } 61 | -------------------------------------------------------------------------------- /src/screen/templates/generateLayout.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import { ScreenConfigPayloadLayout } from "../../../interfaces/screen"; 3 | import { removeUnusedImport } from "../../utils/removeUnusedImport"; 4 | 5 | /** 6 | * 生成布局组件 7 | * @param payload 8 | */ 9 | export default function(layoutPath: string, layout: ScreenConfigPayloadLayout) { 10 | const code = ` 11 | import React from 'react'; 12 | import { Row, Col } from 'antd'; 13 | ${layout.rows.map(item => `import ${item.name} from './${item.name}';`).join('')} 14 | 15 | export default () => { 16 | return ( 17 | 18 |
24 | ${layout.rows.map(row => `<${row.name} />`).join('')} 25 |
26 | 27 | ); 28 | } 29 | `; 30 | const removeUnusedImportCode = removeUnusedImport(code); 31 | if (removeUnusedImportCode) { 32 | writeFileSync(`${layoutPath}/index.tsx`, removeUnusedImportCode, 'utf-8'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/screen/templates/generateRow.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import { ScreenConfigPayloadRow } from "../../../interfaces/screen"; 3 | import { removeUnusedImport } from "../../utils/removeUnusedImport"; 4 | 5 | export default function(rowPath: string, gutter: number, row: ScreenConfigPayloadRow) { 6 | const code = ` 7 | import React from 'react'; 8 | import { Row, Col } from 'antd'; 9 | ${row.cols.map(item => `import ${item.name} from './${item.name}';`).join('')} 10 | 11 | export default () => { 12 | return ( 13 | 14 | ${row.cols.map(col => `<${col.name} />`).join('')} 15 | 16 | ); 17 | } 18 | `; 19 | const removeUnusedImportCode = removeUnusedImport(code); 20 | if (removeUnusedImportCode) { 21 | writeFileSync(`${rowPath}/index.tsx`, removeUnusedImportCode, 'utf-8'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/screen/templates/generateScreen.ts: -------------------------------------------------------------------------------- 1 | import { ScreenConfigPayload } from "../../../interfaces/screen"; 2 | import { removeUnusedImport } from "../../utils/removeUnusedImport"; 3 | import { writeFileSync } from "fs"; 4 | 5 | /** 6 | * 生成大屏页面 7 | * @param payload 8 | */ 9 | export default function(screenPath: string, payload: ScreenConfigPayload) { 10 | const { title, titleStyle, gutter, layout } = payload; 11 | 12 | const code = ` 13 | import React from 'react'; 14 | import { Row, Col } from 'antd'; 15 | ${layout.map(item => `import ${item.name} from './components/${item.name}';`).join('')} 16 | 17 | export default () => { 18 | return ( 19 |
20 | 21 | 22 |
${title}
23 | 24 |
25 | 26 | ${layout.map(item => ` 27 | <${item.name} /> 28 | `).join('')} 29 | 30 |
31 | ); 32 | } 33 | `; 34 | const removeUnusedImportCode = removeUnusedImport(code); 35 | if (removeUnusedImportCode) { 36 | writeFileSync(`${screenPath}/index.tsx`, removeUnusedImportCode, 'utf-8'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-01 18:11:49 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-06 16:34:22 8 | */ 9 | import fs from 'fs'; 10 | import { join } from 'path'; 11 | import { utils } from 'umi'; 12 | 13 | const { winPath } = utils; 14 | 15 | export interface TreeData { 16 | title: string; 17 | value: string; 18 | key: string; 19 | children?: TreeData[]; 20 | } 21 | 22 | /** 23 | * 遍历文件地址 24 | * @param path 25 | */ 26 | export const getFolderTreeData = ( 27 | path: string, 28 | parentPath: string = '/', 29 | depth: number = 0, 30 | ): TreeData[] => { 31 | const files = fs.readdirSync(winPath(path)); 32 | return files 33 | .map((fileName: string) => { 34 | const status = fs.statSync(join(path, fileName)); 35 | // 是文件夹 并且不已 . 开头, 且最深三层 36 | if (status.isDirectory() && fileName.indexOf('.') !== 0 && depth < 3) { 37 | const absPath = winPath(join(path, fileName)); 38 | const absPagePath = winPath(join(parentPath, fileName)); 39 | const children = getFolderTreeData(absPath, absPagePath, depth + 1); 40 | if (children && children.length > 0) { 41 | return { 42 | key: absPagePath, 43 | title: fileName, 44 | value: absPagePath, 45 | children, 46 | } as TreeData; 47 | } 48 | return { 49 | title: fileName, 50 | value: absPagePath, 51 | key: absPagePath, 52 | } as TreeData; 53 | } 54 | return undefined; 55 | }) 56 | .filter(item => item !== undefined) as TreeData[]; 57 | }; 58 | 59 | /** 60 | * 遍历文件地址 61 | * 包含文件 62 | * @param path 63 | */ 64 | export const getFilesTreeData = ( 65 | path: string, 66 | parentPath: string = '/', 67 | depth: number = 0, 68 | ): TreeData[] => { 69 | const files = fs.readdirSync(winPath(path)); 70 | return files 71 | .map((fileName: string) => { 72 | const status = fs.statSync(join(path, fileName)); 73 | const isDirectory = status.isDirectory(); 74 | // 是文件夹 并且不已 . 开头, 且最深五层 75 | if (fileName.indexOf('.') !== 0 && depth < 5) { 76 | if ( 77 | !isDirectory && 78 | !fileName.includes('.tsx') && 79 | !fileName.includes('.jsx') && 80 | !fileName.includes('.js') && 81 | !fileName.includes('.ts') 82 | ) { 83 | return undefined; 84 | } 85 | const absPath = winPath(join(path, fileName)); 86 | const absPagePath = winPath(join(parentPath, fileName)); 87 | const children = isDirectory ? getFilesTreeData(absPath, absPagePath, depth + 1) : []; 88 | return { 89 | selectable: !isDirectory, 90 | key: absPagePath, 91 | title: fileName, 92 | value: absPagePath, 93 | children, 94 | }; 95 | } 96 | return undefined; 97 | }) 98 | .filter(obj => obj) as TreeData[]; 99 | }; 100 | -------------------------------------------------------------------------------- /src/utils/removeUnusedImport.ts: -------------------------------------------------------------------------------- 1 | import * as types from '@babel/types'; 2 | import * as parser from '@babel/parser'; 3 | import traverse, { NodePath } from '@babel/traverse'; 4 | import generate from '@babel/generator'; 5 | 6 | export function removeUnusedImport(code: string) { 7 | // 解析代码,构建ast 8 | const ast: types.File = parser.parse(code, { 9 | sourceType: 'module', 10 | plugins: ['jsx', 'typescript'], 11 | }); 12 | 13 | const opts = { 14 | ignore: ['react'], 15 | }; 16 | 17 | traverse(ast, { 18 | Program: { 19 | enter: path => { 20 | for (const [, binding] of Object.entries(path.scope.bindings)) { 21 | if (binding.kind === 'module') { 22 | if (!binding.referenced) { 23 | const source = binding.path.parentPath.get('source'); 24 | if ( 25 | types.isStringLiteral(source) && 26 | (!opts.ignore || !match(opts.ignore, (source as NodePath).node.value)) 27 | ) { 28 | if (binding.path.node.type === 'ImportSpecifier') { 29 | binding.path.remove(); 30 | } else if (binding.path.parentPath) { 31 | binding.path.parentPath.remove(); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | }, 39 | }); 40 | return generate(ast, {}).code; 41 | } 42 | 43 | function match(rule: any, value: any): boolean { 44 | if (typeof rule === 'string') { 45 | return rule === value; 46 | } 47 | if (rule instanceof RegExp) { 48 | return rule.test(value); 49 | } 50 | if (typeof rule === 'function') { 51 | return rule(value); 52 | } 53 | if (Array.isArray(rule)) { 54 | return rule.some(r => match(r, value)); 55 | } 56 | return false; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/writeNewMenu.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, existsSync, readFileSync } from 'fs'; 2 | import * as types from '@babel/types'; 3 | import * as parser from '@babel/parser'; 4 | import traverse from '@babel/traverse'; 5 | import generate from '@babel/generator'; 6 | import prettier from 'prettier'; 7 | 8 | interface Resource { 9 | menu: string; 10 | path: string; 11 | } 12 | 13 | export function writeNewMenu(resource: Resource, mockPath: string) { 14 | const code = getNewMenuCode(resource, mockPath); 15 | writeFileSync(mockPath, code, 'utf-8'); 16 | } 17 | 18 | /** 19 | * 生成权限资源的mock数据,方便系统构建菜单 20 | * @param resource 21 | * @param mockPath 22 | */ 23 | function getNewMenuCode(resource: Resource, mockPath: string) { 24 | const { menu, path } = resource; 25 | if (existsSync(mockPath)) { 26 | // 解析文件内容,构建ast 27 | const ast: types.File = parser.parse(readFileSync(mockPath, 'utf-8'), { 28 | sourceType: 'module', 29 | plugins: ['typescript'], 30 | }); 31 | 32 | // 对ast的节点进行处理,在指定的位置插入新节点 33 | traverse(ast, { 34 | ObjectExpression({ node, parent }) { 35 | if (types.isArrayExpression(parent)) { 36 | return; 37 | } 38 | const { properties } = node; 39 | const property = properties.find( 40 | p => types.isObjectProperty(p) && types.isIdentifier(p.key) && p.key.name === 'data', 41 | ); 42 | if (property) { 43 | // data: [] 44 | const { elements = [] } = (property as types.ObjectProperty) 45 | .value as types.ArrayExpression; 46 | 47 | if (!menu.includes('/') && !path.substr(1).includes('/') && path.startsWith('/')) { 48 | // 只是一级目录 49 | const newNode = (parser.parse( 50 | `(${JSON.stringify({ 51 | key: path, 52 | apiUrl: path, 53 | description: menu, 54 | children: [], 55 | })})`, 56 | ).program.body[0] as any).expression; 57 | elements.push(newNode); 58 | } else { 59 | // 有二级目录 60 | const menus = menu.split('/').filter(item => item); 61 | const _paths = path.split('/').filter(item => item); 62 | const paths = ['/' + _paths[0], !path.startsWith('/') ? '/' + path : path]; 63 | 64 | /** 65 | * 这里的逻辑比较复杂,注释一下: 66 | * 1. 首先判断一级目录是否存在: 67 | * a). 如果不存在,则直接保存整个对象 68 | * b). 如果存在,进行第二步判断 69 | * 2. 二级目录是否存在: 70 | * a). 如果不存在,在children最后插入 71 | */ 72 | const submenu = { 73 | key: paths[0], 74 | apiUrl: paths[0], 75 | description: menus[0], 76 | children: [ 77 | { 78 | key: paths[1], 79 | apiUrl: paths[1], 80 | description: menus.length === 2 ? menus[1] : menus[0], 81 | children: [], 82 | }, 83 | ], 84 | }; 85 | const menuItem = { 86 | key: paths[1], 87 | apiUrl: paths[1], 88 | description: menus.length === 2 ? menus[1] : menus[0], 89 | children: [], 90 | }; 91 | 92 | /** 93 | * 遍历data的elements(也就是data下的对象(key/apiUrl/description/children)), 94 | * elements里面有properties属性,是一个对象。这个对象就是element的键值对 95 | * 在这个键值对里面找到key.name === submenu.key 96 | * 如果找到了,说明一级目录存在,如果没有找到,说明以及目录不存在 97 | * 一级目录存在的情况下,再从这个element的键值对里面去找key.name === 'children',它也是一个elements,再重复上面的步骤判断二级目录是否存在 98 | */ 99 | let node = null; 100 | let childrenNode = null; 101 | for (const ele of elements) { 102 | const { properties } = ele as types.ObjectExpression; 103 | node = properties.find( 104 | p => 105 | ((p as types.ObjectProperty).value as types.StringLiteral).value === submenu.key, 106 | ); 107 | if (node) { 108 | childrenNode = properties.find( 109 | p => (p as types.ObjectProperty).key.name === 'children', 110 | ); 111 | } 112 | } 113 | if (!node) { 114 | // 父菜单也不存在的话,直接把整个结构添加进去 115 | elements.push( 116 | (parser.parse(`(${JSON.stringify(submenu)})`).program.body[0] as any).expression, 117 | ); 118 | } else if (childrenNode) { 119 | const { elements: childrenElements } = (childrenNode as types.ObjectProperty) 120 | .value as types.ArrayExpression; 121 | if (childrenElements.length === 0) { 122 | childrenElements.push( 123 | (parser.parse(`(${JSON.stringify(menuItem)})`).program.body[0] as any).expression, 124 | ); 125 | } else { 126 | // 判断要添加的菜单是否已经存在,已经存在的话就不添加 127 | let existed = false; 128 | for (const ele of childrenElements) { 129 | const { properties } = ele as types.ObjectExpression; 130 | node = properties.find( 131 | p => 132 | ((p as types.ObjectProperty).value as types.StringLiteral).value === 133 | menuItem.key, 134 | ); 135 | if (node) { 136 | existed = true; 137 | break; 138 | } 139 | } 140 | !existed && 141 | childrenElements.unshift( 142 | (parser.parse(`(${JSON.stringify(menuItem)})`).program.body[0] as any) 143 | .expression, 144 | ); 145 | } 146 | } 147 | } 148 | } 149 | }, 150 | }); 151 | 152 | // 根据处理之后的ast生成最终的代码 153 | const code = generate(ast, {}).code; 154 | return prettier.format(code, { 155 | singleQuote: true, 156 | trailingComma: 'es5', 157 | printWidth: 100, 158 | parser: 'typescript', 159 | }); 160 | } 161 | return ''; 162 | } 163 | -------------------------------------------------------------------------------- /src/utils/writeNewRoute.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, existsSync } from 'fs'; 2 | import { join, dirname } from 'path'; 3 | import * as parser from '@babel/parser'; 4 | import traverse from '@babel/traverse'; 5 | import generate from '@babel/generator'; 6 | import * as t from '@babel/types'; 7 | import { winPath, createDebug } from '@umijs/utils'; 8 | import prettier from 'prettier'; 9 | 10 | const debug = createDebug('umi-build-dev:writeNewRoute'); 11 | 12 | type Route = { 13 | path: string; 14 | component: string; 15 | exact?: boolean; 16 | title?: string; 17 | }; 18 | 19 | /** 20 | * 将路由写入路由文件 21 | * @param {*} newRoute 新的路由配置: { path, component, ... } 22 | * @param {*} configPath 配置路径 23 | * @param {*} absSrcPath 代码路径 24 | */ 25 | export function writeNewRoute(newRoute: Route, configPath: string, absSrcPath: string) { 26 | const { code, routesPath } = getNewRouteCode(configPath, newRoute, absSrcPath); 27 | writeFileSync(routesPath, code, 'utf-8'); 28 | } 29 | 30 | /** 31 | * 获取目标 32 | * @param {*} configPath 33 | * @param {*} newRoute 34 | */ 35 | export function getNewRouteCode(configPath: string, newRoute: Route, absSrcPath: string) { 36 | debug(`find routes in configPath: ${configPath}`); 37 | const ast: any = parser.parse(readFileSync(configPath, 'utf-8'), { 38 | sourceType: 'module', 39 | plugins: ['typescript'], 40 | }); 41 | let routesNode = null; 42 | const importModules = []; 43 | // 查询当前配置文件是否导出 routes 属性 44 | traverse(ast, { 45 | Program({ node }) { 46 | // find import 47 | const { body } = node; 48 | body.forEach(item => { 49 | if (t.isImportDeclaration(item)) { 50 | const { specifiers } = item; 51 | const defaultSpecifier = specifiers.find( 52 | s => t.isImportDefaultSpecifier(s) && t.isIdentifier(s.local), 53 | ); 54 | if (defaultSpecifier && t.isStringLiteral(item.source)) { 55 | importModules.push({ 56 | identifierName: defaultSpecifier.local.name, 57 | modulePath: item.source.value, 58 | }); 59 | } 60 | } 61 | }); 62 | }, 63 | AssignmentExpression({ node }) { 64 | // for exports.routes 65 | const { left, operator, right } = node; 66 | if ( 67 | operator === '=' && 68 | t.isMemberExpression(left) && 69 | t.isIdentifier(left.object) && 70 | left.object.name === 'exports' && 71 | t.isIdentifier(left.property) && 72 | left.property.name === 'routes' 73 | ) { 74 | routesNode = right; 75 | } 76 | }, 77 | ExportDefaultDeclaration({ node }) { 78 | // export default [] 79 | const { declaration } = node; 80 | if (t.isArrayExpression(declaration)) { 81 | routesNode = declaration; 82 | } 83 | }, 84 | ObjectExpression({ node, parent }) { 85 | // find routes on object, like { routes: [] } 86 | if (t.isArrayExpression(parent)) { 87 | // children routes 88 | return; 89 | } 90 | const { properties } = node; 91 | properties.forEach((p: any) => { 92 | const { key, value } = p; 93 | if (t.isObjectProperty(p) && t.isIdentifier(key) && key.name === 'routes') { 94 | routesNode = value; 95 | } 96 | }); 97 | }, 98 | }); 99 | if (routesNode) { 100 | // routes 配置不在当前文件, 需要 load 对应的文件 export default { routes: pageRoutes } case 1 101 | if (!t.isArrayExpression(routesNode)) { 102 | const source = importModules.find(m => m.identifierName === routesNode.name); 103 | if (source) { 104 | const newConfigPath = getModulePath(configPath, source.modulePath, absSrcPath); 105 | return getNewRouteCode(newConfigPath, newRoute, absSrcPath); 106 | } 107 | throw new Error(`can not find import of ${routesNode.name}`); 108 | } else { 109 | // 配置在当前文件 // export default { routes: [] } case 2 110 | writeRouteNode(routesNode, newRoute); 111 | } 112 | } else { 113 | throw new Error('route array config not found.'); 114 | } 115 | const code = generateCode(ast); 116 | debug(code, configPath); 117 | return { code, routesPath: configPath }; 118 | } 119 | 120 | function getNewRouteNode(newRoute: any) { 121 | return (parser.parse(`(${JSON.stringify(newRoute)})`).program.body[0] as any).expression; 122 | } 123 | 124 | /** 125 | * 写入节点 126 | * @param {*} node 找到的节点 127 | * @param {*} newRoute 新的路由配置 128 | */ 129 | export function writeRouteNode(targetNode, newRoute: Route, currentPath = '/') { 130 | debug(`writeRouteNode currentPath newRoute.path: ${newRoute.path} currentPath: ${currentPath}`); 131 | const { elements } = targetNode; 132 | const paths: string[] = elements.map(ele => { 133 | if (!t.isObjectExpression(ele)) { 134 | return false; 135 | } 136 | const { properties } = ele; 137 | const redirect = properties.find((p: any) => p.key.name === 'redirect'); 138 | if (redirect) { 139 | return false; 140 | } 141 | const pathProp: any = properties.find((p: any) => p.key.name === 'path'); 142 | if (!pathProp) { 143 | return currentPath; 144 | } 145 | let fullPath: string = pathProp.value.value; 146 | if (fullPath.indexOf('/') !== 0) { 147 | fullPath = join(currentPath, fullPath); 148 | } 149 | return fullPath; 150 | }); 151 | debug('paths', paths); 152 | const matchedIndex = paths.findIndex(p => p && newRoute.path.indexOf(winPath(p)) === 0); 153 | const newNode = getNewRouteNode(newRoute); 154 | if (matchedIndex === -1) { 155 | elements.unshift(newNode); 156 | // return container for test 157 | return targetNode; 158 | } 159 | // matched, insert to children routes 160 | const matchedEle = elements[matchedIndex]; 161 | const routesProp = matchedEle.properties.find(p => p.key.name === 'routes'); 162 | if (!routesProp) { 163 | // not find children routes, insert before the matched element 164 | elements.splice(matchedIndex, 0, newNode); 165 | return targetNode; 166 | } 167 | return writeRouteNode(routesProp.value, newRoute, paths[matchedIndex]); 168 | } 169 | 170 | /** 171 | * 生成代码 172 | * @param {*} ast 173 | */ 174 | function generateCode(ast) { 175 | const newCode = generate(ast, {}).code; 176 | return prettier.format(newCode, { 177 | singleQuote: true, 178 | trailingComma: 'es5', 179 | printWidth: 100, 180 | parser: 'typescript', 181 | }); 182 | } 183 | 184 | /** 185 | * 获取路由配置的真实路径 186 | * @param {*} modulePath 187 | */ 188 | function getModulePath(configPath: string, modulePath: string, absSrcPath: string) { 189 | // like @/route.config 190 | if (/^@\//.test(modulePath)) { 191 | modulePath = join(absSrcPath, modulePath.replace(/^@\//, '')); 192 | } else { 193 | modulePath = join(dirname(configPath), modulePath); 194 | } 195 | if (!/\.[j|t]s$/.test(modulePath)) { 196 | if (existsSync(`${modulePath}.js`)) { 197 | modulePath = `${modulePath}.js`; 198 | } else { 199 | modulePath = `${modulePath}.ts`; 200 | } 201 | } 202 | return modulePath; 203 | } 204 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "jsx": "preserve", 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "declaration": true, 14 | "resolveJsonModule": true 15 | }, 16 | "exclude": ["node_modules", "dist", "**/*.spec.ts", "lib", "fixtures", "examples"] 17 | } 18 | -------------------------------------------------------------------------------- /ui/components/ConfigActions/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Button, Tooltip } from 'antd'; 3 | import { 4 | ArrowUpOutlined, 5 | ArrowDownOutlined, 6 | ArrowLeftOutlined, 7 | ArrowRightOutlined, 8 | SettingOutlined, 9 | DeleteOutlined, 10 | CopyOutlined, 11 | } from '@ant-design/icons'; 12 | 13 | export default ({ 14 | moveUp, 15 | moveDown, 16 | configItem, 17 | deleteItem, 18 | copyItem, 19 | position = 'left', 20 | style, 21 | }: { 22 | position?: 'left' | 'top'; 23 | moveUp?: () => void; 24 | moveDown?: () => void; 25 | configItem?: () => void; 26 | deleteItem?: () => void; 27 | copyItem?: () => void; 28 | style?: CSSProperties; 29 | }) => { 30 | return ( 31 |
39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 53 | 54 | 55 | 58 | 59 | 60 | 63 | 64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /ui/components/FormItemConfig/index.module.less: -------------------------------------------------------------------------------- 1 | .formItemConfig { 2 | display: flex; 3 | align-items: flex-start; 4 | 5 | :global { 6 | .ant-form-item { 7 | flex: 1; 8 | width: 100%; 9 | } 10 | } 11 | } 12 | .top { 13 | flex-direction: column; 14 | } 15 | -------------------------------------------------------------------------------- /ui/components/FormItemConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useState, useMemo } from 'react'; 2 | import { Drawer, Form, Input, Button, Radio, Tooltip, Select } from 'antd'; 3 | import { QuestionCircleOutlined } from '@ant-design/icons'; 4 | import { FormItemProps, FormItemType } from '../../../interfaces/common'; 5 | import { Store } from 'antd/lib/form/interface'; 6 | import * as itemProps from './props'; 7 | import renderFormItem from '../FormItemConfig'; 8 | import { filterEmpty } from '../../utils'; 9 | import Context from '../../pages/manage/Context'; 10 | 11 | const { Option } = Select; 12 | 13 | export default ({ 14 | visible, 15 | onVisible, 16 | index, 17 | formItem, 18 | onConfirm, 19 | submitFetch, 20 | initialFetch, 21 | from = 'form', 22 | }: { 23 | visible: boolean; 24 | onVisible: (visible: boolean) => void; 25 | index?: number; 26 | formItem: FormItemProps; 27 | onConfirm: (index: number, formItem: FormItemProps) => void; 28 | submitFetch?: string[]; 29 | initialFetch?: string[]; 30 | from?: 'form' | 'detail'; 31 | }) => { 32 | const { baseClasses = [] } = useContext(Context); 33 | 34 | const [form] = Form.useForm(); 35 | const { name, type, label, placeholder, ...restProps } = formItem; 36 | 37 | const [paramsName, setParamsName] = useState(); 38 | const [responseName, setResponseName] = useState(); 39 | 40 | useEffect(() => { 41 | form.setFieldsValue(formItem); 42 | }, [formItem]); 43 | 44 | /** submitFetch中第三个值为value-paramsName-responseName,提交表单数据选用paramsName作为DTO */ 45 | useEffect(() => { 46 | if (submitFetch && submitFetch.length === 3) { 47 | const paramsName = submitFetch[2].split('-')[1]; 48 | setParamsName(paramsName); 49 | } 50 | }, [submitFetch]); 51 | 52 | /** initialFetch中第三个值为value-paramsName-responseName,获取初始数据选用responseName作为DTO */ 53 | useEffect(() => { 54 | if (initialFetch && initialFetch.length === 3) { 55 | const responseName = initialFetch[2].split('-')[2]; 56 | setResponseName(responseName); 57 | } 58 | }, [initialFetch]); 59 | 60 | const handleFinish = (values: Store) => { 61 | if (index !== undefined) { 62 | const { prop, ...restValues } = values; 63 | onConfirm(index, { ...formItem, ...filterEmpty(restValues) }); 64 | onVisible(false); 65 | form.resetFields(); 66 | } 67 | }; 68 | 69 | const properties = useMemo(() => { 70 | if (from === 'form') { 71 | return baseClasses.find(item => item.name === paramsName)?.properties || []; 72 | } 73 | return baseClasses.find(item => item.name === responseName)?.properties || []; 74 | }, [baseClasses, paramsName, from, responseName]); 75 | 76 | const handleChange = (value: string) => { 77 | const matchClass = properties.find(item => item.value === value); 78 | form.setFieldsValue({ 79 | label: matchClass?.label, 80 | name: value, 81 | required: matchClass?.required, 82 | }); 83 | }; 84 | 85 | const propVisible = 86 | (from === 'form' && submitFetch && submitFetch.length > 0) || 87 | (from === 'detail' && initialFetch && initialFetch.length > 0); 88 | 89 | return ( 90 | onVisible(false)} 94 | width={400} 95 | > 96 |
103 | {propVisible && ( 104 | 105 | 113 | 114 | )} 115 | 121 | 122 | 123 | 129 | 130 | 131 | {from === 'form' ? ( 132 | <> 133 | 139 | 145 | 146 | 149 | 自定义规则 150 | 161 | 162 | 163 | 164 | } 165 | name="customRules" 166 | > 167 | 168 | 169 | 170 | 171 | 172 | {renderOtherProps(type)} 173 | 174 | ) : ( 175 | 176 | 180 | 181 | )} 182 | 183 | 186 | 187 |
188 |
189 | ); 190 | }; 191 | 192 | /** 193 | * 渲染表单元素本身的属性 194 | * @param type 195 | * @param props 196 | */ 197 | function renderOtherProps(type: FormItemType) { 198 | let props: FormItemProps[] = itemProps[`${type}Props`]; 199 | return props.map(item => renderFormItem({ formItem: item })); 200 | } 201 | -------------------------------------------------------------------------------- /ui/components/FormItemsDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Drawer, 4 | Form, 5 | Cascader, 6 | Checkbox, 7 | Input, 8 | DatePicker, 9 | InputNumber, 10 | Radio, 11 | Switch, 12 | Slider, 13 | Select, 14 | TreeSelect, 15 | Upload, 16 | TimePicker, 17 | Rate, 18 | Button, 19 | } from 'antd'; 20 | import { UploadOutlined } from '@ant-design/icons'; 21 | import { FormItemType } from '../../../interfaces/common'; 22 | import { CheckboxChangeEvent } from 'antd/lib/checkbox'; 23 | 24 | const { RangePicker } = DatePicker; 25 | const { Option } = Select; 26 | 27 | export default ({ 28 | setVisible, 29 | visible, 30 | onSubmit, 31 | }: { 32 | visible: boolean; 33 | setVisible: (visible: boolean) => void; 34 | onSubmit: (checkedComponents: FormItemType[]) => void; 35 | }) => { 36 | const [checkedComponents, setCheckedComponents] = useState([]); 37 | 38 | /** 复选框是否选中 */ 39 | const handleChange = (type: FormItemType) => (e: CheckboxChangeEvent) => { 40 | if (e.target.checked) { 41 | // 加到checkedComponents里面 42 | setCheckedComponents(checked => [...checked, type]); 43 | } else { 44 | // 从checkedComponents移出 45 | const checked = checkedComponents.slice(); 46 | const index = checked.findIndex(item => item === type); 47 | if (index > -1) { 48 | checked.splice(index, 1); 49 | } 50 | setCheckedComponents(checked); 51 | } 52 | }; 53 | 54 | /** 将选中的这些表单元素传出去 */ 55 | const handleSubmit = () => { 56 | onSubmit(checkedComponents); 57 | setCheckedComponents([]); 58 | }; 59 | 60 | const handleClose = () => { 61 | setVisible(false); 62 | setCheckedComponents([]); 63 | }; 64 | 65 | return ( 66 | 67 |
68 | 文本输入框}> 69 | 70 | 71 | 密码输入框}> 72 | 73 | 74 | 文本域输入框}> 75 | 76 | 77 | 级联选择}> 78 | 114 | 115 | 日期选择器}> 116 | 117 | 118 | 日期范围选择器}> 119 | 120 | 121 | 时间选择器}> 122 | 123 | 124 | 数字输入框}> 125 | 126 | 127 | 单选框}> 128 | 129 | 130 | 131 | 132 | 133 | 复选框}> 134 | 135 | 136 | 开关}> 137 | 138 | 139 | 滑动输入条}> 140 | 141 | 142 | 下拉选择}> 143 | 148 | 149 | 树形选择}> 150 | 173 | 174 | 上传}> 175 | 176 | 179 | 180 | 181 | 评分}> 182 | 183 | 184 | 187 |
188 |
189 | ); 190 | }; 191 | -------------------------------------------------------------------------------- /ui/components/Title/index.module.less: -------------------------------------------------------------------------------- 1 | .title { 2 | height: 32px; 3 | line-height: 32px; 4 | 5 | ::before { 6 | content: ''; 7 | display: inline-block; 8 | height: 16px; 9 | width: 4px; 10 | background: #0189fb; 11 | margin-right: 10px; 12 | transform: translateY(2px); 13 | } 14 | 15 | span { 16 | display: inline-block; 17 | flex: 1; 18 | overflow: hidden; 19 | white-space: nowrap; 20 | text-overflow: ellipsis; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.less'; 3 | 4 | export default ({ text = '' }: { text: string }) => ( 5 |
6 | {text} 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /ui/hooks/useCard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 对表单或者详情页的Card的操作的封装。包含上移、下移、复制 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-11 11:07:24 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-14 18:28:44 8 | */ 9 | import { useState } from 'react'; 10 | import { CardItemProps, FormItemProps, FormItemType } from '../../interfaces/common'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | import produce from 'immer'; 13 | import faker from 'faker'; 14 | 15 | export default function useCard() { 16 | const [cards, setCards] = useState([{ title: '自定义Card0', formItems: [] }]); 17 | const [cardIndex, setCardIndex] = useState(0); 18 | const [currentCard, setCurrentCard] = useState(); 19 | const [itemIndex, setItemIndex] = useState(); 20 | const [currentItem, setCurrentItem] = useState(); 21 | 22 | /** 23 | * 上移Card 24 | * @param index 25 | */ 26 | const moveCardUp = (index: number) => () => { 27 | if (index === 0) return; 28 | 29 | setCards( 30 | produce(cards, draft => { 31 | const card = draft.splice(index, 1); 32 | draft.splice(index - 1, 0, ...card); 33 | }), 34 | ); 35 | }; 36 | 37 | /** 38 | * 下移Card 39 | * @param index 40 | */ 41 | const moveCardDown = (index: number) => () => { 42 | if (index === cards.length - 1) return; 43 | 44 | setCards( 45 | produce(cards, draft => { 46 | const card = draft.splice(index, 1); 47 | draft.splice(index + 1, 0, ...card); 48 | }), 49 | ); 50 | }; 51 | 52 | /** 53 | * 配置Card 54 | * @param values 55 | */ 56 | const configCard = (values: Store) => { 57 | setCards( 58 | produce(cards, draft => { 59 | draft[cardIndex].title = values.title; 60 | }), 61 | ); 62 | }; 63 | 64 | /** 往Card中添加表单项 */ 65 | const addFormItemsToCard = (checkedComponents: FormItemType[]) => { 66 | const newFormItems = checkedComponents.map(type => ({ 67 | type, 68 | name: faker.name.lastName(), 69 | label: faker.name.title(), 70 | })); 71 | setCards( 72 | produce(cards, draft => { 73 | draft[cardIndex].formItems.push(...newFormItems); 74 | }), 75 | ); 76 | }; 77 | 78 | /** 79 | * 删除Card 80 | * @param index 81 | */ 82 | const deleteCard = (index: number) => () => { 83 | setCards( 84 | produce(cards, draft => { 85 | draft.splice(index, 1); 86 | }), 87 | ); 88 | }; 89 | 90 | /** 91 | * 复制card 92 | * @param index 93 | */ 94 | const copyCard = (index: number) => () => { 95 | setCards( 96 | produce(cards, draft => { 97 | draft.splice(index + 1, 0, draft[index]); 98 | }), 99 | ); 100 | }; 101 | 102 | /** 103 | * card里面的某个form配置项上移 104 | * @param cardIndex 105 | * @param itemIndex 106 | */ 107 | const moveItemUp = (itemIndex: number, cardIndex: number) => () => { 108 | setCards( 109 | produce(cards, draft => { 110 | const card = draft[cardIndex]; 111 | const { formItems = [] } = card; 112 | if (itemIndex === 0) return; 113 | const items = formItems.splice(itemIndex, 1); 114 | formItems.splice(itemIndex - 1, 0, ...items); 115 | }), 116 | ); 117 | }; 118 | 119 | /** 120 | * card里面某个form配置项下移 121 | * @param cardIndex 122 | * @param itemIndex 123 | */ 124 | const moveItemDown = (itemIndex: number, cardIndex: number) => () => { 125 | setCards( 126 | produce(cards, draft => { 127 | const card = draft[cardIndex]; 128 | const { formItems = [] } = card; 129 | if (itemIndex === formItems.length - 1) return; 130 | const items = formItems.splice(itemIndex, 1); 131 | formItems.splice(itemIndex + 1, 0, ...items); 132 | }), 133 | ); 134 | }; 135 | 136 | /** 137 | * 配置某个Card的某个表单项 138 | * @param itemIndex 139 | * @param formItem 140 | */ 141 | const configItem = (itemIndex: number, formItem: FormItemProps) => { 142 | setCards( 143 | produce(cards, draft => { 144 | const { formItems = [] } = draft[cardIndex]; 145 | formItems[itemIndex] = formItem; 146 | }), 147 | ); 148 | }; 149 | 150 | /** 151 | * 删除某个Card的某个表单项 152 | * @param cardIndex 153 | * @param itemIndex 154 | */ 155 | const deleteItem = (itemIndex: number, cardIndex: number) => () => { 156 | setCards( 157 | produce(cards, draft => { 158 | const card = draft[cardIndex]; 159 | const { formItems = [] } = card; 160 | formItems.splice(itemIndex, 1); 161 | }), 162 | ); 163 | }; 164 | 165 | /** 166 | * 复制某个card的某个表单项 167 | * @param cardIndex 168 | * @param itemIndex 169 | */ 170 | const copyItem = (itemIndex: number, cardIndex: number) => () => { 171 | setCards( 172 | produce(cards, draft => { 173 | const card = draft[cardIndex]; 174 | const { formItems = [] } = card; 175 | const formItem = { 176 | ...formItems[itemIndex], 177 | name: faker.name.lastName(), 178 | }; 179 | formItems.splice(itemIndex + 1, 0, formItem); 180 | }), 181 | ); 182 | }; 183 | 184 | return { 185 | cards, 186 | setCards, 187 | moveCardUp, 188 | moveCardDown, 189 | configCard, 190 | deleteCard, 191 | copyCard, 192 | moveItemUp, 193 | moveItemDown, 194 | configItem, 195 | deleteItem, 196 | copyItem, 197 | cardIndex, 198 | setCardIndex, 199 | currentCard, 200 | addFormItemsToCard, 201 | currentItem, 202 | setCurrentItem, 203 | itemIndex, 204 | setItemIndex, 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /ui/hooks/useConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 黄姗姗 5 | * @Date: 2020-05-28 17:35:45 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-28 17:44:33 8 | */ 9 | import { useState } from 'react'; 10 | 11 | export default function useConfig() { 12 | const [initialFetch, setInitialFetch] = useState(); 13 | const [submitFetch, setSubmitFetch] = useState(); 14 | 15 | return { 16 | initialFetch, 17 | setInitialFetch, 18 | submitFetch, 19 | setSubmitFetch, 20 | } 21 | } -------------------------------------------------------------------------------- /ui/hooks/useConfigVisible.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-11 15:23:37 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-29 10:41:43 8 | */ 9 | import { useState } from 'react'; 10 | 11 | export default function useConfigVisible() { 12 | const [formItemsDrawerVisible, setFormItemsDrawerVisible] = useState(false); 13 | const [pathModalVisible, setPathModalVisible] = useState(false); 14 | const [formConfigDrawerVisible, setFormConfigDrawerVisible] = useState(false); 15 | const [cardDrawerVisible, setCardDrawerVisible] = useState(false); 16 | const [formItemConfigDrawerVisible, setFormItemConfigDrawerVisible] = useState(false); 17 | const [tableConfigDrawerVisible, setTableConfigDrawerVisible] = useState(false); 18 | const [columnConfigDrawerVisible, setColumnConfigDrawerVisible] = useState(false); 19 | const [apiConfigDrawerVisible, setApiConfigDrawerVisible] = useState(false); 20 | 21 | return { 22 | formItemsDrawerVisible, 23 | pathModalVisible, 24 | formConfigDrawerVisible, 25 | cardDrawerVisible, 26 | formItemConfigDrawerVisible, 27 | tableConfigDrawerVisible, 28 | columnConfigDrawerVisible, 29 | apiConfigDrawerVisible, 30 | setFormItemsDrawerVisible, 31 | setPathModalVisible, 32 | setFormConfigDrawerVisible, 33 | setCardDrawerVisible, 34 | setFormItemConfigDrawerVisible, 35 | setTableConfigDrawerVisible, 36 | setColumnConfigDrawerVisible, 37 | setApiConfigDrawerVisible, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /ui/hooks/useFormItem.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 对表单元素操作的封装,包含上移、下移、配置 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-30 11:31:49 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-13 10:32:59 8 | */ 9 | import { useState } from 'react'; 10 | import { FormItemProps } from '../../interfaces/common'; 11 | 12 | export default function useFormItem() { 13 | const [formItems, setFormItems] = useState([]); 14 | const [index, setIndex] = useState(); // 当前选中的表单元素的index 15 | const [currentItem, setCurrentItem] = useState(); // 当前选中的表单元素 16 | 17 | /** 18 | * 上移表单项 19 | * @param index 20 | */ 21 | const moveUp = (index: number) => () => { 22 | if (index === 0) return; 23 | const _formItems = formItems.slice(); 24 | 25 | // 拿到这个index对应的item 26 | const formItem = _formItems.splice(index, 1); 27 | // 插入回去 28 | _formItems.splice(index - 1, 0, ...formItem); 29 | 30 | setFormItems(_formItems); 31 | }; 32 | /** 33 | * 下移表单项 34 | * @param index 35 | */ 36 | const moveDown = (index: number) => () => { 37 | if (index === formItems.length - 1) return; 38 | const _formItems = formItems.slice(); 39 | 40 | // 拿到这个index对应的item 41 | const formItem = _formItems.splice(index, 1); 42 | // 插入回去 43 | _formItems.splice(index + 1, 0, ...formItem); 44 | 45 | setFormItems(_formItems); 46 | }; 47 | /** 48 | * 配置表单项 49 | * @param formItem 50 | * @param index 51 | */ 52 | const configItem = (formItem: FormItemProps, index: number) => { 53 | setIndex(index); 54 | setCurrentItem(formItem); 55 | }; 56 | 57 | /** 表单元素配置项配置完成 */ 58 | const handleConfirm = (index: number, formItem: FormItemProps) => { 59 | const _formItems = formItems.slice(); 60 | const item = _formItems.splice(index, 1); 61 | const mergedItem = { 62 | ...item[0], 63 | ...formItem, 64 | }; 65 | _formItems.splice(index, 0, mergedItem); 66 | setFormItems(_formItems); 67 | }; 68 | 69 | /** 70 | * 删除配置项 71 | * @param index 72 | */ 73 | const deleteItem = (index: number) => () => { 74 | const _formItems = formItems.slice(); 75 | _formItems.splice(index, 1); 76 | setFormItems(_formItems); 77 | }; 78 | 79 | /** 80 | * 复制配置项 81 | * @param index 82 | */ 83 | const copyItem = (index: number) => () => { 84 | const _formItems = formItems.slice(); 85 | const formItem = _formItems[index]; 86 | _formItems.splice(index + 1, 0, formItem); 87 | 88 | setFormItems(_formItems); 89 | }; 90 | 91 | return { 92 | formItems, 93 | setFormItems, 94 | moveUp, 95 | moveDown, 96 | configItem, 97 | deleteItem, 98 | copyItem, 99 | index, 100 | currentItem, 101 | onConfirm: handleConfirm, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /ui/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 对表格的列的操作 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-30 11:31:49 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-14 11:44:43 8 | */ 9 | import { useState } from 'react'; 10 | import { ColumnType } from 'antd/lib/table'; 11 | import { useImmer } from 'use-immer'; 12 | 13 | export default function useTable() { 14 | const [columns, setColumns] = useImmer[]>([]); 15 | const [index, setIndex] = useState(0); // 当前选中的表单元素的index 16 | const [currentColumn, setCurrentColumn] = useState>(); // 当前选中的表单元素 17 | 18 | /** 19 | * 上移表单项 20 | * @param index 21 | */ 22 | const moveUp = (index: number) => () => { 23 | if (index === 0) return; 24 | 25 | setColumns(draft => { 26 | // 拿到这个index对应的item 27 | const column = draft.splice(index, 1); 28 | // 插入回去 29 | draft.splice(index - 1, 0, ...column); 30 | }); 31 | }; 32 | /** 33 | * 下移表单项 34 | * @param index 35 | */ 36 | const moveDown = (index: number) => () => { 37 | if (index === columns.length - 1) return; 38 | 39 | setColumns(draft => { 40 | // 拿到这个index对应的item 41 | const column = draft.splice(index, 1); 42 | // 插入回去 43 | draft.splice(index + 1, 0, ...column); 44 | }); 45 | }; 46 | /** 47 | * 配置表格列 48 | * @param formItem 49 | * @param index 50 | */ 51 | const configColumn = (column: ColumnType, index: number) => { 52 | setIndex(index); 53 | setCurrentColumn(column); 54 | }; 55 | 56 | /** 配置完成 */ 57 | const handleConfirm = (columnProps: ColumnType) => { 58 | setColumns(draft => { 59 | const column = draft.find(item => item.dataIndex === columnProps.dataIndex); 60 | if (!currentColumn && !column) { 61 | draft.push(columnProps); 62 | } else { 63 | draft[index] = columnProps; 64 | } 65 | }); 66 | }; 67 | 68 | /** 69 | * 删除配置项 70 | * @param index 71 | */ 72 | const deleteColumn = (index: number) => () => { 73 | setColumns(draft => { 74 | draft.splice(index, 1); 75 | }); 76 | }; 77 | 78 | /** 79 | * 复制配置项 80 | * @param index 81 | */ 82 | const copyColumn = (index: number) => () => { 83 | setColumns(draft => { 84 | const column = draft[index]; 85 | draft.splice(index + 1, 0, column); 86 | }); 87 | }; 88 | 89 | return { 90 | columns, 91 | setColumns, 92 | moveUp, 93 | moveDown, 94 | configColumn, 95 | deleteColumn, 96 | copyColumn, 97 | index, 98 | setIndex, 99 | currentColumn, 100 | setCurrentColumn, 101 | onConfirm: handleConfirm, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { IUiApi } from '@umijs/ui-types'; 2 | import { DashboardFilled } from '@ant-design/icons'; 3 | import ManageConfigPanel from './pages/manage'; 4 | import ScreenConfigPanel from './pages/screen'; 5 | import AppConfigPanel from './pages/mobile'; 6 | 7 | export default (api: IUiApi) => { 8 | api.addPanel({ 9 | title: '中后台', 10 | path: '/manageConfig', 11 | icon: , 12 | component: () => , 13 | }); 14 | api.addPanel({ 15 | title: '大屏', 16 | path: '/screenConfig', 17 | icon: , 18 | component: () => , 19 | }); 20 | api.addPanel({ 21 | title: '移动端', 22 | path: '/appConfig', 23 | icon: , 24 | component: () => , 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /ui/pages/manage/Context.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 11:29:07 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-29 14:12:04 8 | */ 9 | import { createContext } from 'react'; 10 | import { IUiApi } from '@umijs/ui-types'; 11 | import { TemplateType } from '../../../interfaces/common'; 12 | import { CascaderOptionType } from 'antd/lib/cascader'; 13 | import { BaseClass } from '../../../interfaces/api'; 14 | 15 | const MainContext = createContext({} as { 16 | /** 主体 */ 17 | api: IUiApi; 18 | templateType?: TemplateType; 19 | addTemplate: (templateType: TemplateType) => void; 20 | databases: CascaderOptionType[]; 21 | baseClasses: BaseClass[]; 22 | impConfigJson: string; 23 | setImpConfigJson: (configJson: string) => void; 24 | constantConfig: string; 25 | }); 26 | 27 | export default MainContext; 28 | -------------------------------------------------------------------------------- /ui/pages/manage/components/ConstantConfigAction/index.module.less: -------------------------------------------------------------------------------- 1 | .bubble { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 60px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | .fixed { 10 | position: fixed; 11 | bottom: 40px; 12 | margin-left: 20px; 13 | } 14 | .space { 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /ui/pages/manage/components/ConstantConfigAction/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { Button, Modal } from 'antd'; 3 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 4 | import 'codemirror/mode/javascript/javascript'; 5 | import 'codemirror/lib/codemirror.css'; 6 | import 'codemirror/theme/material-palenight.css'; 7 | import classNames from 'classnames'; 8 | import styles from './index.module.less'; 9 | import Context from '../../Context'; 10 | 11 | export default ({ 12 | visible, 13 | setVisible, 14 | onSubmit, 15 | }: { 16 | visible: boolean; 17 | setVisible: (visible: boolean) => void; 18 | onSubmit: (code: string) => void; 19 | }) => { 20 | const {constantConfig} = useContext(Context); 21 | const [editorValue, setEditorValue] = useState(''); 22 | 23 | return ( 24 | <> 25 | 32 | onSubmit(editorValue)} 38 | onCancel={() => setVisible(false)} 39 | bodyStyle={{ maxHeight: 768, overflowY: 'auto' }} 40 | > 41 | { 50 | editor.setSize('100%', 600); 51 | }} 52 | onChange={(editor, data, value) => { 53 | setEditorValue(value); 54 | }} 55 | /> 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /ui/pages/manage/components/Dashboard/index.module.less: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /ui/pages/manage/components/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Context from '../../Context'; 3 | 4 | import styles from './index.module.less'; 5 | import ShortFormContent from '../content/ShortFormContent'; 6 | import LongFormContent from '../content/LongFormContent'; 7 | import ShortDetailContent from '../content/ShortDetailContent'; 8 | import LongDetailContent from '../content/LongDetailContent'; 9 | import TableContent from '../content/TableContent'; 10 | import ShortFormModalContent from '../content/ShortFormModalContent'; 11 | import LongFormModalContent from '../content/LongFormModalContent'; 12 | import ShortDetailModalContent from '../content/ShortDetailModalContent'; 13 | import LongDetailModalContent from '../content/LongDetailModalContent'; 14 | 15 | export default () => { 16 | const { templateType } = useContext(Context); 17 | 18 | /** 根据模板映射不同的显示内容 */ 19 | const createContentByType = () => { 20 | switch (templateType) { 21 | case 'short-form': 22 | return ; 23 | case 'long-form': 24 | return ; 25 | case 'one-column-form-modal': 26 | return ; 27 | case 'two-columns-form-modal': 28 | return ; 29 | case 'short-detail': 30 | return ; 31 | case 'long-detail': 32 | return ; 33 | case 'one-column-detail-modal': 34 | return ; 35 | case 'two-columns-detail-modal': 36 | return ; 37 | case 'table': 38 | return ; 39 | default: 40 | return null; 41 | } 42 | }; 43 | 44 | return
{createContentByType()}
; 45 | }; 46 | -------------------------------------------------------------------------------- /ui/pages/manage/components/ExportActions/index.module.less: -------------------------------------------------------------------------------- 1 | .bubble { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 60px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | .fixed { 10 | position: fixed; 11 | bottom: 40px; 12 | right: 150px; 13 | } 14 | .space { 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /ui/pages/manage/components/ExportActions/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Button, message } from 'antd'; 3 | import classNames from 'classnames'; 4 | import styles from './index.module.less'; 5 | import Context from '../../Context'; 6 | 7 | export default ({ onClick }: { onClick: () => void }) => { 8 | const { templateType } = useContext(Context); 9 | 10 | return ( 11 | 24 | ) 25 | } -------------------------------------------------------------------------------- /ui/pages/manage/components/ImportAction/index.module.less: -------------------------------------------------------------------------------- 1 | .bubble { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 60px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | .fixed { 10 | position: fixed; 11 | bottom: 40px; 12 | right: 80px; 13 | } 14 | .space { 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /ui/pages/manage/components/ImportAction/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Button, Modal, Form, Input, message } from 'antd'; 3 | import classNames from 'classnames'; 4 | import styles from './index.module.less'; 5 | import { Store } from 'antd/lib/form/interface'; 6 | import Context from '../../Context'; 7 | 8 | const formLayout = { 9 | labelCol: { span: 4 }, 10 | wrapperCol: { span: 20 }, 11 | }; 12 | 13 | export default ({ 14 | modalVisible, 15 | setModalVisible, 16 | onSubmit, 17 | }: { 18 | modalVisible: boolean; 19 | setModalVisible: (visible: boolean) => void; 20 | onSubmit: (values: Store) => void; 21 | }) => { 22 | const [form] = Form.useForm(); 23 | 24 | const { templateType } = useContext(Context); 25 | 26 | const handleFinish = (values: Store) => { 27 | form.resetFields(); 28 | onSubmit(values); 29 | } 30 | return ( 31 | <> 32 | 45 | form.submit()} 51 | onCancel={() => setModalVisible(false)} 52 | bodyStyle={{ maxHeight: 650, overflowY: 'auto' }} 53 | > 54 |
55 | 61 | 62 | 63 |
64 |
65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /ui/pages/manage/components/PathMenuAction/index.module.less: -------------------------------------------------------------------------------- 1 | .bubble { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 60px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | .fixed { 10 | position: fixed; 11 | bottom: 40px; 12 | right: 10px; 13 | } 14 | .space { 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /ui/pages/manage/components/TemplateList/index.module.less: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | height: 152px; 4 | overflow-x: auto; 5 | } 6 | .col { 7 | height: 100px; 8 | margin-right: 15px; 9 | position: relative; 10 | 11 | .hover { 12 | width: 100%; 13 | height: 100%; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | display: none; 18 | } 19 | &:hover { 20 | .hover { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | background-color: rgba(0, 0, 0, 0.65); 25 | } 26 | } 27 | 28 | img { 29 | height: 100%; 30 | display: block; 31 | } 32 | .name { 33 | text-align: center; 34 | } 35 | } 36 | .selected { 37 | border: 2px solid #0189fb; 38 | padding: 2px; 39 | } 40 | -------------------------------------------------------------------------------- /ui/pages/manage/components/TemplateList/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 中后台管理系统的模板以及缩略图 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 11:32:45 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-04-29 22:00:14 8 | */ 9 | import React, { useState, useContext } from 'react'; 10 | import { Button, Tooltip, Modal } from 'antd'; 11 | import { EyeOutlined, PlusOutlined } from '@ant-design/icons'; 12 | import classnames from 'classnames'; 13 | import Context from '../../Context'; 14 | import templateList from './template.json'; 15 | import styles from './index.module.less'; 16 | import { Template } from '../../../../../interfaces/common'; 17 | 18 | export default () => { 19 | const [visible, setVisible] = useState(false); 20 | const [image, setImage] = useState(); 21 | const [title, setTitle] = useState(); 22 | const { addTemplate, templateType } = useContext(Context); 23 | 24 | const previewImage = (image: string, title: string) => { 25 | setImage(image); 26 | setTitle(title); 27 | setVisible(true); 28 | }; 29 | 30 | return ( 31 |
32 | {(templateList as Template[]).map(template => ( 33 |
40 |
41 | 42 | 45 | 46 |
47 | 48 | 51 | 52 |
53 |
54 | {template.name} 55 |
{template.name}
56 |
57 |
58 | ))} 59 | setVisible(false)} 63 | width="80vw" 64 | centered 65 | > 66 | {image && } 67 | 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/LongDetailContent/index.module.less: -------------------------------------------------------------------------------- 1 | .formItemConfig { 2 | display: flex; 3 | align-items: flex-start; 4 | flex-direction: column; 5 | 6 | :global { 7 | .ant-form-item { 8 | flex: 1; 9 | width: 100%; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/LongDetailModalContent/index.module.less: -------------------------------------------------------------------------------- 1 | .formItemConfig { 2 | display: flex; 3 | align-items: flex-start; 4 | 5 | :global { 6 | .ant-form-item { 7 | flex: 1; 8 | width: 100%; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/LongDetailModalContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { Form, Button, Card, message, Input, Row, Col } from 'antd'; 3 | import Title from '../../../../../components/Title'; 4 | import { AjaxResponse } from '../../../../../../interfaces/common'; 5 | import FormItemConfigDrawer from '../../../../../components/FormItemConfigDrawer'; 6 | import Context from '../../../Context'; 7 | import PathMenuAction from '../../PathMenuAction'; 8 | import { Store } from 'antd/lib/form/interface'; 9 | import ShortFormConfigDrawer from '../../drawers/ShortFormConfigDrawer'; 10 | import useFormItem from '../../../../../hooks/useFormItem'; 11 | import produce from 'immer'; 12 | import faker from 'faker'; 13 | import styles from './index.module.less'; 14 | import useConfigVisible from '../../../../../hooks/useConfigVisible'; 15 | import ConfigActions from '../../../../../components/ConfigActions'; 16 | import { transformFormItemLines } from '../../../../../utils'; 17 | import ApiConfigDrawer from '../../drawers/ApiConfigDrawer'; 18 | import useConfig from '../../../../../hooks/useConfig'; 19 | import copy from 'copy-to-clipboard'; 20 | import ExportActions from '../../ExportActions'; 21 | 22 | const formItemLayout = { 23 | labelCol: { 24 | xs: { span: 24 }, 25 | sm: { span: 7 }, 26 | md: { span: 10 }, 27 | }, 28 | wrapperCol: { 29 | xs: { span: 24 }, 30 | sm: { span: 12 }, 31 | md: { span: 14 }, 32 | }, 33 | }; 34 | 35 | export default () => { 36 | const { api, impConfigJson } = useContext(Context); 37 | const [formConfig, setFormConfig] = useState({ 38 | title: '两列详情', 39 | }); 40 | 41 | const { 42 | initialFetch, 43 | setInitialFetch, 44 | submitFetch, 45 | setSubmitFetch, 46 | } = useConfig(); 47 | 48 | const { 49 | pathModalVisible, 50 | setPathModalVisible, 51 | formConfigDrawerVisible, 52 | setFormConfigDrawerVisible, 53 | formItemConfigDrawerVisible, 54 | setFormItemConfigDrawerVisible, 55 | apiConfigDrawerVisible, 56 | setApiConfigDrawerVisible, 57 | } = useConfigVisible(); 58 | 59 | const { 60 | formItems, 61 | setFormItems, 62 | moveUp, 63 | moveDown, 64 | configItem, 65 | deleteItem, 66 | copyItem, 67 | currentItem, 68 | index, 69 | onConfirm, 70 | } = useFormItem(); 71 | 72 | /** 73 | * 添加详情展示项 74 | */ 75 | const addDetailItem = () => { 76 | setFormItems( 77 | produce(formItems, draft => { 78 | draft.push({ 79 | label: faker.name.title(), 80 | name: faker.name.lastName(), 81 | type: 'input', 82 | }); 83 | }), 84 | ); 85 | }; 86 | 87 | const handleApiSubmit = (initialFetch?: string[], submitFetch?: string[]) => { 88 | setInitialFetch(initialFetch); 89 | setSubmitFetch(submitFetch); 90 | }; 91 | 92 | /** 93 | * 把配置的表单信息和添加的表单项配置传到服务端 94 | */ 95 | const remoteCall = async ({ path, dirName }: { path?: string; dirName?: string }) => { 96 | // 对formItems进行遍历,如果其中有任一项没有配置label/name,则不允许提交 97 | if (formItems.length === 0) { 98 | message.error('您还没有添加详情展示项,不能提交!'); 99 | return; 100 | } 101 | const key = 'message'; 102 | try { 103 | message.loading({ content: '正在生成文件,请稍候...', key }); 104 | const result = await api.callRemote({ 105 | type: 'org.umi-plugin-page-creator.longDetailModal', 106 | payload: { 107 | formConfig, 108 | formItems, 109 | path, 110 | dirName, 111 | initialFetch, 112 | submitFetch, 113 | }, 114 | }); 115 | message.success({ content: (result as AjaxResponse).message , key }); 116 | setPathModalVisible(false); 117 | } catch (error) { 118 | message.error({ content: error.message, key }); 119 | } 120 | }; 121 | 122 | /** 把导入的配置信息进行解析 */ 123 | useEffect(() => { 124 | if (impConfigJson) { 125 | const { formConfig = { title: '两列详情', }, formItems = [], initialFetch = [], submitFetch = [] } = JSON.parse(impConfigJson); 126 | setFormConfig(formConfig); 127 | setFormItems(formItems); 128 | setInitialFetch(initialFetch); 129 | setSubmitFetch(submitFetch); 130 | } 131 | }, [impConfigJson]); 132 | 133 | /** 导出 */ 134 | const handleExport = () => { 135 | copy(JSON.stringify({ 136 | formConfig, 137 | formItems, 138 | initialFetch, 139 | submitFetch 140 | }, null, 2)); 141 | message.success('配置已复制到剪贴板'); 142 | }; 143 | 144 | const cols = 2; 145 | // 把formItems分成2列 146 | const formItemLines = transformFormItemLines(formItems, cols); 147 | 148 | return ( 149 | <> 150 | } 152 | extra={ 153 | 156 | } 157 | > 158 |
159 | {formItemLines.map((line, index) => ( 160 | 161 | {line.map((formItem, itemIndex) => ( 162 | 163 |
164 | { 169 | configItem(formItem, index * cols + itemIndex); 170 | setFormItemConfigDrawerVisible(true); 171 | }} 172 | deleteItem={deleteItem(index * cols + itemIndex)} 173 | copyItem={copyItem(index * cols + itemIndex)} 174 | /> 175 | 176 | 177 | 178 |
179 | 180 | ))} 181 |
182 | ))} 183 | 186 | 189 |
190 |
191 | 192 | {/**页面接口配置 */} 193 | 200 | 201 | {/**表单配置 */} 202 | 208 | 209 | {/**配置单个表单项 */} 210 | {currentItem && ( 211 | 220 | )} 221 | 222 | {/**提交时候弹出的输入文件路径 */} 223 | 230 | 231 | 232 | 233 | ); 234 | }; 235 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/ShortDetailContent/index.module.less: -------------------------------------------------------------------------------- 1 | .formItemConfig { 2 | display: flex; 3 | align-items: flex-start; 4 | 5 | :global { 6 | .ant-form-item { 7 | flex: 1; 8 | width: 100%; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/ShortDetailContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { Form, Button, Card, message, Input } from 'antd'; 3 | import Title from '../../../../../components/Title'; 4 | import { AjaxResponse } from '../../../../../../interfaces/common'; 5 | import FormItemConfigDrawer from '../../../../../components/FormItemConfigDrawer'; 6 | import Context from '../../../Context'; 7 | import PathMenuAction from '../../PathMenuAction'; 8 | import { Store } from 'antd/lib/form/interface'; 9 | import ShortFormConfigDrawer from '../../drawers/ShortFormConfigDrawer'; 10 | import useFormItem from '../../../../../hooks/useFormItem'; 11 | import produce from 'immer'; 12 | import faker from 'faker'; 13 | import styles from './index.module.less'; 14 | import useConfigVisible from '../../../../../hooks/useConfigVisible'; 15 | import ConfigActions from '../../../../../components/ConfigActions'; 16 | import ApiConfigDrawer from '../../drawers/ApiConfigDrawer'; 17 | import useConfig from '../../../../../hooks/useConfig'; 18 | import copy from 'copy-to-clipboard'; 19 | import ExportActions from '../../ExportActions'; 20 | 21 | const formItemLayout = { 22 | labelCol: { 23 | xs: { span: 24 }, 24 | sm: { span: 7 }, 25 | md: { span: 10 }, 26 | }, 27 | wrapperCol: { 28 | xs: { span: 24 }, 29 | sm: { span: 12 }, 30 | md: { span: 14 }, 31 | }, 32 | }; 33 | 34 | export default () => { 35 | const { api, impConfigJson } = useContext(Context); 36 | const [formConfig, setFormConfig] = useState({ 37 | title: '单列详情', 38 | }); 39 | 40 | const { 41 | initialFetch, 42 | setInitialFetch, 43 | submitFetch, 44 | setSubmitFetch, 45 | } = useConfig(); 46 | 47 | const { 48 | pathModalVisible, 49 | setPathModalVisible, 50 | formConfigDrawerVisible, 51 | setFormConfigDrawerVisible, 52 | formItemConfigDrawerVisible, 53 | setFormItemConfigDrawerVisible, 54 | apiConfigDrawerVisible, 55 | setApiConfigDrawerVisible, 56 | } = useConfigVisible(); 57 | 58 | const { 59 | formItems, 60 | setFormItems, 61 | moveUp, 62 | moveDown, 63 | configItem, 64 | deleteItem, 65 | copyItem, 66 | currentItem, 67 | index, 68 | onConfirm, 69 | } = useFormItem(); 70 | 71 | /** 72 | * 添加详情展示项 73 | */ 74 | const addDetailItem = () => { 75 | setFormItems( 76 | produce(formItems, draft => { 77 | draft.push({ 78 | label: faker.name.title(), 79 | name: faker.name.lastName(), 80 | type: 'input', 81 | }); 82 | }), 83 | ); 84 | }; 85 | 86 | const handleApiSubmit = (initialFetch?: string[], submitFetch?: string[]) => { 87 | setInitialFetch(initialFetch); 88 | setSubmitFetch(submitFetch); 89 | }; 90 | 91 | /** 92 | * 把配置的表单信息和添加的表单项配置传到服务端 93 | */ 94 | const remoteCall = async ({ path, menu }: { path?: string; menu?: string }) => { 95 | // 对formItems进行遍历,如果其中有任一项没有配置label/name,则不允许提交 96 | if (formItems.length === 0) { 97 | message.error('您还没有添加详情展示项,不能提交!'); 98 | return; 99 | } 100 | const key = 'message'; 101 | try { 102 | message.loading({ content: '正在生成文件,请稍候...', key }); 103 | const result = await api.callRemote({ 104 | type: 'org.umi-plugin-page-creator.shortDetail', 105 | payload: { 106 | formConfig, 107 | formItems, 108 | path, 109 | menu, 110 | initialFetch, 111 | submitFetch, 112 | }, 113 | }); 114 | message.success({ content: (result as AjaxResponse).message , key }); 115 | setPathModalVisible(false); 116 | } catch (error) { 117 | message.error({ content: error.message, key }); 118 | } 119 | }; 120 | 121 | /** 把导入的配置信息进行解析 */ 122 | useEffect(() => { 123 | if (impConfigJson) { 124 | const { formConfig = { title: '单列详情', }, formItems = [], initialFetch = [], submitFetch = [] } = JSON.parse(impConfigJson); 125 | setFormConfig(formConfig); 126 | setFormItems(formItems); 127 | setInitialFetch(initialFetch); 128 | setSubmitFetch(submitFetch); 129 | } 130 | }, [impConfigJson]); 131 | 132 | /** 导出 */ 133 | const handleExport = () => { 134 | copy(JSON.stringify({ 135 | formConfig, 136 | formItems, 137 | initialFetch, 138 | submitFetch 139 | }, null, 2)); 140 | message.success('配置已复制到剪贴板'); 141 | }; 142 | 143 | return ( 144 | <> 145 | } 147 | extra={ 148 | 151 | } 152 | > 153 |
154 | {formItems.map((formItem, index) => ( 155 |
156 | { 160 | configItem(formItem, index); 161 | setFormItemConfigDrawerVisible(true); 162 | }} 163 | deleteItem={deleteItem(index)} 164 | copyItem={copyItem(index)} 165 | /> 166 | 167 | 168 | 169 |
170 | ))} 171 | 174 | 177 |
178 |
179 | 180 | {/**页面接口配置 */} 181 | 188 | 189 | {/**表单配置 */} 190 | 196 | 197 | {/**配置单个表单项 */} 198 | {currentItem && ( 199 | 208 | )} 209 | 210 | {/**提交时候弹出的输入文件路径 */} 211 | 218 | 219 | 220 | 221 | ); 222 | }; 223 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/ShortDetailModalContent/index.module.less: -------------------------------------------------------------------------------- 1 | .formItemConfig { 2 | display: flex; 3 | align-items: flex-start; 4 | 5 | :global { 6 | .ant-form-item { 7 | flex: 1; 8 | width: 100%; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/ShortDetailModalContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { Form, Button, Card, message, Input } from 'antd'; 3 | import Title from '../../../../../components/Title'; 4 | import { AjaxResponse } from '../../../../../../interfaces/common'; 5 | import FormItemConfigDrawer from '../../../../../components/FormItemConfigDrawer'; 6 | import Context from '../../../Context'; 7 | import PathMenuAction from '../../PathMenuAction'; 8 | import { Store } from 'antd/lib/form/interface'; 9 | import ShortFormConfigDrawer from '../../drawers/ShortFormConfigDrawer'; 10 | import useFormItem from '../../../../../hooks/useFormItem'; 11 | import produce from 'immer'; 12 | import faker from 'faker'; 13 | import styles from './index.module.less'; 14 | import useConfigVisible from '../../../../../hooks/useConfigVisible'; 15 | import ConfigActions from '../../../../../components/ConfigActions'; 16 | import ApiConfigDrawer from '../../drawers/ApiConfigDrawer'; 17 | import useConfig from '../../../../../hooks/useConfig'; 18 | import copy from 'copy-to-clipboard'; 19 | import ExportActions from '../../ExportActions'; 20 | 21 | const formItemLayout = { 22 | labelCol: { 23 | xs: { span: 24 }, 24 | sm: { span: 7 }, 25 | md: { span: 10 }, 26 | }, 27 | wrapperCol: { 28 | xs: { span: 24 }, 29 | sm: { span: 12 }, 30 | md: { span: 14 }, 31 | }, 32 | }; 33 | 34 | export default () => { 35 | const { api, impConfigJson } = useContext(Context); 36 | const [formConfig, setFormConfig] = useState({ 37 | title: '单列详情', 38 | }); 39 | 40 | const { 41 | initialFetch, 42 | setInitialFetch, 43 | submitFetch, 44 | setSubmitFetch, 45 | } = useConfig(); 46 | 47 | const { 48 | pathModalVisible, 49 | setPathModalVisible, 50 | formConfigDrawerVisible, 51 | setFormConfigDrawerVisible, 52 | formItemConfigDrawerVisible, 53 | setFormItemConfigDrawerVisible, 54 | apiConfigDrawerVisible, 55 | setApiConfigDrawerVisible, 56 | } = useConfigVisible(); 57 | 58 | const { 59 | formItems, 60 | setFormItems, 61 | moveUp, 62 | moveDown, 63 | configItem, 64 | deleteItem, 65 | copyItem, 66 | currentItem, 67 | index, 68 | onConfirm, 69 | } = useFormItem(); 70 | 71 | /** 72 | * 添加详情展示项 73 | */ 74 | const addDetailItem = () => { 75 | setFormItems( 76 | produce(formItems, draft => { 77 | draft.push({ 78 | label: faker.name.title(), 79 | name: faker.name.lastName(), 80 | type: 'input', 81 | }); 82 | }), 83 | ); 84 | }; 85 | 86 | const handleApiSubmit = (initialFetch?: string[], submitFetch?: string[]) => { 87 | setInitialFetch(initialFetch); 88 | setSubmitFetch(submitFetch); 89 | }; 90 | 91 | /** 92 | * 把配置的表单信息和添加的表单项配置传到服务端 93 | */ 94 | const remoteCall = async ({ path, dirName }: { path?: string; dirName?: string }) => { 95 | // 对formItems进行遍历,如果其中有任一项没有配置label/name,则不允许提交 96 | if (formItems.length === 0) { 97 | message.error('您还没有添加详情展示项,不能提交!'); 98 | return; 99 | } 100 | const key = 'message'; 101 | try { 102 | message.loading({ content: '正在生成文件,请稍候...', key }); 103 | const result = await api.callRemote({ 104 | type: 'org.umi-plugin-page-creator.shortDetailModal', 105 | payload: { 106 | formConfig, 107 | formItems, 108 | path, 109 | dirName, 110 | initialFetch, 111 | submitFetch, 112 | }, 113 | }); 114 | message.success({ content: (result as AjaxResponse).message , key }); 115 | setPathModalVisible(false); 116 | } catch (error) { 117 | message.error({ content: error.message, key }); 118 | } 119 | }; 120 | 121 | /** 把导入的配置信息进行解析 */ 122 | useEffect(() => { 123 | if (impConfigJson) { 124 | const { formConfig = { title: '单列详情', }, formItems = [], initialFetch = [], submitFetch = [] } = JSON.parse(impConfigJson); 125 | setFormConfig(formConfig); 126 | setFormItems(formItems); 127 | setInitialFetch(initialFetch); 128 | setSubmitFetch(submitFetch); 129 | } 130 | }, [impConfigJson]); 131 | 132 | /** 导出 */ 133 | const handleExport = () => { 134 | copy(JSON.stringify({ 135 | formConfig, 136 | formItems, 137 | initialFetch, 138 | submitFetch 139 | }, null, 2)); 140 | message.success('配置已复制到剪贴板'); 141 | }; 142 | 143 | return ( 144 | <> 145 | } 147 | extra={ 148 | 151 | } 152 | > 153 |
154 | {formItems.map((formItem, index) => ( 155 |
156 | { 160 | configItem(formItem, index); 161 | setFormItemConfigDrawerVisible(true); 162 | }} 163 | deleteItem={deleteItem(index)} 164 | copyItem={copyItem(index)} 165 | /> 166 | 167 | 168 | 169 |
170 | ))} 171 | 174 | 177 |
178 |
179 | 180 | {/**页面接口配置 */} 181 | 188 | 189 | {/**表单配置 */} 190 | 196 | 197 | {/**配置单个表单项 */} 198 | {currentItem && ( 199 | 208 | )} 209 | 210 | {/**提交时候弹出的输入文件路径 */} 211 | 218 | 219 | 220 | 221 | ); 222 | }; 223 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/TableContent/TitleWithActions/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfigActions from '../../../../../../components/ConfigActions'; 3 | import { ColumnTitle } from 'antd/lib/table/interface'; 4 | import { Tooltip } from 'antd'; 5 | 6 | export default function({ 7 | title, 8 | moveUp, 9 | moveDown, 10 | configItem, 11 | deleteItem, 12 | copyItem, 13 | }: { 14 | title?: ColumnTitle; 15 | moveUp?: () => void; 16 | moveDown?: () => void; 17 | configItem?: () => void; 18 | deleteItem?: () => void; 19 | copyItem?: () => void; 20 | }) { 21 | return ( 22 |
23 | 34 | } 35 | > 36 |
{title}
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /ui/pages/manage/components/content/TableContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { Button, Card, message, Table } from 'antd'; 3 | import Title from '../../../../../components/Title'; 4 | import { AjaxResponse } from '../../../../../../interfaces/common'; 5 | import Context from '../../../Context'; 6 | import PathMenuAction from '../../PathMenuAction'; 7 | import { Store } from 'antd/lib/form/interface'; 8 | import TableConfigDrawer from '../../drawers/TableConfigDrawer'; 9 | import TableColumnConfigDrawer from '../../drawers/TableColumnConfigDrawer'; 10 | import TitleWithActions from './TitleWithActions'; 11 | import useConfigVisible from '../../../../../hooks/useConfigVisible'; 12 | import useTable from '../../../../../hooks/useTable'; 13 | import { filterEmpty } from '../../../../../utils'; 14 | import ApiConfigDrawer from '../../drawers/ApiConfigDrawer'; 15 | import useConfig from '../../../../../hooks/useConfig'; 16 | import { ColumnType } from 'antd/lib/table/interface'; 17 | import copy from 'copy-to-clipboard'; 18 | import ExportActions from '../../ExportActions'; 19 | 20 | export default () => { 21 | const { api, impConfigJson } = useContext(Context); 22 | const [tableConfig, setTableConfig] = useState({ 23 | headerTitle: '表格配置', 24 | rowKey: 'id', 25 | bordered: false, 26 | }); 27 | 28 | const { initialFetch, setInitialFetch, submitFetch, setSubmitFetch } = useConfig(); 29 | 30 | const { 31 | pathModalVisible, 32 | setPathModalVisible, 33 | tableConfigDrawerVisible, 34 | setTableConfigDrawerVisible, 35 | columnConfigDrawerVisible, 36 | setColumnConfigDrawerVisible, 37 | apiConfigDrawerVisible, 38 | setApiConfigDrawerVisible, 39 | } = useConfigVisible(); 40 | 41 | const { 42 | columns, 43 | index, 44 | moveUp, 45 | moveDown, 46 | copyColumn, 47 | configColumn, 48 | deleteColumn, 49 | currentColumn, 50 | setIndex, 51 | setCurrentColumn, 52 | onConfirm, 53 | } = useTable(); 54 | 55 | const handleApiSubmit = (initialFetch?: string[], submitFetch?: string[]) => { 56 | setInitialFetch(initialFetch); 57 | setSubmitFetch(submitFetch); 58 | }; 59 | 60 | /** 61 | * 把配置的表单信息和添加的表单项配置传到服务端 62 | */ 63 | const remoteCall = async ({ path, menu }: { path?: string; menu?: string }) => { 64 | const key = 'message'; 65 | try { 66 | if (columns.length === 0) { 67 | message.error('你还没有配置表格列'); 68 | return; 69 | } 70 | message.loading({ content: '正在生成文件,请稍候...', key }); 71 | const result = await api.callRemote({ 72 | type: 'org.umi-plugin-page-creator.table', 73 | payload: { 74 | tableConfig, 75 | columns, 76 | path, 77 | menu, 78 | initialFetch, 79 | submitFetch, 80 | }, 81 | }); 82 | message.success({ content: (result as AjaxResponse).message, key }); 83 | setPathModalVisible(false); 84 | } catch (error) { 85 | message.error({ content: error.message, key }); 86 | } 87 | }; 88 | 89 | /** 把导入的配置信息进行解析 */ 90 | useEffect(() => { 91 | if (impConfigJson) { 92 | const { 93 | tableConfig = { 94 | headerTitle: '表格配置', 95 | bordered: true, 96 | }, 97 | columns = [], 98 | initialFetch = [], 99 | submitFetch = [], 100 | } = JSON.parse(impConfigJson); 101 | setTableConfig(tableConfig); 102 | (columns as ColumnType[]).map(item => onConfirm(item)); 103 | setInitialFetch(initialFetch); 104 | setSubmitFetch(submitFetch); 105 | } 106 | }, [impConfigJson]); 107 | 108 | /** 导出 */ 109 | const handleExport = () => { 110 | copy( 111 | JSON.stringify( 112 | { 113 | tableConfig, 114 | columns, 115 | initialFetch, 116 | submitFetch, 117 | }, 118 | null, 119 | 2, 120 | ), 121 | ); 122 | message.success('配置已复制到剪贴板'); 123 | }; 124 | 125 | return ( 126 | <> 127 | } 129 | extra={ 130 | 133 | } 134 | > 135 | ({ 144 | ...column, 145 | title: ( 146 | { 153 | configColumn(column, index); 154 | setColumnConfigDrawerVisible(true); 155 | }} 156 | /> 157 | ), 158 | }))} 159 | dataSource={[]} 160 | /> 161 | 168 | 178 | 179 | 180 | {/**页面接口配置 */} 181 | 188 | 189 | { 194 | setTableConfig(values); 195 | setTableConfigDrawerVisible(false); 196 | }} 197 | /> 198 | 199 | { 203 | const findIndex = columns.findIndex(item => item.dataIndex === values.dataIndex); 204 | // 如果index不存在,或者findIndex和index相同,表示新增或者修改没有改到dataIndex 205 | if ((!index && findIndex > -1) || (index && index === findIndex)) { 206 | message.error('这个dataIndex已存在,请修改后重新提交'); 207 | return; 208 | } 209 | onConfirm(filterEmpty(values)); 210 | setColumnConfigDrawerVisible(false); 211 | }} 212 | current={currentColumn} 213 | initialFetch={initialFetch} 214 | /> 215 | 216 | {/**提交时候弹出的输入文件路径 */} 217 | 223 | 224 | 225 | 226 | ); 227 | }; 228 | -------------------------------------------------------------------------------- /ui/pages/manage/components/drawers/ApiConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 接口API的配置内容 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 17:56:31 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-28 18:24:49 8 | */ 9 | import React, { useContext, useEffect } from 'react'; 10 | import { Form, Button, Drawer, Cascader } from 'antd'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | import Context from '../../../Context'; 13 | 14 | export default ({ 15 | visible, 16 | setVisible, 17 | onSubmit, 18 | submitFetch, 19 | initialFetch, 20 | }: { 21 | visible: boolean; 22 | setVisible: (visible: boolean) => void; 23 | onSubmit: (initialFetch?: string[], submitFetch?: string[]) => void; 24 | submitFetch?: string[]; 25 | initialFetch?: string[]; 26 | }) => { 27 | const { databases = [] } = useContext(Context); 28 | const [form] = Form.useForm(); 29 | 30 | useEffect(() => { 31 | form.setFieldsValue({ 32 | initialFetch 33 | }) 34 | }, [initialFetch]); 35 | 36 | useEffect(() => { 37 | form.setFieldsValue({ 38 | submitFetch 39 | }) 40 | }, [submitFetch]); 41 | 42 | const handleFinish = (values: Store) => { 43 | setVisible(false); 44 | const { initialFetch, submitFetch } = values; 45 | onSubmit(initialFetch, submitFetch); 46 | }; 47 | 48 | return ( 49 | setVisible(false)}> 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /ui/pages/manage/components/drawers/CardConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 短表单的配置内容 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 17:56:31 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-11 15:21:35 8 | */ 9 | import React from 'react'; 10 | import { Form, Button, Input, Drawer } from 'antd'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | 13 | export default ({ 14 | visible, 15 | setVisible, 16 | onFinish, 17 | }: { 18 | visible: boolean; 19 | setVisible: (visible: boolean) => void; 20 | onFinish: (values: Store) => void; 21 | }) => { 22 | const [form] = Form.useForm(); 23 | 24 | const handleFinish = (values: Store) => { 25 | onFinish(values); 26 | setVisible(false); 27 | }; 28 | 29 | return ( 30 | setVisible(false)}> 31 |
32 | 38 | 39 | 40 | 41 | 44 | 45 | 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /ui/pages/manage/components/drawers/ShortFormConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 短表单的配置内容 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 17:56:31 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-28 18:21:40 8 | */ 9 | import React, { useContext, useEffect } from 'react'; 10 | import { Form, Button, Input, Drawer } from 'antd'; 11 | import { Store } from 'antd/lib/form/interface'; 12 | 13 | export default ({ 14 | visible, 15 | setVisible, 16 | onFinish, 17 | formConfig, 18 | }: { 19 | visible: boolean; 20 | setVisible: (visible: boolean) => void; 21 | onFinish: (values: Store) => void; 22 | formConfig: Store; 23 | }) => { 24 | const [form] = Form.useForm(); 25 | 26 | useEffect(() => { 27 | form.setFieldsValue(formConfig); 28 | }, [formConfig]); 29 | 30 | const handleFinish = (values: Store) => { 31 | setVisible(false); 32 | onFinish(values); 33 | }; 34 | 35 | return ( 36 | setVisible(false)}> 37 |
38 | 44 | 45 | 46 | 47 | 50 | 51 | 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /ui/pages/manage/components/drawers/TableColumnConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo, useEffect, useState } from 'react'; 2 | import { Drawer, Form, Input, Button, Radio, Select, InputNumber, Tooltip, message } from 'antd'; 3 | import { QuestionCircleOutlined } from '@ant-design/icons'; 4 | import { Store } from 'antd/lib/form/interface'; 5 | import { ColumnType } from 'antd/lib/table'; 6 | import Context from '../../../Context'; 7 | 8 | const { Option } = Select; 9 | 10 | export default function ({ 11 | setVisible, 12 | visible, 13 | onSubmit, 14 | current, 15 | initialFetch, 16 | }: { 17 | visible: boolean; 18 | setVisible: (visible: boolean) => void; 19 | onSubmit: (values: Store) => void; 20 | current?: ColumnType; 21 | initialFetch?: string[]; 22 | }) { 23 | const [form] = Form.useForm(); 24 | const { baseClasses = [] } = useContext(Context); 25 | const [responseName, setResponseName] = useState(); 26 | 27 | const initialValues = { 28 | title: '', 29 | dataIndex: '', 30 | align: 'left', 31 | ellipsis: false, 32 | copyable: false, 33 | valueType: 'text', 34 | hideInSearch: false, 35 | hideInTable: false, 36 | order: undefined, 37 | }; 38 | useEffect(() => { 39 | if (current) { 40 | form.setFieldsValue(current); 41 | } else { 42 | form.setFieldsValue(initialValues); 43 | } 44 | }, [current]); 45 | 46 | /** initialFetch中第三个值为value-paramsName-responseName,获取初始数据选用responseName作为DTO */ 47 | useEffect(() => { 48 | if (initialFetch && initialFetch.length === 3) { 49 | const responseName = initialFetch[2].split('-')[2]; 50 | setResponseName(responseName); 51 | } 52 | }, [initialFetch]); 53 | 54 | const properties = useMemo( 55 | () => baseClasses.find(item => item.name === responseName)?.properties || [], 56 | [baseClasses, responseName], 57 | ); 58 | 59 | const handleChange = (value: string) => { 60 | const matchClass = properties.find(item => item.value === value); 61 | form.setFieldsValue({ 62 | title: matchClass?.label, 63 | dataIndex: value, 64 | }); 65 | }; 66 | 67 | return ( 68 | { 73 | form.setFieldsValue(initialValues); 74 | setVisible(false); 75 | }} 76 | > 77 |
85 | {initialFetch && initialFetch.length > 0 && ( 86 | 87 | 95 | 96 | )} 97 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | 121 | 122 | 123 | 129 | 130 | 131 | 137 | 138 | 141 | 枚举值 142 | 154 | 155 | 156 | 157 | } 158 | name="valueEnum" 159 | > 160 | 161 | 162 | 163 | 188 | 189 | 190 | 196 | 197 | 198 | 204 | 205 | 206 | 207 | 208 | 211 | 212 |
213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /ui/pages/manage/components/drawers/TableConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Drawer, Form, Input, Button, Radio } from 'antd'; 3 | import { filterEmpty } from '../../../../../utils'; 4 | import { Store } from 'antd/lib/form/interface'; 5 | 6 | export default ({ 7 | setVisible, 8 | visible, 9 | onSubmit, 10 | tableConfig, 11 | }: { 12 | visible: boolean; 13 | setVisible: (visible: boolean) => void; 14 | onSubmit: (values: Store) => void; 15 | tableConfig: Store; 16 | }) => { 17 | return ( 18 | setVisible(false)} 23 | > 24 |
onSubmit(filterEmpty(values))} 30 | > 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /ui/pages/manage/index.module.less: -------------------------------------------------------------------------------- 1 | :global { 2 | .umi-ui-layouts-dashboard_content { 3 | .ant-layout { 4 | height: 100%; 5 | 6 | &-header { 7 | height: 170px; 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/pages/manage/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 11:06:58 6 | * @LastEditors: 黄姗姗 7 | * @LastEditTime: 2020-05-29 14:27:52 8 | */ 9 | import React, { useState, useEffect } from 'react'; // -> 暂时先解决报错,后期全部删掉 10 | import { Layout, message } from 'antd'; 11 | import { IUiApi } from '@umijs/ui-types'; 12 | import Context from './Context'; 13 | import './index.module.less'; 14 | import { CascaderOptionType } from 'antd/lib/cascader'; 15 | import { Store } from 'antd/lib/form/interface'; 16 | import { TemplateType } from '../../../interfaces/common'; 17 | import { BaseClass } from '../../../interfaces/api'; 18 | import TemplateList from './components/TemplateList'; 19 | import Dashboard from './components/Dashboard'; 20 | import ImportAction from './components/ImportAction'; 21 | import ConstantConfigAction from './components/ConstantConfigAction'; 22 | 23 | const { Header, Content } = Layout; 24 | 25 | export default ({ api }: { api: IUiApi }) => { 26 | const [databases, setDatabases] = useState([]); 27 | const [baseClasses, setBaseClasses] = useState([]); 28 | const [templateType, setTemplate] = useState(); 29 | const [importModalVisible, setImportModalVisible] = useState(false); 30 | const [impConfigJson, setImpConfigJson] = useState(''); // 导入的json 31 | const [constantModalVisible, setConstantModalVisible] = useState(false); 32 | const [constantConfig, setConstantConfig] = useState(''); 33 | 34 | /** 页面加载时调用后端接口,后端从services/api-lock.json读取数据,生成对应的接口以及类型 */ 35 | useEffect(() => { 36 | (async () => { 37 | const result = (await api.callRemote({ 38 | type: 'org.umi-plugin-page-creator.apiGenerator', 39 | payload: { 40 | fetchApiJson: true, 41 | }, 42 | })) as { databases: CascaderOptionType[]; success: boolean; baseClasses: BaseClass[] }; 43 | 44 | if (!result.success) { 45 | message.warning('你的项目没有集成pont'); 46 | } else { 47 | setDatabases(result.databases); 48 | setBaseClasses(result.baseClasses); 49 | } 50 | })(); 51 | }, []); 52 | 53 | /** 54 | * 页面初始化之后,通过服务端读取项目下的constant.ts文件,把文件内容返回回来 55 | */ 56 | useEffect(() => { 57 | (async () => { 58 | const result = (await api.callRemote({ 59 | type: 'org.umi-plugin-page-creator.constantLoad', 60 | })) as { success: boolean; data: string }; 61 | if (result.success) { 62 | setConstantConfig(result.data); 63 | } 64 | })(); 65 | }, []); 66 | 67 | const addTemplate = (templateType: TemplateType) => { 68 | setTemplate(templateType); 69 | setImpConfigJson(''); 70 | message.success('模板添加成功,你可以开始配置了'); 71 | }; 72 | 73 | /** 导入 */ 74 | const handleImportSubmit = (values: Store) => { 75 | setImportModalVisible(false); 76 | const { importConfig } = values; 77 | setImpConfigJson(importConfig); 78 | }; 79 | 80 | /** 81 | * 保存常量的配置,调用服务端接口写回数据 82 | * @param code 83 | */ 84 | const saveConstantConfig = async (code: string) => { 85 | const key = 'message'; 86 | message.loading({ content: '正在保存,请稍候...', key }); 87 | await api.callRemote({ 88 | type: 'org.umi-plugin-page-creator.constantSave', 89 | payload: { 90 | code, 91 | } 92 | }); 93 | message.success({ content: '常量配置保存成功', key }); 94 | }; 95 | 96 | return ( 97 | 109 | 110 |
111 | 112 |
113 | 114 | {/* 导入 */} 115 | 120 | 125 | 126 | 127 |
128 |
129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /ui/pages/mobile/Context.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 11:29:07 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-04-29 11:29:42 8 | */ 9 | import { createContext } from 'react'; 10 | import { IUiApi } from '@umijs/ui-types'; 11 | 12 | const UIContext = createContext({} as { api: IUiApi }); 13 | 14 | export default UIContext; 15 | -------------------------------------------------------------------------------- /ui/pages/mobile/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { IUiApi } from '@umijs/ui-types'; 3 | 4 | export default ({ api }: { api: IUiApi }) => { 5 | const { callRemote } = api; 6 | return ( 7 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /ui/pages/screen/ColConfigDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Drawer, Form, InputNumber, Divider, Button, Select, Row, Col, Input } from 'antd'; 3 | import { Store } from 'antd/lib/form/interface'; 4 | import * as props from './props'; 5 | import renderFormItem from '../../../components/FormItemConfig'; 6 | import { FormItemProps } from '../../../../interfaces/common'; 7 | import { renderPreviewImage } from '../helper'; 8 | import { ScreenColConfig } from '../../../../interfaces/screen'; 9 | 10 | const formLayout = { 11 | labelCol: { 12 | span: 8, 13 | }, 14 | wrapperCol: { 15 | span: 16, 16 | }, 17 | }; 18 | const initialValues = { 19 | xs: 1, 20 | sm: 1, 21 | md: 1, 22 | lg: 1, 23 | xl: 1, 24 | xxl: 1, 25 | type: 'custom', 26 | }; 27 | 28 | export default ({ 29 | visible, 30 | toggleVisible, 31 | onFinish, 32 | col, 33 | }: { 34 | visible: boolean; 35 | toggleVisible: () => void; 36 | onFinish: (values: Store) => void; 37 | col: ScreenColConfig; 38 | }) => { 39 | const [form] = Form.useForm(); 40 | const [type, setType] = useState('custom'); 41 | 42 | useEffect(() => { 43 | if (col) { 44 | const {config: { type, chartConfig, xs, sm, md, lg, xl, xxl }} = col; 45 | form.setFieldsValue({ 46 | type, 47 | ...chartConfig, 48 | xs: xs?.span, 49 | sm: sm?.span, 50 | md: md?.span, 51 | lg: lg?.span, 52 | xl: xl?.span, 53 | xxl: xxl?.span, 54 | }); 55 | setType(type!); 56 | } else { 57 | form.setFieldsValue(initialValues); 58 | setType('custom'); 59 | } 60 | }, [col]) 61 | 62 | const renderOtherProps = () => { 63 | const otherProps = props[`${type}Props`]; 64 | if (otherProps) { 65 | return otherProps.map((prop: FormItemProps) => renderFormItem({ formItem: prop })); 66 | } 67 | return null; 68 | }; 69 | 70 | const handleFinish = (values: Store) => { 71 | onFinish(values); 72 | toggleVisible(); 73 | }; 74 | 75 | return ( 76 | 77 | 78 |
79 | 80 | 宽度 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 图表 100 | 101 | 102 | 103 | 104 | 129 | 130 | {renderOtherProps()} 131 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | {renderPreviewImage(type)} 141 | 142 | 143 | 144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /ui/pages/screen/Context.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-04-29 11:29:07 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-06 16:25:51 8 | */ 9 | import { createContext } from 'react'; 10 | import { IUiApi } from '@umijs/ui-types'; 11 | 12 | const UIContext = createContext({} as { api: IUiApi }); 13 | 14 | export default UIContext; 15 | -------------------------------------------------------------------------------- /ui/pages/screen/Dashboard/index.module.less: -------------------------------------------------------------------------------- 1 | .floatBtn { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 60px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | position: fixed; 9 | bottom: 40px; 10 | right: 10px; 11 | } 12 | .submitBtn { 13 | right: 80px; 14 | } 15 | .layout { 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: space-between; 20 | } 21 | .col { 22 | height: 100%; 23 | min-height: 225px; 24 | border: 1px solid blue; 25 | position: relative; 26 | flex: 1; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | 31 | :global { 32 | .ant-btn { 33 | position: absolute; 34 | right: 0; 35 | top: -30px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/pages/screen/helper.tsx: -------------------------------------------------------------------------------- 1 | import preview from './preview'; 2 | 3 | export function renderPreviewImage(type: string) { 4 | const item = preview.find(item => item.type === type && item.image); 5 | if (item) { 6 | return ( 7 | item.type === type)!.image} 9 | alt={type as string} 10 | style={{ width: '100%' }} 11 | /> 12 | ); 13 | } 14 | return ( 15 |
对不起,暂无预览图
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /ui/pages/screen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IUiApi } from '@umijs/ui-types'; 3 | import Context from './Context'; 4 | import Dashboard from './Dashboard'; 5 | 6 | export default ({ api }: { api: IUiApi }) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /ui/pages/screen/structure.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: '', // <- 大屏的名称 3 | titleStyle: {}, // <- 大屏名称的样式 4 | gutter: 16, // <- 大屏每个组件之间的间距 5 | layout: [{ 6 | name: 'Left', // <- 要渲染的组件名称 7 | span: 7, // <- 左/中/右 布局占比 8 | rows: [{ 9 | name: 'Row1', // <- 要渲染的组件名称 10 | height: 1, // <- 每行高度占比 11 | cols: [{ 12 | name: 'Row1Col1', // <- 要渲染的组件名称 13 | span: 24, // <- 每列宽度占比 14 | type: 'bar', // <- 列中元素的类型(图表、自定义、其他) 15 | chartConfig: {}, 16 | }] 17 | }, { 18 | name: 'Row2', // <- 要渲染的组件名称 19 | height: 1, 20 | cols: [{ 21 | name: 'Row2Col1', // <- 要渲染的组件名称 22 | span: 24, 23 | type: 'bar', 24 | chartConfig: {}, 25 | }] 26 | }, { 27 | name: 'Row3', // <- 要渲染的组件名称 28 | height: 1, 29 | cols: [{ 30 | name: 'Row3Col1', // <- 要渲染的组件名称 31 | span: 24, 32 | type: 'custom', // 自定义时,用一个div占位 33 | chartConfig: {}, 34 | }] 35 | }] 36 | }], 37 | } 38 | -------------------------------------------------------------------------------- /ui/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @文件描述: 3 | * @公司: thundersdata 4 | * @作者: 陈杰 5 | * @Date: 2020-05-11 17:20:18 6 | * @LastEditors: 陈杰 7 | * @LastEditTime: 2020-05-14 11:17:27 8 | */ 9 | import { Store } from 'antd/lib/form/interface'; 10 | import { FormItemProps } from '../../interfaces/common'; 11 | import { ScreenConfig, ScreenConfigPayload, LayoutType } from '../../interfaces/screen'; 12 | 13 | /** 14 | * 将一个数组按照指定的列拆分成N个二维数组 15 | * @param formItems 16 | * @param cols 17 | */ 18 | export function transformFormItemLines(formItems: FormItemProps[], cols = 3) { 19 | let lineNum = 20 | formItems.length % cols === 0 21 | ? formItems.length / cols 22 | : Math.floor(formItems.length / cols + 1); 23 | let res = []; 24 | for (let i = 0; i < lineNum; i++) { 25 | let temp = formItems.slice(i * cols, i * cols + cols); 26 | res.push(temp); 27 | } 28 | return res; 29 | } 30 | 31 | /** 32 | * 过滤掉空数据 33 | * @param values 34 | */ 35 | export function filterEmpty(values: Store) { 36 | const filteredValues = {}; 37 | 38 | Object.entries(values).forEach(([key, value]) => { 39 | if (value !== '' && value !== undefined && value !== null) { 40 | filteredValues[key] = value; 41 | } 42 | }); 43 | return filteredValues; 44 | } 45 | 46 | /** 47 | * 将大屏配置参数转换成后端需要的数据结构 48 | * @param screenConfig 49 | */ 50 | type ScreenConfigPayloadPart = Omit; 51 | export function transformConfig(screenConfig: ScreenConfig): ScreenConfigPayloadPart { 52 | const payload: ScreenConfigPayloadPart = { 53 | layout: [], 54 | }; 55 | 56 | function generateConfig(type: LayoutType, config: ScreenConfig) { 57 | const layout = config[type]; 58 | const { rows, ...restProps } = layout; 59 | return { 60 | ...restProps, 61 | name: type.charAt(0).toUpperCase() + type.substr(1, type.length), 62 | rows: rows.map((row, rowIndex) => ({ 63 | name: `Row${rowIndex}`, 64 | height: row.height, 65 | cols: row.cols.map((col, colIndex) => ({ 66 | name: `Row${rowIndex}Col${colIndex}`, 67 | ...col, 68 | })), 69 | })), 70 | }; 71 | } 72 | 73 | const leftConfig = generateConfig('left', screenConfig); 74 | const centerConfig = generateConfig('center', screenConfig); 75 | const rightConfig = generateConfig('right', screenConfig); 76 | 77 | payload.layout.push(leftConfig); 78 | payload.layout.push(centerConfig); 79 | payload.layout.push(rightConfig); 80 | 81 | return payload; 82 | } 83 | --------------------------------------------------------------------------------