├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg ├── post-merge ├── pre-commit └── pre-push ├── README.md ├── __tests__ └── cli.spec.ts ├── build.config.ts ├── index.js ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts ├── template-react-ts ├── .env.production ├── .eslintrc.cjs ├── .husky │ ├── commit-msg │ ├── post-merge │ ├── pre-commit │ └── pre-push ├── Dockerfile ├── _gitignore ├── _npmrc ├── index.html ├── makefile ├── package.json ├── public │ └── vite.svg ├── readme.md ├── src │ ├── assets │ │ ├── images │ │ │ ├── logo.svg │ │ │ └── logo_mini.svg │ │ └── react.svg │ ├── config.ts │ ├── core │ │ ├── error-boundary │ │ │ └── index.tsx │ │ ├── http │ │ │ ├── TanStackQuery.tsx │ │ │ ├── config.ts │ │ │ └── request.ts │ │ ├── openapi │ │ │ └── index.ts │ │ ├── router │ │ │ ├── CreateBrowserRouter.tsx │ │ │ └── LazyImportComponent.tsx │ │ ├── store │ │ │ ├── index.ts │ │ │ └── loggerMiddleware.ts │ │ └── style │ │ │ ├── defaultStyleConfig.ts │ │ │ └── utils.ts │ ├── index.css │ ├── index.tsx │ ├── login │ │ ├── config.ts │ │ ├── index.tsx │ │ └── routes.tsx │ ├── mainLayout │ │ ├── MainLayoutComp.tsx │ │ ├── index.tsx │ │ ├── routes.tsx │ │ └── utils.tsx │ ├── pages │ │ ├── dashboard │ │ │ ├── index.tsx │ │ │ └── routes.tsx │ │ ├── routes.tsx │ │ ├── ui-list │ │ │ ├── UIOne.tsx │ │ │ └── routes.tsx │ │ └── util-list │ │ │ ├── RequestDemo.tsx │ │ │ ├── RouterQueryDemo.tsx │ │ │ └── routes.tsx │ ├── rootRoutes.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── template-vue-ts ├── .env.production ├── .eslintrc.cjs ├── .husky │ ├── commit-msg │ ├── post-merge │ ├── pre-commit │ └── pre-push ├── .vscode │ └── extensions.json ├── Dockerfile ├── README.md ├── _gitignore ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── makefile ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── api │ │ ├── hello.ts │ │ ├── index.ts │ │ ├── typings.d.ts │ │ └── user.ts │ ├── assets │ │ └── vue.svg │ ├── components │ │ └── HelloWorld.vue │ ├── config.ts │ ├── core │ │ ├── http │ │ │ ├── config.ts │ │ │ ├── request.ts │ │ │ └── vueQueryConfig.ts │ │ ├── openapi │ │ │ └── index.ts │ │ ├── router │ │ │ └── index.ts │ │ └── store │ │ │ ├── index.ts │ │ │ ├── project.ts │ │ │ └── user.ts │ ├── layouts │ │ ├── MainLayout.vue │ │ └── routes.ts │ ├── login │ │ ├── index.vue │ │ └── routes.ts │ ├── main.ts │ ├── routes.ts │ ├── style.css │ ├── views │ │ ├── dashboard │ │ │ └── index.vue │ │ ├── routes.ts │ │ └── ui-list │ │ │ ├── UIOne.vue │ │ │ └── UITwo.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | build: 8 | # 指定操作系统 9 | runs-on: ubuntu-latest 10 | steps: 11 | # 将代码拉到虚拟机 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | # 指定node版本 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '16.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | # 依赖缓存策略 21 | - name: Cache 22 | id: cache-dependencies 23 | uses: actions/cache@v3 24 | with: 25 | path: | 26 | **/node_modules 27 | key: ${{runner.OS}}-${{hashFiles('**/pnpm-lock.yaml')}} 28 | - name: Install pnpm 29 | run: npm install -g pnpm@7.5.0 30 | # 依赖下载 31 | - name: Installing Dependencies 32 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 33 | run: pnpm install 34 | # 打包 35 | - name: Running Build 36 | run: pnpm run build 37 | # 测试 38 | - name: Running Test 39 | run: pnpm run test-unit 40 | # 发布 41 | - name: Running Publish 42 | run: npm publish 43 | env: 44 | # NPM_TOKEN is access token 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | package-lock.json 27 | yarn.lock 28 | .tmp 29 | .husky/_ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm install 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged --pattern '**/*.*(ts|tsx|js|jsx|vue|json|html)' 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # pnpm test 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-vite-app-cli 2 | a fast create webapp template cli, perfect engineering development experience, currently supported template presets include: 3 | - `react-ts` (React + TypeScript + Vite + Pnpm + Zustand + Openapi + Docker) 4 | - `vue-ts` (Vue 3 + TypeScript + Vite + Pnpm + Pinia + Openapi + Docker) 5 | 6 | ## Quick Start 7 | 8 | > **Compatibility Note:** 9 | > Vite requires [Node.js](https://nodejs.org/en/) version 14.18+, 16+. However, some templates require a higher Node.js version to work, please upgrade if your package manager warns about it. 10 | 11 | With Run: 12 | 13 | ```bash 14 | # npm 15 | $ npm create vite-app-cli@latest 16 | 17 | # pnpm 18 | $ pnpm create vite-app-cli@latest 19 | ``` 20 | 21 | Then follow the prompts! 22 | 23 | You can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a Vite + React project, run: 24 | 25 | ```bash 26 | # npm 27 | npm create vite-app-cli@latest my-react-app --template react-ts 28 | 29 | # pnpm 30 | pnpm create vite-app-cli@latest my-react-app --template react-ts 31 | ``` 32 | 33 | template presets detail: 34 | - `react-ts` [点此去](https://github.com/rookie-luochao/create-vite-app-cli/tree/master/template-react-ts) 35 | - `vue-ts` [点此去](https://github.com/rookie-luochao/create-vite-app-cli/tree/master/template-vue-ts) 36 | 37 | ## Contribute Code 38 | - `react-ts` [跳转](https://github.com/rookie-luochao/create-vite-react-app) 39 | - `vue-ts` [跳转](https://github.com/rookie-luochao/create-vite-vue-app) -------------------------------------------------------------------------------- /__tests__/cli.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import type { ExecaSyncReturnValue, SyncOptions } from 'execa' 3 | import { execaCommandSync } from 'execa' 4 | import fs from 'fs-extra' 5 | import { afterEach, beforeAll, expect, test } from 'vitest' 6 | 7 | const CLI_PATH = join(__dirname, '..') 8 | 9 | const projectName = 'test-app' 10 | const genPath = join(__dirname, projectName) 11 | 12 | const run = ( 13 | args: string[], 14 | options: SyncOptions = {}, 15 | ): ExecaSyncReturnValue => { 16 | return execaCommandSync(`node ${CLI_PATH} ${args.join(' ')}`, options) 17 | } 18 | 19 | // Helper to create a non-empty directory 20 | const createNonEmptyDir = () => { 21 | // Create the temporary directory 22 | fs.mkdirpSync(genPath) 23 | 24 | // Create a package.json file 25 | const pkgJson = join(genPath, 'package.json') 26 | fs.writeFileSync(pkgJson, '{ "foo": "bar" }') 27 | } 28 | 29 | // Vue 3 starter template 30 | const templateFiles = fs 31 | .readdirSync(join(CLI_PATH, 'template-vue-ts')) 32 | // _gitignore is renamed to .gitignore 33 | .map((filePath) => (filePath === '_gitignore' ? '.gitignore' : filePath)) 34 | .sort() 35 | 36 | beforeAll(() => fs.remove(genPath)) 37 | afterEach(() => fs.remove(genPath)) 38 | 39 | test('prompts for the project name if none supplied', () => { 40 | const { stdout } = run([]) 41 | expect(stdout).toContain('Project name:') 42 | }) 43 | 44 | test('prompts for the framework if none supplied when target dir is current directory', () => { 45 | fs.mkdirpSync(genPath) 46 | const { stdout } = run(['.'], { cwd: genPath }) 47 | expect(stdout).toContain('Select a framework:') 48 | }) 49 | 50 | test('prompts for the framework if none supplied', () => { 51 | const { stdout } = run([projectName]) 52 | expect(stdout).toContain('Select a framework:') 53 | }) 54 | 55 | test('prompts for the framework on not supplying a value for --template', () => { 56 | const { stdout } = run([projectName, '--template']) 57 | expect(stdout).toContain('Select a framework:') 58 | }) 59 | 60 | test('prompts for the framework on supplying an invalid template', () => { 61 | const { stdout } = run([projectName, '--template', 'unknown']) 62 | expect(stdout).toContain( 63 | `"unknown" isn't a valid template. Please choose from below:`, 64 | ) 65 | }) 66 | 67 | test('asks to overwrite non-empty target directory', () => { 68 | createNonEmptyDir() 69 | const { stdout } = run([projectName], { cwd: __dirname }) 70 | expect(stdout).toContain(`Target directory "${projectName}" is not empty.`) 71 | }) 72 | 73 | test('asks to overwrite non-empty current directory', () => { 74 | createNonEmptyDir() 75 | const { stdout } = run(['.'], { cwd: genPath }) 76 | expect(stdout).toContain(`Current directory is not empty.`) 77 | }) 78 | 79 | test('successfully scaffolds a project based on vue starter template', () => { 80 | const { stdout } = run([projectName, '--template', 'vue-ts'], { 81 | cwd: __dirname, 82 | }) 83 | const generatedFiles = fs.readdirSync(genPath).sort() 84 | 85 | // Assertions 86 | expect(stdout).toContain(`Scaffolding project in ${genPath}`) 87 | expect(templateFiles).toEqual(generatedFiles) 88 | }) 89 | 90 | test('works with the -t alias', () => { 91 | const { stdout } = run([projectName, '-t', 'vue-ts'], { 92 | cwd: __dirname, 93 | }) 94 | const generatedFiles = fs.readdirSync(genPath).sort() 95 | 96 | // Assertions 97 | expect(stdout).toContain(`Scaffolding project in ${genPath}`) 98 | expect(templateFiles).toEqual(generatedFiles) 99 | }) 100 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: ['src/index'], 5 | clean: true, 6 | rollup: { 7 | inlineDependencies: true, 8 | esbuild: { 9 | minify: true, 10 | }, 11 | }, 12 | alias: { 13 | // we can always use non-transpiled code since we support 14.18.0+ 14 | prompts: 'prompts/lib/index.js', 15 | }, 16 | hooks: { 17 | 'rollup:options'(ctx, options) { 18 | options.plugins = [ 19 | options.plugins, 20 | ] 21 | }, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import './dist/index.mjs' 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-vite-app-cli", 3 | "version": "0.8.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "author": "Lane", 7 | "bin": { 8 | "create-vite-app-cli": "index.js" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "template-*", 13 | "dist" 14 | ], 15 | "scripts": { 16 | "dev": "unbuild --stub", 17 | "build": "unbuild", 18 | "typecheck": "tsc --noEmit", 19 | "test-unit": "vitest run", 20 | "prepare": "husky install" 21 | }, 22 | "engines": { 23 | "node": "^14.18.0 || >=16.0.0" 24 | }, 25 | "keywords": [ 26 | "react-app", 27 | "vue3-app", 28 | "react-ts", 29 | "vue3-ts", 30 | "docker", 31 | "openapi", 32 | "swagger", 33 | "query" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/rookie-luochao/create-vite-app-cli" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/rookie-luochao/create-vite-app-cli/issues" 41 | }, 42 | "homepage": "https://github.com/rookie-luochao/create-vite-app-cli#readme", 43 | "devDependencies": { 44 | "@commitlint/cli": "^17.6.7", 45 | "@commitlint/config-conventional": "^17.6.7", 46 | "@types/cross-spawn": "^6.0.2", 47 | "@types/fs-extra": "^11.0.1", 48 | "@types/minimist": "^1.2.2", 49 | "@types/node": "^18.16.19", 50 | "@types/prompts": "^2.4.4", 51 | "cross-spawn": "^7.0.3", 52 | "execa": "^7.1.1", 53 | "fs-extra": "^11.1.1", 54 | "husky": "^8.0.3", 55 | "kolorist": "^1.8.0", 56 | "lint-staged": "^13.2.3", 57 | "minimist": "^1.2.8", 58 | "prettier": "^2.8.8", 59 | "pretty-quick": "^3.1.3", 60 | "prompts": "^2.4.2", 61 | "unbuild": "^1.2.1", 62 | "vitest": "^0.33.0" 63 | }, 64 | "commitlint": { 65 | "extends": [ 66 | "@commitlint/config-conventional" 67 | ], 68 | "rules": { 69 | "type-enum": [ 70 | 2, 71 | "always", 72 | [ 73 | "build", 74 | "chore", 75 | "ci", 76 | "docs", 77 | "feat", 78 | "fix", 79 | "perf", 80 | "refactor", 81 | "revert", 82 | "style", 83 | "test" 84 | ] 85 | ] 86 | } 87 | }, 88 | "prettier": { 89 | "trailingComma": "all", 90 | "arrowParens": "always", 91 | "printWidth": 120 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import spawn from "cross-spawn"; 5 | import minimist from "minimist"; 6 | import prompts from "prompts"; 7 | import { blue, cyan, green, red, reset } from "kolorist"; 8 | 9 | // Avoids autoconversion to number of the project name by defining that the args 10 | // non associated with an option ( _ ) needs to be parsed as a string. See #4606 11 | const argv = minimist<{ 12 | t?: string; 13 | template?: string; 14 | }>(process.argv.slice(2), { string: ["_"] }); 15 | const cwd = process.cwd(); 16 | 17 | type ColorFunc = (str: string | number) => string; 18 | type Framework = { 19 | name: string; 20 | display: string; 21 | color: ColorFunc; 22 | variants: FrameworkVariant[]; 23 | }; 24 | type FrameworkVariant = { 25 | name: string; 26 | display: string; 27 | color: ColorFunc; 28 | customCommand?: string; 29 | }; 30 | 31 | const FRAMEWORKS: Framework[] = [ 32 | { 33 | name: "react", 34 | display: "React", 35 | color: cyan, 36 | variants: [ 37 | { 38 | name: "react-ts", 39 | display: "TypeScript", 40 | color: blue, 41 | }, 42 | { 43 | name: "react-swc-ts", 44 | display: "TypeScript + SWC", 45 | color: blue, 46 | }, 47 | ], 48 | }, 49 | { 50 | name: "vue", 51 | display: "Vue", 52 | color: green, 53 | variants: [ 54 | { 55 | name: "vue-ts", 56 | display: "TypeScript", 57 | color: blue, 58 | }, 59 | ], 60 | }, 61 | ]; 62 | 63 | const TEMPLATES = FRAMEWORKS.map((f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]).reduce( 64 | (a, b) => a.concat(b), 65 | [], 66 | ); 67 | 68 | const renameFiles: Record = { 69 | _gitignore: ".gitignore", 70 | _npmrc: ".npmrc", 71 | }; 72 | 73 | const defaultTargetDir = "vite-project"; 74 | 75 | async function init() { 76 | const argTargetDir = formatTargetDir(argv._[0]); 77 | const argTemplate = argv.template || argv.t; 78 | 79 | let targetDir = argTargetDir || defaultTargetDir; 80 | const getProjectName = () => (targetDir === "." ? path.basename(path.resolve()) : targetDir); 81 | 82 | let result: prompts.Answers<"projectName" | "overwrite" | "packageName" | "framework" | "variant">; 83 | 84 | try { 85 | result = await prompts( 86 | [ 87 | { 88 | type: argTargetDir ? null : "text", 89 | name: "projectName", 90 | message: reset("Project name:"), 91 | initial: defaultTargetDir, 92 | onState: (state) => { 93 | targetDir = formatTargetDir(state.value) || defaultTargetDir; 94 | }, 95 | }, 96 | { 97 | type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : "confirm"), 98 | name: "overwrite", 99 | message: () => 100 | (targetDir === "." ? "Current directory" : `Target directory "${targetDir}"`) + 101 | ` is not empty. Remove existing files and continue?`, 102 | }, 103 | { 104 | type: (_, { overwrite }: { overwrite?: boolean }) => { 105 | if (overwrite === false) { 106 | throw new Error(red("✖") + " Operation cancelled"); 107 | } 108 | return null; 109 | }, 110 | name: "overwriteChecker", 111 | }, 112 | { 113 | type: () => (isValidPackageName(getProjectName()) ? null : "text"), 114 | name: "packageName", 115 | message: reset("Package name:"), 116 | initial: () => toValidPackageName(getProjectName()), 117 | validate: (dir) => isValidPackageName(dir) || "Invalid package.json name", 118 | }, 119 | { 120 | type: argTemplate && TEMPLATES.includes(argTemplate) ? null : "select", 121 | name: "framework", 122 | message: 123 | typeof argTemplate === "string" && !TEMPLATES.includes(argTemplate) 124 | ? reset(`"${argTemplate}" isn't a valid template. Please choose from below: `) 125 | : reset("Select a framework:"), 126 | initial: 0, 127 | choices: FRAMEWORKS.map((framework) => { 128 | const frameworkColor = framework.color; 129 | return { 130 | title: frameworkColor(framework.display || framework.name), 131 | value: framework, 132 | }; 133 | }), 134 | }, 135 | { 136 | type: (framework: Framework) => (framework && framework.variants ? "select" : null), 137 | name: "variant", 138 | message: reset("Select a variant:"), 139 | choices: (framework: Framework) => 140 | framework.variants.map((variant) => { 141 | const variantColor = variant.color; 142 | return { 143 | title: variantColor(variant.display || variant.name), 144 | value: variant.name, 145 | }; 146 | }), 147 | }, 148 | ], 149 | { 150 | onCancel: () => { 151 | throw new Error(red("✖") + " Operation cancelled"); 152 | }, 153 | }, 154 | ); 155 | } catch (cancelled: any) { 156 | console.log(cancelled.message); 157 | return; 158 | } 159 | 160 | // user choice associated with prompts 161 | const { framework, overwrite, packageName, variant } = result; 162 | 163 | const root = path.join(cwd, targetDir); 164 | 165 | if (overwrite) { 166 | emptyDir(root); 167 | } else if (!fs.existsSync(root)) { 168 | fs.mkdirSync(root, { recursive: true }); 169 | } 170 | 171 | // determine template 172 | let template: string = variant || framework?.name || argTemplate; 173 | let isReactSwc = false; 174 | if (template.includes("-swc")) { 175 | isReactSwc = true; 176 | template = template.replace("-swc", ""); 177 | } 178 | 179 | const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent); 180 | const pkgManager = pkgInfo ? pkgInfo.name : "npm"; 181 | const isYarn1 = pkgManager === "yarn" && pkgInfo?.version.startsWith("1."); 182 | 183 | const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}; 184 | 185 | if (customCommand) { 186 | const fullCustomCommand = customCommand 187 | .replace(/^npm create /, () => { 188 | // `bun create` uses it's own set of templates, 189 | // the closest alternative is using `bun x` directly on the package 190 | if (pkgManager === "bun") { 191 | return "bun x create-"; 192 | } 193 | return `${pkgManager} create `; 194 | }) 195 | // Only Yarn 1.x doesn't support `@version` in the `create` command 196 | .replace("@latest", () => (isYarn1 ? "" : "@latest")) 197 | .replace(/^npm exec/, () => { 198 | // Prefer `pnpm dlx`, `yarn dlx`, or `bun x` 199 | if (pkgManager === "pnpm") { 200 | return "pnpm dlx"; 201 | } 202 | if (pkgManager === "yarn" && !isYarn1) { 203 | return "yarn dlx"; 204 | } 205 | if (pkgManager === "bun") { 206 | return "bun x"; 207 | } 208 | // Use `npm exec` in all other cases, 209 | // including Yarn 1.x and other custom npm clients. 210 | return "npm exec"; 211 | }); 212 | 213 | const [command, ...args] = fullCustomCommand.split(" "); 214 | // we replace TARGET_DIR here because targetDir may include a space 215 | const replacedArgs = args.map((arg) => arg.replace("TARGET_DIR", targetDir)); 216 | const { status } = spawn.sync(command, replacedArgs, { 217 | stdio: "inherit", 218 | }); 219 | process.exit(status ?? 0); 220 | } 221 | 222 | console.log(`\nScaffolding project in ${root}...`); 223 | 224 | const templateDir = path.resolve(fileURLToPath(import.meta.url), "../..", `template-${template}`); 225 | 226 | const write = (file: string, content?: string) => { 227 | const targetPath = path.join(root, renameFiles[file] ?? file); 228 | if (content) { 229 | fs.writeFileSync(targetPath, content); 230 | } else { 231 | copy(path.join(templateDir, file), targetPath); 232 | } 233 | }; 234 | 235 | const files = fs.readdirSync(templateDir); 236 | for (const file of files.filter((f) => f !== "package.json")) { 237 | write(file); 238 | } 239 | 240 | const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), "utf-8")); 241 | 242 | pkg.name = packageName || getProjectName(); 243 | 244 | write("package.json", JSON.stringify(pkg, null, 2) + "\n"); 245 | 246 | if (isReactSwc) { 247 | setupReactSwc(root, template.endsWith("-ts")); 248 | } 249 | 250 | const cdProjectName = path.relative(cwd, root); 251 | console.log(`\nDone. Now run:\n`); 252 | if (root !== cwd) { 253 | console.log(` cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`); 254 | } 255 | switch (pkgManager) { 256 | case "yarn": 257 | console.log(" git init"); 258 | console.log(" yarn"); 259 | console.log(" yarn dev"); 260 | break; 261 | default: 262 | console.log(" git init"); 263 | console.log(` ${pkgManager} install`); 264 | console.log(` ${pkgManager} run dev`); 265 | break; 266 | } 267 | console.log(); 268 | } 269 | 270 | function formatTargetDir(targetDir: string | undefined) { 271 | return targetDir?.trim().replace(/\/+$/g, ""); 272 | } 273 | 274 | function copy(src: string, dest: string) { 275 | const stat = fs.statSync(src); 276 | if (stat.isDirectory()) { 277 | copyDir(src, dest); 278 | } else { 279 | fs.copyFileSync(src, dest); 280 | } 281 | } 282 | 283 | function isValidPackageName(projectName: string) { 284 | return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName); 285 | } 286 | 287 | function toValidPackageName(projectName: string) { 288 | return projectName 289 | .trim() 290 | .toLowerCase() 291 | .replace(/\s+/g, "-") 292 | .replace(/^[._]/, "") 293 | .replace(/[^a-z\d\-~]+/g, "-"); 294 | } 295 | 296 | function copyDir(srcDir: string, destDir: string) { 297 | fs.mkdirSync(destDir, { recursive: true }); 298 | for (const file of fs.readdirSync(srcDir)) { 299 | const srcFile = path.resolve(srcDir, file); 300 | const destFile = path.resolve(destDir, file); 301 | copy(srcFile, destFile); 302 | } 303 | } 304 | 305 | function isEmpty(path: string) { 306 | const files = fs.readdirSync(path); 307 | return files.length === 0 || (files.length === 1 && files[0] === ".git"); 308 | } 309 | 310 | function emptyDir(dir: string) { 311 | if (!fs.existsSync(dir)) { 312 | return; 313 | } 314 | for (const file of fs.readdirSync(dir)) { 315 | if (file === ".git") { 316 | continue; 317 | } 318 | fs.rmSync(path.resolve(dir, file), { recursive: true, force: true }); 319 | } 320 | } 321 | 322 | function pkgFromUserAgent(userAgent: string | undefined) { 323 | if (!userAgent) return undefined; 324 | const pkgSpec = userAgent.split(" ")[0]; 325 | const pkgSpecArr = pkgSpec.split("/"); 326 | return { 327 | name: pkgSpecArr[0], 328 | version: pkgSpecArr[1], 329 | }; 330 | } 331 | 332 | function setupReactSwc(root: string, isTs: boolean) { 333 | editFile(path.resolve(root, "package.json"), (content) => { 334 | return content.replace(/"@vitejs\/plugin-react": ".+?"/, `"@vitejs/plugin-react-swc": "^3.3.2"`); 335 | }); 336 | editFile(path.resolve(root, `vite.config.${isTs ? "ts" : "js"}`), (content) => { 337 | return content.replace("@vitejs/plugin-react", "@vitejs/plugin-react-swc"); 338 | }); 339 | } 340 | 341 | function editFile(file: string, callback: (content: string) => string) { 342 | const content = fs.readFileSync(file, "utf-8"); 343 | fs.writeFileSync(file, callback(content), "utf-8"); 344 | } 345 | 346 | init().catch((e) => { 347 | console.error(e); 348 | }); 349 | -------------------------------------------------------------------------------- /template-react-ts/.env.production: -------------------------------------------------------------------------------- 1 | # .env.production, vite在build时会自动打包到import.meta.env对象下 2 | VITE_appName=webapp-react 3 | # host需要替换为真实host 4 | VITE_baseURL=https://host 5 | VITE_env=demo 6 | VITE_version="" -------------------------------------------------------------------------------- /template-react-ts/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | '@typescript-eslint/ban-ts-comment': 'off' 18 | } 19 | } -------------------------------------------------------------------------------- /template-react-ts/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /template-react-ts/.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm install 5 | -------------------------------------------------------------------------------- /template-react-ts/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /template-react-ts/.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # pnpm test 5 | -------------------------------------------------------------------------------- /template-react-ts/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | FROM --platform=${BUILDPLATFORM:-linux/amd64} node:17-buster AS builder 3 | 4 | ENV PNPM_HOME="/pnpm" 5 | ENV PATH="$PNPM_HOME:$PATH" 6 | RUN corepack enable 7 | 8 | WORKDIR /src 9 | COPY ./ /src 10 | 11 | RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \ 12 | --mount=type=cache,target=/pnpm/store,id=pnpm_cache \ 13 | pnpm install 14 | 15 | RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \ 16 | pnpm run build 17 | 18 | FROM ghcr.io/zboyco/webrunner:0.0.8 19 | 20 | COPY --from=builder /src/dist /app -------------------------------------------------------------------------------- /template-react-ts/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | package-lock.json 27 | yarn.lock 28 | *.js 29 | .tmp 30 | .husky/_ -------------------------------------------------------------------------------- /template-react-ts/_npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true -------------------------------------------------------------------------------- /template-react-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /template-react-ts/makefile: -------------------------------------------------------------------------------- 1 | PKG = $(shell cat package.json | grep 'name' | sed -e 's/ "name": "//g' -e 's/",//g') 2 | VERSION = $(shell cat package.json | grep 'version' | sed -e 's/ "version": "//g' -e 's/",//g') 3 | 4 | docker-build: 5 | docker build . -t $(PKG):v$(VERSION) 6 | 7 | docker-run: 8 | docker run -d -p 80:80 $(PKG):v$(VERSION) 9 | 10 | docker-build-run: docker-build docker-run 11 | 12 | install: 13 | pnpm install 14 | 15 | dev: 16 | npm run dev 17 | 18 | build: 19 | npm run build 20 | 21 | preview: 22 | npm run preview 23 | 24 | lint: 25 | npm run lint 26 | 27 | lintfix: 28 | npm run lintfix 29 | 30 | prepare: 31 | npm run prepare 32 | 33 | openapi: 34 | npm run openapi -------------------------------------------------------------------------------- /template-react-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp-react", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "lintfix": "eslint . --ext ts,tsx --fix", 12 | "prepare": "husky install", 13 | "openapi": "ts-node './src/core/openapi/index.ts'" 14 | }, 15 | "dependencies": { 16 | "@ant-design/icons": "^5.2.5", 17 | "@emotion/react": "^11.11.1", 18 | "@tanstack/react-query": "^4.32.0", 19 | "ahooks": "^3.7.8", 20 | "antd": "^5.6.4", 21 | "axios": "^1.4.0", 22 | "lodash-es": "^4.17.21", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-error-boundary": "^4.0.11", 26 | "react-markdown": "^8.0.7", 27 | "react-router-dom": "^6.14.1", 28 | "react-router-toolkit": "^1.0.0", 29 | "rxjs": "^7.8.1", 30 | "zustand": "^4.3.9" 31 | }, 32 | "devDependencies": { 33 | "@babel/preset-react": "^7.22.15", 34 | "@commitlint/cli": "^17.0.3", 35 | "@commitlint/config-conventional": "^17.0.3", 36 | "@emotion/babel-plugin": "^11.11.0", 37 | "@types/lodash-es": "^4.17.8", 38 | "@types/react": "^18.2.15", 39 | "@types/react-dom": "^18.2.7", 40 | "@typescript-eslint/eslint-plugin": "^6.0.0", 41 | "@typescript-eslint/parser": "^6.0.0", 42 | "@umijs/openapi": "^1.9.2", 43 | "@vitejs/plugin-react": "^4.0.3", 44 | "csstype": "^3.1.2", 45 | "eslint": "^8.45.0", 46 | "eslint-plugin-react-hooks": "^4.6.0", 47 | "eslint-plugin-react-refresh": "^0.4.3", 48 | "husky": "^8.0.1", 49 | "lint-staged": "^13.0.3", 50 | "prettier": "^3.0.3", 51 | "ts-node": "^10.9.1", 52 | "typescript": "^5.0.2", 53 | "vite": "^4.4.5" 54 | }, 55 | "commitlint": { 56 | "extends": [ 57 | "@commitlint/config-conventional" 58 | ], 59 | "rules": { 60 | "type-enum": [ 61 | 2, 62 | "always", 63 | [ 64 | "build", 65 | "chore", 66 | "ci", 67 | "docs", 68 | "feat", 69 | "fix", 70 | "perf", 71 | "refactor", 72 | "revert", 73 | "style", 74 | "test" 75 | ] 76 | ] 77 | } 78 | }, 79 | "lint-staged": { 80 | "*.(ts|tsx)": [ 81 | "eslint --quiet" 82 | ], 83 | "*.(ts|tsx|json|html)": [ 84 | "prettier --write" 85 | ] 86 | }, 87 | "prettier": { 88 | "arrowParens": "always", 89 | "printWidth": 120, 90 | "singleQuote": true, 91 | "jsxSingleQuote": true 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /template-react-ts/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template-react-ts/readme.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite + Pnpm + Zustand + Openapi + Docker 2 | 3 | 该模板可以帮助您在 Vite 中使用 React 和 TypeScript 进行开发web应用. 4 | 5 | #### 概览 6 | * 学习搭建一个web应用开发脚手架,最大限度使用社区优秀开源方案 7 | * 支持自动根据openapi生成api request函数、类型、枚举等, [openapi格式参考](https://srv-demo-docker.onrender.com/openapi) 8 | * 支持前端工程化 9 | * 支持前端容器化(需要安装docker环境) 10 | * 同步接口请求状态,实现自动loading 11 | * 支持接口联动,方便跨父子组件刷新相关联的接口 12 | * 支持容器化变量注入,无需前端配置文件写死,方便通过 k8s 动态注入 13 | * 后续支持更好用的modal,更好用的form 14 | * 此脚手架最佳实战参考[rookie-luochao/react](https://github.com/rookie-luochao/react) 15 | 16 | #### 核心技术 17 | * 打包编译 - [vite](https://github.com/vitejs/vite) 18 | * 包管理 - [pnpm](https://github.com/pnpm/pnpm) 19 | * 编程语言 - [typescript](https://github.com/microsoft/TypeScript) 20 | * 前端框架 - [react](https://github.com/facebook/react) 21 | * 路由 - [react-router](https://github.com/remix-run/react-router) 22 | * UI组件库 - [antd](https://github.com/ant-design/ant-design) 23 | * cssinjs(不考虑性能开销) - [emotion](https://github.com/emotion-js/emotion) 24 | * 全局数据共享 - [zustand](https://github.com/pmndrs/zustand) 25 | * 自动生成api - [openapi](https://github.com/chenshuai2144/openapi2typescript) 26 | * 网络请求 - [axios](https://github.com/axios/axios) 27 | * 数据请求利器 - [react-query](https://github.com/TanStack/query) 28 | * 通用hook - [ahooks](https://github.com/alibaba/hooks) 29 | * 错误边界 - [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 30 | * 前端日志(暂未集成) - [sentry-javascript](https://github.com/getsentry/sentry-javascript) 31 | * hack - [babel](https://github.com/babel/babel) 32 | * 代码检查 - [eslint](https://github.com/eslint/eslint) 33 | * ts代码检查插件 - [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) 34 | * 代码美化 - [prettier](https://github.com/prettier/prettier) 35 | * git钩子 - [husky](https://github.com/typicode/husky) 36 | * commit格式化 -[commitlint](https://github.com/conventional-changelog/commitlint) 37 | 38 | #### 技术说明 39 | * 自动生成api(openapi): 后端接入apenapi后,前端可以根据openapi文件自动生成request api 40 | * 路由(react-router-dom): 自身默认支持错误边界功能,我觉得react-error-boundary更好用点,所以用hack绕过了react-router-dom的错误边界(ps: react-router-dom暂时不支持参数禁用它自带的错误边界) 41 | * 通用hook(ahooks): 一个hook工具库,该库可以依据个人喜好选择是否使用 42 | * 前端日志(sentry): 暂时未集成,需要进一步调研实用性和可用性 43 | 44 | #### 快速开始 45 | ```bash 46 | // 下载包 47 | pnpm install 48 | # or make install 49 | 50 | // 启动 51 | npm run dev 52 | # or make dev 53 | ``` 54 | 55 | #### 其他常用命令 56 | ```bash 57 | // 打包 58 | npm run build 59 | # or make build 60 | 61 | // 拉取openapi=>自动生成api request 62 | npm run openapi 63 | # or make openapi 64 | 65 | // 制作docker镜像 66 | make docker-build 67 | 68 | // 运行docker镜像 69 | make docker-run 70 | 71 | // 制作docker镜像 and 运行docker镜像 72 | make docker-build-run 73 | ``` 74 | 75 | #### 注意事项 76 | * 如果遇到git commit无法触发husky的情况,则需要手动执行一遍`npm run prepare` 77 | 78 | #### 基于openapi自动获取api请求函数,配置如下 79 | ```js 80 | // src/core/openapi/index.ts 81 | 82 | // 示例代码 83 | generateService({ 84 | // openapi地址 85 | schemaPath: `${appConfig.baseURL}/${urlPath}`, 86 | // 文件生成目录 87 | serversPath: "./src", 88 | // 自定义网络请求函数路径 89 | requestImportStatement: `/// \nimport request from "@request"`, 90 | // 代码组织命名空间, 例如:Api 91 | namespace: "Api", 92 | }); 93 | ``` 94 | 95 | #### 应用配置 96 | ```js 97 | // src/config.ts 98 | 99 | // 一级path, 例如:openapi 100 | export const urlPath = ""; 101 | 102 | // 项目基本变量配置 103 | const appConfig: IConfig = { 104 | // 应用名称, 例如:webapp-react 105 | appName: "", 106 | // 网络请求的域名,例如:https://host 107 | baseURL: "", 108 | // 发布版本,例如:0000000-0.0.1 109 | version: "", 110 | // 代码环境,例如:demo, staging, online 111 | env: "", 112 | }; 113 | ``` 114 | 115 | #### 环境变量 116 | * 项目 dev 环境变量配置在`src/config.ts` 117 | * 项目 prod 环境变量配置在`.env.production`,详情参考:[vite环境变量](https://cn.vitejs.dev/guide/env-and-mode.html) 118 | * 项目 prod 环境变量也可以使用容器变量 ARG,我们会读取容器变量并注入到前端meta标签的content里面,目前html文件提供了两个mate标签(env、app_config)接收变量,格式详情参考:`index.html` 和 `src/core/http/config.ts` 119 | 120 | #### 调用接口(react-query), 支持自动loading和接口请求联动 121 | ```js 122 | // HelloGet是一个基于axios的promise请求 123 | export async function HelloGet( 124 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 125 | params: Api.HelloGetParams, 126 | options?: { [key: string]: any }, 127 | ) { 128 | return request('/gin-demo-server/api/v1/hello', { 129 | method: 'GET', 130 | params: { 131 | ...params, 132 | }, 133 | ...(options || {}), 134 | }); 135 | } 136 | 137 | // 自动调用接口获取数据 138 | const { data, isLoading } = useQuery({ 139 | queryKey: ["hello", name], 140 | queryFn: () => { 141 | return HelloGet({ name: name }); 142 | }, 143 | }); 144 | 145 | export async function HelloPost(body: Api.HelloPostParam, options?: { [key: string]: any }) { 146 | return request('/gin-demo-server/api/v1/hello', { 147 | method: 'POST', 148 | headers: { 149 | 'Content-Type': 'application/json', 150 | }, 151 | data: body, 152 | ...(options || {}), 153 | }); 154 | } 155 | 156 | // 提交编辑数据 157 | const { mutate, isLoading } = useMutation({ 158 | mutationFn: HelloPost, 159 | onSuccess(data) { 160 | setName(data?.data || ""); 161 | }, 162 | onError() { 163 | // 清除queryKey为hello的接口数据缓存,自动重新获取接口数据 164 | queryClient.invalidateQueries({ queryKey: ['hello'] }); 165 | } 166 | }) 167 | 168 | mutate({ name: "lisi" }); 169 | 170 | ``` -------------------------------------------------------------------------------- /template-react-ts/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /template-react-ts/src/assets/images/logo_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /template-react-ts/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template-react-ts/src/config.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | appName: string; 3 | baseURL: string; 4 | version?: string; 5 | env?: string; 6 | } 7 | 8 | // 一级path, 例如:openapi 9 | export const urlPath = ""; 10 | 11 | // 项目基本变量配置 12 | const appConfig: IConfig = { 13 | // 应用名称, 例如:webapp-react 14 | appName: "", 15 | // 网络请求的域名,例如:https://host 16 | baseURL: "", 17 | // 发布版本,例如:0000000-0.0.1 18 | version: "", 19 | // 代码环境,例如:demo 20 | env: "", 21 | }; 22 | 23 | export default appConfig; 24 | -------------------------------------------------------------------------------- /template-react-ts/src/core/error-boundary/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "antd"; 2 | import { isString } from "lodash-es"; 3 | import { ErrorInfo, ReactNode } from "react"; 4 | import { ErrorBoundary, FallbackProps } from "react-error-boundary"; 5 | import { Outlet } from "react-router-dom"; 6 | 7 | export function fallbackRender({ error, resetErrorBoundary }: FallbackProps) { 8 | // Call resetErrorBoundary() to reset the error boundary and retry the render. 9 | return ( 10 |
11 |

