├── mock └── .gitkeep ├── scripts ├── site │ ├── CNAME │ ├── spa-gh-pages.js │ ├── gh-pages-publish.js │ └── 404.html ├── git │ ├── utils.js │ ├── commit-message.json │ ├── commit-msg.js │ └── validate-commit-message.js └── build.js ├── types └── global.d.ts ├── screenshots ├── image1.jpg ├── image2.jpg ├── image3.jpg ├── image4.png ├── image5.jpg └── image6.jpg ├── .prettierignore ├── .prettierrc ├── src ├── shared │ ├── config.json │ ├── storage │ │ ├── interface.ts │ │ ├── types.ts │ │ ├── index.ts │ │ └── __tests__ │ │ │ └── index.test.ts │ ├── swaggerUrl.ts │ ├── getResponseParams.ts │ ├── getApiNameAsPageName.ts │ ├── codesandbox │ │ ├── indexHtml.ts │ │ ├── mainJs.ts │ │ ├── packageJson.ts │ │ └── index.ts │ ├── ts │ │ ├── api-interface.ts │ │ ├── custom.ts │ │ └── settings.ts │ ├── baidu-translate.ts │ ├── fetch │ │ └── requestToBody.ts │ ├── useBus.ts │ ├── common.ts │ ├── getParameterObject.ts │ ├── vscode │ │ └── index.ts │ └── searchPageText.ts ├── stores │ ├── index.ts │ ├── swagger.store.ts │ ├── settings.store.ts │ └── custom.store.ts ├── pages │ ├── swagger-codegen │ │ ├── index.module.less │ │ ├── code-generate │ │ │ ├── generate-transform-text.ts │ │ │ ├── avue │ │ │ │ ├── generate-avue-columns-transcoding.ts │ │ │ │ ├── generate-avue-transcode-all.ts │ │ │ │ ├── generate-avue-pro-table.ts │ │ │ │ ├── generate-avue-api-request.js │ │ │ │ ├── generate-avue-form-columns.ts │ │ │ │ ├── generate-avue-search-columns.ts │ │ │ │ ├── generate-avue-form.ts │ │ │ │ ├── generate-avue-table.ts │ │ │ │ ├── generate-avue-table-columns.ts │ │ │ │ └── generate-avue-table-custom.js │ │ │ ├── generate-split-transform-text.ts │ │ │ ├── generate-base-object.ts │ │ │ ├── generate-transform-text-by-form.ts │ │ │ ├── generate-api-const-name.ts │ │ │ ├── generate-assemblies-utils-fn.ts │ │ │ ├── generate-typescript-type.ts │ │ │ ├── generate-model-class.ts │ │ │ ├── generate-api-notes.ts │ │ │ ├── generate-table-columns-props.ts │ │ │ ├── generate-api-defineition.ts │ │ │ ├── generate-api-axios-request.ts │ │ │ ├── generate-model-form-items-code.ts │ │ │ ├── generate-rhtable-page.ts │ │ │ ├── generate-react-antd-page-transcoding-all.ts │ │ │ ├── generate-element-table-form-transcoding.ts │ │ │ ├── generate-extract-baidu-ocrapi.ts │ │ │ ├── generate-rhtable-page-transcoding.ts │ │ │ ├── generate-api-group-object.ts │ │ │ └── generate-enum-code.ts │ │ ├── ApiurlPrefixDrawer.tsx │ │ ├── CodeGenDropdown.tsx │ │ ├── components │ │ │ ├── TextTransformDropdown.tsx │ │ │ ├── HistoryTextDropdown.tsx │ │ │ └── SearchFixedBox.tsx │ │ ├── ModelCodeDrawer.tsx │ │ ├── BaseConfigDrawer.tsx │ │ ├── ParameterTableDefinition.tsx │ │ ├── useHeader.tsx │ │ ├── parametersSelect.tsx │ │ ├── ApiDefinitionDropdown.tsx │ │ ├── ApiurlPrefixEditTable.tsx │ │ ├── CustomMethodsDrawer.tsx │ │ ├── EditCustomMethodDrawer.tsx │ │ ├── ResourcesTree.tsx │ │ └── EditableTable.tsx │ ├── index.tsx │ └── index.less ├── components │ └── DefaultDrawer.tsx └── models │ └── useApiSwitchModel.ts ├── typings.d.ts ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── .umirc.ts ├── .github └── workflows │ ├── test.yml │ ├── pr.yml │ └── deploy.yml ├── package.json └── README.md /mock/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/site/CNAME: -------------------------------------------------------------------------------- 1 | codegen.leekhub.com 2 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare function acquireVsCodeApi(): any; 2 | -------------------------------------------------------------------------------- /screenshots/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image1.jpg -------------------------------------------------------------------------------- /screenshots/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image2.jpg -------------------------------------------------------------------------------- /screenshots/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image3.jpg -------------------------------------------------------------------------------- /screenshots/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image4.png -------------------------------------------------------------------------------- /screenshots/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image5.jpg -------------------------------------------------------------------------------- /screenshots/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RootLinkFE/roothub-codegen/HEAD/screenshots/image6.jpg -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ocrApi": { 3 | "url": "https://api.ocr.space/parse/image", 4 | "apikey": "K84668927688957" 5 | }, 6 | "storeTipsInVscode": "数据存储于vscode.config", 7 | "storeTipsInBrowser": "数据存储于localStorage" 8 | } 9 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import swagger from './swagger.store'; 2 | import settings from './settings.store'; 3 | import custom from './custom.store'; 4 | 5 | const store = { 6 | swagger, 7 | settings, 8 | custom, 9 | }; 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/shared/storage/interface.ts: -------------------------------------------------------------------------------- 1 | import { ValueType } from './types'; 2 | 3 | export interface IStorage { 4 | set: (key: string, value: ValueType) => void; 5 | get: (key: string) => T; 6 | has: (key: string) => boolean; 7 | remove: (key: string) => void; 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/git/utils.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').execSync; 2 | 3 | function getRespGitBranch() { 4 | const name = exec('git rev-parse --abbrev-ref HEAD').toString().trim(); 5 | // console.log(`当前分支:${name}`); 6 | return name; 7 | } 8 | 9 | module.exports = { getRespGitBranch }; 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/shared/swaggerUrl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description: defaultSwaggerUrl 5 | */ 6 | export const defaultSwaggerUrl = 'https://petstore.swagger.io/v2/swagger.json'; 7 | 8 | const apiUrlList = [defaultSwaggerUrl]; 9 | 10 | export default apiUrlList; 11 | -------------------------------------------------------------------------------- /.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 | 8 | # production 9 | /dist 10 | 11 | # misc 12 | .DS_Store 13 | 14 | # umi 15 | /src/.umi 16 | /src/.umi-production 17 | /src/.umi-test 18 | /.env.local 19 | -------------------------------------------------------------------------------- /src/shared/getResponseParams.ts: -------------------------------------------------------------------------------- 1 | import getParameterObject from './getParameterObject'; 2 | 3 | export default function getResponseParams(api: any, resourceDetail: any) { 4 | if (!api.responses['200']) { 5 | return []; 6 | } 7 | return ( 8 | getParameterObject(resourceDetail, api.responses['200']).children || [] 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/getApiNameAsPageName.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'lodash'; 2 | 3 | export default function getApiNameAsPageName(apiPath: string) { 4 | const api = apiPath.replace(/\/api/, ''); 5 | const camelCaseApiName = camelCase(api); 6 | return `${camelCaseApiName 7 | .substring(0, 1) 8 | .toUpperCase()}${camelCaseApiName.substring(1)}`; 9 | } 10 | -------------------------------------------------------------------------------- /scripts/git/commit-message.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxLength": 150, 3 | "types": [ 4 | "build", 5 | "ci", 6 | "docs", 7 | "feat", 8 | "fix", 9 | "perf", 10 | "refactor", 11 | "release", 12 | "style", 13 | "test", 14 | "chore", 15 | "revert", 16 | "Merge" 17 | ], 18 | "scopes": ["showcase", "packaging", "changelog", "schematics", "*"] 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/index.module.less: -------------------------------------------------------------------------------- 1 | .h2Title { 2 | font-size: 20px; 3 | margin: 12px; 4 | } 5 | 6 | .h2BorderTitle { 7 | font-size: 20px; 8 | margin: 12px; 9 | position: relative; 10 | &::before { 11 | position: absolute; 12 | content: ''; 13 | top: 0; 14 | left: -12px; 15 | width: 4px; 16 | height: 32px; 17 | background: var(--antd-wave-shadow-color) 100%; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/codesandbox/indexHtml.ts: -------------------------------------------------------------------------------- 1 | const indexHtml = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 | 12 |
13 | 14 | 15 | `; 16 | 17 | export default indexHtml; 18 | -------------------------------------------------------------------------------- /src/shared/codesandbox/mainJs.ts: -------------------------------------------------------------------------------- 1 | const MainJs = ` 2 | /** 3 | * This is an auto-generated demo by codegen 4 | * if you think it is not working as expected, 5 | * please report the issue at 6 | * http://codegen.leekhub.com/codegen/ 7 | **/ 8 | import React from "react"; 9 | import ReactDOM from "react-dom"; 10 | import "antd/dist/antd.css"; 11 | import "@ant-design/pro-table/dist/table.css"; 12 | import App from "./App"; 13 | 14 | ReactDOM.render(, document.getElementById("root")); 15 | `; 16 | 17 | export default MainJs; 18 | -------------------------------------------------------------------------------- /src/shared/storage/types.ts: -------------------------------------------------------------------------------- 1 | export type StoreType = typeof window.localStorage & 2 | typeof window.sessionStorage; 3 | 4 | export type ValueType = 5 | | Record 6 | | number 7 | | string 8 | | undefined 9 | | null; 10 | 11 | export type StorageOptions = { 12 | /** 13 | * 前缀 14 | */ 15 | prefix?: string; 16 | /** 17 | * storage 类型 18 | */ 19 | store?: StoreType; 20 | /** 21 | * 过期时间, 秒(s) 22 | */ 23 | expires?: number; 24 | }; 25 | 26 | export type StoreDataType = { value: ValueType; expires?: number }; 27 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const { getRespGitBranch } = require('./git/utils'); 2 | const child_process = require('child_process'); 3 | const execSync = child_process.execSync; 4 | 5 | const branchName = getRespGitBranch(); 6 | 7 | if (branchName === 'master') { 8 | buildCommand = 'npm run build:prod'; 9 | } 10 | 11 | // console.log(process.env, process.env.ENV); 12 | // 不同环境构建配置,CD脚本都是 npm run build 13 | let buildCommand = `npm run build:${process.env.ENV}`; 14 | 15 | console.log(branchName, '打包命令:', buildCommand); 16 | 17 | execSync(buildCommand, { stdio: [0, 1, 2] }); 18 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-transform-text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: generateTransformText 剔除非中文及英文 3 | * @return {string[] | string} 4 | */ 5 | 6 | export default function generateTransformTextToZhAndEn(values: any) { 7 | const reg = new RegExp(/[^a-zA-Z\u4e00-\u9fa5]/g); // 非中英文 8 | if (values instanceof Array) { 9 | values = values.map((v: any) => { 10 | return v.replace(reg, ''); 11 | }); 12 | } 13 | if (typeof values === 'string') { 14 | values = values.replace(reg, '').split(','); 15 | } 16 | return values; 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-columns-transcoding.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-04-23 09:17:07 4 | * @Description: avueColumnTranscoding 5 | */ 6 | import { transcoding } from '../generate-rhtable-page-transcoding'; 7 | 8 | const avueColumnTranscoding = (...argetment: [body: any, record?: any, api?: any, selectedData?: any]) => { 9 | return transcoding( 10 | { 11 | labelField: 'label', 12 | propField: 'prop', 13 | }, 14 | ...argetment, 15 | ); 16 | }; 17 | 18 | export default avueColumnTranscoding; 19 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import GenCode from './swagger-codegen'; 2 | import { isInVSCode, setupBackgroundManagement, postVSCodeMessage } from '@/shared/vscode'; 3 | import './index.less'; 4 | import utilsFn from './swagger-codegen/code-generate/generate-assemblies-utils-fn'; 5 | import lodash from 'lodash'; 6 | // vscode 7 | isInVSCode && setupBackgroundManagement(); 8 | 9 | export default function IndexPage() { 10 | if (window) { 11 | (window as any).utilsFn = utilsFn; 12 | (window as any).lodash = lodash; 13 | } 14 | isInVSCode && postVSCodeMessage('pageReady'); 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-transcode-all.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-15 09:17:07 4 | * @Description: generate-avue-transcode-all 5 | */ 6 | import { transcodingAll } from '../generate-react-antd-page-transcoding-all'; 7 | 8 | const generatAvueTranscodeAll = (...argetment: [body: any, record?: any, api?: any, selectedData?: any]) => { 9 | return transcodingAll( 10 | { 11 | labelField: 'label', 12 | propField: 'prop', 13 | }, 14 | ...argetment, 15 | ); 16 | }; 17 | 18 | export default generatAvueTranscodeAll; 19 | -------------------------------------------------------------------------------- /src/components/DefaultDrawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-02-01 15:45:05 4 | * @Description: 默认抽屉90%弹窗 5 | */ 6 | import { Drawer, DrawerProps } from 'antd'; 7 | import { observer } from 'mobx-react-lite'; 8 | 9 | const DefaultDrawer: React.FC = (props) => { 10 | const { ...drawerProps } = props; 11 | 12 | return ( 13 | <> 14 | 15 | {props.children} 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default observer(DefaultDrawer); 22 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-split-transform-text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: generateSplitTransformText 分割剔除非中文及英文 3 | * @return {string[] | string} 4 | */ 5 | 6 | export default function generateSplitTransformTextToZhAndEn(values: any) { 7 | if (typeof values === 'string') { 8 | values = values.split(','); 9 | } 10 | if (values instanceof Array) { 11 | let result: string[] = []; 12 | values = values.forEach((v: any) => { 13 | const arr = v.split(/[^a-zA-Z\u4e00-\u9fa5]+/); // 非中英文 14 | result = [...result, ...arr]; 15 | }); 16 | return result.filter((item) => item !== ''); 17 | } 18 | return values; 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/codesandbox/packageJson.ts: -------------------------------------------------------------------------------- 1 | const packageJson = { 2 | name: 'codegen-react-demo', 3 | main: 'index.tsx', 4 | dependencies: { 5 | antd: '^4.14.0', 6 | react: '^17.0.0', 7 | 'react-dom': '^17.0.0', 8 | '@ant-design/icons': '4.7.0', 9 | '@ant-design/pro-table': '2.60.0', 10 | '@roothub/components': '0.2.15', 11 | }, 12 | devDependencies: { 13 | typescript: '^4.2.2', 14 | }, 15 | keywords: ['react', 'antd'], 16 | description: 'React.js example by codegen', 17 | }; 18 | 19 | export default function genJson(dependencies = {}) { 20 | packageJson.dependencies = Object.assign( 21 | packageJson.dependencies, 22 | dependencies, 23 | ); 24 | return JSON.stringify(packageJson, null, 2); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/git/commit-msg.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // from https://github.com/angular/angular/blob/master/scripts/git/commit-msg.js 4 | 5 | 'use strict'; 6 | 7 | const fs = require('fs'); 8 | const checkMsg = require('./validate-commit-message'); 9 | const msgFile = process.argv[2]; 10 | 11 | let isValid = true; 12 | if (msgFile || true) { 13 | const commitMsg = fs.readFileSync(msgFile, { encoding: 'utf-8' }); 14 | const firstLine = commitMsg.split('\n')[0]; 15 | isValid = checkMsg(firstLine); 16 | if (!isValid) { 17 | console.error( 18 | '\x1b[36mCheck CONTRIBUTING.md at the root of the repo for more information.(请查看根目录下的 CONTRIBUTING.md 获取更多信息)\x1b[0m\n', 19 | ); 20 | } 21 | } 22 | 23 | process.exit(isValid ? 0 : 1); 24 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ApiurlPrefixDrawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-11-25 11:22:05 4 | * @Description: apiurl默认前缀-设置抽屉弹窗 5 | */ 6 | import { Drawer, DrawerProps } from 'antd'; 7 | import ApiurlPrefixEditTable from './ApiurlPrefixEditTable'; 8 | import { observer } from 'mobx-react-lite'; 9 | 10 | const ApiurlPrefixDrawer: React.FC = (props) => { 11 | const { ...drawerProps } = props; 12 | 13 | return ( 14 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default observer(ApiurlPrefixDrawer); 28 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-base-object.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-26 14:46:27 4 | * @Description: generateCodeGenObject 5 | */ 6 | import { cleanParameterDescription } from '@/shared/utils'; 7 | 8 | // 基础对象 9 | export default function generateCodeGenObject( 10 | selectedData: any, 11 | apiData: { api: string; description: string; summary: string; children: any[] }, 12 | ) { 13 | const columns: any[] = []; 14 | let body: any[] = Array.isArray(selectedData) ? selectedData : apiData?.children || []; 15 | 16 | body.map((row) => { 17 | columns.push(`${row.name}: '', ${row.description ? '// ' + cleanParameterDescription(row.description) : ''}`); 18 | }); 19 | return `{ 20 | ${columns.reduce((a, b) => a + '\n ' + b)} 21 | }`; 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-transform-text-by-form.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: generateTransformText 剔除非中文及英文, 及剔除表单的包含请输出、请输入字段 3 | * @return {string[] | string} 4 | */ 5 | 6 | export default function generateTransformTextToZhAndEn(values: any) { 7 | const reg = new RegExp(/[^a-zA-Z\u4e00-\u9fa5]/g); 8 | if (values instanceof Array) { 9 | values = values.map((v: any) => { 10 | return v.replace(reg, ''); 11 | }); 12 | 13 | let result = []; 14 | for (let i = 0; i < values.length; i++) { 15 | const text = values[i]; 16 | if (!/[请输入|请选择]/.test(text)) { 17 | result.push(text); 18 | } 19 | } 20 | return result; 21 | } 22 | 23 | if (typeof values === 'string') { 24 | return values.replace(reg, '').split(','); 25 | } 26 | return values; 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "importHelpers": true, 8 | "jsx": "react-jsx", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "strict": true, 13 | "paths": { 14 | "@/*": ["src/*"], 15 | "@@/*": ["src/.umi/*"] 16 | }, 17 | "allowSyntheticDefaultImports": true 18 | }, 19 | "include": [ 20 | "types", 21 | "mock/**/*", 22 | "src/**/*", 23 | "config/**/*", 24 | ".umirc.ts", 25 | "typings.d.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "lib", 30 | "es", 31 | "dist", 32 | "typings", 33 | "**/__test__", 34 | "test", 35 | "docs", 36 | "tests" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/shared/ts/api-interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-05 14:54:37 4 | * @Description: api-ts-接口声明 5 | */ 6 | export interface resourceItems { 7 | location: string; 8 | name: string; 9 | swaggerVersion: string; 10 | url: string; 11 | header?: string; 12 | paths: resourceItems[]; 13 | } 14 | 15 | export interface basePathsItem { 16 | description: string; 17 | api: string; 18 | method: string; 19 | operationId: string; 20 | parameters: any[]; 21 | produces: ['*/*']; 22 | responses: any[]; 23 | summary: string; 24 | tags: string[]; 25 | } 26 | 27 | export interface pathsItem extends basePathsItem { 28 | uuid: string; 29 | methodUpper: string; 30 | } 31 | 32 | export interface tagsItem { 33 | description: string; 34 | name: string; 35 | paths: pathsItem[]; 36 | } 37 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | // const basePath = '/'; 3 | 4 | export default defineConfig({ 5 | favicon: 'https://avatars.githubusercontent.com/u/76474279?s=200&v=4', 6 | devServer: { 7 | port: 3031, 8 | }, 9 | title: 'CodeGen', 10 | // base: basePath, 11 | // publicPath: basePath, 12 | nodeModulesTransform: { 13 | type: 'none', 14 | }, 15 | proxy: { 16 | // 'http://youip/swagger-resources': { 17 | // target: 'http://youip/swagger-resources', 18 | // changeOrigin: true, 19 | // }, 20 | }, 21 | history: { 22 | // 默认的类型是 `browser`,但是由于 vscode webview 环境不存在浏览器路由,改成 `memory` 和 `hash` 都可以(这里坑了好久) 23 | type: 'memory', 24 | }, 25 | // 兼容VSCode开发 26 | outputPath: process.env.APP_TYPE === 'site' ? 'dist' : '../../templates/codegen', 27 | routes: [{ path: '/', component: '@/pages/index' }], 28 | fastRefresh: {}, 29 | }); 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Test 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow 8 | push: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | Test: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - name: Checkout 🛎️ 24 | uses: actions/checkout@v2 25 | 26 | - name: Install 🔧 27 | run: npx yarn 28 | 29 | - name: Test ⛏️ 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /src/shared/ts/custom.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-13 10:30:49 4 | * @Description: Custom-ts-声明 5 | */ 6 | 7 | export type types = ['api', 'model', 'request', 'response']; 8 | 9 | export interface CustomMethodsItem { 10 | key: string; 11 | label: string; 12 | language?: string; // CodeMirror.language 13 | description?: string; 14 | type: string; // types 15 | source: string; // root、soucre 16 | status: number; // 0 #未启用 1#启用; 17 | sort: number; 18 | function: string | void; 19 | } 20 | 21 | export interface CustomTypeMethods { 22 | model: { 23 | key: 'model'; 24 | description: string; 25 | list: CustomMethodsItem[]; 26 | }; 27 | request: { 28 | key: 'request'; 29 | description: string; 30 | list: CustomMethodsItem[]; 31 | }; 32 | response: { 33 | key: 'response'; 34 | description: string; 35 | list: CustomMethodsItem[]; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: PR 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | Test: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - name: Checkout 🛎️ 24 | uses: actions/checkout@v2 25 | 26 | - name: Install 🔧 27 | run: npx yarn 28 | 29 | - name: Test ⛏️ 30 | run: npm run test 31 | 32 | - name: Build Test ⛏️ 33 | run: npm run build:web 34 | -------------------------------------------------------------------------------- /src/shared/baidu-translate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-04 11:32:36 4 | * @Description: 百度翻译-处理 5 | */ 6 | import { requestToBody } from '@/shared/fetch/requestToBody'; 7 | import state from '@/stores/index'; 8 | const SparkMD5 = require('spark-md5/spark-md5.js'); 9 | 10 | const qfrom = 'zh'; 11 | const qto = 'en'; 12 | const apiUrl = 'https://fanyi-api.baidu.com/api/trans/vip/translate'; 13 | 14 | // 存在跨域限制 15 | // https://fanyi-api.baidu.com/doc/21 16 | 17 | /** 18 | * @description: 百度翻译中文转英文 19 | * @param {string} q \n分割多个文本 20 | * @return {*} 21 | */ 22 | export const translateZhToEn = (q: string) => { 23 | const appid = state.settings.Settings.baiduTransAppid; 24 | const secret = state.settings.Settings.baiduTransSecret; 25 | const salt = Date.now(); 26 | const sign = SparkMD5.hash(appid + q + salt + secret); 27 | const url = `${apiUrl}?q=${q}&from=${qfrom}&to=${qto}&appid=${appid}&salt=${salt}&sign=${sign}`; 28 | return requestToBody(url, 'GET'); 29 | }; 30 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-api-const-name.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-12-01 11:25:13 4 | * @Description: api名称生成 5 | */ 6 | import { pathsItem } from '@/shared/ts/api-interface'; 7 | import { camelCase } from 'lodash'; 8 | 9 | /** 10 | * @description: api名称生成 11 | * @param {pathsItem} apiData 12 | * @return {string} 13 | */ 14 | export default function generateApiConstName(apiData: pathsItem) { 15 | const { api, method } = apiData; 16 | const apiStrReg = /\{([\d\D]*)\}/; 17 | const preReg = new RegExp(`^${method}`); 18 | let resultStr = ''; 19 | const splitApi = api.split('/'); 20 | for (let i = splitApi.length - 1; i > splitApi.length - 4; i--) { 21 | const str = preReg.test(splitApi[i]) ? splitApi[i]?.replace(method, '') : splitApi[i]; 22 | if (!apiStrReg.test(str) && !['v1'].includes(str)) { 23 | resultStr = str + '-' + resultStr; 24 | if (resultStr.length > 24) { 25 | break; 26 | } 27 | } 28 | } 29 | return camelCase(method + '-' + resultStr); 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/ts/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-11-25 14:13:40 4 | * @Description: Settings 5 | */ 6 | 7 | export interface apiurlPrefixItem { 8 | key: string; 9 | url: string; // 接口文档地址 10 | prefix: string; // 前缀 11 | status: boolean; // 状态 12 | } 13 | 14 | export interface Settings { 15 | language: string; 16 | theme: string; 17 | apiurlPrefixList: apiurlPrefixItem[]; 18 | baiduTransAppid: string; 19 | baiduTransSecret: string; 20 | baiduApiToken: string; 21 | baiduApiTokenExpires: number; 22 | baiduOCRAppid: string; 23 | baiduOCRSecret: string; 24 | matchCodeStatus: boolean; 25 | matchCodeFnKey: string; 26 | } 27 | 28 | export type TransformSateHistoryArrayItem = { 29 | key: string; 30 | text: string; 31 | }; 32 | export interface TransformSate { 33 | status: boolean; // 代码转换关联转换文本 34 | textRecord: string[] | { search: string[]; column: string[]; length?: any }; // 最后文本记录 35 | historyArray: TransformSateHistoryArrayItem[]; // 历史文本转换记录 36 | baseCode: any; // 用做匹配的原始代码 37 | isTranslate: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-assemblies-utils-fn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-12 15:05:23 4 | * @Description: 5 | */ 6 | import axios from 'axios'; 7 | import generateApiNotes from './generate-api-notes'; 8 | import generateApiConstName from './generate-api-const-name'; 9 | import getApiNameAsPageName from '@/shared/getApiNameAsPageName'; 10 | import { prettyCode, filterTransformArrayByRows } from '@/shared/utils'; 11 | import generateTableColumnsProps from './generate-table-columns-props'; 12 | import { cleanParameterDescription, filetoBase64 } from '@/shared/utils'; 13 | import generateAvueTableColumns from './avue/generate-avue-table-columns'; 14 | import { requestToBody } from '@/shared/fetch/requestToBody'; 15 | 16 | export default { 17 | axios, 18 | generateApiNotes, 19 | generateApiConstName, 20 | getApiNameAsPageName, 21 | prettyCode, 22 | filterTransformArrayByRows, 23 | generateTableColumnsProps, 24 | cleanParameterDescription, 25 | filetoBase64, 26 | generateAvueTableColumns, 27 | requestToBody, 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/fetch/requestToBody.ts: -------------------------------------------------------------------------------- 1 | import { fetchInVSCode, isInVSCode } from '@/shared/vscode'; 2 | import axios, { AxiosRequestTransformer } from 'axios'; 3 | 4 | export async function requestToBody( 5 | endpoint = '', 6 | method: any = 'GET', 7 | header?: any, 8 | data?: any, 9 | params?: any, 10 | ): Promise { 11 | const url = endpoint.toString(); 12 | let headers: any = {}; 13 | if (header) { 14 | if (typeof header === 'string') { 15 | // knfie4j工具类增强 16 | headers['knfie4j-gateway-request'] = header; 17 | headers['knfie4j-gateway-code'] = 'ROOT'; 18 | } else { 19 | headers = header; 20 | } 21 | } 22 | 23 | if (isInVSCode) { 24 | return fetchInVSCode({ 25 | url, 26 | method, 27 | headers, 28 | data, 29 | params, 30 | }); 31 | } 32 | 33 | try { 34 | const res = await axios({ 35 | url, 36 | method, 37 | headers, 38 | data, 39 | }); 40 | if (res.status !== 200) { 41 | return null; 42 | } else { 43 | return res.data; 44 | } 45 | } catch (err) { 46 | console.error('axios-err', err); 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/shared/useBus.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | let subscribers: any[] = []; 4 | 5 | const subscribe = (filter: any, callback: () => void) => { 6 | if (filter === undefined || filter === null) return undefined; 7 | if (callback === undefined || callback === null) return undefined; 8 | 9 | subscribers = [...subscribers, [filter, callback]]; 10 | 11 | return () => { 12 | subscribers = subscribers.filter((subscriber) => subscriber[1] !== callback); 13 | }; 14 | }; 15 | 16 | export const dispatch = (event: any) => { 17 | let { type } = event; 18 | if (typeof event === 'string') type = event; 19 | 20 | const args: any[] = []; 21 | if (typeof event === 'string') args.push({ type }); 22 | else args.push(event); 23 | 24 | subscribers.forEach(([filter, callback]) => { 25 | if (typeof filter === 'string' && filter !== type) return; 26 | if (Array.isArray(filter) && !filter.includes(type)) return; 27 | if (filter instanceof RegExp && !filter.test(type)) return; 28 | if (typeof filter === 'function' && !filter(...args)) return; 29 | 30 | callback(...args); 31 | }); 32 | }; 33 | 34 | const useBus = (type: string, callback: () => void, deps: any[] = []) => { 35 | useEffect(() => subscribe(type, callback), deps); 36 | 37 | return dispatch; 38 | }; 39 | 40 | export default useBus; 41 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-typescript-type.ts: -------------------------------------------------------------------------------- 1 | import { cleanParameterDescription, prettyCode } from '@/shared/utils'; 2 | 3 | const FieldTypeMap: Record = { 4 | integer: 'number', 5 | }; 6 | 7 | function getFieldType(prop: any): string { 8 | const { type, format, $ref } = prop; 9 | if ($ref) { 10 | return $ref.replace(/#\/definitions\//, '') + 'Result'; 11 | } 12 | return FieldTypeMap[type] || prop.type; 13 | } 14 | 15 | export default function generateTypeScriptType(definition: any) { 16 | const { properties, required = [], title, description } = definition; 17 | const fields = Object.entries(properties).map(([propKey, prop]: [string, any]) => { 18 | /* let result = `{ name: '${propKey}', label: '${cleanParameterDescription( 19 | prop.description 20 | )}', type: ${getFieldType(prop)},`; 21 | if (required.includes(propKey)) { 22 | result += ` required:true,`; 23 | } 24 | result += " }"; */ 25 | let result = ` 26 | /** 27 | * ${cleanParameterDescription(prop.description ?? prop.summary)} 28 | */ 29 | ${propKey}: ${getFieldType(prop)};`; 30 | return result; 31 | }); 32 | 33 | return prettyCode( 34 | ` 35 | /** 36 | * ${description} 37 | */ 38 | interface ${title}Result { 39 | ${fields.join('')} 40 | } 41 | `, 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-model-class.ts: -------------------------------------------------------------------------------- 1 | import { cleanParameterDescription, prettyCode } from '@/shared/utils'; 2 | 3 | const FieldTypeMap: Record = { 4 | integer: 'number', 5 | }; 6 | 7 | function getFieldType(prop: any): string { 8 | const { type, format, $ref } = prop; 9 | if ($ref) { 10 | return 'FieldType.object'; 11 | } 12 | if (type === 'string' && format === 'date-time') { 13 | return 'FieldType.dateTime'; 14 | } 15 | return `FieldType.${FieldTypeMap[type] || prop.type}`; 16 | } 17 | 18 | export default function generateModelClass(definition: any) { 19 | const { properties, required = [], title, description } = definition; 20 | const fields = Object.entries(properties).map(([propKey, prop]: [string, any]) => { 21 | let result = `{ name: '${propKey}', label: '${cleanParameterDescription(prop.description)}', type: ${getFieldType( 22 | prop, 23 | )},`; 24 | if (required.includes(propKey)) { 25 | result += ` required:true,`; 26 | } 27 | result += ' }'; 28 | return result; 29 | }); 30 | 31 | return prettyCode( 32 | ` 33 | import { Model, FieldType } from '@rh/model'; 34 | 35 | /** 36 | * ${description || title} 37 | */ 38 | export class ${title} extends Model { 39 | constructor() { 40 | super({ 41 | fields:[ 42 | ${fields.join(',\n ')} 43 | ] 44 | }) 45 | } 46 | } 47 | `, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /scripts/site/spa-gh-pages.js: -------------------------------------------------------------------------------- 1 | // https://github.com/rafgraph/spa-github-pages 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | 6 | fs.copySync( 7 | path.join(__dirname, './CNAME'), 8 | path.join(__dirname, '../../dist/CNAME'), 9 | ); 10 | 11 | fs.copySync( 12 | path.join(__dirname, './404.html'), 13 | path.join(__dirname, '../../dist/404.html'), 14 | ); 15 | 16 | const scriptCode = ` 17 |
18 | 19 | 38 | 39 | `; 40 | 41 | const filePath = path.join(__dirname, '../../dist/index.html'); 42 | 43 | const htmlCode = fs.readFileSync(filePath).toString(); 44 | let newHtmlCode = htmlCode.replace(`
`, scriptCode); 45 | fs.writeFileSync(filePath, newHtmlCode); 46 | console.log(`spa-gh-pages修改文件:`, filePath); 47 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/CodeGenDropdown.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-11 17:26:45 4 | * @Description: 下拉caodegen-menu 5 | */ 6 | import { Button, Dropdown, Menu } from 'antd'; 7 | import type { MenuProps } from 'antd'; 8 | import { useMemo } from 'react'; 9 | import { DownOutlined } from '@ant-design/icons'; 10 | import { observer } from 'mobx-react-lite'; 11 | import { CustomMethodsItem } from '@/shared/ts/custom'; 12 | import state from '@/stores/index'; 13 | 14 | const CodeGenDropdown: React.FC<{ onChange: (key: string, item?: CustomMethodsItem | undefined) => void }> = ( 15 | props, 16 | ) => { 17 | const { onChange } = props; 18 | 19 | const CustomTypeMethods = useMemo(() => state.custom.CustomTypeMethods, [state.custom.CustomTypeMethods]); 20 | const CustomMethods = useMemo(() => state.custom.EnabledCustomMethods, [state.custom.EnabledCustomMethods]); 21 | 22 | const dropdownMenuClick: MenuProps['onClick'] = ({ key }) => { 23 | onChange( 24 | key, 25 | CustomMethods.find((v) => v.key === key), 26 | ); 27 | }; 28 | 29 | const dropdownMenu = useMemo(() => { 30 | return ; 31 | }, []); 32 | 33 | return ( 34 | 35 | 39 | 40 | ); 41 | }; 42 | 43 | export default CodeGenDropdown; 44 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-api-notes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-12-01 11:25:13 4 | * @Description: api请求函数注释生成 5 | */ 6 | import { pathsItem } from '@/shared/ts/api-interface'; 7 | 8 | /** 9 | * @description: api请求函数注释生成 10 | * @param {pathsItem} apiData 11 | * @return {string} notes 12 | */ 13 | export default function generateNotes(apiData: pathsItem & { requestParams: any }) { 14 | const { summary, requestParams } = apiData; 15 | function forEachParam(list: any[], params: string[], parentName?: string) { 16 | const FieldTypeMap: Record = { 17 | integer: 'number', 18 | parseFloat: 'number', 19 | float: 'number', 20 | }; 21 | 22 | list.forEach((row: any) => { 23 | if (row.in === 'body') { 24 | if (row.children?.length > 0) { 25 | forEachParam(row.children, params); 26 | } 27 | } else { 28 | params.push( 29 | `* @param {${FieldTypeMap[row.type] || row.type}} ${parentName ? `${parentName}.` : ''}${row.name || ''} ${ 30 | row.description || '' 31 | }`, 32 | ); 33 | if (row.children?.length > 0) { 34 | forEachParam(row.children, params, row.name); 35 | } 36 | } 37 | }); 38 | } 39 | const params: any[] = []; 40 | forEachParam(requestParams || [], params); 41 | 42 | return `/** 43 | * @description: ${summary} 44 | ${params.reduce((pre, cur) => { 45 | return `${pre ? pre + '\n' : pre} ${cur}`; 46 | }, '')} 47 | * @return {*} 48 | */`; 49 | } 50 | -------------------------------------------------------------------------------- /scripts/site/gh-pages-publish.js: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch, mv, mkdir } = require('shelljs'); 2 | const url = require('url'); 3 | require('./spa-gh-pages'); 4 | /* 5 | const { readFileSync } = require('fs'); 6 | const { execSync } = require('child_process'); 7 | 8 | let pkg = JSON.parse(readFileSync('package.json')); 9 | if (typeof pkg.repository === 'object') { 10 | if (!pkg.repository.hasOwnProperty('url')) { 11 | throw new Error('URL does not exist in repository section'); 12 | } 13 | repoUrl = pkg.repository.url; 14 | } else { 15 | repoUrl = pkg.repository; 16 | } */ 17 | 18 | let repoUrl = 'https://github.com/RootLinkFE/roothub-codegen-site'; 19 | 20 | let parsedUrl = url.parse(repoUrl); 21 | let repository = (parsedUrl.host || '') + (parsedUrl.path || ''); 22 | 23 | let ghToken = process.env.GH_TOKEN; 24 | let npmUser = process.env.NPM_USER; 25 | let npmEmail = process.env.NPM_EMAIL; 26 | 27 | // const npmUser = execSync('git config user.name').toString(); 28 | // const npmEmail = execSync('git config user.email').toString(); 29 | cd('dist'); 30 | touch('.nojekyll'); 31 | // 配合微前端basePath 32 | // mkdir('codegen'); 33 | // mv('-f', '*', 'codegen/'); 34 | // mv('-f', 'codegen/CNAME', '../dist/'); 35 | exec('git init'); 36 | exec('git add .'); 37 | exec(`git config user.name "${npmUser}"`); 38 | exec(`git config user.email "${npmEmail}"`); 39 | exec('git commit -m "docs(docs): update gh-pages"'); 40 | exec( 41 | `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages`, 42 | ); 43 | // 因为没有在runner执行,本地自己输入账号密码 44 | // exec(`git push --force --quiet "${repoUrl}" master:gh-pages`); 45 | echo('Docs deployed!!'); 46 | -------------------------------------------------------------------------------- /src/stores/swagger.store.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { defaultSwaggerUrl } from '@/shared/swaggerUrl'; 3 | import storage from '../shared/storage'; 4 | import { HistoryItem } from '@/shared/common'; 5 | import { filterHasIdByHistoryTexts } from '@/shared/utils'; 6 | 7 | // url历史记录获取 8 | const storageUrls: string[] = storage.get('storageUrls') ?? []; 9 | // 默认提取方法 10 | const storageExtractType: string = storage.get('storageExtractType') ?? 'ExtractBaiduOcrapi'; 11 | // 历史文本转换记录 12 | const storageHistoryTexts: HistoryItem[] = storage.get('storageHistoryTexts') ?? []; 13 | 14 | class SwaggerStore { 15 | urlValue: string = storageUrls?.length > 0 ? storageUrls[0] : defaultSwaggerUrl; 16 | apiUrls: string[] = storageUrls; // api历史列表 17 | extractType: string = storageExtractType; // 提取方法 18 | historyTexts: HistoryItem[] = filterHasIdByHistoryTexts(storageHistoryTexts); // 历史文本转换记录 19 | 20 | constructor() { 21 | makeAutoObservable(this); 22 | } 23 | 24 | setApiUrlsOrInitUrlValue(urls: string[]) { 25 | this.apiUrls = urls; 26 | this.urlValue = urls[0] || this.urlValue; 27 | } 28 | 29 | setApiUrls(urls: string[]) { 30 | this.apiUrls = urls; 31 | storage.set('storageUrls', urls); 32 | } 33 | 34 | setUrlValue(v: string) { 35 | this.urlValue = v; 36 | } 37 | 38 | setExtractType(type: string) { 39 | this.extractType = type; 40 | storage.set('storageExtractType', type); 41 | } 42 | 43 | setHistoryTexts(list: HistoryItem[]) { 44 | this.historyTexts = filterHasIdByHistoryTexts(list); 45 | storage.set('storageHistoryTexts', list); 46 | } 47 | } 48 | 49 | export default new SwaggerStore(); 50 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-table-columns-props.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description: 表格code生成 | 生成 ReactTable 列配置 5 | */ 6 | import { cleanParameterDescription, prettyJSON, filterTransformArrayByRows } from '@/shared/utils'; 7 | 8 | type filterItemFn = (row: any, index: number, rows: any[]) => any | null; 9 | 10 | /** 11 | * 获取表格字段 12 | * @param rows 字段记录 13 | * @param filter 是否过滤表格不需要字段 14 | * @returns 15 | */ 16 | export default function generateTableColumnsProps( 17 | selectedData: any, 18 | filter: boolean = true, 19 | filterItem?: filterItemFn, 20 | ) { 21 | let rows: any[] = []; 22 | if (Array.isArray(selectedData)) { 23 | rows = selectedData; 24 | } else { 25 | rows = selectedData.responseSelectedData; 26 | if (selectedData?.transformTextRecord) { 27 | rows = filterTransformArrayByRows(rows, selectedData.transformTextRecord); 28 | } 29 | } 30 | 31 | const columns: any[] = []; 32 | rows.forEach((row, index) => { 33 | if (filter) { 34 | if (/\[\]/.test(row.type)) { 35 | // 数组类型过滤掉 36 | return; 37 | } 38 | if (/\S*Desc$/.test(row.name)) { 39 | // 枚举翻译字段去掉 40 | return; 41 | } 42 | if (/id|ID/.test(row.name)) { 43 | // 主键id去掉 44 | return; 45 | } 46 | } 47 | 48 | const c = filterItem 49 | ? filterItem(row, index, rows) 50 | : { 51 | dataIndex: row.name, 52 | title: cleanParameterDescription(row.description), 53 | hideInSearch: index === 0 || row.name === 'name' ? false : true, // 临时只显示第一个字段作为查询 54 | }; 55 | 56 | columns.push(c); 57 | }); 58 | 59 | return prettyJSON(columns); 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "roothub-codegen", 4 | "scripts": { 5 | "start": "umi dev", 6 | "build": "umi build", 7 | "build:web": "cross-env APP_TYPE=site umi build", 8 | "postinstall": "umi generate tmp", 9 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 10 | "test": "umi-test", 11 | "deploy": "node ./scripts/site/gh-pages-publish.js", 12 | "test:coverage": "umi-test --coverage" 13 | }, 14 | "gitHooks": { 15 | "pre-commit": "lint-staged" 16 | }, 17 | "lint-staged": { 18 | "*.{js,jsx,less,md,json}": [ 19 | "prettier --write" 20 | ], 21 | "*.ts?(x)": [ 22 | "prettier --parser=typescript --write" 23 | ] 24 | }, 25 | "dependencies": { 26 | "@ant-design/pro-layout": "^6.5.0", 27 | "@codemirror/lang-javascript": "0.19.3", 28 | "@uiw/react-codemirror": "4.2.4", 29 | "ahooks": "^2.10.9", 30 | "axios": "^0.26.0", 31 | "codesandbox": "^2.2.3", 32 | "js-yaml": "^4.1.0", 33 | "lodash": "^4.17.21", 34 | "react": "17.x", 35 | "react-copy-to-clipboard": "^5.0.4", 36 | "react-dom": "17.x", 37 | "spark-md5": "^3.0.2", 38 | "umi": "^3.5.20" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^27.4.1", 42 | "@types/lodash": "^4.14.178", 43 | "@types/react": "^17.0.0", 44 | "@types/react-copy-to-clipboard": "^5.0.2", 45 | "@types/react-dom": "^17.0.0", 46 | "@umijs/preset-react": "1.x", 47 | "@umijs/test": "^3.5.20", 48 | "cross-env": "^7.0.3", 49 | "fs-extra": "^10.0.0", 50 | "lint-staged": "^10.0.7", 51 | "mobx": "^6.4.2", 52 | "mobx-react": "^7.3.0", 53 | "prettier": "^2.2.0", 54 | "shelljs": "0.8.4", 55 | "typescript": "^4.1.2", 56 | "yorkie": "^2.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/components/TextTransformDropdown.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-14 13:50:44 4 | * @Description: 文本过滤方法下拉 5 | */ 6 | import { Dropdown, Menu, Button } from 'antd'; 7 | import { useMemo } from 'react'; 8 | import state from '@/stores/index'; 9 | import { orderCodeGenerateMethods } from '../code-generate/index'; 10 | 11 | const TextTransformDropdown: React.FC<{ 12 | value: any; 13 | methodType?: string; 14 | dropdownTitle?: string; 15 | buttonType?: 'link' | 'text' | 'dashed' | 'default' | 'ghost' | 'primary' | undefined; 16 | onChange: (value: any) => void; 17 | }> = function (props) { 18 | const { value, dropdownTitle = '过滤', methodType = 'transform', buttonType = 'link', onChange } = props; 19 | const generateMethods = orderCodeGenerateMethods.filter((v) => v.type === methodType && v.status); 20 | const CustomMethods = useMemo(() => Array.from(state.custom.EnabledCustomMethods), [ 21 | state.custom.EnabledCustomMethods, 22 | ]); 23 | 24 | const items = useMemo(() => { 25 | // let currentCustomMethods = CustomMethods.filter((v: CustomMethodsItem) => v.type === methodType); 26 | return generateMethods.map((v) => { 27 | return { 28 | key: v.key, 29 | label: v.label, 30 | }; 31 | }); 32 | }, [CustomMethods]); 33 | 34 | const handleMenuItemClick = ({ key }: any) => { 35 | let item: any = generateMethods.find((v) => v.key === key); 36 | let textRecord: any = item.function(value); 37 | onChange(textRecord); 38 | }; 39 | 40 | return ( 41 | } trigger={['hover']}> 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default TextTransformDropdown; 48 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-api-defineition.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-14 14:31:41 4 | * @Description: api声明生成方法 5 | */ 6 | import { pathsItem } from '@/shared/ts/api-interface'; 7 | import generateApiNotes from './generate-api-notes'; 8 | import generateApiConstName from './generate-api-const-name'; 9 | 10 | export default function generateApiDefineition(apiData: pathsItem & { requestParams: any }, prefix: string = '') { 11 | const { api, method, parameters } = apiData; 12 | const name = generateApiConstName(apiData) ?? method; 13 | let apiParams = 'params'; 14 | let apiPath = prefix + api; 15 | const apiStrReg = /\{([\d\D]*)\}/g; 16 | let argumentsData = ['params']; 17 | let inBody = false; 18 | let inQuery = false; 19 | (parameters || []).forEach((v) => { 20 | if (v.in === 'body') { 21 | inBody = true; 22 | } else if (v.in === 'query') { 23 | inQuery = true; 24 | } 25 | }); 26 | // 处理接口传参格式 27 | if (inBody && inQuery) { 28 | argumentsData.push('data'); 29 | apiParams = `params, 30 | data: data`; 31 | } else if (inQuery) { 32 | apiParams = 'params'; 33 | } else if (inBody || method === 'post') { 34 | apiParams = 'data: params'; 35 | } 36 | 37 | const matchPathId = apiStrReg.exec(api); 38 | if (matchPathId) { 39 | argumentsData.unshift(matchPathId[1]); 40 | apiPath = apiPath.replace(apiStrReg, function (str) { 41 | return `$${str}`; 42 | }); 43 | } 44 | 45 | const notes = generateApiNotes(apiData); 46 | 47 | return `${notes} 48 | const ${name || 'fetch'} = (${argumentsData.join(', ')}) => { 49 | return axios( 50 | { 51 | path: ${'`'}${apiPath}${'`'}, 52 | method: '${method}', 53 | ${apiParams} 54 | } 55 | ) 56 | }; 57 | `; 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-api-axios-request.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2024-08-22 14:31:41 4 | * @Description: api-axios-request 声明生成方法 5 | */ 6 | import { pathsItem } from '@/shared/ts/api-interface'; 7 | import generateApiNotes from './generate-api-notes'; 8 | import generateApiConstName from './generate-api-const-name'; 9 | 10 | export default function generateApiAxiosRequestDefineition( 11 | apiData: pathsItem & { requestParams: any }, 12 | prefix: string = '', 13 | ) { 14 | const { api, method, parameters } = apiData; 15 | const name = generateApiConstName(apiData) ?? method; 16 | let apiParams = 'params'; 17 | let apiPath = prefix + api; 18 | const apiStrReg = /\{([\d\D]*)\}/g; 19 | let argumentsData = ['params']; 20 | let inBody = false; 21 | let inQuery = false; 22 | (parameters || []).forEach((v) => { 23 | if (v.in === 'body') { 24 | inBody = true; 25 | } else if (v.in === 'query') { 26 | inQuery = true; 27 | } 28 | }); 29 | // 处理接口传参格式 30 | if (inBody && inQuery) { 31 | argumentsData.push('data'); 32 | apiParams = `params, 33 | data: data`; 34 | } else if (inQuery) { 35 | apiParams = 'params'; 36 | } else if (inBody || method === 'post') { 37 | apiParams = 'data: params'; 38 | } 39 | 40 | const matchPathId = apiStrReg.exec(api); 41 | if (matchPathId) { 42 | argumentsData.unshift(matchPathId[1]); 43 | apiPath = apiPath.replace(apiStrReg, function (str) { 44 | return `$${str}`; 45 | }); 46 | } 47 | 48 | const notes = generateApiNotes(apiData); 49 | 50 | return `${notes} 51 | const ${name} = (${argumentsData.join(', ')}) => { 52 | return request( 53 | { 54 | url: ${'`'}${apiPath}${'`'}, 55 | method: '${method}', 56 | ${apiParams} 57 | } 58 | ) 59 | }; 60 | `; 61 | } 62 | -------------------------------------------------------------------------------- /src/shared/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-07 19:24:01 4 | * @Description: common 5 | */ 6 | export const MethodColors: any = { 7 | post: '#49cc90', 8 | get: '#61affe', 9 | delete: '#f93e3e', 10 | put: '#fca130', 11 | POST: '#49cc90', 12 | GET: '#61affe', 13 | DELETE: '#f93e3e', 14 | PUT: '#fca130', 15 | }; 16 | 17 | // 方法类型 18 | export const MethodTypes: string[] = ['api', 'text', 'model', 'extract', 'request', 'response']; 19 | 20 | export type OptionItem = { 21 | value: string; 22 | label: string; 23 | }; 24 | export const languageOptions: OptionItem[] = [ 25 | { 26 | label: 'Chinese(Simplified)', 27 | value: 'chs', 28 | }, 29 | { 30 | label: 'Chinese(Traditional)', 31 | value: 'cht', 32 | }, 33 | { 34 | label: 'English', 35 | value: 'eng', 36 | }, 37 | { 38 | label: 'Finnish', 39 | value: 'fin', 40 | }, 41 | { 42 | label: '中英文混合', 43 | value: 'CHN_ENG', 44 | }, 45 | { 46 | label: '英文', 47 | value: 'ENG', 48 | }, 49 | ]; 50 | 51 | export const ImageTypes: string[] = ['PDF', 'GIF', 'PNG', 'JPG', 'JPEG', 'TIF', 'BMP']; 52 | 53 | export const ApiDataTypes: OptionItem[] = [ 54 | { 55 | label: '请求参数', 56 | value: 'request', 57 | }, 58 | { 59 | label: '响应参数', 60 | value: 'response', 61 | }, 62 | ]; 63 | 64 | // CodeMirror-language-types 65 | export const CodeMirrorTypes: string[] = [ 66 | 'javascript', 67 | 'javascriptreact', 68 | 'typescript', 69 | 'typescriptreact', 70 | 'vue', 71 | 'vue-postcss', 72 | 'vue-sugarss', 73 | 'vue-html', 74 | 'json', 75 | 'jsonc', 76 | 'graphql', 77 | 'dart', 78 | 'sql', 79 | 'go', 80 | 'java', 81 | 'php', 82 | 'jade', 83 | 'python', 84 | 'swift', 85 | 'markdown', 86 | ]; 87 | 88 | export type HistoryItem = { 89 | id: string; 90 | content: any[] | string; 91 | }; 92 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-pro-table.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-02-09 18:13:19 4 | * @Description: generate-avue-pro-table 5 | */ 6 | import getApiNameAsPageName from '@/shared/getApiNameAsPageName'; 7 | import { prettyCode, filterTransformArrayByRows } from '@/shared/utils'; 8 | import generateAvueTableColumns from './generate-avue-table-columns'; 9 | 10 | export default function generateAvueProTablePageCode( 11 | selectedData: any, 12 | api: { api: string; description: string; summary: string }, 13 | ) { 14 | let { responseSelectedData: body, transformTextRecord } = selectedData; 15 | if (transformTextRecord) { 16 | body = filterTransformArrayByRows(body, transformTextRecord); 17 | } 18 | const columnCode = generateAvueTableColumns(body, {}, api); 19 | const componentName = getApiNameAsPageName(api.api); 20 | const matchApiName: any[] | null = api?.api.match(/^\/api\/[a-zA-Z]+/); 21 | const apiName = matchApiName && matchApiName?.length > 0 ? matchApiName[0].replace('/api/', '') : componentName; 22 | 23 | const fetchListName = `fetch${componentName}List`; 24 | return prettyCode(` 25 | /** * ${api?.description ?? api.summary} */ 26 | 33 | 34 | 52 | 53 | `); 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/codesandbox/index.ts: -------------------------------------------------------------------------------- 1 | import { getParameters } from 'codesandbox/lib/api/define'; 2 | import { message } from 'antd'; 3 | import { fetchInVSCode, isInVSCode } from '../vscode'; 4 | import indexHtml from './indexHtml'; 5 | import MainJs from './mainJs'; 6 | import genJson from './packageJson'; 7 | 8 | export default function openOnCodeSandbox({ componentCode = '', boxDependencies = {} }) { 9 | const parameters = getParameters({ 10 | files: { 11 | 'package.json': { 12 | content: genJson(boxDependencies), 13 | isBinary: false, 14 | }, 15 | 'index.html': { 16 | content: indexHtml, 17 | isBinary: false, 18 | }, 19 | 'index.tsx': { 20 | content: MainJs, 21 | isBinary: false, 22 | }, 23 | 'App.tsx': { 24 | content: componentCode, 25 | isBinary: false, 26 | }, 27 | 'sandbox.config.json': { 28 | content: `{ 29 | "template": "create-react-app-typescript" 30 | }`, 31 | isBinary: false, 32 | }, 33 | }, 34 | }); 35 | 36 | const actionUrl = 'https://codesandbox.io/api/v1/sandboxes/define?file=/App.tsx'; 37 | 38 | if (isInVSCode) { 39 | // fetchInVSCode({ url: actionUrl, data: { parameters } }, 'openInCodeSandBox').then((res: any) => { 40 | // console.log('res', res); 41 | // window.location.href = 'about:blank'; 42 | // document.write(res); 43 | // }); 44 | message.warning('VSCode环境下暂不支持CodeSandbox跳转预览,如需使用,请使用网页版 http://codegen.leekhub.com'); 45 | return; 46 | } 47 | const form = document.createElement('form'); 48 | const parametersInput = document.createElement('input'); 49 | form.method = 'POST'; 50 | form.action = actionUrl; 51 | form.target = '_blank'; 52 | parametersInput.name = 'parameters'; 53 | parametersInput.value = parameters; 54 | form.appendChild(parametersInput); 55 | document.body.append(form); 56 | form.submit(); 57 | document.body.removeChild(form); 58 | } 59 | -------------------------------------------------------------------------------- /scripts/site/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 路由跳转中… 6 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roothub-codegen 2 | 3 | 安装 [RootHub VSCode](https://marketplace.visualstudio.com/items?itemName=giscafer.roothub) 插件,使用 CodeGen 生成代码 4 | 5 | ## Screenshots 6 | 7 | 1、类swagger-ui2.0模式查看接口文档,支持快捷复制、当前菜单搜索 8 | ![](./screenshots/image1.jpg) 9 | 10 | 2、预览生成的前端代码 11 | 12 | ![](./screenshots/image2.jpg) 13 | 14 | 3、api声明代码生产(包含入参注释),可单独、批量生产 15 | 16 | ![](./screenshots/image3.jpg) 17 | 18 | 4、支持 codesandbox 打开预览效果 19 | 20 | ![](./screenshots/image4.png) 21 | 22 | 5、自定义代码生成方法-在线编辑 23 | 24 | ![](./screenshots/image5.jpg) 25 | 26 | 6、图片文字提取、文本代码生产(字段英文翻译)、代码与接口文档匹配 27 | 28 | ![](./screenshots/image6.jpg) 29 | 30 | 31 | ## 开发说明文档 32 | 33 | ### 页面开发说明 34 | 35 | 本项目基于 Umi3.x,本地开发和 Umi开发没区别 36 | 37 | ### RootHub VSCode插件开发说明 38 | 39 | 在 vscode-roothub 工程下,git submodule update 代码后,cd 进入 codegen 工程 40 | 41 | 然后 `yarn start` 启动项目(端口默认 3031) 42 | 43 | `.vscode/launch.json`,运行 `vscode extension` 时添加启动环境变量 ,打开 CodeGen 会自动获取 http://localhost:3031 的地址内容。 44 | 45 | ```json 46 | { 47 | "version": "0.2.0", 48 | "configurations": [ 49 | { 50 | "name": "Run Extension", 51 | "type": "extensionHost", 52 | "request": "launch", 53 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 54 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 55 | "preLaunchTask": "${defaultBuildTask}", 56 | // 启动环境变量 57 | "env": { 58 | "NODE_ENV": "development" 59 | } 60 | }, 61 | { 62 | "name": "Extension Tests", 63 | "type": "extensionHost", 64 | "request": "launch", 65 | "args": [ 66 | "--extensionDevelopmentPath=${workspaceFolder}", 67 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 68 | ], 69 | "outFiles": [ 70 | "${workspaceFolder}/out/**/*.js", 71 | "${workspaceFolder}/dist/**/*.js" 72 | ], 73 | "preLaunchTask": "tasks: watch-tests" 74 | } 75 | ] 76 | } 77 | ``` 78 | 79 | ### 常见问题汇总 80 | 81 | 1. 获取失败常见于:输入的文档地址携带不正常后缀;文档在更新;插件更新需重新加载;swagger需要权限;本地使用了代理工具。 82 | 2. 使用百度文字提取功能、翻译功能(百度翻译),需要在基础配置维护相关appid、screet、token。 83 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-api-request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-03-15 09:58:48 4 | * @Description: 5 | */ 6 | /** 7 | * avue-api-request 8 | */ 9 | function generateApiDefineition(apiData, prefix) { 10 | const { api, method, parameters, summary } = apiData; 11 | const utilsFn = window.utilsFn ?? {}; 12 | // window.lodash 13 | const name = utilsFn.generateApiConstName(apiData) ?? method; 14 | let apiParams = 'params'; 15 | let apiPath = prefix + api; 16 | const apiStrReg = /\{([\d\D]*)\}/g; 17 | let responseType = ''; 18 | let argumentsData = ['params']; 19 | let inBody = false; 20 | let inQuery = false; 21 | (parameters || []).forEach((v) => { 22 | if (v.in === 'body') { 23 | inBody = true; 24 | } else if (v.in === 'query') { 25 | inQuery = true; 26 | } 27 | }); 28 | // 处理接口传参格式 29 | if (inBody && inQuery) { 30 | argumentsData.push('data'); 31 | apiParams = `params, 32 | data: data`; 33 | } else if (inQuery) { 34 | apiParams = 'params'; 35 | } else if (inBody || method === 'post') { 36 | apiParams = 'data: params'; 37 | } 38 | 39 | let packTableData = ''; 40 | if (/(page|list)$/.test(api) || /列表|分页/.test(summary)) { 41 | packTableData = '.then(packTableData)'; 42 | } else if (/导出/.test(summary)) { 43 | responseType = `responseType: 'blob',`; 44 | packTableData = ''; 45 | } 46 | 47 | const matchPathId = apiStrReg.exec(api); 48 | if (matchPathId) { 49 | argumentsData.unshift(matchPathId[1]); 50 | apiPath = apiPath.replace(apiStrReg, function (str) { 51 | return `$${str}`; 52 | }); 53 | } 54 | const notes = utilsFn.generateApiNotes(apiData); 55 | 56 | return `${notes} 57 | export const ${name || 'fetch'} = (${argumentsData.join(', ')}) => { 58 | return request( 59 | { 60 | url: ${'`'}${apiPath}${'`'}, 61 | method: '${method}', 62 | ${apiParams}, 63 | ${responseType} 64 | } 65 | ).then(responseHandle)${packTableData}; 66 | }; 67 | `; 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow 6 | push: 7 | branches: 8 | - main 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | Deploy: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - name: Checkout 🛎️ 23 | uses: actions/checkout@v2 24 | 25 | # Setup .npmrc file to publish to npm 26 | - name: Setup node 🔧 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: '12.x' 30 | registry-url: 'https://registry.npmjs.org' 31 | 32 | # Get commit message 33 | - name: Get commit message 34 | run: | 35 | COMMIT_MESSAGE=$(git log --format=%s -n 1) 36 | echo "commitmsg=${COMMIT_MESSAGE}" >> $GITHUB_ENV 37 | - name: Show commit message 38 | run: echo "$commitmsg" 39 | 40 | # - name: Git config 🔧 41 | # run: | 42 | # git config --global user.name "roothub" 43 | # git config --global user.email "roothub@github.com" 44 | 45 | - name: Install Dependencies 46 | run: yarn 47 | - name: Test ⛏️ 48 | run: npm run test 49 | - name: Build ⛏️ 50 | run: yarn build:web 51 | - name: Deploy 🚀 52 | run: yarn deploy 53 | env: 54 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 55 | NPM_USER: ${{ secrets.NPM_USER }} 56 | NPM_EMAIL: ${{ secrets.NPM_EMAIL }} 57 | # - name: Deploy 🚀 58 | # uses: JamesIves/github-pages-deploy-action@4.1.4 59 | # with: 60 | # branch: gh-pages # The branch the action should deploy to. 61 | # folder: dist # The folder the action should deploy. 62 | -------------------------------------------------------------------------------- /src/stores/settings.store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-13 10:22:26 4 | * @Description: CustomStore 5 | */ 6 | import { makeAutoObservable } from 'mobx'; 7 | import { Settings, apiurlPrefixItem } from '@/shared/ts/settings'; 8 | import storage from '../shared/storage'; 9 | import { isInVSCode } from '@/shared/vscode'; 10 | import { postVSCodeMessage } from '@/shared/vscode'; 11 | 12 | // storage基础设置获取 13 | const rhSettings: Settings = storage.get('settings'); 14 | const baseData = { 15 | language: 'zh-CN', 16 | theme: 'default', 17 | apiurlPrefixList: [], 18 | baiduTransAppid: '', 19 | baiduTransSecret: '', 20 | baiduApiToken: '24.ba24da4592cdd39ac59057f1dc836656.2592000.1687513296.282335-31896638', 21 | baiduApiTokenExpires: 1684924027509, // 过期时间戳 22 | baiduOCRAppid: '', 23 | baiduOCRSecret: '', 24 | matchCodeStatus: true, 25 | matchCodeFnKey: '', 26 | }; 27 | 28 | class SettingsStore { 29 | Settings: Settings = rhSettings ? rhSettings : baseData; 30 | baseCode: string = ''; 31 | searchFixedText: string = ''; 32 | 33 | constructor() { 34 | makeAutoObservable(this); 35 | } 36 | 37 | setSettings(data: Settings) { 38 | this.Settings = data; 39 | if (!isInVSCode) { 40 | storage.set('settings', data); 41 | } else { 42 | postVSCodeMessage('saveCodeGenSettings', JSON.parse(JSON.stringify(data))); 43 | } 44 | } 45 | 46 | updateSettings(data: Settings) { 47 | const params = { ...this.Settings, ...data }; 48 | if (!isInVSCode) { 49 | storage.set('settings', params); 50 | } else { 51 | postVSCodeMessage('updateCodeGenSettings', JSON.parse(JSON.stringify(params))); 52 | } 53 | this.Settings = params; 54 | } 55 | 56 | setSettingsApiurlPrefixList(list: apiurlPrefixItem[]) { 57 | const data = { 58 | ...this.Settings, 59 | apiurlPrefixList: list, 60 | }; 61 | this.setSettings(data); 62 | } 63 | 64 | // 更改baseCode 65 | setBaseCode(text: string) { 66 | this.baseCode = text; 67 | } 68 | 69 | setSearchFixedText(text: string) { 70 | this.searchFixedText = text; 71 | } 72 | } 73 | 74 | export default new SettingsStore(); 75 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-model-form-items-code.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-27 17:16:27 4 | * @Description: 5 | */ 6 | import { prettyCode, filterTransformArrayByRows } from '../../../shared/utils'; 7 | 8 | const FieldTypeMap: Record = { 9 | integer: 'number', 10 | }; 11 | 12 | function getFieldInputCode(prop: any): string { 13 | const { type, format, $ref, name, description, required } = prop; 14 | const descReg = /^#([^(#|ENUM)]+)(#|ENUM#)/g; 15 | const result = descReg.exec(description); 16 | 17 | const desc = result && result[1]; 18 | 19 | if ($ref) { 20 | return ` 21 | 26 | `; 27 | } 28 | if (type === 'string' && format === 'date-time') { 29 | return ` 30 | 35 | `; 36 | } 37 | 38 | switch (type) { 39 | case 'integer': 40 | return ` 41 | 46 | `; 47 | default: 48 | return ` 49 | 54 | `; 55 | } 56 | } 57 | 58 | export default function generateModelFormItemsCode( 59 | selectedData: { 60 | requestSelectedData: any[]; 61 | responseSelectedData: any[]; 62 | transformTextRecord?: any[]; 63 | }, 64 | api: any = {}, 65 | ) { 66 | let { requestSelectedData: rows, transformTextRecord } = selectedData; 67 | if (transformTextRecord) { 68 | rows = filterTransformArrayByRows(rows, transformTextRecord); 69 | } 70 | 71 | return prettyCode( 72 | ` 73 | /** 74 | * ${api.description ?? api.summary} Form Items 75 | */ 76 | import React from "react"; 77 | const renderFormItems = () =>(<>${rows 78 | .map( 79 | (row) => ` 80 | ${getFieldInputCode(row)} 81 | `, 82 | ) 83 | .join('')})`, 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/shared/getParameterObject.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description: 5 | */ 6 | function getSwaggerRef(obj: any) { 7 | return obj.schema?.$ref || obj.$ref || obj.items?.$ref || obj.schema?.items?.$ref || obj?.originalRef; 8 | } 9 | 10 | export default function getParameterObject(resourceDetail: any, parameter: any, parent: string = ''): any { 11 | const $ref = getSwaggerRef(parameter); 12 | let definition: any; 13 | let children; 14 | if ($ref) { 15 | let refKey = $ref; // originalRef 可直接获取 16 | if (/#\/definitions\//.test($ref)) { 17 | refKey = $ref.replace('#/definitions/', ''); 18 | } 19 | definition = resourceDetail.definitions?.[refKey]; 20 | if (definition) { 21 | const { properties = {}, required } = definition; 22 | children = Object.entries(properties).map(([propertyKey, property]: [string, any]) => { 23 | let result; 24 | const hasRef = getSwaggerRef(property); 25 | if (hasRef) { 26 | if (hasRef !== $ref && parent.indexOf(propertyKey) === -1) { 27 | // propertyKey - indexOf避免文档不规范出现死循环 28 | result = { 29 | name: propertyKey, 30 | ...getParameterObject(resourceDetail, property, `${parent}${propertyKey}#`), 31 | }; 32 | } else { 33 | result = { 34 | name: propertyKey, 35 | type: refKey, 36 | description: definition.title, 37 | }; 38 | } 39 | 40 | if (hasRef && property.type === 'array') { 41 | result.type = result.type + '[]'; 42 | } 43 | } else { 44 | result = { 45 | name: propertyKey, 46 | ...property, 47 | }; 48 | } 49 | result.key = parent + propertyKey; 50 | result.required = required?.includes(propertyKey) ?? false; 51 | return result; 52 | }); 53 | } 54 | } 55 | const result = { ...parameter, children, key: parent + parameter.name }; 56 | if (definition) { 57 | result.description = definition.title; 58 | result.type = definition.title; 59 | result.schema = { 60 | ...definition, 61 | }; 62 | } 63 | 64 | return result; 65 | } 66 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-rhtable-page.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description:generate-rhtable-page 5 | */ 6 | import getApiNameAsPageName from '@/shared/getApiNameAsPageName'; 7 | import { prettyCode, filterTransformArrayByRows } from '@/shared/utils'; 8 | import generateTableColumnsProps from './generate-table-columns-props'; 9 | import state from '@/stores/index'; 10 | 11 | export default function generateRhTablePageCode( 12 | selectedData: any, 13 | apiData: { api: string; description: string; summary: string; children: any[] }, 14 | ) { 15 | let body: any[] = Array.isArray(selectedData) ? selectedData : apiData?.children || []; 16 | 17 | const { urlValue } = state.swagger; 18 | const { apiurlPrefixList } = state.settings.Settings; 19 | const prefix = apiurlPrefixList.find((v) => v.status && v.url === urlValue)?.prefix || ''; 20 | if (selectedData.transformTextRecord) { 21 | body = filterTransformArrayByRows(body, selectedData.transformTextRecord); 22 | } 23 | const columnCode = generateTableColumnsProps(body, true); 24 | 25 | const componentName = apiData.api ? getApiNameAsPageName(apiData.api) : 'get'; 26 | 27 | const columnCodeBlock = ` 28 | const columns: any[] = ${columnCode} 29 | `; 30 | 31 | const getListBlock = ` 32 | const getList = useCallback( 33 | async (params) => { 34 | return fetch('${urlValue + prefix}${apiData.api ?? ''}', { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify(params), 40 | }) 41 | .then(response => response.json()) 42 | }, 43 | [], 44 | ) 45 | `; 46 | 47 | return prettyCode(` 48 | /** 49 | * ${apiData.description ?? apiData.summary} 50 | */ 51 | import React, { useCallback } from 'react'; 52 | import { RhTable } from '@roothub/components'; 53 | 54 | function ${componentName}Table() { 55 | 56 | ${columnCodeBlock} 57 | 58 | ${getListBlock} 59 | 60 | return ( 61 | 62 | ); 63 | } 64 | 65 | export default ${componentName}Table;`); 66 | } 67 | -------------------------------------------------------------------------------- /src/shared/storage/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author giscafer 3 | * @homepage http://giscafer.com 4 | * @created 2022-03-12 14:11:54 5 | * @description LocalStorage和SessionStorage的统一使用和管理、解决微前端下,同域缓存覆盖问题 6 | */ 7 | 8 | import { IStorage } from './interface'; 9 | import { StorageOptions, StoreDataType, StoreType, ValueType } from './types'; 10 | 11 | const serialize = (data: ValueType) => { 12 | try { 13 | return JSON.stringify(data); 14 | } catch (e) { 15 | throw new Error('data must object'); 16 | } 17 | }; 18 | 19 | const deserialize = (data: string | null) => { 20 | return data ? JSON.parse(data) : null; 21 | }; 22 | 23 | export class RhStorage implements IStorage { 24 | prefix: string; 25 | private storage: StoreType; 26 | private expires: number | undefined; 27 | constructor(options?: StorageOptions) { 28 | // 默认localStorage 29 | this.storage = options?.store || window.localStorage; 30 | // 预留前缀,避免微前端同域情况覆盖 31 | this.prefix = options?.prefix || 'rh_'; 32 | this.expires = options?.expires; 33 | } 34 | 35 | has(key: string) { 36 | let hasKey = false; 37 | for (const keyName in this.storage) { 38 | if (keyName === `${this.prefix}${key}`) { 39 | hasKey = true; 40 | } 41 | } 42 | return hasKey; 43 | } 44 | 45 | get(key: string) { 46 | const storeData = deserialize(this.storage.getItem(`${this.prefix}${key}`)); 47 | if (storeData?.expires && storeData?.expires <= Date.now()) { 48 | this.remove(key); 49 | return null; 50 | } 51 | return storeData?.value; 52 | } 53 | 54 | set(key: string, value: ValueType) { 55 | if (!key) { 56 | throw new Error('必须指定key!'); 57 | } else if (typeof key === 'object') { 58 | throw new Error('key不能是一个对象!'); 59 | } 60 | const storeData: StoreDataType = { value }; 61 | 62 | if (this.expires) { 63 | storeData.expires = Date.now() + this.expires * 1000; 64 | } 65 | this.storage.setItem(`${this.prefix}${key}`, serialize(storeData)); 66 | } 67 | 68 | remove(key: string) { 69 | this.storage.removeItem(`${this.prefix}${key}`); 70 | } 71 | 72 | clear() { 73 | // 避免清除了别的 74 | for (const keyName in this.storage) { 75 | if (keyName.indexOf(this.prefix) === 0) { 76 | this.storage.removeItem(keyName); 77 | } 78 | } 79 | } 80 | } 81 | 82 | export default new RhStorage(); 83 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-form-columns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-26 16:18:42 4 | * @Description: generateAvueFormColumns 5 | */ 6 | import generateTableColumnsProps from '../generate-table-columns-props'; 7 | import { cleanParameterDescription } from '@/shared/utils'; 8 | 9 | export default function generateAvueFormColumns(body: any, record?: any, api?: any) { 10 | const TypeMap: Record = { 11 | integer: 'number', 12 | // string: 'input', 13 | }; 14 | 15 | function getFieldType(prop: any): string { 16 | const { type, format, $ref } = prop; 17 | if ($ref) { 18 | return 'object'; 19 | } 20 | if (type === 'string' && format === 'date-time') { 21 | return 'datetime'; 22 | } 23 | return TypeMap[type] || (prop.type === 'string' ? null : prop.type); 24 | } 25 | 26 | const rows = Array.isArray(body) ? body : record?.children || body?.requestSelectedData || []; 27 | return generateTableColumnsProps(rows, true, (row, index) => { 28 | let result: any = { 29 | prop: row.name, 30 | label: cleanParameterDescription(row.description), 31 | span: 24, 32 | placeholder: '请输入', 33 | }; 34 | const type = getFieldType(row); 35 | 36 | if ( 37 | row.description?.indexOf('状态') !== -1 || 38 | (row.description && row.description.indexOf('ENUM#') !== -1) || 39 | (row.enum && row.enum.length > 0) 40 | ) { 41 | result.type = 'select'; 42 | result.dicData = [ 43 | { label: '启用', value: 0 }, 44 | { label: '停用', value: 1 }, 45 | ]; 46 | result.placeholder = '请选择'; 47 | } else if (['date', 'time'].includes(row.name)) { 48 | result.type = 'datetime'; 49 | result.format = 'YYYY-MM-DD HH:mm:ss'; 50 | result.valueFormat = 'YYYY-MM-DD HH:mm:ss'; 51 | result.placeholder = '请选择'; 52 | } else if (type === 'number') { 53 | if (!/(Id|id)$/.test(row.name)) { 54 | result.min = 0; 55 | result.max = 999999999; 56 | } 57 | } else if ( 58 | row.description?.indexOf('备注') !== -1 || 59 | row.description?.indexOf('说明') !== -1 || 60 | row.description?.indexOf('原因') !== -1 61 | ) { 62 | result.maxLength = 200; 63 | result.row = 3; 64 | if (type) { 65 | result.type = type; 66 | } 67 | } else { 68 | result.maxLength = 50; 69 | if (type) { 70 | result.type = type; 71 | } 72 | } 73 | return result; 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-search-columns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-26 16:18:42 4 | * @Description: generateAvueSearchColumns 5 | */ 6 | import generateTableColumnsProps from '../generate-table-columns-props'; 7 | import { cleanParameterDescription } from '@/shared/utils'; 8 | 9 | export default function generateAvueSearchColumns(body: any, record?: any, option?: any) { 10 | const TypeMap: Record = { 11 | integer: 'number', 12 | // string: 'input', 13 | }; 14 | 15 | function getFieldType(prop: any): string { 16 | const { type, format, $ref } = prop; 17 | if ($ref) { 18 | return 'object'; 19 | } 20 | if (type === 'string' && format === 'date-time') { 21 | return 'datetime'; 22 | } 23 | return TypeMap[type] || (prop.type === 'string' ? null : prop.type); 24 | } 25 | 26 | const rows = Array.isArray(body) ? body : record?.children || body?.requestSelectedData || []; 27 | return generateTableColumnsProps(rows, true, (row, index) => { 28 | let result: any = { 29 | prop: row.name, 30 | label: cleanParameterDescription(row.description), 31 | span: 24, 32 | placeholder: '请输入', 33 | }; 34 | const type = getFieldType(row); 35 | 36 | if ( 37 | row.description?.indexOf('状态') !== -1 || 38 | (row.description && row.description.indexOf('ENUM#') !== -1) || 39 | (row.enum && row.enum.length > 0) 40 | ) { 41 | result.type = 'select'; 42 | result.dicData = [ 43 | { label: '启用', value: 0 }, 44 | { label: '停用', value: 1 }, 45 | ]; 46 | result.placeholder = '请选择'; 47 | } else if (['date', 'time'].includes(row.name)) { 48 | result.type = 'datetime'; 49 | result.format = 'YYYY-MM-DD HH:mm:ss'; 50 | result.valueFormat = 'YYYY-MM-DD HH:mm:ss'; 51 | result.placeholder = '请选择'; 52 | } else if (type === 'number') { 53 | if (!/(Id|id)$/.test(row.name)) { 54 | result.min = 0; 55 | result.max = 999999999; 56 | } 57 | } else if ( 58 | row.description?.indexOf('备注') !== -1 || 59 | row.description?.indexOf('说明') !== -1 || 60 | row.description?.indexOf('原因') !== -1 61 | ) { 62 | result.maxLength = 200; 63 | result.row = 3; 64 | if (type) { 65 | result.type = type; 66 | } 67 | } else { 68 | result.maxLength = 50; 69 | if (type) { 70 | result.type = type; 71 | } 72 | } 73 | return result; 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-react-antd-page-transcoding-all.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-15 09:17:07 4 | * @Description: generate-rhtable-page-transcoding-all 5 | */ 6 | import { 7 | replaceDescriptionByRows, 8 | getReplacePropKey, 9 | getMatchRowsName, 10 | matchCodeReplace, 11 | prettyJSON, 12 | } from '@/shared/utils'; 13 | 14 | type FieldData = { 15 | labelField: string; 16 | propField: string; 17 | }; 18 | 19 | /** 20 | * @description: avueColums的代码匹配后全代码替换 21 | * @param {any} list 22 | * @param {any} baseCode 23 | * @return {string} 24 | */ 25 | export function transcodingByRows(list: any[], baseCode: any, fieldData: FieldData) { 26 | const rows = replaceDescriptionByRows(list); 27 | const { labelField, propField } = fieldData; 28 | let resultCode = baseCode; 29 | if (typeof baseCode === 'string') { 30 | const splitReg = '},'; 31 | const splitCodes = baseCode.split(splitReg); 32 | const matchReg = new RegExp(`(?=.*${propField}:)(?=.*${labelField}:)`, 's'); 33 | const matchList = splitCodes.map((code) => { 34 | const isMatch = matchReg.test(code); 35 | return { 36 | isMatch, 37 | replaceKey: isMatch ? getReplacePropKey(code, propField) : null, 38 | replaceName: isMatch ? getMatchRowsName(code, rows, labelField) : null, 39 | }; 40 | }); 41 | matchList.map((item) => { 42 | if (item.isMatch && item.replaceKey !== null && item.replaceName !== null) { 43 | resultCode = matchCodeReplace(resultCode, item.replaceKey, item.replaceName); 44 | } 45 | }); 46 | } 47 | return resultCode; 48 | } 49 | 50 | export function transcodingAll(fieldData: FieldData, body: any, record?: any, api?: any, selectedData?: any) { 51 | let baseCode = selectedData?.baseCode || body?.baseCode || ''; // baseCode获取 52 | let rows = body?.requestSelectedData || record?.children || []; // 处理rows传入 53 | 54 | if (baseCode === '') { 55 | return 'no base code'; 56 | } else if (rows?.length === 0) { 57 | return 'no rows'; 58 | } 59 | 60 | let code = transcodingByRows(rows, baseCode, fieldData); 61 | return typeof code === 'string' ? code : prettyJSON(code); 62 | } 63 | 64 | const generatReactAntdPageTranscodeAll = (...argetment: [body: any, record?: any, api?: any, selectedData?: any]) => { 65 | return transcodingAll( 66 | { 67 | labelField: 'dataIndex', 68 | propField: 'title', 69 | }, 70 | ...argetment, 71 | ); 72 | }; 73 | 74 | export default generatReactAntdPageTranscodeAll; 75 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-element-table-form-transcoding.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-15 16:17:07 4 | * @Description: generate-element-table-form-transcoding 5 | */ 6 | import { replaceDescriptionByRows, matchCodeByName, prettyJSON } from '@/shared/utils'; 7 | 8 | type FieldData = { 9 | labelField: string; 10 | propField: string; 11 | symbolField?: string; 12 | }; 13 | 14 | /** 15 | * @description: 原始代码(el-table-column|el-form-item)分割后与响应参数匹配字段后返回 16 | * @param {any} list 17 | * @param {any} baseCode 18 | * @return {string} 19 | */ 20 | export function transcodingByRows(list: any[], baseCode: any, fieldData: FieldData) { 21 | const rows = replaceDescriptionByRows(list); 22 | const { labelField, propField, symbolField } = fieldData; 23 | if (typeof baseCode === 'string') { 24 | const splitReg = /( { 29 | return { 30 | isMatch: matchReg.test(code), 31 | code: code, 32 | }; 33 | }); 34 | 35 | const newCodeList: string[] = baseCodeList.map((codeItem) => { 36 | if (codeItem.isMatch) { 37 | return matchCodeByName(codeItem.code, rows, { labelField, propField, symbolField }); 38 | } else { 39 | return codeItem.code; 40 | } 41 | }); 42 | return newCodeList.join(''); 43 | } 44 | // 未匹配项原样输出 45 | return 'no transcoding'; 46 | } 47 | 48 | export function transcoding(fieldData: FieldData, body: any, record?: any, api?: any, selectedData?: any) { 49 | let baseCode = selectedData?.baseCode || body?.baseCode || ''; // baseCode获取 50 | let rows = body?.requestSelectedData || record?.children || []; // 处理rows传入 51 | 52 | if (baseCode === '') { 53 | return 'no base code'; 54 | } else if (rows?.length === 0) { 55 | return 'no rows'; 56 | } 57 | 58 | let code = transcodingByRows(rows, baseCode, fieldData); 59 | return typeof code === 'string' ? code : prettyJSON(code); 60 | } 61 | 62 | const elTableOrFormTranscoding = (...argetment: [body: any, record?: any, api?: any, selectedData?: any]) => { 63 | return transcoding( 64 | { 65 | labelField: 'label', 66 | propField: 'prop', 67 | symbolField: '=', 68 | }, 69 | ...argetment, 70 | ); 71 | }; 72 | 73 | export default elTableOrFormTranscoding; 74 | -------------------------------------------------------------------------------- /src/shared/storage/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import storage, { RhStorage } from '../index'; 2 | 3 | describe('numberFormat', () => { 4 | describe('set&get', () => { 5 | it('value type is Number', () => { 6 | storage.set('a', 123456); 7 | const result = storage.get('a'); 8 | expect(result).toEqual(123456); 9 | }); 10 | it('value type is String', () => { 11 | storage.set('domain', 'giscafer.com'); 12 | const result = storage.get('domain'); 13 | expect(result).toEqual('giscafer.com'); 14 | }); 15 | it('value type is Object', () => { 16 | const obj = { 17 | name: 'giscafer', 18 | }; 19 | storage.set('objKey', obj); 20 | const result = storage.get('objKey'); 21 | expect(result.name).toEqual('giscafer'); 22 | }); 23 | it('value type is Array', () => { 24 | storage.set('arrKey', '123456'.split('')); 25 | const result = storage.get('arrKey'); 26 | expect(result[1]).toEqual('2'); 27 | }); 28 | }); 29 | 30 | describe('has', () => { 31 | it('has key', () => { 32 | storage.set('giscafer', 'giscafer'); 33 | const hasKey = storage.has('giscafer'); 34 | expect(hasKey).toBeTruthy(); 35 | }); 36 | it('not has key', () => { 37 | const hasKey = storage.has('giscafer1'); 38 | expect(hasKey).toBeFalsy(); 39 | }); 40 | }); 41 | 42 | describe('remove', () => { 43 | it('remove key', () => { 44 | storage.set('giscafer', 'giscafer'); 45 | storage.remove('giscafer'); 46 | const hasKey = storage.has('giscafer'); 47 | expect(hasKey).toBeFalsy(); 48 | }); 49 | }); 50 | 51 | describe('clear', () => { 52 | it('clear', () => { 53 | storage.set('b', 'bbb'); 54 | storage.clear(); 55 | const hasKey = storage.has('b'); 56 | expect(hasKey).toBeFalsy(); 57 | }); 58 | }); 59 | 60 | describe('expires', () => { 61 | it('expires working', () => { 62 | const s = new RhStorage({ expires: 1 }); 63 | s.set('c', 'ccc'); 64 | const result = s.get('c'); 65 | expect(result).toEqual('ccc'); 66 | setTimeout(() => { 67 | const result1 = storage.get('c'); 68 | expect(result1).toBeNull(); 69 | }, 2000); 70 | }); 71 | }); 72 | 73 | describe('prefix', () => { 74 | it('prefix working', () => { 75 | const s = new RhStorage({ prefix: 'cg_' }); 76 | s.set('url', 'http://giscafer.com'); 77 | const hasUrl = s.has('url'); 78 | expect(hasUrl).toBeTruthy(); 79 | expect(s.prefix).toEqual('cg_'); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/stores/custom.store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-13 10:22:26 4 | * @Description: CustomStore 5 | */ 6 | import { makeAutoObservable } from 'mobx'; 7 | import { CustomMethodsItem, types } from '@/shared/ts/custom'; 8 | import storage from '../shared/storage'; 9 | import { isInVSCode } from '@/shared/vscode'; 10 | import { postVSCodeMessage } from '@/shared/vscode'; 11 | import { MethodTypes } from '@/shared/common'; 12 | 13 | // storage自定义方法获取 14 | const rhCustomMethods: any[] = storage.get('customMethods') ?? []; 15 | 16 | const baseTypeData: any = {}; 17 | MethodTypes.slice(-3).map((v: string) => { 18 | baseTypeData[v] = { 19 | key: v, 20 | label: v, 21 | type: 'group', 22 | description: '', 23 | children: [], 24 | }; 25 | }); 26 | 27 | const filterCustomMethods = (arr: CustomMethodsItem[]) => { 28 | let typeData: any = baseTypeData; 29 | arr.forEach((item: CustomMethodsItem, index) => { 30 | const current = typeData[item.type]; 31 | if (current) { 32 | current.children.push(item); 33 | } 34 | }); 35 | return Object.values(typeData); 36 | }; 37 | 38 | class CustomStore { 39 | CustomMethods: CustomMethodsItem[] = !isInVSCode && rhCustomMethods.length > 0 ? rhCustomMethods : []; 40 | EnabledCustomMethods: CustomMethodsItem[] = 41 | !isInVSCode && rhCustomMethods.length > 0 ? rhCustomMethods.filter((v) => v.status) : []; 42 | // 将自定义方法分类,且dropdownMenu可直接使用 43 | CustomTypeMethods: any[] = !isInVSCode && rhCustomMethods.length > 0 ? filterCustomMethods(rhCustomMethods) : []; 44 | 45 | constructor() { 46 | makeAutoObservable(this); 47 | } 48 | 49 | setCustomMethods(arr: CustomMethodsItem[]) { 50 | this.CustomMethods = arr; 51 | this.EnabledCustomMethods = arr.filter((v) => v.status); 52 | this.setFilterCustomMethods(arr); 53 | if (!isInVSCode) { 54 | storage.set('customMethods', arr); 55 | } else { 56 | postVSCodeMessage('saveCodeGenCustomMethods', JSON.parse(JSON.stringify(arr))); 57 | } 58 | } 59 | 60 | updateCustomMethods(arr: CustomMethodsItem[]) { 61 | this.CustomMethods = arr; 62 | this.EnabledCustomMethods = arr.filter((v) => v.status); 63 | this.setFilterCustomMethods(arr); 64 | if (!isInVSCode) { 65 | storage.set('customMethods', arr); 66 | } else { 67 | postVSCodeMessage('updateCodeGenCustomMethods', JSON.parse(JSON.stringify(arr))); 68 | } 69 | } 70 | 71 | setFilterCustomMethods(arr: CustomMethodsItem[]) { 72 | this.CustomTypeMethods = filterCustomMethods(arr); 73 | } 74 | } 75 | 76 | export default new CustomStore(); 77 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ModelCodeDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, DrawerProps, Space, Button, message } from 'antd'; 2 | import { CodeSandboxOutlined, CopyOutlined } from '@ant-design/icons'; 3 | import React, { Suspense, useMemo } from 'react'; 4 | import { useModel } from 'umi'; 5 | import { omit } from 'lodash'; 6 | import openOnCodeSandbox from '@/shared/codesandbox'; 7 | import CodeMirror from '@uiw/react-codemirror'; 8 | import { javascript } from '@codemirror/lang-javascript'; 9 | import copy from 'copy-to-clipboard'; 10 | 11 | const ModelCodeDrawer: React.FC = (props) => { 12 | const { definitionCodeDrawerProps } = useModel('useApiSwitchModel'); 13 | 14 | const code = definitionCodeDrawerProps.generateCode?.(); 15 | 16 | const editorHeight = useMemo(() => { 17 | return window.innerHeight - 70; 18 | }, [window.innerHeight]); 19 | 20 | const handleCopy = () => { 21 | copy(code); 22 | message.success('代码已复制到剪贴板!'); 23 | }; 24 | 25 | return ( 26 | 34 | {definitionCodeDrawerProps.showCodeSandbox && ( 35 | 46 | )} 47 | 50 | 51 | } 52 | > 53 |
61 | {definitionCodeDrawerProps.visible && ( 62 | Loading...
}> 63 | { 69 | console.log('value:', value); 70 | }} 71 | /> 72 | 73 | )} 74 | 75 |
76 | ); 77 | }; 78 | 79 | export default ModelCodeDrawer; 80 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-form.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-26 16:00:23 4 | * @Description: generateAvueFormCode 5 | */ 6 | import getApiNameAsPageName from '@/shared/getApiNameAsPageName'; 7 | import { prettyCode, filterTransformArrayByRows } from '@/shared/utils'; 8 | import generateAvueFormColumns from './generate-avue-form-columns'; 9 | 10 | export default function generateAvueFormCode( 11 | selectedData: any, 12 | api: { api: string; description: string; summary: string }, 13 | ) { 14 | let { requestSelectedData: body, transformTextRecord } = selectedData; 15 | if (transformTextRecord) { 16 | body = filterTransformArrayByRows(body, transformTextRecord); 17 | } 18 | const columnCode = generateAvueFormColumns(body); 19 | const componentName = getApiNameAsPageName(api.api); 20 | const matchApiName: any[] | null = api?.api.match(/^\/api\/[a-zA-Z]+/); 21 | const apiName = matchApiName && matchApiName?.length > 0 ? matchApiName[0].replace('/api/', '') : ''; 22 | 23 | return prettyCode(` 24 | /** * ${api?.description ?? api.summary} */ 25 | 35 | 36 | 76 | 77 | `); 78 | } 79 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-table.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-12 11:31:18 4 | * @Description: 5 | */ 6 | import getApiNameAsPageName from '@/shared/getApiNameAsPageName'; 7 | import { prettyCode, filterTransformArrayByRows } from '@/shared/utils'; 8 | import generateAvueTableColumns from './generate-avue-table-columns'; 9 | 10 | export default function generateAvueTablePageCode( 11 | selectedData: any, 12 | api: { api: string; description: string; summary: string }, 13 | ) { 14 | let { responseSelectedData: body, transformTextRecord } = selectedData; 15 | if (transformTextRecord) { 16 | body = filterTransformArrayByRows(body, transformTextRecord); 17 | } 18 | const columnCode = generateAvueTableColumns(body); 19 | const componentName = getApiNameAsPageName(api.api); 20 | const matchApiName: any[] | null = api?.api.match(/^\/api\/[a-zA-Z]+/); 21 | const apiName = matchApiName && matchApiName?.length > 0 ? matchApiName[0].replace('/api/', '') : ''; 22 | 23 | return prettyCode(` 24 | /** * ${api?.description ?? api.summary} */ 25 | 42 | 43 | 88 | 89 | `); 90 | } 91 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-table-columns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-14 17:19:48 4 | * @Description: generateAvueTableColumns 5 | */ 6 | import generateTableColumnsProps from '../generate-table-columns-props'; 7 | import { 8 | cleanParameterDescription, 9 | filterTransformArrayByRows, 10 | filterBaseCodeByRows, 11 | prettyJSON, 12 | } from '@/shared/utils'; 13 | 14 | export default function generateAvueTableColumns(body: any, record?: any, api?: any, selectedData?: any) { 15 | const TypeMap: Record = { 16 | integer: 'number', 17 | // string: 'input', 18 | }; 19 | 20 | function getFieldType(prop: any): string { 21 | const { type, format, $ref } = prop; 22 | if ($ref) { 23 | return 'object'; 24 | } 25 | if (type === 'string' && format === 'date-time') { 26 | return 'datetime'; 27 | } 28 | return TypeMap[type] || (prop.type === 'string' ? null : prop.type); 29 | } 30 | 31 | const parametersSet = new Set(); 32 | (api?.parameters ?? []).forEach((param: { name: string }) => { 33 | parametersSet.add(param.name); 34 | }); 35 | let rows = Array.isArray(body) ? body : record?.children || []; 36 | if (selectedData?.baseCode) { 37 | let code = filterBaseCodeByRows(rows, selectedData.baseCode); 38 | return typeof code === 'string' ? code : prettyJSON(code); 39 | } else if (selectedData?.transformTextRecord) { 40 | rows = filterTransformArrayByRows(rows, selectedData.transformTextRecord); 41 | } 42 | 43 | return generateTableColumnsProps(rows, true, (row, index) => { 44 | const type = getFieldType(row); 45 | let result: any = { 46 | prop: row.name, 47 | label: cleanParameterDescription(row.description), 48 | minWidth: 150, 49 | overHidden: true, 50 | type, 51 | }; 52 | if (type) { 53 | result.type = type; 54 | } 55 | const item = parametersSet.has(row.name); 56 | if (item) { 57 | result.search = true; 58 | result.searchPlaceholder = '请输入'; 59 | 60 | if ( 61 | row.description?.indexOf('状态') !== -1 || 62 | (row.description && row.description.indexOf('ENUM#') !== -1) || 63 | (row.enum && row.enum.length > 0) 64 | ) { 65 | result.searchType = 'select'; 66 | result.type = 'select'; 67 | result.searchPlaceholder = '请选择'; 68 | result.dicData = [ 69 | { label: '启用', value: 0 }, 70 | { label: '停用', value: 1 }, 71 | ]; 72 | } else if (['date', 'time'].includes(row.name)) { 73 | result.searchType = 'datetime'; 74 | result.type = 'datetime'; 75 | result.format = 'YYYY-MM-DD HH:mm:ss'; 76 | result.valueFormat = 'YYYY-MM-DD HH:mm:ss'; 77 | result.searchPlaceholder = '请选择'; 78 | result.minWidth = 170; 79 | } else if (type === 'number') { 80 | result.min = 0; 81 | result.max = 999999999; 82 | } 83 | } 84 | return result; 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-extract-baidu-ocrapi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-02-07 13:41:23 4 | * @Description: 5 | */ 6 | /** 7 | * @description: generate-extract-baidu-ocrapi 8 | 9 | * window.utilsFn: { 10 | generateApiNotes, 11 | getApiNameAsPageName, 12 | prettyCode, 13 | filterTransformArrayByRows, 14 | generateTableColumnsProps, 15 | cleanParameterDescription, 16 | filetoBase64, 17 | generateAvueTableColumns, 18 | }, 19 | * @return {string} 20 | */ 21 | import state from '@/stores/index'; 22 | 23 | /** 24 | * @description: 获取baiduOCR-token 25 | * @return {string} 26 | */ 27 | const getBaiDuApiToken = async () => { 28 | const Settings = state.settings.Settings; 29 | const { baiduApiToken, baiduApiTokenExpires, baiduOCRAppid, baiduOCRSecret } = Settings; 30 | const nowTime = Date.now(); 31 | if (baiduApiToken && baiduApiTokenExpires > nowTime) { 32 | // 判断token是否过期,有效期30天 33 | return baiduApiToken; 34 | } else { 35 | if (!baiduOCRAppid || !baiduOCRSecret) { 36 | console.error('baiduOCRAppid or baiduOCRSecret is empty'); 37 | return baiduApiToken; 38 | } 39 | const utilsFn: any = (window as any).utilsFn ?? {}; 40 | const res = await utilsFn.requestToBody( 41 | `https://aip.baidubce.com/oauth/2.0/token?client_id=${baiduOCRAppid}&client_secret=${baiduOCRSecret}&grant_type=client_credentials`, // 获取AccessToken 42 | 'POST', 43 | { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, 44 | ); 45 | if (res.access_token) { 46 | state.settings.updateSettings({ 47 | ...Settings, 48 | baiduApiToken: res.access_token, 49 | baiduApiTokenExpires: nowTime + res.expires_in, 50 | }); 51 | return res.access_token; 52 | } else { 53 | console.log('baiduOCR-token-error', res); 54 | return baiduApiToken; 55 | } 56 | } 57 | }; 58 | 59 | export default function generateExtractBaiduOcrapi(file: any, base64Image: any) { 60 | async function generateExtract(file: any, base64Image: any) { 61 | const utilsFn: any = (window as any).utilsFn ?? {}; 62 | const getFiletoBase64 = (f: any) => { 63 | return utilsFn.filetoBase64(f).then((imageBase64: any) => { 64 | return imageBase64; 65 | }); 66 | }; 67 | const imageBase64 = base64Image || (await getFiletoBase64(file)); 68 | const token = await getBaiDuApiToken(); 69 | const res = await utilsFn.requestToBody( 70 | `https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token=${token}`, // 通用文字识别(高精度版) - https://ai.baidu.com/ai-doc/OCR/1k3h7y3db 71 | 'POST', 72 | { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, 73 | {}, 74 | { 75 | image: imageBase64, 76 | language_type: 'CHN_ENG', 77 | detect_language: false, // 是否检测语言 78 | paragraph: false, // 是否输出段落信息 79 | probability: false, // 是否返回识别结果中每一行的置信度 80 | detect_direction: false, // 是否检测图像朝向 81 | }, 82 | ); 83 | console.log('baiduOCR-res', res); 84 | if (res.status === 200 && res?.data.words_result?.length > 0) { 85 | return res.data; 86 | } else { 87 | return res; 88 | } 89 | } 90 | return generateExtract(file, base64Image); 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-rhtable-page-transcoding.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-04-23 09:17:07 4 | * @Description: rhtablePageTranscoding 5 | */ 6 | import { replaceDescriptionByRows, filterStrRepeat, matchCodeByName, prettyJSON } from '@/shared/utils'; 7 | import { isNil } from 'lodash'; 8 | 9 | type FieldData = { 10 | labelField: string; 11 | propField: string; 12 | }; 13 | 14 | /** 15 | * @description: rhtablePage原始代码与响应参数匹配字段后返回 16 | * @param {any} list 17 | * @param {any} baseCode 18 | * @return {string} 19 | */ 20 | export function transcodingByRows(list: any[], baseCode: any, fieldData: FieldData) { 21 | const rows = replaceDescriptionByRows(list); 22 | const { labelField, propField } = fieldData; 23 | if (Array.isArray(baseCode)) { 24 | const nextArr: { i: number; codeItem: any }[] = []; // baseCode未比对部分 25 | 26 | baseCode.forEach((codeItem: any, i: number) => { 27 | const item = rows.find((v) => { 28 | return ( 29 | !isNil(v.description) && 30 | (v.description === codeItem[labelField] || v.description.indexOf(codeItem[labelField]) !== -1) 31 | ); 32 | }); 33 | if (item) { 34 | codeItem[labelField] = item.name; 35 | } else { 36 | nextArr.push({ i, codeItem }); 37 | } 38 | }); 39 | nextArr.forEach((m: { i: number; codeItem: any }) => { 40 | let item = filterStrRepeat(rows, m.codeItem[labelField]); 41 | if (item) { 42 | baseCode[m.i] = { 43 | ...m.codeItem, 44 | [labelField]: item.name, 45 | }; 46 | } 47 | }); 48 | } else if (typeof baseCode === 'string') { 49 | const splitReg = '},'; 50 | const splitCodes = baseCode.split(splitReg); 51 | const matchReg = new RegExp(`(?=.*${propField}:)(?=.*${labelField}:)`, 's'); 52 | const baseCodeList = splitCodes.map((code, index) => { 53 | return { 54 | isMatch: matchReg.test(code), 55 | code: code + (index !== splitCodes.length - 1 ? splitReg : ''), 56 | }; 57 | }); 58 | 59 | const newCodeList: string[] = baseCodeList.map((codeItem) => { 60 | if (codeItem.isMatch) { 61 | return matchCodeByName(codeItem.code, rows, { labelField, propField }); 62 | } else { 63 | return codeItem.code; 64 | } 65 | }); 66 | return newCodeList.join(''); 67 | } 68 | // 未匹配项原样输出 69 | return baseCode; 70 | } 71 | 72 | export function transcoding(fieldData: FieldData, body: any, record?: any, api?: any, selectedData?: any) { 73 | let baseCode = selectedData?.baseCode || body?.baseCode || ''; // baseCode获取 74 | let rows = body?.responseParamsData || body?.requestSelectedData || record?.children || []; // 处理rows传入 75 | 76 | if (baseCode === '') { 77 | return 'no base code'; 78 | } else if (rows?.length === 0) { 79 | return 'no rows'; 80 | } 81 | 82 | let code = transcodingByRows(rows, baseCode, fieldData); 83 | return typeof code === 'string' ? code : prettyJSON(code); 84 | } 85 | 86 | const rhtablePageTranscoding = (...argetment: [body: any, record?: any, api?: any, selectedData?: any]) => { 87 | return transcoding( 88 | { 89 | labelField: 'title', 90 | propField: 'dataIndex', 91 | }, 92 | ...argetment, 93 | ); 94 | }; 95 | 96 | export default rhtablePageTranscoding; 97 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/BaseConfigDrawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-04 15:45:10 4 | * @Description: 基础设置抽屉弹窗 5 | */ 6 | import { observer } from 'mobx-react-lite'; 7 | import { Drawer, DrawerProps, Form, Input, Button, Switch, Select, Typography } from 'antd'; 8 | import state from '@/stores/index'; 9 | import { Settings } from '@/shared/ts/settings'; 10 | import { trancodingOptions, CodeGenerateOption } from './code-generate/index'; 11 | import { storeTips } from '@/shared/vscode'; 12 | 13 | const { Option } = Select; 14 | 15 | const ApiurlPrefixDrawer: React.FC = (props) => { 16 | const { Settings } = state.settings; 17 | 18 | const { ...drawerProps } = props; 19 | const [form] = Form.useForm(); 20 | // const themOptions: any = []; 21 | 22 | const handleSubmit = (values: Settings) => { 23 | state.settings.setSettings({ ...Settings, ...values }); 24 | }; 25 | 26 | const itemCol = { 27 | labelCol: { span: 6 }, 28 | wrapperCol: { span: 18 }, 29 | }; 30 | 31 | return ( 32 | 40 |
{}} 48 | autoComplete="off" 49 | > 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 78 | 79 | {/* 80 | 81 | 82 | 83 | 84 | */} 85 | 86 | 89 | {storeTips} 90 | 91 |
92 |
93 | ); 94 | }; 95 | 96 | export default observer(ApiurlPrefixDrawer); 97 | -------------------------------------------------------------------------------- /scripts/git/validate-commit-message.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // from https://github.com/angular/angular/blob/master/tools/validate-commit-message/validate-commit-message.js 4 | 5 | 'use strict'; 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const configPath = path.resolve(__dirname, './commit-message.json'); 10 | const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); 11 | const PATTERN = /^(\w+)(?:\(([^)]+)\))?\:(.+)$/; 12 | const FIXUP_SQUASH = /^(fixup|squash)\! /i; 13 | const REVERT = /^revert:? /i; 14 | const MERGE = /^Merge? /i; 15 | 16 | module.exports = function (commitSubject) { 17 | const subject = commitSubject.replace(FIXUP_SQUASH, ''); 18 | 19 | if (subject.match(REVERT)) { 20 | return true; 21 | } 22 | 23 | if (subject.match(MERGE)) { 24 | return true; 25 | } 26 | 27 | if (subject.length > config['maxLength']) { 28 | error( 29 | `The commit message is longer than ${config['maxLength']} characters`, 30 | commitSubject, 31 | ); 32 | error( 33 | `commit 信息不能超过 ${config['maxLength']} 字符`, 34 | commitSubject, 35 | 'zh', 36 | ); 37 | return false; 38 | } 39 | 40 | const match = PATTERN.exec(subject); 41 | if (!match) { 42 | error( 43 | `The commit message does not match the format of '(): ' OR ': '`, 44 | commitSubject, 45 | ); 46 | error( 47 | `这条 commit 信息格式不正确 '(): ' 或 ': `, 48 | commitSubject, 49 | 'zh', 50 | ); 51 | return false; 52 | } 53 | 54 | const type = match[1]; 55 | 56 | if (type.toLowerCase() === 'wip') { 57 | error( 58 | `wip are not allowed in a commit, you can change this PR title`, 59 | commitSubject, 60 | ); 61 | error( 62 | `wip 不允许出现在 commit 中,你可以在 PR 中修改它的标题`, 63 | commitSubject, 64 | 'zh', 65 | ); 66 | return false; 67 | } 68 | 69 | if (config['types'].indexOf(type) === -1) { 70 | error( 71 | `${type} is not an allowed type.\n => TYPES: ${config['types'].join( 72 | ', ', 73 | )}`, 74 | commitSubject, 75 | ); 76 | error( 77 | `${type} 是不允许的 type.\n => TYPES: ${config['types'].join(', ')}`, 78 | commitSubject, 79 | 'zh', 80 | ); 81 | return false; 82 | } 83 | 84 | const scope = match[2]; 85 | 86 | if ( 87 | scope && 88 | !config['scopes'].includes(scope) && 89 | type !== 'release' && 90 | !/.+/.test(scope) 91 | ) { 92 | error( 93 | `"${scope}" is not an allowed scope.\n => SCOPES: ${config['scopes'].join( 94 | ', ', 95 | )}`, 96 | commitSubject, 97 | ); 98 | error( 99 | `"${scope}" 是不允许的 scope.\n => SCOPES: ${config['scopes'].join( 100 | ', ', 101 | )}`, 102 | commitSubject, 103 | 'zh', 104 | ); 105 | return false; 106 | } 107 | 108 | return true; 109 | }; 110 | 111 | function error(errorMessage, commitMessage, lang) { 112 | if (lang === 'zh') { 113 | console.error( 114 | `\x1b[33m无效的 COMMIT 信息: "${commitMessage}"\x1b[0m\n\x1b[31m => 错误: ${errorMessage}\x1b[0m\n`, 115 | ); 116 | } else { 117 | console.error( 118 | `\x1b[33mINVALID COMMIT MSG: "${commitMessage}"\x1b[0m\n\x1b[31m => ERROR: ${errorMessage}\x1b[0m\n`, 119 | ); 120 | } 121 | } 122 | 123 | module.exports.config = config; 124 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ParameterTableDefinition.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description: 模型-代码生成下拉 5 | */ 6 | import { Dropdown, Menu } from 'antd'; 7 | import CodeOutlined from '@ant-design/icons/lib/icons/CodeOutlined'; 8 | import { useModel } from 'umi'; 9 | import { useCallback, useMemo } from 'react'; 10 | import state from '@/stores/index'; 11 | import { getStringToFn } from '@/shared/utils'; 12 | import { CustomMethodsItem } from '@/shared/ts/custom'; 13 | import { pathsItem } from '@/shared/ts/api-interface'; 14 | import { codeGenerateMethods } from './code-generate/index'; 15 | 16 | const ParameterTableDefinition: React.FC<{ definition: any; record: any; api: pathsItem }> = function (props) { 17 | const { definition, record, api } = props; 18 | 19 | const { setSelectedDefinition, setDefinitionCodeDrawerProps, transformSate } = useModel('useApiSwitchModel'); 20 | const modelGenerateMethods = codeGenerateMethods.filter((v) => v.type === 'model' && v.status); 21 | 22 | const title = useMemo(() => { 23 | return definition.title || definition?.xml?.name || (record?.$ref && record?.$ref.replace('#/definitions/', '')); 24 | }, [definition, record]); 25 | 26 | const CustomMethods = useMemo(() => state.custom.EnabledCustomMethods, [state.custom.EnabledCustomMethods]); 27 | 28 | const items = useMemo(() => { 29 | return [ 30 | { 31 | key: 'root', 32 | label: 'root', 33 | type: 'group', 34 | children: modelGenerateMethods.map((v) => { 35 | return { 36 | key: v.key, 37 | label: v.label, 38 | }; 39 | }), 40 | }, 41 | { 42 | key: 'custom', 43 | label: 'custom', 44 | type: 'group', 45 | children: state.custom.EnabledCustomMethods.filter((v: CustomMethodsItem) => v.type === 'model'), 46 | }, 47 | ]; 48 | }, [state.custom.EnabledCustomMethods]); 49 | 50 | const handleMenuItemClick = ({ key }: any) => { 51 | const params = { 52 | ...definition, 53 | title, 54 | }; 55 | const generateMethod: any = modelGenerateMethods.find((v) => v.key === key); 56 | let drawerProps = { 57 | title: `${api.summary}(${title})`, 58 | visible: true, 59 | language: 'javascript', 60 | generateCode: () => {}, 61 | }; 62 | const textRecord = transformSate.status && transformSate.textRecord?.length > 0 ? transformSate.textRecord : null; 63 | const baseCode = state.settings.baseCode; 64 | if (generateMethod) { 65 | drawerProps.generateCode = () => 66 | generateMethod.function(definition, record, api, { transformTextRecord: textRecord, baseCode }); 67 | } else { 68 | let item: any = CustomMethods.find((v) => v.key === key) ?? {}; 69 | const cutomCodeFn = item?.function ? getStringToFn(item.function) : () => {}; 70 | drawerProps.generateCode = () => 71 | cutomCodeFn({ definition, record }, api, { transformTextRecord: textRecord, baseCode }); 72 | } 73 | setSelectedDefinition(params); 74 | setDefinitionCodeDrawerProps(drawerProps); 75 | }; 76 | 77 | return ( 78 |
} trigger={['hover']}> 79 | e.preventDefault()}> 80 | {title} 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default ParameterTableDefinition; 87 | -------------------------------------------------------------------------------- /src/pages/index.less: -------------------------------------------------------------------------------- 1 | // @import '~antd/dist/antd.dark.less'; // 引入官方提供的暗色 less 样式入口文件 2 | // @import 'antd/dist/antd.variable.min.css'; 3 | .title { 4 | background: rgb(121, 242, 157); 5 | } 6 | body { 7 | padding: 0; 8 | } 9 | 10 | #root, 11 | .codegen-main { 12 | height: 100%; 13 | } 14 | 15 | .codegen-main { 16 | flex-direction: column; 17 | overflow: hidden; 18 | .ant-tabs { 19 | .ant-tabs-nav { 20 | margin-bottom: 0; 21 | } 22 | .ant-tabs-nav-add { 23 | display: none; 24 | } 25 | } 26 | } 27 | 28 | // ResourcesTree Menu Items 29 | .menu-item-title-content { 30 | width: 90%; 31 | white-space: nowrap; 32 | text-overflow: ellipsis; 33 | -o-text-overflow: ellipsis; 34 | overflow: hidden; 35 | } 36 | 37 | .copy-link { 38 | cursor: pointer; 39 | &:hover { 40 | text-decoration: underline; 41 | -webkit-text-decoration-style: dashed; 42 | text-decoration-style: dashed; 43 | } 44 | } 45 | 46 | .codegen-header { 47 | padding: 10px 24px; 48 | border-bottom: 1px solid rgba(0, 0, 0, 0.06); 49 | } 50 | 51 | .resources-tree { 52 | padding: 0 6px; 53 | height: calc(100vh - 105px); 54 | display: flex; 55 | flex-direction: column; 56 | min-width: 300px !important; 57 | position: relative; 58 | .ant-menu-root { 59 | overflow-y: auto; 60 | overflow-x: hidden; 61 | } 62 | .docs-select { 63 | padding: 4px 0; 64 | flex: 1; 65 | & ~ .ant-button { 66 | width: 24px; 67 | } 68 | } 69 | .tree-collapse-box { 70 | position: absolute; 71 | right: -10px; 72 | top: 76px; 73 | width: 24px; 74 | height: 24px; 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | cursor: pointer; 79 | box-shadow: 0px 2px 6px 0px rgb(0 0 0 / 20%); 80 | border-radius: 50%; 81 | border: 1px solid #e9e7e7; 82 | z-index: 9; 83 | background-color: #fff; 84 | .anticon { 85 | font-size: 14px; 86 | } 87 | } 88 | .input-row { 89 | width: 100%; 90 | } 91 | } 92 | .resources-tree-collapsed { 93 | width: 80px !important; 94 | min-width: 80px !important; 95 | flex: none !important; 96 | text-align: center; 97 | .input-row { 98 | width: inherit; 99 | } 100 | } 101 | 102 | .ant-typography kbd { 103 | color: #1890ff !important; 104 | } 105 | 106 | .dropdown-menu-item-icon { 107 | font-size: 14px; 108 | color: #ccc; 109 | & :hover { 110 | color: #1890ff; 111 | } 112 | } 113 | 114 | .editable-row .ant-form-item-explain { 115 | position: absolute; 116 | top: 100%; 117 | font-size: 12px; 118 | } 119 | 120 | // 重置vscode中的img默认样式,不影响生成base64图片的大小 121 | img { 122 | max-width: inherit !important; 123 | max-height: inherit !important; 124 | } 125 | 126 | .search-fixed-box { 127 | width: 340px; 128 | border-radius: 4px; 129 | position: fixed; 130 | z-index: 9; 131 | right: 18px; 132 | top: 154px; 133 | .search-fixed-option-box { 134 | flex-wrap: nowrap; 135 | } 136 | .search-fixed-option { 137 | cursor: pointer; 138 | margin: 0 4px; 139 | border-radius: 50%; 140 | &:hover { 141 | background-color: #fff; 142 | } 143 | } 144 | } 145 | 146 | // 高亮对象设置class 147 | ::highlight(search-results) { 148 | background-color: #f06; 149 | color: white; 150 | } 151 | 152 | ::highlight(search-results-current) { 153 | // background-color: #ff0; 154 | background-color: #eded03; 155 | // color: white; 156 | } 157 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-api-group-object.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-11-06 14:46:27 4 | * @Description: generateApiGroupObject - API组基础对象生成 5 | */ 6 | import { cleanParameterDescription } from '@/shared/utils'; 7 | import { pathsItem } from '@/shared/ts/api-interface'; 8 | import generateApiConstName from './generate-api-const-name'; 9 | 10 | // API组基础对象生成 11 | export default function generateApiGroupObject( 12 | apiData: pathsItem & { requestParams: any; responseParams: any }, 13 | prefix: string = '', 14 | ) { 15 | console.log('apiData', apiData); 16 | const { api, summary, method, requestParams, responseParams } = apiData; 17 | const apiName = generateApiConstName(apiData) || summary.replace(/\s+/g, '') || 'api'; 18 | let apiPath = prefix + api; 19 | const apiStrReg = /\{([\d\D]*)\}/g; 20 | 21 | // 处理URL路径参数 22 | const matchPathId = apiStrReg.exec(api); 23 | if (matchPathId) { 24 | apiPath = apiPath.replace(apiStrReg, function (str) { 25 | return `$${str}`; 26 | }); 27 | } 28 | 29 | let result = `// ${summary || '无描述'}\n`; 30 | result += `const ${apiName} = {`; 31 | result += ` url: \`${apiPath}\`,\n`; 32 | result += ` method: '${method}',\n`; 33 | 34 | // 递归处理带有children的项 35 | function processParams(item: any, indent = 2): string { 36 | let result = ''; 37 | const spaces = ' '.repeat(indent); 38 | const { name, type, description, children, schema } = item; 39 | 40 | // 如果是对象或数组类型且有children 41 | if (schema && (schema.type === 'object' || schema.type === 'array') && children && children.length > 0) { 42 | result += `\n${spaces}${name || ''}: `; 43 | 44 | if (type.includes('[]')) { 45 | result += `${spaces}[{`; 46 | } else if (schema.type === 'object') { 47 | result += `${spaces}{`; 48 | } 49 | 50 | // 递归处理children 51 | children.forEach((child: any, childIdx: number) => { 52 | // if (child.type.includes('[]')) { 53 | // result += `\n${spaces} {`; 54 | // result += processParams(child, indent + 2); 55 | // result += `\n${spaces} }`; 56 | // } else { 57 | result += processParams(child, indent + 2); 58 | // } 59 | result += childIdx < children.length - 1 ? ',' : ''; 60 | }); 61 | 62 | if (type.includes('[]')) { 63 | result += `\n${spaces}}]`; 64 | } else if (type === 'object') { 65 | result += `\n${spaces}}`; 66 | } 67 | } else if (name) { 68 | // 常规类型 69 | result += `\n${spaces}${name}: '', // ${type || typeof item} - ${cleanParameterDescription(description || '')}`; 70 | } 71 | 72 | return result; 73 | } 74 | 75 | // 请求参数 76 | if (requestParams && requestParams.length > 0) { 77 | result += ' requestParams: {'; 78 | requestParams.forEach((param: any, index: number) => { 79 | result += processParams(param); 80 | result += index < requestParams.length - 1 ? ',' : ''; 81 | }); 82 | result += ' \n},'; 83 | } else { 84 | result += ' requestParams: {},'; 85 | } 86 | 87 | // 返回参数 88 | if (responseParams && responseParams.length > 0) { 89 | result += ' \nresponseParams: {'; 90 | responseParams.forEach((param: any, index: number) => { 91 | const { name } = param; 92 | if (name === 'data') { 93 | result += processParams(param); 94 | } 95 | }); 96 | result += ' \n}'; 97 | } else { 98 | result += ` responseParams: {}`; 99 | } 100 | 101 | result += '\n};'; 102 | return result; 103 | } 104 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/useHeader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-27 17:16:27 4 | * @Description: ApiSwitchHeader 5 | */ 6 | import { CloseOutlined } from '@ant-design/icons'; 7 | import { isInVSCode } from '@/shared/vscode'; 8 | import { Button, Col, Dropdown, Input, Menu, Row, Popover, Typography } from 'antd'; 9 | import { observer } from 'mobx-react'; 10 | import React from 'react'; 11 | import { useModel } from 'umi'; 12 | import state from '@/stores/index'; 13 | import storage from '@/shared/storage'; 14 | 15 | const ApiSwitchHeader: React.FC = () => { 16 | const { fetchResources, resourcesLoading } = useModel('useApiSwitchModel'); 17 | 18 | const swaggerStore = state.swagger; 19 | const { urlValue, apiUrls } = state.swagger; 20 | 21 | const handleApiUrlDelete = (e: React.MouseEvent, i: number) => { 22 | e.stopPropagation(); 23 | const urls = [...swaggerStore.apiUrls]; 24 | urls.splice(i, 1); 25 | swaggerStore.setApiUrls(urls); 26 | storage.set('storageUrls', urls); 27 | }; 28 | 29 | const inputKeyDown = (e: any) => { 30 | if (e.keyCode === 13) { 31 | fetchResources(); 32 | } 33 | }; 34 | 35 | const menu = ( 36 | { 38 | return { 39 | label: ( 40 | 41 | {apiUrl} 42 | { 46 | handleApiUrlDelete(e, i); 47 | }} 48 | /> 49 | 50 | ), 51 | key: apiUrl, 52 | }; 53 | })} 54 | onClick={(event) => { 55 | swaggerStore.setUrlValue(event.key); 56 | }} 57 | > 58 | ); 59 | 60 | return ( 61 | 62 | 63 | 66 | 67 | 68 | 73 | 通过Swagger文档地址获取api列表,只需要填写 74 | doc.html 75 | 前一部分地址。
76 | 比如 77 | http://xxx-dev.leekhub.com/order-server/doc.html 78 | 地址,就输入 79 | http://xxx-dev.leekhub.com/order-server 80 |
81 | 也支持openApi内容格式的 82 | json、yaml文件 83 | {!isInVSCode && ( 84 |
85 | Web网页存在跨域问题,推荐使用VSCode插件: 86 | 91 | 点击安装 92 | 93 |
94 | )} 95 | 96 | } 97 | > 98 | { 100 | swaggerStore.setUrlValue((e.nativeEvent.target as any).value); 101 | }} 102 | onKeyDown={inputKeyDown} 103 | placeholder="输入接口文档url,如swagger" 104 | defaultValue={urlValue} 105 | value={urlValue} 106 | /> 107 |
108 | 109 | 110 | 111 | 历史地址 112 | 113 | 114 |
115 | ); 116 | }; 117 | export default observer(ApiSwitchHeader); 118 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/avue/generate-avue-table-custom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: generateAvueTablePageCode 3 | * selectedData 4 | * selectedData.requestSelectedData 请求参数-勾选 5 | * selectedData.responseSelectedData 响应参数-勾选 6 | * selectedData.modelData 模型参数-勾选 7 | * selectedData.resourceDetail 当前api菜单完整数据 8 | * window.utilsFn: { 9 | getApiNameAsPageName: any; 10 | prettyCode: any; 11 | generateTableColumnsProps: any; 12 | cleanParameterDescription: any; 13 | }, 14 | * @return {string} 15 | */ 16 | 17 | function generateAvueTablePageCode(selectedData, api) { 18 | const utilsFn = window.utilsFn ?? {}; 19 | const { responseSelectedData: body } = selectedData; 20 | 21 | const parametersSet = new Set(); 22 | (api?.parameters ?? []).forEach((param) => { 23 | parametersSet.add(param.name); 24 | }); 25 | const columnCode = utilsFn.generateTableColumnsProps(body, true, (row, index) => { 26 | let reg = new RegExp(/[a-zA-Z]+/g); 27 | let result = { 28 | prop: row.name, 29 | label: utilsFn.cleanParameterDescription(row.description), 30 | minWidth: reg.test(text) 31 | ? text.length > 16 32 | ? text.length * 7 + 10 33 | : 140 34 | : text.length > 8 35 | ? text.length * 14 + 10 36 | : 140, 37 | overHidden: true, 38 | }; 39 | const item = parametersSet.has(row.name); 40 | if (item) { 41 | result.search = true; 42 | 43 | if ( 44 | row.name.indexOf('状态') !== -1 || 45 | (row.description && row.description.indexOf('ENUM#') !== -1) || 46 | (row.enum && row.enum.length > 0) 47 | ) { 48 | result.searchType = 'select'; 49 | } 50 | if (['date', 'time'].includes(row.name)) { 51 | result.searchType = 'datetime'; 52 | result.format = 'YYYY-MM-DD HH:mm:ss'; 53 | result.valueFormat = 'YYYY-MM-DD HH:mm:ss'; 54 | } 55 | } 56 | return result; 57 | }); 58 | 59 | const componentName = utilsFn.getApiNameAsPageName(api.api); 60 | const matchApiName = api?.api.match(/^\/api\/[a-zA-Z]+/); 61 | const apiName = matchApiName && matchApiName?.length > 0 ? matchApiName[0].replace('/api/', '') : ''; 62 | 63 | return ` 64 | /** * ${api.description ?? api.summary} */ 65 | 82 | 83 | 128 | 129 | `; 130 | } 131 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/components/HistoryTextDropdown.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-02-03 10:23:22 4 | * @Description: HistoryTextDropdown 5 | */ 6 | import { Row, Dropdown, Menu, Popover, Button, Table } from 'antd'; 7 | import { CloseOutlined } from '@ant-design/icons'; 8 | import state from '@/stores/index'; 9 | import { observer } from 'mobx-react-lite'; 10 | import { useMemo, useState } from 'react'; 11 | import type { ColumnsType } from 'antd/es/table'; 12 | import { filterSplitTextTowords } from '@/shared/utils'; 13 | import { uniqueId } from 'lodash'; 14 | import { HistoryItem } from '@/shared/common'; 15 | 16 | const HistoryTextDropdown: React.FC<{ onChange: (type: string, key: any) => void }> = (props) => { 17 | const { onChange } = props; 18 | const swaggerStore = state.swagger; 19 | const { historyTexts } = state.swagger; 20 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 21 | 22 | const columns: ColumnsType = [ 23 | { 24 | title: '文本', 25 | key: 'content', 26 | dataIndex: 'content', 27 | render: (value) => { 28 | return JSON.stringify(value); 29 | }, 30 | width: 650, 31 | }, 32 | { 33 | title: '操作', 34 | key: 'operation', 35 | fixed: 'right', 36 | width: 220, 37 | render: (value, row, index) => ( 38 | <> 39 | 48 | 58 | 68 | 78 | 79 | ), 80 | }, 81 | ]; 82 | 83 | const handleClick = (t: string, row: any, index: number) => { 84 | if (t === 'select') { 85 | onChange('text', row.content); 86 | } else if (t === 'join') { 87 | // 拼接文本 88 | const orderRows = concatOrderRows(row.id); 89 | const record: HistoryItem = { 90 | id: uniqueId('history_text_'), 91 | content: [...row.content, ...orderRows], 92 | }; 93 | swaggerStore.setHistoryTexts([record, ...historyTexts]); 94 | } else if (t === 'setting') { 95 | onChange( 96 | 'searchColumn', 97 | JSON.stringify({ 98 | search: row.content.map((v: any) => v.words), 99 | column: concatOrderRows(row.id).map((v: any) => v.words), 100 | }), 101 | ); 102 | } else if (t === 'delete') { 103 | handleDelete(row, index); 104 | } 105 | setSelectedRowKeys([]); 106 | }; 107 | 108 | const concatOrderRows = (currentId: string) => { 109 | let orderRows: any = []; 110 | selectedRowKeys.forEach((v) => { 111 | const item = historyTexts.find((o) => v === o.id && v !== currentId); 112 | if (item) { 113 | orderRows = [...orderRows, ...filterSplitTextTowords(item.content)]; 114 | } 115 | }); 116 | return orderRows; 117 | }; 118 | 119 | const handleDelete = (row: HistoryItem, i: number) => { 120 | const list = [...historyTexts]; 121 | list.splice(i, 1); 122 | swaggerStore.setHistoryTexts(list); 123 | }; 124 | 125 | const onSelectChange = (newSelectedRowKeys: React.Key[]) => { 126 | setSelectedRowKeys(newSelectedRowKeys); 127 | }; 128 | 129 | const rowSelection = { 130 | selectedRowKeys, 131 | onChange: onSelectChange, 132 | }; 133 | 134 | const content = useMemo(() => { 135 | return ( 136 | 137 | ); 138 | }, [historyTexts, selectedRowKeys]); 139 | 140 | return ( 141 | 142 | 143 | 144 | ); 145 | }; 146 | 147 | export default observer(HistoryTextDropdown); 148 | -------------------------------------------------------------------------------- /src/shared/vscode/index.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise, AxiosRequestConfig } from 'axios'; 2 | import { noop, uniqueId } from 'lodash'; 3 | import storage from '../storage'; 4 | import state from '@/stores/index'; 5 | import { message } from 'antd'; 6 | import { dispatch } from '@/shared/useBus'; 7 | import { storeTipsInVscode, storeTipsInBrowser } from '@/shared/config.json'; 8 | 9 | export const fetchResponsePromiseMap: Record void)[]> = {}; 10 | 11 | export let isInVSCode = false; 12 | 13 | let vscode: 14 | | undefined 15 | | { 16 | postMessage(message: any): Thenable; 17 | getState: () => void; 18 | setState: (newState: object) => void; 19 | }; 20 | 21 | function getVscode() { 22 | if (!vscode) { 23 | try { 24 | vscode = acquireVsCodeApi(); 25 | isInVSCode = true; 26 | } catch (err) { 27 | console.warn('runningTime is not in vscode'); 28 | vscode = { 29 | getState: noop, 30 | postMessage: (message) => Promise.resolve(true), 31 | setState: noop, 32 | }; 33 | } 34 | } 35 | return vscode; 36 | } 37 | getVscode(); 38 | 39 | export function postVSCodeMessage(command: string, data: any = {}) { 40 | // console.log('post command: ', command, data); 41 | getVscode()?.postMessage({ 42 | command, 43 | data, 44 | }); 45 | } 46 | 47 | const CommandHandler: Record any> = { 48 | fetchResponse(data) { 49 | const [resolve, reject] = fetchResponsePromiseMap[data.sessionId]; 50 | console.log('===================================='); 51 | console.log(data.success); 52 | console.log('===================================='); 53 | if (data.success) { 54 | resolve(data.response); 55 | } else { 56 | message.error('获取失败!'); 57 | reject(data.response); 58 | } 59 | delete fetchResponsePromiseMap[data.sessionId]; 60 | }, 61 | updateGlobalStorage(data) { 62 | const arr = data ? Object.keys(data) : []; 63 | if (arr.length > 0) { 64 | for (let key in data) { 65 | storage.set(key, data[key]); 66 | if (key === 'storageUrls' && data[key]) { 67 | state.swagger.setApiUrlsOrInitUrlValue(data[key]); 68 | } else if (key === 'storageHistoryTexts' && data[key]) { 69 | let parmas = data[key]; 70 | if (typeof parmas === 'string') { 71 | try { 72 | parmas = JSON.parse(parmas); 73 | } catch (error) {} 74 | } 75 | state.swagger.setHistoryTexts(parmas); 76 | } else if (key === 'extractType' && data[key]) { 77 | state.swagger.setExtractType(data[key]); 78 | } 79 | } 80 | } 81 | }, 82 | updateCodeGenSettings(data) { 83 | state.settings.updateSettings(data || []); 84 | }, 85 | updateCodeGenCustomMethods(data) { 86 | state.custom.updateCustomMethods(data || []); 87 | }, 88 | postWebviewActiveText(data) { 89 | const { mounted, activeText, type } = data; 90 | if (type === 'AutoMatchActiveText') { 91 | state.settings.setBaseCode(activeText); 92 | if (mounted) { 93 | dispatch('activeTextMatchCode'); 94 | } 95 | } else if (type === 'SearchFixedText') { 96 | state.settings.setSearchFixedText(activeText); 97 | dispatch('activeSearchFixedText'); 98 | } 99 | }, 100 | }; 101 | 102 | const messageListener = (event: any) => { 103 | const { command, data } = event.data; 104 | try { 105 | if (CommandHandler[command]) { 106 | CommandHandler[command](data); 107 | } 108 | } catch (err) {} 109 | }; 110 | 111 | export function setupBackgroundManagement() { 112 | window.removeEventListener('message', messageListener); 113 | window.addEventListener('message', messageListener); 114 | } 115 | 116 | export function fetch(option: AxiosRequestConfig & { sessionId?: string }, commandName = 'fetch'): AxiosPromise { 117 | if (option.headers) { 118 | option.headers = option.headers; 119 | } else { 120 | option.headers = { 121 | 'User-Agent': 122 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36', 123 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 124 | }; 125 | } 126 | 127 | const sessionId = uniqueId('rh_codegen_'); 128 | option.sessionId = sessionId; 129 | const promise = new Promise((resolve, reject) => { 130 | fetchResponsePromiseMap[sessionId] = [resolve, reject]; 131 | }); 132 | postVSCodeMessage(commandName, option); 133 | return promise as AxiosPromise; 134 | } 135 | 136 | export function fetchInVSCode(option: AxiosRequestConfig, commandName = 'fetch') { 137 | option.headers = option.headers || {}; 138 | return fetch(option, commandName); 139 | } 140 | 141 | export const storeTips = isInVSCode ? storeTipsInVscode : storeTipsInBrowser; 142 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/components/SearchFixedBox.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-21 17:11:40 4 | * @Description: SearchFixedBox 5 | */ 6 | import { Row, Input } from 'antd'; 7 | import type { InputRef } from 'antd'; 8 | import React, { useEffect, useRef } from 'react'; 9 | import { useModel } from 'umi'; 10 | import { UpOutlined, DownOutlined, CloseOutlined } from '@ant-design/icons'; 11 | import { useKeyPress } from 'ahooks'; 12 | import useSearchPageText from '@/shared/searchPageText'; 13 | import useBus from '@/shared/useBus'; 14 | import state from '@/stores/index'; 15 | import { debounce } from 'lodash'; 16 | 17 | const SearchFixedBox: React.FC<{ onUrlTextChange: (urlText: string) => boolean }> = (props) => { 18 | const { onUrlTextChange } = props; 19 | const { 20 | searchTextFixed, 21 | setSearchTextFixed, 22 | apiSearchText, 23 | setapiSearchText, 24 | selectedApi, 25 | resourceDetail, 26 | searchTags, 27 | } = useModel('useApiSwitchModel'); 28 | const { 29 | clearHighlights, 30 | setTextNodeRange, 31 | setNewTextNodes, 32 | rangeIndexAdd, 33 | rangeIndexSubtract, 34 | rangeIndexText, 35 | } = useSearchPageText('.api-detail-tabs .ant-tabs-content .ant-tabs-tabpane-active', '.api-detail-content'); 36 | const inputRef = useRef(null); 37 | 38 | const inputChange = (e: any) => { 39 | const value = e.target.value; 40 | setapiSearchText(value); 41 | debounceValueChange(value); 42 | }; 43 | 44 | const debounceValueChange = debounce((value: string) => { 45 | if (value === '') { 46 | clearHighlights(); 47 | } else { 48 | setTextNodeRange(value); 49 | } 50 | }, 400); 51 | 52 | // 设置搜索框弹起,focus 53 | const handleFixed = (isKeyPress?: boolean) => { 54 | if (selectedApi) { 55 | setSearchTextFixed(true); 56 | setTimeout(() => { 57 | requestAnimationFrame(() => { 58 | inputRef.current?.focus({ 59 | cursor: isKeyPress ? 'all' : 'end', 60 | }); 61 | }); 62 | }, 100); 63 | } 64 | }; 65 | 66 | const handleSetNewText = (text?: string) => { 67 | setNewTextNodes(); // 收集文本TextNode 68 | setTextNodeRange(text ?? apiSearchText); // 设置选中文本高亮 69 | }; 70 | 71 | const handleActiveSearchText = () => { 72 | const { searchFixedText } = state.settings; 73 | if (searchFixedText !== '') { 74 | const urlReg = /^\/[a-z0-9-]+(?:\/[a-zA-Z0-9-${}]+)*$/; 75 | if (urlReg.test(searchFixedText)) { 76 | // 如果匹配为接口url 77 | onUrlTextChange(searchFixedText); 78 | } 79 | 80 | selectedApi && 81 | requestAnimationFrame(() => { 82 | state.settings.setSearchFixedText(''); // 即时清空 83 | setapiSearchText(searchFixedText); // 设置当前值 84 | handleFixed(); // 弹起,focus 85 | handleSetNewText(searchFixedText); 86 | }); 87 | } 88 | }; 89 | 90 | useKeyPress(['ctrl.f'], () => { 91 | handleFixed(true); 92 | handleSetNewText(); 93 | }); 94 | 95 | useEffect(() => { 96 | if ((apiSearchText ?? true) && searchTextFixed) { 97 | handleSetNewText(apiSearchText); 98 | } 99 | return () => { 100 | clearHighlights(); 101 | }; 102 | }, [selectedApi]); 103 | 104 | useKeyPress( 105 | 'esc', 106 | () => { 107 | if (searchTextFixed) { 108 | setSearchTextFixed(false); 109 | clearHighlights(); 110 | } 111 | }, 112 | { 113 | target: document.getElementById('search-fixed-box'), 114 | }, 115 | ); 116 | 117 | // 事件汇总收集activeSearchText触发 118 | useBus( 119 | 'activeSearchFixedText', 120 | () => { 121 | handleActiveSearchText(); 122 | }, 123 | [selectedApi, resourceDetail, searchTags], 124 | ); 125 | 126 | if (searchTextFixed) { 127 | return ( 128 | 129 | 137 | {rangeIndexText} 138 | { 140 | rangeIndexSubtract(); 141 | }} 142 | className="search-fixed-option search-fixed-sub" 143 | > 144 | 145 | 146 | { 148 | rangeIndexAdd(); 149 | }} 150 | className="search-fixed-option search-fixed-add" 151 | > 152 | 153 | 154 | { 156 | setSearchTextFixed(false); 157 | clearHighlights(); 158 | }} 159 | className="search-fixed-option search-fixed-close" 160 | > 161 | 162 | 163 | 164 | } 165 | /> 166 | 167 | ); 168 | } else { 169 | return <>; 170 | } 171 | }; 172 | 173 | export default SearchFixedBox; 174 | -------------------------------------------------------------------------------- /src/shared/searchPageText.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2023-05-21 17:11:40 4 | * @Description: searchPageText 5 | */ 6 | import { useRef, useState, useMemo, useCallback } from 'react'; 7 | type Highlight = any; 8 | 9 | declare const CSS: { 10 | highlights: Map; 11 | }; 12 | 13 | type useSearchPageText = { 14 | clearHighlights: () => void; 15 | setTextNodeRange: (str: string) => void; 16 | setNewTextNodes: () => void; 17 | rangeIndexAdd: () => void; 18 | rangeIndexSubtract: () => void; 19 | rangeIndexText: string; 20 | }; 21 | 22 | const useSearchPageText = (selectorKey: string, scrollElementClass?: string): useSearchPageText => { 23 | const clearHighlights = () => { 24 | // 清除上个高亮 25 | CSS.highlights.clear(); 26 | }; 27 | 28 | let allTextNodes: any[] = []; 29 | const ranges = useRef([]); 30 | const [rangesLen, setRangesLen] = useState(0); 31 | const [rangeIndex, setRangeIndex] = useState(0); 32 | const rangeIndexText = useMemo(() => { 33 | return `${rangesLen > 0 ? rangeIndex + 1 : 0}/${rangesLen}`; 34 | }, [rangeIndex, rangesLen]); 35 | 36 | /** 37 | * @description: 获取区域所有文本节点 38 | * @return {*} 39 | */ 40 | const setNewTextNodes = () => { 41 | const result = []; 42 | if (!document) { 43 | return; 44 | } 45 | if (!selectorKey) { 46 | console.log('please input selectorKey'); 47 | return; 48 | } 49 | const articleNode: any = document.querySelector(selectorKey); 50 | if (articleNode) { 51 | // Find all text nodes in the article. We'll search within 52 | // these text nodes. 53 | const treeWalker = document.createTreeWalker(articleNode, NodeFilter.SHOW_TEXT); 54 | let currentNode = treeWalker.nextNode(); 55 | while (currentNode) { 56 | result.push(currentNode); 57 | currentNode = treeWalker.nextNode(); 58 | } 59 | } 60 | allTextNodes = result; 61 | }; 62 | 63 | setNewTextNodes(); 64 | 65 | /** 66 | * @description: 过滤文本节点是否与选中文本匹配 67 | * @param {string} str 68 | * @return {*} 69 | */ 70 | const setTextNodeRange = (str: string) => { 71 | if (allTextNodes.length > 0 && str !== '') { 72 | clearHighlights(); 73 | 74 | // 查找所有文本节点是否包含搜索词 75 | const curRanges: any[] = allTextNodes 76 | .map((el) => { 77 | return { el, text: el.textContent }; 78 | }) 79 | .map(({ text, el }) => { 80 | const indices = []; 81 | let startPos = 0; 82 | while (startPos < text.length) { 83 | const index: number = text.indexOf(str, startPos); 84 | if (index === -1) break; 85 | indices.push(index); 86 | startPos = index + str.length; 87 | } 88 | 89 | // 根据搜索词的位置创建选区 90 | return indices.map((index) => { 91 | const range = new Range(); 92 | range.setStart(el, index); 93 | range.setEnd(el, index + str.length); 94 | return range; 95 | }); 96 | }); 97 | 98 | const Highlight = (window as any).Highlight; 99 | ranges.current = curRanges.filter((v) => v.length > 0).flat(); 100 | setRangesLen(ranges.current.length); 101 | // 创建高亮对象 102 | const searchResultsHighlight = new Highlight(...ranges.current); 103 | 104 | // 注册高亮 105 | CSS.highlights.set('search-results', searchResultsHighlight); 106 | setTimeout(() => { 107 | requestAnimationFrame(() => { 108 | handleScrollToRange(0); 109 | }); 110 | }, 0); 111 | } 112 | }; 113 | 114 | /** 115 | * @description: 将滚动区域的滚动条滚动到range索引位置 116 | * @return {*} 117 | */ 118 | const handleScrollToRange = (index: number) => { 119 | setRangeIndex(index); 120 | if (!scrollElementClass) { 121 | console.log('please input scrollElementClass'); 122 | return; 123 | } 124 | const scrollElement = document.querySelector(scrollElementClass); 125 | if (ranges.current[index]) { 126 | const parentNode = ranges.current[index]?.startContainer?.parentNode; 127 | if (scrollElement && parentNode) { 128 | parentNode.scrollIntoView({ block: 'center', behavior: 'smooth' }); 129 | const Highlight = (window as any).Highlight; 130 | // 创建当前选中高亮对象-注册class 131 | CSS.highlights.set('search-results-current', new Highlight(ranges.current[index])); 132 | } 133 | } 134 | }; 135 | 136 | /** 137 | * @description: range索引增加并滚动 138 | * @return {*} 139 | */ 140 | const rangeIndexAdd = () => { 141 | if (ranges.current.length === 0) return; 142 | const curRangeIndex = rangeIndex < ranges.current.length - 1 ? rangeIndex + 1 : 0; 143 | handleScrollToRange(curRangeIndex); 144 | }; 145 | 146 | /** 147 | * @description: range索引减少并滚动 148 | * @return {*} 149 | */ 150 | const rangeIndexSubtract = () => { 151 | if (ranges.current.length === 0) return; 152 | const curRangeIndex = rangeIndex > 0 ? rangeIndex - 1 : ranges.current.length - 1; 153 | handleScrollToRange(curRangeIndex); 154 | }; 155 | 156 | return { 157 | clearHighlights, 158 | setTextNodeRange, 159 | setNewTextNodes, 160 | rangeIndexAdd, 161 | rangeIndexSubtract, 162 | rangeIndexText, 163 | }; 164 | }; 165 | 166 | export default useSearchPageText; 167 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/parametersSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input, Row, Card, Button } from 'antd'; 3 | 4 | import { RefObject } from '@umijs/renderer-react/node_modules/@types/react'; 5 | import EditableTable from './EditableTable'; 6 | 7 | class ParametersSelect extends React.Component< 8 | { 9 | value: string; 10 | parameters: { 11 | requestParameters: { properties: {} }; 12 | responseParameters: {}; 13 | }; 14 | definitions: {}; 15 | }, 16 | { 17 | value: string; 18 | requestParameters: any; 19 | responseParameters: {}; 20 | } 21 | > { 22 | EditableTableRef: RefObject; 23 | constructor(props: { 24 | value: string; 25 | parameters: { 26 | requestParameters: { properties: {} }; 27 | responseParameters: {}; 28 | }; 29 | definitions: {}; 30 | }) { 31 | super(props); 32 | this.state = { 33 | value: props.value, 34 | requestParameters: [ 35 | { 36 | type: 'integer', 37 | format: 'int64', 38 | description: '#城市id#', 39 | key: 'cityId', 40 | name: '城市id', 41 | componentType: 'InputNumber', 42 | }, 43 | { 44 | type: 'string', 45 | description: '#城市名称(冗余)#', 46 | key: 'cityName', 47 | name: '城市名称(冗余)', 48 | componentType: 'Input', 49 | }, 50 | { 51 | type: 'integer', 52 | format: 'int64', 53 | description: '#区域id#', 54 | key: 'districtId', 55 | name: '区域id', 56 | componentType: 'InputNumber', 57 | }, 58 | { 59 | type: 'string', 60 | description: '#区域名称(冗余)#', 61 | key: 'districtName', 62 | name: '区域名称(冗余)', 63 | componentType: 'Input', 64 | }, 65 | { 66 | type: 'string', 67 | description: '#详细地址#', 68 | key: 'parkplaceholder', 69 | name: '详细地址', 70 | componentType: 'Input', 71 | }, 72 | { 73 | type: 'string', 74 | description: '#描述#', 75 | key: 'parkDesc', 76 | name: '描述', 77 | componentType: 'Input', 78 | }, 79 | { 80 | type: 'string', 81 | description: '园区名称', 82 | key: 'parkName', 83 | name: '园区名称', 84 | componentType: 'Input', 85 | }, 86 | { 87 | type: 'boolean', 88 | description: '#是否显示#', 89 | key: 'parkShow', 90 | name: '是否显示', 91 | componentType: 'Input', 92 | }, 93 | { 94 | type: 'integer', 95 | format: 'int64', 96 | description: '#省份id#', 97 | key: 'provinceId', 98 | name: '省份id', 99 | componentType: 'InputNumber', 100 | }, 101 | { 102 | type: 'string', 103 | description: '#省份名称(冗余)#', 104 | key: 'provinceName', 105 | name: '省份名称(冗余)', 106 | componentType: 'Input', 107 | }, 108 | { 109 | type: 'integer', 110 | format: 'int32', 111 | description: '#排序#', 112 | key: 'sort', 113 | name: '排序', 114 | componentType: 'InputNumber', 115 | }, 116 | ], 117 | responseParameters: [], 118 | }; 119 | this.EditableTableRef = React.createRef(); 120 | } 121 | 122 | componentDidMount() { 123 | this.parametersHandle(); 124 | } 125 | componentWillReceiveProps() { 126 | this.parametersHandle(); 127 | } 128 | 129 | parametersHandle() { 130 | console.log('this.props', this.props); 131 | 132 | let { parameters } = this.props; 133 | if (parameters.requestParameters) { 134 | let properties = parameters.requestParameters.properties; 135 | let requestParameters = []; 136 | for (const key in properties) { 137 | if (Object.prototype.hasOwnProperty.call(properties, key)) { 138 | const element: { description: string; type: string } = 139 | properties[key as keyof typeof properties]; 140 | let name = element.description.replace(/#/g, ''); 141 | requestParameters.push({ 142 | ...element, 143 | key: key, 144 | name, 145 | // 前置判断组件类型 146 | componentType: element.type === 'integer' ? 'InputNumber' : 'Input', 147 | require: name.indexOf('冗余') === -1, 148 | placeholder: `请输入${name}`, 149 | }); 150 | } 151 | } 152 | this.setState({ 153 | requestParameters, 154 | }); 155 | // this.EditableTableRef.init(requestParameters); 156 | } 157 | } 158 | 159 | render() { 160 | return ( 161 | 162 |

请求参数

163 | 164 | 172 | 177 |
185 | { 186 | // this.state.requestParameters.map((item: { name: '', componentType: '', key: '' }) => { 187 | // return ( 191 | // {item.componentType} 192 | // ) 193 | // }) 194 | } 195 | 196 |
197 |
198 | ); 199 | } 200 | } 201 | 202 | export default ParametersSelect; 203 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ApiDefinitionDropdown.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-14 13:50:44 4 | * @Description: api方法下拉代码生成 5 | */ 6 | import { Dropdown, Menu, Button } from 'antd'; 7 | import CodeOutlined from '@ant-design/icons/lib/icons/CodeOutlined'; 8 | import { useModel } from 'umi'; 9 | import { useMemo } from 'react'; 10 | import state from '@/stores/index'; 11 | import { getStringToFn, getRequestParams, filterTransResult } from '@/shared/utils'; 12 | import getResponseParams from '@/shared/getResponseParams'; 13 | import { CustomMethodsItem } from '@/shared/ts/custom'; 14 | import { codeGenerateMethods } from './code-generate/index'; 15 | import { observer } from 'mobx-react-lite'; 16 | import { translateZhToEn } from '@/shared/baidu-translate'; 17 | 18 | const ApiDefinitionDropdown: React.FC<{ 19 | api: any; 20 | methodType?: string; 21 | dropdownTitle?: string; 22 | buttonType?: 'link' | 'text' | 'dashed' | 'default' | 'ghost' | 'primary' | undefined; 23 | onChange?: (key: string, item?: CustomMethodsItem | undefined) => void; 24 | isPaths?: boolean; // 是否Paths循环生成 25 | }> = function (props) { 26 | const { api, dropdownTitle = '代码生成', methodType = 'api', buttonType = 'link', onChange, isPaths } = props; 27 | 28 | const generateMethods = codeGenerateMethods 29 | .filter((v) => v.type === methodType && v.status) 30 | .sort((a, b) => b.sort - a.sort); 31 | const { setDefinitionCodeDrawerProps, resourceDetail, apiurlPrefix } = useModel('useApiSwitchModel'); 32 | 33 | const CustomMethods = useMemo(() => Array.from(state.custom.EnabledCustomMethods), [ 34 | state.custom.EnabledCustomMethods, 35 | ]); 36 | 37 | const prefix = useMemo(() => { 38 | // 默认前缀 + basePath 39 | return `${apiurlPrefix}${resourceDetail?.basePath}`; 40 | }, [apiurlPrefix, resourceDetail]); 41 | 42 | const items = useMemo(() => { 43 | let currentCustomMethods = CustomMethods.filter((v: CustomMethodsItem) => v.type === methodType); 44 | return [ 45 | { 46 | key: 'root', 47 | label: 'root', 48 | type: 'group', 49 | children: generateMethods.map((v) => { 50 | return { 51 | key: v.key, 52 | label: v.label, 53 | }; 54 | }), 55 | }, 56 | currentCustomMethods.length > 0 57 | ? { 58 | key: 'custom', 59 | label: 'custom', 60 | type: 'group', 61 | children: currentCustomMethods, 62 | } 63 | : null, 64 | ]; 65 | }, [CustomMethods]); 66 | 67 | const requestParams = getRequestParams(api, resourceDetail); 68 | 69 | const handleMenuItemClick = async ({ key }: any) => { 70 | let drawerProps = { 71 | title: '', 72 | visible: true, 73 | language: 'javascript', 74 | generateCode: () => {}, 75 | }; 76 | const generateMethod: any = generateMethods.find((v) => v.key === key); 77 | if (isPaths) { 78 | if (methodType === 'api') { 79 | drawerProps.title = api?.summary || ''; 80 | const { paths } = api; 81 | let apiFn: any = () => {}; 82 | if (generateMethod) { 83 | apiFn = generateMethod.function; 84 | } else { 85 | let item: any = CustomMethods.find((v) => v.key === key) ?? {}; 86 | apiFn = item?.function ? getStringToFn(item.function) : () => {}; 87 | } 88 | drawerProps.generateCode = () => { 89 | let resultText = ''; 90 | paths.forEach((m: any) => { 91 | const curRequestParams = m.requestParams || getRequestParams(m, resourceDetail); 92 | const curResponseParams = m.responseParams || getResponseParams(m, resourceDetail); 93 | resultText += 94 | apiFn({ ...m, requestParams: curRequestParams, responseParams: curResponseParams }, prefix) + 95 | ` 96 | `; 97 | }); 98 | return resultText; 99 | }; 100 | 101 | setDefinitionCodeDrawerProps(drawerProps); 102 | } 103 | } else { 104 | if (methodType === 'api') { 105 | drawerProps.title = api?.summary || ''; 106 | if (generateMethod) { 107 | drawerProps.generateCode = () => generateMethod.function({ ...api, requestParams }, prefix); 108 | } else { 109 | let item: any = CustomMethods.find((v) => v.key === key) ?? {}; 110 | const cutomCodeFn = item?.function ? getStringToFn(item.function) : () => {}; 111 | drawerProps.generateCode = () => cutomCodeFn({ ...api, requestParams }, prefix); 112 | } 113 | setDefinitionCodeDrawerProps(drawerProps); 114 | } else if (methodType === 'text') { 115 | const value = api; 116 | drawerProps.title = JSON.stringify(value) || ''; 117 | let translateReault = new Map(); 118 | const res = await translateZhToEn(value.join('\n')); 119 | if (res) { 120 | translateReault = filterTransResult(res?.trans_result || []); 121 | } 122 | if (generateMethod) { 123 | drawerProps.generateCode = () => generateMethod.function(value, translateReault); 124 | } else { 125 | let item: any = CustomMethods.find((v) => v.key === key) ?? {}; 126 | const cutomCodeFn = item?.function ? getStringToFn(item.function) : () => {}; 127 | drawerProps.generateCode = () => cutomCodeFn(value, translateReault); 128 | } 129 | setDefinitionCodeDrawerProps(drawerProps); 130 | } else { 131 | if (onChange) { 132 | let item: any = generateMethods.find((v) => v.key === key); 133 | if (item) { 134 | onChange(key, { ...item }); 135 | } else { 136 | item = CustomMethods.find((v) => v.key === key); 137 | onChange(key, { ...item }); 138 | } 139 | } 140 | } 141 | } 142 | }; 143 | 144 | return ( 145 | } trigger={['hover']}> 146 | {isPaths ? ( 147 | 148 | ) : ( 149 | 152 | )} 153 | 154 | ); 155 | }; 156 | 157 | export default observer(ApiDefinitionDropdown); 158 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ApiurlPrefixEditTable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-11-25 11:27:44 4 | * @Description: ApiurlPrefixEditTable 5 | */ 6 | import { Form, Input, Switch, Popconfirm, Table, Typography, Space, Button } from 'antd'; 7 | import React, { useState, useMemo } from 'react'; 8 | import { apiurlPrefixItem } from '@/shared/ts/settings'; 9 | import state from '@/stores/index'; 10 | import { uniqueId } from 'lodash'; 11 | import { storeTips } from '@/shared/vscode'; 12 | 13 | interface Item extends apiurlPrefixItem {} 14 | 15 | interface EditableCellProps extends React.HTMLAttributes { 16 | editing: boolean; 17 | dataIndex: string; 18 | title: any; 19 | inputType: 'switch' | 'text'; 20 | record: Item; 21 | index: number; 22 | children: React.ReactNode; 23 | } 24 | 25 | const EditableCell: React.FC = ({ 26 | editing, 27 | dataIndex, 28 | title, 29 | inputType, 30 | record, 31 | index, 32 | children, 33 | ...restProps 34 | }) => { 35 | const inputNode = inputType === 'switch' ? : ; 36 | 37 | return ( 38 |
56 | ); 57 | }; 58 | 59 | const ApiurlPrefixEditTable: React.FC = () => { 60 | const apiurlPrefixList = useMemo(() => { 61 | return state.settings.Settings?.apiurlPrefixList ?? []; 62 | }, [state.settings.Settings]); 63 | const [form] = Form.useForm(); 64 | const [data, setData] = useState(apiurlPrefixList); 65 | const [editingKey, setEditingKey] = useState(''); 66 | 67 | const isEditing = (record: Item) => record.key === editingKey; 68 | 69 | const handleAdd = () => { 70 | const item: apiurlPrefixItem = { 71 | key: uniqueId(Date.now().toString()), 72 | url: '', 73 | prefix: '', 74 | status: true, 75 | }; 76 | const list = [item, ...data]; 77 | setData(list); 78 | form.setFieldsValue(item); 79 | setEditingKey(item.key); 80 | }; 81 | 82 | const edit = (record: Partial & { key: React.Key }) => { 83 | form.setFieldsValue({ ...record }); 84 | setEditingKey(record.key); 85 | }; 86 | 87 | const handleDelete = (record: Partial & { key: React.Key }, index: number) => { 88 | const list = [...data]; 89 | list.splice(index, 1); 90 | setEditingKey(''); 91 | setData(list); 92 | state.settings.setSettingsApiurlPrefixList(list); 93 | }; 94 | 95 | const cancel = () => { 96 | if (!apiurlPrefixList.find((v) => v.key === editingKey)) { 97 | const list = [...data]; 98 | list.shift(); 99 | setData(list); 100 | } 101 | setEditingKey(''); 102 | }; 103 | 104 | const save = async (key: React.Key) => { 105 | try { 106 | const row = (await form.validateFields()) as Item; 107 | 108 | const newData: apiurlPrefixItem[] = [...data]; 109 | const index = newData.findIndex((item) => key === item.key); 110 | if (index > -1) { 111 | const item = newData[index]; 112 | newData.splice(index, 1, { 113 | ...item, 114 | ...row, 115 | }); 116 | setData(newData); 117 | setEditingKey(''); 118 | } else { 119 | newData.push(row); 120 | setData(newData); 121 | setEditingKey(''); 122 | } 123 | state.settings.setSettingsApiurlPrefixList(newData); 124 | } catch (errInfo) { 125 | console.log('Validate Failed:', errInfo); 126 | } 127 | }; 128 | 129 | const columns = [ 130 | { 131 | title: '接口文档地址', 132 | dataIndex: 'url', 133 | editable: true, 134 | }, 135 | { 136 | title: '前缀', 137 | dataIndex: 'prefix', 138 | editable: true, 139 | }, 140 | { 141 | title: '状态', 142 | dataIndex: 'status', 143 | render: (value: any) => {value ? '启用' : '停用'}, 144 | editable: true, 145 | }, 146 | { 147 | title: '操作', 148 | dataIndex: 'operation', 149 | render: (_: any, record: Item, index: number) => { 150 | const editable = isEditing(record); 151 | return editable ? ( 152 | 153 | save(record.key)} style={{ marginRight: 8 }}> 154 | 保存 155 | 156 | 157 | 取消 158 | 159 | 160 | ) : ( 161 | <> 162 | edit(record)}> 163 | 编辑 164 | 165 | { 168 | handleDelete(record, index); 169 | }} 170 | onCancel={() => {}} 171 | okText="是" 172 | cancelText="否" 173 | > 174 | 177 | 178 | 179 | ); 180 | }, 181 | }, 182 | ]; 183 | 184 | const mergedColumns = columns.map((col) => { 185 | if (!col.editable) { 186 | return col; 187 | } 188 | return { 189 | ...col, 190 | onCell: (record: Item) => ({ 191 | record, 192 | inputType: col.dataIndex === 'status' ? 'switch' : 'text', 193 | dataIndex: col.dataIndex, 194 | title: col.title, 195 | editing: isEditing(record), 196 | }), 197 | }; 198 | }); 199 | 200 | return ( 201 |
202 | 203 | 212 | {storeTips} 213 | 214 |
39 | {editing ? ( 40 | 50 | {inputNode} 51 | 52 | ) : ( 53 | children 54 | )} 55 |
228 | 229 | ); 230 | }; 231 | 232 | export default ApiurlPrefixEditTable; 233 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/CustomMethodsDrawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-13 11:51:08 4 | * @Description: 自定义方法列表 5 | */ 6 | import { Button, Drawer, DrawerProps, Space, Table, TableColumnsType, Popconfirm, message, Tabs } from 'antd'; 7 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 8 | import state from '@/stores/index'; 9 | import { CustomMethodsItem } from '@/shared/ts/custom'; 10 | import EditCustomMethodDrawer from './EditCustomMethodDrawer'; 11 | import { observer } from 'mobx-react-lite'; 12 | import { codeGenerateMethods } from './code-generate/index'; 13 | 14 | const { TabPane } = Tabs; 15 | 16 | const CustomMethodsDrawer: React.FC = (props) => { 17 | const { ...drawerProps } = props; 18 | const [editCustomMethodsVisible, setEditCustomMethodsVisible] = useState(false); 19 | const [drawerType, setDrawerType] = useState('add'); 20 | const [row, setRow] = useState(undefined); 21 | const dataSource = useMemo(() => { 22 | return state.custom.EnabledCustomMethods; 23 | }, [state.custom.EnabledCustomMethods]); 24 | const rootDataSource = codeGenerateMethods 25 | .filter((v) => v.status) 26 | .map((v) => { 27 | return { 28 | ...v, 29 | function: v.function.toString(), 30 | }; 31 | }); 32 | 33 | const [activeKey, setActiveKey] = useState('custom'); 34 | 35 | const handleConfirmDelete = (r: CustomMethodsItem, index: number) => { 36 | let list = [...dataSource]; 37 | list.splice(index, 1); 38 | state.custom.setCustomMethods(list); 39 | message.success(`删除(${r.key})成功!`); 40 | }; 41 | 42 | const handleEdit = (r: CustomMethodsItem) => { 43 | setEditCustomMethodsVisible(true); 44 | setRow(r); 45 | setDrawerType('EDIT'); 46 | }; 47 | 48 | const handleCopy = (r: CustomMethodsItem) => { 49 | setEditCustomMethodsVisible(true); 50 | setRow({ 51 | ...r, 52 | key: `${r.key}(1)`, 53 | }); 54 | setDrawerType('COPY'); 55 | }; 56 | 57 | const basColumns = [ 58 | { 59 | dataIndex: 'key', 60 | key: 'key', 61 | title: '键值', 62 | width: 120, 63 | }, 64 | { 65 | dataIndex: 'label', 66 | key: 'label', 67 | title: '名称', 68 | width: 120, 69 | }, 70 | { 71 | dataIndex: 'type', 72 | key: 'type', 73 | title: '类型', 74 | width: 120, 75 | }, 76 | { 77 | dataIndex: 'language', 78 | key: 'language', 79 | title: '语言', 80 | width: 100, 81 | }, 82 | { 83 | dataIndex: 'function', 84 | key: 'function', 85 | title: '方法', 86 | ellipsis: true, 87 | }, 88 | ]; 89 | 90 | const columns: TableColumnsType | undefined = [ 91 | ...basColumns, 92 | { 93 | dataIndex: 'sort', 94 | key: 'sort', 95 | title: '排序', 96 | width: 80, 97 | }, 98 | { 99 | dataIndex: 'status', 100 | key: 'status', 101 | title: '状态', 102 | render: (v: number) => { 103 | return v === 1 ? '启用' : '禁用'; 104 | }, 105 | width: 80, 106 | }, 107 | { 108 | dataIndex: 'action', 109 | key: 'action', 110 | title: '操作', 111 | width: 220, 112 | render: (key, r, index) => { 113 | return ( 114 | <> 115 | 123 | 131 | 132 | { 135 | handleConfirmDelete(r, index); 136 | }} 137 | onCancel={() => {}} 138 | okText="是" 139 | cancelText="否" 140 | > 141 | 144 | 145 | 146 | ); 147 | }, 148 | }, 149 | ]; 150 | 151 | const rootColumns: 152 | | TableColumnsType<{ 153 | key: string; 154 | label: string; 155 | type: string; 156 | source: string; 157 | language: string; 158 | status: number; 159 | sort: number; 160 | function: any; 161 | }> 162 | | undefined = [ 163 | ...basColumns, 164 | { 165 | dataIndex: 'action', 166 | key: 'action', 167 | title: '操作', 168 | width: 80, 169 | fixed: 'right', 170 | render: (key, r) => { 171 | return ( 172 | <> 173 | 181 | 182 | ); 183 | }, 184 | }, 185 | ]; 186 | 187 | const handleAdd = () => { 188 | setEditCustomMethodsVisible(true); 189 | setRow(undefined); 190 | setDrawerType('ADD'); 191 | }; 192 | 193 | const tabsChange = (key: string) => { 194 | setActiveKey(key); 195 | }; 196 | 197 | return ( 198 | <> 199 | 208 | 211 | 212 | } 213 | > 214 | 215 | 216 |
217 | 218 | 219 |
226 | 227 | 228 | 229 | { 234 | status && setActiveKey('custom'); 235 | setEditCustomMethodsVisible(false); 236 | }} 237 | /> 238 | 239 | ); 240 | }; 241 | 242 | export default observer(CustomMethodsDrawer); 243 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/code-generate/generate-enum-code.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-27 17:16:27 4 | * @Description: 枚举格式化方法 5 | */ 6 | 7 | import { prettyCode } from '@/shared/utils'; 8 | import { unionBy } from 'lodash'; 9 | 10 | /** 判断是否存在枚举项,返回数字对应枚举类型 11 | * @description: 12 | * @param {any} item 13 | * @return {number} 14 | */ 15 | export function checkIsEnum(item: any): number { 16 | if (item.enum && item.enum.length > 0) { 17 | return 1; 18 | } else if (item && item.description) { 19 | const description = item.description; 20 | if (description.indexOf('ENUM#') !== -1) { 21 | return 2; 22 | } else if (/[是否|排序|状态]/.test(description) && /[0\:|\:0|0\-|0为]/.test(description)) { 23 | return 3; 24 | } 25 | } 26 | return 0; 27 | } 28 | 29 | /** 30 | * @description: 正则格式化键值对 31 | * @param {string} str 32 | * @param {RegExp} reg 33 | * @return {Object} 34 | */ 35 | function getExecObj(str: string, reg: RegExp): Object { 36 | const enumObj: Record = {}; 37 | const matchs = str.match(reg) || []; 38 | 39 | for (let i = 0; i < matchs.length; i++) { 40 | let execItem = reg.exec(str); 41 | if (execItem) { 42 | let leftKey: number = 2; 43 | let rightKey: number = 1; 44 | if ( 45 | (execItem[leftKey] && /.*[\u4e00-\u9fa5]+.*/.test(execItem[leftKey])) || 46 | (execItem[rightKey] && /\d/g.test(execItem[rightKey])) 47 | ) { 48 | // 中文为唯一键或数字为键值时转换 49 | leftKey = 1; 50 | rightKey = 2; 51 | } 52 | enumObj[execItem[leftKey] || i] = execItem[rightKey]; 53 | } 54 | } 55 | return enumObj; 56 | } 57 | 58 | // 获取枚举项,及数据格式化 59 | /** 60 | * @description: 61 | * @param {any} rows 62 | * @return {Array} 63 | */ 64 | export function getEnumCodeData(rows: any[]): any[] { 65 | return rows.reduce((pre, item) => { 66 | const { description } = item; 67 | let enumObj: Record = {}; 68 | switch (checkIsEnum(item)) { 69 | case 1: // enum 70 | // pre.push(item); 71 | case 2: // ENUM 72 | const enumStrReg = /([^ENUM]+)ENUM#(.+)(#)?$/g; 73 | let enumStr: string = ''; 74 | let nameStr: string | null = ''; 75 | 76 | if (/ENUM/g.test(description)) { 77 | const result = enumStrReg.exec(description); 78 | nameStr = result && result[0] && result[0].replace(/[ENUM|#]/g, ''); 79 | enumStr = (result && result[2].replace(/#/g, '')) || ''; 80 | 81 | if (enumStr && /\d:([^:]+)[:!,]([^:,;]+)/g.test(enumStr)) { 82 | // 1:a:A 83 | enumObj = getExecObj(enumStr, /\d:([^:]+):([^:,;]+)/g); 84 | } else if (enumStr && /\d:([^,]+)/.test(enumStr)) { 85 | // 1:a 86 | enumObj = getExecObj(enumStr, /\d:([^,]+)/g); 87 | } 88 | } else if (/^#/g.test(description)) { 89 | // 未标明ENUM 90 | enumStr = description.replace(/#/g, '') || ''; 91 | enumObj = getExecObj(enumStr, /([^::、]+):([^:、]+)/g); 92 | } else { 93 | item.enum.forEach((v: string) => { 94 | enumObj[v] = v; 95 | }); 96 | } 97 | 98 | pre.push({ 99 | ...item, 100 | enumStr: enumStr || description, 101 | nameStr: nameStr || description, // 注释 102 | enum: item.enum || enumObj, 103 | enumDescription: enumObj, 104 | }); 105 | return pre; 106 | case 3: // other 非规范模式 107 | // case 3 用例 108 | // let a = [ 109 | // '是否禁用,0:启用,1:禁用', 110 | // '是否存在有效期, 0:无,1:有', 111 | // '是否启用(1:是 0:否)', 112 | // '是否开启,0-否,1-是', 113 | // '是否已禁用 0=否 1=是', 114 | // '状态,启用:0 停用:1', 115 | // '状态,启用:0 停用:1', 116 | // '任务类型排序, 0为升序、1为降序', 117 | // ]; 118 | 119 | if (/([^::、, ,(]+)[:|:|\-|=|为]([^:、,,) ]+)/g.test(description)) { 120 | enumObj = getExecObj(description, /([^::、, ,(]+)[:|:|\-|=|为]([^:、,,) ]+)/g); 121 | } 122 | pre.push({ 123 | ...item, 124 | enumStr: description, 125 | nameStr: description, // 注释 126 | enum: item.enum || enumObj, 127 | enumDescription: enumObj, 128 | }); 129 | return pre; 130 | 131 | default: 132 | return pre; 133 | } 134 | }, []); 135 | } 136 | 137 | /** 138 | * @description: 单项枚举数据转换为数组 139 | * @param {any} prop 140 | * @return {string} 141 | */ 142 | export function getEnumCode(prop: any): string { 143 | const { name, nameStr } = prop; 144 | return ` 145 | // ${nameStr} 146 | const ${name} = ${JSON.stringify(prop.enum, null)} 147 | `; 148 | } 149 | 150 | /** 151 | * @description: 单项枚举数据转换为options 152 | * @param {any} prop 153 | * @return {string} 154 | */ 155 | export function getEnumCodeToOptions(prop: any): string { 156 | const { name, description, nameStr, enumDescription = {} } = prop; 157 | const options = []; 158 | 159 | for (let key in enumDescription) { 160 | options.push({ value: key, label: enumDescription[key] }); 161 | } 162 | 163 | return ` 164 | // ${nameStr} 165 | const ${name}Options = ${JSON.stringify(options, null)} 166 | `; 167 | } 168 | 169 | /** 170 | * @description: 枚举数组格式化为array字符串 171 | * @param {any} rows 172 | * @return {string} 173 | */ 174 | export function handleEnumCodeDataToArray(rows: any): string { 175 | return rows 176 | .map( 177 | (row: any) => ` 178 | ${getEnumCode(row)} 179 | `, 180 | ) 181 | .join(''); 182 | } 183 | 184 | /** 185 | * @description: 枚举数组格式化为options字符串 186 | * @param {any} rows 187 | * @return {string} 188 | */ 189 | export function handleEnumCodeDataToOptions(rows: any): string { 190 | return rows 191 | .map( 192 | (row: any) => ` 193 | ${getEnumCodeToOptions(row)} 194 | `, 195 | ) 196 | .join(''); 197 | } 198 | 199 | // 收集枚举项并格式化字符串显示 200 | /** 201 | * @description: 202 | * @param {any} selectedData 203 | * @return {string} 204 | */ 205 | export function generateEnumCode( 206 | selectedData: 207 | | { 208 | requestSelectedData: any[]; 209 | responseSelectedData: any[]; 210 | } 211 | | any[], 212 | ): string { 213 | let rows = []; 214 | if (Array.isArray(selectedData)) { 215 | rows = getEnumCodeData(selectedData); 216 | } else { 217 | const { requestSelectedData, responseSelectedData } = selectedData; 218 | 219 | let resData = []; 220 | if (responseSelectedData && responseSelectedData.length) { 221 | resData = responseSelectedData.find((item: { name: string }) => item.name === 'data')?.children ?? []; 222 | } 223 | if (requestSelectedData && requestSelectedData.length) { 224 | resData = [ 225 | ...resData, 226 | ...(requestSelectedData.find((item: { name: string }) => item.name === 'data')?.children ?? []), 227 | ...(requestSelectedData[0]?.children || []), 228 | ]; 229 | } 230 | 231 | function recursionReduce(list: any[]) { 232 | list.forEach((item: { children: Record[] }) => { 233 | if (item && item.children && item.children.length) { 234 | recursionReduce(item.children); 235 | } 236 | }); 237 | } 238 | recursionReduce(resData || []); 239 | rows = getEnumCodeData(resData); 240 | // 去掉重复的枚举 241 | rows = unionBy(rows, 'name'); 242 | } 243 | 244 | return prettyCode( 245 | ` 246 | ${handleEnumCodeDataToArray(rows)} 247 | ${handleEnumCodeDataToOptions(rows)}`, 248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/EditCustomMethodDrawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-13 11:51:08 4 | * @Description: 新增、编辑自定义方法抽屉 5 | */ 6 | import { Button, Drawer, DrawerProps, message, Form, Input, InputNumber, Select, Row, Col } from 'antd'; 7 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 8 | import state from '@/stores/index'; 9 | import { CustomMethodsItem } from '@/shared/ts/custom'; 10 | import CodeMirror from '@uiw/react-codemirror'; 11 | import { javascript } from '@codemirror/lang-javascript'; 12 | import { MethodTypes } from '@/shared/common'; 13 | import { getStringToFn } from '@/shared/utils'; 14 | import { codeGenerateMethods } from './code-generate/index'; 15 | import { CodeMirrorTypes } from '@/shared/common'; 16 | const { Option } = Select; 17 | 18 | const EditCustomMethodDrawer: React.FC< 19 | { data: CustomMethodsItem | undefined; type: string; onClose: (event?: boolean) => void } & DrawerProps 20 | > = (props) => { 21 | const { data, type, onClose, ...drawerProps } = props; 22 | const [form] = Form.useForm(); 23 | const rootDataSource = codeGenerateMethods 24 | .filter((v) => v.status) 25 | .map((v) => { 26 | return { 27 | ...v, 28 | function: v.function.toString(), 29 | }; 30 | }); 31 | const methodList = useMemo(() => { 32 | return [...state.custom.EnabledCustomMethods, ...rootDataSource]; 33 | }, [state.custom.EnabledCustomMethods]); 34 | 35 | const [currentState, setCurrentState] = useState({ 36 | title: '自定义方法', 37 | type: type, 38 | }); 39 | 40 | useEffect(() => { 41 | if (drawerProps.visible) { 42 | setCurrentState({ 43 | title: `${data ? '编辑' : '新增'}自定义方法`, 44 | type: type, 45 | }); 46 | if (data) { 47 | form.setFieldsValue({ ...data }); 48 | } 49 | } 50 | }, [drawerProps.visible, type]); 51 | 52 | const editorHeight = useMemo(() => { 53 | const height = window.innerHeight - 400; 54 | return height < 300 ? 300 : height; 55 | }, [window.innerHeight]); 56 | 57 | const validateKey = (rule: any, value: any, callback: any) => { 58 | if (value && type !== 'EDIT') { 59 | const index = methodList.findIndex((v: CustomMethodsItem) => { 60 | return value === v.key; 61 | }); 62 | if (index !== -1) { 63 | callback(new Error('键值已被使用,请修改')); 64 | } else { 65 | callback(); 66 | } 67 | } else { 68 | callback(); 69 | } 70 | }; 71 | 72 | const filterFunction = (fn: any) => { 73 | try { 74 | return Object.prototype.toString.call(getStringToFn(fn)) === '[object Function]'; 75 | } catch (error) { 76 | return false; 77 | } 78 | }; 79 | 80 | const submit = useCallback( 81 | (values: any) => { 82 | if (!filterFunction(values.function)) { 83 | message.error('方法-校验是否函数未通过!'); 84 | return false; 85 | } 86 | const list = [...state.custom.EnabledCustomMethods]; 87 | let text = ''; 88 | const index = list.findIndex((v: CustomMethodsItem) => { 89 | return values.key === v.key; 90 | }); 91 | if (currentState.type === 'ADD' || currentState.type === 'COPY') { 92 | text = '新增'; 93 | list.push({ ...values, source: 'custom' }); 94 | } else if (currentState.type === 'EDIT') { 95 | text = '编辑'; 96 | list[index] = { ...values, source: 'custom' }; 97 | } 98 | state.custom.setCustomMethods(list.sort((a, b) => b.sort - a.sort)); 99 | clearFormData(); 100 | onClose(true); 101 | message.success(`${text}成功!`); 102 | }, 103 | [currentState], 104 | ); 105 | 106 | const clearFormData = () => { 107 | form.setFieldsValue({ 108 | key: '', 109 | label: '', 110 | type: '', 111 | function: '', 112 | description: '', 113 | }); 114 | }; 115 | 116 | const itemCol = { 117 | labelCol: { span: 4 }, 118 | wrapperCol: { span: 20 }, 119 | }; 120 | 121 | return ( 122 | { 131 | onClose(false); 132 | clearFormData(); 133 | }} 134 | > 135 |
{}} 143 | autoComplete="off" 144 | > 145 | 146 |
147 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 174 | 175 | 176 | 177 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 222 | 223 | 224 | 225 | ); 226 | }; 227 | 228 | export default EditCustomMethodDrawer; 229 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/ResourcesTree.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-07-04 15:39:44 4 | * @Description: 侧边菜单 5 | */ 6 | import { Col, Select, Menu, MenuProps, Input, Row, Spin, Button, Badge } from 'antd'; 7 | import { DownloadOutlined, ReloadOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons'; 8 | import React, { useCallback, useEffect, useState } from 'react'; 9 | import { useMemo } from 'react'; 10 | import { useModel } from 'umi'; 11 | import { pathsItem, tagsItem } from '@/shared/ts/api-interface'; 12 | import { MethodColors } from '@/shared/common'; 13 | import { dataSaveToJSON } from '@/shared/utils'; 14 | import ApiDefinitionDropdown from './ApiDefinitionDropdown'; 15 | import state from '@/stores/index'; 16 | import { observer } from 'mobx-react'; 17 | 18 | const { Search } = Input; 19 | 20 | type MenuItem = Required['items'][number]; 21 | 22 | function getItem( 23 | label: React.ReactNode, 24 | key: React.Key, 25 | children?: MenuItem[], 26 | icon?: React.ReactNode, 27 | type?: 'group', 28 | ): MenuItem { 29 | return { 30 | key, 31 | label, 32 | children, 33 | icon, 34 | type, 35 | } as MenuItem; 36 | } 37 | 38 | const ResourcesTree: React.FC<{ labelKey: string } & MenuProps> = ({ labelKey }) => { 39 | const { 40 | searchTags, 41 | setSearchTags, 42 | selectedResourceIndex, 43 | setSelectedResourceIndex, 44 | selectedResource, 45 | fetchSelectedResource, 46 | resources, 47 | resourceDetail, 48 | resourceDetailLoading, 49 | type, 50 | selectedApi, 51 | setItemSelectedApi, 52 | collapsed, 53 | setCollapsed, 54 | } = useModel('useApiSwitchModel'); 55 | 56 | const { urlValue } = state.swagger; 57 | 58 | // 当前显示tags 59 | const currentResourceTags: tagsItem[] = useMemo(() => { 60 | return searchTags ? searchTags : resourceDetail?.tags ?? []; 61 | }, [resourceDetail, searchTags]); 62 | 63 | // resourceDetail对应tag[paths] Map 64 | const resourceTagsMap: Map = useMemo(() => { 65 | let hasMap = new Map(); 66 | (resourceDetail?.tags ?? []).forEach((tag: tagsItem) => { 67 | tag.paths.forEach((v: pathsItem) => { 68 | hasMap.set(v.uuid, v); 69 | }); 70 | }); 71 | return hasMap; 72 | }, [resourceDetail]); 73 | 74 | /** 75 | * @description: 接口搜索 76 | * @param {string} val 77 | * @return {*} 78 | */ 79 | const onSearch = (val: string) => { 80 | if (val === '') { 81 | setSearchTags(null); 82 | } else { 83 | const list: tagsItem[] = []; 84 | (resourceDetail?.tags ?? []).forEach((tag: tagsItem) => { 85 | const reg = new RegExp(val, 'i'); 86 | const pathList = tag.paths.filter((v: pathsItem) => { 87 | return reg.test(v.api) || reg.test(v.summary); 88 | }); 89 | if (pathList.length > 0) { 90 | list.push({ 91 | ...tag, 92 | paths: pathList, 93 | }); 94 | } 95 | }); 96 | setSearchTags(list); 97 | } 98 | }; 99 | 100 | /** 101 | * @description: 列表、分页 显示过滤 102 | * @param {string} menuName 103 | * @return {HTMLElement} 104 | */ 105 | const highlightMenuName: any = useCallback((menuName = '', labelKey = 'summary') => { 106 | const flag = /列表|分页/.test(menuName); 107 | if (!flag) { 108 | return menuName; 109 | } 110 | if (labelKey === 'summary') { 111 | const matchResult = menuName.match(/列表|分页/); 112 | if (!matchResult) { 113 | return menuName; 114 | } 115 | const arr = [menuName.substring(0, matchResult.index), menuName.substring((matchResult.index as number) + 2)]; 116 | 117 | return ( 118 | <> 119 | {arr[0]} 120 | {matchResult[0]} 121 | {arr[1]} 122 | 123 | ); 124 | } 125 | return menuName; 126 | }, []); 127 | 128 | /** 129 | * @description: menuItem点击回调 130 | * @param {item, key, keyPath, domEvent} 131 | */ 132 | const onItemSelect = ({ key }: any) => { 133 | const current = resourceTagsMap.get(key); 134 | current && setItemSelectedApi(current); 135 | }; 136 | 137 | // Menu Items 138 | const menuItems: MenuItem[] = useMemo(() => { 139 | return currentResourceTags.map((item: tagsItem, idx: number) => { 140 | let { name, paths = [] } = item; 141 | const nameLabel = ( 142 | 143 |
144 | {name} 145 |
146 | 147 | {paths && paths.length ? : null} 148 |
149 | ); 150 | return getItem( 151 | nameLabel, 152 | name, 153 | paths.map((row: pathsItem) => { 154 | return getItem( 155 | 156 | {row.methodUpper} 157 | {highlightMenuName(row.summary, labelKey)} 158 | , 159 | row.uuid, 160 | ); 161 | }), 162 | ); 163 | }); 164 | }, [currentResourceTags]); 165 | 166 | const defaultSelectedKeys = useMemo(() => { 167 | return selectedApi?.uuid ? [selectedApi?.uuid] : []; 168 | }, [selectedApi]); 169 | 170 | if (!resources || (type === 'api' && !resources.length)) { 171 | return null; 172 | } else { 173 | return ( 174 |
175 | 176 | 177 | {!collapsed && ( 178 |
179 | 180 |
181 | )} 182 | 193 |
194 | {type === 'api' && ( 195 | 196 | {!collapsed && ( 197 | 204 | )} 205 | 216 | 217 | )} 218 |
219 | {resourceDetailLoading ? ( 220 | 221 | 222 | 223 | ) : ( 224 | 232 | )} 233 | 234 |
setCollapsed(!collapsed)} 238 | > 239 | {collapsed ? : } 240 |
241 | 242 | ); 243 | } 244 | }; 245 | 246 | export default observer(ResourcesTree); 247 | -------------------------------------------------------------------------------- /src/models/useApiSwitchModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZtrainWilliams ztrain1224@163.com 3 | * @Date: 2022-06-14 17:11:40 4 | * @Description: 5 | */ 6 | import { requestToBody } from '@/shared/fetch/requestToBody'; 7 | import { formatUrlChar } from '@/shared/utils'; 8 | import { useRequest } from 'ahooks'; 9 | import { isArray, uniq } from 'lodash'; 10 | import { useCallback, useEffect, useMemo, useState } from 'react'; 11 | import storage from '@/shared/storage'; 12 | import { postVSCodeMessage } from '@/shared/vscode'; 13 | import state from '@/stores/index'; 14 | import { message } from 'antd'; 15 | import { resourceItems, pathsItem, tagsItem } from '@/shared/ts/api-interface'; 16 | import { TransformSate } from '@/shared/ts/settings'; 17 | import { classifyPathsToTags } from '@/shared/utils'; 18 | const yaml = require('js-yaml'); 19 | 20 | export default function useApiSwitchModel() { 21 | const [collapsed, setCollapsed] = useState(false); // 左侧菜单选项收起 22 | const [searchTextFixed, setSearchTextFixed] = useState(false); // 搜索text框显示 23 | const [apiSearchText, setapiSearchText] = useState(''); // api详情搜索文本 24 | // 搜索显示tags 25 | const [searchTags, setSearchTags] = useState(null); 26 | 27 | // 类型 api | json 28 | const [type, setType] = useState('api'); 29 | 30 | // 通过当前资源地址获取资源 31 | const { run: fetchResources, data: resources, loading: resourcesLoading } = useRequest( 32 | async () => { 33 | // 重置选择 34 | setSelectedApiRows([]); 35 | selectedApiMaps.clear(); 36 | setSelectedApi(null); 37 | setSelectedResourceIndex(''); 38 | 39 | let swaggerUrl = state.swagger.urlValue; 40 | let urlType = type; 41 | if (/.json$/.test(swaggerUrl)) { 42 | // https://petstore.swagger.io/v2/swagger.json 43 | urlType = 'json'; 44 | } else if (/.yaml$/.test(swaggerUrl)) { 45 | urlType = 'yaml'; 46 | } else { 47 | urlType = 'api'; 48 | swaggerUrl = formatUrlChar(state.swagger.urlValue) + '/swagger-resources'; 49 | } 50 | 51 | let res = await requestToBody(swaggerUrl); 52 | if (typeof res === 'string') { 53 | console.log('res', res); 54 | return []; 55 | } 56 | if (res) { 57 | handleStorageUrl(); 58 | if (['json', 'yaml'].includes(urlType)) { 59 | if (urlType === 'yaml') { 60 | res = yaml.load(res); // yaml文件格式解析成json形式 61 | } 62 | classifyPathsToTags(res.tags, res.paths); 63 | } else if (urlType === 'api') { 64 | // url成功,重置选中的key,兼容处理刷新 65 | if (isArray(res) && res.length > 0) { 66 | setSelectedResourceIndex(res[0].location || res[0].url); 67 | } 68 | } 69 | } else { 70 | message.error('获取swagger-resources失败!'); 71 | } 72 | setType(urlType); 73 | return type === 'api' ? res || [] : res; 74 | }, 75 | { 76 | manual: true, 77 | }, 78 | ); 79 | 80 | /** 81 | * 处理url结合历史url存储到storage 82 | */ 83 | const handleStorageUrl = () => { 84 | const current = formatUrlChar(state.swagger.urlValue); 85 | const storageUrls: any[] = storage.get('storageUrls'); 86 | let newStorageUrls: any[] = [current]; 87 | if (storageUrls) { 88 | const item = storageUrls.find((v: string) => v === current); 89 | newStorageUrls = (item ? uniq([current, ...storageUrls]) : [current, ...storageUrls]).slice(0, 10); 90 | } 91 | 92 | postVSCodeMessage('pushStorage', { 93 | key: 'storage', 94 | data: { ['storageUrls']: newStorageUrls }, 95 | }); 96 | state.swagger.setApiUrls(newStorageUrls); 97 | }; 98 | 99 | // 当前选择的资源key 100 | const [selectedResourceIndex, setSelectedResourceIndex] = useState(''); 101 | const resourcesMap: Map = useMemo(() => { 102 | return Array.isArray(resources) && resources?.length 103 | ? resources?.reduce((p: Map, item: resourceItems) => { 104 | p.set(item.location || item.url, item); 105 | return p; 106 | }, new Map()) 107 | : new Map(); 108 | }, [resources]); 109 | const selectedResource: resourceItems = useMemo(() => { 110 | return resourcesMap.get(selectedResourceIndex) as resourceItems; 111 | }, [selectedResourceIndex, resourcesMap]); 112 | 113 | // 当前选中的资源 Key 获取详情 114 | const { data: requestResourceDetail, loading: resourceDetailLoading, run: fetchSelectedResource } = useRequest( 115 | async () => { 116 | if (selectedResourceIndex) { 117 | const formatUrl = formatUrlChar(state.swagger.urlValue); 118 | const res = await requestToBody( 119 | formatUrl + (selectedResource.location || selectedResource.url), 120 | 'GET', 121 | selectedResource.header, 122 | ); 123 | classifyPathsToTags(res.tags ?? [], res.paths ?? []); 124 | return res; 125 | } 126 | return null; 127 | }, 128 | { 129 | refreshDeps: [selectedResource, state.swagger.urlValue], 130 | }, 131 | ); 132 | 133 | const resourceDetail = useMemo(() => { 134 | if (['json', 'yaml'].includes(type)) { 135 | return resources; 136 | } else { 137 | return selectedResourceIndex ? requestResourceDetail : null; 138 | } 139 | }, [requestResourceDetail, type, resources]); 140 | 141 | // 已选中接口的列表 142 | const [selectedApiRows, setSelectedApiRows] = useState([]); 143 | const [selectedApiMaps] = useState>(new Map()); 144 | 145 | // 选中的接口 146 | const [selectedApi, setSelectedApi] = useState(null); 147 | const setItemSelectedApi = useCallback( 148 | (row: pathsItem) => { 149 | const list: pathsItem[] = selectedApiRows; 150 | if (!selectedApiMaps.get(row.uuid)) { 151 | if (list.length >= 15) { 152 | const firstItem: pathsItem = list[0]; 153 | list.splice(0, 1); 154 | selectedApiMaps.delete(firstItem.uuid); 155 | } 156 | list.push(row); 157 | setSelectedApiRows(list); 158 | } 159 | selectedApiMaps.set(row.uuid, row); 160 | setSelectedApi(row); 161 | }, 162 | [selectedApiMaps, selectedApiRows], 163 | ); 164 | 165 | // 选择的模型 166 | const [selectedDefinition, setSelectedDefinition] = useState(); 167 | // 模型编码设置 168 | const [definitionCodeDrawerProps, originSetDefinitionCodeDrawerProps] = useState({}); 169 | const setDefinitionCodeDrawerProps = useCallback( 170 | (props) => { 171 | originSetDefinitionCodeDrawerProps({ 172 | ...props, 173 | onClose: () => { 174 | originSetDefinitionCodeDrawerProps((prev: any) => ({ 175 | ...prev, 176 | visible: false, 177 | })); 178 | }, 179 | }); 180 | }, 181 | [originSetDefinitionCodeDrawerProps], 182 | ); 183 | 184 | // apiurl默认前缀 185 | const apiurlPrefix = useMemo(() => { 186 | const urlValue = state.swagger.urlValue; 187 | const { apiurlPrefixList } = state.settings.Settings; 188 | return apiurlPrefixList.find((v) => v.status && v.url === urlValue)?.prefix || ''; 189 | }, [state.swagger.urlValue, state.settings.Settings, resources]); 190 | 191 | // 文本转换配置 192 | const [transformSate, setTransformSate] = useState({ 193 | status: true, // 代码转换关联转换文本 194 | textRecord: [], // 最后文本记录 195 | historyArray: [], // 历史文本转换记录 196 | baseCode: null, 197 | isTranslate: true, 198 | }); 199 | 200 | return { 201 | collapsed, 202 | setCollapsed, 203 | searchTextFixed, 204 | setSearchTextFixed, 205 | apiSearchText, 206 | setapiSearchText, 207 | searchTags, 208 | setSearchTags, 209 | type, 210 | setType, 211 | selectedResourceIndex, 212 | setSelectedResourceIndex, 213 | selectedResource, 214 | fetchSelectedResource, 215 | fetchResources, 216 | resources, 217 | resourcesLoading, 218 | resourceDetail, 219 | resourceDetailLoading, 220 | selectedApi, 221 | setSelectedApi, 222 | selectedApiRows, 223 | setSelectedApiRows, 224 | selectedApiMaps, 225 | setItemSelectedApi, 226 | selectedDefinition, 227 | setSelectedDefinition, 228 | definitionCodeDrawerProps, 229 | setDefinitionCodeDrawerProps, 230 | apiurlPrefix, 231 | transformSate, 232 | setTransformSate, 233 | }; 234 | } 235 | -------------------------------------------------------------------------------- /src/pages/swagger-codegen/EditableTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect, useRef } from 'react'; 2 | import { Form, Input, Row, Card, Button, InputNumber, Select, TreeSelect, message, Popconfirm, Table } from 'antd'; 3 | const { Option } = Select; 4 | 5 | import { FormInstance } from 'antd/lib/form'; 6 | import { TableRowSelection } from '@ant-design/pro-table/lib/typing'; 7 | 8 | const EditableContext = React.createContext | null>(null); 9 | 10 | interface Item { 11 | key: string; 12 | name: string; 13 | componentType: string; 14 | placeholder: string; 15 | } 16 | 17 | interface EditableRowProps { 18 | index: number; 19 | } 20 | 21 | const EditableRow: React.FC = ({ index, ...props }) => { 22 | const [form] = Form.useForm(); 23 | return ( 24 |
25 | 26 |
27 | 28 | 29 | ); 30 | }; 31 | 32 | interface EditableCellProps { 33 | title: React.ReactNode; 34 | editable: boolean; 35 | type: string; 36 | children: React.ReactNode; 37 | dataIndex: keyof Item; 38 | record: Item; 39 | handleSave: (record: Item) => void; 40 | } 41 | 42 | const EditableCell: React.FC = ({ 43 | title, 44 | editable, 45 | type, 46 | children, 47 | dataIndex, 48 | record, 49 | handleSave, 50 | ...restProps 51 | }) => { 52 | const [editing, setEditing] = useState(false); 53 | const inputRef = useRef(null); 54 | const form = useContext(EditableContext)!; 55 | const componentList = [ 56 | { 57 | value: 'Input', 58 | name: '输入框', 59 | }, 60 | { 61 | value: 'Select', 62 | name: '下拉框', 63 | }, 64 | { 65 | value: 'Switch', 66 | name: '开关', 67 | }, 68 | { 69 | value: 'InputNumber', 70 | name: '数字输入框', 71 | }, 72 | ]; 73 | 74 | useEffect(() => { 75 | if (editing) { 76 | inputRef.current!.focus(); 77 | } 78 | }, [editing]); 79 | 80 | const toggleEdit = () => { 81 | setEditing(!editing); 82 | form.setFieldsValue({ [dataIndex]: record[dataIndex] }); 83 | }; 84 | 85 | const save = async () => { 86 | try { 87 | const values = await form.validateFields(); 88 | 89 | toggleEdit(); 90 | handleSave({ ...record, ...values }); 91 | } catch (errInfo) { 92 | console.log('Save failed:', errInfo); 93 | } 94 | }; 95 | 96 | let childNode = children; 97 | let selectChildrenNode: any = []; 98 | if (type === 'componentSelect') { 99 | componentList.forEach((item: { value: string; name: string }) => { 100 | selectChildrenNode.push( 101 | , 104 | ); 105 | }); 106 | } 107 | 108 | if (editable) { 109 | childNode = editing ? ( 110 | 120 | {type === 'componentSelect' ? ( 121 | 132 | ) : ( 133 | 134 | )} 135 | 136 | ) : ( 137 |
138 | {children} 139 |
140 | ); 141 | } 142 | 143 | return
; 144 | }; 145 | 146 | type EditableTableProps = Parameters[0] & { 147 | definitions?: any; 148 | requestParameters?: []; 149 | responseParameters?: {}; 150 | }; 151 | 152 | interface DataType { 153 | key: React.Key; 154 | name: string; 155 | componentType: string; 156 | placeholder: string; 157 | type: string; 158 | format: string; 159 | description: string; 160 | } 161 | 162 | interface EditableTableState { 163 | dataSource: DataType[]; 164 | selectedRows: DataType[]; 165 | count: number; 166 | } 167 | 168 | type ColumnTypes = Exclude; 169 | 170 | class EditableTable extends React.Component { 171 | columns: (ColumnTypes[number] & { 172 | editable?: boolean; 173 | type: string; 174 | dataIndex: string; 175 | })[]; 176 | 177 | constructor(props: EditableTableProps) { 178 | super(props); 179 | 180 | this.columns = [ 181 | { 182 | title: '名称', 183 | dataIndex: 'name', 184 | width: '30%', 185 | editable: true, 186 | type: 'input', 187 | }, 188 | { 189 | title: '组件类型', 190 | dataIndex: 'componentType', 191 | editable: true, 192 | type: 'componentSelect', 193 | }, 194 | { 195 | title: '提示语', 196 | dataIndex: 'placeholder', 197 | editable: true, 198 | type: 'input', 199 | }, 200 | // { 201 | // title: 'operation', 202 | // dataIndex: 'operation', 203 | // render: (_, record: { key: React.Key }) => 204 | // this.state.dataSource.length >= 1 ? ( 205 | // this.handleDelete(record.key)}> 206 | // Delete 207 | // 208 | // ) : null, 209 | // }, 210 | ]; 211 | 212 | this.state = { 213 | dataSource: [], 214 | selectedRows: [], 215 | count: 2, 216 | }; 217 | } 218 | 219 | componentWillReceiveProps() { 220 | this.handleData(); 221 | } 222 | 223 | handleDelete = (key: React.Key) => { 224 | const dataSource = [...this.state.dataSource]; 225 | this.setState({ 226 | dataSource: dataSource.filter((item) => item.key !== key), 227 | }); 228 | }; 229 | 230 | handleData = () => { 231 | let requestParameters: any = this.props.requestParameters; 232 | this.setState({ 233 | dataSource: requestParameters, 234 | selectedRows: requestParameters.filter((m: { require: boolean }) => { 235 | return m.require; 236 | }), 237 | }); 238 | }; 239 | 240 | handleSave = (row: DataType) => { 241 | const newData = [...this.state.dataSource]; 242 | const index = newData.findIndex((item) => row.key === item.key); 243 | const item = newData[index]; 244 | newData.splice(index, 1, { 245 | ...item, 246 | ...row, 247 | }); 248 | this.setState({ dataSource: newData }); 249 | }; 250 | 251 | render() { 252 | const { dataSource } = this.state; 253 | 254 | const components = { 255 | body: { 256 | row: EditableRow, 257 | cell: EditableCell, 258 | }, 259 | }; 260 | 261 | const columns = this.columns.map((col) => { 262 | if (!col.editable) { 263 | return col; 264 | } 265 | return { 266 | ...col, 267 | onCell: (record: DataType) => ({ 268 | record, 269 | editable: col.editable, 270 | type: col.type, 271 | dataIndex: col.dataIndex, 272 | title: col.title, 273 | handleSave: this.handleSave, 274 | }), 275 | }; 276 | }); 277 | 278 | const rowSelection = { 279 | onChange: (selectedRowKeys: React.Key[], selectedRows = []) => { 280 | this.setState({ selectedRows }); 281 | }, 282 | }; 283 | 284 | return ( 285 |
286 | {/* */} 289 |
{childNode}
'editable-row'} 292 | bordered 293 | dataSource={dataSource} 294 | columns={columns as ColumnTypes} 295 | rowSelection={rowSelection as TableRowSelection} 296 | style={{ width: '100%' }} 297 | pagination={false} 298 | /> 299 | 300 | ); 301 | } 302 | } 303 | 304 | export default EditableTable; 305 | --------------------------------------------------------------------------------