├── .eslintrc.json ├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── package_copy.json ├── shims-vue.d.ts ├── src ├── commands │ ├── contact │ │ └── index.ts │ └── create │ │ ├── index.ts │ │ ├── project_template │ │ ├── .env.development │ │ ├── .env.production │ │ ├── .eslintrc_js.cjs │ │ ├── .eslintrc_ts.cjs │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── env.d.ts │ │ ├── index.html │ │ ├── public │ │ │ └── image │ │ │ │ ├── bd.jpg │ │ │ │ └── star-squashed.jpg │ │ ├── src │ │ │ ├── App.scss │ │ │ ├── App.ts │ │ │ ├── App.vue │ │ │ ├── api │ │ │ │ └── user.ts │ │ │ ├── components │ │ │ │ ├── svgIcon │ │ │ │ │ ├── icon.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── index.vue │ │ │ │ └── table │ │ │ │ │ ├── table.ts │ │ │ │ │ └── table.vue │ │ │ ├── config │ │ │ │ └── white-list.ts │ │ │ ├── hooks │ │ │ │ ├── useI18.ts │ │ │ │ ├── userEcharts.ts │ │ │ │ └── userVModel.ts │ │ │ ├── icons │ │ │ │ └── svg │ │ │ │ │ ├── 404.svg │ │ │ │ │ ├── MagicStick.svg │ │ │ │ │ ├── a-businesscard.svg │ │ │ │ │ ├── a-chartqushitiaoyantongji.svg │ │ │ │ │ ├── account.svg │ │ │ │ │ ├── duoyuyan.svg │ │ │ │ │ ├── full-screen.svg │ │ │ │ │ ├── fullscreen-exit.svg │ │ │ │ │ ├── graph.svg │ │ │ │ │ ├── jiazai.svg │ │ │ │ │ ├── login-bg.svg │ │ │ │ │ ├── mima.svg │ │ │ │ │ └── money.svg │ │ │ ├── lang │ │ │ │ ├── en.json │ │ │ │ ├── lang.ts │ │ │ │ └── zh-CN.json │ │ │ ├── layouts │ │ │ │ ├── components │ │ │ │ │ ├── aside │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── menu-item.scss │ │ │ │ │ │ ├── menu-item.ts │ │ │ │ │ │ ├── menu-item.vue │ │ │ │ │ │ ├── menu.scss │ │ │ │ │ │ ├── menu.ts │ │ │ │ │ │ ├── menu.vue │ │ │ │ │ │ ├── subMenu.scss │ │ │ │ │ │ ├── subMenu.ts │ │ │ │ │ │ └── subMenu.vue │ │ │ │ │ ├── head │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── tab-bar.scss │ │ │ │ │ │ ├── tab-bar.ts │ │ │ │ │ │ └── tab-bar.vue │ │ │ │ │ └── main │ │ │ │ │ │ └── index.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── useLang.ts │ │ │ │ │ ├── useScreen.ts │ │ │ │ │ └── useTheme.ts │ │ │ │ ├── index.scss │ │ │ │ ├── index.ts │ │ │ │ ├── index.vue │ │ │ │ ├── lay.scss │ │ │ │ ├── lay.ts │ │ │ │ ├── lay.vue │ │ │ │ ├── leftModel.scss │ │ │ │ ├── leftModel.ts │ │ │ │ └── leftModel.vue │ │ │ ├── router │ │ │ │ ├── index.ts │ │ │ │ └── protector.ts │ │ │ ├── store │ │ │ │ ├── index.ts │ │ │ │ └── modules │ │ │ │ │ ├── system.ts │ │ │ │ │ └── user.ts │ │ │ ├── style.css │ │ │ ├── styles │ │ │ │ ├── dark.css │ │ │ │ ├── darkBlue.css │ │ │ │ ├── florid.css │ │ │ │ ├── index.scss │ │ │ │ ├── light.css │ │ │ │ └── normal.css │ │ │ ├── utils │ │ │ │ ├── index.ts │ │ │ │ ├── request.ts │ │ │ │ └── three │ │ │ │ │ ├── ModelThree.ts │ │ │ │ │ ├── SpriteThree.ts │ │ │ │ │ ├── Three.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── indexdb │ │ │ │ │ └── index.ts │ │ │ │ │ ├── interface │ │ │ │ │ └── index.ts │ │ │ │ │ └── utils │ │ │ │ │ └── index.ts │ │ │ └── views │ │ │ │ ├── error │ │ │ │ ├── 404.scss │ │ │ │ ├── 404.ts │ │ │ │ └── 404.vue │ │ │ │ ├── option_1 │ │ │ │ ├── a.scss │ │ │ │ ├── a.ts │ │ │ │ ├── a.vue │ │ │ │ ├── b.scss │ │ │ │ ├── b.ts │ │ │ │ ├── b.vue │ │ │ │ ├── c.scss │ │ │ │ ├── c.ts │ │ │ │ └── c.vue │ │ │ │ ├── resident │ │ │ │ ├── home │ │ │ │ │ ├── home.scss │ │ │ │ │ ├── home.ts │ │ │ │ │ └── home.vue │ │ │ │ └── login │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ └── sign-in.vue │ │ │ │ ├── system │ │ │ │ ├── router │ │ │ │ │ └── router.vue │ │ │ │ └── user │ │ │ │ │ ├── editUser.ts │ │ │ │ │ ├── editUser.vue │ │ │ │ │ ├── user.ts │ │ │ │ │ └── user.vue │ │ │ │ └── three │ │ │ │ ├── three.ts │ │ │ │ └── three.vue │ │ ├── tsconfig.json │ │ └── tsconfig.node.json │ │ └── utils │ │ ├── convertVue.ts │ │ ├── index.ts │ │ ├── main.ts │ │ ├── packageConfig.ts │ │ ├── uiReflection.ts │ │ └── viteConfig.ts ├── const │ └── index.ts ├── helpers │ ├── log.ts │ └── spinner.ts ├── index.ts └── utils │ └── index.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": {} 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | bd-admin是一款能根据需求快速配置vue后台管理的脚手架,内置使用vue3 + vue-router + pinia + axios 其他功能均可自定义。 4 | 5 | - 极简操作,安装就可使用 6 | - 轻装上阵,模块功能自己决定是否使用,可以快速修改为自己想要的模板。 7 | - 自定义技术栈 : vue3 +elementUI or vue3+Ant Design 由你搭配 8 | - 自定义后台管理功能模块:权限配置 or 多语言 or 动态换肤 项目功能由你选择 9 | - 代码规范可配置:自选是否在项目中应用eslint 和Prettier 10 | 11 | ## 下载 12 | 13 | ``` 14 | npm i bd-admin -g 15 | ``` 16 | 17 | ## 使用方法 18 | 19 | ``` 20 | bd-admin create 21 | ``` 22 | 23 | ## 固定功能 24 | 25 | - [x] 多主题:内置普通、黑暗、深蓝三种主题模式 26 | - [x] 权限管理:内置页面权限(动态路由)、按钮权限 27 | 28 | ## 功能 29 | 30 | - 框架技术自提 31 | - [x] 语言选择: typeScript or javaScript 32 | - [x] ui库选择 :element Plus or Ant Design 33 | - [x] css扩展语言选择: less or scss 34 | - [x] 代码规范:eslint 和Prettier 35 | - [x] 多语言:使用i18配置多语言 36 | - 框架模块自提 37 | - [x] echarts 38 | - [x] three.js 39 | 40 | ### 生成项目 目录结构 41 | 42 | ``` 43 | bd-admin 44 | ├─ .env.development # 开发环境 45 | ├─ .env.production # 生产环境 46 | ├─ .eslintrc.cjs # eslint 47 | ├─ README.md 48 | ├─ dist # 打包dist 49 | ├─ public # 静态资源 50 | ├─ src # 源码 51 | │ ├─ api # 接口请求 52 | │ ├─ components # 公共组件 53 | │ ├─ config # 全部配置 54 | │ └─ white-list.ts# 路由白名单 55 | │ ├─ layouts # 全局Layout 56 | │ ├─ lang # 国际化 57 | │ ├─ router # 路由 58 | │ ├─ store # 全局store管理 59 | │ ├─ styles # 全局样式 60 | │ ├─ utils # 全局公共方法 61 | │ └─ views # 所有业务页面 62 | ├─ tsconfig.json # ts 编译配置 63 | └─ vue.config.js # vue-cli 配置 64 | ``` 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // 文件复制 2 | import fs from "fs-extra"; 3 | import { join, extname } from "path"; 4 | const cwdUrl = process.cwd(); 5 | 6 | const getFileListAndIncludes = (startUrl, destUrl) => { 7 | return new Promise((res, rej) => { 8 | fs.readdir(startUrl, (err, files) => { 9 | if (err) throw err; 10 | const filePromiseAll = []; 11 | files.forEach((file) => { 12 | const filedir = join(startUrl, file); 13 | const stats = fs.statSync(filedir); 14 | const isFile = stats.isFile(); //是文件 15 | const isDir = stats.isDirectory(); //是文件夹 16 | if (isFile) { 17 | // 将tsconfig.json无法编译的文件复制到project_template 18 | if (extname(file) !== ".ts") { 19 | const copyToJs = destUrl.replace( 20 | "project_template_ts", 21 | "project_template", 22 | ); 23 | fs.ensureDirSync(copyToJs); // 确保文件夹存在 24 | filePromiseAll.push( 25 | fs.copyFile(join(startUrl, file), join(copyToJs, file)), 26 | ); 27 | } 28 | filePromiseAll.push( 29 | fs.copyFile(join(startUrl, file), join(destUrl, file)), 30 | ); 31 | } 32 | if (isDir) { 33 | fs.mkdir(join(destUrl, file)).then(() => { 34 | getFileListAndIncludes( 35 | join(startUrl, file), 36 | join(destUrl, file), 37 | ).then((filePromiseAll) => { 38 | Promise.all(filePromiseAll); 39 | }); 40 | }); 41 | } 42 | }); 43 | res(filePromiseAll); 44 | }); 45 | }); 46 | }; 47 | 48 | fs.readFile(join("./", "package.json"), function (err, data) { 49 | if (err) throw err; 50 | fs.writeFile(join("./", "build", "package.json"), data, function (err) { 51 | if (err) throw err; 52 | }); 53 | }); 54 | 55 | fs.readFile(join("./", "README.md"), function (err, data) { 56 | if (err) throw err; 57 | fs.writeFile(join("./", "build", "README.md"), data, function (err) { 58 | if (err) throw err; 59 | }); 60 | }); 61 | 62 | const destUrl = join( 63 | cwdUrl, 64 | "build", 65 | "commands", 66 | "create", 67 | "project_template_ts", 68 | ); 69 | 70 | // 复制ts文件 71 | fs.mkdir(destUrl).then(() => { 72 | getFileListAndIncludes( 73 | join(cwdUrl, "src", "commands", "create", "project_template"), 74 | destUrl, 75 | false, 76 | ).then((filePromiseAll) => { 77 | Promise.all(filePromiseAll); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bd-admin", 3 | "version": "1.0.12", 4 | "description": "一款能根据需求快速配置vue后台管理的脚手架", 5 | "main": "index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "bd": "npm run build & npm run dev", 9 | "dev": "npm link & npm run clear:test & npm run cli", 10 | "build": " npm run clear:build & tsc & node index.js ", 11 | "cli": "bd-admin-dev create test", 12 | "clear:test": "rimraf test", 13 | "clear:build": "rimraf build", 14 | "copy": "node index.js", 15 | "prettier": "prettier --write ." 16 | }, 17 | "bin": { 18 | "bd-admin": "./index.js", 19 | "bd-admin-dev": "./build/index.js" 20 | }, 21 | "keywords": [ 22 | "vue", 23 | "admin", 24 | "自动构建", 25 | "后台管理", 26 | "脚手架", 27 | "多版本" 28 | ], 29 | "author": { 30 | "name": "lhp", 31 | "email": "1374487808@qq.com", 32 | "url": "https://github.com/lhpCode/bd-admin" 33 | }, 34 | "bugs": "https://github.com/lhpCode/bd-admin/issues", 35 | "repository": "https://github.com/lhpCode/bd-admin", 36 | "homepage": "https://github.com/lhpCode/bd-admin", 37 | "license": "ISC", 38 | "devDependencies": { 39 | "eslint": "^8.57.0", 40 | "rimraf": "^5.0.5", 41 | "ts-node": "^10.9.2", 42 | "typescript": "^5.3.3" 43 | }, 44 | "dependencies": { 45 | "chalk": "^5.3.0", 46 | "commander": "^12.0.0", 47 | "ejs": "^3.1.9", 48 | "eslint-config-prettier": "^9.1.0", 49 | "eslint-plugin-prettier": "^5.1.3", 50 | "execa": "^8.0.1", 51 | "figlet": "^1.7.0", 52 | "fs-extra": "^11.2.0", 53 | "globby": "^14.0.1", 54 | "handlebars": "^4.7.8", 55 | "inquirer": "^9.2.15", 56 | "module-alias": "^2.2.3", 57 | "ora": "^8.0.1", 58 | "pacote": "^17.0.6", 59 | "vue": "^3.4.21" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /package_copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bd-admin", 3 | "version": "1.0.12", 4 | "description": "一款能根据需求快速配置vue后台管理的脚手架", 5 | "main": "index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "bd": "npm run build & npm run dev", 9 | "dev": "npm link & npm run clear:test & npm run cli", 10 | "build": " npm run clear:build && tsc && node index.js", 11 | "cli": "admin-cli-dev create test", 12 | "clear:test": "rimraf test", 13 | "clear:build": "rimraf build", 14 | "file": "node index.js", 15 | "prettier": "prettier --write ." 16 | }, 17 | "bin": { 18 | "admin-cli": "./index.js" 19 | }, 20 | "keywords": ["vue", "admin", "自动构建", "后台管理", "脚手架", "多版本"], 21 | "author": { 22 | "name": "lhp", 23 | "email": "1374487808@qq.com", 24 | "url": "https://github.com/lhpCode/bd-admin" 25 | }, 26 | "bugs": "https://github.com/lhpCode/bd-admin/issues", 27 | "repository": "https://github.com/lhpCode/bd-admin", 28 | "homepage": "https://github.com/lhpCode/bd-admin", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "eslint": "^8.57.0", 32 | "rimraf": "^5.0.5", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.3.3" 35 | }, 36 | "dependencies": { 37 | "chalk": "^5.3.0", 38 | "commander": "^12.0.0", 39 | "ejs": "^3.1.9", 40 | "eslint-config-prettier": "^9.1.0", 41 | "eslint-plugin-prettier": "^5.1.3", 42 | "execa": "^8.0.1", 43 | "figlet": "^1.7.0", 44 | "fs-extra": "^11.2.0", 45 | "globby": "^14.0.1", 46 | "handlebars": "^4.7.8", 47 | "inquirer": "^9.2.15", 48 | "module-alias": "^2.2.3", 49 | "ora": "^8.0.1", 50 | "pacote": "^17.0.6" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/contact/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | const action = () => { 3 | console.log(chalk.blue("你可以通过以下方式联系我")); 4 | console.log(chalk.blue("email:"), chalk.bold("1374487808@qq.com")); 5 | }; 6 | export default { 7 | command: "contact", 8 | description: "获取作者的联系方式", 9 | action: action, 10 | }; 11 | -------------------------------------------------------------------------------- /src/commands/create/index.ts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import fs from "fs-extra"; 3 | import chalk from "chalk"; 4 | import { join, dirname, basename, extname } from "path"; 5 | import { createFolder } from "../../utils/index.js"; 6 | import { questionsList } from "./utils/index.js"; 7 | import { ConvertVue } from "./utils/convertVue.js"; 8 | import { createViteConfig } from "./utils/viteConfig.js"; 9 | import { createPackage } from "./utils/packageConfig.js"; 10 | import { createMain } from "./utils/main.js"; 11 | import { fileURLToPath } from "url"; 12 | import spawn from "cross-spawn"; 13 | import ora from "ora"; 14 | const message = "项目创建中。。。"; 15 | const spinner = ora(message); 16 | 17 | const __filename = fileURLToPath(import.meta.url); 18 | const __dirname = dirname(__filename); 19 | 20 | const userQuestions = (projectName: string) => { 21 | inquirer 22 | .prompt(questionsList) 23 | .then(async (answers: any) => { 24 | spinner.start(); 25 | const { checkbox, modelCheckbox } = answers; 26 | const modelCheckboxList = ["echarts", "three"]; 27 | const checkboxList = ["eslint", "i18n"]; 28 | const checkboxObj = {}; 29 | checkboxList.forEach((key) => { 30 | checkboxObj[key] = checkbox.includes(key); 31 | }); 32 | modelCheckboxList.forEach((key) => { 33 | checkboxObj[key] = modelCheckbox.includes(key); 34 | }); 35 | 36 | await createTemplates({ ...answers, ...checkboxObj, projectName }); 37 | 38 | const child = spawn("npm", ["run", "prettier"]); 39 | 40 | spinner.color = "blue"; 41 | spinner.text = "格式化代码"; 42 | 43 | child.on("close", function (code) { 44 | if (code !== 0) { 45 | console.log("\n"); 46 | console.log(chalk.cyanBright("项目创建完毕")); 47 | console.log(chalk.cyanBright(`cd ${projectName}`)); 48 | console.log(chalk.cyanBright("npm install")); 49 | spinner.stop(); // 停止 50 | delEmptyFile(projectName); 51 | spinner.succeed("Loading succeed"); // 成功 ✔ 52 | } else { 53 | spinner.stop(); // 停止 54 | console.log(chalk.red("构建异常")); 55 | } 56 | }); 57 | }) 58 | .catch((error) => { 59 | spinner.stop(); // 停止 60 | if (error.isTtyError) { 61 | // Prompt couldn't be rendered in the current environment 62 | } else { 63 | // Something else went wrong 64 | } 65 | }); 66 | }; 67 | 68 | const delEmptyFile = (projectName: string) => { 69 | const path = join(process.cwd(), projectName); 70 | 71 | const getFiles = (getPath: string) => { 72 | fs.readdir(getPath, (err, files: string[]) => { 73 | if (err) throw err; 74 | files.forEach((file) => { 75 | const filedir = join(getPath, file); 76 | const stats = fs.statSync(filedir); 77 | const isDir = stats.isDirectory(); 78 | if (isDir) { 79 | fs.readdir(filedir, (cErr, cFiles: string[]) => { 80 | if (cErr) throw err; 81 | if (cFiles.length <= 0) { 82 | fs.rmdir(filedir); 83 | } else { 84 | getFiles(filedir); 85 | } 86 | }); 87 | } 88 | }); 89 | }); 90 | }; 91 | getFiles(path); 92 | }; 93 | 94 | const createTemplates = async (answers: any) => { 95 | const { variant, projectName } = answers; 96 | const templatePath = 97 | variant === "TypeScript" ? "project_template_ts" : "project_template"; 98 | // 从模板复制项目 99 | await projectCopy(templatePath, projectName, answers); 100 | }; 101 | 102 | const projectCopy = async ( 103 | templatePath: string, 104 | projectName: string, 105 | answers, 106 | ) => { 107 | const { variant, eslint } = answers; 108 | 109 | const files = await fs.readdir(join(__dirname, templatePath), { 110 | withFileTypes: true, 111 | }); 112 | const manualOperation = [ 113 | "src", 114 | "package.json", 115 | ".prettierrc", 116 | ".eslintrc_js.cjs", 117 | ".eslintrc_ts.cjs", 118 | "tsconfig.json", 119 | "tsconfig.node.json", 120 | "env.d.ts", 121 | ]; 122 | 123 | const model = { 124 | eslint: [".prettierrc"], 125 | ts: ["tsconfig.json", "tsconfig.node.json", "env.d.ts"], 126 | }; 127 | 128 | files.forEach((item: any) => { 129 | //兼容低版本 130 | const ItemPath = item.path 131 | ? item.path 132 | : join( 133 | process.cwd(), 134 | "build", 135 | "commands", 136 | "create", 137 | "project_template_ts", 138 | ); 139 | 140 | const path = join(ItemPath, item.name); 141 | const targetPath = join( 142 | process.cwd(), 143 | projectName, 144 | path.split(templatePath)[1], 145 | ); 146 | const fileName = item.name; 147 | // 处理src下文件 148 | if (fileName === "src") { 149 | fs.ensureDirSync(join(process.cwd(), projectName, "src")); 150 | readFileList(join(__dirname, templatePath, "src"), answers).then( 151 | (fileList: string[]) => {}, 152 | ); 153 | } 154 | // ts 155 | if (variant === "TypeScript" && model.ts.includes(fileName)) { 156 | fs.copy(path, targetPath, (err) => { 157 | if (err) throw err; 158 | }); 159 | } 160 | // eslint 161 | if (eslint) { 162 | if (fileName === ".prettierrc") { 163 | fs.copy(path, targetPath, (err) => { 164 | if (err) throw err; 165 | }); 166 | } 167 | if (variant === "TypeScript" && fileName === ".eslintrc_ts.cjs") { 168 | fs.copy( 169 | path, 170 | targetPath.replace(".eslintrc_ts", ".eslintrc"), 171 | (err) => { 172 | if (err) throw err; 173 | }, 174 | ); 175 | } 176 | if (variant !== "TypeScript" && fileName === ".eslintrc_js.cjs") { 177 | fs.copy( 178 | path, 179 | targetPath.replace(".eslintrc_js", ".eslintrc"), 180 | (err) => { 181 | if (err) throw err; 182 | }, 183 | ); 184 | } 185 | } 186 | 187 | // 其他文件自动复制 188 | if (manualOperation.includes(item.name)) return; 189 | fs.copy(path, targetPath, (err) => { 190 | if (err) throw err; 191 | }); 192 | }); 193 | 194 | const fileSuffix = variant === "TypeScript" ? "ts" : "js"; 195 | // 追加写入viteConfig文件 196 | const viteConfigData = createViteConfig(answers); 197 | fs.outputFileSync( 198 | join(process.cwd(), projectName, `vite.config.${fileSuffix}`), 199 | viteConfigData, 200 | ); 201 | // 追加写入package文件 202 | const createPackageData = createPackage(answers); 203 | fs.outputFileSync( 204 | join(process.cwd(), projectName, "package.json"), 205 | createPackageData, 206 | ); 207 | 208 | //追加main文件 209 | const mainData = createMain(answers); 210 | fs.outputFileSync( 211 | join(process.cwd(), projectName, "src", `main.${fileSuffix}`), 212 | mainData, 213 | ); 214 | }; 215 | 216 | const readFileList = async (path, answers) => { 217 | const { projectName, variant } = answers; 218 | const templatePath = 219 | variant === "TypeScript" ? "project_template_ts" : "project_template"; 220 | const files = await fs.readdir(path, { withFileTypes: true }); 221 | const fileList = []; 222 | // 将当前目录下文件写入 223 | fileWrite(path, files, projectName, templatePath, answers); 224 | // 获取目录 225 | for (const dirent of files) { 226 | const filedir = join(path, dirent.name); 227 | const targetPath = join( 228 | process.cwd(), 229 | projectName, 230 | filedir.split(templatePath)[1], 231 | ); 232 | if (dirent.isFile()) { 233 | // 处理文件 234 | fileList.push(filedir); 235 | } else if (dirent.isDirectory()) { 236 | fs.ensureDirSync(targetPath); 237 | // 递归读取子目录中的文件 238 | const subDirFiles = await readFileList(filedir, answers); 239 | fileList.push(...subDirFiles); 240 | } 241 | } 242 | // 当所有文件都被读取后,返回文件列表 243 | return fileList; 244 | }; 245 | 246 | const isCopyFile = (answers, filePath: string) => { 247 | const file = filePath.substring(filePath.indexOf("src"), filePath.length); 248 | const { variant } = answers; 249 | const filePostfix = variant === "TypeScript" ? ".ts" : ".js"; 250 | const modelFile = { 251 | echarts: [join("src", "hooks", `userEcharts${filePostfix}`)], 252 | i18n: [ 253 | join("src", "hooks", `useI18${filePostfix}`), 254 | join("src", "lang", "en.json"), 255 | join("src", "lang", "zh-CN.json"), 256 | join("src", "lang", `lang${filePostfix}`), 257 | join("src", "layouts", "hooks", `useLang${filePostfix}`), 258 | ], 259 | three: [ 260 | join("src", "utils", "three", `index${filePostfix}`), 261 | join("src", "utils", "three", `ModelThree${filePostfix}`), 262 | join("src", "utils", "three", `SpriteThree${filePostfix}`), 263 | join("src", "utils", "three", `Three${filePostfix}`), 264 | join("src", "utils", "three", "indexdb", `index${filePostfix}`), 265 | join("src", "utils", "three", "interface", `index${filePostfix}`), 266 | join("src", "utils", "three", "utils", `index${filePostfix}`), 267 | ], 268 | }; 269 | let flag = true; 270 | for (const [key, value] of Object.entries(modelFile)) { 271 | if (!value.includes(file)) continue; 272 | if (typeof answers[key] === "boolean" && !answers[key]) { 273 | flag = false; 274 | return false; 275 | } 276 | } 277 | return flag; 278 | }; 279 | 280 | // css文件处理 281 | const cssFile = (fileName, fromPath, toPath, answers) => { 282 | const { css } = answers; 283 | if (css === "less") { 284 | fs.copyFile(fromPath, toPath.replace("scss", "less")); 285 | } else { 286 | fs.copyFile(fromPath, toPath); 287 | } 288 | }; 289 | 290 | const fileWrite = (path, files, projectName, templatePath, answers) => { 291 | const fileList = files.filter((dirent) => dirent.isFile()); 292 | const vueFileNameList = fileList 293 | .filter((dirent) => extname(dirent.name) === ".vue") 294 | .map((item) => item.name.replace(extname(item.name), "")); 295 | 296 | fileList.forEach(() => {}); 297 | if (vueFileNameList.length === 0) { 298 | // 目录下没有vue文件 299 | fileList.forEach((dirent) => { 300 | const filedir = join(path, dirent.name); 301 | const targetPath = join( 302 | process.cwd(), 303 | projectName, 304 | filedir.split(templatePath)[1], 305 | ); 306 | // 根据模块判断是否需要复制 307 | if (isCopyFile(answers, targetPath)) { 308 | const [name, suffix] = dirent.name.split("."); 309 | if (suffix === "scss") { 310 | cssFile(dirent.name, filedir, targetPath, answers); 311 | } else { 312 | fs.copyFile(filedir, targetPath); 313 | } 314 | } 315 | }); 316 | } else { 317 | fileList.forEach(async (item) => { 318 | const filedir = join(path, item.name); 319 | const targetPath = join( 320 | process.cwd(), 321 | projectName, 322 | filedir.split(templatePath)[1], 323 | ); 324 | const fileName = item.name.replace(extname(item.name), ""); 325 | if (vueFileNameList.includes(fileName)) { 326 | // vue文件写入 327 | if (extname(item.name) === ".vue") { 328 | // 判断vue文件是否需要写入 329 | const flag = writeVueFile(answers, targetPath); 330 | if (flag) { 331 | const convertVue = new ConvertVue({ 332 | path, 333 | fileName, 334 | answers, 335 | model: ["hook_1"], 336 | }); 337 | const vueFile = await convertVue.init(); 338 | fs.outputFileSync(targetPath, vueFile); 339 | } 340 | } 341 | } else { 342 | // 其他文件复制 343 | // 根据模块判断是否需要复制 344 | if (isCopyFile(answers, targetPath)) { 345 | const [name, suffix] = item.name.split("."); 346 | if (suffix === "scss") { 347 | cssFile(item.name, filedir, targetPath, answers); 348 | } else { 349 | fs.copyFile(filedir, targetPath); 350 | } 351 | } 352 | } 353 | }); 354 | } 355 | }; 356 | 357 | const writeVueFile = (answers, filePath: string) => { 358 | const file = filePath.substring(filePath.indexOf("src"), filePath.length); 359 | const modelFile = { 360 | three: [join("src", "views", "three", `three.vue`)], 361 | }; 362 | let flag = true; 363 | for (const [key, value] of Object.entries(modelFile)) { 364 | if (!value.includes(file)) continue; 365 | if (typeof answers[key] === "boolean" && !answers[key]) { 366 | flag = false; 367 | return false; 368 | } 369 | } 370 | return flag; 371 | }; 372 | 373 | const action = (projectName: string) => { 374 | const cwdUrl = process.cwd(); 375 | createFolder(join(cwdUrl, projectName)) 376 | .then(() => { 377 | userQuestions(projectName); 378 | }) 379 | .catch(() => { 380 | console.log( 381 | chalk.red(`项目名可能已存在,请更换项目名或者删除文件夹${projectName}`), 382 | ); 383 | }); 384 | }; 385 | export default { 386 | command: "create ", 387 | description: "create a new project", 388 | action: action, 389 | }; 390 | -------------------------------------------------------------------------------- /src/commands/create/project_template/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境 2 | VITE_APP_BASE_API = '/api' 3 | VITE_BASE_API="https://mock.mengxuegu.com/mock/65fd4c17838cf807b819d872" -------------------------------------------------------------------------------- /src/commands/create/project_template/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | VITE_APP_BASE_API = '/api' 3 | VITE_BASE_API="http://192.168.1.32:3000" -------------------------------------------------------------------------------- /src/commands/create/project_template/.eslintrc_js.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | "plugin:vue/vue3-essential", 5 | "eslint:recommended", 6 | "@vue/eslint-config-prettier", 7 | ], 8 | env: { 9 | browser: true, 10 | node: true, 11 | es6: true, 12 | }, 13 | overrides: [ 14 | { 15 | env: { 16 | node: true, 17 | }, 18 | files: [".eslintrc.{js,cjs}"], 19 | parserOptions: { 20 | sourceType: "script", 21 | }, 22 | }, 23 | ], 24 | parserOptions: { 25 | ecmaVersion: 2020, 26 | sourceType: "module", 27 | jsxPragma: "React", 28 | ecmaFeatures: { 29 | jsx: true, 30 | tsx: true, 31 | }, 32 | }, 33 | plugins: ["vue", "prettier"], 34 | rules: { 35 | "vue/no-v-html": "off", 36 | "vue/require-default-prop": "off", 37 | "vue/require-explicit-emits": "off", 38 | "vue/multi-word-component-names": "off", 39 | "vue/no-v-model-argument": "off", 40 | "vue/no-multiple-template-root": "off", 41 | "vue/html-self-closing": [ 42 | "error", 43 | { 44 | html: { 45 | void: "always", 46 | normal: "always", 47 | component: "always", 48 | }, 49 | svg: "always", 50 | math: "always", 51 | }, 52 | ], 53 | "prettier/prettier": [ 54 | "error", 55 | { 56 | endOfLine: "auto", 57 | }, 58 | ], 59 | "no-alert": 0, //禁止使用alert confirm prompt 60 | "default-case": "warn", // 要求switch语句中有default分支 61 | eqeqeq: "warn", // 要求使用 === 和 !== 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/commands/create/project_template/.eslintrc_ts.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | }, 8 | extends: [ 9 | "plugin:vue/vue3-essential", 10 | "eslint:recommended", 11 | "@vue/typescript/recommended", 12 | "@vue/eslint-config-typescript", 13 | "prettier", 14 | "plugin:vue/essential", // 或者 'plugin:vue/recommended' 根据你的需要 15 | // 添加 Prettier 的配置 16 | "plugin:prettier/recommended", 17 | ], 18 | parser: "vue-eslint-parser", 19 | parserOptions: { 20 | parser: "@typescript-eslint/parser", 21 | ecmaVersion: 2020, 22 | sourceType: "module", 23 | jsxPragma: "React", 24 | ecmaFeatures: { 25 | jsx: true, 26 | tsx: true, 27 | }, 28 | }, 29 | plugins: ["vue", "prettier"], 30 | rules: { 31 | // TS 32 | "@typescript-eslint/no-explicit-any": "off", 33 | "no-debugger": "off", 34 | "@typescript-eslint/explicit-module-boundary-types": "off", 35 | "@typescript-eslint/ban-types": "off", 36 | "@typescript-eslint/ban-ts-comment": "off", 37 | "@typescript-eslint/no-empty-function": "off", 38 | "@typescript-eslint/no-non-null-assertion": "off", 39 | "vue/no-multiple-template-root": "off", 40 | "@typescript-eslint/no-unused-vars": [ 41 | "error", 42 | { 43 | argsIgnorePattern: "^_", 44 | varsIgnorePattern: "^_", 45 | }, 46 | ], 47 | "no-unused-vars": [ 48 | "error", 49 | { 50 | argsIgnorePattern: "^_", 51 | varsIgnorePattern: "^_", 52 | }, 53 | ], 54 | // Vue 55 | "vue/no-v-html": "off", 56 | "vue/require-default-prop": "off", 57 | "vue/require-explicit-emits": "off", 58 | "vue/multi-word-component-names": "off", 59 | "vue/no-v-model-argument": "off", 60 | "vue/html-self-closing": [ 61 | "error", 62 | { 63 | html: { 64 | void: "always", 65 | normal: "always", 66 | component: "always", 67 | }, 68 | svg: "always", 69 | math: "always", 70 | }, 71 | ], 72 | // Prettier 73 | "prettier/prettier": [ 74 | "error", 75 | { 76 | endOfLine: "auto", 77 | }, 78 | ], 79 | "no-alert": 0, //禁止使用alert confirm prompt 80 | "default-case": "warn", // 要求switch语句中有default分支 81 | eqeqeq: "warn", // 要求使用 === 和 !== 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /src/commands/create/project_template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/commands/create/project_template/.prettierrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhpCode/bd-admin/03bfc0e0531d869e48744425eb7980848c972892/src/commands/create/project_template/.prettierrc -------------------------------------------------------------------------------- /src/commands/create/project_template/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface key { 3 | [key: string]: any; 4 | } 5 | declare interface RouterRes { 6 | path: string; 7 | name: string; 8 | meta: key; 9 | component: string; 10 | children?: Router | undefined; 11 | } 12 | declare module "@types/three"; 13 | declare module "three/addons/loaders/GLTFLoader.js"; 14 | declare module "three/examples/jsm/controls/OrbitControls.js"; 15 | declare module "three/addons/loaders/KTX2Loader.js"; 16 | declare module "three/addons/libs/meshopt_decoder.module.js"; 17 | declare module "3d-tiles-renderer"; 18 | declare module "three/addons/environments/RoomEnvironment.js"; 19 | declare module "three/addons/loaders/DRACOLoader.js"; 20 | declare module "three/addons/postprocessing/EffectComposer.js"; 21 | declare module "three/addons/postprocessing/RenderPass.js"; 22 | declare module "three/addons/postprocessing/ShaderPass.js"; 23 | declare module "three/addons/postprocessing/OutlinePass.js"; 24 | declare module "three/addons/shaders/FXAAShader.js"; 25 | declare module "three/addons/postprocessing/SMAAPass.js"; 26 | declare module "three/examples/jsm/shaders/GammaCorrectionShader.js"; 27 | declare module "three/addons/postprocessing/GlitchPass.js"; 28 | declare module "three/addons/modifiers/CurveModifier.js"; 29 | declare module "three/addons/loaders/FBXLoader.js"; 30 | declare module "three/addons/renderers/CSS2DRenderer.js"; 31 | declare module "three/addons/renderers/CSS3DRenderer.js"; 32 | declare module "three/addons/loaders/RGBELoader.js"; 33 | -------------------------------------------------------------------------------- /src/commands/create/project_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/commands/create/project_template/public/image/bd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhpCode/bd-admin/03bfc0e0531d869e48744425eb7980848c972892/src/commands/create/project_template/public/image/bd.jpg -------------------------------------------------------------------------------- /src/commands/create/project_template/public/image/star-squashed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhpCode/bd-admin/03bfc0e0531d869e48744425eb7980848c972892/src/commands/create/project_template/public/image/star-squashed.jpg -------------------------------------------------------------------------------- /src/commands/create/project_template/src/App.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/App.ts: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | slot: {}, 4 | hook: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import service from "@/utils/request"; 2 | 3 | /** 登录 */ 4 | export function userLoginApi(data: {}) { 5 | return service({ 6 | url: "user/login", 7 | method: "post", 8 | data, 9 | }); 10 | } 11 | 12 | /** 获取用户信息 */ 13 | export function getUserInfoApi() { 14 | return service({ 15 | url: "user/getUserInfo", 16 | method: "get", 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/components/svgIcon/icon.ts: -------------------------------------------------------------------------------- 1 | import SvgIcon from "@/components/svgIcon/index.vue"; 2 | import { type App } from "vue"; 3 | export const signSvgIcon = (vue: App) => { 4 | vue.component("SvgIcon", SvgIcon); 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/components/svgIcon/index.ts: -------------------------------------------------------------------------------- 1 | import "virtual:svg-icons-register"; 2 | import { useSystemStore } from "@/store/modules/system"; 3 | import { ref, watch } from "vue"; 4 | import { getCssValue } from "@/utils/index"; 5 | const systemStore = useSystemStore(); 6 | const props = defineProps({ 7 | iconName: { 8 | type: String, 9 | default: "", 10 | required: true, 11 | }, 12 | color: { 13 | type: String, 14 | default: "", 15 | }, 16 | styleIcon: { 17 | type: Object, 18 | default: () => ({ 19 | width: "12px", 20 | height: "12px", 21 | fontSize: "12px", 22 | }), 23 | }, 24 | }); 25 | const getId = (id: string) => { 26 | return id.includes("#"); 27 | }; 28 | const textColor = ref(""); 29 | watch( 30 | () => systemStore.themeValue, 31 | () => { 32 | textColor.value = props.color 33 | ? props.color 34 | : getCssValue("--them-head-font-color"); 35 | }, 36 | { immediate: true }, 37 | ); 38 | //#end; 39 | return { 40 | slot: {}, 41 | hook: {}, 42 | }; 43 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/components/svgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/components/table/table.ts: -------------------------------------------------------------------------------- 1 | //#slot:ui_1 2 | const props = defineProps(["dataSource", "colums"]); 3 | //#end; 4 | 5 | return { 6 | slot: { 7 | ui_1: { 8 | element: function ui_1() { 9 | return `import { useAttrs, onUpdated } from "vue"; 10 | let attrs = useAttrs(); 11 | onUpdated(() => { 12 | attrs = useAttrs(); 13 | });`; 14 | }, 15 | antdv: function ui_1() { 16 | return ` `; 17 | }, 18 | }, 19 | }, 20 | hook: {}, 21 | }; 22 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/components/table/table.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | //#end; 6 | 34 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/config/white-list.ts: -------------------------------------------------------------------------------- 1 | const whiteList = ["/login", "/404"]; 2 | const isWhiteListPage = (path: string): string | undefined => { 3 | return whiteList.find((item) => item === path); 4 | }; 5 | 6 | export default isWhiteListPage; 7 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/hooks/useI18.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "vue"; 2 | 3 | export default function () { 4 | const get = getCurrentInstance(); 5 | if (!get) return; 6 | const { 7 | config: { globalProperties }, 8 | } = get.appContext; 9 | return globalProperties.$t; 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/hooks/userEcharts.ts: -------------------------------------------------------------------------------- 1 | import { watch, onMounted, ref } from "vue"; 2 | import * as echarts from "echarts"; 3 | type EChartsOption = echarts.EChartsOption; 4 | type EChartsType = echarts.EChartsType; 5 | export default function ( 6 | option: EChartsOption | any, 7 | element: HTMLElement | string, 8 | ) { 9 | const myChart = ref(null); 10 | watch( 11 | () => option, 12 | () => { 13 | if (!myChart.value) return; 14 | myChart.value.setOption(option.value as EChartsOption); 15 | }, 16 | { immediate: true, deep: true }, 17 | ); 18 | onMounted(() => { 19 | const elementNode: HTMLElement | null = 20 | element instanceof Element ? element : document.getElementById(element)!; 21 | myChart.value = echarts.init(elementNode); 22 | myChart.value.setOption(option.value as EChartsOption); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/hooks/userVModel.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | export default function (props: any, proName: string, emit: any) { 3 | return computed({ 4 | get() { 5 | return new Proxy(props[proName], { 6 | get(target, key) { 7 | return Reflect.get(target, key); 8 | }, 9 | set(target, key, value) { 10 | emit(`update:${proName}`, { 11 | ...target, 12 | [key]: value, 13 | }); 14 | return true; 15 | }, 16 | }); 17 | }, 18 | set() {}, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/MagicStick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/a-businesscard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/a-chartqushitiaoyantongji.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/account.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/duoyuyan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/full-screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/fullscreen-exit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/graph.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/jiazai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/mima.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "system": { 4 | "router": { 5 | "Home": "Home", 6 | "OutLink": "Out Link", 7 | "Three": "Three", 8 | "Menu": "Menu", 9 | "Button ": "Button", 10 | "SystemManagement": "System management", 11 | "MultiLevel": "Multi-level", 12 | "SecondMenu": "Second menu", 13 | "ThirdMenu": "Third Menu", 14 | "FourthMenu": "Fourth Menu" 15 | } 16 | }, 17 | "login": { 18 | "Login": "Login", 19 | "PleaseEnterYourAccountNumber": "Please enter your account number" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/lang/lang.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | import en from "./en.json"; // 英文语言配置 3 | import zhCN from "./zh-CN.json"; // 中文语言配置 4 | import { getLocalStorage } from "@/utils/index"; 5 | 6 | const config: any = getLocalStorage("system"); 7 | 8 | const lang = config && config.lang ? config.lang : "zhCN"; 9 | 10 | export const i18n = createI18n({ 11 | legacy: false, // componsition API需要设置为false 12 | locale: lang, 13 | globalInjection: true, // 可以在template模板中使用$t 14 | messages: { 15 | en, 16 | zhCN, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "system": { 4 | "router": { 5 | "Home": "首页", 6 | "OutLink": "外链", 7 | "Three": "three", 8 | "Menu": "菜单权限", 9 | "Button ": "按钮权限", 10 | "SystemManagement": "系统管理", 11 | "MultiLevel": "多级菜单", 12 | "SecondMenu": "二级菜单", 13 | "ThirdMenu": "三级菜单", 14 | "FourthMenu": "四级菜单" 15 | } 16 | }, 17 | "login": { 18 | "Login": "登录", 19 | "PleaseEnterYourAccountNumber": "请输入账号" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/index.scss: -------------------------------------------------------------------------------- 1 | .aside { 2 | transition: width 0.35s; 3 | .close-menu { 4 | width: 64px; 5 | } 6 | .open-menu { 7 | width: 220px; 8 | } 9 | .log { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | height: 80px; 14 | background-color: var(--them-logo-bg-color); 15 | .log-image { 16 | width: 50px; 17 | height: 50px; 18 | border-radius: 50%; 19 | } 20 | .log-name { 21 | margin-left: 10px; 22 | font-size: 18px; 23 | font-weight: 900; 24 | display: -webkit-box; 25 | -webkit-box-orient: vertical; 26 | -webkit-line-clamp: 1; 27 | overflow: hidden; 28 | } 29 | } 30 | } 31 | //#hook_1 32 | 33 | //#end; 34 | return { 35 | hook_1:{ 36 | element:function(){ 37 | return `` 38 | }, 39 | antdv:function(){ 40 | return `:deep(.ant-menu-dark) { 41 | & > .ant-menu { 42 | background-color: var(--them-menu-bg-color) !important; 43 | } 44 | }` 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { useSystemStore } from "@/store/modules/system"; 3 | 4 | import Menu from "./menu.vue"; 5 | import SubMenu from "./subMenu.vue"; 6 | import MenuItem from "./menu-item.vue"; 7 | //#hook:hook_1 8 | const systemStore = useSystemStore(); 9 | interface MenuList { 10 | key: string; 11 | label: string; 12 | title: string; 13 | icon?: string; 14 | children?: MenuList[] | undefined; 15 | } 16 | const getMenuList = (routerList: RouterRes[]): MenuList[] => { 17 | if (!routerList || routerList.length === 0) return []; 18 | return routerList.map((router: RouterRes): MenuList => { 19 | //#hook:hook_2 20 | return { 21 | key: router.path, 22 | //#hook:hook_3 23 | title: router.path, 24 | icon: router?.meta?.icon, 25 | children: getMenuList(router.children), 26 | }; 27 | }); 28 | }; 29 | const items = ref(getMenuList(systemStore.routerList)); 30 | //#end; 31 | () => { 32 | console.log(Menu, SubMenu, MenuItem, systemStore); 33 | }; 34 | return { 35 | slot: {}, 36 | hook: { 37 | hook_1: { 38 | i18n: { 39 | HOOK: function () { 40 | return `import useI18n from "../../hooks/useLang"; 41 | const { menuSwitchesToLang } = useI18n();`; 42 | }, 43 | }, 44 | }, 45 | hook_2: { 46 | i18n: { 47 | HOOK: function () { 48 | return `const title = menuSwitchesToLang(router.meta.title);`; 49 | }, 50 | }, 51 | }, 52 | hook_3: { 53 | i18n: { 54 | HOOK: function () { 55 | return `label: title,`; 56 | }, 57 | FALSE: function () { 58 | return `label: router.meta.title,`; 59 | }, 60 | }, 61 | }, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/index.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu-item.scss: -------------------------------------------------------------------------------- 1 | .my-menu { 2 | //#hook_1 3 | } 4 | //#end; 5 | return { 6 | hook_1:{ 7 | element:function(){ 8 | return ` box-sizing: border-box; 9 | .is-active { 10 | border-radius: 4px; 11 | background-color: #1677ff; 12 | color: var(--them-menu-font-color) !important; 13 | } 14 | 15 | :deep(.el-menu-item) { 16 | color: var(--them-menu-font-color); 17 | &:hover { 18 | opacity: 0.7; 19 | color: var(--them-menu-font-color) !important; 20 | background-color: var(--them-menu-hove-color); 21 | border-radius: 4px; 22 | } 23 | }` 24 | }, 25 | antdv:function(){ 26 | return `:deep(.ant-menu-item:not(.ant-menu-item-selected)) { 27 | &:hover { 28 | opacity: 0.7; 29 | background-color: var(--them-menu-hove-color) !important; 30 | } 31 | }` 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu-item.ts: -------------------------------------------------------------------------------- 1 | import SvgIcon from "@/components/svgIcon/index.vue"; 2 | //#slot:ui_1 3 | const props = defineProps(["menuItem"]); 4 | const styleIcon = { 5 | width: "20px", 6 | height: "20px", 7 | fontSize: "20px", 8 | }; 9 | //#end; 10 | () => { 11 | console.log(styleIcon, props, SvgIcon); 12 | }; 13 | return { 14 | slot: { 15 | ui_1: { 16 | element: function ui_1() { 17 | return ``; 18 | }, 19 | antdv: function ui_1() { 20 | return ``; 21 | }, 22 | }, 23 | }, 24 | hook: {}, 25 | }; 26 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu-item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | //#end; 10 | 30 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu.scss: -------------------------------------------------------------------------------- 1 | //#hook_1 2 | //#end; 3 | return { 4 | hook_1:{ 5 | element:function(){ 6 | return `.el-menu-vertical-demo { 7 | width: 100%; 8 | border: 0 !important; 9 | }` 10 | }, 11 | antdv:function(){ 12 | return `.a-menu { 13 | :deep(.ant-menu-submenu-arrow) { 14 | width: 0; 15 | } 16 | } 17 | :deep(.ant-menu-submenu-selected) { 18 | .ant-menu-submenu-title { 19 | .ant-menu-title-content { 20 | color: var(--them-menu-hove-color) !important; 21 | } 22 | } 23 | } 24 | :deep(.ant-menu-vertical) { 25 | background-color: var(--them-menu-bg-color) !important; 26 | } 27 | :deep(.ant-menu-dark) { 28 | background-color: var(--them-menu-hove-color) !important; 29 | } 30 | 31 | .el-menu-vertical-demo { 32 | width: 100%; 33 | border: 0 !important; 34 | background-color: var(--them-menu-bg-color) !important; 35 | :deep(.ant-menu-inline) { 36 | background-color: var(--them-menu-bg-color) !important; 37 | } 38 | }` 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "vue-router"; 2 | import { useSystemStore } from "@/store/modules/system"; 3 | //#hook:hook_1 4 | //#slot:ui_1 5 | const systemStore = useSystemStore(); 6 | const router = useRouter(); 7 | const props = defineProps({ 8 | isCollapse: { 9 | type: Boolean, 10 | default: false, 11 | required: true, 12 | }, 13 | }); 14 | 15 | const addTabBar = (path: string) => { 16 | const routerList = router.getRoutes(); 17 | const tab = routerList.find((item) => item.path === path); 18 | if (!tab) return; 19 | //#hook:hook_2 20 | systemStore.addTabBar({ 21 | path: tab.path, 22 | //#hook:hook_3 23 | }); 24 | }; 25 | addTabBar("/home"); 26 | //#slot:ui_2 27 | const defaultActive = ref(router.currentRoute.value.fullPath); 28 | 29 | //#end; 30 | () => { 31 | console.log(select, defaultActive, props); 32 | }; 33 | return { 34 | slot: { 35 | ui_1: { 36 | element: function ui_1() { 37 | return `import { ref, watch } from "vue"; 38 | import { getCssValue } from "@/utils/index";`; 39 | }, 40 | antdv: function ui_1() { 41 | return `import { ref } from "vue";`; 42 | }, 43 | }, 44 | ui_2: { 45 | element: function ui_1() { 46 | return `const select = (v) => { 47 | v.indexOf("https") === -1 ? router.push(v) : window.open(v); 48 | addTabBar(v); 49 | 50 | }; 51 | const textColor = ref(""); 52 | const backgroundColor = ref(""); 53 | watch( 54 | () => systemStore.themeValue, 55 | () => { 56 | textColor.value = getCssValue("--them-menu-font-color"); 57 | backgroundColor.value = getCssValue("--them-menu-bg-color"); 58 | }, 59 | { immediate: true }, 60 | ); 61 | `; 62 | }, 63 | antdv: function ui_1() { 64 | return `const openKeys = ref([]); 65 | const getOpenKeys = (path) => { 66 | if (systemStore.isCollapse) return; 67 | const key = path.split("").reverse().join(""); 68 | const index = key.indexOf("/") + 1; 69 | const keyPath = path.slice(0, key.length - index); 70 | if (!keyPath) return (openKeys.value = ["/"]); 71 | const array = []; 72 | const pathList = keyPath 73 | .split("/") 74 | .filter((item) => item) 75 | .map((item) => "/" + item); 76 | if (pathList.length > 1) { 77 | pathList.reduce((a, b) => { 78 | if (array.length === 0) array.push(a); 79 | array.push(a + b); 80 | return a + b; 81 | }); 82 | openKeys.value = array; 83 | } else { 84 | openKeys.value = pathList; 85 | } 86 | }; 87 | getOpenKeys(router.currentRoute.value.fullPath); 88 | 89 | const select = (v) => { 90 | defaultActive.value = v.key; 91 | getOpenKeys(v.key); 92 | v.key.indexOf("https") === -1 ? router.push(v.key) : window.open(v.key); 93 | addTabBar(v.key); 94 | };`; 95 | }, 96 | }, 97 | }, 98 | hook: { 99 | hook_1: { 100 | i18n: { 101 | HOOK: function () { 102 | return `import useI18n from "../../hooks/useLang"; 103 | const { menuSwitchesToLang } = useI18n();`; 104 | }, 105 | }, 106 | }, 107 | hook_2: { 108 | i18n: { 109 | HOOK: function () { 110 | return `const title = menuSwitchesToLang(tab.meta.title);`; 111 | }, 112 | }, 113 | }, 114 | hook_3: { 115 | i18n: { 116 | HOOK: function () { 117 | return `name: title,`; 118 | }, 119 | FALSE: function () { 120 | return `name:tab.meta.title`; 121 | }, 122 | }, 123 | }, 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/menu.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/subMenu.scss: -------------------------------------------------------------------------------- 1 | .el-menu-vertical-demo { 2 | border: 0 !important; 3 | } 4 | //#hook_1 5 | 6 | //#end; 7 | return { 8 | hook_1:{ 9 | element:function(){ 10 | return `:deep(.el-sub-menu__title) { 11 | color: var(--them-menu-font-color) !important; 12 | }` 13 | }, 14 | antdv:function(){ 15 | return `` 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/subMenu.ts: -------------------------------------------------------------------------------- 1 | import SvgIcon from "@/components/svgIcon/index.vue"; 2 | import { useSystemStore } from "@/store/modules/system"; 3 | const systemStore = useSystemStore(); 4 | import MenuItem from "./menu-item.vue"; 5 | const props = defineProps(["menu", "isCollapse", "view"]); 6 | const styleIcon = { 7 | width: "20px", 8 | height: "20px", 9 | fontSize: "20px", 10 | }; 11 | //#end; 12 | () => { 13 | console.log(SvgIcon, systemStore, MenuItem, props, styleIcon); 14 | }; 15 | return { 16 | slot: {}, 17 | hook: {}, 18 | }; 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/aside/subMenu.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/index.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | box-sizing: border-box; 3 | overflow: hidden; 4 | .head-left { 5 | flex: 1; 6 | .fold-icon { 7 | width: 50px; 8 | height: 50px; 9 | } 10 | a { 11 | cursor: not-allowed; 12 | } 13 | .breadcrumb { 14 | color: var(--them-head-font-color); 15 | } 16 | } 17 | .head-right { 18 | height: 100%; 19 | .tooltip { 20 | margin: 0 10px; 21 | color: var(--them-head-font-color); 22 | } 23 | .user-right { 24 | position: relative; 25 | margin: 0 10px; 26 | .user-name { 27 | margin: 0 5px; 28 | color: var(--them-head-font-color); 29 | } 30 | span { 31 | margin: 0 5px; 32 | color: #606266; 33 | } 34 | .user-hove { 35 | position: absolute; 36 | left: 0; 37 | top: 10px; 38 | } 39 | } 40 | } 41 | } 42 | //#end; 43 | return { 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/index.ts: -------------------------------------------------------------------------------- 1 | import { watch, ref } from "vue"; 2 | import SvgIcon from "@/components/svgIcon/index.vue"; 3 | import { useSystemStore } from "@/store/modules/system"; 4 | import { useUserInfoStore } from "@/store/modules/user"; 5 | import { useRouter } from "vue-router"; 6 | import TabBar from "./tab-bar.vue"; 7 | import { useScreen } from "@/layouts/hooks/useScreen"; 8 | import { useTheme } from "@/layouts/hooks/useTheme"; 9 | //#hook:hook_1 10 | const { clickFullscreen, screen } = useScreen(); 11 | const { command, themeList } = useTheme(); 12 | const systemStore = useSystemStore(); 13 | const userInfoStore = useUserInfoStore(); 14 | const router = useRouter(); 15 | const styleIcon = { 16 | width: "18px", 17 | height: "18px", 18 | fontSize: "18px", 19 | }; 20 | 21 | const clickGoGithub = () => { 22 | window.open("https://github.com/lhpCode/bd-admin"); 23 | }; 24 | const dropLogin = () => { 25 | userInfoStore.dropLogin(); 26 | systemStore.resetSystem(); 27 | router.push("/login"); 28 | }; 29 | const clickIcon = () => { 30 | systemStore.switchCollapse(); 31 | }; 32 | 33 | const breadcrumbList = ref([]); 34 | const routerList = router.getRoutes(); 35 | watch( 36 | () => router.currentRoute.value.path, 37 | (newValue) => { 38 | const breadcrumb = newValue.split("/"); 39 | let path = ""; 40 | breadcrumbList.value = []; 41 | for (let i = 0; i < breadcrumb.length; i++) { 42 | if (!breadcrumb[i]) continue; 43 | path = path + "/" + breadcrumb[i]; 44 | const routerObj = routerList.find((item) => item.path === path); 45 | if (!routerObj) continue; 46 | //#hook:hook_2 47 | } 48 | }, 49 | { immediate: true }, 50 | ); 51 | //#end; 52 | () => { 53 | console.log(SvgIcon, TabBar); 54 | }; 55 | return { 56 | slot: {}, 57 | hook: { 58 | hook_1: { 59 | i18n: { 60 | HOOK: function () { 61 | return `import useLang from "../../hooks/useLang"; 62 | const { checkI18, i18List, menuSwitchesToLang } = useLang();`; 63 | }, 64 | }, 65 | }, 66 | hook_2: { 67 | i18n: { 68 | HOOK: function () { 69 | return `const title = menuSwitchesToLang(routerObj.meta.title); 70 | breadcrumbList.value.push({ 71 | path: routerObj.path, 72 | title: title, 73 | });`; 74 | }, 75 | FALSE: function () { 76 | return ` breadcrumbList.value.push({ 77 | path:routerObj.path, 78 | title:routerObj.meta.title 79 | });`; 80 | }, 81 | }, 82 | }, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/index.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | //#end; 82 | 170 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/tab-bar.scss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | padding: 0 10px; 3 | .icon { 4 | margin: 0 10px; 5 | } 6 | .scroll { 7 | padding-top: 3px; 8 | flex: 1; 9 | height: 30px; 10 | position: relative; 11 | padding-bottom: 5px; 12 | text-align: center; 13 | white-space: nowrap; 14 | overflow-x: scroll; 15 | overflow-y: hidden; 16 | box-sizing: border-box; 17 | &::-webkit-scrollbar { 18 | position: absolute; 19 | bottom: 0; 20 | width: 10px; 21 | height: 0px; 22 | } 23 | &:hover { 24 | &::-webkit-scrollbar { 25 | position: absolute; 26 | bottom: 0; 27 | width: 10px; 28 | height: 5px; 29 | } 30 | } 31 | .tab-bar { 32 | position: absolute; 33 | left: 0px; 34 | top: 0; 35 | .tab { 36 | position: relative; 37 | margin-left: 5px; 38 | padding: 2px 5px; 39 | height: 30px; 40 | color: var(--them-head-font-color); 41 | border: 1px solid #eee; 42 | border-radius: 5px; 43 | font-size: 14px; 44 | cursor: pointer; 45 | &:hover { 46 | .close { 47 | display: flex; 48 | } 49 | } 50 | .close { 51 | display: none; 52 | position: absolute; 53 | top: 0; 54 | right: 0; 55 | transform: translate(50%, -20%); 56 | width: 12px; 57 | height: 12px; 58 | color: #fff; 59 | border-radius: 50%; 60 | background-color: #b9b9b9; 61 | } 62 | } 63 | .select { 64 | padding-left: 12px; 65 | background-color: #1677ff; 66 | border: 1px solid #1677ff; 67 | color: #fff; 68 | &::after { 69 | content: ""; 70 | display: block; 71 | position: absolute; 72 | left: 6px; 73 | top: 50%; 74 | transform: translate(-50%, -50%); 75 | width: 6px; 76 | height: 6px; 77 | border-radius: 50%; 78 | background-color: #fff; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/tab-bar.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, ref } from "vue"; 2 | import SvgIcon from "@/components/svgIcon/index.vue"; 3 | import { useSystemStore } from "@/store/modules/system"; 4 | import { useRouter } from "vue-router"; 5 | const systemStore = useSystemStore(); 6 | const router = useRouter(); 7 | let scrollNode: HTMLElement | null = null; 8 | onMounted(() => { 9 | scrollNode = document.querySelector("#scroll"); 10 | if (!scrollNode) return; 11 | scrollNode.addEventListener("scroll", scrollTop, true); 12 | }); 13 | onUnmounted(() => { 14 | if (!scrollNode) return; 15 | scrollNode.removeEventListener("scroll", scrollTop); 16 | }); 17 | const scrollLeft = ref(0); 18 | // 实时滚动条高度 19 | const scrollTop = () => { 20 | if (!scrollNode) return; 21 | const scroll = scrollNode.scrollLeft || scrollNode.scrollLeft; 22 | scrollLeft.value = scroll; 23 | }; 24 | 25 | const clickScroll = (number: number) => { 26 | if (!scrollNode) return; 27 | scrollNode.scrollLeft = scrollLeft.value + number; 28 | }; 29 | const goRouter = (path: string) => { 30 | router.push(path); 31 | }; 32 | const styleIcon = { 33 | width: "10px", 34 | height: "10px", 35 | fontSize: "10px", 36 | }; 37 | //#end; 38 | () => { 39 | console.log(SvgIcon); 40 | }; 41 | return { 42 | slot: {}, 43 | hook: {}, 44 | }; 45 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/head/tab-bar.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | //#end; 31 | 61 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/components/main/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 15 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/hooks/useLang.ts: -------------------------------------------------------------------------------- 1 | import { useSystemStore } from "@/store/modules/system"; 2 | import langJSON from "@/lang/zh-CN.json"; 3 | import { i18n } from "@/lang/lang"; 4 | const systemStore = useSystemStore(); 5 | 6 | const checkI18 = (value: string) => { 7 | i18n.global.locale.value = value; 8 | systemStore.setLang(value); 9 | }; 10 | const i18List = [ 11 | { 12 | name: "中文", 13 | value: "zhCN", 14 | }, 15 | { 16 | name: "English", 17 | value: "en", 18 | }, 19 | ]; 20 | 21 | const getLangKey = ( 22 | data: Object, 23 | findValue: string, 24 | path: string, 25 | ): string | undefined => { 26 | if (typeof data !== "object") return path; 27 | for (const [key, value] of Object.entries(data)) { 28 | const pathKey = path ? `${path}.${key}` : key; 29 | if (typeof value === "object") { 30 | const find = getLangKey(value, findValue, pathKey); 31 | if (find) return find; 32 | } else { 33 | if (value === findValue) { 34 | return pathKey; 35 | } 36 | } 37 | } 38 | }; 39 | 40 | const menuSwitchesToLang = (findValue: string | unknown) => { 41 | if (!findValue || typeof findValue !== "string") return ""; 42 | const get = getLangKey(langJSON, findValue, ""); 43 | return get ? get : findValue; 44 | }; 45 | export default function () { 46 | return { checkI18, i18List, menuSwitchesToLang }; 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/hooks/useScreen.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onBeforeUnmount } from "vue"; 2 | 3 | const screen = ref(false); 4 | const clickFullscreen = () => { 5 | fullscreen(); 6 | screen.value = getFullscreen(); 7 | }; 8 | 9 | const fullscreen = () => { 10 | const element = document.documentElement; 11 | if (!element) return; 12 | if (!document.fullscreenEnabled) { 13 | return Promise.reject(new Error("全屏模式被禁用")); 14 | } 15 | if (getFullscreen()) { 16 | exitFullScreen(); 17 | return; 18 | } 19 | let result = null; 20 | if (element.requestFullscreen) { 21 | result = element.requestFullscreen(); 22 | } else if (element.mozRequestFullScreen) { 23 | /* Firefox */ 24 | result = element.mozRequestFullScreen(); 25 | } else if (element.webkitRequestFullscreen) { 26 | /* Chrome, Safari 和 Opera */ 27 | result = element.webkitRequestFullscreen(); 28 | } else if (element.msRequestFullscreen) { 29 | /* IE/Edge */ 30 | result = element.msRequestFullscreen(); 31 | } 32 | return result || Promise.reject(new Error("不支持全屏")); 33 | }; 34 | 35 | // 退出全屏 36 | const exitFullScreen = (element?: HTMLElement | undefined) => { 37 | const d = element ? element : document; 38 | const full = 39 | d.cancelFullScreen || 40 | d.webkitCancelFullScreen || 41 | d.mozCancelFullScreen || 42 | d.exitFullScreen; 43 | if (typeof full !== "undefined" && full) { 44 | full.call(d); 45 | return; 46 | } 47 | }; 48 | 49 | const getFullscreen = (): boolean => { 50 | const flag = 51 | document.fullscreen || 52 | document.mozFullScreen || 53 | document.webkitIsFullScreen || 54 | document.webkitFullScreen || 55 | document.msFullScreen; 56 | return flag; 57 | }; 58 | 59 | function resize() { 60 | screen.value = getFullscreen(); 61 | } 62 | function KeyDown(event: any) { 63 | if (event.keyCode === 122 || event.keyCode === 27) { 64 | event.returnValue = false; 65 | clickFullscreen(); 66 | return; 67 | } 68 | } 69 | export function useScreen() { 70 | onBeforeUnmount(() => { 71 | document.removeEventListener("keydown", KeyDown); // 监听按键事件 72 | document.removeEventListener("resize", resize); 73 | }); 74 | onMounted(() => { 75 | window.addEventListener("resize", resize); 76 | document.addEventListener("keydown", KeyDown, true); // 监听按键事件 77 | }); 78 | return { clickFullscreen, screen }; 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | // 主题切换 2 | import { onMounted } from "vue"; 3 | import { switchThemeColor } from "@/utils/index"; 4 | import { useSystemStoreWithOut } from "@/store/modules/system"; 5 | 6 | const systemStore = useSystemStoreWithOut(); 7 | const themeList = [ 8 | { 9 | name: "默认", 10 | value: "normal", 11 | }, 12 | { 13 | name: "暗黑", 14 | value: "dark", 15 | }, 16 | { 17 | name: "深蓝", 18 | value: "darkBlue", 19 | }, 20 | { 21 | name: "蓝白", 22 | value: "light", 23 | }, 24 | { 25 | name: "炫彩", 26 | value: "florid", 27 | }, 28 | ]; 29 | switchThemeColor(systemStore.themeValue); 30 | const command = (v: string) => { 31 | systemStore.themeValue = v; 32 | switchThemeColor(v); 33 | }; 34 | 35 | export function useTheme() { 36 | onMounted(() => {}); 37 | return { command, themeList }; 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/index.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | import leftModel from "./leftModel.vue"; 2 | //#end; 3 | () => { 4 | console.log(leftModel); 5 | }; 6 | return { 7 | slot: {}, 8 | hook: {}, 9 | }; 10 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/lay.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/lay.ts: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | slot: {}, 4 | hook: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/lay.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/leftModel.scss: -------------------------------------------------------------------------------- 1 | .left-model { 2 | display: flex; 3 | width: 100vw; 4 | height: 100vh; 5 | background-image: var(--them-image-url); 6 | overflow: hidden; 7 | } 8 | .aside { 9 | height: 100vh; 10 | } 11 | .conter { 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | .main { 17 | padding: 10px; 18 | flex: 1; 19 | box-sizing: border-box; 20 | } 21 | //#end; 22 | return { 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/leftModel.ts: -------------------------------------------------------------------------------- 1 | import headLay from "./components/head/index.vue"; 2 | import asideLay from "./components/aside/index.vue"; 3 | import mainLay from "./components/main/index.vue"; 4 | 5 | //#end; 6 | () => { 7 | console.log(headLay, asideLay, mainLay); 8 | }; 9 | return { 10 | slot: {}, 11 | hook: {}, 12 | }; 13 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/layouts/leftModel.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | 3 | const routes = [ 4 | { 5 | path: "/", 6 | name: "/", 7 | redirect: "/home", 8 | component: () => import("@/layouts/index.vue"), 9 | children: [], 10 | }, 11 | { 12 | path: "/login", 13 | name: "login", 14 | component: () => import("@/views/resident/login/index.vue"), 15 | children: [], 16 | }, 17 | { 18 | path: "/404", 19 | component: () => import("@/views/error/404.vue"), 20 | }, 21 | { 22 | path: "/:path(.*)", 23 | component: () => import("@/views/error/404.vue"), 24 | }, 25 | ]; 26 | 27 | // 路由 28 | const router = createRouter({ 29 | history: createWebHashHistory(), 30 | routes: routes, 31 | }); 32 | 33 | // 导出 34 | export default router; 35 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/router/protector.ts: -------------------------------------------------------------------------------- 1 | import router from "./index"; 2 | import { useUserInfoStoreWithOut } from "@/store/modules/user"; 3 | import { useSystemStore } from "@/store/modules/system"; 4 | import { useSystemStoreWithOut } from "@/store/modules/system"; 5 | import isWhiteListPage from "@/config/white-list"; 6 | import { type RouteRecordRaw } from "vue-router"; 7 | import { getUserInfoApi } from "@/api/user"; 8 | 9 | const layouts = import.meta.glob("../**/**.vue"); 10 | const pages = [...Object.keys(layouts)]; 11 | 12 | // 动态路由 13 | const asyncRouter = (routerList: RouterRes[]): RouteRecordRaw[] => { 14 | if (!routerList || routerList.length === 0) return []; 15 | // 路由过滤 16 | const filterRouterList = routerList.filter((item) => 17 | pages.find( 18 | (path) => 19 | path === item.component || item.component.indexOf("https") !== -1, 20 | ), 21 | ); 22 | return filterRouterList.map((r: RouterRes) => ({ 23 | path: r.path, 24 | name: r.name, 25 | component: 26 | r.component.indexOf("https") === -1 27 | ? () => import(/* @vite-ignore */ r.component) 28 | : null, 29 | children: asyncRouter(r.children), 30 | meta: JSON.parse(JSON.stringify(r.meta)), 31 | })); 32 | }; 33 | 34 | //动态添加路由 35 | const addRouter = async (userInfoStore: any) => { 36 | const { data }: any = await getUserInfoApi(); 37 | userInfoStore.setUserInfo(data.role, data.routerList); 38 | const systemStore = useSystemStore(); 39 | const systemStoreWithOut = useSystemStoreWithOut(); 40 | const routerRes = await userInfoStore.getUserInfo(); 41 | const routerList = asyncRouter(routerRes) as RouteRecordRaw[]; 42 | systemStoreWithOut.setRouterList(routerList); 43 | routerList.forEach((item: RouteRecordRaw) => { 44 | // 注册动态路由 45 | if (!item.component) return; 46 | let routerFn; 47 | if (item.meta && item.meta.ifFull) { 48 | routerFn = router.addRoute(item); 49 | } else { 50 | routerFn = router.addRoute("/", item); 51 | } 52 | systemStore.addRemoveRouterList(routerFn); 53 | }); 54 | }; 55 | 56 | router.beforeEach(async (to, _from, next) => { 57 | const userInfoStore = useUserInfoStoreWithOut(); 58 | if (!userInfoStore.token) { 59 | // 判断是否在免登录的白名单内 60 | if (!isWhiteListPage(to.fullPath)) { 61 | return next("/login"); 62 | } else { 63 | return next(); 64 | } 65 | } 66 | if (to.path === "/login") { 67 | return next({ path: "/" }); 68 | } 69 | try { 70 | if (!userInfoStore.role) { 71 | await addRouter(userInfoStore); 72 | return next(to.fullPath); 73 | } 74 | next(); 75 | } catch (err) { 76 | next(); 77 | throw new Error("路由错误" + err); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from "pinia"; 2 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 3 | import { App } from "vue"; 4 | const store = createPinia(); 5 | store.use(piniaPluginPersistedstate); 6 | const initStore = (app: App) => { 7 | app.use(store); 8 | }; 9 | export { store, initStore }; 10 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/store/modules/system.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "../index"; 3 | import router from "@/router"; 4 | 5 | interface TabBar { 6 | path: string; 7 | name: string; 8 | } 9 | 10 | interface State { 11 | lang: string; 12 | themeValue: string; 13 | isCollapse: boolean; 14 | removeRouterList: Function[]; 15 | routerList: RouterRes[]; 16 | tabBarList: TabBar[]; 17 | } 18 | export const useSystemStore = defineStore({ 19 | id: "system", 20 | state: (): State => ({ 21 | lang: "zhCN", 22 | themeValue: "normal", 23 | isCollapse: false, 24 | removeRouterList: [], 25 | routerList: [], 26 | tabBarList: [], 27 | }), 28 | actions: { 29 | setLang(lang: any) { 30 | this.lang = lang; 31 | }, 32 | addTabBar(tab: TabBar) { 33 | const find = this.tabBarList.find( 34 | (item: TabBar) => item.path === tab.path, 35 | ); 36 | if (find) return; 37 | this.tabBarList.push(tab); 38 | }, 39 | delTabbar(path: string) { 40 | this.tabBarList = this.tabBarList.filter( 41 | (item: TabBar) => item.path !== path, 42 | ); 43 | if ( 44 | path === window.location.hash.replace("#", "") && 45 | this.tabBarList.length > 0 46 | ) { 47 | router.push(this.tabBarList[this.tabBarList.length - 1].path); 48 | } 49 | }, 50 | switchCollapse() { 51 | this.isCollapse = !this.isCollapse; 52 | }, 53 | resetSystem() { 54 | this.removeRouterList.forEach((item: Function) => item()); 55 | this.removeRouterList = []; 56 | this.tabBarList = []; 57 | }, 58 | setRouterList(routerList: any) { 59 | this.routerList = routerList; 60 | }, 61 | addRemoveRouterList(router: Function) { 62 | this.removeRouterList.push(router); 63 | }, 64 | }, 65 | persist: { 66 | paths: ["isCollapse", "routerList", "tabBarList", "themeValue", "lang"], 67 | }, 68 | }); 69 | 70 | export const useSystemStoreWithOut = () => { 71 | return useSystemStore(store); 72 | }; 73 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "../index"; 3 | export interface State extends UserInfoParams { 4 | userName: string; 5 | token: string; 6 | role: string; 7 | routerList: RouterRes[]; 8 | } 9 | export interface UserInfoParams { 10 | nickName: string; 11 | userName: string; 12 | token: string; 13 | role?: string; 14 | } 15 | 16 | export const useUserInfoStore = defineStore({ 17 | id: "user", 18 | state: (): State => ({ 19 | nickName: "", 20 | userName: "", 21 | token: "", 22 | role: "", 23 | routerList: [], 24 | }), 25 | actions: { 26 | dropLogin() { 27 | this.userName = ""; 28 | this.token = ""; 29 | this.role = ""; 30 | this.routerList = []; 31 | }, 32 | getToken() { 33 | return this.token; 34 | }, 35 | login(params: UserInfoParams) { 36 | const { userName, token, nickName } = params; 37 | this.userName = userName; 38 | this.token = token; 39 | this.nickName = nickName; 40 | }, 41 | setUserInfo(role: string, routerList: any) { 42 | this.role = role; 43 | this.routerList = routerList; 44 | }, 45 | async getUserInfo() { 46 | return this.routerList; 47 | }, 48 | }, 49 | persist: { 50 | paths: ["nickName", "userName", "token"], 51 | }, 52 | }); 53 | 54 | export const useUserInfoStoreWithOut = () => { 55 | return useUserInfoStore(store); 56 | }; 57 | -------------------------------------------------------------------------------- /src/commands/create/project_template/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 | font-synthesis: none; 6 | text-rendering: optimizeLegibility; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | a { 12 | font-weight: 500; 13 | color: #646cff; 14 | text-decoration: inherit; 15 | } 16 | a:hover { 17 | color: #535bf2; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | display: flex; 23 | place-items: center; 24 | min-width: 320px; 25 | min-height: 100vh; 26 | } 27 | 28 | .flex { 29 | display: flex; 30 | } 31 | .flex-center { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | } 36 | .flex-align { 37 | display: flex; 38 | align-items: center; 39 | } 40 | .flex-justify { 41 | display: flex; 42 | justify-content: center; 43 | } 44 | 45 | .el-tooltip__trigger:focus-visible, 46 | .tooltip:focus { 47 | outline: unset; 48 | } 49 | 50 | /* 美化滚动条 */ 51 | ::-webkit-scrollbar { 52 | width: 10px; 53 | height: 10px; 54 | } 55 | 56 | ::-webkit-scrollbar-track { 57 | width: 6px; 58 | background: rgba(#101f1c, 0.1); 59 | -webkit-border-radius: 2em; 60 | -moz-border-radius: 2em; 61 | border-radius: 2em; 62 | } 63 | 64 | ::-webkit-scrollbar-thumb { 65 | background-color: rgba(144, 147, 153, 0.5); 66 | background-clip: padding-box; 67 | min-height: 28px; 68 | -webkit-border-radius: 2em; 69 | -moz-border-radius: 2em; 70 | border-radius: 2em; 71 | transition: background-color 0.3s; 72 | cursor: pointer; 73 | } 74 | 75 | ::-webkit-scrollbar-thumb:hover { 76 | background-color: rgba(144, 147, 153, 0.3); 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/dark.css: -------------------------------------------------------------------------------- 1 | /* 黑暗 */ 2 | [data-theme="dark"] { 3 | --them-head-bg-color: #1f1f1f; 4 | --them-head-font-color: #ffffff; 5 | 6 | --them-logo-bg-color: #1f1f1f; 7 | --them-logo-font-color: #ffffff; 8 | 9 | --them-menu-bg-color: #1f1f1f; 10 | --them-menu-hove-color: #1677ff; 11 | --them-menu-font-color: #ffffff; 12 | 13 | --them-main-bg-color: #1f1f1f; 14 | 15 | --them-image-url: url(""); 16 | 17 | --main-color: #fff; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/darkBlue.css: -------------------------------------------------------------------------------- 1 | /* 深蓝 */ 2 | [data-theme="darkBlue"] { 3 | --them-head-bg-color: #081947; 4 | --them-head-font-color: #ffffff; 5 | 6 | --them-logo-bg-color: #081947; 7 | --them-logo-font-color: #ffffff; 8 | 9 | --them-menu-bg-color: #081947; 10 | --them-menu-hove-color: #1677ff; 11 | --them-menu-font-color: #ffffff; 12 | 13 | --them-main-bg-color: #081947; 14 | 15 | --them-image-url: url(""); 16 | 17 | --main-color: #fff; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/florid.css: -------------------------------------------------------------------------------- 1 | /* 默认 */ 2 | [data-theme="florid"] { 3 | --them-head-bg-color: transparent; 4 | --them-head-font-color: #ffffff; 5 | 6 | --them-logo-bg-color: transparent; 7 | --them-logo-font-color: #ffffff; 8 | 9 | --them-menu-bg-color: transparent; 10 | --them-menu-hove-color: #1677ff; 11 | --them-menu-font-color: #ffffff; 12 | 13 | --them-main-bg-color: transparent; 14 | 15 | --them-image-url: url("./image/star-squashed.jpg"); 16 | 17 | --main-color: #fff; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url("./dark.css"); 2 | @import url("./darkBlue.css"); 3 | @import url("./normal.css"); 4 | @import url("./light.css"); 5 | @import url("./florid.css"); 6 | 7 | :root { 8 | .aside { 9 | color: var(--them-menu-font-color); 10 | height: 100%; 11 | background-color: var(--them-menu-bg-color); 12 | box-shadow: -5px 0px 18px 0px rgba(0, 0, 0, 0.1); 13 | } 14 | .head-lay { 15 | box-sizing: border-box; 16 | .header { 17 | width: 100%; 18 | color: var(--them-head-font-color); 19 | background-color: var(--them-head-bg-color); 20 | border-bottom: 2px solid #eee !important; 21 | box-sizing: border-box; 22 | } 23 | } 24 | .main { 25 | width: 100%; 26 | height: 100%; 27 | background-color: var(--them-main-bg-color); 28 | color: var(--main-color); 29 | } 30 | } 31 | 32 | .ant-menu-dark { 33 | background-color: var(--them-menu-bg-color) !important; 34 | & > .ant-menu { 35 | background-color: var(--them-menu-bg-color) !important; 36 | } 37 | } 38 | .ant-menu-title-content { 39 | color: var(--them-menu-font-color); 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/light.css: -------------------------------------------------------------------------------- 1 | /* 默认 */ 2 | [data-theme="light"] { 3 | --them-head-bg-color: #1677ff; 4 | --them-head-font-color: #ffffff; 5 | 6 | --them-logo-bg-color: #1677ff; 7 | --them-logo-font-color: #ffffff; 8 | 9 | --them-menu-bg-color: #ffffff; 10 | --them-menu-hove-color: #1677ff; 11 | --them-menu-font-color: #1f1f1f; 12 | 13 | --them-main-bg-color: #f2f6fc; 14 | 15 | --them-image-url: url(""); 16 | 17 | --main-color: #1f1f1f; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/styles/normal.css: -------------------------------------------------------------------------------- 1 | /* 默认 */ 2 | [data-theme="normal"] { 3 | --them-head-bg-color: #ffffff; 4 | --them-head-font-color: #1f1f1f; 5 | 6 | --them-logo-bg-color: #1f1f1f; 7 | --them-logo-font-color: #ffffff; 8 | 9 | --them-menu-bg-color: #1f1f1f; 10 | --them-menu-hove-color: #1677ff; 11 | --them-menu-font-color: #ffffff; 12 | 13 | --them-main-bg-color: #f2f6fc; 14 | 15 | --them-image-url: url(""); 16 | 17 | --main-color: #1f1f1f; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import router from "@/router"; 2 | import { DirectiveBinding } from "vue"; 3 | /** 4 | * 获取css全局变量 5 | * @param {string} cssValueName css全局变量名 6 | * @returns {string} 颜色 7 | */ 8 | export const getCssValue = (cssValueName: string): string => { 9 | try { 10 | return getComputedStyle(document.documentElement).getPropertyValue( 11 | cssValueName, 12 | ); 13 | } catch { 14 | return ""; 15 | } 16 | }; 17 | 18 | /** 19 | * 设置css全局变量 20 | * @param {string}cssName 全局css变量 21 | * @param {string}cssValue 颜色 22 | */ 23 | export const setCssValue = (cssName: string, cssValue: string) => { 24 | try { 25 | document.documentElement.style.setProperty(cssName, cssValue); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | }; 30 | // 切换主题 31 | export const switchThemeColor = (themeName: string) => { 32 | window.document.documentElement.setAttribute("data-theme", themeName); 33 | }; 34 | 35 | // 获取LocalStorage 36 | export const getLocalStorage = (keyName: string): T | null => { 37 | if (!keyName) return null; 38 | const ls = localStorage.getItem(keyName); 39 | if (!ls) return null; 40 | return JSON.parse(ls); 41 | }; 42 | 43 | /** 44 | *设置LocalStorage 45 | * @param {string} keyName 46 | * @param {object} value 47 | * @returns {boolean} 48 | */ 49 | export const setLocalStorage = (keyName: string, value: Object): boolean => { 50 | if (!keyName) return false; 51 | localStorage.setItem(keyName, JSON.stringify(value)); 52 | return true; 53 | }; 54 | 55 | /** 56 | * 删除LocalStorage 57 | * @param {string} keyName 删除key 58 | * @param {Boolean} isAll 是否清空LocalStorage 59 | */ 60 | export const delLocalStorage = (keyName: string, isAll: Boolean = false) => { 61 | if (isAll) localStorage.clear(); 62 | localStorage.removeItem(keyName); 63 | }; 64 | 65 | /** 66 | * 验证权限 67 | * 权限从当前路由下meta对象的permission中获取 68 | */ 69 | export const hasPermission = (el: any, binding: DirectiveBinding) => { 70 | const { arg, value } = binding; 71 | const path = router.currentRoute.value.fullPath; 72 | const routers = router.getRoutes(); 73 | const routerObj = routers.find((item) => item.path === path); 74 | if (routerObj && routerObj.meta) { 75 | const { permission } = routerObj.meta as { permission: Array }; 76 | if (Object.prototype.toString.call(permission) !== "[object Array]") return; 77 | const flag = permission.some((item) => item === arg || item === value); 78 | if (!flag) el.parentNode.removeChild(el); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getLocalStorage } from "@/utils/index"; 3 | import { UserInfoParams } from "@/store/modules/user"; 4 | // import { useUserInfoStore } from "@/store/modules/user"; 5 | 6 | // const userInfoStoreWithOut = useUserInfoStore(); 7 | const service = axios.create({ 8 | baseURL: import.meta.env.VITE_APP_BASE_API, 9 | timeout: 10000, 10 | }); 11 | 12 | /** 13 | * "Content-Type" 14 | * "application/octet-stream", // 二进制流" 15 | * "application/msword", // Word文档格式 16 | * "application/json:charset=utf-8", // JSON数据格式 17 | * "multipart/form-data", //文件上传 18 | */ 19 | 20 | service.interceptors.request.use( 21 | (request) => { 22 | const userLocal = getLocalStorage("user"); 23 | const { token } = userLocal || ({} as UserInfoParams); 24 | request.headers.Authorization = token ? "Bearer " + token : undefined; 25 | return request; 26 | }, 27 | (err) => { 28 | return Promise.reject(err); 29 | }, 30 | ); 31 | 32 | // 响应拦截,根据业务文档自行调整 33 | service.interceptors.response.use( 34 | (response) => { 35 | const { status, data } = response; 36 | if (status !== 200) return {}; 37 | // resultCode是与后端约定好的 38 | switch (data.resultCode) { 39 | case 200: 40 | return data; 41 | default: 42 | } 43 | return response.data; 44 | }, 45 | (err) => { 46 | if (err.response) { 47 | const { status } = err.response; 48 | switch (status) { 49 | case 404: 50 | err.message = "请求地址出错"; 51 | break; 52 | default: 53 | break; 54 | } 55 | } 56 | return Promise.reject(err); 57 | }, 58 | ); 59 | 60 | export default service; 61 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/SpriteThree.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { Vector3, LabelParams } from "./interface/index"; 3 | import { CSS2DObject } from "three/addons/renderers/CSS2DRenderer.js"; 4 | // import { Flow } from "three/addons/modifiers/CurveModifier.js"; 5 | 6 | import { 7 | CSS3DObject, 8 | CSS3DSprite, 9 | } from "three/addons/renderers/CSS3DRenderer.js"; 10 | 11 | type Params = { 12 | width: number; 13 | height: number; 14 | }; 15 | 16 | export default class SpriteThree { 17 | private readonly spriteGroup: THREE.Group; 18 | private readonly spriteMap: Map; 19 | protected readonly params: Params; 20 | private readonly scene: THREE.Scene; 21 | private readonly camera: THREE.PerspectiveCamera; 22 | constructor( 23 | scene: THREE.Scene, 24 | camera: THREE.PerspectiveCamera, 25 | params: Params, 26 | ) { 27 | this.scene = scene; 28 | this.camera = camera; 29 | this.spriteMap = new Map(); 30 | this.params = params; 31 | const group = new THREE.Group(); 32 | group.name = "spriteGroup"; 33 | this.spriteGroup = group; 34 | this.scene.add(group); 35 | } 36 | //绘制加载动画 37 | createLoading(value: string, schedule: number, color: string = "#00F3B8") { 38 | const { width, height } = this.params; 39 | const dpr = 1; 40 | const canvas = document.createElement("canvas"); 41 | const ctx: any = canvas.getContext("2d"); 42 | canvas.width = Math.round(width * dpr); 43 | canvas.height = Math.round(height * dpr); 44 | canvas.style.width = width + "px"; 45 | canvas.style.height = height + "px"; 46 | canvas.style.left = "0px"; 47 | canvas.style.top = "0px"; 48 | canvas.style.pointerEvents = "none"; 49 | ctx.fillStyle = `rgba(0,0,0,${1 - schedule / 100})`; 50 | ctx.beginPath(); 51 | ctx.fillRect(0, 0, width, height); 52 | ctx.fill(); 53 | ctx.save(); 54 | ctx.fillStyle = color; 55 | const x = width / 3; 56 | const y = 200; 57 | const startWidth = width / 2 - x / 2; 58 | const font = 26; 59 | const left = 75; 60 | let ctxHeight = height / 2 - font; 61 | const boxHeight = y / 4; 62 | ctx.font = font + "px Calibri"; 63 | const text = ctx.measureText(value); 64 | ctx.strokeStyle = color; 65 | ctx.fillText(value, width / 2 - text.width / 2, ctxHeight); 66 | ctx.restore(); 67 | ctxHeight = ctxHeight + font * 1.5; 68 | ctx.strokeStyle = color; 69 | ctx.moveTo(startWidth + left + 8, ctxHeight); 70 | ctx.lineTo(startWidth + x - left - 8, ctxHeight); 71 | ctx.arcTo( 72 | startWidth + x - left, 73 | ctxHeight, 74 | startWidth + x - left, 75 | ctxHeight + boxHeight, 76 | 10, 77 | ); 78 | ctx.arcTo( 79 | startWidth + x - left, 80 | ctxHeight + boxHeight, 81 | startWidth + left, 82 | ctxHeight + boxHeight, 83 | 10, 84 | ); 85 | ctx.arcTo( 86 | startWidth + left, 87 | ctxHeight + boxHeight, 88 | startWidth + left, 89 | ctxHeight, 90 | 10, 91 | ); 92 | ctx.arcTo( 93 | startWidth + left, 94 | ctxHeight, 95 | startWidth + x - left, 96 | ctxHeight, 97 | 10, 98 | ); 99 | ctx.stroke(); 100 | const border = 8; 101 | const loadingWidth = 20; 102 | let progressLeft = left + border; 103 | ctxHeight = ctxHeight + border; 104 | ctx.fillStyle = color; 105 | const progressWidth = x - left * 2 - 2 - border * 2; 106 | const num = Math.floor(progressWidth / (loadingWidth + border)); 107 | const index = Math.floor((num * schedule) / 100); 108 | for (let i = 0; i < index; i++) { 109 | ctx.fillRect( 110 | progressLeft + startWidth, 111 | ctxHeight, 112 | loadingWidth, 113 | boxHeight - border * 2, 114 | ); 115 | progressLeft = progressLeft + loadingWidth + border; 116 | const addWidth = progressWidth - (loadingWidth + border) * index; 117 | if (addWidth > border && i === num - 1) { 118 | ctx.fillRect( 119 | progressLeft + startWidth, 120 | ctxHeight, 121 | addWidth, 122 | boxHeight - border * 2, 123 | ); 124 | } 125 | } 126 | ctx.font = font + "px Calibri"; 127 | ctx.strokeStyle = color; 128 | ctx.fillStyle = color; 129 | const text1 = Math.floor(schedule) + "%"; 130 | const textSchedule = ctx.measureText(text1); 131 | ctx.fillText( 132 | text1, 133 | startWidth + x - textSchedule.width - (left - textSchedule.width) / 2, 134 | ctxHeight + boxHeight / 2, 135 | ); 136 | ctx.restore(); 137 | return canvas; 138 | } 139 | // 添加精灵图标签 140 | addLabel(labelParams: LabelParams) { 141 | const { 142 | name, 143 | element, 144 | type = "CSS2DObject", 145 | position = { x: 0, y: 0, z: 0 }, 146 | scale = { x: 1, y: 1, z: 1 }, 147 | } = labelParams; 148 | if (this.spriteMap.has(name)) return; 149 | if (typeof element !== "object") return; 150 | if (typeof HTMLElement === "function") { 151 | const flag = element instanceof HTMLElement; 152 | if (!flag) return; 153 | } 154 | let nodeModel = null; 155 | switch (type) { 156 | case "CSS2DObject": 157 | nodeModel = new CSS2DObject(element); 158 | break; 159 | case "CSS3DObject": 160 | nodeModel = new CSS3DObject(element); 161 | break; 162 | case "CSS3DSprite": 163 | nodeModel = new CSS3DSprite(element); 164 | break; 165 | default: 166 | throw new Error("type类型错误"); 167 | } 168 | if (!nodeModel) return; 169 | nodeModel.name = name; 170 | nodeModel.position.set(position.x, position.y, position.z); 171 | const { x, y, z } = scale; 172 | if (x > 0 && y > 0 && z > 0) { 173 | nodeModel.scale.set(x, y, z); 174 | } 175 | nodeModel.center = new THREE.Vector2(0.5, 1); 176 | this.spriteMap.set(name, nodeModel); 177 | this.spriteGroup.add(nodeModel); 178 | } 179 | // 删除标签 180 | deleteLabel(name: string) { 181 | if (!this.spriteMap.has(name)) return; 182 | const model: any = this.spriteGroup.getObjectByName(name); 183 | if (!model) return; 184 | this.spriteMap.delete(name); 185 | this.spriteGroup.remove(model); 186 | } 187 | // 设置标签显示隐藏 188 | showLabel(name: string, visible: boolean) { 189 | const model = this.spriteGroup.getObjectByName(name); 190 | if (!model) return; 191 | model.visible = visible; 192 | } 193 | // 设置精灵图标签位置 194 | setLabelPostion(name: string, position: Vector3) { 195 | const model = this.spriteGroup.getObjectByName(name); 196 | if (!model) return; 197 | model.position.set(position.x, position.y, position.z); 198 | } 199 | // 设置精灵图标签样式 200 | setLabelStyle(labelParams: LabelParams) { 201 | const model = this.spriteGroup.getObjectByName(labelParams.name); 202 | if (model) { 203 | this.deleteLabel(labelParams.name); 204 | } 205 | this.addLabel(labelParams); 206 | } 207 | // 添加线 208 | addLine(coordArray: Vector3[], lineName: string = "") { 209 | const points = coordArray.map((p) => new THREE.Vector3(p.x, p.y, p.z)); 210 | const curve = new THREE.CatmullRomCurve3(points, false); 211 | const line = new THREE.Line( 212 | new THREE.BufferGeometry().setFromPoints( 213 | curve.getPoints(points.length * 10), 214 | ), 215 | new THREE.LineBasicMaterial({ 216 | color: 0x0000ff, 217 | }), 218 | ); 219 | line.name = lineName; 220 | this.scene.add(line); 221 | 222 | const geometry = new THREE.BoxGeometry(1, 1, 1); 223 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 224 | const cube = new THREE.Mesh(geometry, material); 225 | cube.name = "测试"; 226 | this.scene.add(cube); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/Three.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | // "three": "^0.160.0", 5 | // 渲染组合器 6 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 7 | // 渲染通道 8 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 9 | // ShaderPass功能:使用后处理Shader创建后处理通道 10 | import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; 11 | // 发光描边 12 | import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js"; 13 | // SMAA抗锯齿通道 14 | import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js"; 15 | // 伽马校正 16 | import { GammaCorrectionShader } from "three/examples/jsm/shaders/GammaCorrectionShader.js"; 17 | 18 | import { Params, Vector3, FnRenderType } from "./interface/index"; 19 | import { CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js"; 20 | 21 | import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js"; 22 | 23 | import { RGBELoader } from "three/addons/loaders/RGBELoader.js"; 24 | 25 | import { MoveTo } from "./utils/index"; 26 | class Raycaster { 27 | protected readonly event: any; 28 | protected readonly camera: THREE.PerspectiveCamera; 29 | constructor(event: any, camera: THREE.PerspectiveCamera) { 30 | this.event = event; 31 | this.camera = camera; 32 | } 33 | init() { 34 | const raycaster = new THREE.Raycaster( 35 | this.camera.position, 36 | this.normalize(), 37 | ); 38 | raycaster.camera = this.camera; 39 | return raycaster; 40 | } 41 | protected normalize = () => { 42 | const vector = new THREE.Vector3( 43 | (this.event.offsetX / window.innerWidth) * 2 - 1, 44 | -(this.event.offsetY / window.innerHeight) * 2 + 1, 45 | 0.5, 46 | ); //三维坐标对象 47 | vector.unproject(this.camera); 48 | return vector.sub(this.camera.position).normalize(); 49 | }; 50 | } 51 | 52 | class Controls { 53 | public readonly controls: any; 54 | constructor( 55 | camera: THREE.PerspectiveCamera, 56 | domElement: HTMLCanvasElement, 57 | params: Params, 58 | ) { 59 | const controls = new OrbitControls(camera, domElement); 60 | const { 61 | enableZoom = true, 62 | zoomSpeed = 0.5, 63 | enableDamping = true, 64 | maxDistance = 1000, 65 | minDistance = 30, 66 | rotateSpeed = 0.5, 67 | maxPolarAngle = Math.PI / 2, 68 | maxAzimuthAngle = Math.PI / 4, 69 | minAzimuthAngle = -Math.PI / 4, 70 | } = params.controls || {}; 71 | controls.enableZoom = enableZoom; // 启用或禁用摄像机的缩放。 72 | controls.zoomSpeed = zoomSpeed; // 摄像机缩放的速度 73 | controls.enableDamping = enableDamping; //启用阻尼 74 | controls.maxDistance = maxDistance ? maxDistance : params.far; // 相机向外移动多少 75 | controls.minDistance = minDistance; // 相机向内移动多少 76 | controls.rotateSpeed = rotateSpeed; //旋转的速度 77 | controls.maxPolarAngle = maxPolarAngle; //垂直旋转的角度的上限,范围是0到Math.PI 78 | controls.maxAzimuthAngle = maxAzimuthAngle; //水平旋转的角度的上限,范围是-Math.PI到Math.PI(或Infinity无限制) 79 | controls.minAzimuthAngle = minAzimuthAngle; //水平旋转的角度的下限,范围是-Math.PI到Math.PI(或-Infinity无限制), 80 | controls.screenSpacePanning = false; 81 | this.controls = controls; 82 | } 83 | } 84 | 85 | export default class Thre3d { 86 | protected readonly params: Params; 87 | public scene: THREE.Scene; // 网格 88 | public camera: THREE.PerspectiveCamera | null; // 相机 89 | public renderer: THREE.WebGLRenderer | null; // WebGL渲染器 90 | protected directionalLight: THREE.DirectionalLight | null; // 模拟太阳光 91 | private fnEvent: Map; // 事件类型函数 92 | public width: number; 93 | public height: number; 94 | protected controls: any; // 轨道控制器 95 | protected composer: any; 96 | protected dome: HTMLElement | null; 97 | protected cameraPositionsFn: Function | null; 98 | protected cameraLookAtFn: Function | null; 99 | labelRenderer: any; 100 | css3Renderer: any; 101 | constructor(params: Params) { 102 | this.params = params; 103 | this.cameraPositionsFn = null; 104 | this.cameraLookAtFn = null; 105 | this.width = 0; 106 | this.height = 0; 107 | this.labelRenderer = null; 108 | this.css3Renderer = null; 109 | this.dome = null; 110 | this.directionalLight = null; 111 | this.fnEvent = new Map(); 112 | this.controls = null; 113 | this.composer = null; 114 | this.renderer = null; 115 | this.camera = null; 116 | this.scene = new THREE.Scene(); 117 | this.#init(); 118 | } 119 | #init() { 120 | this.dome = document.querySelector(`#${this.params.id}`); 121 | if (!this.dome) return; 122 | this.width = this.dome.clientWidth; //宽度 123 | this.height = this.dome.clientHeight; //高度 124 | this.camera = new THREE.PerspectiveCamera( 125 | 30, 126 | this.width / this.height, 127 | 1, 128 | this.params.far ? this.params.far : 10000, 129 | ); 130 | this.renderer = new THREE.WebGLRenderer({ 131 | alpha: true, //渲染器透明 132 | antialias: true, //抗锯齿 133 | precision: "highp", //着色器开启高精度 134 | logarithmicDepthBuffer: true, // 是否使用对数深度缓存 135 | }); 136 | this.renderer.render(this.scene, this.camera); //执行渲染操作 137 | this.scene.fog = new THREE.Fog(0xffffff, 400, 10000); 138 | this.dome.addEventListener("click", this.#onMouseClick, false); //点击事件 139 | // WebGL渲染器 140 | const renderer = this.renderer; 141 | //开启HiDPI设置 142 | renderer.setPixelRatio(window.devicePixelRatio); 143 | renderer.shadowMap.enabled = true; 144 | renderer.toneMapping = THREE.ACESFilmicToneMapping; 145 | renderer.toneMappingExposure = 1; 146 | renderer.outputColorSpace = THREE.SRGBColorSpace; 147 | 148 | this.dome.appendChild(renderer.domElement); 149 | 150 | const labelRenderer = new CSS2DRenderer(); 151 | 152 | labelRenderer.domElement.style.position = "absolute"; 153 | // 相对标签原位置位置偏移大小 154 | labelRenderer.domElement.style.top = "0px"; 155 | labelRenderer.domElement.style.left = "0px"; 156 | labelRenderer.domElement.style.pointerEvents = "none"; 157 | this.labelRenderer = labelRenderer; 158 | this.dome.appendChild(labelRenderer.domElement); 159 | 160 | // 创建一个CSS3渲染器CSS3DRenderer 161 | const css3Renderer = new CSS3DRenderer(); 162 | 163 | // HTML标签
外面父元素叠加到canvas画布上且重合 164 | css3Renderer.domElement.style.position = "absolute"; 165 | css3Renderer.domElement.style.top = "0px"; 166 | //设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡 167 | css3Renderer.domElement.style.pointerEvents = "none"; 168 | this.css3Renderer = css3Renderer; 169 | this.dome.appendChild(css3Renderer.domElement); 170 | this.initControls(); 171 | this.#reset(); 172 | 173 | this.#setScene(); 174 | } 175 | // 添加发光通道 176 | addEffectComposer = () => { 177 | const effectColorSpaceConversion = new ShaderPass(GammaCorrectionShader); 178 | const composer = new EffectComposer(this.renderer); 179 | const renderPass = new RenderPass(this.scene, this.camera); 180 | composer.addPass(renderPass); 181 | const outlinePass = new OutlinePass( 182 | new THREE.Vector2(this.width, this.height), 183 | this.scene, 184 | this.camera, 185 | ); 186 | //模型描边颜色,默认白色 187 | outlinePass.visibleEdgeColor.set(0xffff00); 188 | //高亮发光描边厚度 189 | outlinePass.edgeThickness = 4; 190 | //高亮描边发光强度 191 | outlinePass.edgeStrength = 6; 192 | //模型闪烁频率控制,默认0不闪烁 193 | outlinePass.pulsePeriod = 2; 194 | composer.addPass(outlinePass); 195 | const pixelRatio = this.renderer!.getPixelRatio(); 196 | // SMAA抗锯齿通道 197 | const smaaPass = new SMAAPass( 198 | this.width * pixelRatio, 199 | this.height * pixelRatio, 200 | ); 201 | composer.addPass(smaaPass); 202 | 203 | this.renderer!.domElement.style.touchAction = "none"; 204 | this.renderer!.domElement.addEventListener("pointermove", onPointerMove); 205 | const mouse = new THREE.Vector2(); 206 | let selectedObjects: THREE.Object3D[] = []; 207 | function onPointerMove(event: any) { 208 | if (event.isPrimary === false) return; 209 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1; 210 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 211 | checkIntersection(); 212 | } 213 | const raycaster = new THREE.Raycaster(); 214 | const checkIntersection = () => { 215 | raycaster.setFromCamera(mouse, this.camera!); 216 | const intersects = raycaster.intersectObject(this.scene, true); 217 | if (intersects.length > 0) { 218 | const selectedObject = intersects[0].object; 219 | addSelectedObject(selectedObject); 220 | outlinePass.selectedObjects = selectedObjects; 221 | } else { 222 | // outlinePass.selectedObjects = []; 223 | } 224 | }; 225 | function addSelectedObject(object: THREE.Object3D) { 226 | selectedObjects = []; 227 | selectedObjects.push(object); 228 | } 229 | composer.addPass(effectColorSpaceConversion); 230 | this.composer = composer; 231 | }; 232 | #setScene = () => { 233 | this.scene.background = new THREE.Color(0x333333); 234 | // this.scene.fog = new THREE.Fog(0x333333, 10, 15); 235 | this.scene.environment = new RGBELoader().load( 236 | "https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr", 237 | ); 238 | this.scene.environment.mapping = THREE.EquirectangularReflectionMapping; 239 | // 天空 240 | // 注释,有需要自己添加背景图 241 | // this.scene.background = new THREE.CubeTextureLoader() 242 | // .setPath("/texture/sky/") 243 | // .load([ 244 | // "sky.left.jpg", 245 | // "sky.right.jpg", 246 | // "sky.top.jpg", 247 | // "sky.bottom.jpg", 248 | // "sky.back.jpg", 249 | // "sky.front.jpg", 250 | // ]); 251 | // const AxesHelper = new THREE.AxesHelper(10000); 252 | // AxesHelper.name = "辅助坐标系"; 253 | }; 254 | //点击事件 255 | #onMouseClick = (event: any) => { 256 | event.preventDefault(); 257 | const raycaster = new Raycaster(event, this.camera!).init(); 258 | const intersects = raycaster.intersectObjects(this.scene.children); 259 | const selected = intersects[0]; //intersects是射线沿着摄像机机镜头的方向穿过的所有物体,这里取第一个物体 260 | if (!selected) return; 261 | const { 262 | object: { type }, 263 | } = selected; 264 | const x = Math.floor(selected.point.x * 100) / 100; 265 | const y = Math.floor(selected.point.y * 100) / 100; 266 | const z = Math.floor(selected.point.z * 100) / 100; 267 | console.log("x:" + x + ",y:" + y + ",z:" + z); 268 | let returnFlag = false; 269 | let sFn, mFn; 270 | switch (type) { 271 | case "Sprite": 272 | sFn = this.fnEvent.get("spriteClick"); 273 | if (sFn) sFn(selected); 274 | returnFlag = true; 275 | console.log("精灵图", selected); 276 | break; 277 | case "Mesh": 278 | mFn = this.fnEvent.get("modelClick"); 279 | if (mFn) mFn(selected); 280 | break; 281 | default: 282 | console.log("暂未定义的类型", type); 283 | } 284 | if (returnFlag) return; 285 | const cFn = this.fnEvent.get("click"); 286 | if (cFn) cFn(new THREE.Vector3(x, y, z)); 287 | }; 288 | #onMouseMove = (event: any) => { 289 | event.preventDefault(); 290 | const raycaster = new Raycaster(event, this.camera!).init(); 291 | const intersects = raycaster.intersectObjects(this.scene.children); 292 | const selected = intersects[0]; //intersects是射线沿着摄像机机镜头的方向穿过的所有物体,这里取第一个物体 293 | if (!selected) return; 294 | const x = Math.floor(selected.point.x * 1000) / 1000; 295 | const y = Math.floor(selected.point.y * 1000) / 1000; 296 | const z = Math.floor(selected.point.z * 1000) / 1000; 297 | const address = new THREE.Vector3(x, y, z); 298 | const mFn = this.fnEvent.get("mouseMove"); 299 | if (mFn) mFn(address); 300 | return address; 301 | }; 302 | render = () => { 303 | if (this.renderer) { 304 | this.renderer.render(this.scene, this.camera!); //执行渲染操作 305 | } 306 | if (this.composer) { 307 | this.composer.render(this.scene, this.camera); 308 | } 309 | if (this.labelRenderer) { 310 | this.labelRenderer.render(this.scene, this.camera); 311 | } 312 | if (this.css3Renderer) { 313 | this.css3Renderer.render(this.scene, this.camera); 314 | } 315 | if (this.controls) { 316 | this.controls.update(); 317 | } 318 | if (this.cameraPositionsFn) { 319 | this.cameraPositionsFn(); 320 | } 321 | if (this.cameraLookAtFn) { 322 | this.cameraLookAtFn(); 323 | } 324 | if ( 325 | this.directionalLight && 326 | this.params.shadow && 327 | this.params.timeMultiply 328 | ) { 329 | const r = Date.now() * (0.0001 / this.params.timeMultiply); 330 | this.directionalLight.position.x = 700 * Math.cos(r); 331 | this.directionalLight.position.y = 700 * Math.sin(r); 332 | // const angle: number = 333 | // (new THREE.Vector2( 334 | // this.directionalLight.position.x, 335 | // this.directionalLight.position.y, 336 | // ).angle() * 337 | // 180) / 338 | // Math.PI; 339 | // if (1 < angle && angle < 180) { 340 | // console.log("白天", angle); 341 | // } else { 342 | // console.log("夜晚", angle); 343 | // } 344 | } 345 | }; 346 | resize = () => { 347 | this.dome = document.querySelector(`#${this.params.id}`); 348 | if (!this.dome) return; 349 | this.width = this.dome.clientWidth; //宽度 350 | this.height = this.dome.clientHeight; //高度 351 | this.#reset(); 352 | }; 353 | #reset = () => { 354 | this.camera!.aspect = this.width / this.height; 355 | this.camera!.updateProjectionMatrix(); 356 | this.renderer!.setSize(this.width, this.height); 357 | this.labelRenderer.setSize(this.width, this.height); 358 | this.css3Renderer.setSize(this.width, this.height); 359 | }; 360 | /** 平行光 361 | * @param {Vector3} position 坐标 362 | * @param {boolean} intensity 光线强度 363 | */ 364 | addSun(position: Vector3 | undefined, intensity: number | undefined = 2.5) { 365 | const { x = 2000, y = 40000, z = 2000 } = position || {}; 366 | if (this.params.shadow) { 367 | const directionalLight = new THREE.DirectionalLight(0xffffff, intensity); // 新建一个平行光源,颜色未白色,强度为1 368 | directionalLight.position.set(x, y, z); // 将此平行光源调整到一个合适的位置 369 | directionalLight.castShadow = true; // 将此平行光源产生阴影的属性打开 370 | const d = 100; //阴影范围 // 设置平行光的的阴影属性,即一个长方体的长宽高,在设定值的范围内的物体才会产生阴影 371 | directionalLight.shadow.camera.left = -d; 372 | directionalLight.shadow.camera.right = d; 373 | directionalLight.shadow.camera.top = d; 374 | directionalLight.shadow.camera.bottom = -d; 375 | directionalLight.shadow.camera.near = 20; 376 | directionalLight.shadow.camera.far = 8000; 377 | directionalLight.shadow.mapSize.x = 2048; // 定义阴影贴图的宽度和高度,必须为2的整数此幂 378 | directionalLight.shadow.mapSize.y = 2048; // 较高的值会以计算时间为代价提供更好的阴影质量 379 | directionalLight.shadow.bias = -0.0005; //解决条纹阴影的出现 380 | this.directionalLight = directionalLight; 381 | directionalLight.name = "sunShadow"; 382 | this.scene.add(directionalLight); // 将此平行光源加入场景中,我们才可以看到这个光源 383 | } else { 384 | const group = new THREE.Group(); 385 | group.name = "sun"; 386 | const directionalLight1 = new THREE.DirectionalLight( 387 | 0xffffff, 388 | intensity / 4, 389 | ); 390 | directionalLight1.position.set(x, y, z); 391 | 392 | group.add(directionalLight1); 393 | this.scene.add(group); 394 | } 395 | } 396 | // 注册事件 397 | on(type: FnRenderType, fn: Function) { 398 | this.fnEvent.set(type, fn); 399 | } 400 | // 移除事件 401 | removeOn(type: FnRenderType) { 402 | const m = this.fnEvent.get(type); 403 | if (m) this.fnEvent.delete(type); 404 | } 405 | /** 406 | * 切换视角 407 | * @param {Vector3} position position目的坐标 408 | * @param {boolean} animation 是否启动动画 409 | * @param {Function} callback 到达目的回调函数 410 | */ 411 | setCameraPositions = ( 412 | position: Vector3, 413 | animation?: boolean, 414 | callback?: Function, 415 | ) => { 416 | if (!animation) { 417 | this.cameraPositionsFn = null; 418 | this.camera!.position.set(position.x, position.y, position.z); 419 | if (callback) callback(); 420 | return; 421 | } 422 | const endPosition = new THREE.Vector3(position.x, position.y, position.z); // 结束位置 423 | const M = new MoveTo(this.camera!.position, endPosition, 5); 424 | const target = JSON.parse(JSON.stringify(this.controls.target)); 425 | this.cameraPositionsFn = () => { 426 | this.controls.target.set(0, 0, 0); 427 | this.controls.update(); 428 | const manhattanDistanceTo = M.move(); 429 | this.controls.target.set(target.x, target.y, target.z); 430 | this.controls.update(); 431 | if (manhattanDistanceTo < 1) { 432 | this.cameraPositionsFn = null; 433 | } 434 | // 435 | }; 436 | }; 437 | /**切换注视点 438 | * @param {Vector3} position position目的坐标 439 | * @param {boolean} animation 是否启动动画 440 | * @param {Function} callback 到达目的回调函数 441 | * @returns 442 | */ 443 | setCameraLookAt = ( 444 | position: Vector3, 445 | animation?: boolean, 446 | callback?: Function, 447 | ) => { 448 | if (!animation) { 449 | if (!this.camera) return; 450 | this.cameraLookAtFn = null; 451 | this.camera.lookAt(position.x, position.y, position.z); 452 | if (!this.controls) return; 453 | this.controls.target.set(position.x, position.y, position.z); 454 | if (callback) callback(); 455 | return; 456 | } 457 | const endPosition = new THREE.Vector3(position.x, position.y, position.z); // 结束位置 458 | const M = new MoveTo(this.controls.target, endPosition, 30); 459 | this.cameraLookAtFn = () => { 460 | const manhattanDistanceTo = M.move(); 461 | if (manhattanDistanceTo < 1) { 462 | this.controls.target.copy(endPosition); 463 | this.cameraLookAtFn = null; 464 | if (callback) callback(); 465 | } 466 | }; 467 | }; 468 | removeControls() { 469 | if (!this.controls) return; 470 | this.controls.object = null; 471 | this.controls = null; 472 | } 473 | initControls() { 474 | if (!this.camera || !this.renderer) return; 475 | const { 476 | cameraPosition: { x = 0, y = 100, z = 200 }, 477 | lookAt, 478 | } = this.params; 479 | const { x: lookAtX = 0, y: lookAtY = 0, z: lookAtZ = 0 } = lookAt || {}; 480 | // 设置相机控件轨道控制器OrbitControls 481 | const controls = new Controls( 482 | this.camera, 483 | this.renderer.domElement, 484 | this.params, 485 | ).controls; 486 | controls.target.set(lookAtX, lookAtY, lookAtZ); 487 | this.controls = controls; 488 | this.camera.lookAt(lookAtX, lookAtY, lookAtZ); 489 | this.camera.position.set(x, y, z); 490 | this.controls.update(); 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/index.ts: -------------------------------------------------------------------------------- 1 | import three from "./Three"; 2 | import ModelThree from "./ModelThree"; 3 | import SpriteThree from "./SpriteThree"; 4 | import { Params } from "./interface/index"; 5 | import * as THREE from "three"; 6 | import { openDB } from "./indexdb"; 7 | export default class SLThree { 8 | public readonly Three: three; 9 | public readonly ModelThree: ModelThree; 10 | public readonly SpriteThree: SpriteThree; 11 | 12 | constructor(params: Params) { 13 | let db; 14 | if (openDB) { 15 | openDB("threeDB", "model", 2)!.then((v) => { 16 | db = v; 17 | }); 18 | } 19 | this.Three = new three(params); 20 | const scene = this.Three.scene; 21 | const camera = this.Three.camera!; 22 | const renderer = this.Three.renderer!; 23 | this.ModelThree = new ModelThree(scene, camera, renderer, params, db); 24 | this.SpriteThree = new SpriteThree(scene, camera, { 25 | width: this.Three.width, 26 | height: this.Three.height, 27 | }); 28 | let fps = 0; 29 | const clock = new THREE.Clock(); 30 | const render = () => { 31 | const dt = clock.getDelta(); 32 | fps = Math.floor(1 / dt); 33 | this.Three.render(dt); 34 | this.ModelThree.render(dt); 35 | requestAnimationFrame(render); 36 | }; 37 | render(); 38 | window.addEventListener("resize", () => { 39 | this.Three.resize(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/indexdb/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param dbName 数据库名称 4 | * @param storeName 仓库名称 5 | * @param version 版本号 6 | * @returns 7 | */ 8 | export const openDB = (dbName: string, storeName: string, version: number) => { 9 | if (!indexedDB) return; 10 | return new Promise((res, rej) => { 11 | const request = window.indexedDB.open(dbName, version); 12 | request.onerror = (event) => { 13 | rej(event); 14 | }; 15 | request.onsuccess = (event: any) => { 16 | if (!event) { 17 | return rej(event); 18 | } 19 | const db = event.target.result; 20 | res(db); 21 | }; 22 | request.onupgradeneeded = (event: any) => { 23 | const db = event.target.result; 24 | const objectStore = db.createObjectStore(storeName, { keyPath: "path" }); 25 | objectStore.createIndex("model", "model"); 26 | }; 27 | }); 28 | }; 29 | 30 | export const addData = (db: any, storeName: string, data: any) => { 31 | return new Promise((res, rej) => { 32 | const transaction = db.transaction([storeName], "readwrite"); 33 | transaction.oncomplete = (event: any) => { 34 | res(event); 35 | }; 36 | transaction.onerror = (event: any) => { 37 | rej(event); 38 | }; 39 | const objectStore = transaction.objectStore(storeName); 40 | objectStore.put(data); 41 | }); 42 | }; 43 | 44 | export const getData = (db: any, storeName: string, path: string) => { 45 | return new Promise((res, rej) => { 46 | const transaction = db.transaction([storeName], "readwrite"); 47 | const objectStore = transaction.objectStore(storeName); 48 | const request = objectStore.get(path); 49 | request.onerror = (event: any) => { 50 | rej(event); 51 | }; 52 | request.onsuccess = (event: any) => { 53 | res(event.target.result); 54 | }; 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/interface/index.ts: -------------------------------------------------------------------------------- 1 | interface Controls { 2 | enableZoom?: boolean; // 启用或禁用摄像机的缩放。==>true 3 | zoomSpeed?: number; // 摄像机缩放的速度 ==>0.5 4 | enableDamping?: boolean; //启用阻尼 ==> true 5 | maxDistance?: number; // 相机向外移动多少 ==> 1000 6 | minDistance?: number; // 相机向内移动多少 ==> 30 7 | rotateSpeed?: number; // 旋转的速度 ==> 0.5 8 | maxPolarAngle?: number; // 垂直旋转的角度的上限,范围是0到Math.PI ==> Math.PI / 2 9 | maxAzimuthAngle?: number; // 水平旋转的角度的上限,范围是-Math.PI到Math.PI(或Infinity无限制) ==> Math.PI / 4, 10 | minAzimuthAngle?: number; // 水平旋转的角度的下限,范围是-Math.PI到Math.PI(或-Infinity无限制) ==> -Math.PI / 4, 11 | } 12 | export interface Params { 13 | id: string; 14 | cameraPosition: Vector3; // 相机坐标 15 | lookAt?: Vector3; // 相机视角 16 | timeMultiply?: number; // 模拟太阳光旋转时间倍率 timeMultiply * 31.4秒 仅 shadow为true有效 17 | shadow?: boolean; // 阴影 18 | far?: number; // 摄像机视锥体远端面 19 | lightPostion?: Vector3; // 光照高度 20 | intensity?: number; // 光照强度 21 | bgImageArray?: string[]; // 背景图 22 | controls?: Controls; //相机控制器 23 | } 24 | export type Vector3 = { 25 | x: number; 26 | y: number; 27 | z: number; 28 | }; 29 | 30 | export interface SetModel { 31 | name: string; 32 | position: Vector3; 33 | } 34 | 35 | export interface ModelParams { 36 | name: string; // 模型名 37 | url: string; // 模型路径 38 | KTX2?: string; //KTX2材质 39 | position: Vector3; // 模型坐标 40 | scale?: Vector3; // 模型缩放比例 41 | rotation?: Vector3; // 模型旋转弧度 42 | activeAction?: string; //模型默认播放动画 43 | DRACO?: string; 44 | } 45 | 46 | export interface ModelMove { 47 | name: string; 48 | speed: number[]; 49 | dt: number; 50 | } 51 | 52 | // 注册回调事件类型,点击事件click和渲染事件render,精灵图点击spriteClick,模型点击modelClick 53 | export type FnRenderType = "click" | "spriteClick" | "modelClick" | "mouseMove"; 54 | 55 | export type ModelMap = { 56 | mode: any; // 模型 57 | actions: { 58 | // 播放动画使用 59 | [key: string]: THREE.AnimationAction; 60 | }; 61 | mixerArray: THREE.AnimationMixer[]; // 推进混合器时间并更新动画,在渲染循环中完成 62 | }; 63 | 64 | export interface Loader { 65 | loaderStartTime: number; //加载开始时间 66 | loaderEndTime: number; // 加载结束时间 67 | loader: number; // 加载进度 68 | renderStatus: boolean; // 状态 69 | renderTime: null | number; // 渲染时间 70 | } 71 | 72 | export interface SpriteParams { 73 | value: string; 74 | x?: number; 75 | y?: number; 76 | z?: number; 77 | width?: number; 78 | height?: number; 79 | color?: string; 80 | zoom?: number; 81 | name?: string; 82 | } 83 | 84 | export interface ShapeGeometry { 85 | name: string; // 围栏名称 *唯一性 86 | visible?: boolean; // 围栏显示状态 默认显示 87 | type?: "curve" | "line" | "face"; // 围栏类型 , curve曲线 line 直线 默认值为line 88 | color?: string; //"围栏颜色" 89 | linewidth?: number; // 围栏宽度 90 | linecap?: "butt" | "round" | "square"; // 两端的样式 默认值为 'round' 91 | } 92 | 93 | export type LabelType = "CSS2DObject" | "CSS3DObject" | "CSS3DSprite"; 94 | 95 | export interface LabelParams { 96 | name: string; // 标签名称 *唯一性 97 | type: LabelType; // 标签类型 98 | element: HTMLElement; // html 99 | position?: Vector3; // 坐标 100 | scale?: Vector3; // 缩放比例 101 | } 102 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/utils/three/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { Vector3 } from "../interface/index"; 3 | export class MoveTo { 4 | start: THREE.Vector3; 5 | end: THREE.Vector3; 6 | v: number; 7 | manhattanDistanceToNumber: number; 8 | constructor(start: THREE.Vector3, end: THREE.Vector3, v?: number) { 9 | this.start = start; 10 | this.end = end; 11 | this.v = v ? v : Math.ceil(this.manhattanDistanceTo() / 200); 12 | this.manhattanDistanceToNumber = 0; 13 | } 14 | setSpeed(v: number) { 15 | this.v = v; 16 | } 17 | manhattanDistanceTo = () => { 18 | return this.start.manhattanDistanceTo(this.end); 19 | }; 20 | move() { 21 | const delta = new THREE.Vector3() 22 | .subVectors(this.end, this.start) 23 | .normalize(); 24 | const manhattanDistanceTo = this.manhattanDistanceTo(); 25 | if (manhattanDistanceTo < this.v && manhattanDistanceTo >= 1) { 26 | this.start.addScaledVector(delta, 1); 27 | } else if (manhattanDistanceTo > this.v) { 28 | this.start.addScaledVector(delta, this.v); 29 | } 30 | if (this.manhattanDistanceToNumber === Math.floor(manhattanDistanceTo)) { 31 | return 0; 32 | } 33 | this.manhattanDistanceToNumber = Math.floor(manhattanDistanceTo); 34 | return manhattanDistanceTo; 35 | } 36 | } 37 | 38 | /** 39 | *巡逻 40 | * @param {} params 41 | * @param {} params.three3D 场景 42 | * @param {} params.coordArray 坐标数组 43 | * @param {} params.meshName 模型名称 44 | * @param {} params.isFirstPerson 是否是第三人称 45 | * @param {} params.modelType 跟随的是模型还是图标 46 | * @param {} params.factor 移动因素 1 |10 |100 47 | */ 48 | export class Patrol { 49 | private three3D: any; 50 | private meshName: string; 51 | private isFirstPerson: boolean; 52 | private curvePoints: Vector3[]; 53 | private runGen: any; 54 | private modelType?: "mash" | "sprite"; 55 | private factor?: 1 | 10 | 100; 56 | isStop: boolean; 57 | constructor(params: { 58 | three3D: any; 59 | coordArray: Vector3[]; 60 | meshName?: string; 61 | isFirstPerson?: boolean; 62 | modelType?: "mash" | "sprite"; 63 | factor?: 1 | 10 | 100; 64 | }) { 65 | const { 66 | three3D, 67 | coordArray, 68 | meshName = "", 69 | isFirstPerson = false, 70 | modelType = "mash", 71 | factor = 1, 72 | } = params; 73 | this.isFirstPerson = isFirstPerson; 74 | this.three3D = three3D; 75 | this.meshName = meshName; 76 | this.isStop = false; 77 | this.modelType = modelType; 78 | this.factor = factor; 79 | // 总路长 80 | let distanceToPoint = 0; 81 | const points = coordArray.map((p) => new THREE.Vector3(p.x, p.y, p.z)); 82 | points.forEach((p, i) => { 83 | if (i !== 0 && points[i + 1]) { 84 | distanceToPoint += p.distanceTo(points[i + 1]); 85 | } 86 | }); 87 | console.log("距离", distanceToPoint); 88 | const curve = new THREE.CatmullRomCurve3(points, false); 89 | this.curvePoints = curve.getPoints(distanceToPoint * (100 / factor)); 90 | // this.init(); 91 | } 92 | init() { 93 | console.log("初始化"); 94 | 95 | // eslint-disable-next-line @typescript-eslint/no-this-alias 96 | const vm = this; 97 | async function* stepGen() { 98 | for (let i = 0; i < vm.curvePoints.length; i++) { 99 | yield vm.step(vm.curvePoints, i); 100 | } 101 | yield false; 102 | } 103 | this.runGen = stepGen(); 104 | } 105 | stop() { 106 | this.isStop = true; 107 | } 108 | run() { 109 | if (!this.isStop && this.runGen) return; 110 | if (!this.runGen) { 111 | this.init(); 112 | } 113 | this.isStop = false; 114 | if (this.isFirstPerson) { 115 | this.three3D.Three.removeControls(); 116 | } 117 | this.asyncGenerator(this.runGen); 118 | } 119 | switch(flag: boolean) { 120 | this.isFirstPerson = flag; 121 | if (this.isFirstPerson) { 122 | this.three3D.Three.removeControls(); 123 | } else { 124 | this.three3D.Three.initControls(); 125 | } 126 | } 127 | private async asyncGenerator(g: any) { 128 | g.next().then((v: any) => { 129 | if (!v || !v.value) { 130 | if (this.isFirstPerson) { 131 | this.three3D.Three.initControls(); 132 | } 133 | this.runGen = null; 134 | return; 135 | } 136 | if (this.isStop) { 137 | if (this.isFirstPerson) { 138 | this.three3D.Three.initControls(); 139 | } 140 | return; 141 | } 142 | setTimeout(() => { 143 | this.asyncGenerator(g); 144 | }, 1); 145 | }); 146 | } 147 | private step(point: Vector3[], i: number): Promise { 148 | // eslint-disable-next-line @typescript-eslint/no-this-alias 149 | const vm = this; 150 | return new Promise((res, rej) => { 151 | if (!this.three3D) return rej(false); 152 | if (this.modelType === "mash") { 153 | vm._runModel(point, i, res, vm); 154 | } else { 155 | vm._runSprite(point, i, res, vm); 156 | } 157 | }); 158 | } 159 | private _runModel(point: Vector3[], i: number, res: Function, vm: this) { 160 | if (this.meshName) { 161 | this.three3D.ModelThree.modelMove( 162 | this.meshName, 163 | point[i], 164 | true, 165 | 1, 166 | function () { 167 | res(true); 168 | }, 169 | function (position: Vector3) { 170 | if (vm.isFirstPerson) { 171 | vm.three3D?.Three.setCameraPositions(position); 172 | vm.three3D?.Three.setCameraLookAt(point[i]); 173 | } 174 | }, 175 | ); 176 | } else { 177 | res(true); 178 | if (vm.isFirstPerson) { 179 | this.three3D?.Three.setCameraPositions(point[i]); 180 | this.three3D?.Three.setCameraLookAt(point[i + 1]); 181 | } 182 | } 183 | } 184 | private _runSprite(point: Vector3[], i: number, res: Function, vm: this) { 185 | if (this.meshName) { 186 | this.three3D.SpriteThree.setLabelPostion(this.meshName, point[i]); 187 | } else { 188 | if (vm.isFirstPerson) { 189 | this.three3D?.Three.setCameraPositions(point[i]); 190 | this.three3D?.Three.setCameraLookAt(point[i + 1]); 191 | } 192 | } 193 | res(true); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/error/404.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/error/404.ts: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | slot: {}, 4 | hook: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 9 | 15 | 24 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/a.scss: -------------------------------------------------------------------------------- 1 | //#hook_1 2 | 3 | //#end; 4 | return { 5 | hook_1:{ 6 | element:function(){ 7 | return `` 8 | }, 9 | antdv:function(){ 10 | return `` 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/a.ts: -------------------------------------------------------------------------------- 1 | //#hook:hook_1 2 | //#slot:ui_1 3 | 4 | //#end; 5 | return { 6 | slot: { 7 | ui_1: { 8 | element: function ui_1() { 9 | return ``; 10 | }, 11 | antdv: function ui_1() { 12 | return ``; 13 | }, 14 | }, 15 | }, 16 | hook: { 17 | hook_1: { 18 | echarts: { 19 | HOOK: function () { 20 | return ""; 21 | }, 22 | FALSE: function () { 23 | return ``; 24 | }, 25 | element: function () {}, 26 | antdv: function () {}, 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/a.vue: -------------------------------------------------------------------------------- 1 | 4 | //#end; 5 | 31 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/b.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/b.ts: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | slot: {}, 4 | hook: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/b.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/c.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/c.ts: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | slot: {}, 4 | hook: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/option_1/c.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/home/home.scss: -------------------------------------------------------------------------------- 1 | //#end; 2 | return { 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/home/home.ts: -------------------------------------------------------------------------------- 1 | import { useUserInfoStore } from "@/store/modules/user"; 2 | //#hook:hook_1 3 | const userInfoStore = useUserInfoStore(); 4 | const styleIcon = { 5 | width: "80px", 6 | height: "80px", 7 | fontSize: "80px", 8 | }; 9 | 10 | //#end; 11 | () => { 12 | console.log(userEcharts, ref); 13 | }; 14 | return { 15 | slot: {}, 16 | hook: { 17 | hook_1: { 18 | echarts: { 19 | HOOK: function () { 20 | return `import userEcharts from "@/hooks/userEcharts"; 21 | import { ref } from "vue"; 22 | const option_1 = ref({ 23 | title: { 24 | text: "Website", 25 | subtext: "Fake Data", 26 | left: "center", 27 | }, 28 | tooltip: { 29 | trigger: "item", 30 | }, 31 | legend: { 32 | orient: "vertical", 33 | left: "left", 34 | }, 35 | series: [ 36 | { 37 | name: "Access From", 38 | type: "pie", 39 | radius: "50%", 40 | data: [ 41 | { value: 1048, name: "Search Engine" }, 42 | { value: 735, name: "Direct" }, 43 | { value: 580, name: "Email" }, 44 | { value: 484, name: "Union Ads" }, 45 | { value: 300, name: "Video Ads" }, 46 | ], 47 | emphasis: { 48 | itemStyle: { 49 | shadowBlur: 10, 50 | shadowOffsetX: 0, 51 | shadowColor: "rgba(0, 0, 0, 0.5)", 52 | }, 53 | }, 54 | }, 55 | ], 56 | }); 57 | 58 | const option_2 = ref({ 59 | xAxis: { 60 | type: "category", 61 | boundaryGap: false, 62 | data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 63 | }, 64 | yAxis: { 65 | type: "value", 66 | }, 67 | series: [ 68 | { 69 | data: [820, 932, 901, 934, 1290, 1330, 1320], 70 | type: "line", 71 | areaStyle: {}, 72 | }, 73 | ], 74 | }); 75 | 76 | userEcharts(option_1, "leftEcharts"); 77 | userEcharts(option_2, "rightEcharts");`; 78 | }, 79 | element: function () {}, 80 | antdv: function () {}, 81 | }, 82 | }, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/home/home.vue: -------------------------------------------------------------------------------- 1 | 52 | 133 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/login/index.ts: -------------------------------------------------------------------------------- 1 | import SingIn from "./sign-in.vue"; 2 | const styleIcon = { 3 | width: "400px", 4 | height: "500px", 5 | fontSize: "18px", 6 | }; 7 | //#end; 8 | () => { 9 | console.log(SingIn); 10 | }; 11 | return { 12 | slot: {}, 13 | hook: {}, 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/login/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 104 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/login/sign-in.ts: -------------------------------------------------------------------------------- 1 | import { reactive, ref } from "vue"; 2 | import { useUserInfoStore } from "@/store/modules/user"; 3 | import { useRouter } from "vue-router"; 4 | import type { FormRules, FormInstance } from "element-plus"; 5 | import { userLoginApi } from "@/api/user"; 6 | const userInfoStore = useUserInfoStore(); 7 | const router = useRouter(); 8 | const styleIcon = { 9 | width: "16px", 10 | height: "16px", 11 | fontSize: "16px", 12 | }; 13 | interface RuleForm { 14 | name: string; 15 | password: string; 16 | } 17 | const loginForm = reactive({ 18 | name: "", 19 | password: "", 20 | }); 21 | 22 | const rules = reactive>({ 23 | name: [ 24 | { 25 | required: true, 26 | message: "Please input Account ", 27 | trigger: "change", 28 | }, 29 | ], 30 | password: [ 31 | { required: true, message: "Please input Password", trigger: "change" }, 32 | ], 33 | }); 34 | 35 | const ruleFormRef = ref(); 36 | const loadingFlag = ref(false); 37 | const submitForm = async (formEl: FormInstance | undefined) => { 38 | loadingFlag.value = true; 39 | if (!formEl) return (loadingFlag.value = false); 40 | formEl 41 | .validate() 42 | .then(async () => { 43 | const { data } = await userLoginApi(loginForm); 44 | const { userName, token, nickName } = data; 45 | loadingFlag.value = false; 46 | if (!token) { 47 | return alert("测试账号 test 和admin ,密码任意字符"); 48 | } 49 | userInfoStore.login({ 50 | userName, 51 | token, 52 | nickName, 53 | }); 54 | 55 | router.push("/"); 56 | }) 57 | .catch((fields) => { 58 | loadingFlag.value = false; 59 | console.log("error submit!", fields); 60 | }); 61 | }; 62 | 63 | //#end; 64 | return { 65 | slot: {}, 66 | hook: {}, 67 | }; 68 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/resident/login/sign-in.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 71 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/system/router/router.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/system/user/editUser.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import userVModel from "@/hooks/userVModel"; 3 | import type { FormInstance } from "element-plus"; 4 | const emit = defineEmits(["update:form", "update:state"]); 5 | 6 | const props = defineProps(["form", "state"]); 7 | 8 | const form = userVModel(props, "form", emit); 9 | const state = userVModel(props, "state", emit); 10 | 11 | const loadingFlag = ref(false); 12 | const submitForm = async (formEl: FormInstance | undefined) => { 13 | loadingFlag.value = true; 14 | if (!formEl) return (loadingFlag.value = false); 15 | formEl 16 | .validate() 17 | .then(async () => { 18 | ElMessage.error("演绎模式,无法操作"); 19 | }) 20 | .catch((fields) => { 21 | loadingFlag.value = false; 22 | console.log("error submit!", fields); 23 | }); 24 | }; 25 | const ruleFormRef = ref(); 26 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/system/user/editUser.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/system/user/user.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import Table from "@/components/table/table.vue"; 3 | import EditUser from "./editUser.vue"; 4 | 5 | const dataSource = [ 6 | { 7 | userName: "aaa", 8 | nickname: "测试用户", 9 | role: "admin", 10 | email: "8888@qq.com", 11 | phoneNumber: "19166666666", 12 | createTime: "2024/3/29", 13 | updateTime: "2024/3/29", 14 | }, 15 | ]; 16 | const columns = [ 17 | { 18 | title: "用户名", 19 | dataIndex: "userName", 20 | key: "userName", 21 | }, 22 | { 23 | title: "昵称", 24 | dataIndex: "nickname", 25 | key: "nickname", 26 | }, 27 | { 28 | title: "角色", 29 | dataIndex: "role", 30 | key: "role", 31 | }, 32 | { 33 | title: "邮箱", 34 | dataIndex: "email", 35 | key: "email", 36 | }, 37 | { 38 | title: "手机", 39 | dataIndex: "phoneNumber", 40 | key: "phoneNumber", 41 | }, 42 | { 43 | title: "创建时间", 44 | dataIndex: "createTime", 45 | key: "createTime", 46 | }, 47 | { 48 | title: "修改时间", 49 | dataIndex: "updateTime", 50 | key: "updateTime", 51 | }, 52 | ]; 53 | 54 | const form = ref({}); 55 | const state = ref<{ 56 | show: boolean; 57 | status: "edit" | "add"; 58 | }>({ 59 | show: false, 60 | status: "add", 61 | }); 62 | 63 | const add = () => { 64 | form.value = {}; 65 | state.value.show = true; 66 | state.value.status = "add"; 67 | }; 68 | const edit = () => { 69 | form.value = { 70 | userName: "aaa", 71 | nickname: "测试用户", 72 | role: "admin", 73 | email: "8888@qq.com", 74 | phoneNumber: "19166666666", 75 | createTime: "2024/3/29", 76 | updateTime: "2024/3/29", 77 | }; 78 | state.value.show = true; 79 | state.value.status = "edit"; 80 | }; 81 | //#end; 82 | () => { 83 | console.log(Table, EditUser); 84 | }; 85 | return { 86 | slot: {}, 87 | hook: {}, 88 | }; 89 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/system/user/user.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/three/three.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from "vue"; 2 | import modelThree from "@/utils/three/index"; 3 | import * as THREE from "three"; 4 | 5 | const officeThree = ref(null); 6 | const params = { 7 | id: "office", 8 | cameraPosition: { 9 | x: 1, 10 | y: 0.3, 11 | z: 0, 12 | }, 13 | lookAt: { x: 0, y: 0, z: 0 }, 14 | timeMultiply: 0, // 31.4秒一圈的倍率仅 shadow为true有效 15 | shadow: false, // 阴影 16 | // far: 10000; // 摄像机视锥体远端面 17 | lightPostion: { x: 0, y: 10, z: 10 }, 18 | intensity: 7, // 光照强度 19 | // bgImageArray: string[] // 背景图 20 | controls: { 21 | //相机控制器 22 | enableZoom: true, // 启用或禁用摄像机的缩放。==>true 23 | zoomSpeed: 1, // 摄像机缩放的速度 ==>0.5 24 | enableDamping: true, //启用阻尼 ==> true 25 | maxDistance: 100, // 相机向外移动多少 ==> 1000 26 | minDistance: 10, // 相机向内移动多少 ==> 30 27 | rotateSpeed: 0.1, // 旋转的速度 ==> 0.5 28 | maxPolarAngle: Math.PI / 2, // 垂直旋转的角度的上限,范围是0到Math.PI ==> Math.PI / 2 29 | maxAzimuthAngle: Math.PI, // 水平旋转的角度的上限,范围是-Math.PI到Math.PI(或Infinity无限制) ==> Math.PI / 4, 30 | minAzimuthAngle: -Math.PI, // 水平旋转的角度的下限,范围是-Math.PI到Math.PI(或-Infinity无限制) ==> -Math.PI / 4, 31 | }, 32 | }; 33 | let grid; 34 | const loadModel = (url: string) => { 35 | if (!officeThree.value) return; 36 | officeThree.value.ModelThree.loadModel( 37 | { 38 | name: "car", // 模型名 39 | url: url, // 模型路径 40 | position: { x: 0, y: 0, z: 0 }, // 模型坐标 41 | scale: { x: 1, y: 1, z: 1 }, // 模型缩放比例 42 | DRACO: "https://threejs.org/examples/jsm/libs/draco/gltf/", 43 | rotation: { x: 0, y: 0, z: 0 }, // 模型旋转弧度 44 | }, 45 | () => { 46 | const glass = officeThree.value.ModelThree.getModel("glass"); 47 | if (!glass) return; 48 | const glassMaterial = new THREE.MeshPhysicalMaterial({ 49 | color: 0xffffff, 50 | metalness: 0.25, 51 | roughness: 0, 52 | transmission: 1.0, 53 | }); 54 | glass.material = glassMaterial; 55 | carWheelRun(); 56 | 57 | grid = new THREE.GridHelper(20, 40, 0xffffff, 0xffffff); 58 | grid.material.opacity = 0.2; 59 | grid.material.depthWrite = false; 60 | grid.material.transparent = true; 61 | 62 | officeThree.value.Three.scene.add(grid); 63 | }, 64 | ); 65 | }; 66 | 67 | const carWheelRun = () => { 68 | const model = []; 69 | const wheel_fl = officeThree.value.ModelThree.getModel("wheel_fl"); 70 | if (!wheel_fl) return; 71 | const wheel_fr = officeThree.value.ModelThree.getModel("wheel_fr"); 72 | const wheel_rl = officeThree.value.ModelThree.getModel("wheel_rl"); 73 | const wheel_rr = officeThree.value.ModelThree.getModel("wheel_rr"); 74 | model.push(wheel_fl, wheel_fr, wheel_rl, wheel_rr); 75 | 76 | setInterval(() => { 77 | for (let i = 0; i < model.length; i++) { 78 | const time = -new Date() / 2000; 79 | model[i].rotation.x = time * Math.PI * 2; 80 | grid.position.z = -time % 1; 81 | } 82 | }, 100); 83 | }; 84 | 85 | const initThree3d = () => { 86 | officeThree.value = new modelThree(params); 87 | loadModel("https://threejs.org/examples/models/gltf/ferrari.glb"); 88 | }; 89 | 90 | onMounted(() => { 91 | initThree3d(); 92 | }); 93 | -------------------------------------------------------------------------------- /src/commands/create/project_template/src/views/three/three.vue: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /src/commands/create/project_template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "ignoreDeprecations": "5.0", 13 | "suppressImplicitAnyIndexErrors": true, 14 | "lib": ["ESNext", "DOM"], 15 | "skipLibCheck": true, 16 | "noEmit": true, 17 | // 配置@别名 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src/**/*.ts", "**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [ 25 | { 26 | "path": "./tsconfig.node.json" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/create/project_template/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/create/utils/convertVue.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import { log } from "handlebars"; 3 | import { join, dirname } from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { mappingData } from "./uiReflection.js"; 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | export class ConvertVue { 9 | private readonly path: string; 10 | private readonly fileName: string; 11 | private readonly answers: any; 12 | private readonly model: string[]; 13 | constructor(params) { 14 | const { path, fileName, answers, model } = params; 15 | this.path = path; 16 | this.fileName = fileName; 17 | this.answers = answers; 18 | this.model = model; 19 | } 20 | async init() { 21 | /** 22 | * //#end; 识别 23 | * #slot:ui_1 =》 slot插槽类型 ui_1插槽点 24 | * #hook:hook_1 =》 hook模块类型 hook_1模块点 25 | */ 26 | const js = await this.readJsFile(); 27 | 28 | /** 29 | * //#end; 识别 30 | * #scss_1 =》 类别 根据ui库插入 31 | */ 32 | const css = await this.readScssFile(); 33 | const html = await this.readHtmlFile(); 34 | 35 | return js.replace(`"use strict";`, "") + html + css; 36 | } 37 | private async readJsFile() { 38 | const { variant, ui } = this.answers; 39 | const fileName = 40 | variant === "TypeScript" ? this.fileName + ".ts" : this.fileName + ".js"; 41 | const { oldF, fn } = await this._readTemplate(fileName, this.path); 42 | let jsFile = oldF; 43 | const { slot, hook } = new Function(fn)() || {}; 44 | // ui 45 | if (slot) { 46 | for (const [key, value] of Object.entries(slot)) { 47 | jsFile = jsFile.replace( 48 | `//#slot:${key}`, 49 | this.extractFunctionBody(value[ui]), 50 | ); 51 | } 52 | } 53 | // 模块 54 | if (hook) { 55 | for (const [key, hookValue] of Object.entries(hook)) { 56 | if (!hookValue && typeof hookValue !== "object") continue; 57 | for (const [modelKey, modelValue] of Object.entries(hookValue)) { 58 | if (!this.answers[modelKey]) { 59 | if (modelValue["FALSE"]) { 60 | jsFile = jsFile.replace(`//#hook:${key}`, modelValue["FALSE"]()); 61 | } else { 62 | jsFile = jsFile.replace(`//#hook:${key}`, ""); 63 | } 64 | 65 | continue; 66 | } 67 | if (modelValue["HOOK"]) { 68 | jsFile = jsFile.replace(`//#hook:${key}`, modelValue["HOOK"]()); 69 | continue; 70 | } 71 | if (!modelValue[ui]) continue; 72 | jsFile = jsFile.replace(`//#hook:${key}`, modelValue[ui]()); 73 | } 74 | } 75 | } 76 | if (!jsFile) return ""; 77 | return ` 80 | `; 81 | } 82 | private async readScssFile() { 83 | const { ui, css } = this.answers; 84 | const { oldF, fn } = await this._readTemplate( 85 | this.fileName + ".scss", 86 | this.path, 87 | ); 88 | let scssFile = oldF; 89 | const fnObj = new Function(fn)() || {}; 90 | for (const [key, value] of Object.entries(fnObj)) { 91 | if (value[ui]) { 92 | scssFile = oldF.replace(`//#${key}`, value[ui]()); 93 | } 94 | } 95 | const cssSymbol = css === "scss" ? "$" : "@"; 96 | if (!scssFile) return ""; 97 | return ``; 100 | } 101 | private async readHtmlFile() { 102 | // 加载组件映射 103 | // 根据映射进行转换 104 | /**注意 105 | * 1.z组件和属性兼容性,如果目标ui库没有这个属性时应该怎么做 106 | * 例如ant-design-vue和element 的menu组件,展开所选中菜单时 107 | * ant需要绑定openKeys和selectedKeys 而element 只需要default-active就可以 108 | * 109 | */ 110 | const { oldF, fn } = await this._readTemplate( 111 | this.fileName + ".vue", 112 | this.path, 113 | ); 114 | let html = oldF; 115 | 116 | html = conditionalCompilation(fn, oldF, this.answers); 117 | 118 | html = this.readHtml(html, mappingData); 119 | 120 | if (!this.answers["i18n"]) { 121 | html = removeI18(html); 122 | } 123 | 124 | if (this.answers.css === "less") { 125 | html = html.replace("scss", "less"); 126 | } 127 | 128 | return html; 129 | } 130 | 131 | private async _readTemplate(fileName: string, path: string) { 132 | try { 133 | const jsFilePath = join(path, fileName); 134 | const data = fs.readFileSync(jsFilePath, "utf-8"); 135 | const endIndex = data.indexOf("//#end;"); 136 | const end = "//#end;"; // 结束标签 137 | let oldF = data.slice(0, endIndex); 138 | let runF = ""; 139 | if (endIndex === -1) { 140 | runF = ""; 141 | } else { 142 | runF = data.slice(endIndex + end.length, data.length); 143 | } 144 | return { 145 | oldF, 146 | fn: runF, 147 | }; 148 | } catch { 149 | return { 150 | oldF: "", 151 | fn: "", 152 | }; 153 | } 154 | } 155 | private extractFunctionBody(func: Function | undefined) { 156 | if (typeof func !== "function" || !func) { 157 | return ""; 158 | } 159 | let functionSource = func.toString(); 160 | let functionBody = functionSource 161 | .replace(/^function\s.*?\s*\(/, "") 162 | .replace(/\)\s*{/, "{") 163 | .replace(/}\s*$/, "}"); 164 | 165 | if (functionBody) { 166 | const fn = functionBody 167 | .trim() 168 | .slice(1, functionBody.length - 1) 169 | .split(";") 170 | .map((item) => item.trim()) 171 | .join(";\n"); 172 | return new Function(fn)() || ""; 173 | } else { 174 | throw new Error("Unable to extract function body"); 175 | } 176 | } 177 | private readHtml(str: string, mappingData: any) { 178 | const { variant, ui } = this.answers; 179 | if (ui !== "element") { 180 | return convertVue(str, mappingData, ui); 181 | } else { 182 | return str; 183 | } 184 | } 185 | } 186 | 187 | const conditionalCompilation = (fn: string, oldF: string, answers: any) => { 188 | const { ui } = answers; 189 | if (!fn) return oldF; 190 | let newF = oldF; 191 | let fnStr = fn; 192 | if (fn.includes("", ""); 194 | } 195 | try { 196 | const conditional = new Function(fnStr)(); 197 | // 功能 198 | if (conditional && conditional.model) { 199 | for (const [key, keyValue] of Object.entries(conditional.model)) { 200 | if (!keyValue) continue; 201 | for (const [modelKey, modelValue] of Object.entries(keyValue)) { 202 | // ============ 203 | if (!answers[modelKey]) { 204 | if (modelValue["FALSE"]) { 205 | newF = newF.replace(`//#${key}`, modelValue["FALSE"]()); 206 | } else { 207 | newF = newF.replace(`//#${key}`, ""); 208 | } 209 | 210 | continue; 211 | } 212 | if (modelValue["HOOK"]) { 213 | newF = newF.replace(`//#${key}`, modelValue["HOOK"]()); 214 | continue; 215 | } 216 | if (!modelValue[ui]) continue; 217 | newF = newF.replace(`//#${key}`, modelValue[ui]()); 218 | // =========== 219 | } 220 | } 221 | } 222 | // ui不同 223 | if (conditional && conditional.slot) { 224 | for (const [key, value] of Object.entries(conditional.slot)) { 225 | newF = newF.replace(`//#${key}`, conditional.slot[key][ui]()); 226 | } 227 | } 228 | } catch (err) { 229 | console.log("html文件错误", newF); 230 | } 231 | return newF; 232 | }; 233 | 234 | const convertVue = (str: string, mappingData: any, ui: string) => { 235 | const sourceTabSet = new Set(); 236 | let html = str; 237 | let i = 0; 238 | for (i; i < str.length; i++) { 239 | if (str[i] !== "<") continue; 240 | for (let j = i; j < str.length; j++) { 241 | if (str[j] === ">") { 242 | let label = str.slice(i + 1, j + 1); 243 | let newLabel = label; 244 | const sourceTab = getTab(label); 245 | if (label.includes("el-" && sourceTab)) { 246 | const attributeList = getTabAttribute(label); 247 | if (attributeList.length > 0) { 248 | attributeList.forEach((item) => { 249 | const [source, attributeValue] = item.split("="); 250 | let sourceKey = source.replace(":", "").replace("@", ""); 251 | if (sourceTab && mappingData.attributeMappings[sourceTab]) { 252 | const attributeMappings = 253 | mappingData.attributeMappings[sourceTab]?.[ 254 | source.replace(":", "") 255 | ]?.[ui]; 256 | 257 | if (attributeMappings) { 258 | const { key, value } = attributeMappings(attributeValue); 259 | //对应ui库没有该属性 260 | if (key === "") { 261 | newLabel = newLabel.replace(item, ""); 262 | } else if (!value) { 263 | newLabel = newLabel.replace(sourceKey, key); 264 | } else if (value && key) { 265 | newLabel = newLabel.replace( 266 | `${sourceKey}=${attributeValue}`, 267 | `${key}=${value}`, 268 | ); 269 | } 270 | } 271 | } 272 | }); 273 | } 274 | newLabel = addSupplement(newLabel, sourceTab, ui); 275 | sourceTabSet.add(sourceTab); 276 | const sourceLabel = mappingData.componentMappings[sourceTab]; 277 | if (sourceLabel && sourceLabel[ui]) { 278 | newLabel = newLabel.replace(sourceTab, sourceLabel[ui]); 279 | } 280 | } 281 | html = html.replace(label, newLabel); 282 | i = j + 1; 283 | } 284 | } 285 | } 286 | return html; 287 | }; 288 | 289 | const addSupplement = ( 290 | newLabel: string, 291 | sourceTab: string, 292 | ui: string, 293 | ): string => { 294 | const supplement = mappingData.attributeMappings[sourceTab]?.supplement; 295 | let add = ""; 296 | if (newLabel && !newLabel.includes("/") && supplement) { 297 | const attribute = supplement[ui]; 298 | // 补充属性 supplement 299 | for (const [key, value] of Object.entries(attribute)) { 300 | add += `\n ${key}="${value}"`; 301 | } 302 | } 303 | return newLabel.replace(">", add + ">"); 304 | }; 305 | 306 | const getTab = (tab: string) => { 307 | const index = tab.indexOf("el-") as number; 308 | let endIndex = tab.slice(index, tab.length).indexOf(" "); 309 | if (endIndex === -1) { 310 | endIndex = tab.slice(index, tab.length).indexOf(">"); 311 | } 312 | return tab.slice(index, index + endIndex).trim(); 313 | }; 314 | const getTabAttribute = (tab: string) => { 315 | const index = tab.indexOf("el-") as number; 316 | let endIndex = tab.slice(index, tab.length).indexOf(">"); 317 | const attribute = tab 318 | .slice(index, index + endIndex) 319 | .trim() 320 | .split(" "); 321 | const attributeList = []; 322 | attribute.forEach((item) => { 323 | if (!item.includes("el-")) { 324 | if (item.includes("=")) { 325 | attributeList.push(item); 326 | } else { 327 | attributeList[attributeList.length - 1] += ` ${item}`; 328 | } 329 | } 330 | }); 331 | return attributeList; 332 | }; 333 | 334 | const removeI18 = (html: string) => { 335 | if (!html.includes("$t(")) return html; 336 | let newHtml = html; 337 | const start = html.indexOf("$t("); 338 | const end = html.indexOf(")", start); 339 | const action = html.slice(start + 3, end); 340 | const replace = html.slice(start, end + 1); 341 | newHtml = newHtml.replace(replace, action); 342 | if (newHtml.includes("$t")) { 343 | return removeI18(newHtml); 344 | } else { 345 | return newHtml; 346 | } 347 | }; 348 | -------------------------------------------------------------------------------- /src/commands/create/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from "path"; 2 | import fs from "fs-extra"; 3 | import { fileURLToPath } from "url"; 4 | const __filename = fileURLToPath(import.meta.url); 5 | const __dirname = dirname(__filename); 6 | export const questionsList = [ 7 | { 8 | type: "input", 9 | name: "version", 10 | message: "请输入项目的版本号", 11 | default: "0.0.0", 12 | validate: (value) => { 13 | const str = value.split("."); 14 | if (str.length !== 3) return "input format(0.0.0)"; 15 | const flag = str.every((currentValue) => Number(currentValue) >= 0); 16 | if (!flag) return "please number"; 17 | return flag; 18 | }, 19 | }, 20 | { 21 | type: "list", 22 | name: "variant", // key 名 23 | message: "请选择一个需要的语言", 24 | choices: [ 25 | { 26 | name: "TypeScript", 27 | value: "TypeScript", 28 | }, 29 | { 30 | name: "javaScript", 31 | value: "javaScript", 32 | }, 33 | ], 34 | }, 35 | { 36 | type: "list", 37 | name: "ui", // key 名 38 | message: "请选择一个需要的ui", 39 | choices: [ 40 | { 41 | name: "element-plus", 42 | value: "element", 43 | }, 44 | { 45 | name: "Ant Design Vue", 46 | value: "antdv", 47 | }, 48 | ], 49 | }, 50 | { 51 | type: "list", 52 | name: "css", 53 | message: "请选择一个css预处理器", 54 | choices: [ 55 | { 56 | name: "scss", 57 | value: "scss", 58 | }, 59 | { 60 | name: "less", 61 | value: "less", 62 | }, 63 | ], 64 | }, 65 | { 66 | type: "checkbox", 67 | name: "checkbox", 68 | message: "请选择需要的功能(多选)", 69 | choices: [ 70 | { 71 | name: "eslint and Prettier", 72 | value: "eslint", 73 | }, 74 | { 75 | name: "i18n国际化", 76 | value: "i18n", 77 | // disabled: "国际化正在开发中", 78 | }, 79 | ], 80 | default: ["eslint"], 81 | }, 82 | { 83 | type: "checkbox", 84 | name: "modelCheckbox", 85 | message: "请选择需要的模块(多选)", 86 | choices: [ 87 | { 88 | name: "echars", 89 | value: "echarts", 90 | }, 91 | { 92 | name: "three.js", 93 | value: "three", 94 | }, 95 | ], 96 | default: ["echarts"], 97 | }, 98 | ]; 99 | -------------------------------------------------------------------------------- /src/commands/create/utils/main.ts: -------------------------------------------------------------------------------- 1 | const ui = (type) => { 2 | let importUi = ""; 3 | let useUi = ""; 4 | switch (type) { 5 | case "antdv": 6 | importUi = `import Antd from 'ant-design-vue'; 7 | import 'ant-design-vue/dist/reset.css'; 8 | import * as antdIconsVue from "@ant-design/icons-vue";`; 9 | useUi = `app.use(Antd) 10 | for (const [key, component] of Object.entries(antdIconsVue)) { 11 | app.component(key, component); 12 | } 13 | `; 14 | break; 15 | case "element": 16 | importUi = `import * as ElementPlusIconsVue from "@element-plus/icons-vue";`; 17 | useUi = `for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 18 | app.component(key, component); 19 | }`; 20 | default: 21 | break; 22 | } 23 | return { 24 | importUi, 25 | useUi, 26 | }; 27 | }; 28 | 29 | const i18n = (type) => { 30 | let importI18n = ""; 31 | let useI18n = ""; 32 | if (type) { 33 | importI18n = ` 34 | import { i18n } from "./lang/lang";`; 35 | useI18n = ".use(i18n)"; 36 | } 37 | return { 38 | importI18n, 39 | useI18n, 40 | }; 41 | }; 42 | 43 | const pinia = () => { 44 | let importPinia = `import { initStore } from "@/store/index"`; 45 | let usePinia = `initStore(app);`; 46 | return { 47 | importPinia, 48 | usePinia, 49 | }; 50 | }; 51 | export const createMain = (answers) => { 52 | const { importUi, useUi } = ui(answers.ui); 53 | const { importI18n, useI18n } = i18n(answers.i18n); 54 | const { importPinia, usePinia } = pinia(); 55 | return `import { createApp } from "vue"; 56 | import "./style.css"; 57 | import App from "./App.vue"; 58 | import router from "@/router"; 59 | import "@/router/protector"; 60 | import "@/styles/index.${answers.css}"; 61 | import { signSvgIcon } from "@/components/svgIcon/icon"; 62 | import { hasPermission } from "@/utils/index"; 63 | ${importPinia} 64 | ${importUi} 65 | ${importI18n} 66 | const app = createApp(App); 67 | signSvgIcon(app); 68 | ${usePinia} 69 | app.use(router)${useI18n}; 70 | ${useUi} 71 | router.isReady().then(() => { 72 | app.mount("#app"); 73 | }); 74 | //自定义按钮指令 75 | app.directive("auth", { 76 | mounted(el, binding) { 77 | hasPermission(el, binding); 78 | }, 79 | }); 80 | `; 81 | }; 82 | -------------------------------------------------------------------------------- /src/commands/create/utils/packageConfig.ts: -------------------------------------------------------------------------------- 1 | const lang = (variant) => { 2 | let ts = ""; 3 | if (variant === "TypeScript") { 4 | ts = `, 5 | "typescript": "^5.2.2", 6 | "vue-tsc": "^1.8.27"`; 7 | } 8 | return `"@vitejs/plugin-vue": "^5.0.4", 9 | "vite": "^5.1.4"${ts}`; 10 | }; 11 | 12 | const ui = (type) => { 13 | let dependencies = ""; 14 | let devDependencies = ""; 15 | switch (type) { 16 | case "element": 17 | devDependencies = `, 18 | "unplugin-auto-import": "^0.17.5", 19 | "unplugin-vue-components": "^0.26.0"`; 20 | dependencies = `"@element-plus/icons-vue": "^2.3.1", 21 | "element-plus": "^2.6.0"`; 22 | break; 23 | case "antdv": 24 | dependencies = `"ant-design-vue": "^4.1.2"`; 25 | devDependencies = `, 26 | "fast-glob": "^3.3.2"`; 27 | break; 28 | default: 29 | return; 30 | } 31 | return { 32 | dependencies, 33 | devDependencies, 34 | }; 35 | }; 36 | 37 | const css = (type) => { 38 | if (type === "scss") { 39 | return `"sass": "^1.71.1", 40 | "sass-loader": "^14.1.1"`; 41 | } else { 42 | return `"less": "^4.2.0", 43 | "less-loader": "^12.2.0"`; 44 | } 45 | }; 46 | 47 | const eslintWrite = (answers) => { 48 | const { variant, eslint } = answers; 49 | if (!eslint) return ""; 50 | if (variant === "TypeScript") { 51 | return `, 52 | "vite-plugin-eslint": "^1.8.1", 53 | "@vue/eslint-config-typescript": "^12.0.0", 54 | "eslint-config-prettier": "^9.1.0", 55 | "eslint-config-standard-with-typescript": "^43.0.1", 56 | "eslint-plugin-import": "^2.29.1", 57 | "eslint-plugin-n": "^16.6.2", 58 | "eslint-plugin-prettier": "^5.1.3", 59 | "eslint-plugin-promise": "^6.1.1", 60 | "eslint-plugin-vue": "^9.22.0", 61 | "vue-eslint-parser": "^9.4.2"`; 62 | } else { 63 | return `, 64 | "vite-plugin-eslint": "^1.8.1", 65 | "@vue/eslint-config-prettier": "^9.0.0", 66 | "eslint": "^8.57.0", 67 | "eslint-config-prettier": "^9.1.0", 68 | "eslint-plugin-prettier": "^5.1.3", 69 | "eslint-plugin-vue": "^9.22.0", 70 | "prettier": "^3.2.5"`; 71 | } 72 | }; 73 | 74 | const eslintScripts = (answers) => { 75 | if (answers.eslint) { 76 | return `, 77 | "lint": "eslint --ext .js,.vue,ts,jsx,tsx src", 78 | "prettier": "prettier --write ."`; 79 | } else { 80 | return ""; 81 | } 82 | }; 83 | 84 | const npmRunDev = (answers) => { 85 | if (answers.eslint) { 86 | return `"test": "prettier --write . & eslint --ext .js,.vue,ts,jsx,tsx src & vite --mode development",`; 87 | } else { 88 | return `"test": "vite --mode development",`; 89 | } 90 | }; 91 | 92 | const i18n = (type) => { 93 | if (type) { 94 | return `, 95 | "vue-i18n": "^9.10.1"`; 96 | } else { 97 | return ""; 98 | } 99 | }; 100 | 101 | const echarts = (type) => { 102 | if (type) { 103 | return `, 104 | "echarts": "^5.5.0"`; 105 | } else { 106 | return ""; 107 | } 108 | }; 109 | const three3D = (type) => { 110 | if (type) { 111 | return `, 112 | "@types/three": "^0.160.0", 113 | "three": "^0.160.0"`; 114 | } else { 115 | return ""; 116 | } 117 | }; 118 | 119 | export const createPackage = (answers) => { 120 | const { projectName } = answers; 121 | return `{ 122 | "name": "${projectName}", 123 | "private": true, 124 | "version": "${answers.version}", 125 | "type": "module", 126 | "scripts": { 127 | ${npmRunDev(answers)} 128 | "dev": "vite --mode development", 129 | "serve": "vite --mode development", 130 | "serve:pro": "vite --mode production", 131 | "build": "vite build --mode development", 132 | "build:pro": "vite build --mode production", 133 | "preview": "vite preview"${eslintScripts(answers)} 134 | }, 135 | "dependencies": { 136 | "path": "^0.12.7", 137 | "vue": "^3.4.19", 138 | "vue-router": "^4.3.0", 139 | "pinia": "^2.1.7", 140 | "pinia-plugin-persistedstate": "^3.2.1", 141 | "axios": "^1.6.7", 142 | ${ui(answers.ui).dependencies}${i18n(answers.i18n)}${echarts(answers.echarts)}${three3D(answers.three)} 143 | }, 144 | "devDependencies": { 145 | "vite-plugin-svg-icons": "^2.0.1", 146 | "vite-svg-loader": "^5.1.0", 147 | ${lang(answers.variant)}${ui(answers.ui).devDependencies}, 148 | ${css(answers.css)}${eslintWrite(answers)} 149 | } 150 | } 151 | `; 152 | }; 153 | -------------------------------------------------------------------------------- /src/commands/create/utils/uiReflection.ts: -------------------------------------------------------------------------------- 1 | class Attribute { 2 | key: string; 3 | value: string; 4 | constructor(key: string, value: any) { 5 | this.key = key; 6 | this.value = value; 7 | } 8 | } 9 | 10 | export const mappingData = { 11 | sourceUILibrary: "elementUi", 12 | componentMappings: { 13 | "el-menu": { 14 | antdv: "a-menu", 15 | }, 16 | "el-menu-item": { 17 | antdv: "a-menu-item", 18 | }, 19 | "el-sub-menu": { 20 | antdv: "a-sub-menu", 21 | }, 22 | "el-input": { 23 | antdv: "a-input", 24 | }, 25 | "el-button": { 26 | antdv: "a-button", 27 | }, 28 | "el-breadcrumb": { 29 | antdv: "a-breadcrumb", 30 | }, 31 | "el-breadcrumb-item": { 32 | antdv: "a-breadcrumb-item", 33 | }, 34 | "el-avatar": { 35 | antdv: "a-avatar", 36 | }, 37 | "el-dropdown": { 38 | antdv: "a-dropdown", 39 | }, 40 | "el-dropdown-menu": { 41 | antdv: "a-menu", 42 | }, 43 | "el-dropdown-item": { 44 | antdv: "a-menu-item", 45 | }, 46 | "el-tooltip": { 47 | antdv: "a-tooltip", 48 | }, 49 | "el-form": { 50 | antdv: "a-form", 51 | }, 52 | "el-form-item": { 53 | antdv: "a-form-item", 54 | }, 55 | "el-dialog": { 56 | antdv: "a-modal", 57 | }, 58 | "el-select": { 59 | antdv: "a-select", 60 | }, 61 | "el-option": { 62 | antdv: "a-option", 63 | }, 64 | }, 65 | attributeMappings: { 66 | "el-menu": { 67 | "collapse-transition": { 68 | antdv: (value) => new Attribute("", ""), 69 | }, 70 | collapse: { 71 | antdv: (value) => new Attribute("inline-collapsed", ""), 72 | }, 73 | "active-text-color": { 74 | antdv: (value) => new Attribute("", ""), 75 | }, 76 | "default-active": { 77 | antdv: (value) => 78 | new Attribute( 79 | "selectedKeys", 80 | `"[${value 81 | .replace(/\"/g, "") 82 | .replace(/\/n/g, "")}]" \n :openKeys="openKeys" \n`, 83 | ), 84 | }, 85 | "text-color": { 86 | antdv: (value) => new Attribute("", ""), 87 | }, 88 | "background-color": { 89 | antdv: (value) => new Attribute("", ""), 90 | }, 91 | class: { 92 | antdv: (value) => new Attribute("", ""), 93 | }, 94 | "show-timeout": { 95 | antdv: (value) => new Attribute("", ""), 96 | }, 97 | "hide-timeout": { 98 | antdv: (value) => new Attribute("", ""), 99 | }, 100 | select: { 101 | antdv: (value) => new Attribute("select", ""), 102 | }, 103 | supplement: { 104 | antdv: { 105 | mode: "inline", 106 | theme: "dark", 107 | }, 108 | }, 109 | }, 110 | "el-menu-item": {}, 111 | "el-tooltip": { 112 | content: { 113 | antdv: (value) => new Attribute("", ""), 114 | }, 115 | }, 116 | "el-form-item": { 117 | prop: { 118 | antdv: (value) => new Attribute("name", ""), 119 | }, 120 | }, 121 | "el-input": { 122 | "v-model": { 123 | antdv: (value) => new Attribute("v-model:value", ""), 124 | }, 125 | }, 126 | "el-dialog": { 127 | "v-model": { 128 | antdv: (value) => new Attribute("v-model:open", ""), 129 | }, 130 | }, 131 | }, 132 | }; 133 | -------------------------------------------------------------------------------- /src/commands/create/utils/viteConfig.ts: -------------------------------------------------------------------------------- 1 | const replacement = () => { 2 | return '`${pathResolve("src")}/`'; 3 | }; 4 | const ui = (type) => { 5 | let plugins = ""; 6 | let importStr = ""; 7 | switch (type) { 8 | case "element": 9 | plugins = ` 10 | AutoImport({ 11 | resolvers: [ElementPlusResolver()], 12 | }), 13 | Components({ 14 | resolvers: [ElementPlusResolver()], 15 | })`; 16 | importStr = `import AutoImport from "unplugin-auto-import/vite" 17 | import Components from "unplugin-vue-components/vite" 18 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"`; 19 | break; 20 | case "antdv": 21 | break; 22 | default: 23 | return; 24 | } 25 | return { 26 | importStr, 27 | plugins, 28 | }; 29 | }; 30 | 31 | const eslint = (answers) => { 32 | const { eslint } = answers; 33 | if (!eslint) 34 | return { 35 | eslintImportModel: "", 36 | eslintUseModel: "", 37 | }; 38 | return { 39 | eslintImportModel: `import eslint from "vite-plugin-eslint";`, 40 | eslintUseModel: `eslint(),`, 41 | }; 42 | }; 43 | export const createViteConfig = (answers) => { 44 | const { eslintImportModel, eslintUseModel } = eslint(answers); 45 | return `import { defineConfig, loadEnv } from "vite"; 46 | import vue from "@vitejs/plugin-vue" 47 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 48 | import svgLoader from "vite-svg-loader"; 49 | ${ui(answers.ui).importStr} 50 | ${eslintImportModel} 51 | import { resolve } from "path"; 52 | 53 | 54 | // https://vitejs.dev/config/ 55 | export default defineConfig((configEnv)=>{ 56 | const { VITE_BASE_API } = loadEnv(configEnv.mode, process.cwd()); 57 | return { 58 | plugins: [ 59 | vue(), 60 | // ${eslintUseModel} 61 | svgLoader(), 62 | createSvgIconsPlugin({ 63 | iconDirs: [resolve(process.cwd(), "src/icons/svg")], 64 | symbolId: "[name]", 65 | }), 66 | ${ui(answers.ui).plugins} 67 | ], 68 | resolve: { 69 | extensions: [ 70 | ".mjs", 71 | ".js", 72 | ".ts", 73 | ".jsx", 74 | ".tsx", 75 | ".json", 76 | ".scss", 77 | ".css", 78 | ], 79 | alias: { 80 | "@": resolve(__dirname, "./src"), 81 | "@public": resolve(__dirname, "./public"), 82 | }, 83 | }, 84 | base: './', 85 | server: { 86 | host: true, 87 | port: 8888, 88 | open: true, 89 | cors: true, 90 | proxy: { 91 | "/api": { 92 | target: VITE_BASE_API, // 测试地址 93 | changeOrigin: true, 94 | rewrite: (path) => path.replace(/^\\/api/, ""), 95 | }, 96 | }, 97 | warmup: { 98 | clientFiles: ["./src/layouts/**/*.vue"], 99 | }, 100 | }, 101 | } 102 | }) 103 | `; 104 | }; 105 | -------------------------------------------------------------------------------- /src/const/index.ts: -------------------------------------------------------------------------------- 1 | export const version = "1.0.12"; 2 | -------------------------------------------------------------------------------- /src/helpers/log.ts: -------------------------------------------------------------------------------- 1 | import figlet from "figlet"; 2 | console.log(figlet.textSync("bd-admin")); 3 | -------------------------------------------------------------------------------- /src/helpers/spinner.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | const message = "Loading unicorns"; 3 | export const spinner = ora(message); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import { program } from "commander/esm.mjs"; 3 | import "./helpers/log.js"; 4 | import { getFileList } from "./utils/index.js"; 5 | import { version } from "./const/index.js"; 6 | import { dirname, join } from "path"; 7 | import { fileURLToPath } from "url"; 8 | import fs from "fs-extra"; 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const nodeV = process.versions; 13 | const start = async () => { 14 | console.log("当前版本", version); 15 | program.version(version); // 设置版本 16 | const fileList = await getFileList(join(__dirname, "commands")); 17 | const filePromise = []; 18 | fileList.forEach((fileName) => { 19 | filePromise.push( 20 | new Promise((res, rej) => { 21 | import(`./commands/${fileName}/index.js`).then((data) => { 22 | res(data); 23 | }); 24 | }) 25 | ); 26 | }); 27 | Promise.all(filePromise).then((res) => { 28 | res.forEach((data) => { 29 | const { command, description, action } = data.default; 30 | program.command(command).description(description).action(action); 31 | }); 32 | program.parse(); 33 | }); 34 | }; 35 | 36 | try { 37 | const [first, second] = nodeV.node.split("."); 38 | if (Number(first) < 18 || (Number(first) <= 18 && Number(second) <= 12)) { 39 | console.log("node版本需>=18.13.0"); 40 | } else { 41 | start(); 42 | } 43 | } catch (err) { 44 | console.error("异常", err); 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import spawn from "cross-spawn"; 3 | import chalk from "chalk"; 4 | 5 | export const getFileList = (filesPath: string): Promise => { 6 | return new Promise((res, rej) => { 7 | fs.readdir(filesPath, (err: any, fileList: string[]) => { 8 | if (err) rej(err); 9 | res(fileList); 10 | }); 11 | }); 12 | }; 13 | 14 | export const createFolder = (path: string): Promise => { 15 | return new Promise((res, rej) => { 16 | fs.readdir(path, (err: any, files: string[]) => { 17 | if (err) { 18 | fs.mkdir(path, (err: any) => { 19 | if (err) { 20 | rej(err); 21 | } else { 22 | res([]); 23 | } 24 | }); 25 | } else { 26 | rej(files); 27 | } 28 | }); 29 | }); 30 | }; 31 | 32 | export const installDependencies = ( 33 | path: string, 34 | command: string, 35 | ): Promise => { 36 | return new Promise((res, rej) => { 37 | const dependencies = ["ejs@3.1.8"]; 38 | const child = spawn(command, ["install", "-D"].concat(dependencies), { 39 | stdio: "inherit", 40 | cwd: path, 41 | }); 42 | child.on("close", function (code) { 43 | // 执行失败 44 | if (code !== 0) { 45 | console.log(chalk.red("Error occurred while installing dependencies!")); 46 | rej(); 47 | } else { 48 | res(code); 49 | } 50 | }); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2022", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "alwaysStrict": true, 10 | "sourceMap": false, 11 | "noEmit": false, 12 | "noEmitHelpers": false, 13 | "importHelpers": false, 14 | "strictNullChecks": false, 15 | "allowUnreachableCode": true, 16 | "lib": ["es6"], 17 | "typeRoots": ["./node_modules/@types"], 18 | "outDir": "./build", // 重定向输出目录 19 | "rootDir": "./src", // 仅用来控制输出的目录结构 20 | "allowJs": true, // 是否对js文件进行编译,默认:false。 21 | "removeComments": false, // 是否移除注释,默认:false 22 | // 配置@别名 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": ["src/*"] 26 | }, 27 | "noImplicitAny": false 28 | }, 29 | "exclude": [ 30 | // 不参与打包的目录 31 | "node_modules", 32 | "build" 33 | ], 34 | "include": [ 35 | // 指定被编译文件所在的目录 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "**/*.d.ts" 40 | ], 41 | "esModuleInterop": true, 42 | "allowSyntheticDefaultImports": true, 43 | "compileOnSave": false, 44 | "buildOnSave": false 45 | } 46 | --------------------------------------------------------------------------------