├── mock └── .gitkeep ├── .npmrc ├── banner1.png ├── .prettierignore ├── .vscode └── settings.json ├── .prettierrc ├── src ├── pages │ ├── setting │ │ ├── components │ │ │ ├── codeEditor │ │ │ │ ├── CodeDrawer.less │ │ │ │ ├── CodeModal.tsx │ │ │ │ ├── CodePopover.tsx │ │ │ │ ├── CodeDrawer.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── SourceCodeDrawer.tsx │ │ │ ├── right │ │ │ │ ├── index.tsx │ │ │ │ ├── index.less │ │ │ │ ├── Parser │ │ │ │ │ ├── ComponentModal.less │ │ │ │ │ ├── ComponentModal.tsx │ │ │ │ │ └── index.less │ │ │ │ └── SelectBox │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ ├── left │ │ │ │ ├── Setting.less │ │ │ │ ├── UILib.less │ │ │ │ ├── index.less │ │ │ │ ├── UILib.tsx │ │ │ │ ├── OptionModal.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── Request.tsx │ │ │ └── header │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── wdyr.ts │ │ ├── template.vue │ │ ├── index.less │ │ ├── index.tsx │ │ ├── const │ │ │ ├── index.ts │ │ │ ├── editModalDSL.ts │ │ │ ├── dsl.ts │ │ │ ├── detailDSL.ts │ │ │ └── editDSL.ts │ │ └── model │ │ │ ├── index.ts │ │ │ └── componentXML.ts │ ├── 404.tsx │ ├── index.tsx │ ├── index.less │ ├── ErrorBoundary.tsx │ ├── code │ │ ├── index.tsx │ │ ├── online.tsx │ │ ├── dsl.ts │ │ ├── GenerateVue.tsx │ │ └── GenerateReact.tsx │ └── document.ejs ├── tests │ └── index.js ├── utils │ ├── mock.ts │ └── index.ts └── auto │ ├── page │ ├── detail.js │ ├── editModal.js │ ├── common.js │ └── list.js │ ├── base.js │ ├── mockApiData.js │ ├── index.js │ └── transform.js ├── .editorconfig ├── .gitignore ├── typings.d.ts ├── tsconfig.json ├── .umirc.ts ├── README.md ├── package.json ├── idou copy.md ├── idou.md └── README-EN.md /mock/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org -------------------------------------------------------------------------------- /banner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctq123/idou/HEAD/banner1.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileheader.Author": "chengtianqing", 3 | "fileheader.LastModifiedBy": "chengtianqing" 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/setting/components/codeEditor/CodeDrawer.less: -------------------------------------------------------------------------------- 1 | .title-con { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | .title { 6 | font-size: 16px; 7 | font-weight: 600; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | 3 | const NotFound = () => ( 4 | 9 | ); 10 | 11 | export default NotFound; 12 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-05-29 21:54:56 4 | * @LastEditTime: 2021-07-03 00:40:46 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | import Setting from './setting'; 9 | 10 | export default Setting; 11 | -------------------------------------------------------------------------------- /src/pages/setting/wdyr.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 5 | whyDidYouRender(React, { 6 | // trackAllPureComponents: true, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/setting/template.vue: -------------------------------------------------------------------------------- 1 | 2 | 取消 3 | 7 | 再次提交 8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | /src/.umi 18 | /src/.umi-production 19 | /src/.umi-test 20 | /.env.local 21 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Parser from './Parser'; 3 | import styles from './index.less'; 4 | 5 | const Right = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default Right; 16 | -------------------------------------------------------------------------------- /src/pages/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: PingFang SC; 3 | font-weight: 500; 4 | font-size: 14px; 5 | color: #2b2c3c; 6 | } 7 | body { 8 | overflow-y: auto; 9 | } 10 | 11 | body::-webkit-scrollbar { 12 | width: 4px; 13 | background-color: #fff; 14 | } 15 | body::-webkit-scrollbar-thumb { 16 | border-radius: 8px; 17 | background-color: rgb(224, 223, 223); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/index.less: -------------------------------------------------------------------------------- 1 | .b-right { 2 | flex: 1; 3 | padding: 16px; 4 | width: calc(100% - 351px); 5 | // height: calc(100vh - 50px); 6 | // overflow-y: auto; 7 | .content { 8 | background-color: #ffffff; 9 | } 10 | } 11 | 12 | .b-right::-webkit-scrollbar { 13 | width: 4px; 14 | background-color: #fff; 15 | } 16 | .b-right::-webkit-scrollbar-thumb { 17 | border-radius: 8px; 18 | background-color: rgb(224, 223, 223); 19 | } 20 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent( 6 | props: React.SVGProps, 7 | ): React.ReactElement; 8 | const url: string; 9 | export default url; 10 | } 11 | 12 | /// 13 | 14 | declare module 'uuid'; 15 | 16 | declare function prettierFormat(str: string | null, type: string): any; 17 | -------------------------------------------------------------------------------- /src/pages/setting/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: PingFang SC; 3 | font-weight: 500; 4 | font-size: 14px; 5 | color: #2b2c3c; 6 | .c-body { 7 | display: flex; 8 | min-height: calc(100vh - 50px); 9 | } 10 | } 11 | 12 | body { 13 | overflow-y: auto; 14 | } 15 | 16 | body::-webkit-scrollbar { 17 | width: 4px; 18 | background-color: #fff; 19 | } 20 | body::-webkit-scrollbar-thumb { 21 | border-radius: 8px; 22 | background-color: rgb(224, 223, 223); 23 | } 24 | -------------------------------------------------------------------------------- /src/tests/index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | (async () => { 4 | const browser = await puppeteer.launch({ headless: false }); 5 | const page = await browser.newPage(); 6 | // console.log("打开页面", page) 7 | await page.setDefaultNavigationTimeout(0); 8 | await page.goto('https://yapi.baidu.com/project/66009/interface/api/934862'); 9 | await page.screenshot({ 10 | path: `/Users/chengtianqing/Desktop/${Date.now()}.png`, 11 | }); 12 | 13 | await browser.close(); 14 | })(); 15 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/Parser/ComponentModal.less: -------------------------------------------------------------------------------- 1 | .modal-content { 2 | height: 500px; 3 | overflow: auto; 4 | .component { 5 | margin-top: 24px; 6 | padding: 0 5px; 7 | .item { 8 | display: flex; 9 | padding: 12px; 10 | border: 1px solid #f1f1f5; 11 | img { 12 | width: 40px; 13 | height: 40px; 14 | margin-right: 8px; 15 | } 16 | } 17 | .item + .item { 18 | margin-top: 8px; 19 | } 20 | .active-item { 21 | border: 1px solid #4285f4; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | 4 | class ErrorBoundary extends React.Component { 5 | constructor(props: any) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | componentDidCatch(error: any, info: any) { 11 | // Display fallback UI 12 | this.setState({ hasError: true }); 13 | } 14 | 15 | render() { 16 | if (this.state.hasError) { 17 | return ; 18 | } 19 | return this.props.children; 20 | } 21 | } 22 | 23 | export default ErrorBoundary; 24 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/Setting.less: -------------------------------------------------------------------------------- 1 | .setting-container { 2 | font-size: 12px; 3 | box-sizing: border-box; 4 | .list-item { 5 | padding-bottom: 8px; 6 | } 7 | .list-item + .list-item { 8 | border-top: 1px solid rgba(0, 0, 0, 0.06); 9 | padding-top: 16px; 10 | } 11 | .w-100 { 12 | width: 100%; 13 | } 14 | :global { 15 | .ant-form-item { 16 | margin-bottom: 8px; 17 | } 18 | .ant-input { 19 | font-size: 12px; 20 | } 21 | .ant-select { 22 | font-size: 12px; 23 | } 24 | .ant-input-group-addon { 25 | padding: 0 2px; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/UILib.less: -------------------------------------------------------------------------------- 1 | .form-container { 2 | padding: 0 8px; 3 | .item { 4 | margin-bottom: 16px; 5 | .label { 6 | margin-bottom: 8px; 7 | } 8 | .ui { 9 | padding: 16px; 10 | border: 1px solid #f1f1f5; 11 | img { 12 | height: 30px; 13 | margin-right: 8px; 14 | } 15 | } 16 | .ui:hover { 17 | border: 1px dotted #4285f4; 18 | } 19 | .active-ui, 20 | .active-ui:hover { 21 | border: 1px solid #4285f4; 22 | } 23 | .ui + .ui { 24 | margin-top: 8px; 25 | } 26 | .icon-con { 27 | text-align: center; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/setting/components/header/index.less: -------------------------------------------------------------------------------- 1 | .c-header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | background: #ffffff; 6 | box-shadow: 0px 0px 5px rgba(153, 153, 153, 0.2); 7 | height: 50px; 8 | padding: 0px 24px; 9 | .h-left { 10 | display: flex; 11 | img { 12 | width: 30px; 13 | height: 30px; 14 | } 15 | .title { 16 | margin-left: 8px; 17 | font-size: 16px; 18 | font-weight: 600; 19 | } 20 | } 21 | .h-center { 22 | display: flex; 23 | button + button { 24 | margin-left: 8px; 25 | } 26 | } 27 | .h-right { 28 | display: flex; 29 | button + button { 30 | margin-left: 8px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": [ 19 | "mock/**/*", 20 | "src/**/*", 21 | "config/**/*", 22 | ".umirc.ts", 23 | "typings.d.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "typings", 31 | "**/__test__", 32 | "test", 33 | "docs", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin'); 3 | 4 | export default defineConfig({ 5 | nodeModulesTransform: { 6 | type: 'none', 7 | }, 8 | routes: [ 9 | { path: '/', component: '@/pages/index' }, 10 | // { path: '/code', component: '@/pages/code/index' }, 11 | { path: '/setting', component: '@/pages/setting/index' }, 12 | { component: '@/pages/404' }, 13 | ], 14 | fastRefresh: {}, 15 | externals: { 16 | react: 'React', 17 | 'react-dom': 'ReactDOM', 18 | }, 19 | headScripts: [ 20 | 'https://unpkg.com/react@17.0.1/umd/react.production.min.js', 21 | 'https://unpkg.com/react-dom@17.0.1/umd/react-dom.production.min.js', 22 | ], 23 | chainWebpack(config) { 24 | config.plugin('antd-dayjs-webpack-plugin').use(AntdDayjsWebpackPlugin); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/SelectBox/index.less: -------------------------------------------------------------------------------- 1 | .select-box { 2 | position: absolute; 3 | border: 1px solid #4285f4; 4 | background: rgba(66, 133, 244, 0.1); 5 | width: 0px; 6 | height: 0px; 7 | top: 0px; 8 | left: 0px; 9 | display: none; 10 | z-index: 100; 11 | .top { 12 | position: absolute; 13 | right: -1px; 14 | top: -25px; 15 | background-color: #4285f4; 16 | cursor: pointer; 17 | span { 18 | pointer-events: auto; 19 | color: #fff; 20 | font-size: 16px; 21 | padding-right: 2px; 22 | padding: 2px 5px; 23 | position: relative; 24 | } 25 | // span:hover:after { 26 | // content: attr(title); 27 | // position: absolute; 28 | // right: 0px; 29 | // top: 25px; 30 | // padding: 2px 6px; 31 | // font-size: 12px; 32 | // color: #fff; 33 | // background-color: #4285f4; 34 | // } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/code/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import GenerateVue from './GenerateVue'; 3 | import GenerateReact from './GenerateReact'; 4 | import Online from './online'; 5 | import ErrorBoundary from '../ErrorBoundary'; 6 | 7 | export default function CodePage() { 8 | const vueEl = useRef(null); 9 | const reactEl = useRef(null); 10 | 11 | const generateCode = (type: any) => { 12 | if (type === 'react') { 13 | return reactEl.current.getSourceCode(); 14 | } else { 15 | return vueEl.current.getSourceCode(); 16 | } 17 | }; 18 | 19 | return ( 20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 |
29 | 30 | generateCode(type)} /> 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import './wdyr'; 2 | import React, { useState, useReducer } from 'react'; 3 | import Header from './components/header'; 4 | import Left from './components/left'; 5 | import Right from './components/right'; 6 | import ErrorBoundary from '../ErrorBoundary'; 7 | import { reducer, initState, Context } from '@/pages/setting/model'; 8 | import styles from './index.less'; 9 | 10 | const SettingPage = () => { 11 | const [state, dispatch] = useReducer(reducer, initState); 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 | ); 31 | }; 32 | 33 | // SettingPage.whyDidYouRender = true 34 | 35 | export default SettingPage; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # idou 2 | 3 | 简体中文 | [English](./README-EN.md) 4 | 5 | idou项目是一个低代码开发平台,通过配置化,最终输出的是源码(vue2/react),也可以将它理解为代码生成器。 6 | https://idou100.netlify.app 7 | 8 | ![图片](./banner1.png) 9 | ### 开始 10 | 11 | 安装 12 | ```bash 13 | $ yarn 14 | ``` 15 | 16 | 启动 17 | 18 | ```bash 19 | $ yarn start 20 | ``` 21 | ### 使用 22 | 适用场景:中后台,传统开发模式,vue 23 | 24 | 其实idou只是搭建器中的一种实现方式,它属于非运行时,具有非常强的灵活性和可控性,与现有的传统开发模式不冲突,与其他现成运行时的搭建器也不冲突,它与现成的搭建器不是对立的关系,而是一种补充。如果你们公司没有还不具备搭建器,采用的又是传统的开发模式,这个工具将非常适合你。 25 | 26 | ### 自动化 27 | idou项目其实由两部分组成:代码生成器和自动化源码生成器。 28 | 29 | 代码生成器就是你所看到的界面,另一个隐藏的利器是自动化源码生成器。 30 | 31 | 你可以尝试一下运行一下命令 32 | 33 | ```bash 34 | $ yarn auto 35 | ``` 36 | 37 | 它的核心是数据驱动生成代码,也就是以接口数据为核心,适用它自动生成我们对应的页面 38 | 39 | 目前里面是mock数据,真正的场景是后端会定义好数据接口给我们,我们只要通过爬虫抓取到接口数据,根据接口数据判断要生成的模版页面,同时转换成平台能识别到的数据,从而模拟我们真实环境生成的页面,从而省去我们手动配置这一步。 40 | 41 | 当然,需要根据结合自身的业务去修改。 42 | 43 | ### 未来可能的规划 44 | 1)UI库的选择,允许用户引入其他UI库 45 | 46 | 2)预览功能,打通codesandbox,实现项目的在线预览 47 | 48 | 3)允许增加自定义的模版,除了四表一局(列表,表格,表单,图表,布局)扩展外,希望能处理增加自定义模版 49 | 50 | 4)增加物料市场管理,实现物料共享和编辑,服务拆分,需要支持vscode插件 51 | 52 | 5)接通github仓库,自动上传源码(nodejs),形成整条链路的闭环。[已实现demo](https://github.com/ctq123/dslService),但现实中肯定会遇到很多问题,比如如何解决代码冲突的问题。 53 | 54 | 6)改善UI交互 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/SelectBox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import { 3 | ArrowUpOutlined, 4 | ArrowDownOutlined, 5 | CopyOutlined, 6 | DeleteOutlined, 7 | PlusOutlined, 8 | } from '@ant-design/icons'; 9 | import styles from './index.less'; 10 | 11 | interface IProps { 12 | style: object; 13 | handleCB: Function; 14 | } 15 | 16 | const SelectBox = (props: IProps) => { 17 | const { style, handleCB } = props; 18 | return ( 19 |
20 |
21 | handleCB('up')} 25 | > 26 | 27 | 28 | handleCB('down')} 32 | > 33 | 34 | 35 | handleCB('add')} 39 | > 40 | 41 | 42 | handleCB('copy')} 46 | > 47 | 48 | 49 | handleCB('delete')} 53 | > 54 | 55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default SelectBox; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "umi build", 6 | "postinstall": "umi generate tmp", 7 | "analyze": "ANALYZE=1 umi build", 8 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 9 | "auto": "node ./src/auto/index.js", 10 | "test": "umi-test", 11 | "test:coverage": "umi-test --coverage" 12 | }, 13 | "gitHooks": { 14 | "pre-commit": "lint-staged" 15 | }, 16 | "lint-staged": { 17 | "*.{js,jsx,less,md,json}": [ 18 | "prettier --write" 19 | ], 20 | "*.ts?(x)": [ 21 | "prettier --parser=typescript --write" 22 | ] 23 | }, 24 | "dependencies": { 25 | "@ant-design/pro-layout": "^6.5.0", 26 | "@monaco-editor/react": "^4.1.3", 27 | "@stackblitz/sdk": "^1.5.2", 28 | "@umijs/preset-react": "1.x", 29 | "antd": "^4.12.3", 30 | "copy-to-clipboard": "^3.3.1", 31 | "dayjs": "^1.10.7", 32 | "file-saver": "^2.0.5", 33 | "jszip": "^3.6.0", 34 | "lodash": "^4.17.21", 35 | "puppeteer": "^10.0.0", 36 | "react": "17.x", 37 | "react-dom": "17.x", 38 | "serialize-javascript": "^5.0.1", 39 | "umi": "^3.5.20", 40 | "uuid": "^8.3.2" 41 | }, 42 | "devDependencies": { 43 | "@types/file-saver": "^2.0.3", 44 | "@types/gtag.js": "^0.0.7", 45 | "@types/lodash": "^4.14.171", 46 | "@types/prettier": "^2.3.2", 47 | "@types/react": "^17.0.0", 48 | "@types/react-dom": "^17.0.0", 49 | "@umijs/test": "^3.3.9", 50 | "@welldone-software/why-did-you-render": "^6.2.0", 51 | "antd-dayjs-webpack-plugin": "^1.0.6", 52 | "lint-staged": "^10.0.7", 53 | "prettier": "^2.2.0", 54 | "typescript": "^4.1.2", 55 | "yorkie": "^2.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/setting/components/codeEditor/CodeModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Button } from 'antd'; 3 | import CodeEditor from './index'; 4 | 5 | interface IProps { 6 | value: any; 7 | visible: boolean; 8 | type?: 'component' | 'function' | 'html'; 9 | title?: string; 10 | handleCB?: any; 11 | } 12 | 13 | const CodeModal = (props: IProps) => { 14 | const { handleCB, type = 'component', title = '选项设置' } = props; 15 | const codeRef: any = React.useRef(null); 16 | 17 | const handleSave = () => { 18 | const code = codeRef.current.getEditorValue(); 19 | console.log('code', code); 20 | handleCB && handleCB({ visible: false, code }); 21 | gtag('event', 'handleSave', { 22 | event_category: 'CodeModal', 23 | event_label: `确定`, 24 | value: 1, 25 | }); 26 | }; 27 | 28 | const handleClear = () => { 29 | const obj = {}; 30 | codeRef && codeRef.current.forceSetEditorValue(obj); 31 | }; 32 | 33 | return ( 34 | handleSave()} 38 | onOk={() => handleSave()} 39 | footer={[ 40 | // , 43 | , 50 | , 58 | ]} 59 | > 60 | (codeRef.current = ref)} 65 | /> 66 | 67 | ); 68 | }; 69 | 70 | export default CodeModal; 71 | -------------------------------------------------------------------------------- /src/pages/setting/components/codeEditor/CodePopover.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Popover, Button } from 'antd'; 3 | import { UnorderedListOutlined } from '@ant-design/icons'; 4 | import CodeEditor from './index'; 5 | 6 | interface IProps { 7 | value: any; 8 | visible: boolean; 9 | type?: 'component' | 'function' | 'html'; 10 | placement?: any; 11 | title?: string; 12 | children?: any; 13 | handleCB?: any; 14 | } 15 | 16 | const CodePopover = (props: IProps) => { 17 | const { handleCB, type = 'component', placement = 'right', children } = props; 18 | const codeRef: any = React.useRef(null); 19 | 20 | const onVisibleChange = (v: boolean) => { 21 | handleCB && handleCB({ visible: v }); 22 | }; 23 | 24 | const handleSave = () => { 25 | const code = codeRef.current.getEditorValue(); 26 | console.log('code', code); 27 | handleCB && handleCB({ visible: false, code }); 28 | }; 29 | 30 | const titleNode = () => ( 31 |
38 |
{props.title || '编辑'}
39 | 46 |
47 | ); 48 | 49 | const contentNode = () => ( 50 | (codeRef.current = ref)} 54 | /> 55 | ); 56 | 57 | return ( 58 | 66 | {children || ( 67 | 49 | 50 | 51 | 52 | ); 53 | return ( 54 | 64 | (codeRef.current = ref)} 68 | /> 69 | 70 | ); 71 | }; 72 | 73 | export default CodeDrawer; 74 | -------------------------------------------------------------------------------- /idou copy.md: -------------------------------------------------------------------------------- 1 | 2 | # 低代码平台的理解 3 | 4 | 今天跟大家分享一下低代码平台的思考。 5 | 6 | 内容主要分四个部分。一是对低代码的背景,二是低代码的思考,三是低代码解决的问题,四是低代码的未来 7 | 8 | ## 低代码介绍及背景 9 | 关于可视化编程,是指通过可视化的环境平台,以更快捷的配置方式实现应用程序的生成。它有两个主要特征:一是可视化,二是可配置。 10 | 11 | 按目标代码划分,可分为以下两类 12 | 13 | | | 低代码 | 无代码 | 14 | | ---- | ---- | ---- | 15 | | 平台 | 可视化 | 可视化 | 16 | | 编码 | 少编码 | 无编码 | 17 | | 面向群体 | 开发 | 运营 | 18 | 19 | 按技术实现,可分为以下两类 20 | 21 | | | 运行时 | 非运行时 | 22 | | ---- | ---- | ---- | 23 | | 出码能力 | 不输出源码 | 输出源码 | 24 | | 编排时机 | 运行时编排 | 编译时编排 | 25 | | 运行效率 | 低(项目越复杂越明显) | 较高 | 26 | | 灵活性 | 低 | 较高 | 27 | | 维护成本 | 较低 | 较高 | 28 | | 风险性 | 较高且影响广 | 较低 | 29 | | 面向群体 | 运营 | 开发 | 30 | 31 | 当然运行时的灵活性也可能很高,如果设计成原子化编程,那么灵活性的确也可以很高,但这就违背了低代码提效的初衷,只是用另外一种方式编写代码罢了,有点本末倒置的感觉。 32 | 33 | 而非运行时会输出源码,后续一般由开发者接管,但也可实现无代码编程,这里先买个关子。 34 | 35 | 无论是低代码还是无代码,它们都有共同的目标,那就是降本、提效和赋能。 36 | 37 | 我认为搭建器最本质的作用就是提效。 38 | 39 | 于是乎,我们就有了面向H5的搭建器——哪吒搭建器;有了面向PC端的搭建器——翱翔天城搭建器 40 | 41 | 当然搭建器范围有点泛,我们今天主要讨论的是中后台领域的低代码搭建。 42 | 43 | 由于PC端的搭建器起步相对晚一些,没有很完善,并不能覆盖所有的业务场景。因此,在页面搭建器还没有完善起来之前,很多老业务系统还是使用传统的pro code方式进行开发的。 44 | #### 现状和痛点 45 | 我们项目中存在各种各样的差异性问题,有的是有共性,有的是某个项目中特有的。首先是交互不统一的问题,这是由于内部系统大多都没有交互设计,而且迭代也比较快,设计资源也跟不上。其次,代码风格各异,代码繁杂,对维护成本造成一定的影响,同时可能出现隐藏的bug。第三,系统多业务重,对后端赋能的能力有限。第四,重复性开发太多,同时公共资源无法及时同步更新,对新手不友好。 46 | 47 | ![图片](https://cdn.poizon.com/node-common/267b9ff988320af71373aad817dd46c4.png) 48 | 49 | ## 问题和思考 50 | 51 | 传统的开发模式如何进一步提效呢? 52 | ### 分析 53 | #### 页面类型 54 | 经过分析和观察,我们大多数中后台系统其实最多的就是列表页面,这一占比通常有65%以上,有的更高。其次,便是编辑页面和详情页面,占比25%以上,剩余10%都是其他类型的页面,当然这是普通的主流后台类型,其他如图表展示类系统除外。 55 | 56 | ![图片](https://cdn.poizon.com/node-common/ec1f1ea93eef825b4f7aac0b65e5d863.png) 57 | 58 | #### 页面布局 59 | 由于我们公司已经有集成了布局组件(即头部+菜单),市面上也有比较成熟的方案,比如基于vue3的[fesjs](https://webank.gitee.io/fes.js/)。而我们日常中开发接触更多的应该是下面红色框内部页面的开发 60 | 61 | ![图片](https://cdn.poizon.com/node-common/c16891f52aad2816de75ce01a0438e60.png) 62 | 63 | #### 语言特性 64 | 目前我们大多的项目框架是基于vue2和react进行开发的,而我们更多的需求都集中在vue这边,因此先要解决vue的需求,然后再求跨语言。 65 | 66 | #### 结构特性 67 | 其实只要分析以下列表,查看,编辑页面的代码,只要类型相同,它们的代码结构都基本相同;尤其是列表。如下: 68 | 69 | ![图片](https://cdn.poizon.com/node-common/ff8d1f513a48be886e18c920d084d37c.png) 70 | 71 | 它组成的元素通常有4部分,分别为搜索栏,操作模块,表格和分页模块。我们公司内部主流的列表类都是这种结构。其他详情页和编辑页会有些差异,但都有主流结构样式,这里就不一一列举了。 72 | 73 | ## 目标 74 | 我们的目标是在现有的开发模式上进一步提效, 75 | 76 | ## 自动化生成器 77 | 78 | 79 | ## 展望 80 | 81 | 82 | -------------------------------------------------------------------------------- /idou.md: -------------------------------------------------------------------------------- 1 | 2 | # 自动化生成源码的探索与实践 3 | 4 | 今天跟大家分享一下自动化生成源码的探索和演进之路。 5 | 6 | 内容主要分四个部分。一是对低代码的简单介绍和我们的项目背景,二是我们遇到的问题和解决问题的思考,三是我们为了解决这些问题引出的自动化代码生成器的介绍,四是对未来的展望 7 | ## 低代码介绍及背景 8 | 关于可视化编程,是指通过可视化的环境平台,以更快捷的配置方式实现应用程序的生成。它有两个主要特征:一是可视化,二是可配置。 9 | 10 | 按目标代码划分,可分为以下两类 11 | 12 | | | 低代码 | 无代码 | 13 | | ---- | ---- | ---- | 14 | | 平台 | 可视化 | 可视化 | 15 | | 编码 | 少编码 | 无编码 | 16 | | 面向群体 | 开发 | 运营 | 17 | 18 | 按技术实现,可分为以下两类 19 | 20 | | | 运行时 | 非运行时 | 21 | | ---- | ---- | ---- | 22 | | 出码能力 | 不输出源码 | 输出源码 | 23 | | 编排时机 | 运行时编排 | 编译时编排 | 24 | | 运行效率 | 低(项目越复杂越明显) | 较高 | 25 | | 灵活性 | 低 | 较高 | 26 | | 维护成本 | 较低 | 较高 | 27 | | 风险性 | 较高且影响广 | 较低 | 28 | | 面向群体 | 运营 | 开发 | 29 | 30 | 当然运行时的灵活性也可能很高,如果设计成原子化编程,那么灵活性的确也可以很高,但这就违背了低代码提效的初衷,只是用另外一种方式编写代码罢了,有点本末倒置的感觉。 31 | 32 | 而非运行时会输出源码,后续一般由开发者接管,但也可实现无代码编程,这里先买个关子。 33 | 34 | 无论是低代码还是无代码,它们都有共同的目标,那就是降本、提效和赋能。 35 | 36 | 我认为搭建器最本质的作用就是提效。 37 | 38 | 于是乎,我们就有了面向H5的搭建器——哪吒搭建器;有了面向PC端的搭建器——翱翔天城搭建器 39 | 40 | 当然搭建器范围有点泛,我们今天主要讨论的是中后台领域的低代码搭建。 41 | 42 | 由于PC端的搭建器起步相对晚一些,没有很完善,并不能覆盖所有的业务场景。因此,在页面搭建器还没有完善起来之前,很多老业务系统还是使用传统的pro code方式进行开发的。 43 | #### 现状和痛点 44 | 我们项目中存在各种各样的差异性问题,有的是有共性,有的是某个项目中特有的。首先是交互不统一的问题,这是由于内部系统大多都没有交互设计,而且迭代也比较快,设计资源也跟不上。其次,代码风格各异,代码繁杂,对维护成本造成一定的影响,同时可能出现隐藏的bug。第三,系统多业务重,对后端赋能的能力有限。第四,重复性开发太多,同时公共资源无法及时同步更新,对新手不友好。 45 | 46 | ![图片](https://cdn.poizon.com/node-common/267b9ff988320af71373aad817dd46c4.png) 47 | 48 | ## 问题和思考 49 | 50 | 传统的开发模式如何进一步提效呢? 51 | ### 分析 52 | #### 页面类型 53 | 经过分析和观察,我们大多数中后台系统其实最多的就是列表页面,这一占比通常有65%以上,有的更高。其次,便是编辑页面和详情页面,占比25%以上,剩余10%都是其他类型的页面,当然这是普通的主流后台类型,其他如图表展示类系统除外。 54 | 55 | ![图片](https://cdn.poizon.com/node-common/ec1f1ea93eef825b4f7aac0b65e5d863.png) 56 | 57 | #### 页面布局 58 | 由于我们公司已经有集成了布局组件(即头部+菜单),市面上也有比较成熟的方案,比如基于vue3的[fesjs](https://webank.gitee.io/fes.js/)。而我们日常中开发接触更多的应该是下面红色框内部页面的开发 59 | 60 | ![图片](https://cdn.poizon.com/node-common/c16891f52aad2816de75ce01a0438e60.png) 61 | 62 | #### 语言特性 63 | 目前我们大多的项目框架是基于vue2和react进行开发的,而我们更多的需求都集中在vue这边,因此先要解决vue的需求,然后再求跨语言。 64 | 65 | #### 结构特性 66 | 其实只要分析以下列表,查看,编辑页面的代码,只要类型相同,它们的代码结构都基本相同;尤其是列表。如下: 67 | 68 | ![图片](https://cdn.poizon.com/node-common/ff8d1f513a48be886e18c920d084d37c.png) 69 | 70 | 它组成的元素通常有4部分,分别为搜索栏,操作模块,表格和分页模块。我们公司内部主流的列表类都是这种结构。其他详情页和编辑页会有些差异,但都有主流结构样式,这里就不一一列举了。 71 | 72 | ## 目标 73 | 我们的目标是在现有的开发模式上进一步提效, 74 | 75 | ## 自动化生成器 76 | 77 | 78 | ## 展望 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/utils/mock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-07 01:10:38 4 | * @LastEditTime: 2021-06-14 03:32:40 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | import dayjs from 'dayjs'; 9 | 10 | const lastDataCache: any = {}; 11 | /** 12 | * 根据key获取对应的数据 13 | * @param keyStr 14 | */ 15 | const createMockData = (keyStr: string, i: number) => { 16 | const mockTypes = [ 17 | { 18 | keys: ['number', 'id'], 19 | value: Date.now().toString().substr(-8) + i, 20 | }, 21 | { 22 | keys: ['no', 'name', 'code'], 23 | value: 'AB' + Math.random().toString(36).slice(-8) + i, 24 | }, 25 | { 26 | keys: ['mount', 'price'], 27 | value: Math.floor(Math.random() * 100) * 100, 28 | }, 29 | { 30 | keys: ['status'], 31 | value: Math.floor(Math.random() * 4), 32 | }, 33 | { 34 | keys: ['time'], 35 | value: dayjs().subtract(i, 'days').format('YYYY-MM-DD HH:mm:ss'), 36 | }, 37 | ]; 38 | for (let i = 0; i < mockTypes.length; i++) { 39 | let item = mockTypes[i]; 40 | if (item.keys.find((it) => keyStr.toLowerCase().indexOf(it) > -1)) { 41 | return item.value; 42 | } 43 | } 44 | return Math.random().toString(36).slice(-6); 45 | }; 46 | 47 | /** 48 | * 异步获取数据 49 | * @param params 50 | */ 51 | export function getMockListAsync(data = {}) { 52 | return new Promise((resolve, reject) => { 53 | setTimeout(() => { 54 | resolve(getMockListSync(data)); 55 | }, 1000); 56 | }); 57 | } 58 | 59 | /** 60 | * 同步获取数据 61 | * @param data 62 | * @returns 63 | */ 64 | export function getMockListSync(data = {}) { 65 | const keys = Object.keys(data).join('-'); 66 | const list: any = []; 67 | if (!keys) { 68 | return []; 69 | } 70 | if (lastDataCache[keys]) { 71 | return lastDataCache[keys]; 72 | } 73 | if (Object.keys(lastDataCache).length >= 500) { 74 | // 防止缓存数据过大 75 | Object.keys(lastDataCache).forEach((k) => { 76 | delete lastDataCache[k]; 77 | }); 78 | } 79 | for (let i = 0; i < 3; i++) { 80 | let item: any = {}; 81 | Object.keys(data).forEach((k) => { 82 | item[k] = createMockData(k, i); 83 | }); 84 | if (item.id === undefined) { 85 | item.id = createMockData('id', i); 86 | } 87 | list.push(item); 88 | } 89 | lastDataCache[keys] = list; 90 | return list; 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/code/online.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // import Vue from 'vue' 3 | // import App from './App' 4 | 5 | // new Vue({ 6 | // el: '#app', 7 | // render: h => h(App), 8 | // }) 9 | 10 | import React from 'react'; 11 | import { Button } from 'antd'; 12 | import sdk from '@stackblitz/sdk'; 13 | 14 | interface IProps { 15 | generateCode?: any; 16 | } 17 | 18 | const Online = (props: IProps) => { 19 | const handleOnlineVue = () => { 20 | const code = props.generateCode('vue'); 21 | const html = `
`; 22 | const project = { 23 | files: { 24 | 'index.vue': code, 25 | 'index.html': html, 26 | }, 27 | title: 'Dynamically Generated Project', 28 | description: 'Created with <3 by the StackBlitz SDK!', 29 | template: 'typescript', 30 | tags: ['stackblitz', 'sdk'], 31 | dependencies: { 32 | 'element-ui': '*', 33 | moment: '*', // * = latest version 34 | }, 35 | }; 36 | 37 | sdk.embedProject('app-code', project, { height: 1000 }); 38 | }; 39 | 40 | const handleOnlineReact = () => { 41 | const code = props.generateCode('react'); 42 | const html = `
`; 43 | const project = { 44 | files: { 45 | 'index.tsx': code, 46 | 'index.html': html, 47 | }, 48 | title: 'Dynamically Generated Project', 49 | description: 'Created with <3 by the StackBlitz SDK!', 50 | template: 'create-react-app', 51 | tags: ['stackblitz', 'sdk'], 52 | dependencies: { 53 | antd: '*', 54 | moment: '*', // * = latest version 55 | }, 56 | }; 57 | 58 | sdk.embedProject('app-code', project, { height: 1000 }); 59 | }; 60 | 61 | const handleOpenCodesandbox = () => { 62 | window.open('https://codesandbox.io/s/github/ctq123/vue-admin-template'); 63 | }; 64 | 65 | return ( 66 |
67 | 70 | 71 | 74 | 75 | 78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Online; 85 | -------------------------------------------------------------------------------- /src/pages/setting/const/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-14 01:33:29 4 | * @LastEditTime: 2021-06-27 02:49:10 5 | * @LastEditors: chengtianqing 6 | * @备注: 图片中无敏感信息 7 | */ 8 | import { getDomain } from '@/utils'; 9 | /** 10 | * 左侧内容tab 11 | */ 12 | export const tabs = [ 13 | { code: 'template', label: '模版' }, 14 | { code: 'component', label: '组件' }, 15 | { code: 'setting', label: '设置' }, 16 | { code: 'request', label: '请求' }, 17 | { code: 'uilib', label: 'UI库' }, 18 | ]; 19 | 20 | const domain = getDomain(); 21 | const baseUrl = `https://cdn.${domain}.com/node-common`; 22 | 23 | /** 24 | * UI库 25 | */ 26 | export const UILib: any = { 27 | react: [ 28 | { 29 | prefixUI: '', 30 | name: 'Ant Design', 31 | libUrl: 'https://ant.design/components/overview-cn/', 32 | iconUrl: 33 | 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 34 | }, 35 | ], 36 | vue2: [ 37 | { 38 | prefixUI: 'el', 39 | name: 'Element UI', 40 | libUrl: 'https://element.eleme.io/#/zh-CN/component/installation', 41 | iconUrl: `${baseUrl}/3f4f24f336036d47968bb94f0a87fe36.png`, 42 | }, 43 | { 44 | prefixUI: 'a', 45 | name: 'Ant Design Vue', 46 | libUrl: 'https://1x.antdv.com/docs/vue/introduce-cn', 47 | iconUrl: 'https://alicdn.antdv.com/v2/assets/logo.1ef800a8.svg', 48 | }, 49 | ], 50 | vue3: [ 51 | { 52 | prefixUI: 'el', 53 | name: 'Element Plus UI', 54 | libUrl: 'https://element-plus.org/#/zh-CN/component/installation', 55 | iconUrl: `${baseUrl}/27fa8f4a2a5405682c276053ac51f1db.png`, 56 | }, 57 | { 58 | prefixUI: 'a', 59 | name: 'Ant Design Vue', 60 | libUrl: 'https://2x.antdv.com/components/overview-cn/', 61 | iconUrl: 'https://alicdn.antdv.com/v2/assets/logo.1ef800a8.svg', 62 | }, 63 | ], 64 | }; 65 | 66 | /** 67 | * 模版 68 | */ 69 | export const templates = [ 70 | { 71 | code: 'list', 72 | label: '管理列表', 73 | img: `${baseUrl}/fa2b31239e9b8d18d0ff2a85186a665e.png`, 74 | }, 75 | { 76 | code: 'detail', 77 | label: '弹窗详情', 78 | img: `${baseUrl}/cbd6486d427d9f7fd9a8119fb3d11b0f.png`, 79 | }, 80 | { 81 | code: 'editModal', 82 | label: '弹窗编辑', 83 | img: `${baseUrl}/409459c03e1caf55df0b87ec31be1523.png`, 84 | }, 85 | { 86 | code: 'edit', 87 | label: '编辑页面', 88 | img: `${baseUrl}/9848298872089476cd6d678c1f1168f4.png`, 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /src/pages/document.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | idou 9 | 10 | 11 | 12 | 13 | 14 | 21 | 63 | 64 | 65 |
66 | 67 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/UILib.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Form, Input, Button, Radio, message } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import styles from './UILib.less'; 5 | import { UILib } from '../../const'; 6 | 7 | interface IProps { 8 | codeType?: any; 9 | prefixUI?: any; 10 | handleCB?: any; 11 | } 12 | 13 | const UILibCom = (props: IProps) => { 14 | const { codeType, prefixUI, handleCB } = props || {}; 15 | const [code, setCode] = useState(codeType); 16 | const [UI, setUI] = useState(prefixUI); 17 | 18 | const handleIconClick = (item: any) => { 19 | if (item && item.libUrl) { 20 | window.open(item.libUrl); 21 | } 22 | }; 23 | 24 | const handleCode = (e: any) => { 25 | const val = e.target.value; 26 | setCode(val); 27 | if (Array.isArray(UILib[val]) && UILib[val].length) { 28 | const item = val === 'vue3' ? UILib[val][1] : UILib[val][0]; 29 | setUI(item.prefixUI); 30 | } 31 | }; 32 | 33 | const addUI = () => { 34 | message.warn('功能尚在开发中……'); 35 | // TODO 36 | }; 37 | 38 | const onsubmit = () => { 39 | if (!code || !(UI === '' || UI)) { 40 | message.error('请选择有效值'); 41 | return; 42 | } 43 | handleCB && handleCB({ codeType: code, prefixUI: UI }); 44 | }; 45 | 46 | return ( 47 |
48 |
49 |
源码:
50 | 51 | {Object.keys(UILib).map((k) => ( 52 | 53 | {k} 54 | 55 | ))} 56 | 57 |
58 | 59 |
60 |
UI库:
61 | {(UILib[code] || []).map((item: any, i: number) => ( 62 |
setUI(item.prefixUI)} 70 | > 71 | handleIconClick(item)} /> 72 | {item.name} 73 |
74 | ))} 75 |
addUI()} 78 | > 79 | 80 |
81 |
82 | 83 | 86 |
87 | ); 88 | }; 89 | 90 | export default UILibCom; 91 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | # idou 2 | 3 | [简体中文](./README.md) | English 4 | 5 | The idou(idol)project is a low-code development platform. Through configuration, the final output is vue2 source code, which can also be understood as a code generator. 6 | https://idou100.netlify.app 7 | 8 | ![图片](./banner1.png) 9 | ### Start 10 | 11 | install 12 | ```bash 13 | $ yarn 14 | ``` 15 | 16 | start serve 17 | 18 | ```bash 19 | $ yarn start 20 | ``` 21 | 22 | ### Usage 23 | Applicable scenarios: background management projects, traditional development mode, use vue 24 | 25 | In fact, idou(idol)is just an implementation in the builder. It belongs to non-runtime and has very strong flexibility and controllability. It does not conflict with the existing traditional development model, nor does it conflict with other ready-made runtime builders. , It is not an antagonistic relationship with the ready-made builders, but a supplement. If your company does not yet have a builder and uses a traditional development model, this tool will be very suitable for you. 26 | 27 | ### Automation 28 | The idou project actually consists of two parts: a code generator and an automated source code generator. 29 | 30 | The code generator is the interface you see, and another hidden weapon is the automated source code generator. 31 | 32 | You can try it and run the command 33 | 34 | ```bash 35 | $ yarn auto 36 | ``` 37 | 38 | Its core is data-driven code generation, which is based on interface data, and it is used to automatically generate our corresponding pages 39 | 40 | At present, there is mock data. The real scenario is that the backend will define the data interface for us. We only need to grab the interface data through the crawler, judge the template page to be generated according to the interface data, and convert it into data that the platform can recognize. So as to simulate the page generated by our real environment, thus eliminating the need for us to manually configure this step. 41 | 42 | Different companies need to modify according to their own business. 43 | 44 | ### Future plans 45 | 1)Generate multiple sets of source code (vue, react, applet, etc.), the demo for generating react has actually been implemented in the /code directory, see the generateReact.tsx file for details 46 | 47 | 2)Preview function, open codesandbox, realize online preview of the project, will give priority to upgrading to vue3, because codesandbox does not support vue2 temporarily 48 | 49 | 3)Connect to the github warehouse and automatically upload the source code (nodejs) to form a closed loop of the entire link. In fact, the demo (https://github.com/ctq123/dslService) has been implemented, but in reality there will definitely be many problems, such as how to resolve code conflicts. 50 | 51 | 4)Decompile the source code, by importing the source code, you can decompile and get the DSL -------------------------------------------------------------------------------- /src/auto/page/detail.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-07-03 00:31:18 4 | * @LastEditTime: 2021-07-04 00:43:56 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | const get = require('lodash/get'); 9 | const base = require('../base.js'); 10 | const common = require('./common.js'); 11 | 12 | const generatePage = async ({ page, apiData }) => { 13 | let ele = null; 14 | 15 | // 信息头 16 | const baseInfoChange = async () => { 17 | await page.waitForSelector("#root div[class^='modal'] section"); 18 | await base.clickDom( 19 | page, 20 | null, 21 | "#root div[class^='modal'] section .ant-row", 22 | ); 23 | await page.waitForSelector('#rc-tabs-0-panel-setting form div button'); 24 | await page.waitForTimeout(1000); 25 | 26 | ele = await page.$('#rc-tabs-0-panel-setting'); 27 | // 先清空所有数据 28 | await base.clickAllDom( 29 | page, 30 | ele, 31 | 'form div div div div div div button span.anticon-delete', 32 | ); 33 | await page.waitForTimeout(500); 34 | let recordObj = get(apiData, 'recordObj'); 35 | let i = 1; 36 | let plusEl = await ele.$( 37 | `form > div > div > div > div button .anticon-plus`, 38 | ); 39 | for (let k in recordObj) { 40 | // await plusEl.click();// 诡异有时候不会触发 41 | await page.evaluate((el) => { 42 | return el.click(); 43 | }, plusEl); 44 | await page.waitForSelector( 45 | `#dynamic_form_nest_item div:nth-child(${i}) input`, 46 | { timeout: 10000 }, 47 | ); 48 | await base.setInput( 49 | page, 50 | ele, 51 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(1)`, 52 | recordObj[k].label, 53 | ); 54 | await base.setInput( 55 | page, 56 | ele, 57 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(2)`, 58 | k, 59 | ); 60 | console.log(i, k, recordObj[k].label); 61 | await base.setSelect( 62 | page, 63 | ele, 64 | `form div:nth-child(${i}) .ant-space:nth-child(2) .ant-space-item:nth-child(1)`, 65 | recordObj[k].componentType, 66 | i, 67 | ); 68 | if (['状态'].includes(recordObj[k].componentType)) { 69 | await common.setOptionModal({ page, enumObj: recordObj[k].enumObj }); 70 | } 71 | i++; 72 | } 73 | 74 | // 提交 75 | await base.clickButton(page, ele, 'div div div div', '提交'); 76 | await page.waitForTimeout(1000); 77 | }; 78 | 79 | // 处理 80 | await common.tmplChange({ page, text: '弹窗详情' }); 81 | await common.apiChange({ page, apiData }); 82 | await common.modalTitleChange({ page, apiData, text: 'XX详情' }); 83 | await baseInfoChange(); 84 | await common.generateCode({ page }); 85 | }; 86 | 87 | module.exports = { 88 | generatePage, 89 | }; 90 | -------------------------------------------------------------------------------- /src/auto/page/editModal.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-07-03 00:31:18 4 | * @LastEditTime: 2021-07-04 00:41:59 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | const get = require('lodash/get'); 9 | const base = require('../base.js'); 10 | const common = require('./common.js'); 11 | 12 | const generatePage = async ({ page, apiData }) => { 13 | let ele = null; 14 | 15 | // 表单 16 | const formChange = async () => { 17 | await page.waitForSelector("#root div[class^='modal'] form"); 18 | await base.clickDom(page, null, "#root div[class^='modal'] form .ant-row"); 19 | await page.waitForSelector('#rc-tabs-0-panel-setting form div button'); 20 | await page.waitForTimeout(1000); 21 | 22 | ele = await page.$('#rc-tabs-0-panel-setting'); 23 | // 先清空所有数据 24 | await base.clickAllDom( 25 | page, 26 | ele, 27 | 'form > div > div > div > div > div div button span.anticon-delete', 28 | ); 29 | await page.waitForTimeout(500); 30 | let formObj = get(apiData, 'formObj'); 31 | let i = 1; 32 | let plusEl = await ele.$( 33 | `form > div > div > div > div button .anticon-plus`, 34 | ); 35 | for (let k in formObj) { 36 | // await plusEl.click();// 诡异有时候不会触发 37 | await page.evaluate((el) => { 38 | return el.click(); 39 | }, plusEl); 40 | await page.waitForSelector( 41 | `#dynamic_form_nest_item div:nth-child(${i}) input`, 42 | { timeout: 10000 }, 43 | ); 44 | await base.setInput( 45 | page, 46 | ele, 47 | `#dynamic_form_nest_item div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(1)`, 48 | formObj[k].label, 49 | ); 50 | await base.setInput( 51 | page, 52 | ele, 53 | `#dynamic_form_nest_item div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(2)`, 54 | k, 55 | ); 56 | console.log(i, k, formObj[k].label, formObj[k].enumObj); 57 | await base.setSelect( 58 | page, 59 | ele, 60 | `#dynamic_form_nest_item div:nth-child(${i}) .ant-space:nth-child(2) .ant-space-item:nth-child(1)`, 61 | formObj[k].componentType, 62 | i, 63 | ); 64 | if (['选择器', '单选框'].includes(formObj[k].componentType)) { 65 | await common.setOptionModal({ page, enumObj: formObj[k].enumObj }); 66 | } 67 | i++; 68 | } 69 | 70 | // 提交 71 | await base.clickButton(page, ele, 'div div div div', '提交'); 72 | await page.waitForTimeout(1000); 73 | }; 74 | 75 | // 处理 76 | await common.tmplChange({ page, text: '弹窗编辑' }); 77 | await common.apiChange({ page, apiData }); 78 | await common.modalTitleChange({ page, apiData, text: 'XX编辑' }); 79 | await formChange(); 80 | await common.generateCode({ page }); 81 | }; 82 | 83 | module.exports = { 84 | generatePage, 85 | }; 86 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/Parser/ComponentModal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-07-03 01:17:49 4 | * @LastEditTime: 2021-07-07 00:06:16 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | import { useState } from 'react'; 9 | import { Modal, Button, Input, message } from 'antd'; 10 | import { ModuleComponents } from '../../../const/componentDSL'; 11 | import styles from './ComponentModal.less'; 12 | 13 | interface IProps { 14 | visible: boolean; 15 | handleCB?: any; 16 | } 17 | 18 | const { Search } = Input; 19 | 20 | const ComponentModal = (props: IProps) => { 21 | const { handleCB } = props; 22 | const [list, setList] = useState(ModuleComponents); 23 | const [selectedItem, setSelectedItem] = useState(null) as any; 24 | 25 | const handleSave = () => { 26 | if (!selectedItem) { 27 | message.warning('请选择组件'); 28 | return; 29 | } 30 | const com = selectedItem.componentDSL; 31 | handleCB && handleCB({ visible: false, com }); 32 | }; 33 | const handleCancel = () => { 34 | handleCB && handleCB({ visible: false }); 35 | }; 36 | 37 | const handleOnSearch = (val: any) => { 38 | if (val) { 39 | const arr = ModuleComponents.filter(({ key, name }: any) => 40 | [key, name].some((t) => t.indexOf(val) > -1), 41 | ); 42 | setList(arr); 43 | } else { 44 | setList(ModuleComponents); 45 | } 46 | }; 47 | 48 | return ( 49 | handleCancel()} 53 | onOk={() => handleSave()} 54 | footer={[ 55 | , 58 | , 61 | ]} 62 | > 63 |
64 | handleOnSearch(e.target.value)} 72 | /> 73 |
74 | {(list || []).map((item: any) => ( 75 |
setSelectedItem(item)} 84 | > 85 | 图片 91 |
92 |

{item.key}

93 |
{item.name}
94 |
95 |
96 | ))} 97 |
98 |
99 |
100 | ); 101 | }; 102 | 103 | export default ComponentModal; 104 | -------------------------------------------------------------------------------- /src/pages/setting/components/codeEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { message } from 'antd'; 3 | import Editor, { useMonaco } from '@monaco-editor/react'; 4 | import { serialize, deserialize } from '@/utils'; 5 | interface IProps { 6 | value: any; 7 | type?: 'component' | 'function' | 'html'; 8 | language?: string; 9 | height?: number; 10 | [key: string]: any; 11 | } 12 | class CodeEditor extends PureComponent { 13 | editorRef: any = null; 14 | CONFIG: any = ``; 15 | 16 | componentDidUpdate(prevProps: any) { 17 | if (this.props.value !== prevProps.value) { 18 | this.forceSetEditorValue(this.props.value); 19 | } 20 | } 21 | 22 | setEditorValue = (val: any) => { 23 | const { type } = this.props; 24 | this.CONFIG = ``; 25 | switch (type) { 26 | case 'component': 27 | this.CONFIG = `const config = `; 28 | return `${this.CONFIG}${serialize(val, { space: 2, unsafe: true })}`; 29 | case 'html': 30 | return prettierFormat(val, 'html'); 31 | case 'function': 32 | return prettierFormat(val, 'babel'); 33 | default: 34 | return val; 35 | } 36 | }; 37 | 38 | forceSetEditorValue = (val: any) => { 39 | if (this.editorRef) { 40 | this.editorRef.setValue(this.setEditorValue(val)); 41 | } 42 | }; 43 | 44 | getEditorValue = () => { 45 | const { type } = this.props; 46 | const value = this.editorRef 47 | ? this.editorRef.getValue().slice(this.CONFIG.length) 48 | : null; 49 | try { 50 | const code = type === 'component' ? deserialize(value) : value; 51 | return code; 52 | } catch (e) { 53 | message.error(`JSON 格式错误`); 54 | } 55 | }; 56 | 57 | onEditorDidMount = (editor: any, monaco: any) => { 58 | const { type } = this.props; 59 | this.editorRef = editor; 60 | if (type !== 'component') return; 61 | editor.onKeyDown((e: any) => { 62 | if (e.shiftKey) { 63 | this.editorRef && 64 | this.editorRef.trigger( 65 | 'auto completion', 66 | 'editor.action.triggerSuggest', 67 | ); 68 | } 69 | }); 70 | editor.onDidChangeCursorPosition((e: any) => { 71 | const lineCount = editor.getModel().getLineCount(); 72 | // console.log("type", type) 73 | if (type === 'component') { 74 | if (e.position.lineNumber === 1) { 75 | editor.setPosition({ 76 | lineNumber: 2, 77 | column: 1, 78 | }); 79 | } else if (e.position.lineNumber === lineCount) { 80 | editor.setPosition({ 81 | lineNumber: lineCount - 1, 82 | column: 1, 83 | }); 84 | } 85 | } 86 | }); 87 | }; 88 | 89 | render() { 90 | const { value, language = 'javascript', height } = this.props; 91 | 92 | return ( 93 | 108 | ); 109 | } 110 | } 111 | 112 | export default CodeEditor; 113 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/OptionModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Modal, Form, Input, Button, Space } from 'antd'; 3 | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; 4 | 5 | interface IProps { 6 | value: any; 7 | visible: boolean; 8 | title?: string; 9 | handleCB?: any; 10 | } 11 | 12 | const CodeModal = (props: IProps) => { 13 | const { handleCB, title = '选项设置', value = [], visible } = props; 14 | const [form] = Form.useForm(); 15 | 16 | useEffect(() => { 17 | if (visible) { 18 | form.setFieldsValue({ 19 | configs: value, 20 | }); 21 | } 22 | }, [visible]); 23 | 24 | const handleSave = () => { 25 | form 26 | .validateFields() 27 | .then((values: any) => { 28 | // console.log("values", values) 29 | const { configs } = values || {}; 30 | handleHideCB(configs); 31 | }) 32 | .catch(() => {}); 33 | gtag('event', 'handleSave', { 34 | event_category: 'CodeModal', 35 | event_label: '确定', 36 | value: 1, 37 | }); 38 | }; 39 | 40 | const handleHideCB = (configs = null) => { 41 | if (!configs) { 42 | configs = form.getFieldValue('configs'); 43 | } 44 | const list = (configs || []).filter(Boolean); 45 | handleCB && handleCB({ visible: false, list }); 46 | gtag('event', 'handleHideCB', { 47 | event_category: 'CodeModal', 48 | event_label: '关闭', 49 | value: 1, 50 | }); 51 | }; 52 | 53 | return ( 54 | handleHideCB()} 58 | onOk={() => handleSave()} 59 | footer={[ 60 | // , 63 | , 66 | , 69 | ]} 70 | > 71 |
72 | 73 | {(fields, { add, remove }) => ( 74 | <> 75 | {fields.map((field: any, i: number) => ( 76 |
77 | 78 | 85 | 86 | 87 | 93 | 94 | 95 | 96 |
105 | ))} 106 | 107 | 114 | 115 | 116 | )} 117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default CodeModal; 124 | -------------------------------------------------------------------------------- /src/pages/code/dsl.ts: -------------------------------------------------------------------------------- 1 | const DSL = { 2 | componentName: 'Page', 3 | props: {}, 4 | children: [ 5 | { 6 | componentName: 'Form', 7 | props: { 8 | 'label-width': 80, 9 | }, 10 | dataKey: 'form', 11 | children: [ 12 | { 13 | label: '姓名', 14 | key: 'trueName', 15 | initValue: 'Jack', 16 | children: [ 17 | { 18 | componentName: 'Input', 19 | props: {}, 20 | }, 21 | ], 22 | }, 23 | { 24 | label: '身份证', 25 | key: 'buyerIdCardNo', 26 | children: [ 27 | { 28 | componentName: 'Input', 29 | props: {}, 30 | }, 31 | ], 32 | }, 33 | { 34 | label: '订单号', 35 | key: 'orderNo', 36 | children: [ 37 | { 38 | componentName: 'Input', 39 | props: {}, 40 | }, 41 | ], 42 | }, 43 | { 44 | label: '', 45 | children: [ 46 | { 47 | componentName: 'Button', 48 | props: { 49 | type: 'primary', 50 | htmlType: 'submit', 51 | }, 52 | children: '搜索', 53 | onClick: `function search() { 54 | this.pagination.currentPage = 1; 55 | this.queryList(); 56 | }`, 57 | }, 58 | { 59 | componentName: 'Button', 60 | props: {}, 61 | children: '重置', 62 | onClick: `function reset() { 63 | this.pagination.currentPage = 1; 64 | this.form = {}; 65 | this.queryList(); 66 | }`, 67 | }, 68 | ], 69 | }, 70 | ], 71 | }, 72 | { 73 | componentName: 'Table', 74 | props: {}, 75 | dataKey: 'list', 76 | children: [ 77 | { 78 | key: 'id', 79 | label: '序号', 80 | minWidth: 100, 81 | }, 82 | { 83 | key: 'orderNo', 84 | label: '订单号', 85 | }, 86 | { 87 | key: 'trueName', 88 | label: '姓名', 89 | }, 90 | { 91 | key: 'buyerIdCardNo', 92 | label: '身份证号', 93 | }, 94 | { 95 | key: 'amount', 96 | label: '订单金额', 97 | }, 98 | { 99 | key: 'status', 100 | label: '校验状态', 101 | }, 102 | { 103 | key: 'createTime', 104 | label: '创建时间', 105 | }, 106 | { 107 | key: 'modifyTime', 108 | label: '修改时间', 109 | }, 110 | ], 111 | }, 112 | { 113 | componentName: 'Pagination', 114 | props: {}, 115 | onPageChange: `function handleCurrentChange(val) { 116 | this.pagination.currentPage = val; 117 | this.queryList(); 118 | }`, 119 | }, 120 | ], 121 | dataSource: { 122 | colProps: { 123 | xs: 24, 124 | sm: 12, 125 | lg: 8, 126 | xl: 8, 127 | }, 128 | pagination: { 129 | currentPage: 1, 130 | pageSize: 20, 131 | total: 0, 132 | }, 133 | }, 134 | lifeCycle: { 135 | componentDidMount: `function componentDidMount() { 136 | this.queryList(); 137 | }`, 138 | }, 139 | methods: { 140 | queryList: `function queryList() { 141 | const params = Object.assign({}, this.form, { 142 | pageSize: this.pagination.pageSize, 143 | page: this.pagination.currentPage, 144 | }) 145 | deleteEmptyParam(params) 146 | UmiRequest.request({ 147 | url: '/api/v1/h5/oversea/backend/getLimitOrder', 148 | params, 149 | }).then(res => { 150 | if (res.code === 200) { 151 | this.list = res.data.rows 152 | this.pagination.total = Number(res.data.total) 153 | } 154 | }) 155 | }`, 156 | }, 157 | imports: { 158 | '{ deleteEmptyParam }': '@/utils', 159 | UmiRequest: '@du/umi-request', 160 | }, 161 | }; 162 | 163 | export { DSL }; 164 | -------------------------------------------------------------------------------- /src/pages/setting/components/right/Parser/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0px; 3 | font-family: PingFang SC; 4 | .modal { 5 | padding: 16px; 6 | background: rgba(0, 0, 0, 0.4); 7 | position: relative; 8 | box-sizing: border-box; 9 | .modal-close { 10 | position: absolute; 11 | top: 30px; 12 | right: 36px; 13 | } 14 | .header-line { 15 | position: absolute; 16 | top: 72px; 17 | width: calc(100% - 32px); 18 | border-bottom: solid 1px #f5f5f9; 19 | } 20 | } 21 | .edit-container { 22 | .go-back { 23 | display: flex; 24 | align-items: center; 25 | i { 26 | font-size: 16px; 27 | cursor: pointer; 28 | } 29 | i:hover { 30 | color: #01c2c3; 31 | } 32 | .bread { 33 | font-size: 14px; 34 | margin-left: 8px; 35 | } 36 | } 37 | .footer-block { 38 | position: sticky; 39 | bottom: 0px; 40 | height: 64px; 41 | padding: 0 24px; 42 | background-color: #ffffff; 43 | box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15); 44 | z-index: 10; 45 | .footer-con { 46 | height: 64px; 47 | line-height: 64px; 48 | display: flex; 49 | align-items: center; 50 | justify-content: flex-end; 51 | } 52 | } 53 | .edit-content { 54 | min-height: calc(100vh - 204px); 55 | } 56 | .bb { 57 | border-bottom: solid 1px #f5f5f9; 58 | } 59 | .bb:last-child { 60 | border-bottom: none; 61 | } 62 | .f1 { 63 | flex: 1; 64 | min-width: 0; 65 | } 66 | .bshadow { 67 | border-radius: 2px; 68 | box-shadow: 0px 2px 4px 0px #0000001a; 69 | } 70 | .info-list { 71 | color: #2b2c3c; 72 | .title, 73 | span { 74 | color: #7f7f8e; 75 | } 76 | .el-col { 77 | padding: 12px 0; 78 | } 79 | } 80 | :global { 81 | .ant-col { 82 | padding: 12px 0; 83 | } 84 | .ant-form-item { 85 | margin-bottom: 18px; 86 | } 87 | .ant-form { 88 | .ant-col { 89 | padding: 0px; 90 | } 91 | } 92 | } 93 | } 94 | .detail-container { 95 | box-sizing: border-box; 96 | background-color: #fff; 97 | padding: 16px 24px; 98 | border-radius: 5px; 99 | .left { 100 | min-width: 400px; 101 | } 102 | .info-list { 103 | color: #2b2c3c; 104 | .title, 105 | span { 106 | color: #7f7f8e; 107 | } 108 | .el-col { 109 | padding: 12px 0; 110 | } 111 | } 112 | .pro-img { 113 | width: 60px; 114 | height: 60px; 115 | img { 116 | width: 100%; 117 | vertical-align: middle; 118 | } 119 | } 120 | .mt-8 { 121 | margin-top: -8px; 122 | } 123 | .bb { 124 | border-bottom: solid 1px #f5f5f9; 125 | } 126 | .bb:last-child { 127 | border-bottom: none; 128 | } 129 | .f1 { 130 | flex: 1; 131 | min-width: 0; 132 | } 133 | .bshadow { 134 | border-radius: 2px; 135 | box-shadow: 0px 2px 4px 0px #0000001a; 136 | } 137 | .lh1 { 138 | line-height: 1; 139 | } 140 | .tar { 141 | text-align: right; 142 | } 143 | section { 144 | margin-top: 36px; 145 | } 146 | :global { 147 | .ant-col { 148 | padding: 12px 0; 149 | } 150 | .ant-tag { 151 | margin-left: 6px; 152 | } 153 | .ant-form-item { 154 | margin-bottom: 18px; 155 | } 156 | .ant-form { 157 | .ant-col { 158 | padding: 0px; 159 | } 160 | } 161 | } 162 | } 163 | .page-container { 164 | box-sizing: border-box; 165 | font-family: PingFangSC-Regular; 166 | .bshadow { 167 | border-radius: 2px; 168 | box-shadow: 0px 2px 4px 0px #0000001a; 169 | } 170 | .f1 { 171 | flex: 1; 172 | min-width: 0; 173 | } 174 | .lh50 { 175 | line-height: 50px; 176 | } 177 | .tar { 178 | text-align: right; 179 | } 180 | } 181 | :global { 182 | .ant-form-item { 183 | button + button { 184 | margin-left: 8px; 185 | } 186 | } 187 | .ant-picker-range, 188 | .ant-input-number { 189 | width: 100%; 190 | } 191 | .ant-table-wrapper { 192 | overflow-x: auto; 193 | } 194 | .ant-btn-link { 195 | padding: 0px; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/auto/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-12 01:50:35 4 | * @LastEditTime: 2021-07-04 01:05:57 5 | * @LastEditors: chengtianqing 6 | */ 7 | 8 | /** 9 | * 设置选择框 10 | * @param {*} page 11 | * @param {*} ele 父节点 12 | * @param {*} classPath 路径 13 | * @param {*} val 值 14 | * @param {*} index 第几个下拉框 15 | */ 16 | async function setSelect(page, ele, classPath, val, index = 1) { 17 | let el = null, 18 | els = null; 19 | els2 = null; 20 | await (ele || page).focus(`${classPath} input`); 21 | el = await (ele || page).$(`${classPath} .ant-select-clear`); 22 | el && (await el.click()); 23 | 24 | el = await (ele || page).$(`${classPath} .ant-select-selection-search`); 25 | el && (await el.click()); 26 | 27 | await page.waitForSelector( 28 | `body > div[style="position: absolute; top: 0px; left: 0px; width: 100%;"] .ant-select-dropdown`, 29 | { timeout: 10000 }, 30 | ); 31 | 32 | els = await page.$$( 33 | `body > div[style="position: absolute; top: 0px; left: 0px; width: 100%;"]`, 34 | ); 35 | // el = await page.evaluate((els, index) => { 36 | // return els[index - 1]; 37 | // }, els, index); 38 | el = await els[index - 1]; 39 | 40 | els2 = await el.$$('.rc-virtual-list > div > div > div .ant-select-item'); 41 | // 查找对应的内容 42 | const texts = await el.$$eval( 43 | '.rc-virtual-list > div > div > div .ant-select-item > div', 44 | (node) => node.map((n) => n.innerText), 45 | ); 46 | const i = texts.findIndex((k) => k === val); 47 | 48 | await page.waitForTimeout(300); 49 | await els2[i].click(); 50 | } 51 | 52 | /** 53 | * 设置input 54 | * @param {*} page 55 | * @param {*} ele 父节点 56 | * @param {*} classPath 路径 57 | * @param {*} val 值 58 | */ 59 | async function setInput(page, ele, classPath, val = '') { 60 | const input = await (ele || page).$(`${classPath} input`); 61 | if (input) { 62 | let suf = null, 63 | icon = null; 64 | suf = await (ele || page).$(`${classPath} .ant-input-suffix`); 65 | suf && (await suf.click()); 66 | icon = await (ele || page).$( 67 | `${classPath} > .ant-input-suffix > .ant-input-clear-icon-hidden`, 68 | ); 69 | // await page.waitForSelector(`${classPath} > .ant-input-suffix > .ant-input-clear-icon-hidden`) 70 | await input.focus(); 71 | await input.type(val); 72 | } 73 | } 74 | 75 | /** 76 | * 点击按钮 77 | * @param {*} page 78 | * @param {*} ele 父节点 79 | * @param {*} classPath 路径 80 | * @param {*} btnText 按钮文字 81 | */ 82 | async function clickButton(page, ele, classPath, btnText) { 83 | const texts = await (ele || page).$$eval( 84 | `${classPath} button > span`, 85 | (node) => node.map((n) => n.innerText.replace(/\s/g, '')), 86 | ); 87 | const index = texts.findIndex((k) => k === btnText); 88 | 89 | const els = await (ele || page).$$(`${classPath} button`); 90 | await els[index].click(); 91 | } 92 | 93 | /** 94 | * 根据文本查询节点 95 | * @param {*} page 96 | * @param {*} ele 97 | * @param {*} classPath 98 | * @param {*} text 99 | * @returns 100 | */ 101 | async function findEle(page, ele, classPath, text) { 102 | const texts = await (ele || page).$$eval(`${classPath}`, (node) => 103 | node.map((n) => n.innerText.replace(/\s/g, '')), 104 | ); 105 | const index = texts.findIndex((k) => k === text); 106 | // console.log("texts", texts, index) 107 | const els = await (ele || page).$$(`${classPath}`); 108 | return els[index] || null; 109 | } 110 | 111 | /** 112 | * 点击节点 113 | * @param {*} page 114 | * @param {*} ele 父节点 115 | * @param {*} classPath 路径 116 | */ 117 | async function clickDom(page, ele, classPath, text = '') { 118 | let el = null; 119 | if (text) { 120 | el = await findEle(page, ele, classPath, text); 121 | } else { 122 | el = await (ele || page).$(`${classPath}`); 123 | } 124 | await el.click(); 125 | } 126 | 127 | /** 128 | * 点击节点 129 | * @param {*} page 130 | * @param {*} ele 父节点 131 | * @param {*} classPath 路径 132 | */ 133 | async function clickAllDom(page, ele, classPath) { 134 | const els = await (ele || page).$$(`${classPath}`); 135 | // console.log("classPath", els.length) 136 | for (let i = 0; i < els.length; i++) { 137 | await page.evaluate(async (el) => { 138 | await el.click(); 139 | }, els[i]); 140 | // await els[i].click(); 141 | await page.waitForTimeout(50); 142 | } 143 | } 144 | 145 | module.exports = { 146 | clickButton, 147 | setInput, 148 | setSelect, 149 | clickDom, 150 | clickAllDom, 151 | findEle, 152 | }; 153 | -------------------------------------------------------------------------------- /src/pages/setting/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | import { Button, message, Tooltip } from 'antd'; 3 | import { MobileOutlined, LaptopOutlined, EyeOutlined } from '@ant-design/icons'; 4 | import { Context } from '@/pages/setting/model'; 5 | import SourceCodeDrawer from '../codeEditor/SourceCodeDrawer'; 6 | import styles from './index.less'; 7 | 8 | const Header = () => { 9 | const appContext: any = useContext(Context); 10 | const [visible, setVisible] = useState(false); 11 | const [codeList, setCodeList] = useState([]); 12 | const [codeType, setCodeType] = useState(); 13 | useEffect(() => { 14 | const { sourceCode, apiCode, styleCode, codeType } = appContext.state; 15 | let list: any = []; 16 | switch (codeType) { 17 | case 'vue2': 18 | list = [ 19 | { fileName: 'index.vue', fileCode: sourceCode }, 20 | { fileName: 'api/index.js', fileCode: apiCode }, 21 | ]; 22 | break; 23 | case 'vue3': 24 | list = [ 25 | { fileName: 'index.vue', fileCode: sourceCode }, 26 | { fileName: 'api/index.js', fileCode: apiCode }, 27 | ]; 28 | break; 29 | case 'react': 30 | list = [ 31 | { fileName: 'index.jsx', fileCode: sourceCode }, 32 | { fileName: 'api/index.js', fileCode: apiCode }, 33 | { fileName: 'index.less', fileCode: styleCode }, 34 | ]; 35 | break; 36 | } 37 | if (sourceCode) { 38 | setCodeList(list); 39 | setVisible(true); 40 | setCodeType(codeType); 41 | } 42 | }, [appContext.state.showSourceCode]); 43 | 44 | const handleGenerate = (type = 'vue2') => { 45 | if (!['react', 'vue2', 'vue3'].includes(type)) return; 46 | appContext.dispatch({ 47 | type: `generate/${type}`, 48 | data: {}, 49 | }); 50 | gtag('event', 'handleGenerate', { 51 | event_category: 'Header', 52 | event_label: `生成${type}源码`, 53 | value: 1, 54 | }); 55 | }; 56 | 57 | const handleCodeCB = (obj: any) => { 58 | const { visible } = obj; 59 | setVisible(visible); 60 | }; 61 | 62 | const handleMobile = () => { 63 | message.warn('功能尚在开发中……'); 64 | gtag('event', 'handleMobile', { 65 | event_category: 'Header', 66 | event_action: '点击切换', 67 | event_label: '移动端', 68 | value: 1, 69 | }); 70 | }; 71 | const handleLaptop = () => { 72 | message.warn('功能尚在开发中……'); 73 | gtag('event', 'handleLaptop', { 74 | event_category: 'Header', 75 | event_action: '点击切换', 76 | event_label: 'pc', 77 | value: 1, 78 | }); 79 | }; 80 | const handleView = () => { 81 | message.warn('功能尚在开发中……'); 82 | gtag('event', 'handleView', { 83 | event_category: 'Header', 84 | event_label: '预览', 85 | value: 1, 86 | }); 87 | }; 88 | 89 | return ( 90 |
91 |
92 | logo 96 |
idou
97 |
98 |
99 | 104 | 109 |
110 |
111 | 112 | 118 | 119 | 126 | 133 | 140 | handleCodeCB(val)} 144 | /> 145 |
146 |
147 | ); 148 | }; 149 | 150 | export default Header; 151 | -------------------------------------------------------------------------------- /src/auto/page/common.js: -------------------------------------------------------------------------------- 1 | const base = require('../base.js'); 2 | 3 | // 处理模版变化 4 | const tmplChange = async ({ page, text }) => { 5 | let ele = null; 6 | // 打开请求tab 7 | await page.waitForSelector('#rc-tabs-0-tab-template'); 8 | ele = await page.$('#rc-tabs-0-tab-template'); 9 | ele.click(); 10 | 11 | await page.waitForSelector('#rc-tabs-0-panel-template div div img'); 12 | ele = await page.$('#rc-tabs-0-panel-template'); 13 | // 选择框 14 | await base.clickDom(page, ele, 'div div', text); 15 | await page.waitForTimeout(1000); 16 | }; 17 | 18 | // 处理请求tab 19 | const apiChange = async ({ page, apiData }) => { 20 | let ele = null; 21 | // 打开请求tab 22 | await page.waitForSelector('#rc-tabs-0-tab-request'); 23 | ele = await page.$('#rc-tabs-0-tab-request'); 24 | ele.click(); 25 | 26 | await page.waitForSelector('#rc-tabs-0-panel-request form div span input'); 27 | ele = await page.$('#rc-tabs-0-panel-request'); 28 | // 选择框 29 | await base.setSelect( 30 | page, 31 | ele, 32 | 'form > div > div > div .ant-select', 33 | apiData.method || 'POST', 34 | ); 35 | await base.setInput( 36 | page, 37 | ele, 38 | 'form > div > div > div .ant-input-affix-wrapper', 39 | `/api/v1/h5/oversea${apiData.url || ''}`, 40 | ); 41 | 42 | await base.clickButton(page, ele, 'form > div > div > div > div', '提交'); 43 | await page.waitForTimeout(1000); 44 | }; 45 | 46 | // 修改弹窗标题 47 | const modalTitleChange = async ({ page, text, apiData }) => { 48 | await page.waitForSelector("#root div[class^='modal']"); 49 | await base.clickDom(page, null, "#root div[class^='modal'] div span", text); 50 | await page.waitForSelector('#rc-tabs-0-panel-setting form div span input'); 51 | 52 | ele = await page.$('#rc-tabs-0-panel-setting'); 53 | await page.waitForTimeout(500); 54 | ele = await page.$('#rc-tabs-0-panel-setting form'); 55 | await base.setInput(page, ele, `div`, apiData.title); 56 | 57 | await base.clickButton(page, ele, 'div div div div', '提交'); 58 | await page.waitForTimeout(1000); 59 | }; 60 | 61 | // 关闭配置窗口 62 | const closeConfigModal = async ({ page }) => { 63 | await page.waitForSelector( 64 | 'body div .ant-modal .ant-modal-body .monaco-editor .view-lines', 65 | ); 66 | await base.clickButton( 67 | page, 68 | null, 69 | 'body div .ant-modal .ant-modal-footer', 70 | '取消', 71 | ); 72 | await page.waitForTimeout(300); 73 | }; 74 | 75 | // 选项设置 76 | const setOptionModal = async ({ page, enumObj }) => { 77 | await page.waitForSelector('#option_modal_form input'); 78 | // 先清空所有数据 79 | await base.clickAllDom( 80 | page, 81 | null, 82 | '#option_modal_form button .anticon-delete', 83 | ); 84 | let plusEl = await page.$(`#option_modal_form button .anticon-plus`); 85 | let i = 1; 86 | for (let k in enumObj) { 87 | await page.evaluate(async (el) => { 88 | await el.click(); 89 | }, plusEl); 90 | 91 | await page.waitForSelector(`#option_modal_form div:nth-child(${i}) input`, { 92 | timeout: 10000, 93 | }); 94 | await base.setInput( 95 | page, 96 | null, 97 | `#option_modal_form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(1)`, 98 | k, 99 | ); 100 | await base.setInput( 101 | page, 102 | null, 103 | `#option_modal_form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(2)`, 104 | enumObj[k], 105 | ); 106 | i++; 107 | await page.waitForTimeout(100); 108 | } 109 | await base.clickButton( 110 | page, 111 | null, 112 | 'body div .ant-modal .ant-modal-footer', 113 | '确定', 114 | ); 115 | await page.waitForTimeout(1000); 116 | }; 117 | 118 | const generateCode = async ({ page }) => { 119 | await base.clickButton( 120 | page, 121 | null, 122 | "#root div[class^='c-header'] div", 123 | '生成vue2源码', 124 | ); 125 | await page.waitForTimeout(2000); 126 | await page.waitForSelector( 127 | 'body div .ant-drawer .ant-drawer-body .ant-tabs-content .monaco-editor .view-lines', 128 | ); 129 | await page.waitForSelector( 130 | 'body div .ant-drawer div.ant-drawer-header button span[aria-label="download"]', 131 | ); 132 | await base.clickDom( 133 | page, 134 | null, 135 | 'body > div > div > div.ant-drawer-content-wrapper > div > div > div.ant-drawer-header > div > div > div button .anticon-download', 136 | ); 137 | await page.waitForSelector('body div .ant-modal .ant-modal-footer button'); 138 | await page.waitForTimeout(1000); 139 | await base.clickButton( 140 | page, 141 | null, 142 | 'body div .ant-modal .ant-modal-footer', 143 | '下载', 144 | ); 145 | await page.waitForTimeout(1000); 146 | }; 147 | 148 | module.exports = { 149 | apiChange, 150 | tmplChange, 151 | closeConfigModal, 152 | modalTitleChange, 153 | generateCode, 154 | setOptionModal, 155 | }; 156 | -------------------------------------------------------------------------------- /src/auto/mockApiData.js: -------------------------------------------------------------------------------- 1 | const listData = { 2 | title: '预约单列表', 3 | method: 'POST', 4 | url: '/cross/mhk/pageList', 5 | request: { 6 | applicationNo: { type: 'string', description: '预约单号', mock: [Object] }, 7 | status: { 8 | type: 'string', 9 | description: '预约单状态 0-审核中 1-成功 2-失败', 10 | mock: [Object], 11 | }, 12 | warehouseCode: { type: 'string', description: '收货地址', mock: [Object] }, 13 | createTimeStart: { 14 | type: 'string', 15 | description: '创建时间-查询起始时间', 16 | mock: [Object], 17 | }, 18 | createTimeEnd: { 19 | type: 'string', 20 | description: '创建时间-查询结束时间', 21 | mock: [Object], 22 | }, 23 | appointTimeStart: { 24 | type: 'string', 25 | description: '预约到货时间-查询起始时间', 26 | mock: [Object], 27 | }, 28 | appointTimeEnd: { 29 | type: 'string', 30 | description: '预约到货时间-查询结束时间', 31 | mock: [Object], 32 | }, 33 | userId: { type: 'number', description: '用户ID', mock: [Object] }, 34 | page: { type: 'number', description: '分页参数-第几页', mock: [Object] }, 35 | pageSize: { type: 'number', description: '分页参数-页数', mock: [Object] }, 36 | }, 37 | response: { 38 | pageNum: { type: 'number', description: '当前页', mock: [Object] }, 39 | pageSize: { type: 'number', description: '分页大小', mock: [Object] }, 40 | total: { type: 'number', description: '总元素数', mock: [Object] }, 41 | pages: { type: 'number', description: '总页数', mock: [Object] }, 42 | contents: { 43 | type: 'array', 44 | description: '数据 ,T', 45 | items: { 46 | properties: { 47 | applicationNo: { 48 | type: 'string', 49 | description: '预约单号', 50 | mock: [Object], 51 | }, 52 | status: { 53 | type: 'string', 54 | description: '预约单状态 0-审核中 1-成功 2-失败', 55 | mock: [Object], 56 | }, 57 | avgPrice: { 58 | type: 'string', 59 | description: '均价', 60 | mock: [Object], 61 | }, 62 | appointTime: { 63 | type: 'string', 64 | description: '预约到货时间', 65 | mock: [Object], 66 | }, 67 | }, 68 | }, 69 | }, 70 | extra: { 71 | type: 'object', 72 | description: '附加信息(该参数为map)', 73 | properties: [Object], 74 | }, 75 | }, 76 | }; 77 | 78 | const detailData = { 79 | title: '弹窗详情', 80 | method: 'POST', 81 | url: '/admin-growth/pop/detail', 82 | request: { id: { type: 'integer', format: 'int64', description: '弹窗id' } }, 83 | response: { 84 | endTime: { type: 'string', format: 'date-time', description: '结束时间' }, 85 | grayValue: { type: 'integer', format: 'int32', description: '灰度值' }, 86 | limitPopUpNumberFlag: { 87 | type: 'integer', 88 | format: 'int32', 89 | description: '是否限制弹窗次数', 90 | minimum: -128, 91 | maximum: 127, 92 | }, 93 | platform: { 94 | type: 'integer', 95 | format: 'int32', 96 | description: '上线平台1-android 2-ios 3-android和ios', 97 | }, 98 | popImageViewDto: { 99 | type: 'object', 100 | properties: [Object], 101 | title: '图片信息', 102 | $$ref: '#/definitions/图片信息', 103 | }, 104 | popType: { 105 | type: 'integer', 106 | format: 'int32', 107 | description: '弹窗类型 1-网页2-图片', 108 | }, 109 | popUpNumber: { type: 'integer', format: 'int32', description: '弹出次数' }, 110 | populationChoose: { 111 | type: 'integer', 112 | format: 'int32', 113 | description: '人群选择(0-全量 1-人群)-459版本新增', 114 | minimum: -128, 115 | maximum: 127, 116 | }, 117 | startTime: { type: 'string', format: 'date-time', description: '开始时间' }, 118 | }, 119 | }; 120 | 121 | const editModalData = { 122 | title: '频道创建', 123 | method: 'POST', 124 | url: '/dynamic-channels/create', 125 | request: { 126 | code: { type: 'string', description: '频道code', mock: [Object] }, 127 | name: { type: 'string', description: '频道名称', mock: [Object] }, 128 | category: { 129 | type: 'number', 130 | description: '频道种类 1-单个 2-复合', 131 | mock: [Object], 132 | }, 133 | titleLogo: { type: 'string', description: '频道标题图片', mock: [Object] }, 134 | subtitleBidType: { 135 | type: 'number', 136 | description: '频道副标题绑定类型0-人工配置 1-绑定业务', 137 | mock: [Object], 138 | }, 139 | subtitle: { type: 'string', description: '频道副标题内容', mock: [Object] }, 140 | carouselType: { 141 | type: 'number', 142 | description: 'icon图轮播类型1-单图 2-- 3-轮播多图', 143 | mock: [Object], 144 | }, 145 | covers: { 146 | type: 'array', 147 | description: 'icon图/封面图地址列表 ,String', 148 | items: [Object], 149 | }, 150 | bubbleIcon: { type: 'string', description: '气泡图地址', mock: [Object] }, 151 | bubbleIconAspectRatio: { 152 | type: 'number', 153 | description: '气泡图标宽高比 精确度小数点后2位', 154 | mock: [Object], 155 | }, 156 | rely: { 157 | type: 'number', 158 | description: '是否业务依赖 0-无业务依赖 1-有业务依赖', 159 | mock: [Object], 160 | }, 161 | type: { 162 | type: 'number', 163 | description: '支持平台,1-app 2-小程序 3-全部', 164 | mock: [Object], 165 | }, 166 | editor: { type: 'number', description: '编辑者id', mock: [Object] }, 167 | editorName: { type: 'string', description: '编辑者名称', mock: [Object] }, 168 | }, 169 | response: {}, 170 | }; 171 | 172 | module.exports = { 173 | listData, 174 | detailData, 175 | editModalData, 176 | }; 177 | -------------------------------------------------------------------------------- /src/auto/page/list.js: -------------------------------------------------------------------------------- 1 | const get = require('lodash/get'); 2 | const base = require('../base.js'); 3 | const common = require('./common.js'); 4 | 5 | const generatePage = async ({ page, apiData }) => { 6 | let ele = null; 7 | 8 | // 处理搜索框的设置tab 9 | const searchChange = async () => { 10 | // 点击搜索组件 11 | await page.waitForSelector("#root div[class^='page-container'] form"); 12 | await base.clickButton( 13 | page, 14 | null, 15 | "#root div[class^='page-container'] form", 16 | '重置', 17 | ); 18 | await page.waitForSelector('#rc-tabs-0-panel-setting form div span input'); 19 | 20 | ele = await page.$('#rc-tabs-0-panel-setting'); 21 | // 先清空所有数据 22 | await base.clickAllDom( 23 | page, 24 | ele, 25 | 'form > div > div > div > div > div div button span.anticon-delete', 26 | ); 27 | await page.waitForTimeout(500); 28 | const form = get(apiData, 'search.form'); 29 | let i = 1; 30 | let plusEl = await ele.$( 31 | `form > div > div > div > div button .anticon-plus`, 32 | ); 33 | for (let k in form) { 34 | // await plusEl.click();// 诡异有时候不会触发 35 | await page.evaluate((el) => { 36 | return el.click(); 37 | }, plusEl); 38 | await page.waitForSelector( 39 | `#dynamic_form_nest_item div:nth-child(${i}) input`, 40 | { timeout: 10000 }, 41 | ); 42 | await base.setInput( 43 | page, 44 | ele, 45 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(1)`, 46 | form[k].label, 47 | ); 48 | await base.setInput( 49 | page, 50 | ele, 51 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(2)`, 52 | k, 53 | ); 54 | console.log(i, k, form[k].label, form[k].enumObj); 55 | await base.setSelect( 56 | page, 57 | ele, 58 | `form div:nth-child(${i}) .ant-space:nth-child(2) .ant-space-item:nth-child(1)`, 59 | form[k].componentType, 60 | i, 61 | ); 62 | if (['选择器', '单选框'].includes(form[k].componentType)) { 63 | await common.setOptionModal({ page, enumObj: form[k].enumObj }); 64 | } 65 | i++; 66 | } 67 | 68 | // 提交 69 | await base.clickButton(page, ele, 'form div div div div', '提交'); 70 | await page.waitForTimeout(1000); 71 | }; 72 | 73 | // 处理表格顶部模块 74 | const operateChange = async () => { 75 | await page.waitForSelector( 76 | "#root div div div[class^='page-container'] div[class^='df'] button", 77 | ); 78 | await base.clickDom( 79 | page, 80 | null, 81 | "#root div div div[class^='page-container'] div[class^='df'] button", 82 | ); 83 | 84 | await page.waitForSelector('#rc-tabs-0-panel-setting form div span input'); 85 | await page.waitForTimeout(500); 86 | ele = await page.$('#rc-tabs-0-panel-setting form'); 87 | await base.setInput(page, ele, `div`, apiData.title); 88 | 89 | await base.clickButton(page, ele, 'div div div div', '提交'); 90 | await page.waitForTimeout(1000); 91 | }; 92 | 93 | // 处理表格列配置 94 | const tableChange = async () => { 95 | // 点击列表组件 96 | await page.waitForSelector("#root div[class^='page-container'] div table"); 97 | await base.clickDom( 98 | page, 99 | null, 100 | "#root div[class^='page-container'] div table", 101 | ); 102 | await page.waitForSelector('#rc-tabs-0-panel-setting form div span input'); 103 | await page.waitForTimeout(1000); 104 | 105 | // 先清空所有数据 106 | ele = await page.$('#rc-tabs-0-panel-setting'); 107 | // 先清空所有数据 108 | await base.clickAllDom( 109 | page, 110 | ele, 111 | 'form div div button span.anticon-delete', 112 | ); 113 | await page.waitForTimeout(500); 114 | let columnsObj = get(apiData, 'columnsObj'); 115 | columnsObj = Object.assign(columnsObj, { 116 | '-': { 117 | label: '操作', 118 | componentType: '操作', 119 | }, 120 | }); 121 | let i = 1; 122 | let plusEl = await ele.$( 123 | `form > div > div > div > div button .anticon-plus`, 124 | ); 125 | for (let k in columnsObj) { 126 | // await plusEl.click();// 诡异有时候不会触发 127 | await page.evaluate((el) => { 128 | return el.click(); 129 | }, plusEl); 130 | await page.waitForSelector( 131 | `#dynamic_form_nest_item div:nth-child(${i}) input`, 132 | { timeout: 10000 }, 133 | ); 134 | await base.setInput( 135 | page, 136 | ele, 137 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(1)`, 138 | columnsObj[k].label, 139 | ); 140 | await base.setInput( 141 | page, 142 | ele, 143 | `form div:nth-child(${i}) .ant-space:nth-child(1) .ant-space-item:nth-child(2)`, 144 | k, 145 | ); 146 | console.log(i, k, columnsObj[k].label, columnsObj[k].enumObj); 147 | await base.setSelect( 148 | page, 149 | ele, 150 | `form div:nth-child(${i}) .ant-space:nth-child(2) .ant-space-item:nth-child(1)`, 151 | columnsObj[k].componentType, 152 | i, 153 | ); 154 | if (['状态'].includes(columnsObj[k].componentType)) { 155 | await common.setOptionModal({ page, enumObj: columnsObj[k].enumObj }); 156 | } 157 | i++; 158 | } 159 | 160 | // 提交 161 | await base.clickButton(page, ele, 'div div div div', '提交'); 162 | await page.waitForTimeout(1000); 163 | }; 164 | 165 | // 处理 166 | await common.tmplChange({ page, text: '管理列表' }); 167 | await common.apiChange({ page, apiData }); 168 | await searchChange(); 169 | await operateChange(); 170 | await tableChange(); 171 | await common.generateCode({ page }); 172 | }; 173 | 174 | module.exports = { 175 | generatePage, 176 | }; 177 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | import { Tabs, message } from 'antd'; 3 | import { templates, tabs } from '../../const'; 4 | import { Context } from '@/pages/setting/model'; 5 | import { ModuleComponents } from '../../const/componentDSL'; 6 | import Setting from './Setting'; 7 | import Request from './Request'; 8 | import UILib from './UILib'; 9 | import styles from './index.less'; 10 | 11 | const { TabPane } = Tabs; 12 | 13 | const Left = () => { 14 | const [tab, setTab] = useState('template'); 15 | const [tmpl, setTmpl] = useState('list'); 16 | const [selectedComponent, setSelectedComponent]: any = useState(null); 17 | const appContext: any = useContext(Context); 18 | useEffect(() => { 19 | setSelectedComponent(appContext.state.selectedComponent); 20 | if (appContext.state.selectedComponent) { 21 | setTab('setting'); 22 | } 23 | }, [appContext.state.selectedComponent]); 24 | useEffect(() => { 25 | setTmpl(appContext.state.dslType); 26 | }, [appContext.state.dslType]); 27 | 28 | const handleSettingCB = (com: any) => { 29 | const { from } = selectedComponent || {}; 30 | console.log('handleSettingCB com', com); 31 | if (!from) return; 32 | if (com) { 33 | appContext.dispatch({ 34 | type: 'component/replace', 35 | data: { 36 | component: com, 37 | from, 38 | }, 39 | }); 40 | appContext.dispatch({ 41 | type: 'component/selected', 42 | data: { 43 | component: com, 44 | from, 45 | }, 46 | }); 47 | } 48 | }; 49 | 50 | const selectTemplate = (item: any) => { 51 | appContext.dispatch({ 52 | type: 'dsl/type/update', 53 | data: { 54 | dslType: item.code, 55 | }, 56 | }); 57 | gtag('event', 'selectTemplate', { 58 | event_category: 'Left', 59 | event_label: `${item.label}`, 60 | value: 1, 61 | }); 62 | }; 63 | 64 | const handleRequestCB = (apis: any) => { 65 | if (apis) { 66 | appContext.dispatch({ 67 | type: 'dsl/apis/update', 68 | data: { 69 | apis, 70 | }, 71 | }); 72 | } 73 | }; 74 | 75 | const handleUICB = (data: any) => { 76 | appContext.dispatch({ 77 | type: 'dsl/uilib/update', 78 | data, 79 | }); 80 | }; 81 | 82 | const addComponent = (item: any) => { 83 | // const { index, parentUuid, item } = selectedComponent || {} 84 | // TODO 处理点击事件 85 | message.warn('功能尚在开发中……'); 86 | gtag('event', 'addComponent', { 87 | event_category: 'Left', 88 | event_label: `${item.name}`, 89 | value: 1, 90 | }); 91 | }; 92 | 93 | const generateTabPane = () => { 94 | switch (tab) { 95 | case 'template': 96 | return ( 97 |
98 | {(templates || []).map((item, i) => ( 99 |
selectTemplate(item)} 108 | > 109 | 图片 110 |
{item.label}
111 |
112 | ))} 113 |
114 | ); 115 | case 'component': 116 | return ( 117 |
118 | {(ModuleComponents || []).map((item: any, i: number) => ( 119 |
addComponent(item)} 124 | > 125 | 图片 131 |
132 |

{item.key}

133 |
{item.name}
134 |
135 |
136 | ))} 137 |
138 | ); 139 | case 'setting': 140 | const { component = {} } = selectedComponent || {}; 141 | return ( 142 | handleSettingCB(com)} 146 | /> 147 | ); 148 | case 'request': 149 | return ( 150 | handleRequestCB(apis)} 153 | /> 154 | ); 155 | case 'uilib': 156 | return ( 157 | handleUICB(data)} 161 | /> 162 | ); 163 | default: 164 | return ''; 165 | } 166 | }; 167 | 168 | return ( 169 |
170 |
171 |
172 | setTab(k)}> 173 | {(tabs || []).map((item, i) => ( 174 | 175 | {generateTabPane()} 176 | 177 | ))} 178 | 179 |
180 |
181 | ); 182 | }; 183 | 184 | export default Left; 185 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-05-21 01:19:08 4 | * @LastEditTime: 2021-07-19 22:44:38 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | import serialize from 'serialize-javascript'; 9 | 10 | /** 11 | * 获取域 12 | * @param type 13 | * @returns 14 | */ 15 | export const getDomain = (type = 1) => { 16 | switch (type) { 17 | case 1: 18 | return 'P.O.I.Z.O.N'.split('.').join('').toLowerCase(); 19 | case 2: 20 | return 'S.H.I.Z.H.U.A.N.G.-.I.N.C'.split('.').join('').toLowerCase(); 21 | default: 22 | return ''; 23 | } 24 | }; 25 | 26 | /** 27 | * 获取唯一值 28 | * @returns 29 | */ 30 | export const getUid = () => Math.random().toString(36).slice(-8); 31 | 32 | /** 33 | * 由字符串转换成对象 34 | * @param code 35 | * @returns 36 | */ 37 | export const deserialize = (code: any) => { 38 | return eval('(' + code + ')'); 39 | }; 40 | 41 | /** 42 | * 获取function 43 | * @param func 44 | * @param newFuncName 45 | * @returns 46 | */ 47 | export const transformFunc = (func: any, newFuncName = '') => { 48 | const funcStr = func.toString(); 49 | const funcStartIndex = funcStr.indexOf('function ') + 9; 50 | const paramsStartIndex = funcStr.indexOf('('); 51 | const paramsEndIndex = funcStr.indexOf(')'); 52 | const funcName = funcStr.slice(funcStartIndex, paramsStartIndex); 53 | const params = funcStr.slice(paramsStartIndex, paramsEndIndex + 1); 54 | let newFunc = funcStr.replace('function ', ''); 55 | let funcBodyStart = funcStr.indexOf('{') + 1; 56 | let funcBodyEnd = funcStr.lastIndexOf('}'); 57 | let funcBody = funcStr.slice(funcBodyStart, funcBodyEnd); 58 | if (newFuncName) { 59 | newFunc = newFunc.replace(funcName, newFuncName); 60 | } 61 | return { newFunc, newFuncName: newFuncName || funcName, params, funcBody }; 62 | }; 63 | 64 | /** 65 | * 替换对象属性 66 | * @param obj 对象 67 | * @param oldKey 老属性 68 | * @param newKey 新属性 69 | */ 70 | export const replaceObjKey = (obj: any, oldKey: any, newKey: any) => { 71 | Object.keys(obj).forEach((k) => { 72 | if (k === oldKey) { 73 | obj[newKey] = obj[k]; 74 | delete obj[k]; 75 | } 76 | }); 77 | }; 78 | 79 | /** 80 | * 替换字符串 81 | * @param s 82 | * @param fromReg 匹配的正则表达式 83 | * @param toStr 要替换的函数或字符串 84 | * @returns 85 | */ 86 | export const replaceStr = (s: any, fromReg: any, toStr: any) => { 87 | return [undefined, null].includes(s) 88 | ? s 89 | : s.toString().replace(fromReg, toStr); 90 | }; 91 | 92 | /** 93 | * 根据class名称生成css样式 94 | * @param cls class名称 95 | * @returns 96 | */ 97 | export const generateClassStyle = (cls: string) => { 98 | let style = ''; 99 | cls = cls.trim(); 100 | if (cls) { 101 | const spacePre: any = { 102 | p: 'padding', 103 | m: 'margin', 104 | h: 'height', 105 | w: 'width', 106 | }; 107 | const spaceFix: any = { 108 | t: '-top', 109 | b: '-bottom', 110 | l: '-left', 111 | r: '-right', 112 | }; 113 | const fontPre: any = { 114 | fs: 'font-size', 115 | fw: 'font-weight', 116 | }; 117 | const flex: any = { 118 | df: 'display: flex', 119 | jcsb: 'justify-content: space-between', 120 | aic: 'align-items: center', 121 | jcfe: 'justify-content: flex-end', 122 | }; 123 | const colorPre: any = { 124 | c: 'color', 125 | bgc: 'background-color', 126 | }; 127 | 128 | const getStyle = (reg: any, str: any, pre: any, fix: any) => { 129 | const ex = reg.exec(str); 130 | if (ex) { 131 | if (ex.length === 3) { 132 | if (ex[1] === 'fw') { 133 | return `${pre[ex[1]]}: ${ex[2]}`; 134 | } 135 | return `${pre[ex[1]]}: ${ex[2]}px`; 136 | } 137 | if (ex.length === 4) { 138 | if (ex[2] === '-') { 139 | return `${pre[ex[1]]}: ${ex[3]}%`; 140 | } 141 | if (fix[ex[2]]) { 142 | return `${pre[ex[1]]}${fix[ex[2]]}: ${ex[3]}px`; 143 | } 144 | } 145 | } 146 | return ''; 147 | }; 148 | const getColorStyle = (reg: any, str: any, pre: any) => { 149 | const ex = reg.exec(str); 150 | if (ex) { 151 | if (ex.length === 3) { 152 | if (/^([0-9]|[A-F]|[0-9A-F])+$/gi.test(ex[2])) { 153 | return `${pre[ex[1]]}: #${ex[2]}`; 154 | } 155 | return `${pre[ex[1]]}: ${ex[2]}`; 156 | } 157 | } 158 | return ''; 159 | }; 160 | // 处理flex布局 161 | if (flex[cls]) return flex[cls]; 162 | 163 | // 处理内外间距宽高值,w100表示width: 100px 164 | style = getStyle(/^(p|m|h|w)(\d+)$/, cls, spacePre, spaceFix); 165 | if (style) return style; 166 | 167 | // 处理百分比,w-100表示width: 100% 168 | style = getStyle(/^(p|m|h|w)(-)(\d+)$/, cls, spacePre, spaceFix); 169 | if (style) return style; 170 | 171 | // 处理单一内外间距,pr100表示padding-right: 100px 172 | style = getStyle(/^(p|m)(r|l|t|b)(\d+)$/, cls, spacePre, spaceFix); 173 | if (style) return style; 174 | 175 | // 处理字体,fs16表示font-size: 16px 176 | style = getStyle(/^(fw|fs)(\d+)$/, cls, fontPre, spaceFix); 177 | if (style) return style; 178 | 179 | // 处理颜色,c-fff表示color: #fff 180 | style = getColorStyle(/^(c|bgc)-(\w+)$/, cls, colorPre); 181 | if (style) return style; 182 | } 183 | return style; 184 | }; 185 | 186 | /** 187 | * 中划线转驼峰 188 | * @param s 189 | */ 190 | export const toHump = (s: string) => { 191 | return s.replace(/\-(\w)/g, (_, c) => c && c.toUpperCase()); 192 | }; 193 | 194 | /** 195 | * 驼峰转中划线 196 | * @param s 197 | */ 198 | export const toLine = (s: string) => { 199 | return s.replace(/([A-Z])/g, '-$1').toLowerCase(); 200 | }; 201 | 202 | /** 203 | * 处理JSON.stringify 204 | * @param s 205 | * @returns 206 | */ 207 | export const JSONtoString = (s: any) => { 208 | return JSON.stringify(s, function (k, v) { 209 | if (typeof v === 'function') { 210 | return v.toString(); 211 | } 212 | return v; 213 | }); 214 | }; 215 | 216 | // export const JSONtoParse = (s: any) => { 217 | // return JSON.parse(s, function (k, v) { 218 | // if (v && v.toString().indexOf('function') > -1) { 219 | // return v.toString().replace(/\"/g, ''); 220 | // } 221 | // return v; 222 | // }); 223 | // } 224 | 225 | /** 226 | * 对象转换成字符串,function,date,reg不出现丢失 227 | */ 228 | export { serialize }; 229 | -------------------------------------------------------------------------------- /src/pages/setting/components/left/Request.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Form, Input, Button, Select, message } from 'antd'; 3 | import CodeDrawer from '../codeEditor/CodeDrawer'; 4 | 5 | const methods = ['GET', 'POST', 'PUT', 'DELETE']; 6 | const Option = Select.Option; 7 | 8 | const Request = (props: any) => { 9 | const { apis } = props.dsl || {}; 10 | const [visible, setVisible]: any = useState(false); 11 | const [codeStr, setCodeStr]: any = useState(null); 12 | const [form] = Form.useForm(); 13 | 14 | useEffect(() => { 15 | if (apis) { 16 | let method = 'POST', 17 | url = ''; 18 | let codeStr = Object.keys(apis).reduce((pre, cur): any => { 19 | let str = ''; 20 | if (cur === 'imports') { 21 | str = Object.entries(apis.imports) 22 | .map(([k, v]) => `import ${k} from "${v}";`) 23 | .join('\n'); 24 | str += '\n'; 25 | } else { 26 | str = `${apis[cur]}`; 27 | if (!url) { 28 | let s = str; 29 | let [, urlstr] = s.split('url:'); 30 | if (urlstr) { 31 | urlstr = urlstr.substr(0, urlstr.indexOf(',')) || ''; 32 | urlstr = urlstr.replace(/['"]/g, ''); 33 | url = urlstr.trim(); 34 | } 35 | let [, m] = s.split('method:'); 36 | if (m) { 37 | m = m.substr(0, m.indexOf(',')) || ''; 38 | m = m.replace(/['"]/g, ''); 39 | if (m.trim()) { 40 | method = m.trim().toLocaleUpperCase(); 41 | } 42 | } 43 | } 44 | } 45 | return pre + str + '\n'; 46 | }, ''); 47 | form.setFieldsValue({ 48 | method, 49 | url, 50 | }); 51 | setCodeStr(codeStr); 52 | } 53 | }, [apis]); 54 | 55 | const onFinish = (values: any) => { 56 | console.log('Success:', values); 57 | const newApis = { ...apis }; 58 | // 提取第一个api函数字符串,并进行更换其中的url和method方法 59 | const tKey = Object.keys(newApis).find((k) => k !== 'imports') || ''; 60 | let target = newApis[tKey]; 61 | if (target && target.indexOf('url:') > -1) { 62 | // 存在url才认为合法 63 | let s = target; 64 | let urlPositon = [], 65 | methodPosition = []; 66 | urlPositon.push(s.indexOf('url:') + 4); 67 | let [, url2] = s.split('url:'); 68 | urlPositon.push(url2.indexOf(',')); 69 | let oldUrl = s.substr(urlPositon[0], urlPositon[1]); 70 | let oldMethod = ''; 71 | if (s.indexOf('method:')) { 72 | // 存在method字段 73 | methodPosition.push(s.indexOf('method:') + 7); 74 | let [, m2] = s.split('method:'); 75 | methodPosition.push(m2.indexOf(',')); 76 | oldMethod = s.substr(methodPosition[0], methodPosition[1]); 77 | } 78 | oldUrl = oldUrl.trim().replace(/['"]/g, ''); 79 | oldMethod = oldMethod.trim().replace(/['"]/g, ''); 80 | target = s.replace(oldUrl, values.url); 81 | if (oldMethod) { 82 | target = target.replace(oldMethod, values.method); 83 | } else { 84 | // 不存在method字段,直接插入 85 | target = 86 | target.slice(0, urlPositon[0]) + 87 | `method: ${values.method},\n` + 88 | s.slice(urlPositon[0]); 89 | } 90 | // console.log("target", target) 91 | newApis[tKey] = target; 92 | } 93 | props.handleCB && props.handleCB(newApis); 94 | message.success('更新成功!'); 95 | }; 96 | 97 | const handleApisCodeCB = (obj: any) => { 98 | const { visible, code } = obj; 99 | let newCode = code; 100 | setVisible(visible); 101 | if (newCode && newCode.trim()) { 102 | let apis: any = {}; 103 | newCode.split('function').forEach((item: any, i: number) => { 104 | if (i === 0) { 105 | let imports: any = {}; 106 | (item || '').split('\n').forEach((line: any) => { 107 | if ( 108 | line && 109 | line.indexOf('import') > -1 && 110 | line.indexOf('from') > -1 111 | ) { 112 | line = line.substr(line.indexOf('import') + 6); 113 | let [k, v] = line 114 | .split('from') 115 | .map((s: any) => (s ? s.trim() : '')) 116 | .filter(Boolean); 117 | if (v) { 118 | v = v.replace(/['";]/g, ''); 119 | } 120 | imports[k] = v; 121 | } 122 | }); 123 | apis.imports = imports; 124 | } else { 125 | let funcName = 126 | item.substr(0, item.indexOf('(')).trim() || `funcName${i}`; 127 | apis[funcName] = `function${item}`; 128 | } 129 | }); 130 | // console.log("apis", apis) 131 | props.handleCB && props.handleCB(apis); 132 | message.success('保存成功!'); 133 | } 134 | }; 135 | 136 | const layout = { 137 | labelCol: { span: 0 }, 138 | wrapperCol: { span: 24 }, 139 | }; 140 | 141 | return ( 142 | <> 143 |
144 | 148 | 155 | 156 | 157 | 161 | 162 | 163 | 164 | 165 | 168 | 177 | 178 |
179 | handleApisCodeCB(val)} 184 | /> 185 | 186 | ); 187 | }; 188 | 189 | export default Request; 190 | -------------------------------------------------------------------------------- /src/pages/setting/components/codeEditor/SourceCodeDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | Button, 4 | Drawer, 5 | Tooltip, 6 | message, 7 | Modal, 8 | Form, 9 | Input, 10 | Tabs, 11 | } from 'antd'; 12 | import { CopyOutlined, DownloadOutlined } from '@ant-design/icons'; 13 | import copy from 'copy-to-clipboard'; 14 | import JSZIP from 'jszip'; 15 | import CodeEditor from './index'; 16 | import { serialize, deserialize } from '@/utils'; 17 | import { saveAs } from 'file-saver'; 18 | import styles from './CodeDrawer.less'; 19 | interface IProps { 20 | valueList: any[]; 21 | visible: boolean; 22 | handleCB?: any; 23 | } 24 | 25 | const { TabPane } = Tabs; 26 | const SourceCodeDrawer = (props: IProps) => { 27 | const { valueList = [], handleCB } = props; 28 | const [tab, setTab] = useState('0'); 29 | const [modalVisible, setModalVisible] = useState(false); 30 | // const [monacoLanguage, setMonacoLanguage] = useState('javascript') 31 | const codeRef: any = React.useRef(null); 32 | const [form] = Form.useForm(); 33 | 34 | useEffect(() => { 35 | if (props.visible) { 36 | setTab('0'); 37 | } 38 | }, [props.visible]); 39 | 40 | const onClose = () => { 41 | handleCB && handleCB({ visible: false }); 42 | gtag('event', 'onClose', { 43 | event_category: 'SourceCodeDrawer', 44 | event_label: `关闭源码`, 45 | value: 1, 46 | }); 47 | }; 48 | 49 | const handleCopy = () => { 50 | try { 51 | const code = codeRef.current.getEditorValue(); 52 | // console.log('code', code); 53 | const val = serialize(code, { space: 2 }); 54 | copy(deserialize(val)); 55 | message.success('复制成功'); 56 | } catch (e) { 57 | message.error('复制异常'); 58 | console.error(e); 59 | } 60 | gtag('event', 'handleCopy', { 61 | event_category: 'SourceCodeDrawer', 62 | event_label: `复制源码`, 63 | value: 1, 64 | }); 65 | }; 66 | 67 | const handleDown = () => { 68 | form.resetFields(); 69 | setModalVisible(true); 70 | gtag('event', 'handleDown', { 71 | event_category: 'SourceCodeDrawer', 72 | event_label: `显示源码文件夹设置`, 73 | value: 1, 74 | }); 75 | }; 76 | 77 | const handleDownloadCode = (folderName: string, cb: any) => { 78 | try { 79 | const code = codeRef.current.getEditorValue(); 80 | const val = serialize(code, { space: 2 }); 81 | const str = deserialize(val); 82 | 83 | const zip = new JSZIP(); 84 | let fold: any = zip.folder(folderName); 85 | valueList.forEach((item, i) => { 86 | if (i === Number(tab)) { 87 | fold.file(item.fileName, str); 88 | } else { 89 | fold.file(item.fileName, item.fileCode); 90 | } 91 | }); 92 | // fold.file('index.vue', str); 93 | zip.generateAsync({ type: 'blob' }).then(function (content: any) { 94 | saveAs(content, 'code.zip'); 95 | cb && cb(); 96 | }); 97 | } catch (e) { 98 | message.error('下载异常'); 99 | console.error(e); 100 | } 101 | gtag('event', 'onFinish', { 102 | event_category: 'SourceCodeDrawer', 103 | event_label: `下载源码`, 104 | value: 1, 105 | }); 106 | }; 107 | 108 | const onFinish = async () => { 109 | const values = await form.validateFields(); 110 | // values 111 | handleDownloadCode(values.folderName, () => setModalVisible(false)); 112 | }; 113 | 114 | const titleNode = () => ( 115 |
116 |
源码
117 |
118 | 119 | 125 | 126 | 127 | 133 | 134 |
135 |
136 | ); 137 | 138 | const generateTab = () => { 139 | return (valueList || []).map((item, i) => { 140 | const { fileName = '', fileCode = '' } = item || {}; 141 | const pointerIndex = fileName.lastIndexOf('.'); 142 | const fileType = fileName.substring(pointerIndex + 1); 143 | let language; 144 | switch (fileType) { 145 | case 'vue': 146 | language = 'html'; 147 | break; 148 | case 'less': 149 | language = 'less'; 150 | break; 151 | default: 152 | language = 'javascript'; 153 | break; 154 | } 155 | return ( 156 | 157 | (codeRef.current = ref)} 161 | /> 162 | 163 | ); 164 | }); 165 | }; 166 | 167 | return ( 168 | 178 | setTab(k)}> 179 | {generateTab()} 180 | 181 | 182 | setModalVisible(false)} 186 | footer={[ 187 | , 196 | , 205 | ]} 206 | > 207 |
208 | 219 | 224 | 225 |
226 |
227 |
228 | ); 229 | }; 230 | 231 | export default SourceCodeDrawer; 232 | -------------------------------------------------------------------------------- /src/pages/setting/model/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cloneDeep from 'lodash/cloneDeep'; 3 | // import { DSL } from '../const/dsl'; 4 | import { DSL as ListDSL } from '../const/listDSL'; 5 | import { DSL as DetailDSL } from '../const/detailDSL'; 6 | import { DSL as EditModalDSL } from '../const/editModalDSL'; 7 | import { DSL as EditDSL } from '../const/editDSL'; 8 | import { getUid } from '@/utils'; 9 | import { getSourceCode as generateReactCode } from './generateReact'; 10 | import { getSourceCode as generateVueCode } from './generateVue'; 11 | import { getSourceCode as generateVue3Code } from './generateVue3'; 12 | import { VueTableRenderXML } from './componentXML'; 13 | interface IObject { 14 | [key: string]: any; 15 | } 16 | 17 | interface Position { 18 | index: number; 19 | uuid: string; 20 | } 21 | 22 | /** 23 | * 编辑数组 24 | * @param arr 25 | * @param obj 26 | * @param index 27 | * @param type 28 | * @returns 29 | */ 30 | const editArray = ( 31 | arr: any, 32 | obj: IObject | null, 33 | index: number, 34 | type: string, 35 | ) => { 36 | let target = null; 37 | if (Array.isArray(arr)) { 38 | switch (type) { 39 | case 'add': 40 | [target] = arr.splice(index, 0, obj); 41 | break; 42 | case 'delete': 43 | [target] = arr.splice(index, 1); 44 | break; 45 | case 'replace': 46 | [target] = arr.splice(index, 1, obj); 47 | break; 48 | default: 49 | break; 50 | } 51 | } 52 | return target; 53 | }; 54 | 55 | /** 56 | * 递归处理数组 57 | * @param list 58 | * @param obj 59 | * @param position 60 | * @param type 61 | * @returns 62 | */ 63 | const handleTarget = ( 64 | list: any, 65 | obj: IObject, 66 | position: Position, 67 | type: string, 68 | ): any => { 69 | // 递归处理 70 | if (Array.isArray(list) && position) { 71 | const parent = list.find(({ uuid }) => uuid === position.uuid); 72 | if (parent) { 73 | return editArray(parent.children, obj, position.index, type); 74 | } 75 | // BFS广度优先遍历 76 | for (let i = 0; i < list.length; i++) { 77 | if (list[i]) { 78 | const target = handleTarget(list[i].children, obj, position, type); 79 | if (target) return target; 80 | } 81 | } 82 | } 83 | return null; 84 | }; 85 | 86 | /** 87 | * 处理dsl的component变更 88 | * @param dsl 89 | * @param component 90 | * @param position 91 | * @param type 92 | * @returns 93 | */ 94 | const handleDSLComponentChange = ( 95 | dsl: IObject, 96 | component: IObject, 97 | position: Position, 98 | type: string, 99 | ) => { 100 | const newDSL = cloneDeep(dsl); 101 | const newList = [newDSL]; 102 | const target = handleTarget(newList, component, position, type); 103 | return { newDSL, target }; 104 | }; 105 | 106 | /** 107 | * 为dsl添加uuid 108 | * @param data 109 | * @returns 110 | */ 111 | const addUuid = (data: IObject) => { 112 | const copyData = cloneDeep(data); 113 | const addDataUuid = (data: IObject) => { 114 | if (data) { 115 | if (Array.isArray(data.children)) { 116 | data.uuid = getUid(); 117 | data.children.forEach((item) => addDataUuid(item)); 118 | } else if (data.isEdit) { 119 | data.uuid = getUid(); 120 | } 121 | } 122 | }; 123 | addDataUuid(copyData); 124 | return copyData; 125 | }; 126 | 127 | const initState = { 128 | dsl: addUuid(ListDSL), 129 | selectedComponent: null, 130 | sourceCode: null, 131 | apiCode: null, 132 | styleCode: null, 133 | codeType: 'vue2', 134 | prefixUI: 'el', 135 | showSourceCode: false, 136 | dslType: 'list', 137 | VueTableRenderXML, 138 | }; 139 | 140 | const reducer = (state: any, action: any) => { 141 | const { type, data } = action; 142 | const { component, from, to, apis } = data || {}; 143 | console.log('data', data); 144 | switch (type) { 145 | case 'component/add': 146 | const newComponent = addUuid(component); 147 | const { newDSL: addDSL } = handleDSLComponentChange( 148 | state.dsl, 149 | newComponent, 150 | to, 151 | 'add', 152 | ); 153 | return { ...state, dsl: addDSL }; 154 | case 'component/replace': 155 | const { newDSL: replaceDSL } = handleDSLComponentChange( 156 | state.dsl, 157 | component, 158 | from, 159 | 'replace', 160 | ); 161 | return { ...state, dsl: replaceDSL }; 162 | case 'component/delete': 163 | const { newDSL: deleteDSL } = handleDSLComponentChange( 164 | state.dsl, 165 | component, 166 | from, 167 | 'delete', 168 | ); 169 | return { ...state, dsl: deleteDSL }; 170 | case 'component/move': 171 | const { newDSL: moveDSL1, target } = handleDSLComponentChange( 172 | state.dsl, 173 | component, 174 | from, 175 | 'delete', 176 | ); 177 | const { newDSL: moveDSL2 } = handleDSLComponentChange( 178 | moveDSL1, 179 | target, 180 | to, 181 | 'add', 182 | ); 183 | return { ...state, dsl: moveDSL2 }; 184 | case 'component/selected': 185 | return { ...state, selectedComponent: data }; 186 | case 'generate/vue2': 187 | const vue2DSL = cloneDeep(state.dsl); 188 | const { vue2Code, vue2ApiCode } = 189 | generateVueCode(vue2DSL, state.prefixUI) || {}; 190 | // console.log('vueCode', vueCode); 191 | if (!vue2Code) return { ...state }; 192 | return { 193 | ...state, 194 | sourceCode: vue2Code, 195 | apiCode: vue2ApiCode, 196 | showSourceCode: !state.showSourceCode, 197 | codeType: 'vue2', 198 | }; 199 | case 'generate/vue3': 200 | const vue3DSL = cloneDeep(state.dsl); 201 | const { vue3Code, vue3ApiCode } = 202 | generateVue3Code(vue3DSL, state.prefixUI) || {}; 203 | // console.log('vue3Code', vue3Code); 204 | if (!vue3Code) return { ...state }; 205 | return { 206 | ...state, 207 | sourceCode: vue3Code, 208 | apiCode: vue3ApiCode, 209 | showSourceCode: !state.showSourceCode, 210 | codeType: 'vue3', 211 | }; 212 | case 'generate/react': 213 | const reactDSL = cloneDeep(state.dsl); 214 | const { reactCode, reactApiCode, styleCode } = 215 | generateReactCode(reactDSL, state.prefixUI) || {}; 216 | // console.log('reactCode', reactCode); 217 | if (!reactCode) return { ...state }; 218 | return { 219 | ...state, 220 | sourceCode: reactCode, 221 | apiCode: reactApiCode, 222 | styleCode, 223 | showSourceCode: !state.showSourceCode, 224 | codeType: 'react', 225 | }; 226 | case 'dsl/apis/update': 227 | const newDSL2 = cloneDeep(state.dsl); 228 | newDSL2.apis = apis; 229 | return { ...state, dsl: newDSL2 }; 230 | case 'dsl/uilib/update': 231 | return { ...state, codeType: data.codeType, prefixUI: data.prefixUI }; 232 | case 'dsl/type/update': 233 | let newTypeDSL = null; 234 | switch (data.dslType) { 235 | case 'detail': 236 | newTypeDSL = DetailDSL; 237 | break; 238 | case 'editModal': 239 | newTypeDSL = EditModalDSL; 240 | break; 241 | case 'edit': 242 | newTypeDSL = EditDSL; 243 | break; 244 | case 'list': 245 | default: 246 | newTypeDSL = ListDSL; 247 | break; 248 | } 249 | return { 250 | ...state, 251 | dslType: data.dslType, 252 | dsl: addUuid(newTypeDSL), 253 | selectedComponent: null, 254 | }; 255 | default: 256 | return state; 257 | } 258 | }; 259 | 260 | const Context = React.createContext(null); 261 | 262 | export { initState, reducer, Context }; 263 | -------------------------------------------------------------------------------- /src/auto/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-12 00:58:07 4 | * @LastEditTime: 2021-07-06 23:54:25 5 | * @LastEditors: chengtianqing 6 | * @备注: 已检查,无账号密码信息 7 | */ 8 | 9 | const puppeteer = require('puppeteer'); 10 | const get = require('lodash/get'); 11 | const cloneDeep = require('lodash/cloneDeep'); 12 | const transform = require('./transform.js'); 13 | const pageList = require('./page/list.js'); 14 | const pageDetail = require('./page/detail.js'); 15 | const pageEditModal = require('./page/editModal.js'); 16 | const mockApiData = require('./mockApiData.js'); 17 | const { ListContext } = require('antd/lib/list'); 18 | const domain = 'S.H.I.Z.H.U.A.N.G.-.I.N.C'.split('.').join('').toLowerCase(); 19 | // const mockUrl = `https://mock.${domain}.com/project/2492/interface/api/140446`;// 详情 20 | // const mockUrl = `https://mock.${domain}.com/project/2492/interface/api/140456`; // 编辑 21 | // const mockUrl = `https://mock.${domain}.com/project/2492/interface/api/140494`;// 列表 22 | // const mockUrl = `https://mock.${domain}.com/project/2492/interface/api/140436`;// 简单列表 23 | const mockUrl = `https://mock.${domain}.com/project/2492/interface/api/140436`; 24 | const platformUrl = `https://idou100.netlify.app`; 25 | // const platformUrl = `http://localhost:8000/setting`; 26 | let apiData = {}; 27 | let browser = null; 28 | 29 | const getBrowser = async () => { 30 | if (!browser) { 31 | browser = await puppeteer.launch({ 32 | headless: false, //有浏览器界面启动 33 | args: [`--window-size=1350,800`], 34 | }); 35 | } 36 | return browser; 37 | }; 38 | 39 | const autoEditPage = async () => { 40 | await getBrowser(); 41 | const page = await browser.newPage(); 42 | if (!apiData.componentType) { 43 | console.log('接口匹配不到页面类型,退出'); 44 | page.close(); 45 | browser.close(); 46 | return; 47 | } 48 | await page.setViewport({ width: 1350, height: 800 }); 49 | const navigationPromise = page.waitForNavigation(); 50 | await page.goto(platformUrl); 51 | await navigationPromise; 52 | await page.waitForTimeout(1 * 1000); 53 | switch (apiData.componentType) { 54 | case 'list': 55 | await pageList.generatePage({ page, apiData }); 56 | break; 57 | case 'detail': 58 | await pageDetail.generatePage({ page, apiData }); 59 | break; 60 | case 'editModal': 61 | await pageEditModal.generatePage({ page, apiData }); 62 | break; 63 | case 'edit': 64 | // await list.generatePage({ page, apiData }); 65 | break; 66 | } 67 | }; 68 | 69 | /** 70 | * 从YAPI系统中提取接口数据 71 | */ 72 | const handleApiData = async () => { 73 | apiData = {}; 74 | await getBrowser(); 75 | const page = await browser.newPage(); 76 | await page.setViewport({ width: 1350, height: 800 }); 77 | await page.goto( 78 | `https://sso.${domain}.com/?returnUrl=https://mock.${domain}.com/#/login`, 79 | ); 80 | let text = await page.evaluate( 81 | () => document.querySelector('button>span>span').innerHTML, 82 | ); 83 | // console.log("text", text) 84 | if (text && text.includes('飞书扫码登录')) { 85 | // await page.waitForFunction(`document.cookie=${decodeURI(cookie)}`) 86 | let ele = await page.$('button>span>span'); 87 | ele.click(); 88 | 89 | await Promise.all([ele.click(), page.waitForNavigation()]); 90 | 91 | await page.waitForSelector('.user-toolbar'); 92 | 93 | // 监听对应的接口 94 | const [, id] = mockUrl.match(/\d+/g); 95 | const requestUrl = `https://mock.${domain}.com/api/interface/get?id=${id}`; 96 | await page.on('response', async (resp) => { 97 | // 提取对应的数据 98 | console.log('url=', resp.url()); 99 | if (resp.url() == requestUrl) { 100 | console.log('XHR resp received'); 101 | const respData = await resp.json(); 102 | console.log(respData); 103 | const { 104 | title, 105 | method, 106 | path, 107 | req_body_type, 108 | req_body_other, 109 | req_params, 110 | req_query, 111 | res_body_type, 112 | res_body = {}, 113 | } = (respData && respData.data) || {}; 114 | let paramsObj = {}; 115 | let requestObj = {}; 116 | let respObj = {}; 117 | let responseObj = {}; 118 | // 对请求头进行判断 119 | switch (req_body_type) { 120 | case 'raw': 121 | if (Array.isArray(req_query) && req_query.length) { 122 | req_query.forEach((item) => { 123 | // console.log("item", item) 124 | const { name, desc = 'null' } = item || {}; 125 | if (name) { 126 | requestObj[name] = { 127 | description: desc, 128 | // type: 'string' 129 | }; 130 | } 131 | }); 132 | } else if (Array.isArray(req_params) && req_params.length) { 133 | req_params.forEach((item) => { 134 | // console.log("item", item) 135 | const { name, desc = 'null' } = item || {}; 136 | if (name) { 137 | requestObj[name] = { 138 | description: desc, 139 | // type: 'string' 140 | }; 141 | } 142 | }); 143 | } 144 | break; 145 | case 'json': 146 | paramsObj = req_body_other ? JSON.parse(req_body_other) : {}; 147 | requestObj = get(paramsObj, 'properties') 148 | ? { ...get(paramsObj, 'properties') } 149 | : {}; 150 | break; 151 | default: 152 | paramsObj = req_body_other ? JSON.parse(req_body_other) : {}; 153 | requestObj = get(paramsObj, 'properties') 154 | ? { ...get(paramsObj, 'properties') } 155 | : {}; 156 | } 157 | 158 | // 对返回body进行判断 159 | switch (res_body_type) { 160 | case 'json': 161 | respObj = JSON.parse(res_body); 162 | responseObj = get(respObj, 'properties.data.properties') 163 | ? { ...get(respObj, 'properties.data.properties') } 164 | : {}; 165 | // let responseObj1 = get(respObj, 'properties.data.properties') 166 | // ? { ...get(respObj, 'properties.data.properties') } 167 | // : {}; 168 | // console.log("responseObj1", responseObj1) 169 | // let responseObj2 = get(responseObj1, 'rows.items.properties') 170 | // console.log("responseObj2", responseObj2) 171 | // responseObj = Object.assign({}, responseObj1, get(responseObj2, 'createShipmentContent.properties'), get(responseObj2, 'createShipmentContent.properties.handlingUnits.properties.contentDetailItems.properties'), get(responseObj2, 'createShipmentResult.properties')) 172 | break; 173 | default: 174 | respObj = JSON.parse(res_body); 175 | responseObj = get(respObj, 'properties.data.properties') 176 | ? { ...get(respObj, 'properties.data.properties') } 177 | : {}; 178 | } 179 | 180 | apiData['title'] = title; 181 | apiData['method'] = method; 182 | apiData['url'] = path; 183 | apiData['request'] = requestObj; 184 | apiData['response'] = responseObj; 185 | apiData = transform.transData(apiData); 186 | console.log('apiData', apiData); 187 | } 188 | }); 189 | 190 | const resp = await page.goto(mockUrl); 191 | if (resp.ok()) { 192 | await page.waitForTimeout(1 * 1000); 193 | page.close(); 194 | await autoEditPage(); 195 | } 196 | // await page.waitForTimeout(1 * 1000); 197 | // const resp2 = await page.goto("https://www.google.com"); 198 | // const html = await page.content(); 199 | // console.log("html", html); 200 | 201 | // await page.screenshot({ 202 | // path: `/Users/alan/Desktop/${Date.now()}.png`, 203 | // }); 204 | } 205 | }; 206 | 207 | // 真实爬接口 208 | handleApiData(); 209 | 210 | // // 测试mock接口 211 | // apiData = cloneDeep(mockApiData.listData); 212 | // apiData = transform.transData(apiData); 213 | // console.log('apiData', apiData); 214 | // autoEditPage(); 215 | -------------------------------------------------------------------------------- /src/pages/setting/const/editModalDSL.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-29 01:19:08 4 | * @LastEditTime: 2021-06-30 02:07:10 5 | * @LastEditors: chengtianqing 6 | * @Description: 7 | */ 8 | const DSL = { 9 | componentName: 'Modal', 10 | type: 'editModal', 11 | props: { 12 | className: 'detail-container', 13 | size: 'medium', 14 | // TODO 15 | 'close-on-click-modal': false, 16 | // TODO 17 | ':visible.sync': 'detailModalShow', 18 | }, 19 | children: [ 20 | { 21 | componentName: 'div', 22 | componentType: 'native', 23 | props: { 24 | slot: 'title', 25 | className: 'df aic jcsb pr24', 26 | }, 27 | children: [ 28 | { 29 | componentName: 'div', 30 | componentType: 'native', 31 | props: { 32 | className: 'df aic left', 33 | }, 34 | isEdit: true, 35 | children: [ 36 | { 37 | componentName: 'span', 38 | componentType: 'native', 39 | props: { 40 | className: 'fs18 fw600 mr8', 41 | }, 42 | children: 'XX编辑', 43 | }, 44 | ], 45 | }, 46 | ], 47 | }, 48 | { 49 | componentName: 'section', 50 | componentType: 'native', 51 | props: {}, 52 | children: [ 53 | { 54 | componentName: 'Form', 55 | props: { 56 | 'label-width': '130px', 57 | }, 58 | dataKey: 'form', 59 | children: [ 60 | { 61 | label: '类型', 62 | key: 'productType', 63 | rules: [{ required: true, message: '请选择类型' }], 64 | children: [ 65 | { 66 | componentName: 'RadioGroup', 67 | props: { 68 | placeholder: '请选择', 69 | clearable: true, 70 | className: 'w-100', 71 | }, 72 | // type: 'button', 73 | options: [ 74 | { value: '进口', label: '进口' }, 75 | { value: '非进口', label: '非进口' }, 76 | ], 77 | }, 78 | ], 79 | }, 80 | { 81 | label: '名称', 82 | key: 'name', 83 | rules: [ 84 | { required: true, message: '请输入名称' }, 85 | { max: 10, message: '长度最大10个字符' }, 86 | ], 87 | children: [ 88 | { 89 | componentName: 'Input', 90 | props: { 91 | placeholder: '请输入名称', 92 | clearable: true, 93 | className: 'w-100', 94 | }, 95 | }, 96 | ], 97 | }, 98 | { 99 | label: '数量', 100 | key: 'amount', 101 | children: [ 102 | { 103 | componentName: 'InputNumber', 104 | props: { 105 | placeholder: '请输入数量', 106 | className: 'w-100', 107 | ':style': "{width: '100%'}", 108 | }, 109 | }, 110 | ], 111 | }, 112 | { 113 | label: '状态', 114 | key: 'status', 115 | children: [ 116 | { 117 | componentName: 'Select', 118 | props: { 119 | placeholder: '请选择状态', 120 | clearable: true, 121 | className: 'w-100', 122 | }, 123 | options: [ 124 | { value: '0', label: '审批中' }, 125 | { value: '1', label: '已通过' }, 126 | { value: '2', label: '已驳回' }, 127 | ], 128 | }, 129 | ], 130 | }, 131 | { 132 | label: '预约时间', 133 | key: 'limitTime', 134 | children: [ 135 | { 136 | componentName: 'RangePicker', 137 | props: { 138 | className: 'w-100', 139 | }, 140 | }, 141 | ], 142 | }, 143 | { 144 | label: '联系人', 145 | key: 'contactName', 146 | children: [ 147 | { 148 | componentName: 'Input', 149 | props: { 150 | placeholder: '请输入联系人', 151 | clearable: true, 152 | className: 'w-100', 153 | }, 154 | }, 155 | ], 156 | }, 157 | { 158 | label: '手机号', 159 | key: 'phone', 160 | children: [ 161 | { 162 | componentName: 'Input', 163 | props: { 164 | placeholder: '请输入手机号', 165 | clearable: true, 166 | className: 'w-100', 167 | }, 168 | }, 169 | ], 170 | }, 171 | { 172 | label: '联系地址', 173 | key: 'address', 174 | children: [ 175 | { 176 | componentName: 'Input', 177 | props: { 178 | placeholder: '请输入联系地址', 179 | clearable: true, 180 | className: 'w-100', 181 | }, 182 | }, 183 | ], 184 | }, 185 | ], 186 | }, 187 | ], 188 | }, 189 | { 190 | componentName: 'div', 191 | componentType: 'native', 192 | props: { 193 | slot: 'footer', 194 | className: 'df aic jcfe right', 195 | }, 196 | isEdit: true, 197 | children: [ 198 | { 199 | componentName: 'Button', 200 | props: { 201 | type: 'default', 202 | }, 203 | children: '取消', 204 | onClick: `function handleCancel() { 205 | this.detailModalShow=false 206 | }`, 207 | }, 208 | { 209 | componentName: 'Button', 210 | props: { 211 | type: 'primary', 212 | className: 'ml8', 213 | }, 214 | children: '确定', 215 | onClick: `async function handleSubmit() { 216 | const valid = await this.$refs['form'].validate() 217 | if (!valid) { 218 | return 219 | } 220 | const params = { ...this.form } 221 | deleteEmptyParam(params) 222 | await API.updateRecord(params) 223 | }`, 224 | }, 225 | ], 226 | }, 227 | ], 228 | componentProps: { 229 | order: `{ 230 | type: Object, 231 | default: () => ({}), 232 | }`, 233 | visible: `{ 234 | type: Boolean, 235 | default: false, 236 | }`, 237 | }, 238 | dataSource: { 239 | colProps: { 240 | span: 16, 241 | }, 242 | form: {}, 243 | loading: false, 244 | submitLoading: false, 245 | }, 246 | computed: { 247 | detailModalShow: `{ 248 | get() { 249 | return this.visible 250 | }, 251 | set(val) { 252 | this.$emit('update:visible', val) 253 | }, 254 | }`, 255 | }, 256 | methods: { 257 | getRecordDetail: `async function getRecordDetail() { 258 | const params = { id: this.recordId } 259 | const { code, data } = await API.getRecordDetail(params) 260 | if (code === 200 && data) { 261 | this.record = data 262 | } 263 | }`, 264 | }, 265 | imports: { 266 | '{ deleteEmptyParam }': '@/utils', 267 | '* as API': './api', 268 | }, 269 | apis: { 270 | imports: { 271 | UmiRequest: '@du/umi-request', 272 | }, 273 | updateRecord: `function updateRecord(params) { 274 | return UmiRequest.request({ 275 | method: 'POST', 276 | url: '/api/v1/h5/oversea/backend/product/update', 277 | data: params, 278 | }) 279 | }`, 280 | getRecordDetail: `function getRecordDetail(params) { 281 | return UmiRequest.request({ 282 | method: 'POST', 283 | url: '/api/v1/h5/oversea/backend/product/detail', 284 | data: params, 285 | }) 286 | }`, 287 | }, 288 | }; 289 | 290 | export { DSL }; 291 | -------------------------------------------------------------------------------- /src/pages/code/GenerateVue.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { PureComponent } from 'react'; 3 | import { Button, Drawer } from 'antd'; 4 | import { DSL } from './dsl'; 5 | 6 | const DSLStr = JSON.stringify(DSL, function (_, v) { 7 | if (typeof v === 'function') { 8 | return v.toString(); 9 | } 10 | return v; 11 | }); 12 | 13 | interface IObject { 14 | [key: string]: any; 15 | } 16 | interface IProps { 17 | generateCode?: any; 18 | showGenerateButton?: boolean; 19 | } 20 | 21 | let renderData = { 22 | codeStr: '', 23 | template: '', 24 | imports: [], 25 | data: {}, 26 | methods: [], 27 | lifecycles: [], 28 | styles: [], 29 | }; 30 | 31 | const lifeCycleMap = { 32 | constructor: 'created', 33 | getDerivedStateFromProps: 'beforeUpdate', 34 | componentDidMount: 'mounted', 35 | componentDidUpdate: 'updated', 36 | componentWillUnmount: 'beforeDestroy', 37 | }; 38 | 39 | // 通用事件 40 | const commonFunc = ['onClick', 'onChange', 'onBlur', 'onFocus', 'onClear']; 41 | 42 | class GenerateVue extends PureComponent { 43 | state = { 44 | visible: false, 45 | }; 46 | 47 | generateTemplate = (schemaDSL: any, vModel?: any) => { 48 | const { 49 | componentName, 50 | props, 51 | children, 52 | dataKey, 53 | dataSource, 54 | lifeCycle, 55 | methods, 56 | imports, 57 | } = schemaDSL || {}; 58 | const objStr = (obj: any) => 59 | Object.entries(obj) 60 | .map(([k, v]) => { 61 | if (['Table'].includes(componentName)) { 62 | if (k === 'key') { 63 | // 需要转化 64 | k = 'prop'; 65 | } 66 | } 67 | if (typeof v !== 'string') { 68 | return `:${k}="${v}"`; 69 | } else { 70 | return `${k}="${v}"`; 71 | } 72 | }) 73 | .join(' '); 74 | 75 | let xml = ''; 76 | switch (componentName) { 77 | case 'Page': 78 | renderData.data = dataSource || {}; 79 | this.getLifeCycle(lifeCycle); 80 | this.getMethods(methods); 81 | this.getImports(imports); 82 | const childStr = (children || []) 83 | .map((item: any) => this.generateTemplate(item)) 84 | .join('\n'); 85 | xml = `
\n${childStr}\n
`; 86 | break; 87 | case 'Form': 88 | const formDataKey = dataKey || 'form'; 89 | renderData.data[formDataKey] = {}; 90 | 91 | const formItems = (children || []) 92 | .map((item: any) => { 93 | const { key, label, initValue } = item || {}; 94 | const itemPropStr = key 95 | ? `label="${label}" prop="${key}"` 96 | : `label="${label}"`; 97 | const vmodel = key && formDataKey ? `${formDataKey}.${key}` : ''; 98 | if (key && formDataKey) { 99 | // @ts-ignore 100 | renderData.data[formDataKey][key] = 101 | initValue !== undefined ? initValue : ''; 102 | } 103 | const itemChildren = (item.children || []) 104 | .map((child: any) => this.generateTemplate(child, vmodel)) 105 | .join(''); 106 | return ` 107 | 108 | ${itemChildren} 109 | 110 | `; 111 | }) 112 | .join('\n'); 113 | 114 | xml = ` 115 | 116 | ${formItems} 117 | 118 | `; 119 | break; 120 | case 'Input': 121 | const inputEventStr = this.getEventStr(schemaDSL); 122 | xml = ``; 125 | break; 126 | case 'Button': 127 | const buttonEventStr = this.getEventStr(schemaDSL); 128 | xml = `${children}`; 131 | break; 132 | case 'Table': 133 | const listKey = dataKey || 'list'; 134 | renderData.data[listKey] = []; 135 | const columns = (children || []) 136 | .map((item: any) => { 137 | return ``; 138 | }) 139 | .join('\n'); 140 | xml = `\n${columns}\n\n`; 141 | break; 142 | case 'Pagination': 143 | const paginationEventStr = this.getEventStr(schemaDSL, { 144 | onPageChange: '@current-change', 145 | }); 146 | xml = ` 147 | 152 | 153 | `; 154 | break; 155 | default: 156 | xml = ''; 157 | } 158 | return xml + '\n'; 159 | }; 160 | 161 | getLifeCycle = (item: object) => { 162 | Object.entries(item).forEach(([k, v]) => { 163 | // @ts-ignore 164 | const name = lifeCycleMap[k]; 165 | if (name) { 166 | const { newFunc } = this.transformFunc(v, name); 167 | // @ts-ignore 168 | renderData.lifecycles.push(newFunc); 169 | } 170 | }); 171 | }; 172 | 173 | getImports = (item: object) => { 174 | Object.entries(item).forEach(([k, v]) => { 175 | const importStr = `import ${k} from "${v}"`; 176 | // @ts-ignore 177 | renderData.imports.push(importStr); 178 | }); 179 | }; 180 | 181 | getMethods = (item: object) => { 182 | Object.entries(item).forEach(([k, v]) => { 183 | const { newFunc } = this.transformFunc(v); 184 | // @ts-ignore 185 | renderData.methods.push(newFunc); 186 | }); 187 | }; 188 | 189 | getEventStr = (item: object, extraMap: IObject = {}) => { 190 | let funcStr = ''; 191 | Object.entries(item).forEach(([k, v]) => { 192 | if (typeof v === 'string' && v.includes('function')) { 193 | const { newFunc, newFuncName } = this.transformFunc(v); 194 | funcStr = funcStr ? `${funcStr} ` : funcStr; 195 | if (commonFunc.includes(k)) { 196 | // 通用的函数事件 197 | const name = k.replace(/on/, '@').toLowerCase(); 198 | funcStr += `${name}="${newFuncName}"`; 199 | } else if (extraMap[k]) { 200 | // 特定的函数事件 201 | funcStr += `${extraMap[k]}="${newFuncName}"`; 202 | } 203 | // @ts-ignore 204 | renderData.methods.push(newFunc); 205 | } 206 | }); 207 | return funcStr; 208 | }; 209 | 210 | transformFunc = (func: any, newFuncName = '') => { 211 | const funcStr = func.toString(); 212 | const start = funcStr.indexOf('function ') + 9; 213 | const end = funcStr.indexOf('('); 214 | const funcName = funcStr.slice(start, end); 215 | let newFunc = funcStr.slice(start); 216 | if (newFuncName) { 217 | newFunc = newFunc.replace(funcName, newFuncName); 218 | } 219 | return { newFunc, newFuncName: newFuncName || funcName }; 220 | }; 221 | 222 | generateVue = () => { 223 | const vueCode = ` 224 | 227 | 228 | 242 | 243 | 259 | `; 260 | return prettierFormat(vueCode, 'vue'); 261 | }; 262 | 263 | handleGenerate = () => { 264 | this.initData(); 265 | const schema = JSON.parse(DSLStr); 266 | console.log('schema', DSLStr); 267 | renderData.template = this.generateTemplate(schema); 268 | renderData.codeStr = this.generateVue(); 269 | console.log('renderData', renderData); 270 | 271 | this.setState({ visible: true }); 272 | }; 273 | 274 | getSourceCode = () => { 275 | this.initData(); 276 | const schema = JSON.parse(DSLStr); 277 | renderData.template = this.generateTemplate(schema); 278 | renderData.codeStr = this.generateVue(); 279 | return renderData.codeStr; 280 | }; 281 | 282 | initData = () => { 283 | renderData = { 284 | codeStr: '', 285 | template: '', 286 | imports: [], 287 | data: {}, 288 | methods: [], 289 | lifecycles: [], 290 | styles: [], 291 | }; 292 | }; 293 | 294 | render() { 295 | const { showGenerateButton } = this.props; 296 | return ( 297 | showGenerateButton && ( 298 | <> 299 | 302 | this.setState({ visible: false })} 307 | width={800} 308 | visible={this.state.visible} 309 | > 310 |
{renderData.codeStr}
311 |
312 | 313 | ) 314 | ); 315 | } 316 | } 317 | 318 | export default GenerateVue; 319 | -------------------------------------------------------------------------------- /src/pages/setting/const/dsl.ts: -------------------------------------------------------------------------------- 1 | const DSL = { 2 | componentName: 'div', 3 | componentType: 'native', 4 | type: 'list', 5 | props: { 6 | className: 'page-container', 7 | }, 8 | children: [ 9 | { 10 | componentName: 'div', 11 | componentType: 'native', 12 | props: { 13 | className: 'bgc-fff bshadow pl24 pb6 pr24 pt24', 14 | }, 15 | children: [ 16 | { 17 | componentName: 'Form', 18 | props: { 19 | 'label-width': '80px', 20 | }, 21 | dataKey: 'form', 22 | type: 'search', 23 | children: [ 24 | { 25 | label: '姓名', 26 | key: 'trueName', 27 | children: [ 28 | { 29 | componentName: 'Input', 30 | props: { 31 | placeholder: '请输入', 32 | clearable: true, 33 | }, 34 | }, 35 | ], 36 | }, 37 | { 38 | label: '状态', 39 | key: 'status', 40 | children: [ 41 | { 42 | componentName: 'Select', 43 | props: { 44 | placeholder: '请选择', 45 | clearable: true, 46 | }, 47 | options: [ 48 | { value: '0', label: '审批中' }, 49 | { value: '1', label: '已通过' }, 50 | { value: '2', label: '已驳回' }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | { 56 | label: '创建时间', 57 | key: 'createTime', 58 | children: [ 59 | { 60 | componentName: 'RangePicker', 61 | props: {}, 62 | }, 63 | ], 64 | }, 65 | { 66 | label: '商品类目', 67 | key: 'categoryIds', 68 | children: [ 69 | { 70 | componentName: 'Cascader', 71 | props: { 72 | placeholder: '请选择', 73 | clearable: true, 74 | }, 75 | options: [ 76 | { 77 | value: 1, 78 | label: '鞋', 79 | children: [ 80 | { 81 | value: 100, 82 | label: '运动鞋', 83 | children: [ 84 | { 85 | value: 200, 86 | label: '篮球鞋', 87 | }, 88 | ], 89 | }, 90 | ], 91 | }, 92 | ], 93 | }, 94 | ], 95 | }, 96 | { 97 | label: '商品名称', 98 | key: 'productName', 99 | children: [ 100 | { 101 | componentName: 'AutoComplete', 102 | props: { 103 | placeholder: '请输入', 104 | clearable: true, 105 | }, 106 | options: [], 107 | onSearch: `function handleSearchText(searchText) { 108 | this.queryProductName(searchText); 109 | this.productNameOptions = [] 110 | }`, 111 | onSelect: `function handleSelect(data) { 112 | this.productName = data; 113 | }`, 114 | }, 115 | ], 116 | }, 117 | { 118 | label: '', 119 | children: [ 120 | { 121 | componentName: 'Button', 122 | props: { 123 | type: 'default', 124 | }, 125 | children: '重置', 126 | onClick: `function handleReset() { 127 | this.pagination.currentPage = 1; 128 | this.form = {}; 129 | this.queryList(); 130 | }`, 131 | }, 132 | { 133 | componentName: 'Button', 134 | props: { 135 | type: 'primary', 136 | }, 137 | children: '查询', 138 | onClick: `function handleSearch() { 139 | this.pagination.currentPage = 1; 140 | this.queryList(); 141 | }`, 142 | }, 143 | ], 144 | }, 145 | ], 146 | }, 147 | ], 148 | }, 149 | { 150 | componentName: 'div', 151 | componentType: 'native', 152 | props: { 153 | className: 'pl24 pr24 pb24 mt24 bgc-fff bshadow', 154 | }, 155 | children: [ 156 | { 157 | componentName: 'div', 158 | componentType: 'native', 159 | props: { 160 | className: 'flex-between', 161 | }, 162 | isEdit: true, 163 | children: [ 164 | { 165 | componentName: 'div', 166 | componentType: 'native', 167 | props: { 168 | className: 'title', 169 | }, 170 | children: '商品管理列表', 171 | }, 172 | { 173 | componentName: 'div', 174 | componentType: 'native', 175 | props: { 176 | className: 'flex-center', 177 | }, 178 | children: [ 179 | { 180 | componentName: 'Button', 181 | props: { 182 | type: 'text', 183 | }, 184 | children: '导出结果', 185 | onClick: 'function handleExport() {}', 186 | }, 187 | { 188 | componentName: 'Divider', 189 | props: { 190 | direction: 'vertical', 191 | }, 192 | }, 193 | { 194 | componentName: 'Button', 195 | props: { 196 | type: 'text', 197 | }, 198 | children: '批量导入', 199 | onClick: 'function handleUpload() {}', 200 | }, 201 | { 202 | componentName: 'Divider', 203 | props: { 204 | direction: 'vertical', 205 | }, 206 | }, 207 | { 208 | componentName: 'Button', 209 | props: { 210 | type: 'text', 211 | }, 212 | children: '下载模版', 213 | onClick: 'function downloadTemplate() {}', 214 | }, 215 | { 216 | componentName: 'Divider', 217 | props: { 218 | direction: 'vertical', 219 | }, 220 | }, 221 | { 222 | componentName: 'Button', 223 | props: { 224 | type: 'primary', 225 | }, 226 | children: '新增', 227 | onClick: 'function handleCreate() {}', 228 | }, 229 | ], 230 | }, 231 | ], 232 | }, 233 | { 234 | componentName: 'Table', 235 | props: {}, 236 | dataKey: 'list', 237 | children: [ 238 | { 239 | key: 'id', 240 | label: '序号', 241 | minWidth: 100, 242 | renderKey: `renderDefault`, 243 | }, 244 | { 245 | key: 'orderNo', 246 | label: '订单号', 247 | renderKey: `renderDefault`, 248 | }, 249 | { 250 | key: 'trueName', 251 | label: '姓名', 252 | renderKey: `renderDefault`, 253 | }, 254 | { 255 | key: 'amount', 256 | label: '订单金额', 257 | renderKey: `renderAmount`, 258 | }, 259 | { 260 | key: 'status', 261 | label: '校验状态', 262 | renderKey: `renderDefault`, 263 | }, 264 | { 265 | key: 'createTime', 266 | label: '创建时间', 267 | renderKey: 'renderTime', 268 | }, 269 | { 270 | key: '-', 271 | label: '操作', 272 | fixed: 'right', 273 | renderKey: `renderOperate`, 274 | }, 275 | ], 276 | }, 277 | { 278 | componentName: 'Pagination', 279 | props: {}, 280 | dataKey: 'pagination', 281 | onPageChange: `function handleCurrentChange(val) { 282 | this.pagination.currentPage = val; 283 | this.queryList(); 284 | }`, 285 | }, 286 | ], 287 | }, 288 | ], 289 | dataSource: { 290 | colProps: { 291 | xs: 24, 292 | sm: 12, 293 | lg: 8, 294 | xl: 8, 295 | }, 296 | }, 297 | lifeCycle: { 298 | mounted: `function mounted() { 299 | this.queryList(); 300 | }`, 301 | }, 302 | methods: { 303 | queryList: `async function queryList() { 304 | const params = Object.assign({}, this.form, { 305 | pageSize: this.pagination.pageSize, 306 | page: this.pagination.currentPage, 307 | }) 308 | deleteEmptyParam(params) 309 | const res = await API.queryList(params, this) 310 | if (res.code === 200 && res.data) { 311 | this.list = res.data.list 312 | this.pagination.total = res.data.total 313 | } 314 | }`, 315 | }, 316 | imports: { 317 | '{ deleteEmptyParam }': '@/utils', 318 | '* as API': './api', 319 | }, 320 | apis: { 321 | imports: { 322 | UmiRequest: '@du/umi-request', 323 | }, 324 | queryList: `function queryList(params) { 325 | return UmiRequest.request({ 326 | method: 'POST', 327 | url: '/api/v1/h5/oversea/backend/product/productList', 328 | data: params 329 | }) 330 | }`, 331 | }, 332 | }; 333 | 334 | export { DSL }; 335 | -------------------------------------------------------------------------------- /src/pages/setting/const/detailDSL.ts: -------------------------------------------------------------------------------- 1 | const DSL = { 2 | componentName: 'Modal', 3 | type: 'detail', 4 | props: { 5 | className: 'detail-container', 6 | size: 'large', 7 | // TODO 8 | 'close-on-click-modal': false, 9 | // TODO 10 | ':visible.sync': 'detailModalShow', 11 | }, 12 | children: [ 13 | { 14 | componentName: 'div', 15 | componentType: 'native', 16 | props: { 17 | slot: 'title', 18 | className: 'df aic jcsb pr24', 19 | }, 20 | children: [ 21 | { 22 | componentName: 'div', 23 | componentType: 'native', 24 | props: { 25 | className: 'df aic left', 26 | }, 27 | isEdit: true, 28 | children: [ 29 | { 30 | componentName: 'span', 31 | componentType: 'native', 32 | props: { 33 | className: 'fs18 fw600 mr8', 34 | }, 35 | children: 'XX详情', 36 | }, 37 | { 38 | componentName: 'StatusTag', 39 | componentType: 'custom', 40 | props: { 41 | statusKey: 'record.status', 42 | statusTagObj: { 43 | 1: { value: '待提交', tag: 'warning' }, 44 | 2: { value: '审核中', tag: 'info' }, 45 | 3: { value: '已通过', tag: 'success' }, 46 | 4: { value: '已驳回', tag: 'danger' }, 47 | }, 48 | }, 49 | children: null, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | { 56 | componentName: 'section', 57 | componentType: 'native', 58 | props: {}, 59 | children: [ 60 | { 61 | componentName: 'div', 62 | componentType: 'native', 63 | props: { 64 | className: 'info-list bb mb24 pb12', 65 | }, 66 | isEdit: true, 67 | children: [ 68 | { 69 | componentName: 'div', 70 | componentType: 'native', 71 | props: { 72 | className: 'mb12 fs16 fw700', 73 | }, 74 | isEdit: true, 75 | children: '发货信息', 76 | }, 77 | { 78 | componentName: 'Row', 79 | props: { 80 | gutter: 20, 81 | }, 82 | dataKey: 'record', 83 | isEdit: true, 84 | children: [ 85 | { 86 | span: 8, 87 | key: 'applicationNo', 88 | label: '预约单号', 89 | renderKey: `renderDefault`, 90 | }, 91 | { 92 | span: 8, 93 | label: '预约数量', 94 | key: 'appointNum', 95 | render: `0`, 96 | }, 97 | { 98 | span: 8, 99 | label: '退货地址', 100 | key: 'sendAddress', 101 | isEllipsis: true, 102 | renderKey: `renderEllipsis`, 103 | }, 104 | { 105 | span: 8, 106 | label: '发件人手机号', 107 | key: 'senderMobile', 108 | renderKey: `renderDefault`, 109 | }, 110 | { 111 | span: 8, 112 | label: '物流商', 113 | key: 'logisticsName', 114 | renderKey: `renderDefault`, 115 | }, 116 | { 117 | span: 8, 118 | label: '物流单号', 119 | key: 'expressCode', 120 | renderKey: `renderDefault`, 121 | }, 122 | { 123 | span: 8, 124 | label: '物流状态', 125 | key: 'expressStatus', 126 | renderKey: `renderEnum`, 127 | enumObj: { 128 | 0: '已揽件', 129 | 1: '运输中', 130 | 2: '派送中', 131 | 3: '已签收', 132 | }, 133 | }, 134 | { 135 | span: 8, 136 | label: '预约到货时间', 137 | key: 'appointTime', 138 | renderKey: `renderTime`, 139 | }, 140 | ], 141 | }, 142 | ], 143 | }, 144 | { 145 | componentName: 'div', 146 | componentType: 'native', 147 | props: { 148 | className: 'info-list bb mb24 pb12', 149 | }, 150 | isEdit: true, 151 | children: [ 152 | { 153 | componentName: 'div', 154 | componentType: 'native', 155 | props: { 156 | className: 'mb12 fs16 fw700', 157 | }, 158 | isEdit: true, 159 | children: '收件方信息', 160 | }, 161 | { 162 | componentName: 'Row', 163 | props: { 164 | gutter: 20, 165 | }, 166 | isEdit: true, 167 | dataKey: 'record', 168 | children: [ 169 | { 170 | span: 8, 171 | key: 'recipientName', 172 | label: '收件人', 173 | renderKey: `renderDefault`, 174 | }, 175 | { 176 | span: 16, 177 | label: '收件人手机', 178 | key: 'recipientPhone', 179 | renderKey: `renderDefault`, 180 | }, 181 | { 182 | span: 24, 183 | label: '收件地址', 184 | key: 'warehouseAddress', 185 | renderKey: `renderEllipsis`, 186 | }, 187 | ], 188 | }, 189 | ], 190 | }, 191 | { 192 | componentName: 'div', 193 | componentType: 'native', 194 | props: { 195 | className: 'info-list bb mb24 pb12', 196 | }, 197 | isEdit: true, 198 | children: [ 199 | { 200 | componentName: 'div', 201 | componentType: 'native', 202 | props: { 203 | className: 'mb12 fs16 fw700', 204 | }, 205 | children: '预约商品', 206 | isEdit: true, 207 | }, 208 | { 209 | componentName: 'Table', 210 | props: { 211 | size: 'small', 212 | border: true, 213 | }, 214 | dataKey: 'list', 215 | children: [ 216 | { 217 | key: 'info', 218 | label: '商品信息', 219 | minWidth: 250, 220 | render: `
商品信息
`, 221 | // render: `
222 | //
223 | // 227 | //
228 | // 229 | //
230 | // 231 | //
232 | //
233 | //
234 | //
235 | // 货号:{{ row.productNo }} 236 | //
237 | // 238 | //
239 | //
`, 240 | }, 241 | { 242 | key: 'orderNo', 243 | label: '订单号', 244 | renderKey: `renderDefault`, 245 | }, 246 | { 247 | key: 'trueName', 248 | label: '姓名', 249 | renderKey: `renderDefault`, 250 | }, 251 | { 252 | key: 'amount', 253 | label: '订单金额', 254 | renderKey: `renderAmount`, 255 | }, 256 | { 257 | key: 'status', 258 | label: '校验状态', 259 | renderKey: `renderDefault`, 260 | }, 261 | { 262 | key: 'createTime', 263 | label: '创建时间', 264 | renderKey: 'renderTime', 265 | }, 266 | ], 267 | }, 268 | ], 269 | }, 270 | ], 271 | }, 272 | ], 273 | componentProps: { 274 | order: `{ 275 | type: Object, 276 | default: () => ({}), 277 | }`, 278 | visible: `{ 279 | type: Boolean, 280 | default: false, 281 | }`, 282 | }, 283 | dataSource: { 284 | colProps: { 285 | xs: 24, 286 | sm: 12, 287 | lg: 8, 288 | xl: 8, 289 | }, 290 | record: {}, 291 | loading: false, 292 | }, 293 | computed: { 294 | detailModalShow: `{ 295 | get() { 296 | return this.visible 297 | }, 298 | set(val) { 299 | this.$emit('update:visible', val) 300 | }, 301 | }`, 302 | }, 303 | lifeCycle: { 304 | mounted: `function mounted() { 305 | this.getRecordDetail(); 306 | }`, 307 | }, 308 | methods: { 309 | getRecordDetail: `async function getRecordDetail() { 310 | const params = { id: this.recordId } 311 | const { code, data } = await API.getRecordDetail(params) 312 | if (code === 200 && data) { 313 | this.record = data 314 | } 315 | }`, 316 | }, 317 | imports: { 318 | '{ deleteEmptyParam }': '@/utils', 319 | '* as API': './api', 320 | }, 321 | apis: { 322 | imports: { 323 | UmiRequest: '@du/umi-request', 324 | }, 325 | getRecordDetail: `function getRecordDetail(params) { 326 | return UmiRequest.request({ 327 | method: 'POST', 328 | url: '/api/v1/h5/oversea/backend/product/detail', 329 | data: params, 330 | }) 331 | }`, 332 | }, 333 | }; 334 | 335 | export { DSL }; 336 | -------------------------------------------------------------------------------- /src/auto/transform.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chengtianqing 3 | * @Date: 2021-06-12 16:43:10 4 | * @LastEditTime: 2021-07-03 01:12:00 5 | * @LastEditors: chengtianqing 6 | * @Description: 转换api数据,映射对应的编辑内容 7 | */ 8 | const isObject = require('lodash/isObject'); 9 | const get = require('lodash/get'); 10 | 11 | // const listData = { 12 | // title: '商品列表', 13 | // method: 'POST', 14 | // url: '/cross/mhk/pageList', 15 | // request: { 16 | // applicationNo: { type: 'string', description: '预约单号', mock: [Object] }, 17 | // status: { type: 'string', description: '预约单状态', mock: [Object] }, 18 | // warehouseCode: { type: 'string', description: '收货地址', mock: [Object] }, 19 | // createTimeStart: { 20 | // type: 'string', 21 | // description: '创建时间-查询起始时间', 22 | // mock: [Object], 23 | // }, 24 | // createTimeEnd: { 25 | // type: 'string', 26 | // description: '创建时间-查询结束时间', 27 | // mock: [Object], 28 | // }, 29 | // appointTimeStart: { 30 | // type: 'string', 31 | // description: '预约到货时间-查询起始时间', 32 | // mock: [Object], 33 | // }, 34 | // appointTimeEnd: { 35 | // type: 'string', 36 | // description: '预约到货时间-查询结束时间', 37 | // mock: [Object], 38 | // }, 39 | // userId: { type: 'number', description: '用户ID', mock: [Object] }, 40 | // page: { type: 'number', description: '分页参数-第几页', mock: [Object] }, 41 | // pageSize: { type: 'number', description: '分页参数-页数', mock: [Object] }, 42 | // }, 43 | // response: { 44 | // pageNum: { type: 'number', description: '当前页', mock: [Object] }, 45 | // pageSize: { type: 'number', description: '分页大小', mock: [Object] }, 46 | // total: { type: 'number', description: '总元素数', mock: [Object] }, 47 | // pages: { type: 'number', description: '总页数', mock: [Object] }, 48 | // contents: { 49 | // type: 'array', 50 | // description: '数据 ,T', 51 | // items: { 52 | // properties: { 53 | // applicationNo: { 54 | // type: 'string', 55 | // description: '预约单号', 56 | // mock: [Object], 57 | // }, 58 | // status: { type: 'string', description: '预约单状态', mock: [Object] }, 59 | // avgPrice: { 60 | // type: 'string', 61 | // description: '均价', 62 | // mock: [Object], 63 | // }, 64 | // appointTime: { 65 | // type: 'string', 66 | // description: '预约到货时间', 67 | // mock: [Object], 68 | // }, 69 | // }, 70 | // }, 71 | // }, 72 | // extra: { 73 | // type: 'object', 74 | // description: '附加信息(该参数为map)', 75 | // properties: [Object], 76 | // }, 77 | // }, 78 | // }; 79 | 80 | // 类型 81 | const typeEnum = { 82 | default: 'default', 83 | number: 'number', 84 | price: 'price', 85 | enum: 'enum', 86 | date: 'date', 87 | }; 88 | 89 | /** 90 | * 判断类型 91 | * @param {*} item 92 | * @returns 93 | */ 94 | const checkFiledType = (item) => { 95 | let label = ''; 96 | let fileType = 'default'; 97 | let enumObj = {}; 98 | label = item.description || item.title || 'null'; 99 | if (item.properties) { 100 | label = 'object=' + label; 101 | } 102 | let arr = label.match(/\d/g); 103 | if (arr && arr.length > 1) { 104 | // 包含多个枚举值 105 | // 查找第一个位置 106 | let arr1 = label.match(/\d/); 107 | let str1 = label.substr(0, arr1.index); 108 | let str2 = label.substr(arr1.index); 109 | str2 110 | .split(' ') 111 | .filter(Boolean) 112 | .forEach((item) => { 113 | const [k, v] = item.split(/[-::\.]/); 114 | enumObj[k] = v || 'null'; 115 | }); 116 | label = str1 ? str1.replace(/[\s-,,(\(]/g, '') : '--'; 117 | fileType = typeEnum['enum']; 118 | } else if (['状态', '类型'].some((s) => label.indexOf(s) > -1)) { 119 | fileType = typeEnum['enum']; 120 | } else if (['时间', '日期'].some((s) => label.indexOf(s) > -1)) { 121 | fileType = typeEnum['date']; 122 | } else if (['次数', '数量', '小数'].some((s) => label.indexOf(s) > -1)) { 123 | fileType = typeEnum['number']; 124 | } else if (['款', '价', '金额'].some((s) => label.indexOf(s) > -1)) { 125 | fileType = typeEnum['price']; 126 | } 127 | label = label.split(/[\s\-\(,,]/)[0]; 128 | return { label, fileType, enumObj }; 129 | }; 130 | 131 | /** 132 | * 转换data 133 | * @param {*} apiData 134 | * @returns 135 | */ 136 | function transData(apiData, userInput = '') { 137 | if (isObject(apiData)) { 138 | const { request, response, title } = apiData; 139 | 140 | if (userInput) { 141 | // 用户输入值,指定页面 142 | const pageTypeObj = { 143 | 管理列表: 'list', 144 | 弹窗详情: 'detail', 145 | 弹窗编辑: 'editModal', 146 | 编辑页面: 'edit', 147 | }; 148 | apiData.componentType = pageTypeObj[userInput]; 149 | } 150 | 151 | if (!apiData.componentType) { 152 | // 用户没有指定 153 | // 先根据接口名称判断页面类型 154 | if ((title || '').indexOf('列表') > -1) { 155 | apiData.componentType = 'list'; 156 | } else if ((title || '').indexOf('详情') > -1) { 157 | apiData.componentType = 'detail'; 158 | } else if ( 159 | ['新增', '创建', '编辑', '更新'].some( 160 | (s) => (title || '').indexOf(s) > -1, 161 | ) 162 | ) { 163 | apiData.componentType = 'editModal'; 164 | } else { 165 | apiData.componentType = ''; 166 | } 167 | // 根据返回结构进一步判断 168 | if (!apiData.componentType && isObject(response)) { 169 | // 判断是否为列表类型页面 170 | if ( 171 | response.total || 172 | (response.contents && response.contents.type === 'array') || 173 | (response.list && response.list.type === 'array') || 174 | (response.rows && response.rows.type === 'array') 175 | ) { 176 | apiData.componentType = 'list'; 177 | } 178 | } 179 | } 180 | 181 | // 再根据页面类型进行数据处理 182 | if (apiData.componentType === 'list') { 183 | // 列表类型 184 | // 转换搜索组件数据类型 185 | if (isObject(request)) { 186 | const form = {}; 187 | const search = { 188 | form: {}, 189 | pageKey: '', 190 | pageSizeKey: '', 191 | }; 192 | Object.entries(request).forEach(([k, v]) => { 193 | if (['page', 'pageNum'].some((s) => k === s)) { 194 | search.pageKey = k; 195 | } else if (['pageSize'].some((s) => k === s)) { 196 | search.pageSizeKey = k; 197 | } else { 198 | if (isObject(v)) { 199 | const typeObj = { 200 | default: '输入框', 201 | number: '数字输入框', 202 | price: '数字输入框', 203 | enum: '选择器', 204 | date: '日期范围', 205 | }; 206 | const obj = checkFiledType(v); 207 | obj['componentType'] = typeObj[obj.fileType] || '输入框'; 208 | if (obj.componentType === '日期范围') { 209 | if (/Start$|Begin$|End$/i.test(k)) { 210 | k = k.replace(/Begin$/i, ''); 211 | k = k.replace(/Start$/i, ''); 212 | k = k.replace(/End$/i, ''); 213 | } else if (/^start|^end/i.test(k)) { 214 | k = k.replace(/^start/i, ''); 215 | k = k.replace(/^end/i, ''); 216 | } else if (/^lt|^gt/i.test(k)) { 217 | k = k.replace(/^lt/i, ''); 218 | k = k.replace(/^gt/i, ''); 219 | } 220 | } 221 | form[k] = obj; 222 | } 223 | } 224 | }); 225 | search.form = form; 226 | apiData.search = search; 227 | } 228 | 229 | // 转换表格数据类型 230 | if (isObject(response)) { 231 | const { contents = {}, rows = {}, list = {} } = response; 232 | const temp = Object.assign(contents, rows, list); 233 | const columnsObj = get(temp, 'items.properties'); 234 | const col = {}; 235 | Object.entries(columnsObj).forEach(([k, v]) => { 236 | if (isObject(v)) { 237 | const typeObj = { 238 | default: '默认', 239 | price: '金额', 240 | enum: '状态', 241 | date: '时间', 242 | }; 243 | const obj = checkFiledType(v); 244 | obj.componentType = typeObj[obj.fileType] || '默认'; 245 | col[k] = obj; 246 | } else { 247 | col[k] = v; 248 | } 249 | }); 250 | apiData.columnsObj = col; 251 | } 252 | } else if (apiData.componentType === 'detail') { 253 | // 详情类型 254 | // 转换显示数据类型 255 | if (isObject(response)) { 256 | const recordObj = {}; 257 | Object.entries(response).forEach(([k, v]) => { 258 | if (isObject(v)) { 259 | const typeObj = { 260 | default: '默认', 261 | price: '金额', 262 | enum: '状态', 263 | date: '时间', 264 | }; 265 | const obj = checkFiledType(v); 266 | obj.componentType = typeObj[obj.fileType] || '默认'; 267 | recordObj[k] = obj; 268 | } else { 269 | recordObj[k] = v; 270 | } 271 | }); 272 | apiData.recordObj = recordObj; 273 | } 274 | } else if (['editModal', 'edit'].includes(apiData.componentType)) { 275 | // 转换数据类型 276 | if (isObject(request)) { 277 | const form = {}; 278 | Object.entries(request).forEach(([k, v]) => { 279 | if (isObject(v)) { 280 | const typeObj = { 281 | default: '输入框', 282 | number: '数字输入框', 283 | price: '数字输入框', 284 | enum: '选择器', 285 | date: '日期范围', 286 | }; 287 | const obj = checkFiledType(v); 288 | obj['componentType'] = typeObj[obj.fileType] || '输入框'; 289 | if (obj.componentType === '日期范围') { 290 | if (/Start$|End$/i.test(k)) { 291 | k = k.replace(/Start$/i, ''); 292 | k = k.replace(/End$/i, ''); 293 | } else if (/^start|^end/i.test(k)) { 294 | k = k.replace(/^start/i, ''); 295 | k = k.replace(/^end/i, ''); 296 | } else if (/^lt|^gt/i.test(k)) { 297 | k = k.replace(/^lt/i, ''); 298 | k = k.replace(/^gt/i, ''); 299 | } 300 | } 301 | form[k] = obj; 302 | } 303 | }); 304 | apiData.formObj = form; 305 | } 306 | } 307 | } 308 | return apiData; 309 | } 310 | 311 | module.exports = { 312 | transData, 313 | }; 314 | -------------------------------------------------------------------------------- /src/pages/code/GenerateReact.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { PureComponent } from 'react'; 3 | import { Button, Drawer } from 'antd'; 4 | import { DSL } from './dsl'; 5 | 6 | interface IProps { 7 | generateCode?: any; 8 | showGenerateButton?: boolean; 9 | } 10 | 11 | const DSLStr = JSON.stringify(DSL, function (_, v) { 12 | if (typeof v === 'function') { 13 | return v.toString(); 14 | } 15 | return v; 16 | }); 17 | 18 | let renderData = { 19 | codeStr: '', 20 | template: '', 21 | imports: [], 22 | data: {}, 23 | methods: [], 24 | lifecycles: [], 25 | styles: [], 26 | antdComponentMap: {}, 27 | formRefs: [], 28 | }; 29 | 30 | let submitName = 'submit'; 31 | export default class GenerateReact extends PureComponent { 32 | state = { 33 | visible: false, 34 | }; 35 | 36 | generateReact = () => { 37 | const antdComs = Object.keys(renderData.antdComponentMap); 38 | if (antdComs && antdComs.length) { 39 | renderData.imports.unshift( 40 | `import { ${antdComs.join(', ')} } from 'antd'`, 41 | ); 42 | } 43 | if (renderData.formRefs.length) { 44 | renderData.imports.unshift( 45 | `import { FormInstance } from 'antd/lib/form'`, 46 | ); 47 | } 48 | 49 | const reactCode = ` 50 | import React from 'react' 51 | ${renderData.imports.join('\n')} 52 | 53 | export default class Index extends React.Component { 54 | state = ${JSON.stringify(renderData.data, null, 2)} 55 | ${renderData.formRefs.join('\n')} 56 | ${renderData.lifecycles.join('\n')} 57 | ${renderData.methods.join('\n')} 58 | 59 | render() { 60 | return ( 61 | ${renderData.template} 62 | ) 63 | } 64 | } 65 | `; 66 | return prettierFormat(reactCode, 'react'); 67 | }; 68 | 69 | generateTemplate = (schemaDSL: any) => { 70 | const { 71 | componentName, 72 | props, 73 | children, 74 | dataKey, 75 | dataSource, 76 | lifeCycle, 77 | methods, 78 | imports, 79 | } = schemaDSL || {}; 80 | const objStr = (obj: any) => 81 | Object.entries(obj) 82 | .map(([k, v]) => `${k}="${v}"`) 83 | .join(' '); 84 | 85 | let xml = ''; 86 | switch (componentName) { 87 | case 'Page': 88 | renderData.data = dataSource || {}; 89 | if (dataSource.pagination['currentPage']) { 90 | renderData.data['pagination']['current'] = 91 | dataSource.pagination['currentPage']; 92 | delete renderData.data.pagination.currentPage; 93 | } 94 | this.getLifeCycle(lifeCycle); 95 | this.getMethods(methods); 96 | this.getImports(imports); 97 | const childStr = (children || []) 98 | .map((item: any) => this.generateTemplate(item)) 99 | .join('\n'); 100 | xml = `
\n${childStr}\n
`; 101 | break; 102 | case 'Form': 103 | this.pushComponent('Form'); 104 | this.pushComponent('Row'); 105 | this.pushComponent('Col'); 106 | const formDataKey = dataKey || 'form'; 107 | renderData.data[formDataKey] = {}; 108 | const formRef = `formRef${renderData.antdComponentMap[componentName]}`; 109 | renderData.formRefs.push( 110 | `${formRef} = React.createRef()\n`, 111 | ); 112 | 113 | const initialValues = {}; 114 | 115 | const formItems = (children || []) 116 | .map((item: any) => { 117 | const { key, label, initValue } = item || {}; 118 | const itemPropStr = key 119 | ? `label="${label}" name="${key}"` 120 | : `label="${label}"`; 121 | if (key) { 122 | // @ts-ignore 123 | renderData.data[formDataKey][key] = ''; 124 | if (initValue !== undefined) { 125 | renderData.data[formDataKey][key] = initValue; 126 | initialValues[key] = initValue; 127 | } 128 | } 129 | const itemChildren = (item.children || []) 130 | .map((child: any) => this.generateTemplate(child)) 131 | .join(''); 132 | return ` 133 | 134 | ${itemChildren} 135 | 136 | `; 137 | }) 138 | .join('\n'); 139 | 140 | xml = ` 141 |
147 | 148 | ${formItems} 149 | 150 |
`; 151 | break; 152 | case 'Table': 153 | this.pushComponent(componentName); 154 | const listKey = dataKey || 'list'; 155 | renderData.data[listKey] = []; 156 | const columns = (children || []).map((item: any) => { 157 | return { 158 | ...item, 159 | title: item.label, 160 | dataIndex: item.key, 161 | }; 162 | }); 163 | renderData.data['columns'] = columns; 164 | 165 | xml = ` 166 | \`共 \${s} 条\`, 174 | showSizeChanger: false 175 | } 176 | } 177 | > 178 |
179 | `; 180 | break; 181 | case 'Pagination': 182 | this.getEventStr(schemaDSL, { 183 | onPageChange: 'handleTableChange', 184 | }); 185 | break; 186 | case 'Button': 187 | this.pushComponent(componentName); 188 | const buttonEventStr = this.getEventStr(schemaDSL); 189 | xml = ``; 190 | break; 191 | case 'Input': 192 | this.pushComponent(componentName); 193 | const commonEventStr = this.getEventStr(schemaDSL); 194 | xml = `<${componentName} ${objStr(props)} ${commonEventStr} />`; 195 | break; 196 | default: 197 | xml = ''; 198 | } 199 | 200 | return xml + '\n'; 201 | }; 202 | 203 | pushComponent = (componentName = '') => { 204 | if (!renderData.antdComponentMap[componentName]) { 205 | renderData.antdComponentMap[componentName] = 1; 206 | } else { 207 | renderData.antdComponentMap[componentName]++; 208 | } 209 | }; 210 | 211 | getLifeCycle = (item: object) => { 212 | Object.entries(item).forEach(([_, v]) => { 213 | const { newFunc } = this.transformFunc(v); 214 | // @ts-ignore 215 | renderData.lifecycles.push(newFunc); 216 | }); 217 | }; 218 | 219 | getMethods = (item: object) => { 220 | Object.entries(item).forEach(([_, v]) => { 221 | const { newFunc } = this.transformFunc(v); 222 | // @ts-ignore 223 | renderData.methods.push(newFunc); 224 | }); 225 | }; 226 | 227 | getImports = (item: object) => { 228 | Object.entries(item).forEach(([k, v]) => { 229 | const importStr = `import ${k} from "${v}"`; 230 | // @ts-ignore 231 | renderData.imports.push(importStr); 232 | }); 233 | }; 234 | 235 | getEventStr = (item: object, extraMap?: IObject = {}) => { 236 | let funcStr = ''; 237 | Object.entries(item).forEach(([k, v]) => { 238 | if (typeof v === 'string' && v.includes('function')) { 239 | const { newFunc, newFuncName, args } = this.transformFunc( 240 | v, 241 | extraMap[k], 242 | ); 243 | if ( 244 | k === 'onClick' && 245 | item?.componentName === 'Button' && 246 | item?.props?.htmlType === 'submit' 247 | ) { 248 | // 需要将事件绑定到Form上 249 | submitName = newFuncName; 250 | } else { 251 | funcStr = funcStr ? `${funcStr} ` : funcStr; 252 | funcStr += args 253 | ? `${k}="this.${newFuncName}.bind(this, ${args})"` 254 | : `${k}="this.${newFuncName}.bind(this)"`; 255 | } 256 | // @ts-ignore 257 | renderData.methods.push(newFunc); 258 | } 259 | }); 260 | return funcStr; 261 | }; 262 | 263 | transformFunc = (func: any, newFuncName = '') => { 264 | const funcStr = func.toString(); 265 | const start = funcStr.indexOf('function ') + 9; 266 | const end = funcStr.indexOf('('); 267 | const end2 = funcStr.indexOf(')'); 268 | const funcName = funcStr.slice(start, end); 269 | const args = funcStr.slice(end + 1, end2); 270 | let newFunc = funcStr.slice(start); 271 | if (newFuncName) { 272 | newFunc = newFunc.replace(funcName, newFuncName); 273 | } 274 | return { newFunc, newFuncName: newFuncName || funcName, args }; 275 | }; 276 | 277 | initData = () => { 278 | renderData = { 279 | codeStr: '', 280 | template: '', 281 | imports: [], 282 | data: {}, 283 | methods: [], 284 | lifecycles: [], 285 | styles: [], 286 | antdComponentMap: {}, 287 | formRefs: [], 288 | }; 289 | }; 290 | 291 | handleGenerate = () => { 292 | this.initData(); 293 | const schema = JSON.parse(DSLStr); 294 | console.log('schema', DSLStr); 295 | renderData.template = this.generateTemplate(schema); 296 | renderData.codeStr = this.generateReact(); 297 | console.log('renderData', renderData); 298 | 299 | this.setState({ visible: true }); 300 | }; 301 | 302 | getSourceCode = () => { 303 | this.initData(); 304 | const schema = JSON.parse(DSLStr); 305 | renderData.template = this.generateTemplate(schema); 306 | renderData.codeStr = this.generateReact(); 307 | return renderData.codeStr; 308 | }; 309 | 310 | render() { 311 | const { showGenerateButton } = this.props; 312 | return ( 313 | showGenerateButton && ( 314 | <> 315 | 318 | this.setState({ visible: false })} 323 | width={800} 324 | visible={this.state.visible} 325 | > 326 |
{renderData.codeStr}
327 |
328 | 329 | ) 330 | ); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/pages/setting/const/editDSL.ts: -------------------------------------------------------------------------------- 1 | const DSL = { 2 | componentName: 'div', 3 | componentType: 'native', 4 | type: 'edit', 5 | props: { 6 | className: 'edit-container', 7 | }, 8 | children: [ 9 | { 10 | componentName: 'CrumbBack', 11 | componentType: 'custom', 12 | props: {}, 13 | isEdit: true, 14 | children: '编辑商品', 15 | onClick: `function handleGoBack() { 16 | this.$router.go(-1); 17 | }`, 18 | }, 19 | { 20 | componentName: 'div', 21 | componentType: 'native', 22 | props: { 23 | className: 'pl24 pr24 pb24 pt24 mt24 bgc-fff bshadow edit-content', 24 | }, 25 | children: [ 26 | { 27 | componentName: 'div', 28 | componentType: 'native', 29 | props: { 30 | className: 'info-list bb mb24 pb12', 31 | }, 32 | isEdit: true, 33 | children: [ 34 | { 35 | componentName: 'div', 36 | componentType: 'native', 37 | props: { 38 | className: 'mb12 fs16 fw700', 39 | }, 40 | isEdit: true, 41 | children: '商品信息', 42 | }, 43 | { 44 | componentName: 'Row', 45 | props: { 46 | gutter: 20, 47 | }, 48 | dataKey: 'form', 49 | isEdit: true, 50 | children: [ 51 | { 52 | span: 8, 53 | key: 'sendNo', 54 | label: '出库单号', 55 | renderKey: `renderDefault`, 56 | }, 57 | { 58 | span: 8, 59 | label: '发货人手机号', 60 | key: 'phone', 61 | renderKey: `renderDefault`, 62 | }, 63 | { 64 | span: 8, 65 | label: '退货地址', 66 | key: 'warehouseAddress', 67 | renderKey: `renderEllipsis`, 68 | }, 69 | ], 70 | }, 71 | ], 72 | }, 73 | { 74 | componentName: 'div', 75 | componentType: 'native', 76 | props: { 77 | className: 'info-list bb mb24 pb12', 78 | }, 79 | isEdit: true, 80 | children: [ 81 | { 82 | componentName: 'div', 83 | componentType: 'native', 84 | props: { 85 | className: 'mb12 fs16 fw700', 86 | }, 87 | isEdit: true, 88 | children: '备案信息', 89 | }, 90 | { 91 | componentName: 'Form', 92 | props: { 93 | 'label-width': '130px', 94 | }, 95 | dataKey: 'form', 96 | children: [ 97 | { 98 | label: '实际货号', 99 | key: 'actualArticleNumber', 100 | children: [ 101 | { 102 | componentName: 'Input', 103 | props: { 104 | placeholder: '请输入实际货号', 105 | clearable: true, 106 | }, 107 | }, 108 | ], 109 | }, 110 | { 111 | label: '状态', 112 | key: 'status', 113 | children: [ 114 | { 115 | componentName: 'Select', 116 | props: { 117 | placeholder: '请选择状态', 118 | clearable: true, 119 | }, 120 | options: [ 121 | { value: '0', label: '审批中' }, 122 | { value: '1', label: '已通过' }, 123 | { value: '2', label: '已驳回' }, 124 | ], 125 | }, 126 | ], 127 | }, 128 | { 129 | label: '创建时间', 130 | key: 'createTime', 131 | children: [ 132 | { 133 | componentName: 'RangePicker', 134 | props: {}, 135 | }, 136 | ], 137 | }, 138 | { 139 | label: '商品英文名称', 140 | key: 'productEnName', 141 | children: [ 142 | { 143 | componentName: 'Input', 144 | props: { 145 | placeholder: '请输入商品英文名称', 146 | clearable: true, 147 | }, 148 | }, 149 | ], 150 | }, 151 | ], 152 | }, 153 | ], 154 | }, 155 | { 156 | componentName: 'div', 157 | componentType: 'native', 158 | props: { 159 | className: 'info-list bb mb24 pb12', 160 | }, 161 | isEdit: true, 162 | children: [ 163 | { 164 | componentName: 'div', 165 | componentType: 'native', 166 | props: { 167 | className: 'mb12 fs16 fw700', 168 | }, 169 | isEdit: true, 170 | children: 'SKU列表', 171 | }, 172 | { 173 | componentName: 'Table', 174 | props: { 175 | size: 'small', 176 | border: true, 177 | }, 178 | type: 'editTable', 179 | dataKey: 'dataList', 180 | onSelectionChange: `function handleSelectionChange(rows) { 181 | this.multipleSelection = rows 182 | }`, 183 | children: [ 184 | { 185 | key: 'id', 186 | label: '序号', 187 | }, 188 | { 189 | key: 'orderNo', 190 | label: '订单号', 191 | children: [ 192 | { 193 | componentName: 'Input', 194 | props: { 195 | placeholder: '请输入', 196 | clearable: true, 197 | }, 198 | }, 199 | ], 200 | }, 201 | { 202 | key: 'trueName', 203 | label: '姓名', 204 | children: [ 205 | { 206 | componentName: 'Input', 207 | props: { 208 | placeholder: '请输入', 209 | clearable: true, 210 | }, 211 | }, 212 | ], 213 | }, 214 | { 215 | key: 'status', 216 | label: '校验状态', 217 | children: [ 218 | { 219 | componentName: 'Select', 220 | props: { 221 | placeholder: '请选择', 222 | clearable: true, 223 | }, 224 | options: [ 225 | { value: '0', label: '审批中' }, 226 | { value: '1', label: '已通过' }, 227 | { value: '2', label: '已驳回' }, 228 | ], 229 | }, 230 | ], 231 | }, 232 | ], 233 | }, 234 | ], 235 | }, 236 | ], 237 | }, 238 | { 239 | componentName: 'div', 240 | componentType: 'native', 241 | props: { 242 | className: 'footer-block', 243 | }, 244 | isEdit: true, 245 | children: [ 246 | { 247 | componentName: 'div', 248 | componentType: 'native', 249 | props: { 250 | className: 'footer-con', 251 | }, 252 | isEdit: true, 253 | children: [ 254 | { 255 | componentName: 'Button', 256 | props: { 257 | type: 'text', 258 | }, 259 | children: '取消', 260 | onClick: `function handleGoBack() { 261 | this.$router.go(-1); 262 | }`, 263 | }, 264 | { 265 | componentName: 'Button', 266 | props: { 267 | type: 'primary', 268 | }, 269 | dataKey: 'submitLoading', 270 | children: '提交', 271 | onClick: `function handleSubmit() { 272 | if (this.submitLoading) return 273 | this.$refs.formRef.validate(async valid => { 274 | console.log('this.form', this.form) 275 | if (!valid) return false 276 | 277 | const params = { 278 | ...this.form, 279 | } 280 | params.crossStatus = Number(params.crossStatus) 281 | params.detailList = this.multipleSelection 282 | deleteEmptyParam(params) 283 | 284 | const { code } = await API.updateRecord(params) 285 | if (code === 200) { 286 | this.$message.success('提交成功') 287 | this.isReturnDirect = true 288 | this.handleGoBack() 289 | } 290 | }) 291 | }`, 292 | }, 293 | ], 294 | }, 295 | ], 296 | }, 297 | ], 298 | dataSource: { 299 | colProps: { 300 | xs: 24, 301 | sm: 12, 302 | lg: 8, 303 | xl: 8, 304 | }, 305 | submitLoading: false, 306 | }, 307 | lifeCycle: { 308 | mounted: `function mounted() { 309 | this.getRecordDetail(); 310 | }`, 311 | beforeRouteLeave: `function beforeRouteLeave(to, from, next) { 312 | if (this.isReturnDirect) { 313 | next(true) 314 | } else { 315 | this.$modal 316 | .confirm('返回将不保留已生成结果。', '确认返回吗?', { 317 | confirmButtonText: '确定', 318 | cancelButtonText: '取消', 319 | type: 'warning', 320 | }) 321 | .then(() => { 322 | next(true) 323 | }) 324 | .catch(e => { 325 | next(false) 326 | }) 327 | } 328 | }`, 329 | }, 330 | methods: { 331 | getRecordDetail: `async function getRecordDetail() { 332 | const params = { id: this.recordId } 333 | const { code, data } = await API.getRecordDetail(params) 334 | if (code === 200 && data) { 335 | this.form = data 336 | } 337 | }`, 338 | }, 339 | imports: { 340 | '{ deleteEmptyParam }': '@/utils', 341 | '* as API': './api', 342 | }, 343 | apis: { 344 | imports: { 345 | UmiRequest: '@du/umi-request', 346 | }, 347 | updateRecord: `function updateRecord(params) { 348 | return UmiRequest.request({ 349 | method: 'POST', 350 | url: '/api/v1/h5/oversea/backend/product/update', 351 | data: params, 352 | }) 353 | }`, 354 | getRecordDetail: `function getRecordDetail(params) { 355 | return UmiRequest.request({ 356 | method: 'POST', 357 | url: '/api/v1/h5/oversea/backend/product/detail', 358 | data: params, 359 | }) 360 | }`, 361 | }, 362 | }; 363 | 364 | export { DSL }; 365 | -------------------------------------------------------------------------------- /src/pages/setting/model/componentXML.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-组件源码片段 3 | */ 4 | export const VueXML: any = { 5 | VueTemplate: (renderData: any) => { 6 | return ` 7 | 10 | 11 | 35 | 36 | 39 | `; 40 | }, 41 | CrumbBack: (attrStr: any, childStr: any) => { 42 | return ` 43 |
44 | 45 | ${childStr} 46 |
47 | `; 48 | }, 49 | StatusTag: (status: any, tagObj: any) => { 50 | return ` 54 | {{ (${tagObj}[${status}] || {}).value }} 55 | 56 | `; 57 | }, 58 | CreateDom: (name: any, attrStr: any, childStr: any) => { 59 | return `<${name} ${attrStr}> 60 | ${childStr} 61 | `; 62 | }, 63 | }; 64 | 65 | /** 66 | * vue3-组件源码片段 67 | * https://vue3js.cn/docs/zh/style-guide/#%E7%BB%84%E4%BB%B6-%E5%AE%9E%E4%BE%8B%E7%9A%84%E9%80%89%E9%A1%B9%E9%A1%BA%E5%BA%8F%E6%8E%A8%E8%8D%90 68 | */ 69 | export const Vue3XML: any = { 70 | Vue3Template: (renderData: any) => { 71 | return ` 72 | 75 | 106 | 107 | 110 | `; 111 | }, 112 | CrumbBack: (attrStr: any, childStr: any) => { 113 | return ` 114 |
115 | 116 | ${childStr} 117 |
118 | `; 119 | }, 120 | StatusTag: (status: any, tagObj: any) => { 121 | return ` 125 | {{ (${tagObj}[${status}] || {}).value }} 126 | 127 | `; 128 | }, 129 | CreateDom: (name: any, attrStr: any, childStr: any) => { 130 | return `<${name} ${attrStr}> 131 | ${childStr} 132 | `; 133 | }, 134 | }; 135 | 136 | /** 137 | * react-组件源码片段 138 | */ 139 | export const ReactXML: any = { 140 | ReactTemplate: (renderData: any) => { 141 | return ` 142 | ${renderData.imports.join(';\n')}; 143 | ${ 144 | renderData.constOptions.length 145 | ? renderData.constOptions.join(';\n') + ';' 146 | : '' 147 | } 148 | const Index = (props) => { 149 | ${ 150 | renderData.componentProps.length 151 | ? renderData.componentProps.join(';\n') + ';' 152 | : '' 153 | } 154 | ${renderData.useStates.join(';\n')}; 155 | ${renderData.formRefs.length ? renderData.formRefs.join(';\n') + ';' : ''} 156 | ${ 157 | renderData.lifeCycles.length 158 | ? renderData.lifeCycles.join(';\n') + ';' 159 | : '' 160 | } 161 | ${renderData.methods.join(';\n')}; 162 | 163 | return ( 164 | ${renderData.template} 165 | ) 166 | } 167 | `; 168 | }, 169 | CrumbBack: (attrStr: any, childStr: any) => { 170 | return ` 171 |
172 | 173 | ${childStr} 174 |
175 | `; 176 | }, 177 | StatusTag: (status: any, tagObj: any) => { 178 | return `${tagObj}[${status}] && 180 | { (${tagObj}[${status}] || {}).value } 181 | 182 | `; 183 | }, 184 | CreateDom: (name: any, attrStr: any, childStr: any) => { 185 | return `<${name} ${attrStr}> 186 | ${childStr} 187 | `; 188 | }, 189 | }; 190 | 191 | /** 192 | * 表格渲染函数-源码片段 193 | */ 194 | export const VueTableRenderXML: any = { 195 | renderTime: (key: string, obj = 'row') => { 196 | return `{{ ${obj}.${key} | formatTime }}`; 197 | }, 198 | renderAmount: (key: string, obj = 'row') => { 199 | return `{{ ${obj}.${key} ? Number(${obj}.${key}) / 100 : '-' }}`; 200 | }, 201 | renderEllipsis: (key: string, obj = 'row') => { 202 | return ``; 203 | }, 204 | renderEnum: (key: string, obj = 'row') => { 205 | return `{{ ${key}Obj && ${key}Obj[${obj}.${key}] || '-' }}`; 206 | }, 207 | renderDefault: (key: string, obj = 'row') => { 208 | return `{{ ${obj}.${key} }}`; 209 | }, 210 | }; 211 | 212 | /** 213 | * 表格渲染函数-源码片段 214 | */ 215 | export const Vue3TableRenderXML: any = { 216 | renderTime: (key: string, obj = 'row') => { 217 | return `{{ ${obj}.${key} | formatTime }}`; 218 | }, 219 | renderAmount: (key: string, obj = 'row') => { 220 | return `{{ ${obj}.${key} ? Number(${obj}.${key}) / 100 : '-' }}`; 221 | }, 222 | renderEllipsis: (key: string, obj = 'row') => { 223 | return ``; 224 | }, 225 | renderEnum: (key: string, obj = 'row') => { 226 | return `{{ ${key}Obj && ${key}Obj[${obj}.${key}] || '-' }}`; 227 | }, 228 | renderDefault: (key: string, obj = 'row') => { 229 | return `{{ ${obj}.${key} }}`; 230 | }, 231 | }; 232 | 233 | /** 234 | * react表格渲染函数-源码片段 235 | */ 236 | export const ReactTableRenderXML: any = { 237 | renderTime: (key: string, obj = 'row') => { 238 | return `{ ${obj}.${key} ? moment(${obj}.${key}).format('YYYY-MM-DD HH:mm:ss') : '' }`; 239 | }, 240 | renderAmount: (key: string, obj = 'row') => { 241 | return `{ ${obj}.${key} ? Number(${obj}.${key}) / 100 : '-' }`; 242 | }, 243 | renderEllipsis: (key: string, obj = 'row') => { 244 | return `{ ${obj}.${key} }`; 245 | }, 246 | renderEnum: (key: string, obj = 'row') => { 247 | return `{ ${key}Obj && ${key}Obj[${obj}.${key}] || '-' }`; 248 | }, 249 | renderDefault: (key: string, obj = 'row') => { 250 | return `{ ${obj}.${key} }`; 251 | }, 252 | }; 253 | 254 | /** 255 | * 样式-组件源码片段 256 | */ 257 | export const styleXML: any = { 258 | list: () => { 259 | return ` 260 | .page-container { 261 | box-sizing: border-box; 262 | font-family: PingFangSC-Regular; 263 | .bshadow { 264 | border-radius: 2px; 265 | box-shadow: 0px 2px 4px 0px #0000001a; 266 | } 267 | .f1 { 268 | flex: 1; 269 | min-width: 0; 270 | } 271 | .lh50 { 272 | line-height: 50px; 273 | } 274 | .tar { 275 | text-align: right; 276 | } 277 | } 278 | `; 279 | }, 280 | edit: () => { 281 | return ` 282 | .edit-container { 283 | .go-back { 284 | display: flex; 285 | align-items: center; 286 | i { 287 | font-size: 22px; 288 | cursor: pointer; 289 | } 290 | i:hover { 291 | color: #01c2c3; 292 | } 293 | .bread { 294 | font-size: 14px; 295 | margin-left: 8px; 296 | } 297 | } 298 | .footer-block { 299 | position: sticky; 300 | bottom: 0px; 301 | height: 64px; 302 | padding: 0 24px; 303 | background-color: #ffffff; 304 | box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15); 305 | z-index: 10; 306 | .footer-con { 307 | height: 64px; 308 | line-height: 64px; 309 | display: flex; 310 | align-items: center; 311 | justify-content: flex-end; 312 | } 313 | } 314 | .edit-content { 315 | min-height: calc(100vh - 204px); 316 | } 317 | .bb { 318 | border-bottom: solid 1px #f5f5f9; 319 | } 320 | .bb:last-child { 321 | border-bottom: none; 322 | } 323 | .f1 { 324 | flex: 1; 325 | min-width: 0; 326 | } 327 | .bshadow { 328 | border-radius: 2px; 329 | box-shadow: 0px 2px 4px 0px #0000001a; 330 | } 331 | .info-list { 332 | color: #2b2c3c; 333 | .title, 334 | span { 335 | color: #7f7f8e; 336 | } 337 | .el-col { 338 | padding: 12px 0; 339 | } 340 | } 341 | .el-form { 342 | .el-col { 343 | padding: 0px; 344 | } 345 | } 346 | } 347 | `; 348 | }, 349 | editModal: () => { 350 | return ` 351 | .detail-container { 352 | .info-list { 353 | color: #2b2c3c; 354 | .title, 355 | span { 356 | color: #7f7f8e; 357 | } 358 | .el-col { 359 | padding: 12px 0; 360 | } 361 | } 362 | .mt-8 { 363 | margin-top: -8px; 364 | } 365 | .bb { 366 | border-bottom: solid 1px #f5f5f9; 367 | } 368 | .bb:last-child { 369 | border-bottom: none; 370 | } 371 | .f1 { 372 | flex: 1; 373 | min-width: 0; 374 | } 375 | .bshadow { 376 | border-radius: 2px; 377 | box-shadow: 0px 2px 4px 0px #0000001a; 378 | } 379 | .lh1 { 380 | line-height: 1; 381 | } 382 | .tar { 383 | text-align: right; 384 | } 385 | } 386 | `; 387 | }, 388 | detail: () => { 389 | return ` 390 | .detail-container { 391 | .info-list { 392 | color: #2b2c3c; 393 | .title, 394 | span { 395 | color: #7f7f8e; 396 | } 397 | .el-col { 398 | padding: 12px 0; 399 | } 400 | } 401 | .pro-img { 402 | width: 60px; 403 | height: 60px; 404 | img { 405 | width: 100%; 406 | vertical-align: middle; 407 | } 408 | } 409 | .mt-8 { 410 | margin-top: -8px; 411 | } 412 | .bb { 413 | border-bottom: solid 1px #f5f5f9; 414 | } 415 | .bb:last-child { 416 | border-bottom: none; 417 | } 418 | .f1 { 419 | flex: 1; 420 | min-width: 0; 421 | } 422 | .bshadow { 423 | border-radius: 2px; 424 | box-shadow: 0px 2px 4px 0px #0000001a; 425 | } 426 | .lh1 { 427 | line-height: 1; 428 | } 429 | } 430 | `; 431 | }, 432 | }; 433 | --------------------------------------------------------------------------------