Something went wrong:

12 |
{isString(error) ? error : error.message}
13 | 14 |
15 | ); 16 | } 17 | 18 | export const logError = (error: Error, info: ErrorInfo) => { 19 | // Do something with the error, e.g. log to an external API 20 | Modal.error({ 21 | title: error.message, 22 | content: info.componentStack, 23 | width: 756, 24 | }); 25 | 26 | // use error make log 27 | }; 28 | 29 | const ErrorBoundaryWrap = ({ children }: { children: ReactNode }) => { 30 | return ( 31 | { 35 | // Reset the state of your app so the error doesn't happen again 36 | console.log("details:", details); 37 | }} 38 | > 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | export const ErrorBoundaryWrapOutlet = () => ( 45 | 46 | 47 | 48 | ); 49 | -------------------------------------------------------------------------------- /template-react-ts/src/core/http/TanStackQuery.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | 3 | export function TanStackQueryProvider({ children }: { children: React.ReactNode }) { 4 | const queryClient = new QueryClient({ 5 | defaultOptions: { 6 | queries: { 7 | refetchOnWindowFocus: false, 8 | staleTime: 60 * 60 * 1000, 9 | retry: false, 10 | }, 11 | }, 12 | }); 13 | 14 | return {children}; 15 | } 16 | -------------------------------------------------------------------------------- /template-react-ts/src/core/http/config.ts: -------------------------------------------------------------------------------- 1 | import appConfig, { IConfig } from "../../config"; 2 | 3 | export function getConfig(): IConfig { 4 | const mateEnv = import.meta.env; 5 | const defaultAppConfig = { 6 | appName: mateEnv?.VITE_appName || "", 7 | baseURL: mateEnv?.VITE_baseURL || "", 8 | version: mateEnv?.VITE_version || "", 9 | env: mateEnv?.VITE_env || "", 10 | }; 11 | 12 | // 本地开发环境直接从根目录config文件读取, ci环境直接从mate标签读取, 通过容器环境变量写入html的mate标签 13 | // mate标签name为:app_config, content格式为:appName=webapp,baseURL=https://api.com,env=,version= 14 | if (import.meta.env.DEV) { 15 | return appConfig; 16 | } else { 17 | const appConfigStr = getMeta("app_config"); 18 | 19 | if (!appConfigStr) return defaultAppConfig; 20 | 21 | return parseEnvVar(appConfigStr); 22 | } 23 | } 24 | 25 | function getMeta(metaName: string) { 26 | const metas = document.getElementsByTagName("meta"); 27 | 28 | for (let i = 0; i < metas.length; i++) { 29 | if (metas[i].getAttribute("name") === metaName) { 30 | return metas[i].getAttribute("content"); 31 | } 32 | } 33 | 34 | return ""; 35 | } 36 | 37 | function parseEnvVar(envVarURL: string) { 38 | const arrs = envVarURL.split(","); 39 | 40 | return arrs.reduce((pre, item) => { 41 | const keyValues = item.split("="); 42 | 43 | return { 44 | ...pre, 45 | [keyValues[0]]: keyValues[1], 46 | }; 47 | }, {} as IConfig); 48 | } 49 | -------------------------------------------------------------------------------- /template-react-ts/src/core/http/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { ILoginInfoStorageState, defaultLoginInfoStorage, loginInfoStorageKey } from "../store"; 3 | import { getConfig } from "./config"; 4 | import { notification } from "antd"; 5 | 6 | const BASE_URL = getConfig().baseURL; 7 | 8 | const instance = axios.create({ 9 | baseURL: BASE_URL, 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | timeout: 120000, // 超时时间120秒 14 | }); 15 | 16 | instance.interceptors.response.use( 17 | (response) => { 18 | // data解构 19 | if (response.data) { 20 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 21 | return response.data; 22 | } 23 | return response; 24 | }, 25 | (error) => { 26 | // 统一错误处理 27 | if (error.response.status >= 300) { 28 | notification.error({ 29 | message: error.response.data?.msg, 30 | duration: 2, 31 | }); 32 | } 33 | return Promise.reject(error); 34 | }, 35 | ); 36 | 37 | instance.interceptors.request.use((config) => { 38 | const loginInfoStorageStr = globalThis.localStorage.getItem(loginInfoStorageKey); 39 | const loginInfoStorage = loginInfoStorageStr 40 | ? (JSON.parse(loginInfoStorageStr) as ILoginInfoStorageState) 41 | : defaultLoginInfoStorage; 42 | 43 | if (loginInfoStorage.state.loginInfo) { 44 | config.headers.Authorization = loginInfoStorage.state.loginInfo.accessToken; 45 | } 46 | 47 | return config; 48 | }); 49 | 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | const request = async (url: string, options: AxiosRequestConfig & { requestType?: "json" | "form" } = {}) => { 52 | // 兼容from data文件上传的情况 53 | const { requestType, ...rest } = options; 54 | if (requestType === "form") { 55 | return await instance.request({ 56 | url, 57 | ...rest, 58 | headers: { 59 | ...(rest.headers || {}), 60 | "Content-Type": "multipart/form-data", 61 | }, 62 | }); 63 | } else { 64 | return await instance.request({ 65 | url, 66 | ...rest, 67 | }); 68 | } 69 | }; 70 | 71 | export default request; 72 | -------------------------------------------------------------------------------- /template-react-ts/src/core/openapi/index.ts: -------------------------------------------------------------------------------- 1 | import { generateService } from "@umijs/openapi"; 2 | import appConfig, { urlPath } from "../../config.ts"; 3 | 4 | generateService({ 5 | // openapi地址 6 | schemaPath: `${appConfig.baseURL}/${urlPath}`, 7 | // 文件生成目录 8 | serversPath: "./src", 9 | // 自定义网络请求函数路径 10 | requestImportStatement: 'import request from "@request"', 11 | // 代码组织命名空间, 例如:Api 12 | namespace: "Api", 13 | }); 14 | -------------------------------------------------------------------------------- /template-react-ts/src/core/router/CreateBrowserRouter.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from "antd"; 2 | import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom"; 3 | 4 | export function CreateBrowserRouter({ routes }: { routes: RouteObject[] }) { 5 | const router = createBrowserRouter(routes, { 6 | future: { 7 | // Normalize `useNavigation()`/`useFetcher()` `formMethod` to uppercase 8 | v7_normalizeFormMethod: true, 9 | }, 10 | }); 11 | 12 | return } />; 13 | } 14 | -------------------------------------------------------------------------------- /template-react-ts/src/core/router/LazyImportComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from "antd"; 2 | import { ReactNode, Suspense } from "react"; 3 | 4 | export const LazyImportComponent = ({ children }: { children: ReactNode }) => { 5 | return }>{children}; 6 | }; 7 | -------------------------------------------------------------------------------- /template-react-ts/src/core/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { StorageValue, devtools, persist } from "zustand/middleware"; 3 | import { logger } from "./loggerMiddleware"; 4 | 5 | interface ILoginInfo { 6 | accessToken: string; 7 | refreshToken?: string; 8 | uid?: string; 9 | expireAt?: string; 10 | expires_in?: number; 11 | name?: string; 12 | } 13 | 14 | interface ILoginInfoState { 15 | loginInfo: ILoginInfo | null; 16 | updateLoginInfo: (nextState: ILoginInfo) => void; 17 | clear: () => void; 18 | } 19 | 20 | export const loginInfoStorageKey = "login-info-storage"; 21 | export const defaultLoginInfoStorage = { state: { loginInfo: null }, version: 0 }; 22 | export type ILoginInfoStorageState = StorageValue>; 23 | 24 | export const useLoginInfoStore = create()( 25 | logger( 26 | devtools( 27 | persist( 28 | (set) => ({ 29 | loginInfo: null, 30 | updateLoginInfo: (newLoginInfo) => set(() => ({ loginInfo: newLoginInfo })), 31 | clear: () => set(() => ({ loginInfo: null })), 32 | }), 33 | { 34 | name: loginInfoStorageKey, 35 | }, 36 | ), 37 | ), 38 | ), 39 | ); 40 | 41 | interface IProjectInfo { 42 | projectId: string; 43 | projectName: string; 44 | } 45 | 46 | interface IProjectInfoState { 47 | projectInfo: IProjectInfo[] | null; 48 | updateProjectInfo: (nextState: IProjectInfo[]) => void; 49 | clear: () => void; 50 | } 51 | 52 | const projectInfoStorageKey = "project-info-storage"; 53 | 54 | export const useProjectInfoStore = create()( 55 | logger( 56 | devtools( 57 | persist( 58 | (set) => ({ 59 | projectInfo: null, 60 | updateProjectInfo: (newProjectInfo) => set(() => ({ projectInfo: newProjectInfo })), 61 | clear: () => set(() => ({ projectInfo: null })), 62 | }), 63 | { 64 | name: projectInfoStorageKey, 65 | }, 66 | ), 67 | ), 68 | ), 69 | ); 70 | -------------------------------------------------------------------------------- /template-react-ts/src/core/store/loggerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator, StoreMutatorIdentifier } from "zustand"; 2 | 3 | type Logger = < 4 | T, 5 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 6 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 7 | >( 8 | f: StateCreator, 9 | name?: string, 10 | ) => StateCreator; 11 | 12 | type LoggerImpl = (f: StateCreator, name?: string) => StateCreator; 13 | 14 | const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { 15 | const loggedSet: typeof set = (...a) => { 16 | set(...a); 17 | console.log(...(name ? [`${name}:`] : []), get()); 18 | }; 19 | store.setState = loggedSet; 20 | 21 | return f(loggedSet, get, store); 22 | }; 23 | 24 | export const logger = loggerImpl as unknown as Logger; 25 | -------------------------------------------------------------------------------- /template-react-ts/src/core/style/defaultStyleConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 样式基本配置 3 | */ 4 | export const dsc = { 5 | // 颜色 6 | color: { 7 | primary: "#3C4FEB", 8 | 9 | success: "#4CD964", 10 | warning: "#F1911E", 11 | info: "#A9AEFC", 12 | danger: "#E51D30", 13 | 14 | text: "#333", 15 | 16 | bg: "#FFFFFF", 17 | bgGray: "#f0f2f5", 18 | 19 | border: "#DEE2EC", 20 | }, 21 | // 字体大小 22 | fontSize: { 23 | xxs: 10, 24 | xs: 12, 25 | s: 14, 26 | normal: 16, 27 | m: 20, 28 | l: 24, 29 | xl: 30, 30 | xxl: 38, 31 | }, 32 | // 层级 33 | zIndex: { 34 | low: 10, 35 | mid: 100, 36 | high: 300, 37 | higher: 500, 38 | higherPlus: 1000, 39 | }, 40 | // 间距 41 | spacing: { 42 | base: "0.6em 1em", 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /template-react-ts/src/core/style/utils.ts: -------------------------------------------------------------------------------- 1 | import { Property } from "csstype"; 2 | 3 | interface IFlexOptions { 4 | flexDirection?: Property.FlexDirection; 5 | justifyContent?: Property.JustifyContent; 6 | alignItems?: Property.AlignItems; 7 | alignSelf?: Property.AlignSelf; 8 | flexWrap?: Property.FlexWrap; 9 | flexFlow?: Property.FlexFlow; 10 | alignContent?: Property.AlignContent; 11 | } 12 | 13 | export const flexOpts = (flexOpts: IFlexOptions = {}) => ({ 14 | display: "flex", 15 | ...flexOpts, 16 | }); 17 | 18 | export const flexCenterOpts = (arg: IFlexOptions = {}) => { 19 | return { 20 | display: "flex", 21 | justifyContent: "center", 22 | alignItems: "center", 23 | ...arg, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /template-react-ts/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | } 4 | -------------------------------------------------------------------------------- /template-react-ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { ConfigProvider } from "antd"; 4 | import { CreateBrowserRouter } from "./core/router/CreateBrowserRouter"; 5 | import { appRoutes } from "./rootRoutes"; 6 | import { LazyImportComponent } from "./core/router/LazyImportComponent"; 7 | import { TanStackQueryProvider } from "./core/http/TanStackQuery"; 8 | 9 | import zhCN from "antd/locale/zh_CN"; 10 | import "./index.css"; 11 | 12 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | ); 23 | -------------------------------------------------------------------------------- /template-react-ts/src/login/config.ts: -------------------------------------------------------------------------------- 1 | export const defaultLinkPath = "/main/dashboard"; 2 | 3 | export const defaultUserInfo = { 4 | username: "admin", 5 | password: "adminadmin", 6 | }; 7 | 8 | export const defaultLoginInfo = { 9 | accessToken: "123456", 10 | name: "张三", 11 | }; 12 | -------------------------------------------------------------------------------- /template-react-ts/src/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { useLoginInfoStore } from "../core/store"; 3 | import { Button, Checkbox, Form, Input, message } from "antd"; 4 | import { LockOutlined, UserOutlined } from "@ant-design/icons"; 5 | import { flexCenterOpts } from "../core/style/utils"; 6 | import { defaultLinkPath, defaultLoginInfo, defaultUserInfo } from "./config"; 7 | 8 | interface ILogin { 9 | username: string; 10 | password: string; 11 | remember?: boolean; 12 | } 13 | 14 | export default function Login() { 15 | const { updateLoginInfo } = useLoginInfoStore((state) => state); 16 | const [form] = Form.useForm(); 17 | const navigate = useNavigate(); 18 | 19 | const onFinish = (values: ILogin) => { 20 | if (values.username === defaultUserInfo.username && values.password === defaultUserInfo.password) { 21 | updateLoginInfo(defaultLoginInfo); 22 | navigate(defaultLinkPath); 23 | } else { 24 | message.warning("用户名或密码错误,请检查!"); 25 | } 26 | }; 27 | 28 | return ( 29 |
40 |
47 | 48 | } placeholder={defaultUserInfo.username} /> 49 | 50 | 51 | } 53 | type="password" 54 | placeholder={defaultUserInfo.password} 55 | /> 56 | 57 | 58 | 59 | Remember me 60 | 61 | 62 | Forgot password 63 | 64 | 65 | 66 | 69 | Or register now! 70 | 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /template-react-ts/src/login/routes.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | 3 | const Login = lazy(() => import("./index")); 4 | 5 | export const loginRoutes = { 6 | path: "/login", 7 | id: "登录", 8 | element: , 9 | }; 10 | -------------------------------------------------------------------------------- /template-react-ts/src/mainLayout/MainLayoutComp.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Dropdown, Menu } from "antd"; 2 | import { dsc } from "../core/style/defaultStyleConfig"; 3 | import { useLoginInfoStore } from "../core/store"; 4 | import LogoMiniIcon from "../assets/images/logo_mini.svg"; 5 | import LogoIcon from "../assets/images/logo.svg"; 6 | import { uiListModuleName } from "../pages/ui-list/routes"; 7 | import { BuildOutlined, DashboardOutlined, ToolOutlined } from "@ant-design/icons"; 8 | import { Dictionary, parseQueryString } from "react-router-toolkit"; 9 | import { ReactNode, useEffect, useMemo, useState } from "react"; 10 | import { find } from "lodash-es"; 11 | import { appRoutes } from "../rootRoutes"; 12 | import { mainLayoutPath } from "./routes"; 13 | import { getMenus } from "./utils"; 14 | import { flexCenterOpts } from "../core/style/utils"; 15 | import { useNavigate } from "react-router-dom"; 16 | import { dashboardModuleName } from "../pages/dashboard/routes"; 17 | import { utilListModuleName } from "../pages/util-list/routes"; 18 | 19 | export const globalHiddenInMenuParentPath = "globalHiddenInMenuParentPath"; 20 | 21 | export function MenuComp() { 22 | const defaultMenuActivePath = `/${mainLayoutPath}/${dashboardModuleName}`; 23 | const [menuActivePath, setMenuActivePath] = useState([defaultMenuActivePath]); 24 | const pathname = document.location.pathname; 25 | const menuOpenKey = pathname 26 | ? pathname 27 | .split("/") 28 | .slice(0, pathname.split("/").length - 1) 29 | .join("/") 30 | : ""; 31 | 32 | useEffect(() => { 33 | if (pathname) { 34 | const query = document.location.search; 35 | let queryObj; 36 | let menuActivePath = pathname; 37 | if (query) { 38 | queryObj = parseQueryString(query); 39 | } 40 | if (queryObj && queryObj[globalHiddenInMenuParentPath]) { 41 | menuActivePath = queryObj[globalHiddenInMenuParentPath] as string; 42 | } 43 | setMenuActivePath([menuActivePath]); 44 | } 45 | }, [pathname]); 46 | 47 | const modulePathToIconMap = { 48 | [dashboardModuleName]: , 49 | [uiListModuleName]: , 50 | [utilListModuleName]: , 51 | } as Dictionary; 52 | 53 | const menuItems = useMemo(() => { 54 | const mainRoutes = find(appRoutes[0].children, (route) => route.path === mainLayoutPath); 55 | return getMenus({ 56 | routes: mainRoutes?.children || [], 57 | modulePathToIconMap, 58 | to: `/${mainLayoutPath}`, 59 | }); 60 | }, []); 61 | 62 | return ( 63 | { 69 | setMenuActivePath([key]); 70 | }} 71 | items={menuItems} 72 | /> 73 | ); 74 | } 75 | 76 | export const Logo = ({ inlineCollapsed }: { inlineCollapsed?: boolean }) => { 77 | return ( 78 |
87 | logo 88 |
89 | ); 90 | }; 91 | 92 | const UserName = () => { 93 | const { loginInfo } = useLoginInfoStore((state) => state); 94 | 95 | return ( 96 |
97 | 98 | {loginInfo?.name?.slice(0, 1)} 99 | 100 | {loginInfo ? ( 101 |
{loginInfo?.name}
102 | ) : null} 103 |
104 | ); 105 | }; 106 | 107 | export const IconDown = () => ( 108 | 109 | 110 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 136 | 137 | 138 | 139 | ); 140 | 141 | export function ToolBar() { 142 | const navigate = useNavigate(); 143 | const { clear } = useLoginInfoStore((state) => state); 144 | 145 | return ( 146 |
156 |
157 | 158 | 172 | e.preventDefault()}> 173 | 174 | 175 | 176 |
177 |
178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /template-react-ts/src/mainLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Layout } from "antd"; 3 | import { Outlet } from "react-router-dom"; 4 | import { fromEvent, throttleTime } from "rxjs"; 5 | import Sider from "antd/es/layout/Sider"; 6 | import { dsc } from "../core/style/defaultStyleConfig"; 7 | import { Logo, MenuComp, ToolBar } from "./MainLayoutComp"; 8 | 9 | export function MainLayout() { 10 | const [collapsed, setCollapsed] = useState(false); 11 | const [menuHeight, setMenuHeight] = useState(document.documentElement.clientHeight); 12 | const defaultMenuTitleHeight = 64; 13 | 14 | useEffect(() => { 15 | const subscription = fromEvent(window, "resize") 16 | .pipe(throttleTime(1000)) 17 | .subscribe(() => { 18 | const timeoutId = globalThis.setTimeout(() => { 19 | setMenuHeight(document.documentElement.clientHeight); 20 | }, 100); 21 | return () => { 22 | globalThis.clearTimeout(timeoutId); 23 | }; 24 | }); 25 | 26 | return () => { 27 | subscription.unsubscribe(); 28 | }; 29 | }, []); 30 | 31 | return ( 32 | 33 | 41 | 42 | 43 | 44 | 45 | 46 |
55 | 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /template-react-ts/src/mainLayout/routes.tsx: -------------------------------------------------------------------------------- 1 | import { MainLayout } from "./index"; 2 | import { pagesRoutes } from "../pages/routes"; 3 | 4 | export const mainLayoutPath = "main"; 5 | 6 | export const mainRoutes = { 7 | path: mainLayoutPath, 8 | element: , 9 | children: pagesRoutes, 10 | }; 11 | -------------------------------------------------------------------------------- /template-react-ts/src/mainLayout/utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Dictionary } from "react-router-toolkit"; 3 | import { map, startsWith } from "lodash-es"; 4 | import { MenuProps } from "antd"; 5 | import { Link, RouteObject } from "react-router-dom"; 6 | 7 | type MenuItem = Required["items"][number]; 8 | 9 | function getItem(props: { 10 | label: React.ReactNode; 11 | key: React.Key; 12 | icon?: React.ReactNode; 13 | children?: MenuItem[]; 14 | type?: "group"; 15 | }): MenuItem { 16 | return props; 17 | } 18 | 19 | export const getMenus = ({ 20 | routes, 21 | modulePathToIconMap = {}, 22 | to = "", 23 | }: { 24 | routes: RouteObject[]; 25 | modulePathToIconMap?: Dictionary; 26 | to?: string; 27 | }) => { 28 | return map(routes, (item): MenuItem => { 29 | if (item?.children?.length) { 30 | const baseRoutePath = `${startsWith(to, "/") ? to : `/${to}`}/${item.path}`; 31 | 32 | return getItem({ 33 | key: baseRoutePath, 34 | label: item.id || "", 35 | icon: item.path && modulePathToIconMap?.[item.path], 36 | children: item.children ? getMenus({ routes: item.children, to: baseRoutePath }) : undefined, 37 | }); 38 | } 39 | 40 | const routePath = `${startsWith(to, "/") ? to : `/${to}`}/${item.path}`; 41 | 42 | return getItem({ 43 | key: routePath, 44 | label: {item.id}, 45 | icon: item.path && modulePathToIconMap?.[item.path], 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "antd"; 2 | import { useState } from "react"; 3 | import { useErrorBoundary } from "react-error-boundary"; 4 | 5 | export default function Dashboard() { 6 | const [obj, setObj] = useState({ a: { b: "no error! " } }); // eslint-disable-line @typescript-eslint/no-explicit-any 7 | const { showBoundary } = useErrorBoundary(); 8 | 9 | return ( 10 |
11 | this is dashboard 12 |
{obj.a.b}
13 |
14 | 23 |
24 |
25 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/dashboard/routes.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | const Dashboard = lazy(() => import(".")); 3 | 4 | export const dashboardModuleName = "dashboard"; 5 | 6 | export const dashboardRoutes = { 7 | path: dashboardModuleName, 8 | id: "面板", 9 | element: , 10 | }; 11 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/routes.tsx: -------------------------------------------------------------------------------- 1 | import { dashboardRoutes } from "./dashboard/routes"; 2 | import { uiListRoutes } from "./ui-list/routes"; 3 | import { utilListRoutes } from "./util-list/routes"; 4 | 5 | export const pagesRoutes = [dashboardRoutes, uiListRoutes, utilListRoutes]; 6 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/ui-list/UIOne.tsx: -------------------------------------------------------------------------------- 1 | import { IconDown } from "../../mainLayout/MainLayoutComp"; 2 | 3 | export function UIOne() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/ui-list/routes.tsx: -------------------------------------------------------------------------------- 1 | import { UIOne } from "./UIOne"; 2 | 3 | export const uiListModuleName = "ui-list"; 4 | export const uiListModuleNameDefaultPath = "icon"; 5 | 6 | export const uiListRoutes = { 7 | path: uiListModuleName, 8 | id: "UI组件", 9 | children: [ 10 | { 11 | path: uiListModuleNameDefaultPath, 12 | id: "图标", 13 | element: , 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/util-list/RequestDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "antd"; 2 | import ReactMarkdown from "react-markdown"; 3 | 4 | export function RequestDemo() { 5 | const getRequestDemo = ` 6 | ~~~js 7 | // HelloGet是一个基于axios的promise请求 8 | export async function HelloGet( 9 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 10 | params: Api.HelloGetParams, 11 | options?: { [key: string]: any }, 12 | ) { 13 | return request('/gin-demo-server/api/v1/hello', { 14 | method: 'GET', 15 | params: { 16 | ...params, 17 | }, 18 | ...(options || {}), 19 | }); 20 | } 21 | 22 | // 自动调用接口获取数据 23 | const { data, isLoading } = useQuery({ 24 | queryKey: ["hello", name], 25 | queryFn: () => { 26 | return HelloGet({ name: name }); 27 | }, 28 | }); 29 | 30 | // dom 31 | 32 | {data?.data} 33 | 34 | ~~~ 35 | `; 36 | 37 | const postRequestDemo = ` 38 | ~~~js 39 | export async function HelloPost(body: Api.HelloPostParam, options?: { [key: string]: any }) { 40 | return request('/gin-demo-server/api/v1/hello', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | data: body, 46 | ...(options || {}), 47 | }); 48 | } 49 | 50 | // 提交编辑数据 51 | const { mutate, isLoading } = useMutation({ 52 | mutationFn: HelloPost, 53 | onSuccess(data) { 54 | setName(data?.data || ""); 55 | }, 56 | onError() { 57 | // 清除queryKey为hello的接口数据缓存,自动重新获取接口数据 58 | queryClient.invalidateQueries({ queryKey: ['hello'] }); 59 | } 60 | }) 61 | 62 | mutate({ name: "lisi" }); 63 | ~~~ 64 | `; 65 | 66 | const requestDemo = ` 67 | ~~~js 68 | // 自动调用接口获取数据 69 | const { data, isLoading } = useQuery({ 70 | queryKey: ["hello", name], 71 | queryFn: () => { 72 | return HelloGet({ name: name }); 73 | }, 74 | }); 75 | 76 | // 提交编辑数据 77 | const { mutate, isLoading } = useMutation({ 78 | mutationFn: HelloPost, 79 | onSuccess(data) { 80 | setName(data?.data || ""); 81 | }, 82 | onError() { 83 | // 清除queryKey为hello的接口数据缓存,自动重新获取接口数据 84 | queryClient.invalidateQueries({ queryKey: ['hello'] }); 85 | } 86 | }) 87 | 88 | mutate({ name: "lisi" }); 89 | ~~~ 90 | `; 91 | 92 | return ( 93 |
* + *": { marginTop: 12 } }}> 94 | 95 | {getRequestDemo} 96 | 97 | 98 | {postRequestDemo} 99 | 100 | 101 | {requestDemo} 102 | 103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/util-list/RouterQueryDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "antd"; 2 | import ReactMarkdown from "react-markdown"; 3 | 4 | export function RouterQueryDemo() { 5 | const routerAuthDemo = ` 6 | ~~~js 7 | export const mainRoutes: RouteObject = { 8 | path: mainLayoutPath, 9 | element: ( 10 | 11 |
12 | 13 | ), 14 | children: pagesRoutes, 15 | }; 16 | 17 | function ShouldLogon({ children }: { children: ReactNode }) { 18 | const loginInfoStorageStr = globalThis.localStorage.getItem(loginInfoStorageKey); 19 | 20 | if (!loginInfoStorageStr) { 21 | return ; 22 | } 23 | 24 | const loginInfo = (JSON.parse(loginInfoStorageStr) as ILoginInfoStorageState).state.loginInfo; 25 | 26 | if (!loginInfo || !loginInfo.expireAt || dayjs().isAfter(dayjs(loginInfo.expireAt))) { 27 | return ; 28 | } 29 | 30 | return children; 31 | } 32 | ~~~ 33 | `; 34 | 35 | return ( 36 |
* + *": { marginTop: 12 } }}> 37 | 38 | window.open("https://www.npmjs.com/package/react-router-toolkit")}>查看文档 39 | 40 | 41 | {routerAuthDemo} 42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /template-react-ts/src/pages/util-list/routes.tsx: -------------------------------------------------------------------------------- 1 | import { RequestDemo } from "./RequestDemo"; 2 | import { RouterQueryDemo } from "./RouterQueryDemo"; 3 | 4 | export const utilListModuleName = "util-list"; 5 | export const utilListRoutes = { 6 | path: utilListModuleName, 7 | id: "工具", 8 | children: [ 9 | { 10 | path: "request", 11 | id: "请求示例", 12 | element: , 13 | }, 14 | { 15 | path: "router-toolkit", 16 | id: "路由工具箱", 17 | element: , 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /template-react-ts/src/rootRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, RouteObject } from "react-router-dom"; 2 | import { loginRoutes } from "./login/routes"; 3 | import { mainRoutes } from "./mainLayout/routes"; 4 | import { ErrorBoundaryWrapOutlet } from "./core/error-boundary"; 5 | 6 | function getAppRoutes() { 7 | return [ 8 | { 9 | path: "/", 10 | element: , 11 | children: [ 12 | { 13 | index: true, 14 | element: , 15 | }, 16 | loginRoutes, 17 | mainRoutes, 18 | ], 19 | }, 20 | ] as RouteObject[]; 21 | } 22 | 23 | export const appRoutes = getAppRoutes(); 24 | -------------------------------------------------------------------------------- /template-react-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // 变量定义参考/src/config.ts 4 | interface ImportMetaEnv { 5 | readonly VITE_appName: string; 6 | readonly VITE_baseURL: string; 7 | readonly VITE_env?: string; 8 | readonly VITE_version?: string; 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv; 13 | } 14 | -------------------------------------------------------------------------------- /template-react-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["@emotion/react/types/css-prop"], 4 | "paths": { 5 | "@request": ["./src/core/http/request.ts"] 6 | }, 7 | "target": "ES2020", 8 | "useDefineForClassFields": true, 9 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | /* Module Resolution Options */ 28 | // "baseUrl": ".", 29 | 30 | /* Source Map Options */ 31 | "sourceMap": false 32 | }, 33 | // "include": ["src"], 34 | "exclude": ["node_modules"], 35 | "references": [{ "path": "./tsconfig.node.json" }], 36 | "ts-node": { 37 | "esm": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /template-react-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /template-react-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | react({ 8 | babel: { 9 | presets: [["@babel/preset-react", { runtime: "automatic", importSource: "@emotion/react" }]], 10 | plugins: ["@emotion/babel-plugin"], 11 | babelrc: false, 12 | configFile: false, 13 | }, 14 | }), 15 | ], 16 | resolve: { 17 | alias: { 18 | "@request": "../core/http/request.ts", 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /template-vue-ts/.env.production: -------------------------------------------------------------------------------- 1 | # .env.production, vite在build时会自动打包到import.meta.env对象下 2 | VITE_appName=webapp-react 3 | # host需要替换为真实host 4 | VITE_baseURL=https://host 5 | VITE_env=demo 6 | VITE_version="" -------------------------------------------------------------------------------- /template-vue-ts/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:vue/vue3-essential"], 8 | overrides: [ 9 | { 10 | env: { 11 | node: true, 12 | }, 13 | files: [".eslintrc.{js,cjs}"], 14 | parserOptions: { 15 | sourceType: "script", 16 | }, 17 | }, 18 | ], 19 | parserOptions: { 20 | ecmaVersion: "latest", 21 | parser: "@typescript-eslint/parser", 22 | sourceType: "module", 23 | }, 24 | plugins: ["@typescript-eslint", "vue"], 25 | rules: { 26 | "@typescript-eslint/ban-ts-comment": "off", 27 | "vue/multi-word-component-names": [ 28 | "error", 29 | { 30 | ignores: ["index"], //需要忽略的组件名 31 | }, 32 | ], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /template-vue-ts/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /template-vue-ts/.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm install 5 | -------------------------------------------------------------------------------- /template-vue-ts/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /template-vue-ts/.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # pnpm test 5 | -------------------------------------------------------------------------------- /template-vue-ts/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /template-vue-ts/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | FROM --platform=${BUILDPLATFORM:-linux/amd64} node:17-buster AS builder 3 | 4 | ENV PNPM_HOME="/pnpm" 5 | ENV PATH="$PNPM_HOME:$PATH" 6 | RUN corepack enable 7 | 8 | WORKDIR /src 9 | COPY ./ /src 10 | 11 | RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \ 12 | --mount=type=cache,target=/pnpm/store,id=pnpm_cache \ 13 | pnpm install 14 | 15 | RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \ 16 | pnpm run build 17 | 18 | FROM ghcr.io/zboyco/webrunner:0.0.8 19 | 20 | COPY --from=builder /src/dist /app -------------------------------------------------------------------------------- /template-vue-ts/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite + Pnpm + Pinia + Openapi + Docker 2 | 3 | 该模板可以帮助您在 Vite 中使用 Vue 3 和 TypeScript 进行开发. 该模板使用 Vue 3 ` 14 | 15 | 16 | -------------------------------------------------------------------------------- /template-vue-ts/makefile: -------------------------------------------------------------------------------- 1 | PKG = $(shell cat package.json | grep 'name' | sed -e 's/ "name": "//g' -e 's/",//g') 2 | VERSION = $(shell cat package.json | grep 'version' | sed -e 's/ "version": "//g' -e 's/",//g') 3 | 4 | docker-build: 5 | docker build . -t $(PKG):v$(VERSION) 6 | 7 | docker-run: 8 | docker run -d -p 80:80 $(PKG):v$(VERSION) 9 | 10 | docker-build-run: docker-build docker-run 11 | 12 | install: 13 | pnpm install 14 | 15 | dev: 16 | npm run dev 17 | 18 | build: 19 | npm run build 20 | 21 | preview: 22 | npm run preview 23 | 24 | lint: 25 | npm run lint 26 | 27 | lintfix: 28 | npm run lintfix 29 | 30 | prepare: 31 | npm run prepare 32 | 33 | openapi: 34 | npm run openapi -------------------------------------------------------------------------------- /template-vue-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp-vue", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview", 10 | "prepare": "husky install", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "lintfix": "eslint . --ext ts,tsx --fix", 13 | "openapi": "ts-node './src/core/openapi/index.ts'" 14 | }, 15 | "dependencies": { 16 | "@tanstack/vue-query": "^5.7.3", 17 | "axios": "^1.6.0", 18 | "element-plus": "^2.4.1", 19 | "lodash-es": "^4.17.21", 20 | "pinia": "^2.1.7", 21 | "pinia-plugin-persistedstate": "^3.2.0", 22 | "vue": "^3.3.4", 23 | "vue-router": "^4.2.5" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^18.2.0", 27 | "@commitlint/config-conventional": "^18.1.0", 28 | "@types/lodash-es": "^4.17.10", 29 | "@types/node": "^20.8.10", 30 | "@typescript-eslint/eslint-plugin": "^6.9.1", 31 | "@typescript-eslint/parser": "^6.9.1", 32 | "@umijs/openapi": "^1.9.1", 33 | "@vitejs/plugin-vue": "^4.2.3", 34 | "eslint": "^8.52.0", 35 | "eslint-plugin-vue": "^9.18.1", 36 | "husky": "^8.0.3", 37 | "lint-staged": "^15.0.2", 38 | "prettier": "^3.0.3", 39 | "ts-node": "^10.9.1", 40 | "typescript": "^5.0.2", 41 | "unplugin-auto-import": "^0.16.7", 42 | "unplugin-vue-components": "^0.25.2", 43 | "vite": "^4.4.5", 44 | "vue-tsc": "^1.8.5" 45 | }, 46 | "commitlint": { 47 | "extends": [ 48 | "@commitlint/config-conventional" 49 | ], 50 | "rules": { 51 | "type-enum": [ 52 | 2, 53 | "always", 54 | [ 55 | "build", 56 | "chore", 57 | "ci", 58 | "docs", 59 | "feat", 60 | "fix", 61 | "perf", 62 | "refactor", 63 | "revert", 64 | "style", 65 | "test" 66 | ] 67 | ] 68 | } 69 | }, 70 | "lint-staged": { 71 | "*.(ts|vue|cjs)": [ 72 | "eslint --quiet" 73 | ], 74 | "*.(ts|vue|json|html|cjs)": [ 75 | "prettier --write" 76 | ] 77 | }, 78 | "prettier": { 79 | "arrowParens": "always", 80 | "printWidth": 120, 81 | "singleQuote": true, 82 | "jsxSingleQuote": true 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /template-vue-ts/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template-vue-ts/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /template-vue-ts/src/api/hello.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import request from "@request"; 4 | 5 | /** HelloGet GET /demo-docker/api/v1/hello */ 6 | export async function HelloGet( 7 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 8 | params: Api.HelloGetParams, 9 | options?: { [key: string]: any }, 10 | ) { 11 | return request("/demo-docker/api/v1/hello", { 12 | method: "GET", 13 | params: { 14 | ...params, 15 | }, 16 | ...(options || {}), 17 | }); 18 | } 19 | 20 | /** HelloPost POST /demo-docker/api/v1/hello */ 21 | export async function HelloPost(body: Api.HelloPostParam, options?: { [key: string]: any }) { 22 | return request("/demo-docker/api/v1/hello", { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | }, 27 | data: body, 28 | ...(options || {}), 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /template-vue-ts/src/api/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | // API 更新时间: 4 | // API 唯一标识: 5 | import * as hello from "./hello"; 6 | import * as user from "./user"; 7 | export default { 8 | hello, 9 | user, 10 | }; 11 | -------------------------------------------------------------------------------- /template-vue-ts/src/api/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Api { 2 | type CreateOrUpdateUserBody = { 3 | /** 手机号 */ 4 | mobile?: string; 5 | /** 用户名 */ 6 | name?: string; 7 | }; 8 | 9 | type ErrorResp = { 10 | error?: string; 11 | }; 12 | 13 | type HelloGetParams = { 14 | /** Name */ 15 | name: string; 16 | }; 17 | 18 | type HelloPostParam = { 19 | name?: string; 20 | }; 21 | 22 | type HelloResp = { 23 | /** 结果 */ 24 | data?: string; 25 | }; 26 | 27 | type ListUserParams = { 28 | /** 手机号 */ 29 | mobile?: string; 30 | /** 姓名 */ 31 | name?: string; 32 | /** 页数1开始 */ 33 | pageOffset?: number; 34 | /** 每页数量 */ 35 | pageSize?: number; 36 | }; 37 | 38 | type ListUserResp = { 39 | data?: User[]; 40 | total?: number; 41 | }; 42 | 43 | type MobileType = string; 44 | 45 | type User = { 46 | createdAt?: string; 47 | key?: number; 48 | /** 电话 */ 49 | mobile?: string; 50 | /** 电话运营商 */ 51 | mobileType?: MobileType; 52 | /** 昵称 */ 53 | nickName?: string; 54 | updatedAt?: string; 55 | userID?: number; 56 | /** 用户角色 */ 57 | userType?: UserType; 58 | }; 59 | 60 | type UserType = "ADMIN" | "USER"; 61 | } 62 | -------------------------------------------------------------------------------- /template-vue-ts/src/api/user.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import request from "@request"; 4 | 5 | /** 获取用户 GET /demo-docker/api/v1/users */ 6 | export async function ListUser( 7 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 8 | params: Api.ListUserParams, 9 | options?: { [key: string]: any }, 10 | ) { 11 | return request("/demo-docker/api/v1/users", { 12 | method: "GET", 13 | params: { 14 | ...params, 15 | }, 16 | ...(options || {}), 17 | }); 18 | } 19 | 20 | /** 创建用户 POST /demo-docker/api/v1/users */ 21 | export async function CreateOrUpdateUser(body: Api.CreateOrUpdateUserBody, options?: { [key: string]: any }) { 22 | return request("/demo-docker/api/v1/users", { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | }, 27 | data: body, 28 | ...(options || {}), 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /template-vue-ts/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template-vue-ts/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /template-vue-ts/src/config.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | appName: string; 3 | baseURL: string; 4 | version?: string; 5 | env?: string; 6 | } 7 | 8 | // 一级path, 例如:openapi 9 | export const urlPath = "openapi"; 10 | 11 | // 项目基本变量配置 12 | const appConfig: IConfig = { 13 | // 应用名称, 例如:webapp-react 14 | appName: "webapp-vue", 15 | // 网络请求的域名,例如:https://host 16 | baseURL: "https://srv-demo-docker.onrender.com", 17 | // 发布版本,例如:0000000-0.0.1 18 | version: "", 19 | // 代码环境,例如:demo 20 | env: "demo", 21 | }; 22 | 23 | export default appConfig; 24 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/http/config.ts: -------------------------------------------------------------------------------- 1 | import appConfig, { IConfig } from "../../config"; 2 | 3 | export function getConfig(): IConfig { 4 | const mateEnv = import.meta.env; 5 | const defaultAppConfig = { 6 | appName: mateEnv?.VITE_appName || "", 7 | baseURL: mateEnv?.VITE_baseURL || "", 8 | version: mateEnv?.VITE_version || "", 9 | env: mateEnv?.VITE_env || "", 10 | }; 11 | console.log("metaEnv", import.meta.env); 12 | 13 | // 本地开发环境直接从根目录config文件读取, ci环境直接从mate标签读取, 通过容器环境变量写入html的mate标签 14 | // mate标签name为:app_config, content格式为:appName=webapp,baseURL=https://api.com,env=,version= 15 | if (import.meta.env.DEV) { 16 | return appConfig; 17 | } else { 18 | const appConfigStr = getMeta("app_config"); 19 | 20 | if (!appConfigStr) return defaultAppConfig; 21 | 22 | return parseEnvVar(appConfigStr); 23 | } 24 | } 25 | 26 | function getMeta(metaName: string) { 27 | const metas = document.getElementsByTagName("meta"); 28 | 29 | for (let i = 0; i < metas.length; i++) { 30 | if (metas[i].getAttribute("name") === metaName) { 31 | return metas[i].getAttribute("content"); 32 | } 33 | } 34 | 35 | return ""; 36 | } 37 | 38 | function parseEnvVar(envVarURL: string) { 39 | const arrs = envVarURL.split(","); 40 | 41 | return arrs.reduce((pre, item) => { 42 | const keyValues = item.split("="); 43 | 44 | return { 45 | ...pre, 46 | [keyValues[0]]: keyValues[1], 47 | }; 48 | }, {} as IConfig); 49 | } 50 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/http/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | // import { ILoginInfoStorageState, defaultLoginInfoStorage, loginInfoStorageKey } from "../store"; 3 | import { getConfig } from "./config"; 4 | import { IUserInfo, defaultUserInfo, userInfoStorageKey } from "../store/user"; 5 | 6 | const BASE_URL = getConfig().baseURL; 7 | 8 | const instance = axios.create({ 9 | baseURL: BASE_URL, 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | timeout: 60000, // 超时时间60秒 14 | }); 15 | 16 | instance.interceptors.response.use((response) => { 17 | // 统一错误处理 18 | // data解构 19 | if (response.data) { 20 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 21 | return response.data; 22 | } 23 | return response; 24 | }); 25 | 26 | instance.interceptors.request.use((config) => { 27 | const userInfoStorageStr = globalThis.localStorage.getItem(userInfoStorageKey); 28 | const userInfo = userInfoStorageStr ? (JSON.parse(userInfoStorageStr).userInfo as IUserInfo) : defaultUserInfo; 29 | 30 | if (userInfo.accessToken) { 31 | config.headers.Authorization = userInfo.accessToken; 32 | } 33 | 34 | return config; 35 | }); 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | const request = async (url: string, options: AxiosRequestConfig & { requestType?: "json" | "form" } = {}) => { 39 | // 兼容from data文件上传的情况 40 | const { requestType, ...rest } = options; 41 | if (requestType === "form") { 42 | return await instance.request({ 43 | url, 44 | ...rest, 45 | headers: { 46 | ...(rest.headers || {}), 47 | "Content-Type": "multipart/form-data", 48 | }, 49 | }); 50 | } else { 51 | return await instance.request({ 52 | url, 53 | ...rest, 54 | }); 55 | } 56 | }; 57 | 58 | export default request; 59 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/http/vueQueryConfig.ts: -------------------------------------------------------------------------------- 1 | import { VueQueryPluginOptions } from "@tanstack/vue-query"; 2 | 3 | export const vueQueryPluginOptions: VueQueryPluginOptions = { 4 | queryClientConfig: { 5 | defaultOptions: { 6 | queries: { 7 | refetchOnWindowFocus: false, 8 | staleTime: 60 * 60 * 1000, 9 | retry: false, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/openapi/index.ts: -------------------------------------------------------------------------------- 1 | import { generateService } from "@umijs/openapi"; 2 | import appConfig, { urlPath } from "../../config.ts"; 3 | 4 | generateService({ 5 | // openapi地址 6 | schemaPath: `${appConfig.baseURL}/${urlPath}`, 7 | // 文件生成目录 8 | serversPath: "./src", 9 | // 自定义网络请求函数路径 10 | requestImportStatement: 'import request from "@request"', 11 | // 代码组织命名空间, 例如:Api 12 | namespace: "Api", 13 | }); 14 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import { routes } from "../../routes"; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(), 6 | routes, 7 | }); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from "pinia"; 2 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 3 | 4 | const store = createPinia(); 5 | store.use(piniaPluginPersistedstate); 6 | 7 | export default store; 8 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/store/project.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | 4 | interface IProjectInfo { 5 | projectId: string; 6 | projectName: string; 7 | } 8 | 9 | export const useProjectInfoStore = defineStore( 10 | "projectInfo", 11 | () => { 12 | const projectInfo = ref(null); 13 | 14 | function updateProjectInfo(newState: IProjectInfo) { 15 | projectInfo.value = Object.assign(projectInfo.value || {}, newState); 16 | } 17 | 18 | return { 19 | projectInfo, 20 | updateProjectInfo, 21 | }; 22 | }, 23 | { persist: true }, 24 | ); 25 | -------------------------------------------------------------------------------- /template-vue-ts/src/core/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | 4 | export interface IUserInfo { 5 | accessToken: string; 6 | name?: string; 7 | uid?: string; 8 | refreshToken?: string; 9 | expireAt?: string; 10 | expires_in?: number; 11 | } 12 | 13 | export const userInfoStorageKey = "userInfo-storage"; 14 | 15 | export const useUserInfoStore = defineStore( 16 | userInfoStorageKey, 17 | () => { 18 | const userInfo = ref(null); 19 | 20 | function updateUserInfo(newState: IUserInfo) { 21 | userInfo.value = Object.assign(userInfo.value || {}, newState); 22 | } 23 | 24 | return { 25 | userInfo, 26 | updateUserInfo, 27 | }; 28 | }, 29 | { persist: true }, 30 | ); 31 | 32 | export const defaultUserInfo = { 33 | accessToken: "123456", 34 | name: "张三", 35 | uid: "1", 36 | refreshToken: "456789", 37 | expireAt: "2023-12-30", 38 | expires_in: 1703865600, 39 | }; 40 | -------------------------------------------------------------------------------- /template-vue-ts/src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /template-vue-ts/src/layouts/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | import MainLayout from "./MainLayout.vue"; 3 | import { viewsRoutes } from "../views/routes"; 4 | 5 | export const mainRoutes: RouteRecordRaw = { 6 | path: "/main", 7 | name: "Main", 8 | component: MainLayout, 9 | children: viewsRoutes, 10 | }; 11 | -------------------------------------------------------------------------------- /template-vue-ts/src/login/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 57 | 58 | 69 | -------------------------------------------------------------------------------- /template-vue-ts/src/login/routes.ts: -------------------------------------------------------------------------------- 1 | export const loginRoutes = { 2 | path: "/login", 3 | name: "Login", 4 | component: () => import("./index.vue"), 5 | meta: { 6 | title: "登录页", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /template-vue-ts/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./style.css"; 3 | import App from "./App.vue"; 4 | import store from "./core/store"; 5 | import router from "./core/router"; 6 | import { VueQueryPlugin } from "@tanstack/vue-query"; 7 | import { vueQueryPluginOptions } from "./core/http/vueQueryConfig"; 8 | 9 | const app = createApp(App); 10 | 11 | app.use(store).use(router).use(VueQueryPlugin, vueQueryPluginOptions); 12 | 13 | app.mount("#app"); 14 | -------------------------------------------------------------------------------- /template-vue-ts/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | import { loginRoutes } from "./login/routes"; 3 | import { mainRoutes } from "./layouts/routes"; 4 | 5 | export const routes: RouteRecordRaw[] = [ 6 | { 7 | path: "/", 8 | redirect: "/login", 9 | }, 10 | loginRoutes, 11 | mainRoutes, 12 | ]; 13 | -------------------------------------------------------------------------------- /template-vue-ts/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | font-synthesis: none; 7 | text-rendering: optimizeLegibility; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-text-size-adjust: 100%; 11 | } 12 | 13 | body { 14 | min-height: 100vh; 15 | margin: 0; 16 | } -------------------------------------------------------------------------------- /template-vue-ts/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 30 | -------------------------------------------------------------------------------- /template-vue-ts/src/views/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | 3 | export const viewsRoutes: RouteRecordRaw[] = [ 4 | { 5 | path: "dashboard", 6 | name: "Dashboard", 7 | component: () => import("./dashboard/index.vue"), 8 | meta: { 9 | title: "主页", 10 | svgIcon: "dashboard", 11 | affix: true, 12 | }, 13 | }, 14 | { 15 | path: "ui-list", 16 | name: "UIList", 17 | children: [ 18 | { 19 | path: "ui-one", 20 | name: "UIOne", 21 | component: () => import("./ui-list/UIOne.vue"), 22 | }, 23 | { 24 | path: "ui-two", 25 | name: "UITwo", 26 | component: () => import("./ui-list/UITwo.vue"), 27 | }, 28 | ], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /template-vue-ts/src/views/ui-list/UIOne.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /template-vue-ts/src/views/ui-list/UITwo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /template-vue-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // 变量定义参考/src/config.ts 4 | interface ImportMetaEnv { 5 | readonly VITE_appName: string; 6 | readonly VITE_baseURL: string; 7 | readonly VITE_env?: string; 8 | readonly VITE_version?: string; 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv; 13 | } 14 | -------------------------------------------------------------------------------- /template-vue-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@request": ["./src/core/http/request.ts"] 5 | }, 6 | "target": "ES2020", 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 10 | "skipLibCheck": true, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "preserve", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 27 | "references": [{ "path": "./tsconfig.node.json" }], 28 | "ts-node": { 29 | "esm": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /template-vue-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /template-vue-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import AutoImport from "unplugin-auto-import/vite"; 4 | import Components from "unplugin-vue-components/vite"; 5 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | AutoImport({ 12 | resolvers: [ElementPlusResolver()], 13 | }), 14 | Components({ 15 | resolvers: [ElementPlusResolver()], 16 | }), 17 | ], 18 | resolve: { 19 | alias: { 20 | "@request": "../core/http/request.ts", 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["build.config.ts", "src", "__tests__"], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ES2022", 6 | "module": "ES2020", 7 | "moduleResolution": "bundler", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "declaration": false, 11 | "sourceMap": false, 12 | "noUnusedLocals": true, 13 | "esModuleInterop": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------