diff --git a/docs/contribution-guide.md b/docs/contribution-guide.md new file mode 100644 index 00000000..bb72e8b7 --- /dev/null +++ b/docs/contribution-guide.md @@ -0,0 +1,228 @@ +# 贡献指南 + +十分感谢您愿意贡献本项目. 在此之前请先阅读本指南, 本指南会从`cli`贡献与`pro套件`贡献两个方向展开. + +## 前置准备 + +### Clone仓库 + +- 点击 [TinyCLI](https://github.com/opentiny/tiny-cli) 代码仓库右上角的 Fork 按钮,将上游仓库 Fork 到个人仓库 +- Clone 个人仓库到本地 +- 在 Tiny CLI 根目录下运行 npm init, 安装依赖 +- 运行 npm run dev,启动本地代码编译开发 + +```shell +# username 为用户名,执行前请替换 +git clone git@github.com:username/tiny-cli.git +cd tiny-cli +git remote add upstream git@github.com:opentiny/tiny-cli.git +``` + +### 依赖安装 + +在 Tiny CLI 根目录下运行 `npm run init` 安装依赖。 + +## CLI 贡献 + +### 环境准备 + +```bash +npm run dev +``` + +执行上述命令后,出现 `Found 0 errors. Watching for file changes.` 字样则代表开发环境搭建成功。接下来我们需要进入开发阶段 + +### 命令开发 + +本章我们将会开发一个命令叫做 `health` 该命令将会输出操作系统、CPU架构、CPU核心数、内存总数(MB单位)。假设我们输入`tiny health`,控制台应该显示 + +``` + Cpu: + Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz + CpuTotal: 1 +CpuKernalTotal: 4 + Arch: x64 + Platform: win32 + Memory: 8129 MB + FreeMemory: 6832 MB + Time: 2024/9/27 +``` + +左边是机器信息的名字,右边是数值。CPU因为只有一个所以只显示一个,如果有多个那么则显示多个。 + +首先请确保您运行了`npm run dev`。其次,请另开一个终端执行`npm run link`. 当出现`lerna success run Ran npm script 'link' in 1 package in 47.7s:`字样则表示`link`成功。后续操作不需要继续重复`link` + +接下来我们在`packages\cli\commands\src`下创建`health.ts`文件。并写入如下代码 + +```ts +// packages\cli\commands\src\health.ts +export default function(){} +``` + +紧接着我们在`packages\cli\commands\src\index.ts`文件下导入health文件并导出。最终`packages\cli\commands\src\index.ts`文件应该如下所示 + +```ts +// packages\cli\commands\src\index.ts +/** + * Copyright (c) 2022 - present Tiny CLI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import i from './i'; +import clear from './clear'; +import help from './help'; +import init from './init'; +import list from './list'; +import locale from './locale'; +import main from './main'; +import update from './update'; +import version from './version'; +import install from './install'; +import config from './config'; +import health from './health'; + +export default { + i, + clear, + help, + init, + list, + locale, + main, + update, + install, + version, + config, + health +}; +``` + +保存后,在终端中运行`tiny health`命令,会发现控制台没有任何的输出。这是因为`health.ts`文件的默认导出是一个空函数,空函数执行自然不会有任何的输出。所以接下来我们要开始真正的开发了. + +将下列代码复制到`packages\cli\commands\src\health.ts`下并保存。之后运行`tiny health`命令会发现,控制台输出了当前机器的一些信息 + +```ts +// packages\cli\commands\src\health.ts +import { cpus, arch, platform, totalmem, freemem } from 'os' +import { stdout } from 'process'; +const formatTitle = (title: string) => `${title[0].toUpperCase()}${title.slice(1)}` +const toMB = (size: number) => Math.floor(size / 1024 / 1024) +const formatMemoryString = (size: number) => `${size} MB` +export default async function () { + const result = { + cpu: [] as string[], + cpuTotal: 0, + cpuKernalTotal: 0, + arch: arch(), + platform: platform(), + memory: formatMemoryString(toMB(totalmem())), + freeMemory: formatMemoryString(toMB(totalmem()) - toMB(freemem())) + }; + const cpuInfos = cpus() + result.cpu = Array.from(new Set(cpuInfos.map(cpuInfo => cpuInfo.model))); + result.cpuTotal = result.cpu.length; + result.cpuKernalTotal = cpuInfos.length; + const entries = Object.entries(result) + let maxLenKey = entries[0][0] + let maxValueKey = 0; + for (let i = 1; i < entries.length; i++) { + if (entries[i][0].length > maxLenKey.length) { + maxLenKey = entries[i][0]; + } + if (entries[i][1] instanceof Array) { + continue; + } + if (entries[i][1].toString().length > maxValueKey) { + maxValueKey = entries[i][1].toString().length; + } + } + for (const [key, value] of entries) { + stdout.write(formatTitle(key).padStart(maxLenKey.length,) + ': ' + (value instanceof Array ? '\n' : '')); + if (value instanceof Array) { + value.forEach(val => { + stdout.write(val.padStart(maxLenKey.length + 2 + val.length) + '\n') + }) + } else { + stdout.write(value.toString() + '\n') + } + } +} + +``` + +#### 这段代码做了什么事? + +这段代码主要使用`os`包来获取机器信息,而后通过stdout来进行输出。之所以使用stdout主要是可以更加灵活的进行输出,实际上使用`console.log`效果也是一样的 + +## `tiny-toolkit-pro` 套件贡献 + +### 第一节、目录结构 + +``` +packages + toolkits + pro + src + lib + build.ts # 对应的是 tiny pro build + help.ts # 对应的是 tiny pro help + init.ts # 对应的是 tiny pro init + interface.ts + start.ts # 对应的是 tiny pro start + utils.ts + template + server # 后端模板 * 本章只涉及nestjs后端 + tinyng # ng前端模板 + tinyvue # vue3前端模板 * 本章只涉及Vue前端 +``` + +### 第二节、贡献套件命令 + +套件命令对应的文件请参考上一节 `目录结构`. 当修改了套件命令后,贡献者**必须**发布一个私有的套件包。并设置环境变量`TINY_SCOPE`为您的`npm`名称 + +### 第三节、发布私有套件包 + +发布私有套件包前,我们假设你拥有npm账号且已经完成了[前置准备](#前置准备),且已经登陆。如果没有登陆请参考[npm登录](https://docs.npmjs.com/cli/v10/commands/npm-login?v=true)。 + +1. 替换`packages\toolkits\pro\package.json`中`@opentiny/tiny-toolkit-pro` 为 `@<您的npm名称>/tiny-toolkit-pro` +2. 在`packages\toolkits\pro`下运行`npm publish --access=public` +3. 修改环境变量`TINY_SCOPE=<第一步骤中您填写的私有scope>` +4. 安装`npm i -g @opentiny/cli` (如果您安装完成或link了开发产物可忽略该步骤) +5. `rm -rf ~/.tiny` +6. **新启用一个bash**并执行`tiny init pro` + +当出现`[core-module]: 本地尚未安装 @<您的npm名称>/tiny-toolkit-pro ,正在执行自动安装...`后则代表私有包发布成功 + + +### 贡献套件 + +您可以使用IDE打开`packages\toolkits\pro\templates`,进入对应的技术栈后安装依赖,之后跟随开发指南进行开发 + +- [前端二次开发指南](./tiny-pro-front-dev-guideline.md) +- [后端二次开发指南](./tiny-pro-backend-dev-guideline.md) + +#### 如何调试 + +- [ ] nestJs后端 + - 源码位置: `packages\toolkits\pro\templates\server\nestJs` + - 安装nestJs依赖: `npm i` + - 启动nestJs本地调试: `npm run start` + - [启动MySQL数据库](https://dev.mysql.com/doc/refman/8.4/en/tutorial.html) + - [启动Redis](https://redis.io/docs/latest/get-started/) +- [ ] vue3前端 + - 源码位置: `packages\toolkits\pro\templates\tinyvue` + - 安装nestJs依赖: `npm i` + - 启动nestJs本地调试: `npm run start`(默认vite启动) + - webpack启动: `npm run dev:wp` + - rspack启动: `npm run dev:rp` + - [成功启动页面](./images/tiny-pro-show.png) + +## 遇到困难? + +加官方小助手微信 opentiny-official,加入技术交流群 \ No newline at end of file diff --git a/docs/images/tiny-pro-show.png b/docs/images/tiny-pro-show.png new file mode 100644 index 00000000..16974088 Binary files /dev/null and b/docs/images/tiny-pro-show.png differ diff --git "a/docs/images/\344\270\272\346\265\213\350\257\225\347\224\250\346\210\267\347\273\221\345\256\232\350\217\234\345\215\225.png" "b/docs/images/\344\270\272\346\265\213\350\257\225\347\224\250\346\210\267\347\273\221\345\256\232\350\217\234\345\215\225.png" new file mode 100644 index 00000000..7ad93f5e Binary files /dev/null and "b/docs/images/\344\270\272\346\265\213\350\257\225\347\224\250\346\210\267\347\273\221\345\256\232\350\217\234\345\215\225.png" differ diff --git "a/docs/images/\345\217\252\345\213\276\351\200\211\346\265\213\350\257\225\351\241\265\351\235\242\345\215\263\345\217\257.png" "b/docs/images/\345\217\252\345\213\276\351\200\211\346\265\213\350\257\225\351\241\265\351\235\242\345\215\263\345\217\257.png" new file mode 100644 index 00000000..9a3319fb Binary files /dev/null and "b/docs/images/\345\217\252\345\213\276\351\200\211\346\265\213\350\257\225\351\241\265\351\235\242\345\215\263\345\217\257.png" differ diff --git "a/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \344\270\255\346\226\207.png" "b/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \344\270\255\346\226\207.png" new file mode 100644 index 00000000..b8cf3262 Binary files /dev/null and "b/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \344\270\255\346\226\207.png" differ diff --git "a/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \350\213\261\346\226\207.png" "b/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \350\213\261\346\226\207.png" new file mode 100644 index 00000000..c95cc120 Binary files /dev/null and "b/docs/images/\345\267\246\344\276\247\346\265\213\350\257\225\351\241\265\351\235\242 - \350\213\261\346\226\207.png" differ diff --git "a/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220.png" "b/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220.png" new file mode 100644 index 00000000..def75ee2 Binary files /dev/null and "b/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220.png" differ diff --git "a/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220\346\210\220\345\212\237.png" "b/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220\346\210\220\345\212\237.png" new file mode 100644 index 00000000..4515e7c7 Binary files /dev/null and "b/docs/images/\346\226\260\345\242\236\346\235\203\351\231\220\346\210\220\345\212\237.png" differ diff --git "a/docs/images/\346\226\260\345\242\236\350\257\215\346\235\241.png" "b/docs/images/\346\226\260\345\242\236\350\257\215\346\235\241.png" new file mode 100644 index 00000000..4b22e755 Binary files /dev/null and "b/docs/images/\346\226\260\345\242\236\350\257\215\346\235\241.png" differ diff --git "a/docs/images/\346\234\200\347\273\210\351\241\265\351\235\242\346\225\210\346\236\234.png" "b/docs/images/\346\234\200\347\273\210\351\241\265\351\235\242\346\225\210\346\236\234.png" new file mode 100644 index 00000000..800366fd Binary files /dev/null and "b/docs/images/\346\234\200\347\273\210\351\241\265\351\235\242\346\225\210\346\236\234.png" differ diff --git "a/docs/images/\346\235\203\351\231\220\347\273\221\345\256\232\345\261\225\347\244\272.png" "b/docs/images/\346\235\203\351\231\220\347\273\221\345\256\232\345\261\225\347\244\272.png" new file mode 100644 index 00000000..24d67b10 Binary files /dev/null and "b/docs/images/\346\235\203\351\231\220\347\273\221\345\256\232\345\261\225\347\244\272.png" differ diff --git "a/docs/images/\346\237\245\347\234\213\350\217\234\345\215\225\351\241\265.png" "b/docs/images/\346\237\245\347\234\213\350\217\234\345\215\225\351\241\265.png" new file mode 100644 index 00000000..03a563ce Binary files /dev/null and "b/docs/images/\346\237\245\347\234\213\350\217\234\345\215\225\351\241\265.png" differ diff --git "a/docs/images/\346\265\213\350\257\225\347\224\250\346\210\267\347\231\273\345\275\225.png" "b/docs/images/\346\265\213\350\257\225\347\224\250\346\210\267\347\231\273\345\275\225.png" new file mode 100644 index 00000000..073a6a68 Binary files /dev/null and "b/docs/images/\346\265\213\350\257\225\347\224\250\346\210\267\347\231\273\345\275\225.png" differ diff --git "a/docs/images/\346\267\273\345\212\240\350\217\234\345\215\225.png" "b/docs/images/\346\267\273\345\212\240\350\217\234\345\215\225.png" new file mode 100644 index 00000000..f71b7255 Binary files /dev/null and "b/docs/images/\346\267\273\345\212\240\350\217\234\345\215\225.png" differ diff --git "a/docs/images/\346\267\273\345\212\240\350\247\222\350\211\262\345\256\214\345\205\250\344\275\223.png" "b/docs/images/\346\267\273\345\212\240\350\247\222\350\211\262\345\256\214\345\205\250\344\275\223.png" new file mode 100644 index 00000000..a7a67071 Binary files /dev/null and "b/docs/images/\346\267\273\345\212\240\350\247\222\350\211\262\345\256\214\345\205\250\344\275\223.png" differ diff --git "a/docs/images/\347\202\271\345\207\273\346\267\273\345\212\240\350\257\215\346\235\241.png" "b/docs/images/\347\202\271\345\207\273\346\267\273\345\212\240\350\257\215\346\235\241.png" new file mode 100644 index 00000000..d675cbbe Binary files /dev/null and "b/docs/images/\347\202\271\345\207\273\346\267\273\345\212\240\350\257\215\346\235\241.png" differ diff --git "a/docs/images/\347\231\273\351\231\206\346\265\213\350\257\225\350\264\246\345\217\267.png" "b/docs/images/\347\231\273\351\231\206\346\265\213\350\257\225\350\264\246\345\217\267.png" new file mode 100644 index 00000000..6bd527b5 Binary files /dev/null and "b/docs/images/\347\231\273\351\231\206\346\265\213\350\257\225\350\264\246\345\217\267.png" differ diff --git "a/docs/images/\347\273\221\345\256\232\346\235\203\351\231\220.png" "b/docs/images/\347\273\221\345\256\232\346\235\203\351\231\220.png" new file mode 100644 index 00000000..9a2f59a9 Binary files /dev/null and "b/docs/images/\347\273\221\345\256\232\346\235\203\351\231\220.png" differ diff --git "a/docs/images/\347\273\221\345\256\232\350\217\234\345\215\225.png" "b/docs/images/\347\273\221\345\256\232\350\217\234\345\215\225.png" new file mode 100644 index 00000000..e53497ad Binary files /dev/null and "b/docs/images/\347\273\221\345\256\232\350\217\234\345\215\225.png" differ diff --git "a/docs/images/\347\273\221\345\256\232\350\247\222\350\211\262.png" "b/docs/images/\347\273\221\345\256\232\350\247\222\350\211\262.png" new file mode 100644 index 00000000..dfdc75e4 Binary files /dev/null and "b/docs/images/\347\273\221\345\256\232\350\247\222\350\211\262.png" differ diff --git "a/docs/images/\351\200\200\345\207\272\347\231\273\345\275\225.png" "b/docs/images/\351\200\200\345\207\272\347\231\273\345\275\225.png" new file mode 100644 index 00000000..ad41cff5 Binary files /dev/null and "b/docs/images/\351\200\200\345\207\272\347\231\273\345\275\225.png" differ diff --git "a/docs/images/\351\200\211\346\213\251\350\257\255\350\250\200.png" "b/docs/images/\351\200\211\346\213\251\350\257\255\350\250\200.png" new file mode 100644 index 00000000..b474b1fb Binary files /dev/null and "b/docs/images/\351\200\211\346\213\251\350\257\255\350\250\200.png" differ diff --git a/docs/maintainer-guide.md b/docs/maintainer-guide.md new file mode 100644 index 00000000..ddcbde8d --- /dev/null +++ b/docs/maintainer-guide.md @@ -0,0 +1,92 @@ +# 维护者指南 + +## 依赖安装 + +```bash +# npm install -g lerna # 如果没有安装, 请先安装lerna +npm run init +``` + +## 本地开发 + +请参考[贡献者指南](./contribution-guide.md) + +## 构建产物 + +```bash +npm run build +``` + +## 本地联调 + +```bash +npm run dev +``` + +```bash +# 新bash +npm run link +``` + +当出现 `lerna success - @opentiny/cli` 字样后代表link成功 + +## 发布测试包 + +> 执行此步骤前,请您悉知如何修改环境变量 +> +> Linux如何修改环境变量: https://wiki.archlinuxcn.org/wiki/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F +> +> Windows如何修改环境变量: https://support.esri.com/zh-cn/knowledge-base/edit-an-environment-variable-1462478594981-000002146 +> +> Macos如何修改环境变量: https://support.apple.com/zh-cn/guide/terminal/apd382cc5fa-4f58-4449-b20a-41c53c006f8f/mac + + +**!!!在修改环境变量后请重新启动终端(Bash)!!!** + +**!!!在修改环境变量后请重新启动终端(Bash)!!!** + +**!!!在修改环境变量后请重新启动终端(Bash)!!!** + +有时我们会需要发布一些测试包来测试构建产物是否能够正常使用,您可以根据下列指引来进行测试. 这里使用的是`@opentiny/tiny-toolkit-pro +`作为演示 + +0. 自增`package.json`中的`version`字段 +1. `package.json`文件中替换`@opentiny`为`@xxx`(其中xxx为您的npm名称) +2. 运行`npm publish --access=public` +3. 修改环境变量`TINY_SCOPE=xxx` (第一步中替换的结果, 但是不要包含@) + 1. 例如 `@foo/tiny-toolkit-pro`,TINY_SCOPE应该是`foo`而不是`@foo` +4. 安装`npm i -g @opentiny/cli` (如果您安装完成可忽略该步骤) +5. `rm -rf ~/.tiny` +6. `tiny init pro` + +### 发布测试包前检查清单 + +- [ ] lerna已被安装 +- [ ] `npm run init`已被执行 +- [ ] `npm run build`已被执行 +- [ ] 需要发布的测试包`package.json`文件中`version`自增 +- [ ] 需要发布的测试包`package.json`文件中`@opentiny`被替换为`@<您的npm账号名>` +- [ ] `npm publish --access=public`已被执行 +- [ ] 环境变量`TINY_SCOPE`已修改为您的npm账号名 +- [ ] 终端已重启 +- [ ] `npm i -g @opentiny/cli`已被执行 +- [ ] `rm -rf ~/.tiny`已被执行 + +## 发布正式包 + +与发布测试包相同,只是不需要替换`@opentiny`前缀 + +### 发布正式包前检查清单 + +- [ ] lerna已被安装 +- [ ] `npm run init`已被执行 +- [ ] `npm run build`已被执行 +- [ ] 需要发布的正式包`package.json`文件中`version`自增 +- [ ] `npm publish --access=public`已被执行 +- [ ] 终端已重启 +- [ ] `npm i -g @opentiny/cli`已被执行 +- [ ] `rm -rf ~/.tiny`已被执行 + +## 遇到困难? + +加官方小助手微信 opentiny-official,加入技术交流群 \ No newline at end of file diff --git a/docs/tiny-pro-backend-dev-guideline.md b/docs/tiny-pro-backend-dev-guideline.md new file mode 100644 index 00000000..a5e96419 --- /dev/null +++ b/docs/tiny-pro-backend-dev-guideline.md @@ -0,0 +1,211 @@ +# TinyPro 后端开发指南 + +在阅读本指南前,我们假设您已经阅读过[Nest.js官方文档](https://docs.nestjs.com/)并能够独立本机启动`MySQL`与`Redis`的能力。 + +## 项目初始化 + +[快速开始](./tiny-pro.md) + +## 后端启动 + +开发阶段通常不会使用docker进行启动,更多的是本地启动。首先我们要配置环境变量文件, 也就是`.env`文件 + +```properties +# 数据库IP (一般是本地) +DATABASE_HOST = 'localhost' +# 数据库端口 +DATABASE_PORT = 3306 +# 数据库用户名 +DATABASE_USERNAME = 'root' +# 数据库密码 +DATABASE_PASSWORD = 'root' +# 数据库名 (请确保该库存在) +DATABASE_NAME = 'ospp-nest' +# 请阅读: https://www.typeorm.org/migrations +# 线上环境请关闭 +DATABASE_SYNCHRONIZE = 'true' +DATABASE_AUTOLOADENTITIES = 'true' +# jwt secret +AUTH_SECRET = 'secret' +REDIS_SECONDS = 7200 +# redis ip +REDIS_HOST = 'localhost' +# redis 端口 +REDIS_PORT = 6379 +# token过期时间 +EXPIRES_IN = '2h' +# 分页默认起始页 (一般可以不修改) +PAGINATION_PAGE = 1 +# 分页默认大小 +PAGINATION_LIMIT = 10 +``` + +### 开发前检查清单 + +- [ ] 后端项目已被初始化 +- [ ] `.env`文件中`DATABASE_HOST`**是开发环境** +- [ ] `.env`文件中`DATABASE_NAME`为开发库 +- [ ] `.env`文件中`DATABASE_NAME`存在 +- [ ] `.env`文件中`DATABASE_SYNCHRONIZE`为`true` +- [ ] `.env`文件中`REDIS_HOST`**是开发环境** +- [ ] MySQL服务可以正常访问 +- [ ] Redis服务可以正常访问 +- [ ] `dist`目录被删除 (可选,如果你不需要测试初始化数据的话) + +配置好文件后您可以运行`npm run start:dev`来运行后端服务。当出现下述字样时,表示后端启动成功。 + +``` +LOG [NestApplication] Nest application successfully started +11ms +Application is running on: http://[::1]:3000 +``` + + +## 初始化数据 + +有些时候我们需要自动初始化一些数据(比如前端的默认国际化字段). 这些逻辑**均需**写在`App.module.ts`中`AppModule`类中的`onModuleInit`函数中。 + +## 国际化 + +> 这里的国际化指的是报错信息的国际化 + +后端采用的是`nestjs-i18n`依赖库。国际化词条放在`i18n//xxx.json`下 + +``` +i18n + enUS + exception.json + validation.json + zhCN + exception.json + validation.json +``` + +目前仅支持`enUS`与`zhCN`两种语言,且`fallback`为`enUS`. + +### 报错时候使用国际化词条 + +后端服务遵循`Restful`规范,可以直接抛出错误使用HttpStatusCode来代替错误代码。如果需要使用国际化词条,请确保该词条已经存在于`enUS|zhCN/exception.json`文件内。假设有一个服务`PolicyService`需要抛出一个`409`错误。 + +1. 添加国际化词条 +2. 在服务中注入`I18nService` +3. 使用该词条 + +```json +// zhCN/exception.json +{ + // 前面不做修改 + "policy":{ + "exists": "Policy已存在" + } +} +``` + +```ts +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +@Injectable() +export class PolicyService { + constructor( + private readonly i18n: I18nService + ) {} + createPolicy(){ + const exists = ...; + if (exists){ + throw new HttpException( + this.i18n.translate('exception.policy.exists', { + lang: I18nContext.current().lang, + }), + HttpStatus.CONFLICT // 409 + ) + } + //.... + } +} +``` + +## 接口权限管理 + +### Token管理 + +凡是**没有**被`Public`修饰器修饰的接口,均会被`auth/auth.guard.ts`进行校验,如果**token不存在**、**token过期**、**token不合法**,均不允许访问。 + +### 权限控制 + +如果一个接口没有被`Permission`修饰器进行修饰,那么这个接口是**允许**所有**已经登录**的用户访问。如果一个接口**被**`Permission`修饰器进行修饰,那么该接口**仅允许**拥有该权限的用户访问,其余用户会返回**403**错误代码 + +默认`admin`用户存在超级权限`(*)`, 拥有该权限且已经登陆的用户可以访问任何接口。 + +例如 + +```ts +@Controller('/policy') +export class PolicyController { + @Get('/list') + async getPolicy(){} +} +``` + +上述代码中`GET /policy/list`是一个不公开,不受保护的接口。我们可以使用`Permission`修饰器对他进行权限认证,当且仅当用户角色存在`policy::get::list`权限时才放行 + +```ts +@Controller('/policy') +export class PolicyController { + @Get('/list') + @Permission('policy::get::list') + async getPolicy(){} +} +``` + +这样一来`GET /policy/list`就只允许拥有`policy::get::list`权限的角色访问,其余角色访问则会返回一个403错误 + +但有些时候我们需要一个接口允许未登陆的用户访问。例如我们在登陆的时候经常需要获取免责声明,那么我们就可以写一个`GET /policy`接口,用于获取一个免责声明的法律条文。 + +所以我们可以添加如下 + +```ts +@Controller('/policy') +export class PolicyController { + @Get('/list') + @Permission('policy::get::list') + async getPolicies(){} + @Get('/') + @Public() + async getPolicy(){} +} +``` + +这样一来`GET /policy/list`接口只允许**登录**且**拥有policy::get::list**权限的角色访问。`GET /policy`接口则允许**未登陆**的**所有角色**进行访问。 + +如果未来的某一天,我们需要让`/policy/*`都允许未登录的用户访问,那么我们可以这么写 + +```ts +@Public() +@Controller('/policy') +export class PolicyController { + @Get('/list') + async getPolicies(){} + @Get('/') + async getPolicy(){} +} +``` + +这样一来,所有的policy接口都可以被未登录的用户访问了 + +## 遇到困难? + +加官方小助手微信 opentiny-official,加入技术交流群 + +## 常见问题 + +### 打包速度慢 + +请阅读[SWC](https://docs.nestjs.com/recipes/swc) + +### 提示 `Lock file exists, if you want init agin, please remove dist or dist/lock` + +为了避免重复初始化,系统会在第一次初始化的时候在`dist`目录下新建`app/lock`文件,如果您需要再次初始化,那么请您删除`dist/app`或者直接删除`dist`文件夹 + +### docker 部署时数据库超时 + +`docker-compose.yaml`实际上配置了`depends_on`字段,但`mysql`镜像并没有提供对应的健康检查。如果服务挂掉,可以等待`mysql`启动成功后手动重启后端服务 diff --git a/docs/tiny-pro-front-dev-guideline.md b/docs/tiny-pro-front-dev-guideline.md new file mode 100644 index 00000000..3a3643c3 --- /dev/null +++ b/docs/tiny-pro-front-dev-guideline.md @@ -0,0 +1,197 @@ +# TinyPro 前端开发指南 + +在阅读本文时,我们假设您已经学习过了Vue3. 在开始二次开发前, 我们需要先启动`tiny-pro`后端. 请参阅[TinyPro 快速上手](./tiny-pro.md) + +## 页面开发 + +### 第一步、新建页面 + +首先我们在`tiny-pro/web/src/views`下新建一个`test-page`目录, 该目录下只有一个`index.vue`文件。目录结构如下图所示 + +``` +web + src + views + test-page + index.vue +``` + +```html + + +``` + +### 第二步、创建国际化词条 + +点击 `系统管理 > 国际化管理` 打开国际化管理页面。在 `系统管理 > 国际化管理` 页面中单击 `增加词条` 按钮。弹出modal应如下所示 + +![](./images/点击添加词条.png) + +在该modal中,我们将词条Key定义为了`test::page::title`, 词条内容为 `测试页面`。 点击 `词条语言` 下的下拉框,选择`zhCN`(简体中文) + +![](./images/选择语言.png) + +选择好语言后单击 `添加词条` 按钮即可成功将Key 为 `test::page::title` 的简体中文国际化词条添加到数据库中。 + +### 第三步、绑定菜单 + +点击`系统管理 > 查看菜单`打开菜单管理页面, 在 `系统管理 > 查看菜单` 中点击 `添加菜单` + +![](./images/添加菜单.png) + +- 名称 + - 这里一般为英文, 在开发的时候对应的是该路由的id +- 优先级 + - 在菜单中排列的优先级,优先级越高越靠近上方(浏览器顶部) +- 父级菜单 + - 如果设置了父级菜单,则会作为父级菜单的子集 +- 图标 + - 菜单中的图标,必选 +- 组件 + - 组件名称,在该实例中是`test-page/index.vue`。填写该表单项的时候不能包含`src/views`前缀! +- 国际化 + - 菜单的国际化文本 + +点击 `确认` 按钮后, `Modal`会自动关闭。 + +![](./images/绑定菜单.png) + +点击 `系统管理 > 查看角色` 来到角色管理页面 + +点击`绑定菜单`按钮,本实例将菜单绑定到了`admin`用户。勾选`测试页面`后,单击`确认修改`按钮(右下角)后,在左侧菜单便会出现测试页面菜单项 + +![](./images/左侧测试页面%20-%20中文.png) + + +### 第四步、正式开发 + +现在单击 `测试页面` 菜单项会发现只有一个银色的 `hello-world` 字样。接下来我们进入正式开发阶段。 + +我们将在页面中添加一个一级标题,一个按钮,与**两行**文本,点击按钮后,第一行文本自增,第二行文本会在自增的基础上*2. 请将`tiny-pro/web/src/views/test-page/index.vue`替换为如下代码 + +```html + + + +``` + +最终效果如下 + +![](./images/最终页面效果.png) + +## 权限管理 + +本章将会进行组件级别的权限管理。我们依然沿用上一章的测试页面。这一章我们将创建一个`test-role`角色,绑定给一个`test`用户。并且在测试页面中,为`Count * 2`这个元素增加一个`test::page::double::text`权限,并且我们并不给`test-role`用户绑定该权限。 + +### 新增权限 + +请点击 `系统管理 > 查看权限` 来到权限管理页面。在 `系统管理 > 查看权限` 页面中单击 `添加权限` 按钮。并按照图示填写信息 + +![](./images/新增权限.png) + +单击 `确认` 按钮后上方会出现 `表单提交成功` 字样。表明权限已经成功添加到了数据库中。接下里我们需要新增角色 + +### 新增角色 + + +点击 `系统管理 > 查看角色` 来到角色管理页面。在角色管理页面中点击 `添加角色` 按钮。并按照图示填写信息 + +![](./images/绑定权限.png) + +注意!一定不要拥有**test::page::double::text**权限!! + +点击 `确认` 后, 弹出框将会自动关闭. 接下来我们需要创建用户 + +### 新增用户 + +点击 `系统管理 > 查看用户`来到用户管理页面。在用户管理页面中点击 `添加用户` 按钮。并按照图示填写信息 + +![](./images/添加角色完全体.png) + +之后我们需要为`test-role`角色来绑定菜单,我们可以只绑定一个`测试页面`菜单 + +![](./images/为测试用户绑定菜单.png) + +### 修改页面 + +现在我们来到`tiny-pro/web/src/views/test-page/index.vue`中,将文件中的代码替换为如下代码 + +```html + + + +``` + +### 登陆测试用户 + +点击提交后,弹窗将会自动关闭。现在将鼠标放到右上角头像上,在弹出框中选择 `退出登录`。按照下图是输入信息 + +![](./images/测试用户登录.png) + +因为我们只给`test-role`绑定了一个`测试页面`的路由,所以会跳转到测试页面,可以发现原本要出现的`Count * 2`如今已经不存在了。 + +![](./images/权限绑定展示.png) + +## 遇到困难? + +加官方小助手微信 opentiny-official,加入技术交流群 + +## 常见问题 + +### 前端跨域问题如何解决 + +对于开发环境来说,可以直接修改`dev-server`的`proxy`. 例如`vite`工具的`server.proxy` + +### 代码无法提交 + +您可以选择移除husky或根据[Angular 规范](https://zj-git-guide.readthedocs.io/zh-cn/latest/message/Angular%E6%8F%90%E4%BA%A4%E4%BF%A1%E6%81%AF%E8%A7%84%E8%8C%83/)书写commit信息 + +### 页面部署后刷新404 + +请移步[Vue Router服务器部署指南](https://router.vuejs.org/guide/essentials/history-mode.html#Example-Server-Configurations) \ No newline at end of file diff --git a/docs/tiny-pro.md b/docs/tiny-pro.md new file mode 100644 index 00000000..13ee426e --- /dev/null +++ b/docs/tiny-pro.md @@ -0,0 +1,234 @@ +# Tiny Pro 快速启动 + +## 环境准备 + +请确保您安装了`nodejs`, `npm`, `tiny-cli` + +```bash +tiny init pro +``` + +运行上述代码后按照提示输入配置 + +``` +? 请输入项目名称: tiny-pro +? 请输入项目描述: 基于TinyPro套件创建的中后台系统 +* 请选择您希望使用的客户端技术栈: vue +* 请选择您希望使用的服务端技术栈: Nest.js +* 请选择你想要的构建工具: Vite +* 请确保已安装数据库服务(参考文档 +https://www.opentiny.design/tiny-cli/docs/toolkits/pro#database): +已完成数据库服务安装,开始配置 +* 请选择数据库类型: MySql +* 请输入数据库地址: localhost +* 请输入数据库端口: 3306 +* 请输入数据库名称: ospp-nest +* 请输入登录用户名: root +* 请输入密码: [hidden] +``` + +初始化完成后,项目结构应该为 + +``` +tiny-pro + nestJs # 后端服务 + web # 前端服务 +``` + +## 后端启动 + +后端服务支持`docker启动`与`命令启动`, 执行操作前请先确保所处位置为`tiny-pro/nestJS` + +### Docker启动 + +在运行`docker compose up -d`之前,请先修改`.env`环境变量文件,示例如下 + +```properties +# 数据库IP +DATABASE_HOST = 'mysql' +# 数据库端口 +DATABASE_PORT = 3306 +# 数据库用户名 +DATABASE_USERNAME = 'root' +# 数据库密码 +DATABASE_PASSWORD = 'root' +# 数据库名 (请确保该库存在) +DATABASE_NAME = 'ospp-nest' +# 请阅读: https://www.typeorm.org/migrations +# 线上环境请关闭 +DATABASE_SYNCHRONIZE = true +DATABASE_AUTOLOADENTITIES = true +# jwt secret +AUTH_SECRET = 'secret' +REDIS_SECONDS = 7200 +# redis ip +REDIS_HOST = 'redis' +# redis 端口 +REDIS_PORT = 6379 +# token过期时间 +EXPIRES_IN = '2h' +# 分页默认起始页 (一般可以不修改) +PAGINATION_PAGE = 1 +# 分页默认大小 +PAGINATION_LIMIT = 10 +``` + +修改完`.env`文件后,请执行`docker compose up -d` + + +当执行`docker ps`中`STATUS`列均为`Up`时,表示后端启动成功 + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +b76f3ebebe81 nestjs-back "docker-entrypoint.s…" 12 minutes ago Up 11 minutes 0.0.0.0:3000->3000/tcp nestjs-back-1 +32ae9982b96a redis "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 0.0.0.0:6379->6379/tcp nestjs-redis-1 +94f3b55b7b2b mysql:8 "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp nestjs-mysql-1 +``` + +### 命令启动 + +#### 依赖安装 + +```bash +npm i +``` + +#### 环境准备 + +#### 安装MySQL + +请参考[MySQL 8.0安装](https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/windows-installation.html) + +#### 安装Redis服务 + +请参考[Redis 官方手册](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-windows/) + +请确保您的机器已经安装了`Mysql`与`Redis`服务。接下来,我们需要配置`.env`环境变量文件。`.env`文件示例如下 + +```properties +# 数据库IP +DATABASE_HOST = 'localhost' +# 数据库端口 +DATABASE_PORT = 3306 +# 数据库用户名 +DATABASE_USERNAME = 'root' +# 数据库密码 +DATABASE_PASSWORD = 'root' +# 数据库名 (请确保该库存在) +DATABASE_NAME = 'ospp-nest' +# 请阅读: https://www.typeorm.org/migrations +# 线上环境请关闭 +DATABASE_SYNCHRONIZE = true +DATABASE_AUTOLOADENTITIES = true +# jwt secret +AUTH_SECRET = 'secret' +REDIS_SECONDS = 7200 +# redis ip +REDIS_HOST = 'localhost' +# redis 端口 +REDIS_PORT = 6379 +# token过期时间 +EXPIRES_IN = '2h' +# 分页默认起始页 (一般可以不修改) +PAGINATION_PAGE = 1 +# 分页默认大小 +PAGINATION_LIMIT = 10 +``` + +#### 启动项目 + +在启动项目前请您做好如下检查 + +- [ ] MySQL服务可以正常访问 +- [ ] Redis服务可以正常访问 +- [ ] MySQL中存在`.env`文件中`DATABASE_NAME`字段定义的数据库,且该数据库为空 +- [ ] `.env`文件中`DATABASE_SYNCHRONIZE`为`true` +- [ ] `tiny-pro`后端依赖已经安装 + +完成上述检查后,您可以在`tiny-pro/nestJs`下执行`npm run start`. + +``` +LOG [NestApplication] Nest application successfully started +11ms +Application is running on: http://[::1]:3000 +``` + +当出现上述文本时候即为后端启动成功。 + +## 前端启动 + +### 依赖安装 + +```bash +cd tiny-pro/web +npm i +``` + +### 项目启动 + +在项目启动前,请您确保后端服务已经启动成功,且可以正常访问。我们列出了一个启动前检查清单,您可以对照检查清单来进行启动前检查 + +- [ ] 后端服务启动成功 +- [ ] 前端依赖安装完成 +- [ ] `npm run mock`启动mock服务 + +上述列表全部检查完成后,运行 `npm run start` 即可启动前端服务,浏览器会自行打开项目,当出现下图时则代表启动成功。 + +![启动成功](./images/tiny-pro-show.png) + +## 前端打包 + +对于前端项目打包,只需要执行`npm run build`即可 + +## 后端打包 + +### 命令打包 + +运行`npm run build`即可 + +### docker打包 + +> 这里只阐述默认 tiny-pro 后端打包,如果您进行了修改(例如增加了某些node-gyp依赖,请修改`dockerfile`手动安装`node-gyp`系统级前置依赖) + +运行 `docker build -t tinypro:latest` 即可 + +## 遇到困难? + +加官方小助手微信 opentiny-official,加入技术交流群 + +## 常见问题 + +### 后端docker启动时出现 `Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.` + +这是因为宿主机的某些应用占用了`3306`端口, 请先释放宿主机该端口后手动启动MySQL服务 + +### 后端使用`docker compose up -d`启动时候,出现I/O timeout错误 + +这主要是因为网络问题,您可以手动执行下述命令 + +``` +docker pull node:alpine +docker pull node:lts +``` + +输入完上述命令后再次执行`docker compose up -d`即可。 + + +### 提示 `Lock file exists, if you want init agin, please remove dist or dist/lock` + +为了避免重复初始化,系统会在第一次初始化的时候在`dist`目录下新建`app/lock`文件,如果您需要再次初始化,那么请您删除`dist/app`或者直接删除`dist`文件夹 + +### docker 部署时数据库超时 + +`docker-compose.yaml`实际上配置了`depends_on`字段,但`mysql`镜像并没有提供对应的健康检查。如果服务挂掉,可以等待`mysql`启动成功后手动重启后端服务 + +### 前端跨域问题如何解决 + +对于开发环境来说,可以直接修改`dev-server`的`proxy`. 例如`vite`工具的`server.proxy` + +### 代码无法提交 + +您可以选择移除husky或根据[Angular 规范](https://zj-git-guide.readthedocs.io/zh-cn/latest/message/Angular%E6%8F%90%E4%BA%A4%E4%BF%A1%E6%81%AF%E8%A7%84%E8%8C%83/)书写commit信息 + +### 页面部署后刷新404 + +请移步[Vue Router服务器部署指南](https://router.vuejs.org/guide/essentials/history-mode.html#Example-Server-Configurations) diff --git a/package.json b/package.json index 465ae256..66cb32b9 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,14 @@ "scripts": { "init": "npm i && npx lerna bootstrap", "dev": "lerna run watch --stream --parallel", + "dev:local": "esno ./packages/cli/core/src/index.ts", "build": "lerna run build", "link": "lerna run link", "lint": "eslint . --ext .js,.ts --fix", "format": "prettier --write **/*{.vue,.js,.ts,.html,.json}", "publish": "lerna run build && lerna exec npm publish", "clean": "lerna clean && rm -rf node_modules && lerna run clean", + "clean:dist": "lerna run clean", "commit": "git-cz", "prepare": "husky install" }, @@ -30,6 +32,7 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esno": "^4.7.0", "husky": "^7.0.4", "lerna": "^5.4.2", "lint-staged": "^13.0.3", diff --git a/packages/plugins/link/package.json b/packages/plugins/link/package.json index afb5e7b0..6558325f 100644 --- a/packages/plugins/link/package.json +++ b/packages/plugins/link/package.json @@ -41,7 +41,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", diff --git a/packages/plugins/lint/package.json b/packages/plugins/lint/package.json index f74f04be..96d83d8d 100644 --- a/packages/plugins/lint/package.json +++ b/packages/plugins/lint/package.json @@ -38,7 +38,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", @@ -66,13 +66,13 @@ "engines": { "node": ">=8.9" }, - "dependencies": { + "dependencies": { }, "devDependencies": { "@opentiny/cli-devkit": "^1.0.1", "@opentiny/eslint-config": "^1.0.0", "chalk": "^2.4.2", - "inquirer": "^6.5.1", + "inquirer": "^6.5.1", "@bitjson/npm-scripts-info": "1.0.0", "@types/node": "12.6.8", "ava": "2.2.0", diff --git a/packages/toolkits/dev/package.json b/packages/toolkits/dev/package.json index 65d9264a..d28f5b98 100644 --- a/packages/toolkits/dev/package.json +++ b/packages/toolkits/dev/package.json @@ -26,7 +26,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\"", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", diff --git a/packages/toolkits/dev/template/plugin/package.json b/packages/toolkits/dev/template/plugin/package.json index e4f5078f..fef56cd9 100644 --- a/packages/toolkits/dev/template/plugin/package.json +++ b/packages/toolkits/dev/template/plugin/package.json @@ -36,7 +36,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", diff --git a/packages/toolkits/dev/template/toolkit/package.json b/packages/toolkits/dev/template/toolkit/package.json index a5aa63f9..fd7c5e67 100644 --- a/packages/toolkits/dev/template/toolkit/package.json +++ b/packages/toolkits/dev/template/toolkit/package.json @@ -36,7 +36,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", diff --git a/packages/toolkits/docs/package.json b/packages/toolkits/docs/package.json index ec6b9383..36aaba6f 100644 --- a/packages/toolkits/docs/package.json +++ b/packages/toolkits/docs/package.json @@ -38,7 +38,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", diff --git a/packages/toolkits/pro/.npmignore b/packages/toolkits/pro/.npmignore index 3249f88d..efca8ff4 100644 --- a/packages/toolkits/pro/.npmignore +++ b/packages/toolkits/pro/.npmignore @@ -17,4 +17,10 @@ coverage !template/**/src !template/**/tsconfig.json !template/**/tsconfig.module.json -!template/**/tslint.json \ No newline at end of file +!template/**/tslint.json +node_modules +package-lock.json +pnpm-lock.yaml +yarn.lock +template/**/dist/ +template/**/node_modules/ diff --git a/packages/toolkits/pro/package.json b/packages/toolkits/pro/package.json index 336681a3..5b97f1fd 100644 --- a/packages/toolkits/pro/package.json +++ b/packages/toolkits/pro/package.json @@ -25,7 +25,7 @@ "test": "run-s build test:*", "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", "test:unit": "nyc --silent ava", - "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", + "watch": "run-s clean build:main && run-p \"build:main -- -w\" ", "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", @@ -59,21 +59,23 @@ "cross-spawn": "^7.0.3", "dotenv": "^16.0.3", "ejs": "^3.1.9", - "fs-extra": "^10.1.0", "inquirer": "^8.0.2", "mysql2": "^3.4.2", "open": "^8.4.0" }, "devDependencies": { "@bitjson/npm-scripts-info": "^1.0.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.3", "@types/node": "^14.18.63", "ava": "2.2.0", "codecov": "^3.5.0", "cz-conventional-changelog": "^2.1.0", + "fs-extra": "^11.2.0", "gh-pages": "^2.0.1", "npm-run-all": "^4.1.5", - "nyc": "^14.1.1", + "nyc": "^17.0.0", "prettier": "^1.18.2", "trash-cli": "^3.0.0", "tslint": "^5.18.0", diff --git a/packages/toolkits/pro/src/lib/init.ts b/packages/toolkits/pro/src/lib/init.ts index bfeb1e89..a71a4527 100644 --- a/packages/toolkits/pro/src/lib/init.ts +++ b/packages/toolkits/pro/src/lib/init.ts @@ -1,12 +1,24 @@ import * as path from 'path'; +import * as dotenv from 'dotenv'; +import { copySync } from 'fs-extra'; import chalk from 'chalk'; import spawn from 'cross-spawn'; -import * as dotenv from 'dotenv'; -import mysql from 'mysql2/promise'; import inquirer, { QuestionCollection } from 'inquirer'; import { cliConfig, logs, fs } from '@opentiny/cli-devkit'; -import { ProjectInfo, ServerFrameworks } from './interfaces'; +import { + buildCommand, + buildConfigs, + BuildTool, + devCommand, + devDependencies, + ProjectInfo, + removedCommand, + removeDependencies, + ServerFrameworks, + VueVersion, +} from './interfaces'; import utils from './utils'; +import { existsSync, rmSync, writeFileSync } from 'fs'; const log = logs('tiny-toolkit-pro'); const VUE_TEMPLATE_PATH = 'tinyvue'; @@ -50,14 +62,28 @@ const getProjectInfo = (): Promise => { name: 'serverFramework', message: '请选择您希望使用的服务端技术栈:', choices: [ - { name: 'Egg.js', value: ServerFrameworks.EggJs }, - { name: 'Spring Cloud', value: ServerFrameworks.SpringCloud }, + // { name: 'Egg.js', value: ServerFrameworks.EggJs }, + // { name: 'Spring Cloud', value: ServerFrameworks.SpringCloud }, + { name: 'Nest.js', value: ServerFrameworks.NestJs }, { name: '暂不配置', value: ServerFrameworks.Skip }, ], - default: ServerFrameworks.Skip, + default: ServerFrameworks.NestJs, prefix: '*', when: (answers) => answers.framework === VUE_TEMPLATE_PATH, }, + { + type: 'list', + name: 'buildTool', + message: '请选择你想要的构建工具: ', + choices: [ + { name: 'Vite', value: BuildTool.Vite }, + { name: 'Webpack', value: BuildTool.Webpack }, + { name: 'Rspack', value: BuildTool.Rspack }, + { name: 'Farm', value: BuildTool.Farm }, + ], + default: BuildTool.Vite, + prefix: '*', + }, { type: 'list', name: 'serverConfirm', @@ -72,12 +98,28 @@ const getProjectInfo = (): Promise => { answers.framework === VUE_TEMPLATE_PATH && answers.serverFramework !== ServerFrameworks.Skip, }, + { + type: 'input', + name: 'redisHost', + message: '请输入Redis地址:', + default: 'localhost', + prefix: '*', + when: (answers) => answers.serverConfirm, + }, + { + type: 'input', + name: 'redisPort', + message: '请输入Redis端口:', + default: 6379, + prefix: '*', + when: (answers) => answers.serverConfirm, + }, { type: 'list', name: 'dialect', message: '请选择数据库类型:', choices: [ - { name: 'mySQL', value: 'mysql' }, + { name: 'MySql', value: 'mysql' }, { name: '暂不配置', value: '' }, ], default: 'mysql', @@ -127,104 +169,132 @@ const getProjectInfo = (): Promise => { return inquirer.prompt(question); }; -/** - * 创建数据库、表、并插入一条用户(admin)数据 - * @answers 询问客户端问题的选择值 - */ -const createDatabase = async (answers: ProjectInfo) => { - const { - name, - dialect, - host, - port, - database, - username, - password, - serverFramework, - } = answers; - if (!dialect) return; - - log.info('开始连接数据库服务...'); - const connection = await mysql.createConnection({ - host, - port, - password, - user: username, - multipleStatements: true, - }); - - // 连接数据库服务 - await connection.connect(); - log.info(`连接成功,准备创建数据库(${database})和用户数据表...`); - - // 新建数据库 - await connection.query(`CREATE DATABASE IF NOT EXISTS ${database}`); - await connection.query(` USE ${database}`); - - // 读取sql文件、新建表 - const serverPath = utils.getDistPath(`${name}/${serverFramework}`); - let databaseSqlDir = ''; - switch (serverFramework) { - case ServerFrameworks.EggJs: - databaseSqlDir = path.join(serverPath, 'app/database'); - break; - case ServerFrameworks.SpringCloud: - databaseSqlDir = path.join( - serverPath, - 'server/src/main/resources/database' - ); - break; - default: - break; - } - - const tableSqlDirPath = path.join(databaseSqlDir, 'table'); - const files = fs.readdirSync(tableSqlDirPath); - for (const file of files) { - if (/\.sql$/.test(file)) { - const sqlFilePath = path.join(tableSqlDirPath, file); - const createTableSql = fs.readFileSync(sqlFilePath).toString(); - await connection.query(createTableSql); - } - } - log.info( - '创建成功,开始写入初始用户数据(账号:admin@example.com 密码:admin)...' - ); - - // 插入初始用户数据 - const createUserSqlPath = path.join(databaseSqlDir, 'createuser.sql'); - const createUserSql = fs.readFileSync(createUserSqlPath).toString(); - await connection.query(createUserSql); - log.success('数据库初始化成功!'); - - // 断开连接 - await connection.end(); -}; - /** * 同步创建服务端项目文件目录、文件 * @answers 询问客户端问题的选择值 * @dbAnswers 询问服务端配置的选择值 */ const createServerSync = (answers: ProjectInfo) => { - const { name, serverFramework, dialect } = answers; + const { name, serverFramework } = answers; // 复制服务端相关目录 const serverFrom = utils.getTemplatePath(`server/${serverFramework}`); const serverTo = utils.getDistPath(`${name}/${serverFramework}`); - const defaultConfig = { - // 在未配置数据库信息时,使用默认值替换ejs模板 - dialect: 'mysql', - host: 'localhost', - port: 3306, - username: 'root', - password: '123456', - database: 'tiny_pro_server', + const config = { + DATABASE_HOST: answers.dialect && (answers.host ?? 'localhost'), + DATABASE_PORT: answers.dialect && Number(answers.port ?? 3306), + DATABASE_USERNAME: answers.dialect && (answers.username ?? 'root'), + DATABASE_PASSWORD: answers.dialect && (answers.password ?? 'root'), + DATABASE_NAME: answers.dialect && answers.database, + DATABASE_SYNCHRONIZE: false, + DATABASE_AUTOLOADENTITIES: true, + AUTH_SECRET: 'secret', + REDIS_SECONDS: 7200, + REDIS_HOST: answers.redisHost ?? 'localhost', + REDIS_PORT: Number(answers.redisPort ?? 6379), + EXPIRES_IN: '2h', + PAGINATION_PAGE: 1, + PAGINATION_LIMIT: 10, }; + const envStr = objToEnv(config); + copySync(serverFrom, serverTo); + writeFileSync(path.join(serverTo, '.env'), envStr); +}; - fs.copyTpl(serverFrom, serverTo, dialect ? answers : defaultConfig, { - overwrite: true, - notTextFile: ['.jar'], - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const objToEnv = (obj: Record) => { + return Object.entries(obj) + .map(([key, value]) => { + const v = typeof value === 'string' ? `'${value}'` : value; + return [key, '=', v].join(' '); + }) + .join('\n'); +}; + +const packageJsonProcess = ( + buildTool: BuildTool, + packages: Record>, + currentPath: string +) => { + const match = (pattern: RegExp, items: Array) => { + return items.filter((item) => pattern.test(item)); + }; + const removeDeps = () => { + const devDeps = devDependencies[buildTool]; + devDeps.forEach((devDep) => { + if (typeof devDep === 'string') { + packages.devDependencies[devDep] = undefined; + } + if (devDep instanceof RegExp) { + const deps = match(devDep, Object.keys(packages.devDependencies)); + if (!deps.length) { + return; + } + deps.forEach((dep) => { + if (packages.devDependencies[dep]) { + packages.devDependencies[dep] = undefined; + } + }); + } + }); + const dependencies = removeDependencies[buildTool]; + dependencies.forEach((dep: string | RegExp) => { + if (typeof dep === 'string') { + if (!packages.dependencies[dep]) { + return; + } + packages.dependencies[dep] = undefined; + } + if (dep instanceof RegExp) { + const keys = match(dep, Object.keys(packages.devDependencies)); + keys.forEach((key) => { + if (!packages.dependencies[key]) { + return; + } + packages.dependencies[key] = undefined; + }); + } + }); + }; + const replaceScript = (name: string, command: string | undefined) => { + packages.scripts[name] = command; + }; + const removeScripts = () => { + const scripts = removedCommand; + scripts.forEach((script) => { + replaceScript(script, undefined); + }); + }; + const replaceBuildCommand = () => { + const command = buildCommand[buildTool]; + replaceScript('build', command); + }; + const replaceDevCommand = () => { + const command = devCommand[buildTool]; + replaceScript('start', command); + }; + const remove = () => { + const removedPaths = buildConfigs[buildTool]; + const paths = removedPaths + .filter((removedPath) => existsSync(path.join(currentPath, removedPath))) + .map((p) => path.join(currentPath, p)); + if (!paths.length) { + return; + } + let willRemovePath = ''; + try { + paths.forEach((removePath) => { + willRemovePath = removePath; + rmSync(removePath, { recursive: true, force: true }); + }); + } catch { + log.error(`删除${willRemovePath}错误`); + } + }; + removeDeps(); + removeScripts(); + replaceBuildCommand(); + replaceDevCommand(); + return remove; }; /** @@ -233,15 +303,13 @@ const createServerSync = (answers: ProjectInfo) => { * @dbAnswers 询问服务端配置的选择值 */ const createProjectSync = (answers: ProjectInfo) => { - const { framework, description, name, serverConfirm } = answers; + const { framework, description, name, serverConfirm, buildTool } = answers; const templatePath = - framework === VUE_TEMPLATE_PATH ? VUE_TEMPLATE_PATH : NG_TEMPLATE_PATH; - + framework === VUE_TEMPLATE_PATH ? VueVersion.Vue3 : NG_TEMPLATE_PATH; // 模板来源目录 const from = utils.getTemplatePath(templatePath); // 复制模板的目标目录 const to = utils.getDistPath(serverConfirm ? `${name}/web` : name); - fs.copyTpl(from, to); // 将项目名称、描述写入 package.json中 try { @@ -250,11 +318,13 @@ const createProjectSync = (answers: ProjectInfo) => { fs.readFileSync(packageJsonPath, { encoding: 'utf8' }) ); packageJson = { ...packageJson, name, description }; + const remove = packageJsonProcess(buildTool, packageJson, to); fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), { encoding: 'utf8', }); + remove(); } catch (e) { - log.error('配置项目信息创失败'); + log.error('配置项目信息创建失败'); } // ng模板不开启mock以及服务 @@ -295,9 +365,9 @@ export const installDependencies = (answers: ProjectInfo) => { cwd: `${name}/${serverFramework}/`, stdio: 'inherit', }); - if(installServiceResult.status === 0) { + if (installServiceResult.status === 0) { log.success('服务端 npm 依赖安装成功'); - }else { + } else { throw new Error(installServiceResult.error); } } @@ -307,9 +377,9 @@ export const installDependencies = (answers: ProjectInfo) => { cwd: serverConfirm ? `${name}/web` : `${name}/`, stdio: 'inherit', }); - if(installClientResult.status === 0) { + if (installClientResult.status === 0) { log.success('客户端 npm 依赖安装成功'); - }else { + } else { throw new Error(installClientResult.error); } @@ -380,19 +450,8 @@ export default async () => { } // 初始化数据库 - try { - await createDatabase(projectInfo); - } catch (e) { - log.error( - `数据库初始化失败,请确认数据库配置信息正确并手动初始化数据库${e}` - ); - } + // 初始化不应该在cli做,而是在后端 // 安装依赖 - try { - installDependencies(projectInfo); - } catch (e) { - log.error('npm 依赖安装失败'); - log.info('请手动执行 tiny i 或 npm i'); - } + log.info('初始化成功,请运行npm i或tiny i 安装依赖'); }; diff --git a/packages/toolkits/pro/src/lib/interfaces.ts b/packages/toolkits/pro/src/lib/interfaces.ts index 62805915..073f6bfe 100644 --- a/packages/toolkits/pro/src/lib/interfaces.ts +++ b/packages/toolkits/pro/src/lib/interfaces.ts @@ -6,6 +6,10 @@ export interface CliOption { clientOptions: any; } +export enum VueVersion { + Vue3 = 'tinyvue', +} + /** * 服务端类型 */ @@ -16,6 +20,96 @@ export enum ServerFrameworks { Skip = '', } +export enum BuildTool { + Vite = 'vite', + Webpack = 'webpack', + Rspack = 'rspack', + Farm = 'farm', +} + +export const buildConfigs = { + vite: ['webpack.config.js', 'rspack.config.js', 'farm.config.ts'], + webpack: ['config', 'rspack.config.js', 'farm.config.ts'], + rspack: ['config', 'webpack.config.js', 'farm.config.ts'], + farm: ['config', 'webpack.config.js', 'rspack.config.js'], +}; + +export const buildCommand = { + vite: 'vite build --config ./config/vite.config.prod.ts', + webpack: 'webpack --config webpack.config.js', + rspack: 'rspack build', + farm: 'farm build', +}; + +export const devCommand = { + vite: 'vite --config ./config/vite.config.dev.ts --port 3031', + webpack: 'webpack-dev-server --progress --config webpack.config.js', + rspack: 'rspack serve', + farm: 'farm', +}; + +export const removedCommand = [ + 'dev:wp', + 'dev:rp', + 'build:wp', + 'build:rp', + 'dev', + 'dev:fr', + 'build:fr', +]; + +/** + * 需要删除的包 + */ +export const removeDependencies = { + vite: ['style-resources-loader', 'vue-style-loader'], + webpack: [], + rspack: [], + farm: [], +}; + +/** + * 需要删除的包 + */ +export const devDependencies = { + vite: [ + '@babel/preset-env', + '@babel/preset-typescript', + /@rspack\/.*/, + /webpack/, + 'vue-loader', + 'import-meta-loader', + 'css-loader', + 'core-js', + 'babel-loader', + /@farmfe\/.*/, + ], + rspack: [ + /@vitejs\/.*/, + /vite-.*/, + 'vite', + 'webpack', + 'webpack-cli', + 'webpack-dev-server', + /@farmfe\/.*/, + ], + webpack: [ + /@vitejs\/.*/, + /vite-.*/, + 'vite', + '@rspack/cli', + '@rspack/core', + /@farmfe\/.*/, + ], + farm: [ + 'webpack', + 'webpack-cli', + 'webpack-dev-server', + /webpack-.*/, + /@rspack\/.*/, + ], +}; + /** * 初始化问题的选项 -> 选择的值 */ @@ -31,4 +125,8 @@ export interface ProjectInfo { database?: string; username?: string; password?: string; + redisHost?: string; + redisPort?: number; + buildTool: BuildTool; + vueVersion: VueVersion; } diff --git a/packages/toolkits/pro/template/server/nestJs/.dockerignore b/packages/toolkits/pro/template/server/nestJs/.dockerignore new file mode 100644 index 00000000..ebce4aeb --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.gitignore +dist diff --git a/packages/toolkits/pro/template/server/nestJs/.env b/packages/toolkits/pro/template/server/nestJs/.env new file mode 100644 index 00000000..bb0c5104 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/.env @@ -0,0 +1,18 @@ +DATABASE_HOST = 'localhost' +DATABASE_PORT = 3306 +DATABASE_USERNAME = 'root' +DATABASE_PASSWORD = 'root' +DATABASE_NAME = 'ospp-nest' +DATABASE_SYNCHRONIZE = 'false' +DATABASE_AUTOLOADENTITIES = 'true' + +AUTH_SECRET = 'secret' + +REDIS_SECONDS = 7200 +REDIS_HOST = 'localhost' +REDIS_PORT = 6379 + +EXPIRES_IN = '2h' + +PAGINATION_PAGE = 1 +PAGINATION_LIMIT = 10 diff --git a/packages/toolkits/pro/template/server/nestJs/.eslintrc.js b/packages/toolkits/pro/template/server/nestJs/.eslintrc.js index 28471954..23fea9d3 100644 --- a/packages/toolkits/pro/template/server/nestJs/.eslintrc.js +++ b/packages/toolkits/pro/template/server/nestJs/.eslintrc.js @@ -1,8 +1,8 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { - project: 'tsconfig.json', sourceType: 'module', + tsconfigRootDir: __dirname, }, plugins: ['@typescript-eslint/eslint-plugin'], extends: [ diff --git a/packages/toolkits/pro/template/server/nestJs/.gitignore b/packages/toolkits/pro/template/server/nestJs/.gitignore index b5e5f975..12a9d9ff 100644 --- a/packages/toolkits/pro/template/server/nestJs/.gitignore +++ b/packages/toolkits/pro/template/server/nestJs/.gitignore @@ -18,4 +18,7 @@ npm-debug.log /.nyc_output # dist -/dist \ No newline at end of file +/dist + +#env +/.env diff --git a/packages/toolkits/pro/template/server/nestJs/docker-compose.yml b/packages/toolkits/pro/template/server/nestJs/docker-compose.yml index eee25f20..24793fb0 100644 --- a/packages/toolkits/pro/template/server/nestJs/docker-compose.yml +++ b/packages/toolkits/pro/template/server/nestJs/docker-compose.yml @@ -6,6 +6,25 @@ services: restart: always environment: MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: test + MYSQL_DATABASE: ospp-nest ports: - "3306:3306" + redis: + image: redis + ports: + - "6379:6379" + back: + build: + context: . + dockerfile: dockerfile + env_file: + - .env + ports: + - "3000:3000" + depends_on: + - mysql + - redis + volumes: + - ./data:/APP/dist/data + + diff --git a/packages/toolkits/pro/template/server/nestJs/dockerfile b/packages/toolkits/pro/template/server/nestJs/dockerfile new file mode 100644 index 00000000..f9c4e04d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/dockerfile @@ -0,0 +1,31 @@ +FROM node:lts as BUILDER +WORKDIR /builder/ +ADD . . +RUN npm install pnpm -g && \ + pnpm i && \ + pnpm build + +FROM node:alpine as prod + +WORKDIR /APP +COPY --from=BUILDER /builder/dist /APP/dist +COPY --from=BUILDER ["/builder/locales.json", "/builder/package.json", "/APP/"] +RUN npm install pnpm -g && \ + pnpm i +ENV DATABASE_HOST "" +ENV DATABASE_PORT "" +ENV DATABASE_USERNAME "" +ENV DATABASE_PASSWORD "" +ENV DATABASE_USERNAME "" +ENV DATABASE_SYNCHRONIZE "" +ENV DATABASE_AUTOLOADENTITIES true +ENV AUTH_SECRET "" +ENV REDIS_SECONDS "" +ENV REDIS_HOST "" +ENV REDIS_PORT "" +ENV EXPIRES_IN "" +ENV PAGINATION_PAGE "" +ENV PAGINATION_LIMIT 10 +EXPOSE 3000 +VOLUME [ "./dist/data" ] +CMD ["node", "./dist/main.js"] diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts new file mode 100644 index 00000000..bd9c9d54 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DbService } from './db.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + useClass: DbService, + }), + ], + providers: [DbService], + exports: [DbService], +}) +export class DbModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts new file mode 100644 index 00000000..d81007f4 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DbService } from './db.service'; + +describe('DbService', () => { + let service: DbService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DbService], + }).compile(); + + service = module.get(DbService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts new file mode 100644 index 00000000..325c23fc --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; + +@Injectable() +export class DbService implements TypeOrmOptionsFactory { + // 注入config service取得env变量 + constructor() {} + // 回传TypeOrmOptions对象 + createTypeOrmOptions(): TypeOrmModuleOptions { + return { + type: 'mysql', + host: process.env.DATABASE_HOST, + port: parseInt(process.env.DATABASE_PORT), + username: process.env.DATABASE_USERNAME, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + synchronize: process.env.DATABASE_SYNCHRONIZE === 'true', + autoLoadEntities: process.env.DATABASE_AUTOLOADENTITIES === 'true', + }; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts new file mode 100644 index 00000000..07f92699 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts @@ -0,0 +1,2 @@ +export * from './db.module'; +export * from './db.service'; diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json b/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json new file mode 100644 index 00000000..dfa98ed6 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/db" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/i18n.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/i18n.ts new file mode 100644 index 00000000..236b14d3 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/i18n.ts @@ -0,0 +1,15 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Lang } from './lang'; + +@Entity('i18') +export class I18 { + @PrimaryGeneratedColumn() + id: number; + @ManyToOne(() => Lang) + // @Column() + lang: Lang; + @Column() + key: string; + @Column({ type: 'longtext' }) + content: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts new file mode 100644 index 00000000..aaea9903 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts @@ -0,0 +1,6 @@ +export * from './user'; +export * from './permission'; +export * from './role'; +export * from './menu'; +export * from './i18n'; +export * from './lang'; diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/lang.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/lang.ts new file mode 100644 index 00000000..a46a925b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/lang.ts @@ -0,0 +1,12 @@ +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { I18 } from './i18n'; + +@Entity() +export class Lang { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @OneToMany(() => I18, (i18) => i18.lang) + i18: I18[]; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts new file mode 100644 index 00000000..01c91794 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts @@ -0,0 +1,23 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('menu') +export class Menu { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @Column() + order: number; + @Column({ nullable: true }) + parentId: number; + @Column() + menuType: string; + @Column({ nullable: true }) + icon: string; + @Column() + component: string; + @Column() + path: string; + @Column() + locale: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts new file mode 100644 index 00000000..546f3e1a --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('permission') +export class Permission { + @PrimaryGeneratedColumn() + id: number; + @Column() + desc: string; + @Column() + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts new file mode 100644 index 00000000..af361296 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Permission } from './permission'; +import { Menu } from './menu'; + +@Entity('role') +export class Role { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @ManyToMany(() => Permission, { + onUpdate: 'CASCADE', + }) + @JoinTable({ name: 'role_permission' }) + permission: Permission[]; + + @ManyToMany(() => Menu) + @JoinTable({ name: 'role_menu' }) + menus: Menu[]; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts new file mode 100644 index 00000000..079d43e1 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts @@ -0,0 +1,65 @@ +import { + BeforeInsert, + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinTable, + ManyToMany, + PrimaryColumn, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Role } from './role'; +import * as crypto from 'crypto'; + +export const encry = (value: string, salt: string) => + crypto.pbkdf2Sync(value, salt, 1000, 18, 'sha256').toString('hex'); + +@Entity('user') +export class User { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @Column() + email: string; + @Column() + password: string; + @ManyToMany(() => Role) + @JoinTable({ name: 'user_role' }) + role: Role[]; + @Column({ nullable: true }) + department: string; + @Column({ nullable: true }) + employeeType: string; + @Column({ type: 'timestamp', nullable: true }) + probationStart: string; + @Column({ type: 'timestamp', nullable: true }) + probationEnd: string; + @Column({ nullable: true }) + probationDuration: string; + @Column({ type: 'timestamp', nullable: true }) + protocolStart: string; + @Column({ type: 'timestamp', nullable: true }) + protocolEnd: string; + @Column({ nullable: true }) + address: string; + @Column({ nullable: true }) + status: number; + @CreateDateColumn() + createTime: Date; + @UpdateDateColumn() + updateTime: Date; + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + create_time: Date; + @Column({ nullable: true }) + salt: string; + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + update_time: Date; + @BeforeInsert() + beforeInsert() { + this.salt = crypto.randomBytes(4).toString('base64'); + this.password = encry(this.password, this.salt); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json b/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json new file mode 100644 index 00000000..4777fe3b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/models" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts new file mode 100644 index 00000000..b4958682 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts new file mode 100644 index 00000000..9300ac3e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisService } from './redis.service'; + +describe('RedisService', () => { + let service: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RedisService], + }).compile(); + + service = module.get(RedisService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts new file mode 100644 index 00000000..b1b8c0cf --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService { + private redisClient: Redis; + constructor() { + this.redisClient = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + }); + } + async setUserToken( + email: string, + token: string, + ttl: number + ): Promise { + return this.redisClient.set(`user:${email}:token`, token, 'EX', ttl); + } + + async getUserToken(email: string): Promise { + return this.redisClient.get(`user:${email}:token`); + } + + async delUserToken(email: string): Promise { + //退出登录后,将token从Redis删除 + await this.redisClient.del(`user:${email}:token`); + return; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/locales.json b/packages/toolkits/pro/template/server/nestJs/locales.json new file mode 100644 index 00000000..6155579d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/locales.json @@ -0,0 +1,1208 @@ +{ + "enUS": { + "en-US": "English", + "zh-CN": "中文", + "zh-TW": "中国台湾", + "hello": "Hello {name}", + "code": "en-US", + "yes": "Yes", + "no": "No", + "menu.board": "Dashboard Page", + "menu.home": "Monitoring page", + "menu.work": "workbench", + "menu.list": "List", + "menu.result": "Result", + "menu.exception": "Exception", + "menu.form": "Form", + "menu.profile": "Profile", + "menu.profile.detail": "Basic details page", + "menu.visualization": "Data Visualization", + "menu.menuPage": "Menu Page", + "menu.menuPage.second": "Second Page", + "menu.menuPage.third": "Menu Demo Page", + "menu.user": "User Center", + "menu.systemManager": "System Manager", + "menu.userManager": "User Manager", + "menu.userManager.info": "All User Info", + "menu.userManager.setting": "All User Setting", + "menu.userManager.useradd": "Add User", + "menu.permission": "Permission Manager", + "menu.permission.info": "All Permission Info", + "menu.permission.setting": "Permission Setting", + "menu.permission.permissionAdd": "Add Permission", + "menu.role": "Role Manager", + "menu.role.info": "All Role Info", + "menu.menu": "Menu Manager", + "menu.menu.info": "All Menu Info", + "navbar.docs": "Docs", + "navbar.action.locale": "Switch to English", + "messageBox.switchRoles": "Switch Roles", + "messageBox.userCenter": "User Center", + "messageBox.userSettings": "User Settings", + "messageBox.logout": "Logout", + "messageBox.updatePwd": "Update Password", + "message.delete.success": "Delete Success", + "menu.cloud": "Cloud service capability", + "menu.btn.confirm": "Submit", + "menu.i18n": "I18n Manage", + "theme.title.main": "Personalized configuration", + "theme.title.first": "theme", + "theme.title.default": "Default Theme", + "theme.title.honey": "Honey Theme", + "theme.title.violet": "Violet Theme", + "theme.title.deepness": "Deep Night Sky Theme", + "theme.title.deep": "Dark Theme", + "theme.title.light": "Light Theme", + "theme.title.customization": "Custom Themes", + "theme-title-recommend": "Recommended Topics", + "theme-text-default": "Science and technology, exploration, research, precision, tolerance", + "theme-text-honey": "Bright, sensual, warm, positive, energetic", + "theme-text-violet": "Elegant, romantic, gentle, mysterious, noble", + "theme-text-deepness": "Smooth, Neutral, Space, Strength, Hard", + "theme-text-dark": "Deep, decisive, brave, tenacious, yearning", + "settings.title": "Settings", + "settings.themeColor": "Theme Color", + "settings.content": "Content Setting", + "settings.search": "Search", + "settings.language": "Language", + "settings.navbar": "simple mode", + "settings.menuWidth": "Menu Width (px)", + "settings.navbar.alerts": "alerts", + "settings.navbar.help": "Help Center", + "settings.menu": "classic mode", + "settings.tabBar": "Tab Bar", + "settings.footer": "fashion mode", + "settings.colorWeek": "Theme Configuration", + "settings.alertContent": "After the configuration is only temporarily effective, if you want to really affect the project, click the \"Copy Settings\" button below and replace the configuration in settings.json.", + "settings.copySettings": "Copy Settings", + "settings.copySettings.message": "Copy succeeded, please paste to file src/settings.json.", + "settings.close": "Close", + "settings.color.tooltip": "10 gradient colors generated according to the theme color", + "setting.user.set": "User Settings", + "setting.loginout": "Logout succeeded", + "setting.copy": "Copying succeeded", + "setting.input.search": "Enter a keyword", + "setting.foot.title": "Produced by OpenTiny", + "setting.mode.navbar": "Collapse Header, Footer", + "setting.mode.menu": "Collapse menu", + "setting.mode.footer": "Collapse Footer", + "login.form.mode": "Account and password login", + "login.form.mail": "Email login", + "login.form.title": "Login to Tiny Pro", + "login.form.userName.errMsg": "Username cannot be empty", + "login.form.password.errMsg": "Password cannot be empty", + "login.form.mailName.errMsg": "The mailbox name cannot be empty", + "login.form.mailpassword.errMsg": "The email password cannot be empty", + "login.form.mailpassword2.errMsg": "Confirm password cannot be empty", + "login.form.login.errMsg": "Login error, refresh and try again", + "login.form.login.success": "welcome to use", + "login.form.userName.placeholder": "Username: admin", + "login.form.password.placeholder": "Password: admin", + "login.form.mailName.placeholder": "EmailName:123{'@'}example.com", + "login.form.mailpassword.placeholder": "Password:admin", + "login.form.registerMail.placeholder": "Register Email:", + "login.form.registerPassword.placeholder": "Registration password:", + "login.form.registerConfirmPassword.placeholder": "Confirm Password:", + "login.form.rememberPassword": "Remember password", + "login.form.forgetPassword": "Forgot password", + "login.form.registration": "Sign up", + "login.form.login": "login", + "login.form.register": "register", + "login.form.registerPass": "The verification is successful and the registration is successful", + "login.form.registerError": "Verification failed!", + "login.form.change": "Sign in with", + "login.form.mailInput": "Email:", + "login.form.passwordInput": "Password:", + "login.form.passwordConfirm": "Confirm Password:", + "login.form.checkUsername": "Letters, numbers, underscores, dashes, dots{'@'}Letters, numbers, dashes", + "login.form.checkPassword": "At least eight characters, including at least one uppercase letter, one lowercase letter, and one digit", + "login.form.confirmPassword": "Inconsistent passwords", + "login.banner.slogan1": "Out-of-the-box high-quality template", + "login.banner.subSlogan1": "Rich page templates, covering most typical business scenarios", + "login.banner.slogan2": "Built-in solutions to common problems", + "login.banner.subSlogan2": "Internationalization, routing configuration, state management everything", + "login.banner.slogan3": "Access visualization enhancement tool AUX", + "login.banner.subSlogan3": "Realize flexible block development", + "login.icon.language": "language", + "login.tip.info": "User name: admin; password: admin", + "login.tip.mail": "User name: admin{'@'}example.com; password: admin", + "login.tip.right": "Enter the correct user name and password", + "login.main.text": "TinyPro Mid-Back-End Front-End Solution", + "menu.list.searchTable": "Search Table", + "searchTable.form.number": "Set Number", + "searchTable.form.number.placeholder": "Please enter Set Number", + "searchTable.form.name": "Set Name", + "searchTable.form.name.placeholder": "Please enter Set Name", + "searchTable.form.contentType": "Content Type", + "searchTable.form.contentType.img": "image-text", + "searchTable.form.contentType.horizontalVideo": "Horizontal short video", + "searchTable.form.contentType.verticalVideo": "Vertical short video", + "searchTable.form.filterType": "Filter Type", + "searchTable.form.filterType.artificial": "artificial", + "searchTable.form.filterType.rules": "Rules", + "searchTable.form.createdTime": "Create Date", + "searchTable.form.status": "Status", + "searchTable.form.status.online": "Online", + "searchTable.form.status.offline": "Offline", + "searchTable.form.status.doing": "Ongoing", + "searchTable.form.search": "Search", + "searchTable.form.reset": "Reset", + "searchTable.form.selectDefault": "All", + "searchTable.operation.create": "Create", + "searchTable.operation.import": "Export", + "searchTable.operation.download": "Download", + "searchTable.form.collapse": "Collapse", + "searchTable.form.extend": "Extend", + "searchTable.form.input": "Please enter", + "searchTable.form.create": "Creating a Topic", + "searchTable.columns.number": "ID", + "searchTable.columns.name": "Set Name", + "searchTable.columns.department": "Department", + "searchTable.columns.filterType": "Department Level", + "searchTable.columns.count": "Count", + "searchTable.columns.workname": "Workbench", + "searchTable.columns.enablement": "Enablement", + "searchTable.columns.type": "Person Type", + "searchTable.columns.study": "Institute", + "searchTable.columns.role": "Role", + "searchTable.columns.updatesperson": "Updates Person", + "searchTable.columns.createdTime": "CreatedTime", + "searchTable.columns.status": "Status", + "searchTable.columns.operations": "Operations", + "searchTable.columns.operations.view": "View", + "searchTable.columns.operations.delete": "Delete", + "searchTable.collapse.restores": "restores", + "searchTable.collapse.full": "Full", + "menu.form.step": "Step Form", + "stepForm.button.submit": "Create", + "stepForm.button.cancel": "Cancel", + "stepForm.button.restore": "Restores", + "stepForm.probation.day": "Day", + "stepForm.coaching.process": "Coaching Process", + "stepForm.start.date": "Labor Contract Start Date", + "stepForm.end.date": "Labor Contract End Date", + "stepForm.probation.period": "Probation Period", + "stepForm.probation.start": "Trial Start and End Date", + "stepForm.recruitment.type": "Recruitment Type", + "stepForm.recruitment.position": "Position", + "stepForm.recruitment.department": "Department", + "stepForm.start.coaching": "Start coaching", + "stepForm.immediate.supervisor": "Enter the mentor immediate supervisor", + "stepForm.overall.goals": "Set overall goals", + "stepForm.overall.summary": "Submit the overall summary", + "stepForm.overall.end": "End", + "stepForm.collapse.base": "Coaching Basic Information", + "stepForm.collapse.supervisor": "Entry Supervisor", + "stepForm.collapse.goals": "Set overall goals", + "stepForm.collapse.summary": "Submit the overall summary", + "stepForm.coach.position": "Cultivating Positions", + "stepForm.coach.culture": "Training Department", + "stepForm.coach.mentor": "Mentor", + "stepForm.coach.startTime": "Actual Coaching Start Date", + "stepForm.coach.endTime": "Actual Coaching End Date", + "stepForm.dire.supervisor": "Mentor Supervisor", + "stepForm.dire.remarks": "Mentor Information Remarks", + "stepForm.dire.startTime": "Start Coaching Date", + "stepForm.dire.endTime": "Coaching End Date", + "stepForm.target.list": "Target List", + "stepForm.target.sure": "Set goals", + "stepForm.sum.self": "Self-summarization", + "stepForm.error.target": "At least one item exists on the right", + "stepForm.head.admin": "User name", + "menu.form.base": "Base Form", + "baseForm.form.label.no": "no", + "baseForm.form.label.yes": "yes", + "baseForm.form.label.placeholder": "Please select", + "baseForm.form.label.frequencyone": "By Month", + "baseForm.form.label.frequencytwo": "By Week", + "baseForm.form.label.frequencythree": "By biweekly", + "baseForm.form.label.frequencyfour": "By Quarter", + "baseForm.form.label.personone": "Local employees", + "baseForm.form.label.persontwo": "Non-Employee", + "baseForm.form.label.personthree": "Chinese employees", + "baseForm.form.label.projectone": "Training for new employees of the manufacturing department", + "baseForm.form.label.projecttwo": "On-boarding coaching for new employees", + "baseForm.form.label.projectthree": "UI Automation Test Coaching Project", + "baseForm.form.label.people": "Applicable Populations", + "baseForm.form.label.rank": "Job Level", + "baseForm.form.label.type": "Project Type", + "baseForm.form.label.business": "Service attribute", + "baseForm.form.label.Objectives": "Overall objective", + "baseForm.form.label.culture": "Training Department", + "baseForm.form.label.develop": "develop", + "baseForm.form.label.developone": "Trainees", + "baseForm.form.label.developtwo": "Mentor", + "baseForm.form.label.effective": "Effective Condition", + "baseForm.form.label.effectiveone": "Effective without approval", + "baseForm.form.label.effectivetwo": "Mentor Approval", + "baseForm.form.label.effectivethree": "Immediate supervisor approval", + "baseForm.form.label.effectivefour": "Mentors and immediate supervisors approve the application", + "baseForm.form.label.plan": "Phase Plan", + "baseForm.form.label.confirm": "Whether to develop", + "baseForm.form.label.frequency": "Formulation frequency", + "baseForm.form.label.role": "Goal Setting Role", + "baseForm.form.label.roleone": "Trainees", + "baseForm.form.label.roletwo": "Mentor", + "baseForm.form.label.condition": "Target Effective Condition", + "baseForm.form.label.conditionone": "Effective without approval", + "baseForm.form.label.conditiontwo": "Mentor Approval", + "baseForm.form.label.conditionthree": "Immediate supervisor approval", + "baseForm.form.label.conditionfour": "Mentors and immediate supervisors approve the application", + "baseForm.form.label.staged": "Phase Evaluation", + "baseForm.form.label.stagedone": "Only mentor evaluation is required", + "baseForm.form.label.stagedtwo": "Only immediate supervisor evaluation is required", + "baseForm.form.label.stagedthree": "Need to be evaluated by the mentor and immediate supervisor", + "baseForm.form.label.wholeconfirm": "Whether to develop", + "baseForm.form.label.evaluation": "Overall evaluation", + "baseForm.form.label.evaluationyes": "Mentors and immediate supervisors are required for evaluation", + "baseForm.form.label.evaluationno": "No mentor is required, and the immediate supervisor evaluates it", + "baseForm.form.label.mentortitle": "Mentor Selection", + "baseForm.form.label.mentortip": "Only mentors with valid qualifications can be selected from the mentor resource pool. If you do not select a mentor from the mentor resource pool, the basic qualifications of the mentor will not be verified", + "baseForm.form.label.mentor": "Select Mentor Only from Mentor Resource Pool", + "baseForm.form.label.remindertitle": "Reminder of coaching communication records", + "baseForm.form.label.reminder": "Require Reminder", + "baseForm.form.submit": "Submit", + "baseForm.form.submit.success": "Form submitted successfully", + "baseForm.form.cancel": "Cancel", + "baseForm.form.submit.error": "Please complete the required items first", + "baseForm.form.record": "Version Record", + "baseForm.form.project": "Project Type", + "baseForm.form.get.error": "Failed to obtain data", + "menu.result.success": "Success", + "success.result.title": "The submission result page displays the processing results of a series of operation tasks.", + "menu.result.messageSuccess": "The coaching process is submitted successfully", + "menu.btn.submit": "Start", + "menu.btn.cancel": "Cancel", + "menu.line.process": "Current progress", + "menu.result.messageEnd": "The coaching process has been submitted", + "menu.result.error": "Error", + "error.result.title": "The submission result page displays the processing results of a series of operation tasks", + "menu.result.messageError": "Failed to submit the coaching process", + "error.result.home": "Back", + "menu.exception.403": "403", + "exception.result.403.description": "Access to this resource on the server is denied.", + "exception.result.403.back": "Back", + "exception.result.permissions.403": "Contact the administrator to apply for the permission.。", + "menu.exception.404": "404", + "exception.result.404.description": "Whoops, this page is gone.", + "exception.result.404.retry": "Retry", + "exception.result.404.back": "Back", + "exception.result.permissions.404": "Check the network connection and try to refresh the page.", + "menu.exception.500": "500", + "exception.result.500.description": "Internal server error", + "exception.result.500.back": "Back", + "exception.result.permissions.500": "Check the network connection and try to refresh the page.", + "menu.user.info": "User Center", + "userInfo.tab.one": "My plan", + "userInfo.tab.two": "My mission", + "userInfo.filter.sort": "Sort by Time", + "userInfo.filter.startTime": "Start Date", + "userInfo.filter.endTime": "End Date", + "userInfo.end.positiveOrder": "By end time in positive order", + "userInfo.end.reverseOrder": "In reverse order by end time", + "userInfo.start.positiveOrder": "Start time in positive order", + "userInfo.start.reverseOrder": "Start time in reverse order", + "userInfo.btn.search": "Search", + "userInfo.btn.reset": "Reset", + "userInfo.status.status": "Status", + "userInfo.status.optionA": "Completed", + "userInfo.status.optionB": "Overdue", + "userInfo.status.optionC": "About to expire", + "userInfo.status.optionD": "Unfinished", + "userInfo.type.type": "Type", + "userInfo.type.optionA": "Organizational arrangements", + "userInfo.type.optionB": "Phase Plan", + "userInfo.type.optionC": "autonomous learning", + "userInfo.table.columnA": "Program Name", + "userInfo.table.columnB": "Completion Time", + "userInfo.table.columnC": "Status", + "userInfo.table.columnD": "Type", + "userInfo.week.1": "2 weeks onboarding", + "userInfo.month.1": "1 month onboarding", + "userInfo.month.2": "2 month onboarding", + "userInfo.month.3": "3 month onboarding", + "userInfo.month.4": "4 month onboarding", + "userInfo.month.5": "5 month onboarding", + "userInfo.month.6": "6 month onboarding", + "userInfo.month.7": "7 month onboarding", + "userInfo.month.8": "8 month onboarding", + "userInfo.month.9": "9 month onboarding", + "userInfo.month.10": "10 month onboarding", + "userInfo.month.11": "11 month onboarding", + "userInfo.month.12": "12 month onboarding", + "userInfo.month.13": "13 month onboarding", + "userInfo.month.14": "14 month onboarding", + "userInfo.month.15": "15 month onboarding", + "userInfo.month.16": "16 month onboarding", + "userInfo.month.17": "17 month onboarding", + "userInfo.time.message": "The end time is earlier than the start time", + "userInfo.filter.all": "Please complete all current filters", + "menu.user.setting": "User Setting", + "userSetting.cancel": "Cancel", + "userSetting.reset": "Reset", + "userSetting.department": "Department:", + "userSetting.position": "Position:", + "userSetting.type": "Recruitment Type:", + "userSetting.date": "Trial Start and End Date:", + "userSetting.during": "Probation Period:", + "userSetting.startTime": "Labor Contract Start Date:", + "userSetting.endTime": "Labor Contract End Date:", + "userSetting.first": "Start Time", + "userSetting.last": "End Time", + "menu.plan.department": "Training Department", + "menu.plan.resource": "Human Resource Mgmt Dept", + "menu.plan.job": "Job Level", + "menu.plan.person": "Person Type", + "menu.plan.attribute": "Service attribute", + "menu.plan.develop": "Whether to develop", + "menu.plan.yes": "yes", + "menu.plan.no": "no", + "menu.plan.role": "Develop Roles", + "menu.plan.mentor": "Mentor", + "menu.plan.condition": "Effective Condition", + "menu.plan.approval": "Immediate supervisor approval", + "menu.plan.frequency": "Formulation frequency", + "menu.plan.month": "By Month", + "menu.plan.goal": "Goal Setting Role", + "menu.plan.trainees": "Trainees", + "menu.plan.teacher": "Mentor Approval", + "menu.plan.phase": "Phase Evaluation", + "menu.plan.evaluation": "Need to be evaluated by mentors and immediate supervisors", + "menu.plan.whole": "Overall evaluation", + "menu.plan.pool": "Select Mentor Only from Mentor Resource Pool", + "menu.plan.time": "Update Time", + "menu.plan.version": "Version number", + "menu.plan.operation": "Operation", + "menu.plan.updated": "Updated by", + "work.mock.employees": "Transferred employees", + "work.mock.onboard": "New employee onboarding", + "work.mock.Test": "Test coaching", + "work.mock.week1": "Zero promotion practice (1 weeks)", + "work.mock.week2": "Zero promotion practice (2 weeks)", + "work.mock.week3": "Zero promotion practice (3 weeks)", + "work.mock.network": "Network Reality", + "work.mock.centralized": "Centralized training for new employees", + "work.mock.hardware": "Hardware Installation Practice", + "work.index.learn": "Learning Planning", + "work.index.coach": "Learning coaching", + "work.index.formalization": "Learning Formalization", + "work.index.practiced": "Learning practiced", + "work.index.train": "Centralized training", + "work.index.Inquiry": "Life little helper", + "work.index.Home": "New Employee Home", + "work.index.Guide": "Operation Guide", + "work.index.plans": "Number of plans", + "work.index.Unfinished": "Unfinished", + "work.index.beOverdue": "To Be Overdue", + "work.index.Overdue": "Overdue", + "work.index.trainees": "Number of trainees to start coaching", + "work.index.coachNum": "Number of trainees in coaching", + "work.index.allocated": "Number of trainees to be allocated", + "work.index.start": "Number of trainees to start practice", + "work.index.practice": "Number of trainees in practice", + "work.index.unpark": "Waiting for Start-up to Form", + "work.index.entered": "Evaluation result to be entered", + "work.index.approved": "Evaluation result to be approved", + "work.index.put": "Number of trainees in practice", + "work.index.assign": "Number of trainees to be allocated", + "work.index.prepare": "Prepare for class opening", + "work.index.open": "Open a middle class", + "work.index.classes": "Number of classes to be accepted", + "work.index.policy": "policy", + "work.index.Period": "Probation Period and Development Policy Process for New Employees", + "work.index.Hotline": "Hotline", + "work.index.service": "All kinds of practical hotline service", + "work.index.Attendance": "Attendance", + "work.index.FAQs": "Attendance System and FAQs", + "work.index.Payroll": "Payroll", + "work.index.Tax": "Payroll Tax Q&A", + "work.index.Brave": "Brave New World Landing Program", + "work.index.Growth": "100-day Growth Guide for New Employees", + "work.index.Termbase": "Termbase", + "work.index.lingo": "The latest and hottest terms to help you understand the lingo", + "work.index.Library": "Document Library and Community", + "work.index.domain": "Knowledge document library of the business domain", + "work.index.platform": "Online learning platform", + "work.index.learning": "Online learning", + "work.index.Operation": "New Employee Home Operation Guide", + "work.index.Numbers": "Number", + "work.index.Person": "Person", + "work.index.net": "Net", + "work.index.netonline": "Online consultation", + "home.main.one": "first screen", + "home.main.up": "Page Onload", + "home.main.down": "Sampling PV", + "home.main.day": "yesterday", + "home.curve.trend": "Performance Trends", + "home.curve.play": "Visible on the first screen", + "home.curve.page": "Page Onload", + "home.falls.line": "Load Waterfall Flow", + "home.falls.tcp": "TCP Link", + "home.falls.ssl": "SSL Link", + "home.round.title": "Network speed distribution", + "home.round.unknow": "Unknown", + "home.roundtable.index": "index", + "home.roundtable.space": "Network speed", + "home.roundtable.pv": "Sampling PV (percentage)", + "home.roundtable.play": "Visible on the first screen", + "home.roundtable.page": "Page Onload", + "home.region.title": "Geographical distribution", + "menu.cloud.hello": "Hello World", + "menu.cloud.contracts": "Contract Management", + "menu.cloud.create": "Create Contract", + "menu.cloud.edit": "Edit Contract", + "menu.cloud.del": "Delete Contract", + "menu.cloud.name": "Project Name", + "menu.cloud.id": "Contract No", + "menu.cloud.customer": "Customer Name", + "menu.cloud.description": "Description", + "menu.cloud.updatedAt": "Creation Time", + "menu.cloud.editOpa": "Edits", + "menu.cloud.editDel": "Delete", + "menu.cloud.registerErro": "The project name does not meet the verification rules", + "menu.cloud.sure": "OK", + "menu.cloud.cancel": "Cancel", + "menu.cloud.tip": "The value can contain 3 to 255 characters, including Chinese characters, digits, hyphens (-), underscores (_), dots (.), slashes (/), parentheses (:) and colons (:) in Chinese and English formats, and periods (). The value can start with only English, Chinese characters, and digits.", + "menu.cloud.askDel": "Are you sure you want to delete the following", + "menu.cloud.askContracts": "Contract", + "menu.cloud.askInput": "Input", + "menu.cloud.askSure": "confirm", + "menu.cloud.verification": "Verification failed", + "menu.cloud.editpass": "If the verification is successful, the modification is successful", + "menu.cloud.delpass": "Deleted successfully", + "menu.contracts.name": "The contract name is:", + "http.error.TokenExpire": "Login expired, please log in again", + "http.error.UserNotFound": "user does not exist", + "http.error.UserAlreadyExist": "User already exists", + "http.error.InvalidParameter": "Invalid request parameter", + "http.error.InternalError": "Internal error", + "http.error.ErrorPassword": "Account or password error", + "menu.allUser.info": "All User Info", + "userInfo.table.id": "ID", + "userInfo.table.name": "Name", + "userInfo.table.email": "Email", + "userInfo.table.department": "Department", + "userInfo.table.employeeType": "EmployeeType", + "userInfo.table.job": "Job", + "userInfo.table.probation": "Probation", + "userInfo.table.probationStart": "ProbationStart", + "userInfo.table.probationEnd": "ProbationEnd", + "userInfo.table.probationDuration": "ProbationDuration", + "userInfo.table.protocol": "Protocol", + "userInfo.table.protocolStart": "ProtocolStart", + "userInfo.table.protocolEnd": "ProtocolEnd", + "userInfo.table.address": "Address", + "userInfo.table.status": "Status", + "userInfo.table.createTime": "CreateTime", + "userInfo.table.updateTime": "UpdateTime", + "userInfo.table.operations": "Operation", + "userInfo.table.operations.update": "Update", + "userInfo.table.operations.delete": "Delete", + "userInfo.table.operations.pwdUpdate": "Password", + "userInfo.day": "Day", + "userInfo.modal.title.pwdUpdate": "Update Password", + "userInfo.modal.input.oldPassword": "Old Password", + "userInfo.modal.input.newPassword": "New Password", + "userInfo.modal.input.confirmNewPassword": "Confirm New Password", + "userInfo.modal.message.error": "Confirm New Password Error", + "userInfo.modal.message.notNull": "Password Is Not Null", + "menu.allUser.setting": "User Setting", + "userSetting.name": "UserName", + "userSetting.address": "Address", + "userSetting.status": "Status", + "userInfo.modal.title.add": "Add User", + "userInfo.modal.title.update": "Update User", + "menu.allUser.useradd": "User Add", + "userAdd.cancel": "Cancel", + "userAdd.save": "Save", + "userAdd.email": "Email", + "userAdd.password": "Password", + "userAdd.department": "Department:", + "userAdd.position": "Position:", + "userAdd.type": "Recruitment Type:", + "userAdd.date": "Trial Start and End Date:", + "userAdd.during": "Probation Period:", + "userAdd.startTime": "Labor Contract Start Date:", + "userAdd.endTime": "Labor Contract End Date:", + "userAdd.first": "Start Time", + "userAdd.last": "End Time", + "userAdd.name": "UserName", + "userAdd.address": "Address", + "userAdd.status": "Status", + "menu.allPermission.info": "Permission", + "permissionInfo.table.id": "ID", + "permissionInfo.table.name": "Name", + "permissionInfo.table.desc": "Description", + "permissionInfo.table.operations": "Operation", + "permissionInfo.table.operations.update": "Update", + "permissionInfo.table.operations.delete": "Delete", + "permissionInfo.modal.title.update": "Update Permission", + "permissionInfo.modal.title.add": "Add Permission", + "permissionInfo.modal.input.permission": "Description", + "permissionInfo.modal.input.name": "Name", + "permissionInfo.modal.input.id": "ID", + "permissionInfo.modal.message.error": "Error", + "permissionInfo.modal.message.notNull": "Not Null", + "menu.allRole.info": "All Role Info", + "roleInfo.table.id": "ID", + "roleInfo.table.name": "Name", + "roleInfo.table.desc": "Description", + "roleInfo.table.menu": "Menu", + "roleInfo.table.operations": "Operation", + "roleInfo.table.operations.update": "Update", + "roleInfo.table.operations.delete": "Delete", + "roleInfo.modal.title.update": "Update Role", + "roleInfo.modal.title.add": "Add Role", + "roleInfo.modal.input.id": "ID", + "roleInfo.modal.input.name": "Name", + "roleInfo.modal.input.desc": "Permission", + "roleInfo.modal.input.menu": "Menu", + "roleInfo.modal.message.error": "Error", + "roleInfo.modal.message.notNull": "Not Null", + "roleInfo.permissionTable.id": "ID", + "roleInfo.permissionTable.name": "Name", + "roleInfo.permissionTable.desc": "Description", + "roleInfo.menuUpdate.confirm": "Confirm", + "roleInfo.menuUpdate.cancel": "Cancel", + "roleInfo.table.bind": "Bind Directory", + "menu.allMenu.info": "All Menu Info", + "menuInfo.table.id": "ID", + "menuInfo.table.name": "Name", + "menuInfo.table.order": "Order", + "menuInfo.table.parentId": "ParentID", + "menuInfo.table.menuType": "MenuType", + "menuInfo.table.icon": "Icon", + "menuInfo.table.component": "Component", + "menuInfo.table.path": "Path", + "menuInfo.table.locale": "Locale", + "menuInfo.table.operations": "Operation", + "menuInfo.table.operations.info": "Detail", + "menuInfo.table.operations.update": "Update", + "menuInfo.table.operations.delete": "Delete", + "menuInfo.modal.title.info": "Menu Detail", + "menuInfo.modal.title.update": "Update Menu", + "menuInfo.modal.title.add": "Add Menu", + "menuInfo.modal.message.error": "ParentId is not as same as id", + "menuInfo.modal.message.notNull": "Not Null", + "menuInfo.modal.tips.upd-id": "Before modifying the menu ID, please ensure that the front-end engineer is aware of this matter!", + "menu.add.demo": "Menu Demo Page", + "exception.result.demo.description": "This is a new menu demo page!", + "locale.add.btn": "Add Record", + "locale.add.title": "Add Record", + "locale.add.key": "Key", + "locale.add.content": "Content", + "locale.add.lang": "Language", + "lang.add.title": "Name", + "lang.add.btn": "Confirm", + "lang.manage.btn": "Mange Language", + "locale.add.lang.btn": "Add Language", + "lang.manage.title": "Mange Language", + "lang.manage.remove": "Remove", + "locale.remove": "Remove", + "component.error": "Component Error", + "component.error.contact": "Please contact the administrator or log in again" + }, + "zhCN": { + "en-US": "English", + "zh-CN": "中文", + "zh-TW": "中国台湾", + "hello": "你好 {name}", + "code": "zh-CN", + "yes": "是", + "no": "否", + "menu.board": "看板", + "menu.home": "监控页", + "menu.work": "工作台", + "menu.list": "列表页", + "menu.result": "结果页", + "menu.exception": "异常页", + "menu.form": "表单页", + "menu.profile": "详情页", + "menu.profile.detail": "基础详情页", + "menu.visualization": "数据可视化", + "menu.menuPage": "菜单页", + "menu.menuPage.second": "二级菜单", + "menu.menuPage.third": "菜单demo页", + "menu.user": "个人中心", + "menu.systemManager": "系统管理", + "menu.userManager": "用户管理", + "menu.userManager.info": "查看用户", + "menu.userManager.setting": "修改信息", + "menu.userManager.useradd": "添加用户", + "menu.permission": "权限管理", + "menu.permission.info": "查看权限", + "menu.permission.setting": "修改权限", + "menu.permission.permissionAdd": "添加权限", + "menu.role": "角色管理", + "menu.role.info": "查看角色", + "menu.menu": "菜单管理", + "menu.menu.info": "查看菜单", + "navbar.docs": "文档中心", + "navbar.action.locale": "切换为中文", + "messageBox.switchRoles": "切换角色", + "messageBox.userCenter": "用户中心", + "messageBox.userSettings": "用户设置", + "messageBox.logout": "退出登录", + "messageBox.updatePwd": "修改密码", + "message.delete.success": "删除成功", + "menu.cloud": "云服务能力展示", + "menu.btn.confirm": "确认", + "menu.i18n": "国际化管理", + "theme.title.main": "个性化配置", + "theme.title.first": "主题", + "theme.title.default": "默认主题", + "theme.title.honey": "蜜糖主题", + "theme.title.violet": "紫罗兰主题", + "theme.title.deepness": "深邃夜空主题", + "theme.title.deep": "深色主题", + "theme.title.light": "浅色主题", + "theme.title.customization": "自定义主题", + "theme-title-recommend": "推荐主题", + "theme-text-default": "科技、探索、钻研、精尖、包容", + "theme-text-honey": "明快、感性、温暖、积极、活力", + "theme-text-violet": "优雅、浪漫、温柔、神秘、高贵", + "theme-text-deepness": "平稳、中性、空间、力量、坚硬", + "theme-text-dark": "深沉、果断、勇敢、坚韧、向往", + "settings.title": "页面配置", + "settings.themeColor": "主题色", + "settings.content": "内容区域", + "settings.search": "搜索", + "settings.language": "语言", + "settings.navbar": "简约模式", + "settings.menuWidth": "菜单宽度 (px)", + "settings.navbar.alerts": "消息通知", + "settings.navbar.help": "帮助中心", + "settings.menu": "经典模式", + "settings.tabBar": "多页签", + "settings.footer": "时尚模式", + "settings.colorWeek": "主题配置", + "settings.alertContent": "配置之后仅是临时生效,要想真正作用于项目,点击下方的 \"复制配置\" 按钮,将配置替换到 settings.json 中即可。", + "settings.copySettings": "复制配置", + "settings.copySettings.message": "复制成功,请粘贴到 src/settings.json 文件中", + "settings.close": "关闭", + "settings.color.tooltip": "根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)", + "setting.user.set": "用户设置", + "setting.loginout": "登出成功", + "setting.copy": "复制成功", + "setting.input.search": "请输入关键词", + "setting.foot.title": "OpenTiny 出品", + "setting.mode.navbar": "收起页头,页尾", + "setting.mode.menu": "收起菜单", + "setting.mode.footer": "收起页脚", + "login.form.mode": "账号密码登录", + "login.form.mail": "邮箱登录", + "login.form.title": "登录 Tiny Pro", + "login.form.userName.errMsg": "用户名不能为空", + "login.form.password.errMsg": "密码不能为空", + "login.form.mailName.errMsg": "邮箱名不能为空", + "login.form.mailpassword.errMsg": "邮箱密码不能为空", + "login.form.mailpassword2.errMsg": "确认密码不能为空", + "login.form.login.errMsg": "登录出错,轻刷新重试", + "login.form.login.success": "欢迎使用", + "login.form.userName.placeholder": "用户名:admin", + "login.form.password.placeholder": "密码:admin", + "login.form.mailName.placeholder": "邮箱名:123{'@'}example.com", + "login.form.mailpassword.placeholder": "密码:admin", + "login.form.registerMail.placeholder": "注册邮箱:", + "login.form.registerPassword.placeholder": "注册密码:", + "login.form.registerConfirmPassword.placeholder": "确认密码:", + "login.form.rememberPassword": "记住密码", + "login.form.forgetPassword": "忘记密码", + "login.form.registration": "注册账户", + "login.form.login": "登录", + "login.form.register": "注册", + "login.form.registerPass": "校验通过,注册成功", + "login.form.registerError": "校验不通过!", + "login.form.change": "使用已有账户登录", + "login.form.mailInput": "邮箱:", + "login.form.passwordInput": "密码:", + "login.form.passwordConfirm": "确认密码:", + "login.form.checkUsername": "字母、数字、下划线、短线、点号{'@'}字母、数字、短线", + "login.form.checkPassword": "最少八个字符,至少包含一个大写字母,一个小写字母和一个数字", + "login.form.confirmPassword": "密码输入不一致", + "login.banner.slogan1": "开箱即用的高质量模板", + "login.banner.subSlogan1": "丰富的的页面模板,覆盖大多数典型业务场景", + "login.banner.slogan2": "内置了常见问题的解决方案", + "login.banner.subSlogan2": "国际化,路由配置,状态管理应有尽有", + "login.banner.slogan3": "接入可视化增强工具AUX", + "login.banner.subSlogan3": "实现灵活的区块式开发", + "login.icon.language": "语言", + "login.tip.info": "用户名:admin,密码 admin", + "login.tip.mail": "用户名:admin{'@'}no-reply.com,密码 admin", + "login.tip.right": "请输入正确的用户名密码", + "login.main.text": "TinyPro 中后台前端解决方案", + "menu.list.searchTable": "查询表格", + "searchTable.form.number": "集合编号", + "searchTable.form.number.placeholder": "请输入集合编号", + "searchTable.form.name": "集合名称", + "searchTable.form.name.placeholder": "请输入集合名称", + "searchTable.form.contentType": "内容体裁", + "searchTable.form.contentType.img": "图文", + "searchTable.form.contentType.horizontalVideo": "横版短视频", + "searchTable.form.contentType.verticalVideo": "竖版小视频", + "searchTable.form.filterType": "筛选方式", + "searchTable.form.filterType.artificial": "人工筛选", + "searchTable.form.filterType.rules": "规则筛选", + "searchTable.form.createdTime": "创建时间", + "searchTable.form.status": "状态", + "searchTable.form.status.online": "已上线", + "searchTable.form.status.offline": "已下线", + "searchTable.form.status.doing": "进行中", + "searchTable.form.search": "查询", + "searchTable.form.reset": "重置", + "searchTable.form.selectDefault": "全部", + "searchTable.operation.create": "新建", + "searchTable.operation.import": "批量导出", + "searchTable.operation.download": "下载", + "searchTable.form.collapse": "收起", + "searchTable.form.extend": "展开", + "searchTable.form.input": "请输入", + "searchTable.form.create": "创建主题", + "searchTable.columns.number": "工号", + "searchTable.columns.name": "姓名", + "searchTable.columns.department": "部门", + "searchTable.columns.filterType": "部门层级", + "searchTable.columns.count": "内容量", + "searchTable.columns.workname": "工作台名称", + "searchTable.columns.enablement": "赋能项目", + "searchTable.columns.type": "人员类型", + "searchTable.columns.study": "研究所", + "searchTable.columns.role": "角色", + "searchTable.columns.updatesperson": "最后更新人", + "searchTable.columns.createdTime": "创建时间", + "searchTable.columns.status": "状态", + "searchTable.columns.operations": "操作", + "searchTable.columns.operations.view": "查看", + "searchTable.columns.operations.delete": "删除", + "searchTable.collapse.restores": "还原", + "searchTable.collapse.full": "全屏", + "menu.form.step": "分步表单", + "stepForm.button.submit": "创建", + "stepForm.button.cancel": "取消", + "stepForm.button.restore": "重置", + "stepForm.probation.day": "天", + "stepForm.coaching.process": "辅导流程", + "stepForm.start.date": "劳动合同开始日期", + "stepForm.end.date": "劳动合同结束日期", + "stepForm.probation.period": "试用期时长", + "stepForm.probation.start": "试用起止日期", + "stepForm.recruitment.type": "招聘类型", + "stepForm.recruitment.position": "职位", + "stepForm.recruitment.department": "所属部门", + "stepForm.start.coaching": "启动辅导", + "stepForm.immediate.supervisor": "录入主管", + "stepForm.overall.goals": "制定整体目标", + "stepForm.overall.summary": "提交整体总结", + "stepForm.overall.end": "结束", + "stepForm.collapse.base": "辅导基本信息", + "stepForm.collapse.supervisor": "录入主管", + "stepForm.collapse.goals": "制定整体目标", + "stepForm.collapse.summary": "提交整体总结", + "stepForm.coach.position": "培养职位", + "stepForm.coach.culture": "培养部门", + "stepForm.coach.mentor": "导师", + "stepForm.coach.startTime": "实际辅导开始日期", + "stepForm.coach.endTime": "实际辅导结束日期", + "stepForm.dire.supervisor": "导师主管", + "stepForm.dire.remarks": "导师信息备注", + "stepForm.dire.startTime": "开始辅导日期", + "stepForm.dire.endTime": "结束辅导日期", + "stepForm.target.list": "目标列表", + "stepForm.target.sure": "确立目标", + "stepForm.sum.self": "自我总结", + "stepForm.error.target": "右侧至少存在一项", + "stepForm.head.admin": "用户名", + "menu.form.base": "基础表单", + "baseForm.form.label.no": "否", + "baseForm.form.label.yes": "是", + "baseForm.form.label.placeholder": "请选择", + "baseForm.form.label.frequencyone": "按月", + "baseForm.form.label.frequencytwo": "按周", + "baseForm.form.label.frequencythree": "按双周", + "baseForm.form.label.frequencyfour": "按季度", + "baseForm.form.label.personone": "本地员工", + "baseForm.form.label.persontwo": "非雇员", + "baseForm.form.label.personthree": "中方员工", + "baseForm.form.label.projectone": "制造部新员工培训", + "baseForm.form.label.projecttwo": "公司新员工上岗辅导", + "baseForm.form.label.projectthree": "UI自动化测试辅导项目", + "baseForm.form.label.people": "适用人群", + "baseForm.form.label.rank": "职级", + "baseForm.form.label.type": "项目类型", + "baseForm.form.label.business": "业务属性", + "baseForm.form.label.Objectives": "整体目标", + "baseForm.form.label.culture": "培养部门", + "baseForm.form.label.develop": "制定", + "baseForm.form.label.developone": "学员", + "baseForm.form.label.developtwo": "导师", + "baseForm.form.label.effective": "生效条件", + "baseForm.form.label.effectiveone": "无需审批直接生效", + "baseForm.form.label.effectivetwo": "导师审批", + "baseForm.form.label.effectivethree": "直接主管审批", + "baseForm.form.label.effectivefour": "导师,直接主管审批", + "baseForm.form.label.plan": "阶段计划", + "baseForm.form.label.confirm": "是否需制定", + "baseForm.form.label.frequency": "制定频次", + "baseForm.form.label.role": "目标制定角色", + "baseForm.form.label.roleone": "学员", + "baseForm.form.label.roletwo": "导师", + "baseForm.form.label.condition": "目标生效条件", + "baseForm.form.label.conditionone": "无需审批直接生效", + "baseForm.form.label.conditiontwo": "导师审批", + "baseForm.form.label.conditionthree": "直接主管审批", + "baseForm.form.label.conditionfour": "导师,直接主管审批", + "baseForm.form.label.staged": "阶段评价", + "baseForm.form.label.stagedone": "仅需导师评价", + "baseForm.form.label.stagedtwo": "仅需直接主管评价", + "baseForm.form.label.stagedthree": "需导师,直接主管评价", + "baseForm.form.label.wholeconfirm": "是否需制定", + "baseForm.form.label.evaluation": "整体评价", + "baseForm.form.label.evaluationyes": "需要导师,直接主管评价", + "baseForm.form.label.evaluationno": "不需要导师,直接主管评价", + "baseForm.form.label.mentortitle": "导师选择", + "baseForm.form.label.mentortip": "从导师资源池只能选择导师资格有效的导师,如不从导师资源池选择则不对导师做导师基础资质校验。", + "baseForm.form.label.mentor": "是否仅从导师资源池选择导师", + "baseForm.form.label.remindertitle": "辅导沟通记录提醒", + "baseForm.form.label.reminder": "是否需要提醒", + "baseForm.form.submit": "提交", + "baseForm.form.submit.success": "表单提交成功", + "baseForm.form.cancel": "取消", + "baseForm.form.submit.error": "请先完成必填项", + "baseForm.form.record": "版本记录", + "baseForm.form.project": "项目类型", + "baseForm.form.get.error": "获取数据失败", + "menu.result.success": "成功页", + "success.result.title": "提交结果页用于反馈一系列操作任务的处理结果。", + "menu.result.messageSuccess": "辅导流程提交成功!", + "menu.result.messageEnd": "辅导流程已提交结束!", + "menu.btn.submit": "启动新的辅导", + "menu.btn.cancel": "取消", + "menu.line.process": "当前进度", + "menu.result.error": "失败页", + "error.result.title": "提交结果页用于反馈一系列操作任务的处理结果。", + "menu.result.messageError": "辅导流程提交失败", + "error.result.home": "回到首页", + "menu.exception.403": "403", + "exception.result.403.description": "对不起,您没有访问该资源的权限", + "exception.result.403.back": "返回", + "exception.result.permissions.403": "请联系管理员,申请权限。", + "menu.exception.404": "404", + "exception.result.404.description": "抱歉,页面不见了~", + "exception.result.404.retry": "重试", + "exception.result.404.back": "返回", + "exception.result.permissions.404": "请查看网络连接情况,尝试刷新页面", + "menu.exception.500": "500", + "exception.result.500.description": "抱歉,服务器出了点问题~", + "exception.result.500.back": "返回", + "exception.result.permissions.500": "请查看网络连接情况,尝试刷新页面", + "menu.user.info": "用户中心", + "userInfo.tab.one": "我的计划", + "userInfo.tab.two": "我的任务", + "userInfo.filter.sort": "按时间排序", + "userInfo.filter.startTime": "开始日期", + "userInfo.filter.endTime": "结束日期", + "userInfo.end.positiveOrder": "按截止时间正序", + "userInfo.end.reverseOrder": "按截止时间逆序", + "userInfo.start.positiveOrder": "按开始时间正序", + "userInfo.start.reverseOrder": "按开始时间逆序", + "userInfo.btn.search": "查询", + "userInfo.btn.reset": "重置", + "userInfo.status.status": "状态", + "userInfo.status.optionA": "已完成", + "userInfo.status.optionB": "已逾期", + "userInfo.status.optionC": "即将逾期", + "userInfo.status.optionD": "未完成", + "userInfo.type.type": "类型", + "userInfo.type.optionA": "组织安排", + "userInfo.type.optionB": "阶段计划", + "userInfo.type.optionC": "自主学习", + "userInfo.table.columnA": "计划名称", + "userInfo.table.columnB": "完成时间", + "userInfo.table.columnC": "状态", + "userInfo.table.columnD": "类型", + "userInfo.week.1": "入职2周", + "userInfo.month.1": "入职1个月", + "userInfo.month.2": "入职2个月", + "userInfo.month.3": "入职3个月", + "userInfo.month.4": "入职4个月", + "userInfo.month.5": "入职5个月", + "userInfo.month.6": "入职6个月", + "userInfo.month.7": "入职7个月", + "userInfo.month.8": "入职8个月", + "userInfo.month.9": "入职9个月", + "userInfo.month.10": "入职10个月", + "userInfo.month.11": "入职11个月", + "userInfo.month.12": "入职12个月", + "userInfo.month.13": "入职13个月", + "userInfo.month.14": "入职14个月", + "userInfo.month.15": "入职15个月", + "userInfo.month.16": "入职16个月", + "userInfo.month.17": "入职17个月", + "userInfo.time.message": "结束时间小于开始时间", + "userInfo.filter.all": "请完善当前所有筛选条件", + "menu.user.setting": "用户设置", + "userSetting.save": "保存", + "userSetting.cancel": "取消", + "userSetting.department": "所属部门:", + "userSetting.position": "职位:", + "userSetting.type": "招聘类型:", + "userSetting.date": "试用起止日期:", + "userSetting.during": "试用期时长:", + "userSetting.startTime": "劳动合同开始日期:", + "userSetting.endTime": "劳动合同结束日期:", + "userSetting.first": "开始时间", + "userSetting.last": "结束时间", + "menu.plan.department": "培养部门", + "menu.plan.resource": "人力资源管理部", + "menu.plan.job": "职级", + "menu.plan.person": "人员类型", + "menu.plan.attribute": "业务属性", + "menu.plan.develop": "是否需制定", + "menu.plan.yes": "是", + "menu.plan.no": "否", + "menu.plan.role": "制定角色", + "menu.plan.mentor": "导师", + "menu.plan.condition": "生效条件", + "menu.plan.approval": "直接主管审批", + "menu.plan.frequency": "制定频次", + "menu.plan.month": "按月", + "menu.plan.goal": "目标制定角色", + "menu.plan.trainees": "学员", + "menu.plan.teacher": "导师审批", + "menu.plan.phase": "阶段评价", + "menu.plan.evaluation": "需导师、直接主管评价", + "menu.plan.whole": "整体评价", + "menu.plan.pool": "是否仅从导师资源池选择导师", + "menu.plan.time": "更新时间", + "menu.plan.version": "版本号", + "menu.plan.operation": "操作", + "menu.plan.updated": "更新人", + "work.mock.employees": "转岗员工", + "work.mock.onboard": "新员工上岗", + "work.mock.Test": "测试辅导", + "work.mock.week1": "零促实践(1周)", + "work.mock.week2": "零促实践(2周)", + "work.mock.week3": "零促实践(3周)", + "work.mock.network": "网络实践", + "work.mock.centralized": "新员工集中培训", + "work.mock.hardware": "硬装实践", + "work.index.learn": "学习规划", + "work.index.coach": "学习辅导", + "work.index.formalization": "学习转正", + "work.index.practiced": "学习实践", + "work.index.train": "学习集训", + "work.index.Inquiry": "生活小助手", + "work.index.Home": "新员工之家", + "work.index.Guide": "操作指导", + "work.index.plans": "待制定/确认计划数", + "work.index.Unfinished": "未完成", + "work.index.beOverdue": "即将逾期数", + "work.index.Overdue": "已逾期", + "work.index.trainees": "待启动辅导学员数", + "work.index.coachNum": "辅导中学员数", + "work.index.allocated": "待分配学员数", + "work.index.start": "待启动实践学员数", + "work.index.practice": "实践中学员数", + "work.index.unpark": "待启动转正", + "work.index.entered": "待录入评价结果", + "work.index.approved": "待审批评价结果", + "work.index.put": "实践中学员数", + "work.index.assign": "待分配学员数", + "work.index.prepare": "准备开班班级", + "work.index.open": "开班中班班级", + "work.index.classes": "待验收班级数", + "work.index.policy": "政策", + "work.index.Period": "新员工试用期及培养政策流程", + "work.index.Hotline": "热线", + "work.index.service": "各类实用热线服", + "work.index.Attendance": "考勤", + "work.index.FAQs": "考勤制度及常见问题答疑", + "work.index.Payroll": "发薪", + "work.index.Tax": "发薪纳税问答", + "work.index.Brave": "勇敢新世界登陆计划", + "work.index.Growth": "新员工100天成长指南", + "work.index.Termbase": "术语库", + "work.index.lingo": "最新最热术语,助您懂行话", + "work.index.Library": "文档库与社区", + "work.index.domain": "业务领域知识文档库", + "work.index.platform": "在线学习平台", + "work.index.learning": "在线学习", + "work.index.Operation": "新员工之家操作指导", + "work.index.Numbers": "个", + "work.index.Person": "人", + "work.index.net": "网络", + "work.index.netonline": "在线咨询", + "home.main.one": "首屏可见", + "home.main.up": "页面Onload", + "home.main.down": "采样PV", + "home.main.day": "较昨日", + "home.curve.trend": "性能趋势", + "home.curve.play": "首屏可见", + "home.curve.page": "页面Onload", + "home.falls.line": "加载瀑布流", + "home.falls.tcp": "TCP链接", + "home.falls.ssl": "SSL链接", + "home.round.title": "网络速度分布", + "home.round.unknow": "未知", + "home.roundtable.index": "序号", + "home.roundtable.space": "网络速度", + "home.roundtable.pv": "采样PV(占比)", + "home.roundtable.play": "首屏可见", + "home.roundtable.page": "页面Onload", + "home.region.title": "地域分布", + "menu.cloud.hello": "Hello World", + "menu.cloud.contracts": "合同管理", + "menu.cloud.create": "创建合同", + "menu.cloud.edit": "编辑合同", + "menu.cloud.del": "删除合同", + "menu.cloud.name": "项目名称:", + "menu.cloud.id": "合同编号", + "menu.cloud.customer": "客户名称:", + "menu.cloud.description": "项目描述:", + "menu.cloud.updatedAt": "创建时间", + "menu.cloud.editOpa": "编辑", + "menu.cloud.editDel": "删除", + "menu.cloud.registerErro": "项目名称不满足校验规则", + "menu.cloud.sure": "确认", + "menu.cloud.cancel": "取消", + "menu.cloud.tip": "支持汉字、英文、数字、中划线、下划线、点、斜杠、中英文格式下的小括号和冒号、中文格式下的顿号,且只能以英文、汉字和数字开头,3-255个字符。", + "menu.cloud.askDel": "您确定要删除以下", + "menu.cloud.askContracts": "合同", + "menu.cloud.askInput": "输入", + "menu.cloud.askSure": "确认", + "menu.cloud.verification": "校验不通过", + "menu.cloud.editpass": "校验通过, 修改成功", + "menu.cloud.delpass": "删除成功", + "menu.contracts.name": "合同名称为:", + "http.error.TokenExpire": "登录过期,请重新登录", + "http.error.UserNotFound": "用户不存在", + "http.error.UserAlreadyExist": "用户已存在", + "http.error.InvalidParameter": "无效的请求参数", + "http.error.InternalError": "服务器错误", + "http.error.ErrorPassword": "账号或密码错误", + "menu.allUser.info": "查看用户", + "userInfo.table.id": "ID", + "userInfo.table.name": "名称", + "userInfo.table.email": "邮箱", + "userInfo.table.department": "部门", + "userInfo.table.employeeType": "招聘类型", + "userInfo.table.job": "职位", + "userInfo.table.probation": "试用期", + "userInfo.table.probationStart": "试用期开始日期", + "userInfo.table.probationEnd": "试用期结束日期", + "userInfo.table.probationDuration": "试用期时长", + "userInfo.table.protocol": "劳动合同", + "userInfo.table.protocolStart": "劳动合同开始日期", + "userInfo.table.protocolEnd": "劳动合同结束日期", + "userInfo.table.address": "地址", + "userInfo.table.status": "状态", + "userInfo.table.createTime": "创建时间", + "userInfo.table.updateTime": "更新时间", + "userInfo.table.operations": "操作", + "userInfo.table.operations.update": "修改", + "userInfo.table.operations.delete": "删除", + "userInfo.table.operations.pwdUpdate": "密码", + "userInfo.day": "天", + "userInfo.modal.title.pwdUpdate": "修改密码", + "userInfo.modal.input.oldPassword": "旧密码", + "userInfo.modal.input.newPassword": "新密码", + "userInfo.modal.input.confirmNewPassword": "确认新密码", + "userInfo.modal.message.error": "确认新密码错误", + "userInfo.modal.message.notNull": "密码不能为空", + "userInfo.modal.title.add": "添加用户", + "userInfo.modal.title.update": "更新用户", + "menu.allUser.setting": "用户设置", + "userSetting.name": "用户名", + "userSetting.address": "地址", + "userSetting.status": "状态", + "menu.allUser.useradd": "添加用户", + "userAdd.save": "提交", + "userAdd.cancel": "取消", + "userAdd.email": "邮箱", + "userAdd.password": "密码", + "userAdd.department": "所属部门:", + "userAdd.position": "职位:", + "userAdd.type": "招聘类型:", + "userAdd.date": "试用起止日期:", + "userAdd.during": "试用期时长:", + "userAdd.startTime": "劳动合同开始日期:", + "userAdd.endTime": "劳动合同结束日期:", + "userAdd.first": "开始时间", + "userAdd.last": "结束时间", + "userAdd.name": "用户名", + "userAdd.address": "地址", + "userAdd.status": "状态", + "menu.allPermission.info": "查看权限", + "permissionInfo.table.id": "ID", + "permissionInfo.table.name": "名称", + "permissionInfo.table.desc": "权限描述", + "permissionInfo.table.operations": "操作", + "permissionInfo.table.operations.update": "修改", + "permissionInfo.table.operations.delete": "删除", + "permissionInfo.modal.title.update": "修改权限", + "permissionInfo.modal.title.add": "添加权限", + "permissionInfo.modal.input.permission": "权限描述", + "permissionInfo.modal.input.name": "权限名称", + "permissionInfo.modal.input.id": "id", + "permissionInfo.modal.message.error": "错误", + "permissionInfo.modal.message.notNull": "不能为空", + "menu.allRole.info": "查看权限", + "roleInfo.table.id": "ID", + "roleInfo.table.name": "名称", + "roleInfo.table.desc": "权限", + "roleInfo.table.menu": "菜单", + "roleInfo.table.operations": "操作", + "roleInfo.table.operations.update": "修改", + "roleInfo.table.operations.delete": "删除", + "roleInfo.modal.title.update": "修改角色", + "roleInfo.modal.title.add": "添加角色", + "roleInfo.modal.input.id": "ID", + "roleInfo.modal.input.name": "名称", + "roleInfo.modal.input.desc": "权限", + "roleInfo.modal.input.menu": "菜单", + "roleInfo.modal.message.error": "错误", + "roleInfo.modal.message.notNull": "不能为空", + "roleInfo.permissionTable.id": "ID", + "roleInfo.permissionTable.name": "权限名称", + "roleInfo.permissionTable.desc": "权限介绍", + "roleInfo.menuUpdate.confirm": "确认修改", + "roleInfo.menuUpdate.cancel": "取消", + "roleInfo.table.bind": "绑定目录", + "menu.allMenu.info": "查看菜单", + "menuInfo.table.id": "ID", + "menuInfo.table.name": "名称", + "menuInfo.table.order": "优先级", + "menuInfo.table.parentId": "父菜单ID", + "menuInfo.table.menuType": "菜单类型", + "menuInfo.table.icon": "图标", + "menuInfo.table.component": "组件", + "menuInfo.table.path": "路径", + "menuInfo.table.locale": "国际化", + "menuInfo.table.operations": "操作", + "menuInfo.table.operations.info": "查看", + "menuInfo.table.operations.update": "修改", + "menuInfo.table.operations.delete": "删除", + "menuInfo.modal.title.info": "查看菜单", + "menuInfo.modal.title.update": "修改菜单", + "menuInfo.modal.title.add": "添加菜单", + "menuInfo.modal.message.error": "parentId不能和id相同", + "menuInfo.modal.message.notNull": "不能为空", + "menuInfo.modal.tips.upd-id": "修改菜单ID前, 请确保前端工程师知晓此事!", + "menu.add.demo": "菜单Demo页", + "exception.result.demo.description": "这是一个新增的菜单demo页", + "locale.add.btn": "添加词条", + "locale.add.title": "添加词条", + "locale.add.key": "词条Key", + "locale.add.content": "词条内容", + "locale.add.lang": "词条语言", + "lang.add.title": "语言名称", + "lang.add.btn": "确认", + "lang.manage.btn": "管理语言", + "locale.add.lang.btn": "新增语言", + "lang.manage.title": "管理语言", + "lang.manage.remove": "删除", + "locale.remove": "删除", + "component.error": "组件错误", + "component.error.contact": "请联系管理员或重新登录" + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/nest-cli.json b/packages/toolkits/pro/template/server/nestJs/nest-cli.json new file mode 100644 index 00000000..12c7206c --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/nest-cli.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": false, + "webpack": true, + "assets": [ + { "include": "i18n/**/*", "watchAssets": true } + ] + }, + "projects": { + "db": { + "type": "library", + "root": "libs/db", + "entryFile": "index", + "sourceRoot": "libs/db/src", + "compilerOptions": { + "tsConfigPath": "libs/db/tsconfig.lib.json" + } + }, + "models": { + "type": "library", + "root": "libs/models", + "entryFile": "index", + "sourceRoot": "libs/models/src", + "compilerOptions": { + "tsConfigPath": "libs/models/tsconfig.lib.json" + } + } + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/package.json b/packages/toolkits/pro/template/server/nestJs/package.json index 11f79817..d9fda3f4 100644 --- a/packages/toolkits/pro/template/server/nestJs/package.json +++ b/packages/toolkits/pro/template/server/nestJs/package.json @@ -20,15 +20,28 @@ }, "dependencies": { "@nestjs/common": "10.0.3", + "@nestjs/config": "^3.2.3", "@nestjs/core": "10.0.3", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mapped-types": "*", + "@nestjs/microservices": "^10.3.10", "@nestjs/platform-express": "10.0.3", "@nestjs/sequelize": "10.0.0", + "@nestjs/typeorm": "^10.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.5", + "ioredis": "^5.4.1", "mysql2": "3.4.3", + "nestjs-i18n": "^10.4.5", + "nestjs-typeorm-paginate": "^4.0.4", + "redis": "^4.6.15", "reflect-metadata": "0.1.13", "rimraf": "5.0.1", "rxjs": "7.8.1", "sequelize": "6.32.1", "sequelize-typescript": "2.1.5", + "typeorm": "^0.3.20", "typescript": "5.1.6" }, "devDependencies": { @@ -46,6 +59,7 @@ "eslint-plugin-import": "2.27.5", "jest": "29.6.1", "prettier": "2.8.8", + "source-map-support": "^0.5.20", "supertest": "6.3.3", "ts-jest": "29.1.1", "ts-loader": "9.4.4", diff --git a/packages/toolkits/pro/template/server/nestJs/src/.generate/i18n.generated.ts b/packages/toolkits/pro/template/server/nestJs/src/.generate/i18n.generated.ts new file mode 100644 index 00000000..b7afbfe6 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/.generate/i18n.generated.ts @@ -0,0 +1,55 @@ +/* DO NOT EDIT, file generated by nestjs-i18n */ + +/* eslint-disable */ +/* prettier-ignore */ +import { Path } from "nestjs-i18n"; +/* prettier-ignore */ +export type I18nTranslations = { + "exception": { + "common": { + "unauth": string; + "tokenError": string; + "tokenExpire": string; + "forbidden": string; + }; + "user": { + "oldPasswordError": string; + "userExists": string; + "userNumberNull": string; + }; + "role": { + "exists": string; + "notExists": string; + "conflict": string; + }; + "permission": { + "exists": string; + "notExists": string; + }; + "menu": { + "exists": string; + "notExists": string; + }; + "lang": { + "notExists": string; + "exists": string; + "notExistsCommon": string; + "DELETE_LANG_CONFLICT": string; + }; + "i18": { + "exists": string; + "notExists": string; + }; + "auth": { + "userNotExists": string; + "passwordOrEmailError": string; + }; + }; + "validation": { + "NOT_EMPTY": string; + "IS_ARRAY": string; + "NOT_EMPTY_HUMAN": string; + }; +}; +/* prettier-ignore */ +export type I18nPath = Path; diff --git a/packages/toolkits/pro/template/server/nestJs/src/app.module.ts b/packages/toolkits/pro/template/server/nestJs/src/app.module.ts index 3d252121..36979383 100644 --- a/packages/toolkits/pro/template/server/nestJs/src/app.module.ts +++ b/packages/toolkits/pro/template/server/nestJs/src/app.module.ts @@ -1,20 +1,199 @@ -import { Module } from '@nestjs/common'; import { SequelizeModule } from '@nestjs/sequelize'; -import { EmployeesModule } from './employees/employees.module'; +import { + HttpException, + Logger, + LoggerService, + Module, + OnModuleInit, +} from '@nestjs/common'; +import { UserModule } from './user/user.module'; +import { DbModule } from '@app/db'; +import { PermissionModule } from './permission/permission.module'; +import { AuthModule } from './auth/auth.module'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './auth/auth.guard'; +import { PermissionGuard } from './permission/permission.guard'; +import { RoleModule } from './role/role.module'; +import { join } from 'path'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { UserService } from './user/user.service'; +import { RoleService } from './role/role.service'; +import { PermissionService } from './permission/permission.service'; +import { MenuService } from './menu/menu.service'; +import { Permission } from '@app/models'; +import { MenuModule } from './menu/menu.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { menuData } from './menu/init/menuData'; +import { I18Module } from './i18/i18.module'; +import { I18LangService } from './i18/lang.service'; +import { I18Service } from './i18/i18.service'; +import { + AcceptLanguageResolver, + HeaderResolver, + I18nModule, +} from 'nestjs-i18n'; @Module({ imports: [ - SequelizeModule.forRoot({ - dialect: '', - host: '', - port: '', - username: '', - password: '', - database: '', - autoLoadModels: true, - synchronize: true, + DbModule, + UserModule, + PermissionModule, + AuthModule, + RoleModule, + MenuModule, + ConfigModule.forRoot({ + isGlobal: true, }), - EmployeesModule, + I18Module, + I18nModule.forRoot({ + fallbackLanguage: 'enUS', + loaderOptions: { + path: join(__dirname, '/i18n/'), + watch: true, + }, + resolvers: [new HeaderResolver(['x-lang'])], + typesOutputPath: join(__dirname, '../src/.generate/i18n.generated.ts'), + }), + ], + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + { + provide: APP_GUARD, + useClass: PermissionGuard, + }, ], }) -export class AppModule {} +export class AppModule implements OnModuleInit { + constructor( + private user: UserService, + private role: RoleService, + private permission: PermissionService, + private menu: MenuService, + private lang: I18LangService, + private i18: I18Service + ) {} + async onModuleInit() { + const ROOT = __dirname; + const data = join(ROOT, 'data'); + if (!existsSync(data)) { + mkdirSync(data); + } + const LOCK_FILE = join(data, 'lock'); + if (existsSync(LOCK_FILE)) { + Logger.warn( + 'Lock file exists, if you want init agin, please remove dist or dist/lock' + ); + return; + } + const I18_INIT_FILE_PATH = join(process.cwd(), 'locales.json'); + const I18_INIT_FILE = JSON.parse( + readFileSync(I18_INIT_FILE_PATH).toString() + ); + const dbLangNames = (await this.lang.findAll()).map((lang) => lang.name); + const langs = Object.keys(I18_INIT_FILE).filter( + (key) => !dbLangNames.includes(key) + ); + for (const name of langs) { + const { id } = await this.lang.create({ name }); + for (const [key, value] of Object.entries(I18_INIT_FILE[name])) { + const dbValue = await this.i18.has(key, id); + if (dbValue) { + Logger.warn(`${name} - ${key} exists value is ${dbValue.content}`); + continue; + } + Logger.log(`${name} - ${key} not exists`); + await this.i18.create({ key, content: value as string, lang: id }); + Logger.log(`${name} - ${key} save success`); + } + } + const permissions = { + user: ['add', 'remove', 'update', 'query', 'password::force-update'], + permission: ['add', 'remove', 'update', 'get'], + role: ['add', 'remove', 'update', 'query'], + menu: ['add', 'remove', 'update', 'query'], + i18n: ['add', 'remove', 'update', 'query'], + lang: ['add', 'remove', 'update', 'query'], + }; + const tasks = []; + let permission; + const isInit = true; + try { + permission = await this.permission.create( + { + name: '*', + desc: 'super permission', + }, + isInit + ); + } catch (e) { + const err = e as HttpException; + Logger.error(err.message); + Logger.error(`Please clear the database and try again`); + process.exit(-1); + } + for (const [module, actions] of Object.entries(permissions)) { + for (const action of actions) { + tasks.push( + this.permission.create( + { + name: `${module}::${action}`, + desc: '', + }, + isInit + ) + ); + } + } + // TODO Menu + try { + for (const item of menuData) { + await this.menu.createMenu(item, isInit); + } + } catch (e) { + const err = e as HttpException; + Logger.error(err.message); + Logger.error(`Please clear the database and try again`); + process.exit(-1); + } + const status = Promise.allSettled(tasks); + const statusData = await status; + const hasFail = statusData.some((data) => data.status === 'rejected'); + if (hasFail) { + const fail: any[] = statusData.filter( + (data) => data.status === 'rejected' + ); + fail.forEach((data) => { + Logger.error(`${data.reason}`); + }); + Logger.error('Please clear the database and try again'); + process.exit(-1); + } + const menuId = this.menu.getMenuAllId(); + const role = await this.role.create( + { + name: 'admin', + permissionIds: [permission.id], + menuIds: await menuId, + }, + isInit + ); + const user = await this.user.create( + { + email: 'admin@no-reply.com', + password: 'admin', + roleIds: [role.id], + name: 'admin', + status: 1, + }, + isInit + ); + Logger.log(`[APP]: create admin user success`); + Logger.log(`[APP]: email: ${user.email}`); + Logger.log(`[APP]: password: 'admin'`); + Logger.log('Enjoy!'); + writeFileSync(LOCK_FILE, ''); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts new file mode 100644 index 00000000..26b8b048 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts @@ -0,0 +1,24 @@ +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { CreateAuthDto } from './dto/create-auth.dto'; +import { LogoutAuthDto } from './dto/logout-auth.dto'; +import { Public } from '../public/public.decorator'; +import { Permission } from '../public/permission.decorator'; +import { AuthGuard } from './auth.guard'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Public() + @Post('login') + @UseGuards(AuthGuard) + async login(@Body() body: CreateAuthDto) { + return this.authService.login(body); + } + @Post('logout') + @UseGuards(AuthGuard) + async logout(@Body() body: LogoutAuthDto) { + return this.authService.logout(body.token); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts new file mode 100644 index 00000000..441832f2 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts @@ -0,0 +1,76 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; +import { AuthService } from './auth.service'; +import { I18nContext } from 'nestjs-i18n'; +import { I18nTranslations } from '../.generate/i18n.generated'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwt: JwtService, + private readonly reflector: Reflector, + private readonly authService: AuthService + ) {} + async canActivate(ctx: ExecutionContext): Promise { + const i18n = I18nContext.current(); + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + ctx.getHandler(), + ctx.getClass(), + ]); + if (isPublic) { + return true; + } + const req = ctx.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(req); + if (!token) { + throw new HttpException( + i18n.t('exception.common.tokenError', { + lang: I18nContext.current().lang, + }), + HttpStatus.UNAUTHORIZED + ); + } + try { + await this.jwt.verify(token); + const payload = await this.jwt.decode(token); + req['user'] = payload; + const cacheToken = await this.authService.getToken(payload.email); + if (!cacheToken) { + throw new HttpException( + i18n.t('exception.common.tokenExpire', { + lang: I18nContext.current().lang, + }), + HttpStatus.UNAUTHORIZED + ); + } + if (cacheToken !== token) { + throw new HttpException( + i18n.t('exception.common.tokenError', { + lang: I18nContext.current().lang, + }), + HttpStatus.UNAUTHORIZED + ); + } + return true; + } catch (err) { + throw new HttpException( + i18n.t('exception.common.tokenExpire', { + lang: I18nContext.current().lang, + }), + HttpStatus.UNAUTHORIZED + ); + } + } + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts new file mode 100644 index 00000000..16b81c31 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@app/models'; +import { JwtModule } from '@nestjs/jwt'; +import { UserModule } from '../user/user.module'; +import { RedisService } from '../../libs/redis/redis.service'; +import { RedisModule } from '../../libs/redis/redis.module'; + +@Module({ + controllers: [AuthController], + providers: [AuthService, RedisService], + exports: [AuthService], + imports: [ + TypeOrmModule.forFeature([User]), + JwtModule.registerAsync({ + imports: [RedisModule], + useFactory: async () => ({ + secret: process.env.AUTH_SECRET, + global: true, + signOptions: { + expiresIn: process.env.EXPIRES_IN, + }, + }), + global: true, + }), + UserModule, + ], +}) +export class AuthModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts new file mode 100644 index 00000000..2fc5c518 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts @@ -0,0 +1,70 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateAuthDto } from './dto/create-auth.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { encry, User } from '@app/models'; +import { Repository } from 'typeorm'; +import { JwtService } from '@nestjs/jwt'; +import { RedisService } from '../../libs/redis/redis.service'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext, I18nService } from 'nestjs-i18n'; + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private user: Repository, + private jwtService: JwtService, + private readonly redisService: RedisService, + private readonly i18n: I18nService + ) {} + + async getToken(userId: string): Promise { + return this.redisService.getUserToken(`user:${userId}:token`); + } + + async kickOut(email: string) { + await this.redisService.delUserToken(`user:${email}:token`); + } + + async logout(token: string): Promise { + //通过token解析email + const decoded = await this.jwtService.verify(token); + //退出登录后,将token从Redis删除 + await this.redisService.delUserToken(`user:${decoded.email}:token`); + return; + } + + async login(dto: CreateAuthDto) { + const { email, password } = dto; + const userInfo = await this.user.findOne({ where: { email } }); + if (userInfo === null) { + throw new HttpException( + this.i18n.translate('exception.auth.userNotExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + if (encry(password, userInfo.salt) !== userInfo.password) { + throw new HttpException( + this.i18n.translate('exception.auth.passwordOrEmailError', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + const payload = { + email, + }; + const token = this.jwtService.signAsync(payload); + //将token设置到Redis中,有效期2h + await this.redisService.setUserToken( + `user:${email}:token`, + await token, + await parseInt(process.env.REDIS_SECONDS) + ); + return { + token: await token, + }; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts new file mode 100644 index 00000000..bfd059d6 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty } from 'class-validator'; +import { I18nContext, i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreateAuthDto { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + email: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + password: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts new file mode 100644 index 00000000..e8969088 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class LogoutAuthDto { + @IsNotEmpty({ + message: i18nValidationMessage( + 'validation.NOT_EMPTY_HUMAN', + { + name: 'Token', + } + ), + }) + token: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts new file mode 100644 index 00000000..100de4fb --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAuthDto } from './create-auth.dto'; + +export class UpdateAuthDto extends PartialType(CreateAuthDto) {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/global.d.ts b/packages/toolkits/pro/template/server/nestJs/src/global.d.ts new file mode 100644 index 00000000..c738660b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/global.d.ts @@ -0,0 +1,5 @@ +declare type RequestUser = { + user?: { + email: string; + }; +}; diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-i18.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-i18.dto.ts new file mode 100644 index 00000000..389f2771 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-i18.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreateI18Dto { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + lang: number; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + key: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + content: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-lang.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-lang.dto.ts new file mode 100644 index 00000000..100b557d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/create-lang.dto.ts @@ -0,0 +1,10 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreateLang { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/dto/update-i18.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/update-i18.dto.ts new file mode 100644 index 00000000..0a3115dc --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/dto/update-i18.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateI18Dto } from './create-i18.dto'; +import { IsNotEmpty, IsNumber, IsPositive } from 'class-validator'; + +export class UpdateI18Dto extends PartialType(CreateI18Dto) {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.spec.ts new file mode 100644 index 00000000..3c0e2349 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { I18Controller } from './i18.controller'; +import { I18Service } from './i18.service'; + +describe('I18Controller', () => { + let controller: I18Controller; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [I18Controller], + providers: [I18Service], + }).compile(); + + controller = module.get(I18Controller); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.ts new file mode 100644 index 00000000..1d603262 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.controller.ts @@ -0,0 +1,75 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + DefaultValuePipe, + ParseArrayPipe, +} from '@nestjs/common'; +import { I18Service } from './i18.service'; +import { CreateI18Dto } from './dto/create-i18.dto'; +import { UpdateI18Dto } from './dto/update-i18.dto'; +import { I18LangService } from './lang.service'; +import { Permission } from '../public/permission.decorator'; + +@Controller('i18') +export class I18Controller { + constructor(private readonly i18Service: I18Service) {} + + @Permission('i18n::add') + @Post() + create(@Body() createI18Dto: CreateI18Dto) { + return this.i18Service.create(createI18Dto); + } + + @Get('format') + getFormat(@Query('lang') lang: string) { + return this.i18Service.getFormat(lang); + } + + @Permission('i18n::query') + @Get() + findAll( + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number, + @Query('limit', new DefaultValuePipe(0), ParseIntPipe) limit?: number, + @Query('all', ParseIntPipe) all?: number, + @Query('lang', new DefaultValuePipe([]), ParseArrayPipe) lang?: number[], + @Query('key') key?: string, + @Query('content') content?: string + ) { + return this.i18Service.findAll( + page, + limit, + Boolean(all), + lang, + content, + key + ); + } + + @Permission('i18n::query') + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.i18Service.findOne(id); + } + + @Permission('i18n::update') + @Patch(':id') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateI18Dto: UpdateI18Dto + ) { + return this.i18Service.update(id, updateI18Dto); + } + + @Permission('i18n::remove') + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.i18Service.remove(id); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/i18.module.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.module.ts new file mode 100644 index 00000000..d17cc445 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { I18Service } from './i18.service'; +import { I18Controller } from './i18.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { I18, Lang } from '@app/models'; +import { I18LangService } from './lang.service'; +import { I18nLangController } from './lang.controller'; + +@Module({ + controllers: [I18Controller, I18nLangController], + providers: [I18Service, I18LangService], + imports: [TypeOrmModule.forFeature([Lang, I18])], + exports: [I18Service, I18LangService], +}) +export class I18Module {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.spec.ts new file mode 100644 index 00000000..84dab62d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { I18Service } from './i18.service'; + +describe('I18Service', () => { + let service: I18Service; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [I18Service], + }).compile(); + + service = module.get(I18Service); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.ts new file mode 100644 index 00000000..4fec10fe --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/i18.service.ts @@ -0,0 +1,177 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateI18Dto } from './dto/create-i18.dto'; +import { UpdateI18Dto } from './dto/update-i18.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { I18, Lang } from '@app/models'; +import { In, Like, Repository } from 'typeorm'; +import { paginate } from 'nestjs-typeorm-paginate'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext, I18nService } from 'nestjs-i18n'; + +@Injectable() +export class I18Service { + private readonly COUNT_CACHE = 'i18::count::cache'; + constructor( + @InjectRepository(I18) private readonly i18: Repository, + @InjectRepository(Lang) private readonly lang: Repository, + private readonly i18n: I18nService + ) {} + + async getFormat(lang: string) { + const data = await this.lang.find({ + where: { + name: lang === '' ? undefined : lang, + }, + relations: ['i18'], + }); + const ret: Record> = {}; + for (let i = 0; i < data.length; i++) { + const { name, i18 } = data[i]; + ret[name] = {}; + for (let i = 0; i < i18.length; i++) { + const i18Item = i18[i]; + ret[name][i18Item.key] = i18Item.content; + } + } + return ret; + } + + async create(createI18Dto: CreateI18Dto) { + const { key, content, lang } = createI18Dto; + const i18 = this.i18.create(); + const langRecord = await this.lang.findOne({ + where: { + id: lang, + }, + }); + if (!langRecord) { + throw new HttpException( + this.i18n.t('exception.lang.notExists', { + args: { lang }, + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + const i18Item = await this.i18.findOne({ + where: { + key, + lang: langRecord, + }, + }); + if (i18Item) { + throw new HttpException( + this.i18n.t('exception.i18.exists', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + i18.content = content; + i18.key = key; + i18.lang = langRecord; + const items = await this.i18.save(i18); + return items; + } + async has(key: string, langId: number) { + return this.i18.findOne({ + where: { + key, + lang: { + id: langId, + }, + }, + }); + } + async findAll( + page?: number, + limit?: number, + all?: boolean, + lang?: number[], + content?: string, + key?: string + ) { + let count = 0; + if (all) { + count = await this.i18.count(); + } + const where = { + lang: lang && lang.length ? { id: In(lang) } : undefined, + content: content ? Like(content) : undefined, + key: key ? Like(key) : undefined, + }; + if (page && limit) { + return paginate( + this.i18, + { + limit, + page, + }, + { + relations: ['lang'], + loadEagerRelations: true, + where, + } + ); + } else { + return paginate( + this.i18, + { + limit: all ? count || process.env.PAGITION_LIMIT : limit, + page: Number.isNaN(page) ? process.env.PAGITION_PAGE : page, + }, + { + relations: ['lang'], + loadEagerRelations: true, + where, + } + ); + } + } + + async findOne(id: number) { + const [item] = await this.i18.find({ + where: { + id, + }, + loadEagerRelations: true, + relations: ['lang'], + }); + if (!item) { + throw new HttpException( + this.i18n.t('exception.i18.notExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + return item; + } + + async update(id: number, updateI18Dto: UpdateI18Dto) { + const item = await this.findOne(id); + item.content = updateI18Dto.content; + item.key = updateI18Dto.key; + const lang = await this.lang.findOne({ + where: { + id: updateI18Dto.lang, + }, + }); + if (!lang) { + throw new HttpException( + this.i18n.t('exception.lang.notExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + item.lang = lang; + return await this.i18.save(item); + } + + async remove(id: number) { + const item = await this.findOne(id); + await this.i18.remove(item); + return item; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/lang.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/lang.controller.ts new file mode 100644 index 00000000..0ca6c1e1 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/lang.controller.ts @@ -0,0 +1,45 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; +import { I18LangService } from './lang.service'; +import { CreateLang } from './dto/create-lang.dto'; +import { Permission } from '../public/permission.decorator'; + +@Controller('/lang') +export class I18nLangController { + constructor(private readonly langService: I18LangService) {} + + @Permission('lang::add') + @Post('') + createLang(@Body() data: CreateLang) { + return this.langService.create(data); + } + + @Permission('lang::query') + @Get('') + findAllLang() { + return this.langService.findAll(); + } + + @Permission('lang::update') + @Patch(':id') + updateLang( + @Param('id', ParseIntPipe) id: number, + @Body() data: Partial + ) { + return this.langService.update(id, data); + } + + @Permission('lang::remove') + @Delete(':id') + removeLang(@Param('id', ParseIntPipe) id: number) { + return this.langService.remove(id); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18/lang.service.ts b/packages/toolkits/pro/template/server/nestJs/src/i18/lang.service.ts new file mode 100644 index 00000000..7ad9df6c --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18/lang.service.ts @@ -0,0 +1,76 @@ +import { I18, Lang } from '@app/models'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateLang } from './dto/create-lang.dto'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +import { I18nTranslations } from '../.generate/i18n.generated'; + +@Injectable() +export class I18LangService { + constructor( + @InjectRepository(I18) private readonly i18: Repository, + @InjectRepository(Lang) private readonly lang: Repository, + private readonly i18n: I18nService + ) {} + findAll() { + return this.lang.find(); + } + async create({ name }: CreateLang) { + const item = await this.lang.findOneBy({ name }); + if (item) { + throw new HttpException( + this.i18n.t('exception.lang.exists', { + lang: I18nContext.current().lang, + args: { + name, + }, + }), + HttpStatus.CONFLICT + ); + } + const lang = this.lang.create(); + lang.name = name; + return this.lang.save(lang); + } + findOne(id: number) { + return this.lang.findOneBy({ id }); + } + async update(id: number, data: Partial) { + const item = await this.findOne(id); + if (!item) { + throw new HttpException( + this.i18n.t('exception.lang.notExistsCommon', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + item.name = data.name; + return await this.lang.save(item); + } + async remove(id: number) { + const item = await this.findOne(id); + if (!item) { + throw new HttpException( + this.i18n.t('exception.lang.notExistsCommon', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + const i18Record = await this.i18.findOneBy({ lang: item }); + if (i18Record) { + throw new HttpException( + this.i18n.t('exception.lang.DELETE_LANG_CONFLICT', { + lang: I18nContext.current().lang, + args: { + name: item.name, + }, + }), + HttpStatus.CONFLICT + ); + } + return await this.lang.remove(item); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/exception.json b/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/exception.json new file mode 100644 index 00000000..59dd54ca --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/exception.json @@ -0,0 +1,40 @@ +{ + "common":{ + "unauth": "UnAuth", + "tokenError": "Token invalid", + "tokenExpire": "Token expire", + "forbidden": "Need `{permission}` permission, but you don't have it" + }, + "user":{ + "oldPasswordError": "Old password error", + "userExists": "User exists", + "userNumberNull": "Currently the last user, deletion is prohibited" + }, + "role": { + "exists": "Role exists", + "notExists": "Not found role", + "conflict": "There are users under the role, please clear them" + }, + "permission":{ + "exists": "Permission {name} exists", + "notExists": "Not found permission" + }, + "menu":{ + "exists": "Menu {name} exists", + "notExists": "Not found menu" + }, + "lang": { + "notExists": "{lang} not found", + "exists": "{name} exists", + "notExistsCommon": "Not found", + "DELETE_LANG_CONFLICT": "Please clear all language records under {name} first" + }, + "i18":{ + "exists": "I18n field exists", + "notExists": "Not Found" + }, + "auth":{ + "userNotExists": "User not found", + "passwordOrEmailError": "Password or email invalid" + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/validation.json b/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/validation.json new file mode 100644 index 00000000..8ce72583 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18n/enUS/validation.json @@ -0,0 +1,5 @@ +{ + "NOT_EMPTY": "{property} should not to be empty", + "IS_ARRAY": "{property} should to be array", + "NOT_EMPTY_HUMAN": "{name} should not to be empty" +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/exception.json b/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/exception.json new file mode 100644 index 00000000..2a724a0e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/exception.json @@ -0,0 +1,40 @@ +{ + "common":{ + "unauth": "未登录", + "tokenError": "异常token", + "tokenExpire": "token过期", + "forbidden": "需要`{permission}`权限, 但是您并没有该权限" + }, + "user":{ + "oldPasswordError": "旧密码错误", + "userExists": "用户存在", + "userNumberNull": "当前为最后一个用户,禁止删除" + }, + "role": { + "exists": "角色已存在", + "notExists": "角色不存在", + "conflict": "角色下存在用户, 请清空用户" + }, + "permission":{ + "exists": "权限字段 {name} 已经存在", + "notExists": "无法找到权限字段" + }, + "menu":{ + "exists": "菜单字段 {name} 已经存在", + "notExists": "无法找到权限字段" + }, + "lang": { + "notExists": "{lang} 不存在", + "exists": "{name} 存在", + "notExistsCommon": "语言不存在", + "DELETE_LANG_CONFLICT": "请先清空{name}下的所有国际化词条" + }, + "i18":{ + "exists": "国际化词条存在", + "notExists": "字段不存在" + }, + "auth":{ + "userNotExists": "用户不存在", + "passwordOrEmailError": "密码或邮箱错误" + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/validation.json b/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/validation.json new file mode 100644 index 00000000..4f2f9889 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/i18n/zhCN/validation.json @@ -0,0 +1,5 @@ +{ + "NOT_EMPTY": "{property} 不能为空", + "IS_ARRAY": "{property} 应该为数组", + "NOT_EMPTY_HUMAN": "{name} 不能为空" +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/main.ts b/packages/toolkits/pro/template/server/nestJs/src/main.ts index e1e10d96..b7a1992c 100644 --- a/packages/toolkits/pro/template/server/nestJs/src/main.ts +++ b/packages/toolkits/pro/template/server/nestJs/src/main.ts @@ -1,8 +1,24 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import * as dotenv from 'dotenv'; +import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; + +dotenv.config({ path: '.env' }); async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new I18nValidationPipe()); + app.useGlobalFilters( + new I18nValidationExceptionFilter({ + errorFormatter: (errors) => { + const reason: string[] = []; + for (const err of errors) { + reason.push(...Object.values(err.constraints)); + } + return reason; + }, + }) + ); await app.listen(3000); console.log(`Application is running on: ${await app.getUrl()}`); } diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts new file mode 100644 index 00000000..0008e9cb --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts @@ -0,0 +1,207 @@ +import { convertToTree } from '../menu.service'; + +const cretaeMenuItem = ( + id: number, + name: string, + parentId: number | null, + order = 0 +) => { + return { + id, + name, + parentId, + order, + menuType: '', + icon: '', + component: '', + path: '', + }; +}; + +describe('convertToTree', () => { + it('mutil root', () => { + const menus = [ + cretaeMenuItem(1, '0', null, 0), + cretaeMenuItem(2, '1', null, 1), + cretaeMenuItem(3, '2', null, 2), + cretaeMenuItem(4, '3', null, 3), + cretaeMenuItem(5, '4', null, 5), + + cretaeMenuItem(6, '1-1', 1, 6), + cretaeMenuItem(7, '1-2', 2, 7), + cretaeMenuItem(8, '1-3', 1, 8), + cretaeMenuItem(9, '1-4', 1, 9), + cretaeMenuItem(10, '1-5', 1, 10), + + cretaeMenuItem(11, '2-1', 2, 11), + cretaeMenuItem(12, '2-2', 2, 12), + cretaeMenuItem(13, '2-3', 2, 13), + cretaeMenuItem(14, '2-4', 2, 14), + cretaeMenuItem(15, '2-5', 2, 15), + + cretaeMenuItem(16, '1-1-1', 6, 16), + cretaeMenuItem(17, '1-1-2', 6, 17), + cretaeMenuItem(18, '1-1-3', 6, 18), + cretaeMenuItem(19, '1-1-4', 6, 19), + cretaeMenuItem(20, '1-1-5', 6, 20), + ]; + const data = convertToTree(menus); + expect(data).toStrictEqual([ + { + label: '0', + id: 1, + children: [ + { + label: '1-1', + id: 6, + children: [ + { + label: '1-1-1', + id: 16, + children: [], + url: '', + }, + { + label: '1-1-2', + id: 17, + children: [], + url: '', + }, + { + label: '1-1-3', + id: 18, + children: [], + url: '', + }, + { + label: '1-1-4', + id: 19, + children: [], + url: '', + }, + { + label: '1-1-5', + id: 20, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '1-3', + id: 8, + children: [], + url: '', + }, + { + label: '1-4', + id: 9, + children: [], + url: '', + }, + { + label: '1-5', + id: 10, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '1', + id: 2, + children: [ + { + label: '1-2', + id: 7, + children: [], + url: '', + }, + { + label: '2-1', + id: 11, + children: [], + url: '', + }, + { + label: '2-2', + id: 12, + children: [], + url: '', + }, + { + label: '2-3', + id: 13, + children: [], + url: '', + }, + { + label: '2-4', + id: 14, + children: [], + url: '', + }, + { + label: '2-5', + id: 15, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '2', + id: 3, + children: [], + url: '', + }, + { + label: '3', + id: 4, + children: [], + url: '', + }, + { + label: '4', + id: 5, + children: [], + url: '', + }, + ]); + }); + it('empty', () => { + expect(convertToTree([])).toStrictEqual([]); + }); + it('not root', () => { + const menus = [ + cretaeMenuItem(1, '0', 0, 0), + cretaeMenuItem(2, '1', 0, 1), + cretaeMenuItem(3, '2', 0, 2), + cretaeMenuItem(4, '3', 0, 3), + cretaeMenuItem(5, '4', 0, 5), + + cretaeMenuItem(6, '1-1', 1, 6), + cretaeMenuItem(7, '1-2', 2, 7), + cretaeMenuItem(8, '1-3', 1, 8), + cretaeMenuItem(9, '1-4', 1, 9), + cretaeMenuItem(10, '1-5', 1, 10), + + cretaeMenuItem(11, '2-1', 2, 11), + cretaeMenuItem(12, '2-2', 2, 12), + cretaeMenuItem(13, '2-3', 2, 13), + cretaeMenuItem(14, '2-4', 2, 14), + cretaeMenuItem(15, '2-5', 2, 15), + + cretaeMenuItem(16, '1-1-1', 6, 16), + cretaeMenuItem(17, '1-1-2', 6, 17), + cretaeMenuItem(18, '1-1-3', 6, 18), + cretaeMenuItem(19, '1-1-4', 6, 19), + cretaeMenuItem(20, '1-1-5', 6, 20), + ]; + const data = convertToTree(menus); + console.log(data); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts new file mode 100644 index 00000000..1e42a353 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts @@ -0,0 +1,36 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreateMenuDto { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + order: number; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + menuType: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + name: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + path: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + component: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + icon: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + locale: string; + + parentId: number | null; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts new file mode 100644 index 00000000..fb683f53 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts @@ -0,0 +1,12 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateMenuDto } from './create-menu.dto'; +import { IsNotEmpty } from 'class-validator'; +import { I18nTranslations } from '../../.generate/i18n.generated'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export class UpdateMenuDto extends PartialType(CreateMenuDto) { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/init/menuData.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/init/menuData.ts new file mode 100644 index 00000000..59d9936e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/init/menuData.ts @@ -0,0 +1,312 @@ +export const menuData = [ + { + name: 'Board', + order: 1, + parentId: null, + menuType: 'normal', + icon: 'IconApplication', + component: 'board/index', + path: 'board', + locale: 'menu.board', + }, + { + name: 'Home', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'board/home/index', + path: 'home', + locale: 'menu.home', + }, + { + name: 'Work', + order: 2, + parentId: null, + menuType: 'normal', + icon: '', + component: 'board/work/index', + path: 'work', + locale: 'menu.work', + }, + { + name: 'List', + order: 2, + parentId: null, + menuType: 'normal', + icon: 'IconFiles', + component: 'list/index', + path: 'list', + locale: 'menu.list', + }, + { + name: 'Table', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'list/search-table/index', + path: 'table', + locale: 'menu.list.searchTable', + }, + { + name: 'Form', + order: 3, + parentId: null, + menuType: 'normal', + icon: 'IconSetting', + component: 'form/index', + path: 'form', + locale: 'menu.form', + }, + { + name: 'Base', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'form/base/index', + path: 'base', + locale: 'menu.form.base', + }, + { + name: 'Step', + order: 2, + parentId: null, + menuType: 'normal', + icon: '', + component: 'form/step/index', + path: 'step', + locale: 'menu.form.step', + }, + { + name: 'Profile', + order: 4, + parentId: null, + menuType: 'normal', + icon: 'IconFiletext', + component: 'profile/index', + path: 'profile', + locale: 'menu.profile', + }, + { + name: 'Detail', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'profile/detail/index', + path: 'detail', + locale: 'menu.profile.detail', + }, + { + name: 'Result', + order: 5, + parentId: null, + menuType: 'normal', + icon: 'IconSuccessful', + component: 'result/index', + path: 'result', + locale: 'menu.result', + }, + { + name: 'Success', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'result/success/index', + path: 'success', + locale: 'menu.result.success', + }, + { + name: 'Error', + order: 2, + parentId: null, + menuType: 'normal', + icon: '', + component: 'result/error/index', + path: 'error', + locale: 'menu.result.error', + }, + { + name: 'Exception', + order: 6, + parentId: null, + menuType: 'normal', + icon: 'IconCueL', + component: 'exception/index', + path: 'exception', + locale: 'menu.exception', + }, + { + name: '403', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'exception/403/index', + path: '403', + locale: 'menu.exception.403', + }, + { + name: '404', + order: 2, + parentId: null, + menuType: 'normal', + icon: '', + component: 'exception/404/index', + path: '404', + locale: 'menu.exception.404', + }, + { + name: '500', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'exception/500/index', + path: '500', + locale: 'menu.exception.500', + }, + { + name: 'User', + order: 7, + parentId: null, + menuType: 'normal', + icon: 'IconUser', + component: 'user/index', + path: 'user', + locale: 'menu.user', + }, + { + name: 'Info', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'user/info/index', + path: 'info', + locale: 'menu.user.info', + }, + { + name: 'Cloud', + order: 8, + parentId: null, + menuType: 'normal', + icon: 'IconDownloadCloud', + component: 'cloud/index', + path: 'cloud', + locale: 'menu.cloud', + }, + { + name: 'Hello', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'cloud/hello/index', + path: 'hello', + locale: 'menu.cloud.hello', + }, + { + name: 'Contracts', + order: 2, + parentId: null, + menuType: 'normal', + icon: '', + component: 'cloud/contracts/index', + path: 'contracts', + locale: 'menu.cloud.contracts', + }, + { + name: 'MenuPage', + order: 9, + parentId: null, + menuType: 'normal', + icon: 'IconApp', + component: 'menu/index', + path: 'menuPage', + locale: 'menu.menuPage', + }, + { + name: 'SecondMenu', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'menu/index', + path: 'secondMenu', + locale: 'menu.menuPage.second', + }, + { + name: 'ThirdMenu', + order: 1, + parentId: null, + menuType: 'normal', + icon: '', + component: 'menu/demo/index', + path: 'thirdMenu', + locale: 'menu.menuPage.third', + }, + { + name: 'SystemManager', + order: 10, + parentId: null, + menuType: 'normal', + icon: 'IconTotal', + component: 'menu/index', + path: '', + locale: 'menu.systemManager', + }, + { + name: 'AllMenu', + order: 1, + parentId: null, + menuType: 'admin', + icon: 'IconGrade', + component: 'menu/info/index', + path: 'menu/allMenu', + locale: 'menu.menu.info', + }, + { + name: 'AllPermission', + order: 1, + parentId: null, + menuType: 'admin', + icon: 'IconFolderOpened', + component: 'permission/info/index', + path: 'permission/allPermission', + locale: 'menu.permission.info', + }, + { + name: 'AllRole', + order: 1, + parentId: null, + menuType: 'admin', + icon: 'IconActivation', + component: 'role/info/index', + path: 'role/allRole', + locale: 'menu.role.info', + }, + { + name: 'AllInfo', + order: 1, + parentId: null, + menuType: 'admin', + icon: 'IconGroup', + component: 'userManager/info/index', + path: 'userManager/allInfo', + locale: 'menu.userManager.info', + }, + { + name: 'Local', + order: 14, + parentId: null, + menuType: '', + icon: 'IconFlag', + component: 'locale/index', + path: 'locale', + locale: 'menu.i18n', + }, +]; diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts new file mode 100644 index 00000000..0295b995 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts @@ -0,0 +1,47 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Query, + Delete, Param, +} from '@nestjs/common'; +import { MenuService } from './menu.service'; +import { CreateMenuDto } from './dto/create-menu.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdateMenuDto } from './dto/update-menu.dto'; + +@Controller('menu') +export class MenuController { + constructor(private readonly menuService: MenuService) {} + + @Get('/role/:email') + async getMenus(@Param('email') email: string) { + return this.menuService.findRoleMenu(email); + } + + @Get() + @Permission('menu::query') + async getAllMenus() { + return this.menuService.findAllMenu(); + } + + @Post() + @Permission('menu::add') + async createMenu(@Body() dto: CreateMenuDto) { + return this.menuService.createMenu(dto, false); + } + + @Patch() + @Permission('menu::update') + async updateMenu(@Body() dto: UpdateMenuDto) { + return this.menuService.updateMenu(dto); + } + + @Delete() + @Permission('menu::remove') + async deleteMenu(@Query('id') id: number, @Query('parentId') parentId: number) { + return this.menuService.deleteMenu(id, parentId); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts new file mode 100644 index 00000000..721d71bd --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { MenuService } from './menu.service'; +import { MenuController } from './menu.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Menu, Role, User } from '@app/models'; + +@Module({ + imports: [TypeOrmModule.forFeature([Menu, User, Role])], + controllers: [MenuController], + providers: [MenuService], + exports: [MenuService], +}) +export class MenuModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts new file mode 100644 index 00000000..d83c0ca7 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts @@ -0,0 +1,211 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Menu, User } from '@app/models'; +import { Repository } from 'typeorm'; +import { CreateMenuDto } from './dto/create-menu.dto'; +import { UpdateMenuDto } from './dto/update-menu.dto'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext, I18nService } from 'nestjs-i18n'; + +export interface ITreeNodeData { + // node-key='id' 设置节点的唯一标识 + id: number | string; + // 节点显示文本 + label: string; + // 子节点 + children?: ITreeNodeData[]; + // 链接 + url: string; + //组件 + component: string; + //图标 + customIcon: string; + //类型 + menuType: string; + //父节点 + parentId: number; + //排序 + order: number; + //国际化 + locale: string; +} + +interface MenuMap { + [key: number]: Menu; +} + +type NumberArray = number[]; +const toNode = (menu: Menu): ITreeNodeData => { + return { + label: menu.name, + id: menu.id, + children: [], + url: menu.path, + component: menu.component, + customIcon: menu.icon, + menuType: menu.menuType, + parentId: menu.parentId, + order: menu.order, + locale: menu.locale, + }; +}; + +export const convertToTree = ( + menus: Menu[], + parentId: number | null = null +) => { + const tree: ITreeNodeData[] = []; + for (let i = 0; i < menus.length; i++) { + const menu = menus[i]; + if (menu.parentId === parentId) { + const children = convertToTree(menus, menu.id); + const node = toNode(menu); + node.children = children; + tree.push(node); + } + } + return tree; +}; + +@Injectable() +export class MenuService { + private menuId: number[] = []; + constructor( + @InjectRepository(User) + private user: Repository, + @InjectRepository(Menu) + private menu: Repository, + private readonly i18n: I18nService + ) {} + async findRoleMenu(email: string) { + const userInfo = await this.user + .createQueryBuilder('user') + .leftJoinAndSelect('user.role', 'role') + .leftJoinAndSelect('role.menus', 'menus') + .where({ + email: email, + }) + .orderBy('menus.order', 'ASC') + .getOne(); + const menus = userInfo.role.flatMap((role) => role.menus); + const maps: MenuMap = {}; + menus.forEach((menu) => { + maps[menu.id] = menu; + }); + return convertToTree(menus); + } + + async findAllMenu() { + const menu = this.menu.find(); + return convertToTree(await menu); + } + + async getMenuAllId() { + const menu = await this.menu.find(); + for (const item of menu) { + this.menuId.push(item.id); + } + await this.handleMenuParentId(this.menuId); + return this.menuId; + } + + async handleMenuParentId(menuId: number[]) { + const menu = await this.menu.find(); + if (menu) { + menu[1].parentId = menuId[0]; + menu[2].parentId = menuId[0]; + menu[4].parentId = menuId[3]; + menu[6].parentId = menuId[5]; + menu[7].parentId = menuId[5]; + menu[9].parentId = menuId[8]; + menu[11].parentId = menuId[10]; + menu[12].parentId = menuId[10]; + menu[14].parentId = menuId[13]; + menu[15].parentId = menuId[13]; + menu[16].parentId = menuId[13]; + menu[18].parentId = menuId[17]; + menu[20].parentId = menuId[19]; + menu[21].parentId = menuId[19]; + menu[23].parentId = menuId[22]; + menu[24].parentId = menuId[23]; + menu[26].parentId = menuId[25]; + menu[27].parentId = menuId[25]; + menu[28].parentId = menuId[25]; + menu[29].parentId = menuId[25]; + menu[30].parentId = menuId[25]; + } + for (const item of menu) { + await this.menu.update(item.id, { parentId: item.parentId }); + } + } + + async createMenu(dto: CreateMenuDto, isInit: boolean) { + const { + order, + menuType, + name, + path, + component, + icon, + parentId = null, + locale, + } = dto; + const menuInfo = this.menu.findOne({ + where: { name, order, menuType, parentId, path, icon, component, locale }, + }); + if (isInit == true && (await menuInfo)) { + return menuInfo; + } + if ((await menuInfo) && isInit == false) { + throw new HttpException( + this.i18n.t('exception.menu.exists', { + args: { name }, + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + return this.menu.save({ + name, + path, + component, + parentId, + menuType, + icon, + order, + locale, + }); + } + async updateMenu(newData: UpdateMenuDto) { + await this.menu.update(newData.id, { + name: newData.name, + path: newData.path, + component: newData.component, + parentId: newData.parentId, + menuType: newData.menuType, + icon: newData.icon, + order: newData.order, + locale: newData.locale, + }); + return true; + } + async deleteMenu(id: number, parentId: number) { + const menu = this.menu.findOne({ + where: { + id: id, + }, + }); + const allMenu = await this.menu.find(); + for (const tmp of allMenu) { + if (Number(tmp.parentId) === Number(id)) { + if (Number(parentId) === -1) { + tmp.parentId = null; + } else { + tmp.parentId = parentId; + } + await this.updateMenu(tmp); + } + } + return this.menu.remove(await menu); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts new file mode 100644 index 00000000..bb70b900 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreatePermissionDto { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + name: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + desc: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts new file mode 100644 index 00000000..3af81aaa --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts @@ -0,0 +1,12 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreatePermissionDto } from './create-permission.dto'; +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class UpdatePermissionDto extends PartialType(CreatePermissionDto) { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts new file mode 100644 index 00000000..4f8b386a --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts @@ -0,0 +1,49 @@ +import { + Body, + Controller, + DefaultValuePipe, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { PermissionService } from './permission.service'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; + +@Controller('permission') +export class PermissionController { + constructor(private readonly permissionService: PermissionService) {} + + @Permission('permission::add') + @Post() + create(@Body() dto: CreatePermissionDto) { + return this.permissionService.create(dto, false); + } + + @Patch() + @Permission('permission::update') + update(@Body() dto: UpdatePermissionDto) { + return this.permissionService.updatePermission(dto); + } + + @Get() + @Permission('permission::get') + find( + @Query('page', new DefaultValuePipe('1'), ParseIntPipe) page: number, + @Query('limit', new DefaultValuePipe('0'), ParseIntPipe) limit: number, + @Query('name') name?: string + ) { + return this.permissionService.findPermission(page, limit, name); + } + + @Delete('/:id') + @Permission('permission::remove') + del(@Param('id') id: number) { + return this.permissionService.delPermission(id); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts new file mode 100644 index 00000000..f839cf2b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts @@ -0,0 +1,57 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { UserService } from '../user/user.service'; +import { Request } from 'express'; +import { User } from '@app/models'; +import { PERMISSION_KEYS } from '../public/permission.decorator'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext } from 'nestjs-i18n'; + +interface CustomReq extends Request { + user: User; +} + +@Injectable() +export class PermissionGuard implements CanActivate { + constructor(private reflector: Reflector, private userSerivce: UserService) {} + async canActivate(ctx: ExecutionContext) { + const i18n = I18nContext.current(); + const req: CustomReq = ctx.switchToHttp().getRequest(); + const requiredPermission = this.reflector.getAllAndOverride( + PERMISSION_KEYS, + [ctx.getClass(), ctx.getHandler()] + ); + if (!requiredPermission || requiredPermission.length === 0) { + return true; + } + const [, token] = (req.headers.authorization ?? '').split(' ') ?? ['', '']; + const permissionNames = await this.userSerivce.getUserPermission( + token, + req.user + ); + const isContainedPermission = requiredPermission.every((item) => + permissionNames.includes(item) + ); + if (permissionNames.includes('*')) { + return true; + } + if (!isContainedPermission) { + throw new HttpException( + i18n.t('exception.common.forbidden', { + lang: I18nContext.current().lang, + args: { + permission: requiredPermission.join(','), + }, + }), + HttpStatus.FORBIDDEN + ); + } + return true; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts new file mode 100644 index 00000000..7b018ea4 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { PermissionService } from './permission.service'; +import { PermissionController } from './permission.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Permission } from '@app/models'; + +@Module({ + controllers: [PermissionController], + providers: [PermissionService], + imports: [TypeOrmModule.forFeature([Permission])], + exports: [PermissionService], +}) +export class PermissionModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts new file mode 100644 index 00000000..c9652a94 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts @@ -0,0 +1,77 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Permission } from '@app/models'; +import { Like, Repository } from 'typeorm'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +import { paginate } from 'nestjs-typeorm-paginate'; + +@Injectable() +export class PermissionService { + constructor( + @InjectRepository(Permission) + private permission: Repository, + private readonly i18n: I18nService + ) {} + async create(createPermissionDto: CreatePermissionDto, isInit: boolean) { + const { name, desc } = createPermissionDto; + const permissionInfo = this.permission.findOne({ + where: { name }, + }); + if (isInit == true && (await permissionInfo)) { + return permissionInfo; + } + if ((await permissionInfo) && isInit == false) { + throw new HttpException( + this.i18n.t('exception.permission.exists', { + args: { name }, + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + const permission = await this.permission.save({ name, desc }); + return permission; + } + async updatePermission(dto: UpdatePermissionDto) { + const { name, desc, id } = dto; + const permissionInfo = await this.permission.findOne({ + where: { id }, + }); + if (!permissionInfo) { + throw new HttpException( + this.i18n.t('exception.permission.notExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + return this.permission.update(id, { name, desc }); + } + async findPermission(page?: number, limit?: number, name?: string) { + if (!limit) { + return this.permission.find(); + } + const count = await this.permission.count(); + return paginate( + this.permission, + { + limit: limit ? limit : count, + page, + }, + { + where: { + name: name ? Like(name) : undefined, + }, + } + ); + } + async delPermission(id: number) { + const permissionInfo = await this.permission.findOne({ + where: { id }, + }); + return this.permission.remove(permissionInfo); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts b/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts new file mode 100644 index 00000000..a0fc1a5f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; + +export const PERMISSION_KEYS = 'permissions'; + +export const Permission = (...permissions: string[]) => + SetMetadata(PERMISSION_KEYS, permissions); diff --git a/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts b/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts new file mode 100644 index 00000000..5b8bb31b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; + +export const Public = () => SetMetadata('isPublic', true); diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts new file mode 100644 index 00000000..44232ec8 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts @@ -0,0 +1,12 @@ +import { IsArray, IsNotEmpty } from 'class-validator'; + +export class CreateRoleDto { + @IsNotEmpty() + name: string; + @IsArray() + @IsNotEmpty() + permissionIds: number[]; + @IsArray() + @IsNotEmpty() + menuIds: number[]; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts new file mode 100644 index 00000000..6339203f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) { + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts b/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts new file mode 100644 index 00000000..ec816d51 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts @@ -0,0 +1 @@ +export class Role {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts new file mode 100644 index 00000000..bd32ac19 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts @@ -0,0 +1,65 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + DefaultValuePipe, +} from '@nestjs/common'; +import { RoleService } from './role.service'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { Permission } from '../public/permission.decorator'; + +@Controller('role') +export class RoleController { + constructor(private readonly roleService: RoleService) {} + + @Permission('role::add') + @Post() + create(@Body() createRoleDto: CreateRoleDto) { + return this.roleService.create(createRoleDto, false); + } + + @Permission('role::query') + @Get() + getAllRole() { + return this.roleService.findAll(); + } + + @Permission('role::query') + @Get('/detail') + getAllRoleDetail( + @Query('page', new DefaultValuePipe('1'), ParseIntPipe) page?: number, + @Query( + 'limit', + new DefaultValuePipe(process.env.PAGINATION_LIMIT), + ParseIntPipe + ) + limit?: number, + @Query('name') name?: string + ) { + return this.roleService.findAllDetail(page, limit, name); + } + + @Patch() + @Permission('role::update') + updateRole(@Body() dto: UpdateRoleDto) { + return this.roleService.update(dto); + } + + @Delete('/:id') + @Permission('role::remove') + deleteRole(@Param('id') id: number) { + return this.roleService.delete(id); + } + + @Get('/info/:id') + getRoleInfo(@Param('id') id: string) { + return this.roleService.findOne(id); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts new file mode 100644 index 00000000..d4c21f68 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { RoleService } from './role.service'; +import { RoleController } from './role.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Menu, Permission, Role, User } from '@app/models'; + +@Module({ + controllers: [RoleController], + providers: [RoleService], + imports: [TypeOrmModule.forFeature([Role, Permission, Menu, User])], + exports: [RoleService], +}) +export class RoleModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts new file mode 100644 index 00000000..49eeeacc --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts @@ -0,0 +1,162 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Menu, Permission, Role, User } from '@app/models'; +import { In, Like, Repository } from 'typeorm'; +import { convertToTree } from '../menu/menu.service'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +import { I18nTranslations } from '../.generate/i18n.generated'; +import { paginate } from 'nestjs-typeorm-paginate'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Role) + private readonly role: Repository, + @InjectRepository(Permission) + private readonly permission: Repository, + @InjectRepository(Menu) + private readonly menu: Repository, + @InjectRepository(User) + private readonly user: Repository, + private readonly i18n: I18nService + ) {} + async create(createRoleDto: CreateRoleDto, isInit: boolean) { + const { name, permissionIds = [], menuIds = [] } = createRoleDto; + const roleInfo = this.role.findOne({ + where: { + name, + }, + }); + if (isInit == true && (await roleInfo)) { + return roleInfo; + } + if (await roleInfo) { + throw new HttpException( + this.i18n.t('exception.role.exists', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + const permissions = await this.permission.find({ + where: { + id: In(permissionIds), + }, + }); + const menus = await this.menu.find({ + where: { + id: In(menuIds), + }, + }); + return this.role.save({ name, permission: permissions, menus }); + } + findAll() { + return this.role.find(); + } + + async findAllDetail(page?: number, limit?: number, name?: string) { + const roleInfo = await paginate( + this.role, + { + page, + limit, + }, + { + where: { + name: name ? Like(name) : undefined, + }, + relations: ['permission', 'menus'], + } + ); + const menuTree = []; + for (const item of roleInfo.items) { + menuTree.push(convertToTree(item.menus)); + } + return { + roleInfo, + menuTree, + }; + } + + async findOne(id: string) { + const roleInfo = await this.role + .createQueryBuilder('role') + .leftJoinAndSelect('role.menus', 'menus') + .leftJoinAndSelect('role.permission', 'permission') + .where({ + id: parseInt(id), + }) + .getOne(); + if (!roleInfo) { + throw new HttpException( + this.i18n.t('exception.role.notExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.NOT_FOUND + ); + } + return roleInfo; + } + + async update(data: UpdateRoleDto) { + const permission = await this.permission.find({ + where: { + id: In(data.permissionIds ?? []), + }, + }); + const menus = await this.menu.find({ + where: { + id: In(data.menuIds ?? []), + }, + }); + const { id, name } = data; + const roleInfo = await this.role.find({ + where: { + id: id, + }, + }); + if (roleInfo.length === 0) { + throw new HttpException( + this.i18n.t('exception.role.notExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + const role = roleInfo[0]; + role.name = name; + if (data.permissionIds) { + role.permission = permission; + } + if (data.menuIds) { + role.menus = menus; + } + return this.role.save(role); + } + async delete(id: number) { + const role = await this.role.find({ + where: { + id: id, + }, + }); + const user = await this.user.find({ + where: { + role: { + id, + }, + }, + take: 1, + }); + if (user.length) { + throw new HttpException( + this.i18n.t('exception.role.conflict', { + lang: I18nContext.current().lang, + }), + HttpStatus.CONFLICT + ); + } + return this.role.remove(role); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts new file mode 100644 index 00000000..d773a597 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts @@ -0,0 +1,28 @@ +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class CreateUserDto { + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + name: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + email: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + password: string; + roleIds: number[] = []; + department?: string; + employeeType?: string; + probationStart?: string; + probationEnd?: string; + probationDuration?: string; + protocolStart?: string; + protocolEnd?: string; + address?: string; + status?: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/pagination-query.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/pagination-query.dto.ts new file mode 100644 index 00000000..21d6d8c9 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/pagination-query.dto.ts @@ -0,0 +1,12 @@ +import { IsOptional } from 'class-validator'; +import { Transform} from 'class-transformer'; +import * as process from "process"; +export class PaginationQueryDto { + @IsOptional() + @Transform(value => isNaN(Number(value)) ? 1 : Number(value)) + page?: number = Number(process.env.PAGITION_PAGE); + + @IsOptional() + @Transform(value => isNaN(Number(value)) ? 10 : Number(value)) + limit?: number = Number(process.env.PAGITION_PAGE); +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-admin.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-admin.dto.ts new file mode 100644 index 00000000..bd2f6c18 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-admin.dto.ts @@ -0,0 +1,14 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; +import { IsNotEmpty } from 'class-validator'; +import { I18nTranslations } from '../../.generate/i18n.generated'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export class UpdatePwdAdminDto extends PartialType(CreateUserDto) { + email: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + newPassword: string; + confirmNewPassword?: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-user.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-user.dto.ts new file mode 100644 index 00000000..d231e765 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-pwd-user.dto.ts @@ -0,0 +1,18 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class UpdatePwdUserDto extends PartialType(CreateUserDto) { + email: string; + token: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + newPassword: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + oldPassword?: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts new file mode 100644 index 00000000..2c47c14e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts @@ -0,0 +1,58 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; +import { IsNotEmpty } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; +import { I18nTranslations } from '../../.generate/i18n.generated'; + +export class UpdateUserDto extends PartialType(CreateUserDto) { + oldPassword: string; + newPassword: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + email: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + roleIds: number[] = []; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + department: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + employeeType: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + probationStart: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + probationEnd: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + probationDuration: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + protocolStart: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + protocolEnd: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + address: string; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + status: number; + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts new file mode 100644 index 00000000..1f38440d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [UserService], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts new file mode 100644 index 00000000..f13aec44 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts @@ -0,0 +1,84 @@ +import { + Body, + Controller, + DefaultValuePipe, + Delete, + Get, + HttpException, + HttpStatus, + Param, + ParseArrayPipe, + ParseEnumPipe, + ParseIntPipe, + Patch, + Post, + Put, + Query, + Req, +} from '@nestjs/common'; +import { UserService } from './user.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { PaginationQueryDto } from './dto/pagination-query.dto'; +import { UpdatePwdAdminDto } from './dto/update-pwd-admin.dto'; +import { UpdatePwdUserDto } from './dto/update-pwd-user.dto'; +import { I18n, I18nContext } from 'nestjs-i18n'; +import { I18nTranslations } from '../.generate/i18n.generated'; + +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + @Post('reg') + @Permission('user::add') + async register(@Body() body: CreateUserDto) { + return this.userService.create(body, false); + } + @Get('/info/:email?') + async getUserInfo( + @I18n() i18n: I18nContext, + @Req() request: Request & RequestUser, + @Param('email') email?: string + ) { + const _email = email ? email : request.user.email; + if (!_email) { + throw new HttpException( + i18n.t('exception.common.unauth', { lang: I18nContext.current().lang }), + HttpStatus.UNAUTHORIZED + ); + } + return this.userService.getUserInfo(_email, ['role', 'role.permission']); + } + @Delete('/:email') + @Permission('user::remove') + async delUser(@Param('email') email: string) { + return this.userService.deleteUser(email); + } + @Patch('/update') + @Permission('user::update') + async UpdateUser(@Body() body: UpdateUserDto) { + return this.userService.updateUserInfo(body); + } + @Get() + @Permission('user::query') + async getAllUser( + @Query() paginationQuery: PaginationQueryDto, + @Query('name') name?: string, + @Query('role', new DefaultValuePipe([]), ParseArrayPipe) role?: number[], + @Query('email') email?: string + ) { + return this.userService.getAllUser(paginationQuery, name, role, email); + } + + @Patch('/admin/updatePwd') + @Permission('user::password::force-update') + async updatePwdAdmin(@Body() body: UpdatePwdAdminDto) { + return this.userService.updatePwdAdmin(body); + } + + @Patch('/updatePwd') + @Permission('user::update') + async updatePwdUser(@Body() body: UpdatePwdUserDto) { + return this.userService.updatePwdUser(body); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts new file mode 100644 index 00000000..efcdfde2 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { Permission, Role, User } from '@app/models'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthService } from '../auth/auth.service'; +import { RedisService } from '../../libs/redis/redis.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([User, Permission, Role])], + controllers: [UserController], + providers: [UserService, AuthService, RedisService], + exports: [UserService], +}) +export class UserModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts new file mode 100644 index 00000000..873de8ac --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts new file mode 100644 index 00000000..2d4b1bd1 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts @@ -0,0 +1,347 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { PaginationQueryDto } from './dto/pagination-query.dto'; +import { UpdatePwdAdminDto } from './dto/update-pwd-admin.dto'; +import { UpdatePwdUserDto } from './dto/update-pwd-user.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Role, User } from '@app/models'; +import { In, Like, Repository } from 'typeorm'; +import * as crypto from 'crypto'; +import { AuthService } from '../auth/auth.service'; +import { paginate } from 'nestjs-typeorm-paginate'; +import * as process from 'process'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +import { I18nTranslations } from '../.generate/i18n.generated'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private userRep: Repository, + @InjectRepository(Role) + private roleRep: Repository, + private readonly authService: AuthService, + private readonly i18n: I18nService + ) {} + + async create(createUserDto: CreateUserDto, isInit: boolean) { + const { + email, + password, + roleIds = [], + name, + department, + employeeType, + probationStart, + probationEnd, + probationDuration, + protocolStart, + protocolEnd, + address, + status, + } = createUserDto; + const userInfo = this.getUserInfo(email); + if (isInit == true && (await userInfo)) { + return userInfo; + } + if (await userInfo) { + throw new HttpException( + this.i18n.translate('exception.user.userExists', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + const roles = this.roleRep.find({ + where: { + id: In(roleIds), + }, + }); + try { + const user = this.userRep.create({ + email, + password, + name: name, + role: await roles, + department: department, + employeeType: employeeType, + protocolStart: protocolStart, + protocolEnd: protocolEnd, + probationEnd: probationEnd, + probationStart: probationStart, + probationDuration: probationDuration, + address: address, + status: status, + }); + return this.userRep.save(user); + } catch (err) { + throw new HttpException( + (err as Error).message, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + //获取所有用户信息 + async getAllUser( + paginationQuery: PaginationQueryDto, + name?: string, + role?: number[], + email?: string + ): Promise { + const { page, limit } = paginationQuery; // 从DTO获取分页参数 + const relations = ['role', 'role.permission']; + const result = await paginate( + this.userRep, + { + page: Number(page) || Number(process.env.PAGITION_PAGE), + limit: Number(limit) || Number(process.env.PAGITION_LIMIT), + }, + { + select: [ + 'id', + 'name', + 'email', + 'department', + 'employeeType', + 'protocolStart', + 'protocolEnd', + 'probationEnd', + 'probationStart', + 'probationDuration', + 'address', + 'status', + ], + relations, + where: { + name: name ? Like(name) : undefined, + role: + role && role.length + ? { + id: In(role), + } + : undefined, + email: email ? Like(email) : undefined, + }, + } + ); + for (const user of result.items) { + if (user.probationStart !== null) { + user.probationStart = await this.formatDateToDay( + new Date(user.probationStart) + ); + } + if (user.probationEnd !== null) { + user.probationEnd = await this.formatDateToDay( + new Date(user.probationEnd) + ); + } + if (user.protocolStart !== null) { + user.protocolStart = await this.formatDateToDay( + new Date(user.protocolStart) + ); + } + if (user.protocolEnd !== null) { + user.protocolEnd = await this.formatDateToDay( + new Date(user.protocolEnd) + ); + } + } + return result; + } + + async formatDateToDay(date: { + getFullYear: () => any; + getMonth: () => number; + getDate: () => any; + }) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份是从0开始的 + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + async getUserInfo(email: string, relations: string[] = []) { + const user = await this.userRep.findOne({ + where: { email }, + select: [ + 'id', + 'name', + 'email', + 'department', + 'employeeType', + 'protocolStart', + 'protocolEnd', + 'probationEnd', + 'probationStart', + 'probationDuration', + 'address', + 'status', + ], + relations, + }); + if (user) { + if (user.probationStart !== null) { + user.probationStart = await this.formatDateToDay( + new Date(user.probationStart) + ); + } + if (user.probationEnd !== null) { + user.probationEnd = await this.formatDateToDay( + new Date(user.probationEnd) + ); + } + if (user.protocolStart !== null) { + user.protocolStart = await this.formatDateToDay( + new Date(user.protocolStart) + ); + } + if (user.protocolEnd !== null) { + user.protocolEnd = await this.formatDateToDay( + new Date(user.protocolEnd) + ); + } + } + return user; + } + + async getUserPermission(token: string, userInfo: User) { + const { email } = userInfo; + const { role } = (await this.getUserInfo(email, [ + 'role', + 'role.permission', + ])) ?? { role: [] as Role[] }; + const permission = role.flatMap((r) => r.permission); + const permissionNames = permission.map((p) => p.name); + return [...new Set([...permissionNames])]; + } + + //验证旧密码是否正确 + async verifyPassword(password: string, storedHash: string, salt: string) { + const newHash = crypto + .pbkdf2Sync(password, salt, 1000, 18, 'sha256') + .toString('hex'); + return newHash === storedHash; + } + + //修改密码后加密 + async encry(value: string, salt: string) { + return crypto.pbkdf2Sync(value, salt, 1000, 18, 'sha256').toString('hex'); + } + + async deleteUser(email: string) { + const allUser = await this.userRep.find(); + if (allUser.length > 1) { + const user = await this.userRep.findOne({ + where: { email }, + }); + if (!user) { + return; + } + await this.authService.kickOut(email); + return this.userRep.remove(user); + } + throw new HttpException( + this.i18n.translate('exception.user.userNumberNull', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } + + //修改密码 + async updatePwdUser(data: UpdatePwdUserDto) { + const { email, newPassword, oldPassword, token } = data; + const user = this.userRep.findOne({ + where: { email }, + select: ['id', 'name', 'email', 'salt', 'password'], + }); + if (user) { + if ( + !(await this.verifyPassword( + oldPassword, + ( + await user + ).password, + ( + await user + ).salt + )) + ) { + throw new HttpException( + this.i18n.translate('exception.user.oldPasswordError', { + lang: I18nContext.current().lang, + }), + HttpStatus.BAD_REQUEST + ); + } else { + (await user).password = await this.encry( + newPassword, + ( + await user + ).salt + ); + await this.userRep.save(await user); + await this.authService.kickOut((await user).email); + return; + } + } + } + + async updatePwdAdmin(data: UpdatePwdAdminDto) { + const { email, newPassword } = data; + const user = this.userRep.findOne({ + where: { email }, + select: ['id', 'name', 'email', 'salt', 'password'], + }); + if (user) { + (await user).password = await this.encry(newPassword, (await user).salt); + await this.userRep.save(await user); + await this.authService.kickOut((await user).email); + return; + } + } + + async updateUserInfo(updateUserDto: UpdateUserDto) { + const { + email, + roleIds, + department, + employeeType, + probationStart, + probationEnd, + probationDuration, + protocolStart, + protocolEnd, + address, + status, + name, + } = updateUserDto; + const user = this.getUserInfo(email, ['role']); + const roles = this.roleRep.find({ + where: { + id: In(roleIds), + }, + }); + const userRoles = (await user).role.map((role) => role.id).join(''); + if (user) { + (await user).name = name; + (await user).department = department; + (await user).employeeType = employeeType; + (await user).probationStart = probationStart; + (await user).probationEnd = probationEnd; + (await user).probationDuration = probationDuration; + (await user).protocolStart = protocolStart; + (await user).protocolEnd = protocolEnd; + (await user).address = address; + (await user).status = status; + (await user).role = await roles; + } + const newProfile = await this.userRep.save(await user); + if (userRoles !== roleIds.join('')) { + await this.authService.kickOut(email); + } + return newProfile; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/tsconfig.json b/packages/toolkits/pro/template/server/nestJs/tsconfig.json index d9c82ca9..918e5865 100644 --- a/packages/toolkits/pro/template/server/nestJs/tsconfig.json +++ b/packages/toolkits/pro/template/server/nestJs/tsconfig.json @@ -11,7 +11,21 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, - "skipLibCheck": true + "skipLibCheck": true, + "paths": { + "@app/db": [ + "libs/db/src" + ], + "@app/db/*": [ + "libs/db/src/*" + ], + "@app/models": [ + "libs/models/src" + ], + "@app/models/*": [ + "libs/models/src/*" + ] + } }, - "include": ["src/**/*"] + "include": ["src/**/*", "./src/global.d.ts", "libs/**/*"] } diff --git a/packages/toolkits/pro/template/tinyvue/.env b/packages/toolkits/pro/template/tinyvue/.env index 679e6062..27a9a3e9 100644 --- a/packages/toolkits/pro/template/tinyvue/.env +++ b/packages/toolkits/pro/template/tinyvue/.env @@ -1,5 +1,8 @@ VITE_CONTEXT=/vue-pro/ VITE_BASE_API=/api -VITE_SERVER_HOST= http://127.0.0.1:7001 -VITE_USE_MOCK= true -VITE_MOCK_IGNORE= /api/user/userInfo,/api/user/login,/api/user/register,/api/employee/getEmployee \ No newline at end of file +VITE_SERVER_HOST= http://127.0.0.1:3000 +VITE_MOCK_HOST= http://127.0.0.1:8848 +VITE_USE_MOCK= false +VITE_MOCK_IGNORE= /api/user/userInfo,/api/user/login,/api/user/register,/api/employee/getEmployee + +VITE_MOCK_SERVER_HOST=/mock diff --git a/packages/toolkits/pro/template/tinyvue/.gitignore b/packages/toolkits/pro/template/tinyvue/.gitignore index 06d7f53f..69904e8c 100644 --- a/packages/toolkits/pro/template/tinyvue/.gitignore +++ b/packages/toolkits/pro/template/tinyvue/.gitignore @@ -4,3 +4,4 @@ dist dist-ssr *.local *debug.log +*.idea diff --git a/packages/toolkits/pro/template/tinyvue/babel.config.js b/packages/toolkits/pro/template/tinyvue/babel.config.js index dd6f53ce..3f7f17c9 100644 --- a/packages/toolkits/pro/template/tinyvue/babel.config.js +++ b/packages/toolkits/pro/template/tinyvue/babel.config.js @@ -1,3 +1,5 @@ module.exports = { - plugins: ['@vue/babel-plugin-jsx'], + plugins: [ + '@vue/babel-plugin-jsx', + ] }; diff --git a/packages/toolkits/pro/template/tinyvue/config/vite.config.base.ts b/packages/toolkits/pro/template/tinyvue/config/vite.config.base.ts index f1dce32f..1f089374 100644 --- a/packages/toolkits/pro/template/tinyvue/config/vite.config.base.ts +++ b/packages/toolkits/pro/template/tinyvue/config/vite.config.base.ts @@ -3,25 +3,12 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; import svgLoader from 'vite-svg-loader'; -import { viteMockServe } from 'vite-plugin-mock'; - -const useMock = !! process.env.VITE_USE_MOCK const config = { plugins: [ vue(), vueJsx(), svgLoader({ svgoConfig: {} }), - viteMockServe({ - mockPath: '../src/mock', // mock文件地址 - localEnabled:useMock, // 开发打包开关 - prodEnabled:true, // 生产打包开关 // 这样可以控制关闭mock的时候不让mock打包到最终代码内 - injectCode: ` import { setupProdMockServer } from '../src/mockProdServer'; setupProdMockServer(); `, - logger: true, // 是否在控制台显示请求日志 - supportTs: false, // 打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件 - watchFiles:true, - injectFile: resolve('../src/main.ts'), - }) ], build: { outDir: resolve(__dirname, '../dist'), @@ -52,6 +39,7 @@ const config = { }, define: { 'process.env': {}, + BUILD_TOOLS: "'VITE'" }, css: { preprocessorOptions: { diff --git a/packages/toolkits/pro/template/tinyvue/config/vite.config.dev.ts b/packages/toolkits/pro/template/tinyvue/config/vite.config.dev.ts index 951e8188..f6b5f207 100644 --- a/packages/toolkits/pro/template/tinyvue/config/vite.config.dev.ts +++ b/packages/toolkits/pro/template/tinyvue/config/vite.config.dev.ts @@ -7,12 +7,17 @@ const proxyConfig = { target: loadEnv('', process.cwd()).VITE_SERVER_HOST, changeOrigin: true, logLevel: 'debug', - rewrite: (path) => + rewrite: (path: string) => path.replace( new RegExp(`${loadEnv('', process.cwd()).VITE_BASE_API}`), '' ), }, + [loadEnv('', process.cwd()).VITE_MOCK_SERVER_HOST]: { + target: loadEnv('', process.cwd()).VITE_MOCK_HOST, + changeOrigin: true, + rewrite: (path:string) => path.replace(/^\/mock/, '') + } }; export default mergeConfig( { diff --git a/packages/toolkits/pro/template/tinyvue/config/vite.config.preview.base.ts b/packages/toolkits/pro/template/tinyvue/config/vite.config.preview.base.ts index b815ab17..018e9924 100644 --- a/packages/toolkits/pro/template/tinyvue/config/vite.config.preview.base.ts +++ b/packages/toolkits/pro/template/tinyvue/config/vite.config.preview.base.ts @@ -3,23 +3,12 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; import svgLoader from 'vite-svg-loader'; -import { viteMockServe } from 'vite-plugin-mock'; export default defineConfig({ plugins: [ vue(), vueJsx(), svgLoader({ svgoConfig: {} }), - viteMockServe({ - // 更多配置见最下方 - mockPath: '../src/mock', // mock文件地址 - localEnabled: true, // 开发打包开关 - prodEnabled: true, // 生产打包开关 // 这样可以控制关闭mock的时候不让mock打包到最终代码内 - injectCode: ` import { setupProdMockServer } from '../src/mockProdServer'; setupProdMockServer(); `, - logger: false, // 是否在控制台显示请求日志 - supportTs: false, // 打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件 - injectFile: resolve('../src/main.ts'), - }), ], build: { outDir: resolve(__dirname, '../dist/vue-pro/pages'), diff --git a/packages/toolkits/pro/template/tinyvue/config/vite.config.prod.ts b/packages/toolkits/pro/template/tinyvue/config/vite.config.prod.ts index a5ddb3af..abccb66c 100644 --- a/packages/toolkits/pro/template/tinyvue/config/vite.config.prod.ts +++ b/packages/toolkits/pro/template/tinyvue/config/vite.config.prod.ts @@ -1,12 +1,11 @@ import { mergeConfig } from 'vite'; -import baseConig from './vite.config.base'; +import baseConfig from './vite.config.base'; import configCompressPlugin from './plugin/compress'; import configVisualizerPlugin from './plugin/visualizer'; export default mergeConfig( { mode: 'production', - mock: true, plugins: [configCompressPlugin('gzip'), configVisualizerPlugin()], build: { rollupOptions: { @@ -19,5 +18,5 @@ export default mergeConfig( chunkSizeWarningLimit: 2000, }, }, - baseConig + baseConfig ); diff --git a/packages/toolkits/pro/template/tinyvue/dev.env b/packages/toolkits/pro/template/tinyvue/dev.env new file mode 100644 index 00000000..27a9a3e9 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/dev.env @@ -0,0 +1,8 @@ +VITE_CONTEXT=/vue-pro/ +VITE_BASE_API=/api +VITE_SERVER_HOST= http://127.0.0.1:3000 +VITE_MOCK_HOST= http://127.0.0.1:8848 +VITE_USE_MOCK= false +VITE_MOCK_IGNORE= /api/user/userInfo,/api/user/login,/api/user/register,/api/employee/getEmployee + +VITE_MOCK_SERVER_HOST=/mock diff --git a/packages/toolkits/pro/template/tinyvue/farm.config.ts b/packages/toolkits/pro/template/tinyvue/farm.config.ts new file mode 100644 index 00000000..39fdf580 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/farm.config.ts @@ -0,0 +1,65 @@ +import { defineConfig, loadEnv } from '@farmfe/core'; +import vue from '@vitejs/plugin-vue'; +import less from '@farmfe/js-plugin-less'; +import { join, resolve } from 'path'; +import { configDotenv } from 'dotenv'; + +configDotenv({ + path: './.env', +}); + +export default defineConfig({ + vitePlugins: [vue()], + plugins: [ + less({ + additionalData: `@import "${resolve(__dirname, 'src/assets/style/breakpoint.less')}";`, + }), + ], + compilation: { + define: { + 'process.env': {}, + 'BUILD_TOOLS': '"VITE"', + 'import.meta.env.VITE_CONTEXT': '"/vue-pro/"', + 'import.meta.env.VITE_BASE_API': '"/api"', + 'import.meta.env.VITE_SERVER_HOST': '"http://127.0.0.1:3000"', + 'import.meta.env.VITE_MOCK_HOST': '"http://127.0.0.1:8848"', + 'import.meta.env.VITE_USE_MOCK': 'false', + 'import.meta.env.VITE_MOCK_IGNORE': + '"/api/user/userInfo,/api/user/login,/api/user/register,/api/employee/getEmployee"', + 'import.meta.env.VITE_MOCK_SERVER_HOST': '"/mock"', + }, + resolve: { + alias: { + '@': join(__dirname, 'src'), + 'assets': join(__dirname, 'src/assets'), + 'vue-i18n': 'vue-i18n/dist/vue-i18n.esm-bundler.js', + 'vue': 'vue/dist/vue.esm-bundler.js', + }, + extensions: ['.ts', '.js'], + }, + }, + server: { + proxy: { + '/mock': { + pathFilter(pathname, req) { + return Boolean(pathname.match('^/mock')); + }, + pathRewrite: { + '^/mock': '', + }, + target: 'http://localhost:8848', + logger: console, + }, + '/api': { + pathRewrite: { + '^/api': '', + }, + target: 'http://localhost:3000', + pathFilter(pathname, req) { + return Boolean(pathname.match('^/api')); + }, + logger: console, + }, + }, + }, +}); diff --git a/packages/toolkits/pro/template/tinyvue/index.html b/packages/toolkits/pro/template/tinyvue/index.html index 0b1d5c7f..3066b49c 100644 --- a/packages/toolkits/pro/template/tinyvue/index.html +++ b/packages/toolkits/pro/template/tinyvue/index.html @@ -8,6 +8,6 @@
- + diff --git a/packages/toolkits/pro/template/tinyvue/package.json b/packages/toolkits/pro/template/tinyvue/package.json index b5990ca4..de8ad64b 100644 --- a/packages/toolkits/pro/template/tinyvue/package.json +++ b/packages/toolkits/pro/template/tinyvue/package.json @@ -8,9 +8,16 @@ "scripts": { "start": "vite --config ./config/vite.config.dev.ts --port 3031", "build": "vite build --config ./config/vite.config.prod.ts", + "build:wp": "webpack --config webpack.config.js", + "dev:wp": "webpack-dev-server --progress --config webpack.config.js", + "dev:rp": "rspack serve", + "build:rp": "rspack build", "report": "cross-env REPORT=true npm run build", "lint-staged": "npx lint-staged", - "prepare": "git init && husky install" + "prepare": "git init && husky install", + "mock": "tsx ./src/mock/index.ts", + "dev:fr": "farm", + "build:fr": "farm build" }, "lint-staged": { "*.{js,ts,jsx,tsx}": [ @@ -28,72 +35,103 @@ ] }, "dependencies": { - "@opentiny/hwc-client": "^0.0.14", - "@opentiny/vue": "^3.11.1", - "@types/mockjs": "^1.0.9", - "@vueuse/core": "^10.5.0", - "@vueuse/head": "^2.0.0", - "axios": "^1.6.0", - "dayjs": "^1.11.10", - "echarts": "~5.4.3", + "@babel/core": "7.25.2", + "@gaonengwww/mock-server": "1.0.5", + "@opentiny/hwc-client": "0.0.14", + "@opentiny/vue": "3.18.0", + "@opentiny/vue-icon": "3.18.0", + "@opentiny/vue-locale": "3.18.0", + "@opentiny/vue-theme": "3.18.2", + "@types/mockjs": "1.0.10", + "@types/node": "22.7.4", + "@vueuse/core": "10.11.1", + "@vueuse/head": "2.0.0", + "axios": "1.7.7", + "dayjs": "1.11.13", + "echarts": "5.4.3", "echarts4": "npm:echarts@4.9.0", "fp-ts": "2.16.1", - "mitt": "^3.0.1", - "moment": "^2.29.4", - "nprogress": "^1.0.0-1", + "mitt": "3.0.1", + "moment": "2.30.1", + "nprogress": "1.0.0-1", "pinia": "2.1.7", - "query-string": "^8.1.0", - "vue": "^3.3.7", - "vue-eslint-parser": "^9.3.2", - "vue-i18n": "^9.6.2", - "vue-router": "^4.2.5" + "query-string": "8.2.0", + "style-resources-loader": "1.5.0", + "vue": "3.5.10", + "vue-eslint-parser": "9.4.3", + "vue-i18n": "9.14.1", + "vue-router": "4.4.5", + "vue-style-loader": "4.1.3" }, "devDependencies": { - "@commitlint/cli": "^11.0.0", - "@commitlint/config-conventional": "^12.0.1", - "@types/lodash": "^4.14.177", - "@types/nprogress": "^0.2.0", - "@typescript-eslint/eslint-plugin": "^5.10.0", - "@typescript-eslint/parser": "^5.10.0", - "@vitejs/plugin-vue": "^4.3.4", - "@vitejs/plugin-vue-jsx": "^3.0.2", - "@vue/babel-plugin-jsx": "^1.1.5", - "cross-env": "^7.0.3", - "eslint": "^7.2.0", - "eslint-config-airbnb-base": "^14.2.1", - "eslint-config-prettier": "^8.3.0", - "eslint-import-resolver-typescript": "^2.4.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-vue": "^8.3.0", - "husky": "^7.0.4", - "less": "^4.1.2", - "lint-staged": "^11.2.6", - "mockjs": "^1.1.0", - "prettier": "^3.0.3", - "rollup-plugin-visualizer": "^5.9.2", - "stylelint": "^13.8.0", - "stylelint-config-prettier": "^8.0.2", - "stylelint-config-rational-order": "^0.1.2", - "stylelint-config-standard": "^20.0.0", - "stylelint-order": "^4.1.0", - "typescript": "^4.5.5", - "unplugin-vue-components": "^0.17.21", - "vite": "^4.4.9", - "vite-plugin-compression": "^0.5.1", - "vite-plugin-eslint": "^1.8.1", - "vite-plugin-html": "^3.2.0", - "vite-plugin-mock": "^2.9.6", + "@babel/preset-env": "7.25.4", + "@babel/preset-typescript": "7.24.7", + "@commitlint/cli": "11.0.0", + "@commitlint/config-conventional": "12.1.4", + "@farmfe/cli": "1.0.4", + "@farmfe/core": "1.3.28", + "@farmfe/js-plugin-less": "1.9.0", + "@rspack/cli": "0.7.5", + "@rspack/core": "0.7.5", + "@types/lodash": "4.17.9", + "@types/nprogress": "0.2.3", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "@vitejs/plugin-vue": "4.6.2", + "@vitejs/plugin-vue-jsx": "3.1.0", + "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", + "@vue/babel-plugin-jsx": "1.2.5", + "@vue/babel-preset-jsx": "1.4.0", + "babel-loader": "9.2.1", + "core-js": "3.38.1", + "cross-env": "7.0.3", + "css-loader": "7.1.2", + "dotenv": "16.4.5", + "eslint": "7.32.0", + "eslint-config-airbnb-base": "14.2.1", + "eslint-config-prettier": "8.10.0", + "eslint-import-resolver-typescript": "2.7.1", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-prettier": "3.4.1", + "eslint-plugin-vue": "8.7.1", + "html-webpack-plugin": "5.6.0", + "husky": "7.0.4", + "import-meta-loader": "1.1.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "lint-staged": "11.2.6", + "mockjs": "1.1.0", + "prettier": "3.3.3", + "rollup-plugin-visualizer": "5.12.0", + "style-loader": "4.0.0", + "stylelint": "13.13.1", + "stylelint-config-prettier": "8.0.2", + "stylelint-config-rational-order": "0.1.2", + "stylelint-config-standard": "20.0.0", + "stylelint-order": "4.1.0", + "ts-loader": "9.5.1", + "ts-node": "10.9.2", + "tsx": "4.19.1", + "typescript": "4.9.5", + "unplugin-vue-components": "0.17.21", + "vite": "4.5.5", + "vite-plugin-compression": "0.5.1", + "vite-plugin-eslint": "1.8.1", + "vite-plugin-html": "3.2.2", + "vite-plugin-mock": "2.9.8", "vite-plugin-style-import": "2.0.0", - "vite-svg-loader": "^4.0.0", - "vue-tsc": "^1.8.22" + "vite-svg-loader": "4.0.0", + "vue-loader": "17.4.2", + "vue-tsc": "1.8.27", + "webpack": "5.95.0", + "webpack-cli": "5.1.4", + "webpack-dev-server": "5.1.0" }, "engines": { "node": ">=14.0.0" }, "resolutions": { "bin-wrapper": "npm:bin-wrapper-china", - "rollup": "^2.56.3", "gifsicle": "5.2.0" } } diff --git a/packages/toolkits/pro/template/tinyvue/rspack.config.js b/packages/toolkits/pro/template/tinyvue/rspack.config.js new file mode 100644 index 00000000..fc0497a9 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/rspack.config.js @@ -0,0 +1,165 @@ +const { resolve } = require('path'); +const { VueLoaderPlugin } = require('vue-loader'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const rspack = require('@rspack/core'); +const { configDotenv, parse } = require('dotenv'); +const { default: importMetaLoader } = require('import-meta-loader'); +configDotenv({ + path: './.env', +}); + +/** @type {import('@rspack/cli').Configuration} */ +const config = { + context: __dirname, + entry: { + main: './src/main.ts', + }, + output: { + path: resolve(__dirname, 'dist'), // 打包后的文件输出的目录 + filename: `js/[name]_[chunkhash:8].js`, // 设置打包后的 js 文件名,如果在文件名前增加文件路径,会将打包后的 js 文件放在指定的文件夹下 + publicPath: '/', + }, + experiments: { + css: false, + }, + plugins: [ + new VueLoaderPlugin(), + new rspack.HtmlRspackPlugin({ + template: './index.html', + }), + new rspack.DefinePlugin({ + '__VUE_OPTIONS_API__': JSON.stringify(true), + '__VUE_PROD_DEVTOOLS__': JSON.stringify(false), + 'import.meta.env.VITE_CONTEXT': '"/vue-pro/"', + 'import.meta.env.VITE_BASE_API': '"/api"', + 'import.meta.env.VITE_SERVER_HOST': '"http://127.0.0.1:3000"', + 'import.meta.env.VITE_MOCK_HOST': '"http://127.0.0.1:8848"', + 'import.meta.env.VITE_USE_MOCK': 'false', + 'import.meta.env.VITE_MOCK_IGNORE': + '"/api/user/userInfo,/api/user/login,/api/user/register,/api/employee/getEmployee"', + 'import.meta.env.VITE_MOCK_SERVER_HOST': '"/mock"', + 'BUILD_TOOLS': "'RSPACK'", + }), + ], + devServer: { + historyApiFallback: true, + proxy: [ + { + context: [process.env.VITE_BASE_API], + target: process.env.VITE_SERVER_HOST, + changeOrigin: true, + pathRewrite: { + '^/api': '', + }, + }, + { + context: [process.env.VITE_MOCK_SERVER_HOST], + target: process.env.VITE_MOCK_HOST, + changeOrigin: true, + pathRewrite: { + '^/mock': '', + }, + }, + ], + client: { + overlay: false, + }, + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + experimentalInlineMatchResource: true, + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + appendTsSuffixTo: [/\.vue$/], + transpileOnly: true, + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.less$/, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'less-loader', + options: { + additionalData: `@import "${resolve('./src/assets/style/breakpoint.less')}";`, + }, + }, + ], + }, + { + test: /.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件 + type: 'asset', // type选择asset + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, // 小于10kb转base64位 + }, + }, + generator: { + filename: 'static/images/[name].[contenthash:8][ext]', // 文件输出目录和命名 + }, + }, + { + test: /.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件 + type: 'asset', // type选择asset + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, // 小于10kb转base64位 + }, + }, + generator: { + filename: 'static/fonts/[name].[contenthash:8][ext]', // 文件输出目录和命名 + }, + }, + { + test: /.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件 + type: 'asset', // type选择asset + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, // 小于10kb转base64位 + }, + }, + generator: { + filename: 'static/media/[name].[contenthash:8][ext]', // 文件输出目录和命名 + }, + }, + { + test: /\.svg$/, + type: 'asset/resource', + }, + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + 'assets': resolve(__dirname, 'src/assets'), + 'vue-i18n$': 'vue-i18n/dist/vue-i18n.esm-bundler.js', + 'vue$': 'vue/dist/vue.esm-bundler.js', + }, + extensions: ['.ts', '.js', '.vue'], + }, +}; +module.exports = config; diff --git a/packages/toolkits/pro/template/tinyvue/src/App.vue b/packages/toolkits/pro/template/tinyvue/src/App.vue index 328fb058..201e0dab 100644 --- a/packages/toolkits/pro/template/tinyvue/src/App.vue +++ b/packages/toolkits/pro/template/tinyvue/src/App.vue @@ -9,10 +9,14 @@ import { provide } from 'vue'; import * as echarts from 'echarts'; import GlobalSetting from '@/components/global-setting/index.vue'; + import TinyThemeTool from '@opentiny/vue-theme/theme-tool'; + import { useTheme } from './hooks/useTheme'; provide('echarts', echarts); + const theme = new TinyThemeTool(); + useTheme(theme); \ No newline at end of file + diff --git a/packages/toolkits/pro/template/tinyvue/src/api/board.ts b/packages/toolkits/pro/template/tinyvue/src/api/board.ts index 88fd4c49..ba116ece 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/board.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/board.ts @@ -1,19 +1,20 @@ import axios from 'axios'; +axios.defaults.timeout = 5000; // 获取select的option export function getUserData() { - return axios.get('/api/user/getdata'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/user/getdata`); } export function getUserPractic() { - return axios.get('/api/user/getrpractic'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/user/getrpractic`); } export function getUserTrain() { - return axios.get('/api/user/getrtrain'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/user/getrtrain`); } // 切换数据源 export function getUserChange(data: string) { - return axios.post('/api/user/getselect', data as any); + return axios.post(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/user/getselect`, data as any); } diff --git a/packages/toolkits/pro/template/tinyvue/src/api/form.ts b/packages/toolkits/pro/template/tinyvue/src/api/form.ts index 9f1eed04..be1b9bc3 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/form.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/form.ts @@ -22,15 +22,15 @@ export type UnitStepModel = PersonalModel & CompanyInfoModel; // 获取base表单的初始数据选项 export function getBaseData() { - return axios.get('/api/base/getdata'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/base/getdata`); } // 表单的数据提交&&校验 export function submitStepForm(data: UnitStepModel) { - return axios.post('/api/channel-form/submit', { data }); + return axios.post(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/channel-form/submit`, { data }); } // 获取step表单的初始数据选项 export function getStepData() { - return axios.get('/api/step/getdata'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/step/getdata`); } diff --git a/packages/toolkits/pro/template/tinyvue/src/api/interceptor.ts b/packages/toolkits/pro/template/tinyvue/src/api/interceptor.ts index 472b78ae..f7e4d8a2 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/interceptor.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/interceptor.ts @@ -10,8 +10,8 @@ export interface HttpResponse { data: T; } -const { VITE_API_BASE_URL, VITE_BASE_API, VITE_MOCK_IGNORE } = - import.meta.env || {}; +const { VITE_API_BASE_URL, VITE_BASE_API, VITE_MOCK_IGNORE } = import.meta + .env || { VITE_BASE_API: '', VITE_MOCK_IGNORE: '' }; if (VITE_API_BASE_URL) { axios.defaults.baseURL = VITE_API_BASE_URL; @@ -19,7 +19,7 @@ if (VITE_API_BASE_URL) { const ignoreMockApiList = VITE_MOCK_IGNORE?.split(',') || []; axios.interceptors.request.use( - (config: AxiosRequestConfig) => { + (config: AxiosRequestConfig): any => { const isProxy = ignoreMockApiList.includes(config.url); if (isProxy) { config.url = config.url?.replace(VITE_BASE_API, '/api/v1'); @@ -34,45 +34,44 @@ axios.interceptors.request.use( } config.headers = { ...config.headers }; + config.headers['x-lang'] = localStorage.getItem('tiny-locale') ?? 'zhCN'; return config; }, (error) => { // do something return Promise.reject(error); - } + }, ); // add response interceptors axios.interceptors.response.use( (response: AxiosResponse) => { - const res = response.data; - if (res.code !== '0') { - res.errMsg && - Modal.message({ - message: res.errMsg, - status: 'error', - }); - return Promise.reject(new Error(res.errMsg || 'Error')); + const res = response; + if (res.request.responseURL.includes('mock')) { + return res.data; } return res; }, (error) => { const { status, data } = error.response; + if (status === 403 && error.config.method.toLowerCase() === 'get') { + Modal.message({ + message: data.message, + status: 'error', + }); + } if (status === 401) { - clearToken(); - router.replace({ name: 'login' }); Modal.message({ message: locale.t('http.error.TokenExpire'), status: 'error', }); - } else { - data.errMsg && - Modal.message({ - message: locale.t(`http.error.${data.errMsg}`), - status: 'error', - }); + clearToken(); + router.replace({ name: 'login' }); + } + if (status === 400) { + data.message = error.response.data.errors?.[0] ?? data.message; } return Promise.reject(error); - } + }, ); diff --git a/packages/toolkits/pro/template/tinyvue/src/api/lang.ts b/packages/toolkits/pro/template/tinyvue/src/api/lang.ts new file mode 100644 index 00000000..194d9101 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/api/lang.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; + +export type Lang = { + id: number; + name: string; +}; +export interface CreateLangDTO { + name: string; +} + +export const getAllLang = () => { + return axios.get('/api/lang'); +}; +export const createLang = (data: CreateLangDTO) => { + return axios.post('/api/lang', data); +}; + +export const patchLang = (data: Partial, id: number) => { + return axios.patch(`/api/lang/${id}`, data); +}; + +export const deleteLang = (id: number) => { + return axios.delete<{ name: string }>(`/api/lang/${id}`); +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/api/list.ts b/packages/toolkits/pro/template/tinyvue/src/api/list.ts index 2b714115..60f12703 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/list.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/list.ts @@ -7,8 +7,11 @@ export interface QueryTaskParmas { } export function queryEmployeeList(params: QueryTaskParmas) { - return axios.post('/api/employee/getEmployee', params); + return axios.post( + `${import.meta.env.VITE_MOCK_SERVER_HOST}/api/employee/getEmployee`, + params, + ); } export function deleteEmployee(id: string) { - return axios.delete(`/api/employee/delete?id=${id}`); + return axios.delete(`/mock/api/employee/delete?id=${id}`); } diff --git a/packages/toolkits/pro/template/tinyvue/src/api/local.ts b/packages/toolkits/pro/template/tinyvue/src/api/local.ts new file mode 100644 index 00000000..4d0c4373 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/api/local.ts @@ -0,0 +1,65 @@ +import axios from 'axios'; +import { Lang } from './lang'; + +export type I18Table = { + [lang: string]: { + [key: string]: string; + }; +}; +export interface Locals { + items: Local[]; + meta: Meta; +} +export interface Local { + content: string; + id: number; + key: string; + lang: Lang; +} +export interface Meta { + currentPage: number; + itemCount: number; + itemsPerPage: number; + totalItems: number; + totalPages: number; +} +export interface CreateLocal { + content: string; + key: string; + lang: number; +} +export interface CreateLocalReturn { + content: string; + id: number; + key: string; + lang: Lang; +} + +type DeleteLocaleRet = Omit; + +export const getLocalTable = (lang?: string) => { + return axios.get('/api/i18/format', { params: { lang } }); +}; + +export const getAllLocalItems = ( + page?: number, + limit?: number, + all?: number, + filters?: { + [x: string]: number[] | string; + }, +) => { + return axios.get('/api/i18', { + params: { page, limit, all, ...filters }, + }); +}; + +export const createLocalItem = (data: CreateLocal) => { + return axios.post('/api/i18', data); +}; +export const deleteLocale = (id: number) => { + return axios.delete(`/api/i18/${id}`); +}; +export const patchLocal = (id: number, data: Partial) => { + return axios.patch(`/api/i18/${id}`, data); +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/api/menu.ts b/packages/toolkits/pro/template/tinyvue/src/api/menu.ts new file mode 100644 index 00000000..e5a25ad7 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/api/menu.ts @@ -0,0 +1,55 @@ +import axios from 'axios'; + +export interface ITreeNodeData { + // node-key='id' 设置节点的唯一标识 + id: number | string; + // 节点显示文本 + label: string; + // 子节点 + children?: ITreeNodeData[]; + // 链接 + url: string; + // 组件 + component: string; + // 图标 + customIcon: string; + // 类型 + menuType: string; + // 父节点 + parentId: number; + // 排序 + order: number; + // 国际化 + locale: string; +} + +export type CreateMenuDto = { + order: number; + menuType: string; + name: string; + path: string; + component: string; + icon: string; + locale: string; + parentId: number | null; +}; + +export function getAllMenu() { + return axios.get('/api/menu'); +} + +export function getRoleMenu(email: string) { + return axios.get(`/api/menu/role/${email}`); +} + +export function updateMenu(data: any) { + return axios.patch(`/api/menu`, data); +} + +export function deleteMenu(id: number, parentId: number) { + return axios.delete(`/api/menu?id=${id}&parentId=${parentId}`); +} + +export function createMenu(data: any) { + return axios.post(`/api/menu`, data); +} diff --git a/packages/toolkits/pro/template/tinyvue/src/api/permission.ts b/packages/toolkits/pro/template/tinyvue/src/api/permission.ts new file mode 100644 index 00000000..35203630 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/api/permission.ts @@ -0,0 +1,25 @@ +import axios from 'axios'; + +export type Permission = { + desc: string; + id: number; + name: string; +}; + +export function getAllPermission(page?: number, limit?: number, name?: string) { + return axios.get(`/api/permission`, { + params: { page, limit, name }, + }); +} + +export function updatePermission(data: any) { + return axios.patch(`/api/permission`, data); +} + +export function deletePermission(id: number) { + return axios.delete(`/api/permission/${id}`); +} + +export function createPermission(data: any) { + return axios.post(`/api/permission`, data); +} diff --git a/packages/toolkits/pro/template/tinyvue/src/api/profile.ts b/packages/toolkits/pro/template/tinyvue/src/api/profile.ts index 5feef072..bdf3aedf 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/profile.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/profile.ts @@ -2,5 +2,5 @@ import axios from 'axios'; // 获取detail表单的初始数据选项 export function getDetailData() { - return axios.get('/api/detail/getdata'); + return axios.get(`${import.meta.env.VITE_MOCK_SERVER_HOST}/api/detail/getdata`); } diff --git a/packages/toolkits/pro/template/tinyvue/src/api/role.ts b/packages/toolkits/pro/template/tinyvue/src/api/role.ts new file mode 100644 index 00000000..9ffb74fa --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/api/role.ts @@ -0,0 +1,45 @@ +import { IPaginationMeta } from '@/types/global'; +import axios from 'axios'; + +export type Role = { + id: number; + name: string; + permission: { + name: string; + desc: string; + id: number; + }[]; +}; +export type GetAllRoleDetailRet = { + roleInfo: { + meta: IPaginationMeta; + items: Role[]; + }; + menuTree: any[]; +}; + +export function getAllRole() { + return axios.get('/api/role'); +} + +export function getAllRoleDetail(page = 1, limit = 10, name?: string) { + return axios.get('/api/role/detail', { + params: { page, limit, name }, + }); +} + +export function updateRole(data: any) { + return axios.patch(`/api/role`, data); +} + +export function deleteRole(id: number) { + return axios.delete(`/api/role/${id}`); +} + +export function createRole(data: any) { + return axios.post(`/api/role`, data); +} + +export function getRoleInfo(id: number) { + return axios.get(`/api/role/info/${id}`); +} diff --git a/packages/toolkits/pro/template/tinyvue/src/api/user.ts b/packages/toolkits/pro/template/tinyvue/src/api/user.ts index e0806818..651b863d 100644 --- a/packages/toolkits/pro/template/tinyvue/src/api/user.ts +++ b/packages/toolkits/pro/template/tinyvue/src/api/user.ts @@ -1,9 +1,21 @@ import axios from 'axios'; import { UserInfo } from '@/store/modules/user/types'; +import { FilterType } from '@/types/global'; export interface LoginData { + email: string; + password: string; +} + +export interface LogoutData { + token: string | null; +} + +export interface RegisterData { username: string; + email: string; password: string; + roleIds: number[]; } export interface LoginDataMail { @@ -28,28 +40,66 @@ export interface UserData { } export function login(data: LoginData) { - return axios.post('/api/user/login', data); + return axios.post('/api/auth/login', data); } export function loginMail(data: LoginDataMail) { return axios.post('/api/mail/login', data); } -export function logout() { - return axios.post('/api/user/logout'); +export function logout(data: LogoutData) { + return axios.post('/api/auth/logout', data); +} + +// 获取全部用户 +export function getAllUser(page?: number, limit?: number, filter?: FilterType) { + const keys = Object.keys(filter ?? {}); + const params = new URLSearchParams(); + params.set('page', page.toString()); + params.set('limit', limit.toString()); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + const value = filter[key]; + if (value.type === 'enum') { + if (Array.isArray(value.value) && value.value.length) { + params.set(key, value.value.toString()); + } + } + if (value.type === 'input' && !Array.isArray(value.value)) { + let sql = `${value.value.relation === 'contains' ? '%' : ''}${value.value.text}${value.value.relation === 'startwith' || value.value.relation === 'contains' ? '%' : ''}`; + params.set(key, sql); + } + } + return axios.get(`/api/user?${params.toString()}`); } -export function getUserInfo() { - return axios.get(`/api/user/userInfo`); +// 获取单个用户 +export function getUserInfo(email?: string) { + return axios.get(`/api/user/info/${email ?? ''}`); } -export function updateUserInfo(data: UserInfo) { - return axios.put(`/api/user/userInfo`, data); +export function deleteUser(email: string) { + return axios.delete(`/api/user/${email}`); +} + +export function updateUserInfo(data: any) { + return axios.patch('/api/user/update', data); } export function getUserData(data?: UserData) { - return axios.post('/api/user/data', data); + return axios.post( + `${import.meta.env.VITE_MOCK_SERVER_HOST}/api/user/data`, + data, + ); +} + +export function registerUser(data: any) { + return axios.post('/api/user/reg', data); +} + +export function updatePwdAdmin(data: any) { + return axios.patch('/api/user/admin/updatePwd', data); } -export function registerUser(data: LoginData) { - return axios.post('/api/user/register', data); +export function updatePwdUser(data: any) { + return axios.patch('/api/user/updatePwd', data); } diff --git a/packages/toolkits/pro/template/tinyvue/src/assets/style/menu.less b/packages/toolkits/pro/template/tinyvue/src/assets/style/menu.less index 12ca6fc6..d2284054 100644 --- a/packages/toolkits/pro/template/tinyvue/src/assets/style/menu.less +++ b/packages/toolkits/pro/template/tinyvue/src/assets/style/menu.less @@ -27,9 +27,10 @@ .tree-node-name .tiny-svg ) { - fill: var(--ti-tree-menu-node-current-color); + fill: var(--ti-tree-menu-node-current-color); } + :deep(.tiny-collapse-item__header) { + color: var(--ti-common-color-text-highlight); background-color: var(--ti-common-color-bg-light-normal); - color: var(--ti-common-color-text-highlight); -} \ No newline at end of file +} diff --git a/packages/toolkits/pro/template/tinyvue/src/components/index.ts b/packages/toolkits/pro/template/tinyvue/src/components/index.ts index bafecf69..e2f246cd 100644 --- a/packages/toolkits/pro/template/tinyvue/src/components/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/components/index.ts @@ -1,8 +1,16 @@ import { App } from 'vue'; import Breadcrumb from './breadcrumb/index.vue'; +import TransitionFadeDownGroup from './transition/transition-fade-down-group.vue'; +import TransitionFadeDown from './transition/transition-fade-down.vue'; +import TransitionFadeSlideGroup from './transition/transition-fade-slide-group.vue'; +import TransitionFadeSlide from './transition/transition-fade-slide.vue'; export default { install(Vue: App) { Vue.component('Breadcrumb', Breadcrumb); + Vue.component('TransitionFadeDownGroup', TransitionFadeDownGroup); + Vue.component('TransitionFadeDown', TransitionFadeDown); + Vue.component('TransitionSlideGroup', TransitionFadeSlideGroup); + Vue.component('TransitionSlide', TransitionFadeSlide); }, }; diff --git a/packages/toolkits/pro/template/tinyvue/src/components/menu/index.vue b/packages/toolkits/pro/template/tinyvue/src/components/menu/index.vue index 8e166201..407e1f2c 100644 --- a/packages/toolkits/pro/template/tinyvue/src/components/menu/index.vue +++ b/packages/toolkits/pro/template/tinyvue/src/components/menu/index.vue @@ -2,18 +2,20 @@ +
+ + + + +
diff --git a/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-down.vue b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-down.vue new file mode 100644 index 00000000..f05e42d7 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-down.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide-group.vue b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide-group.vue new file mode 100644 index 00000000..c8cacebf --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide-group.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide.vue b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide.vue new file mode 100644 index 00000000..f49d8ec9 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/components/transition/transition-fade-slide.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/toolkits/pro/template/tinyvue/src/components/transition/utils.ts b/packages/toolkits/pro/template/tinyvue/src/components/transition/utils.ts new file mode 100644 index 00000000..8230d40d --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/components/transition/utils.ts @@ -0,0 +1,9 @@ +export const findIdx = (el: Element) => { + let idx = 0; + let cur = el; + while (cur) { + idx += 1; + cur = cur.previousElementSibling; + } + return idx; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/directive/permission/index.ts b/packages/toolkits/pro/template/tinyvue/src/directive/permission/index.ts index 4968e984..e53bdf26 100644 --- a/packages/toolkits/pro/template/tinyvue/src/directive/permission/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/directive/permission/index.ts @@ -1,30 +1,23 @@ -import { DirectiveBinding } from 'vue'; import { useUserStore } from '@/store'; -function checkPermission(el: HTMLElement, binding: DirectiveBinding) { +async function checkPermission(el: HTMLElement, binding: { value: string }) { const { value } = binding; + // // 获取role的permission const userStore = useUserStore(); - const { role } = userStore; - - if (Array.isArray(value)) { - if (value.length > 0) { - const permissionValues = value; - - const hasPermission = permissionValues.includes(role); - if (!hasPermission && el.parentNode) { - el.parentNode.removeChild(el); - } - } - } else { - throw new Error(`need roles! Like v-permission="['admin','user']"`); + const { rolePermission } = userStore; + const permissionList: string[] = rolePermission; + const hasPermission = + permissionList.includes(value) || permissionList.includes('*'); + if (!hasPermission) { + el.remove(); } } export default { - mounted(el: HTMLElement, binding: DirectiveBinding) { + mounted(el: HTMLElement, binding: any) { checkPermission(el, binding); }, - updated(el: HTMLElement, binding: DirectiveBinding) { + updated(el: HTMLElement, binding: any) { checkPermission(el, binding); }, }; diff --git a/packages/toolkits/pro/template/tinyvue/src/env.d.ts b/packages/toolkits/pro/template/tinyvue/src/env.d.ts index 1556eaa5..41f15fda 100644 --- a/packages/toolkits/pro/template/tinyvue/src/env.d.ts +++ b/packages/toolkits/pro/template/tinyvue/src/env.d.ts @@ -1,5 +1,7 @@ /// +export {}; + declare module '*.vue' { import { DefineComponent } from 'vue'; // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types @@ -24,3 +26,14 @@ declare module '@opentiny/vue-theme/theme-tool.js'; declare module '@opentiny/vue-theme/theme'; declare module 'echarts4'; declare module 'query-string'; +declare const BUILD_TOOLS: string; +declare interface NodeRequire { + context: any; +} + +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + $t: (key: string) => string; + $tm: (key: string) => [] | { [p: string]: any }; + } +} diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/useDeepClone.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/useDeepClone.ts new file mode 100644 index 00000000..d8b89de9 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/useDeepClone.ts @@ -0,0 +1,41 @@ +import { Ref, unref } from 'vue'; + +export const useDeepClone = < + T extends { + [x: string]: any; + }, +>( + value: T | Ref, +): T => { + const innerValue = unref(value); + if (value === null || value === undefined) { + return value; + } + if ( + typeof innerValue === 'boolean' || + typeof innerValue === 'string' || + typeof innerValue === 'number' + ) { + return innerValue; + } + const data = Object.create(null); + if (innerValue instanceof Array) { + const arr = []; + for (let i = 0; i < innerValue.length; i += 1) { + arr.push(useDeepClone(innerValue[i])); + } + return arr as unknown as T; + } + const entries = Object.entries(innerValue); + for (let i = 0; i < entries.length; i += 1) { + const [key, v] = entries[i]; + if (typeof v !== 'object') { + data[key] = v; + } + if (typeof v === 'function') { + data[key] = v; + } + data[key] = useDeepClone(v); + } + return data; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/useDisclosure.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/useDisclosure.ts new file mode 100644 index 00000000..69dbf736 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/useDisclosure.ts @@ -0,0 +1,16 @@ +import { ref } from 'vue'; + +export const useDisclosure = () => { + const open = ref(false); + const onClose = () => { + open.value = false; + }; + const onOpen = () => { + open.value = true; + }; + return { + open, + onClose, + onOpen, + }; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/useI18nMenu.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/useI18nMenu.ts new file mode 100644 index 00000000..eb91b0a2 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/useI18nMenu.ts @@ -0,0 +1,22 @@ +import type { ITreeNodeData } from '@/router/guard/menu'; +import { useDeepClone } from './useDeepClone'; + +export const useI18nMenu = ( + data: ITreeNodeData[], + t: (key: string) => string, +) => { + const menus: ITreeNodeData[] = useDeepClone(data); + const dfs = (menu: ITreeNodeData) => { + menu.oldLabel = menu.label; + menu.label = t(menu.locale).toString(); + for (let i = 0; i < menu.children.length; i += 1) { + const item = menu.children[i]; + dfs(item); + } + }; + for (let i = 0; i < menus.length; i += 1) { + const menu = menus[i]; + dfs(menu); + } + return menus; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/useMenuId.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/useMenuId.ts new file mode 100644 index 00000000..1a403428 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/useMenuId.ts @@ -0,0 +1,17 @@ +import type { ITreeNodeData } from '@/router/guard/menu'; + +export const useMenuId = (datas: ITreeNodeData[]) => { + const ids: any[] = []; + const dfs = (menu: ITreeNodeData) => { + ids.push(menu.id); + for (let i = 0; i < menu.children?.length; i += 1) { + const child = menu.children[i]; + dfs(child); + } + }; + for (let i = 0; i < datas.length; i += 1) { + const data = datas[i]; + dfs(data); + } + return ids; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/useTheme.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/useTheme.ts new file mode 100644 index 00000000..c5d93694 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/useTheme.ts @@ -0,0 +1,46 @@ +import { useAppStore } from '@/store'; +import { computed, onMounted, watch } from 'vue'; +import * as Themes from '@/components/theme/type'; +import TinyThemeTool from '@opentiny/vue-theme/theme-tool.js'; +import { tinySmbTheme } from '@opentiny/vue-theme/theme'; +import useThemes from './themes'; + +export const useTheme = (themeTool: typeof TinyThemeTool) => { + const { themelist, $patch } = useAppStore(); + const themeName = computed(() => + themelist.length + ? `${themelist[0].toUpperCase()}${themelist.slice(1).toLowerCase()}Theme` + : 'DefaultTheme', + ); + const { isDark } = useThemes(); + watch( + themeName, + () => { + themeTool.changeTheme( + themeName.value === 'DefaultTheme' + ? tinySmbTheme + : (Themes as any)[themeName.value], + ); + }, + { immediate: true }, + ); + onMounted(() => { + watch( + isDark, + () => { + if (isDark.value) { + document.body.style.filter = 'invert(0.9) hue-rotate(180deg)'; + } else { + document.body.style.filter = ''; + } + }, + { immediate: true }, + ); + }); + const toggleTheme = (name: string) => { + $patch({ + themelist: name, + }); + }; + return { toggleTheme }; +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/hooks/user.ts b/packages/toolkits/pro/template/tinyvue/src/hooks/user.ts index bf07cad3..e9eddefb 100644 --- a/packages/toolkits/pro/template/tinyvue/src/hooks/user.ts +++ b/packages/toolkits/pro/template/tinyvue/src/hooks/user.ts @@ -14,7 +14,7 @@ export default function useUser() { message: t('setting.loginout'), status: 'success', }); - router.push({ + await router.push({ name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login', query: { ...router.currentRoute.value.query, diff --git a/packages/toolkits/pro/template/tinyvue/src/layout/default-layout.vue b/packages/toolkits/pro/template/tinyvue/src/layout/default-layout.vue index 0228da58..6fefad12 100644 --- a/packages/toolkits/pro/template/tinyvue/src/layout/default-layout.vue +++ b/packages/toolkits/pro/template/tinyvue/src/layout/default-layout.vue @@ -11,11 +11,27 @@ + + + diff --git a/packages/toolkits/pro/template/tinyvue/src/layout/page-layout.vue b/packages/toolkits/pro/template/tinyvue/src/layout/page-layout.vue index 979a4b84..f0561bbe 100644 --- a/packages/toolkits/pro/template/tinyvue/src/layout/page-layout.vue +++ b/packages/toolkits/pro/template/tinyvue/src/layout/page-layout.vue @@ -1,9 +1,13 @@ - + diff --git a/packages/toolkits/pro/template/tinyvue/src/locale/en-US.ts b/packages/toolkits/pro/template/tinyvue/src/locale/en-US.ts index 723c8bc9..0da630d4 100644 --- a/packages/toolkits/pro/template/tinyvue/src/locale/en-US.ts +++ b/packages/toolkits/pro/template/tinyvue/src/locale/en-US.ts @@ -1,68 +1,11 @@ import localeLogin from '@/views/login/locale/en-US'; -import localeTheme from '@/components/theme/locale/en-US'; - -import localeSearchTable from '@/views/list/search-table/locale/en-US'; - -import localeStepForm from '@/views/form/step/locale/en-US'; -import localeBaseForm from '@/views/form/base/locale/en-US'; - -import localeDetailForm from '@/views/profile/detail/locale/en-US'; - -import localeSuccess from '@/views/result/success/locale/en-US'; -import localeError from '@/views/result/error/locale/en-US'; - -import locale403 from '@/views/exception/403/locale/en-US'; -import locale404 from '@/views/exception/404/locale/en-US'; -import locale500 from '@/views/exception/500/locale/en-US'; - -import localeUserInfo from '@/views/user/info/locale/en-US'; -import localeUserSetting from '@/views/user/setting/locale/en-US'; - -import localekanban from '@/views/board/locale/en-US'; - -import localeHello from '@/views/cloud/hello/locale/en-US'; - -import localeContracts from '@/views/cloud/contracts/locale/en-US'; - -import localeSettings from './en-US/settings'; - +import localeI18 from '@/views/locale/locale/en-US'; import localeHttpError from './en-US/httpError'; export default { - 'menu.board': 'Dashboard Page', - 'menu.home': 'Monitoring page', - 'menu.work': 'workbench', - 'menu.list': 'List', - 'menu.result': 'Result', - 'menu.exception': 'Exception', - 'menu.form': 'Form', - 'menu.profile': 'Profile', - 'menu.profile.detail': 'Basic details page', - 'menu.visualization': 'Data Visualization', - 'menu.user': 'User Center', - 'navbar.docs': 'Docs', - 'navbar.action.locale': 'Switch to English', - 'messageBox.switchRoles': 'Switch Roles', - 'messageBox.userCenter': 'User Center', - 'messageBox.userSettings': 'User Settings', - 'messageBox.logout': 'Logout', - 'menu.cloud': 'Cloud service capability', - ...localeTheme, - ...localeSettings, ...localeLogin, - ...localeSearchTable, - ...localeStepForm, - ...localeBaseForm, - ...localeSuccess, - ...localeError, - ...locale403, - ...locale404, - ...locale500, - ...localeUserInfo, - ...localeUserSetting, - ...localeDetailForm, - ...localekanban, - ...localeHello, - ...localeContracts, + ...localeI18, ...localeHttpError, + 'router.not-exists-valid-route': + 'Route encountered an exception, please contact the administrator', }; diff --git a/packages/toolkits/pro/template/tinyvue/src/locale/index.ts b/packages/toolkits/pro/template/tinyvue/src/locale/index.ts index 2c45caed..fe8beb71 100644 --- a/packages/toolkits/pro/template/tinyvue/src/locale/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/locale/index.ts @@ -1,4 +1,4 @@ -import { createI18n } from 'vue-i18n'; +import { createI18n, I18n } from 'vue-i18n'; import locale from '@opentiny/vue-locale'; // tiny-vue的国际化 import en from './en-US'; import cn from './zh-CN'; @@ -8,9 +8,19 @@ export const LOCALE_OPTIONS = [ { label: 'English', value: 'enUS' }, ]; +// eslint-disable-next-line no-underscore-dangle, import/no-mutable-exports +export let _i18: + | I18n + | I18n + | null = null; + const i18nmode = (option: any) => { option.legacy = false; - return createI18n(option); + _i18 = createI18n({ + ...option, + missingWarn: false, + }); + return _i18; }; export default (i18n: any) => diff --git a/packages/toolkits/pro/template/tinyvue/src/locale/zh-CN.ts b/packages/toolkits/pro/template/tinyvue/src/locale/zh-CN.ts index 84b1d13f..4896a189 100644 --- a/packages/toolkits/pro/template/tinyvue/src/locale/zh-CN.ts +++ b/packages/toolkits/pro/template/tinyvue/src/locale/zh-CN.ts @@ -1,68 +1,8 @@ import localeLogin from '@/views/login/locale/zh-CN'; -import localeTheme from '@/components/theme/locale/zh-CN'; - -import localeSearchTable from '@/views/list/search-table/locale/zh-CN'; - -import localeStepForm from '@/views/form/step/locale/zh-CN'; -import localeBaseForm from '@/views/form/base/locale/zh-CN'; - -import localeDetailForm from '@/views/profile/detail/locale/zh-CN'; - -import localeSuccess from '@/views/result/success/locale/zh-CN'; -import localeError from '@/views/result/error/locale/zh-CN'; - -import locale403 from '@/views/exception/403/locale/zh-CN'; -import locale404 from '@/views/exception/404/locale/zh-CN'; -import locale500 from '@/views/exception/500/locale/zh-CN'; - -import localeUserInfo from '@/views/user/info/locale/zh-CN'; -import localeUserSetting from '@/views/user/setting/locale/zh-CN'; - -import localekanban from '@/views/board/locale/zh-CN'; - -import localeHello from '@/views/cloud/hello/locale/zh-CN'; - -import localeContracts from '@/views/cloud/contracts/locale/zh-CN'; - -import localeSettings from './zh-CN/settings'; - import localeHttpError from './zh-CN/httpError'; export default { - 'menu.board': '看板', - 'menu.home': '监控页', - 'menu.work': '工作台', - 'menu.list': '列表页', - 'menu.result': '结果页', - 'menu.exception': '异常页', - 'menu.form': '表单页', - 'menu.profile': '详情页', - 'menu.profile.detail': '基础详情页', - 'menu.visualization': '数据可视化', - 'menu.user': '个人中心', - 'navbar.docs': '文档中心', - 'navbar.action.locale': '切换为中文', - 'messageBox.switchRoles': '切换角色', - 'messageBox.userCenter': '用户中心', - 'messageBox.userSettings': '用户设置', - 'messageBox.logout': '退出登录', - 'menu.cloud': '云服务能力展示', - ...localeTheme, - ...localeSettings, ...localeLogin, - ...localeSearchTable, - ...localeStepForm, - ...localeBaseForm, - ...localeSuccess, - ...localeError, - ...locale403, - ...locale404, - ...locale500, - ...localeUserInfo, - ...localeUserSetting, - ...localeDetailForm, - ...localekanban, - ...localeHello, - ...localeContracts, ...localeHttpError, + 'router.not-exists-valid-route': '路由出现异常,请联系管理员', }; diff --git a/packages/toolkits/pro/template/tinyvue/src/locales.json b/packages/toolkits/pro/template/tinyvue/src/locales.json new file mode 100644 index 00000000..ede18068 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/locales.json @@ -0,0 +1,1196 @@ +{ + "enUS": { + "en-US": "English", + "zh-CN": "中文", + "zh-TW": "中国台湾", + "hello": "Hello {name}", + "code": "en-US", + "yes": "Yes", + "no": "No", + "menu.board": "Dashboard Page", + "menu.home": "Monitoring page", + "menu.work": "workbench", + "menu.list": "List", + "menu.result": "Result", + "menu.exception": "Exception", + "menu.form": "Form", + "menu.profile": "Profile", + "menu.profile.detail": "Basic details page", + "menu.visualization": "Data Visualization", + "menu.menuPage": "Menu Page", + "menu.menuPage.second": "Second Page", + "menu.menuPage.third": "Menu Demo Page", + "menu.user": "User Center", + "menu.userManager": "User Manager", + "menu.userManager.info": "All User Info", + "menu.userManager.setting": "All User Setting", + "menu.userManager.useradd": "Add User", + "menu.permission": "Permission Manager", + "menu.permission.info": "All Permission Info", + "menu.permission.setting": "Permission Setting", + "menu.permission.permissionAdd": "Add Permission", + "menu.role": "Role Manager", + "menu.role.info": "All Role Info", + "menu.menu": "Menu Manager", + "menu.menu.info": "All Menu Info", + "navbar.docs": "Docs", + "navbar.action.locale": "Switch to English", + "messageBox.switchRoles": "Switch Roles", + "messageBox.userCenter": "User Center", + "messageBox.userSettings": "User Settings", + "messageBox.logout": "Logout", + "messageBox.updatePwd": "Update Password", + "menu.cloud": "Cloud service capability", + "menu.btn.confirm": "Submit", + "menu.i18n": "I18n Manage", + "theme.title.main": "Personalized configuration", + "theme.title.first": "theme", + "theme.title.default": "Default Theme", + "theme.title.honey": "Honey Theme", + "theme.title.violet": "Violet Theme", + "theme.title.deepness": "Deep Night Sky Theme", + "theme.title.deep": "Dark Theme", + "theme.title.light": "Light Theme", + "theme.title.customization": "Custom Themes", + "theme-title-recommend": "Recommended Topics", + "theme-text-default": "Science and technology, exploration, research, precision, tolerance", + "theme-text-honey": "Bright, sensual, warm, positive, energetic", + "theme-text-violet": "Elegant, romantic, gentle, mysterious, noble", + "theme-text-deepness": "Smooth, Neutral, Space, Strength, Hard", + "theme-text-dark": "Deep, decisive, brave, tenacious, yearning", + "settings.title": "Settings", + "settings.themeColor": "Theme Color", + "settings.content": "Content Setting", + "settings.search": "Search", + "settings.language": "Language", + "settings.navbar": "simple mode", + "settings.menuWidth": "Menu Width (px)", + "settings.navbar.alerts": "alerts", + "settings.navbar.help": "Help Center", + "settings.menu": "classic mode", + "settings.tabBar": "Tab Bar", + "settings.footer": "fashion mode", + "settings.colorWeek": "Theme Configuration", + "settings.alertContent": "After the configuration is only temporarily effective, if you want to really affect the project, click the \"Copy Settings\" button below and replace the configuration in settings.json.", + "settings.copySettings": "Copy Settings", + "settings.copySettings.message": "Copy succeeded, please paste to file src/settings.json.", + "settings.close": "Close", + "settings.color.tooltip": "10 gradient colors generated according to the theme color", + "setting.user.set": "User Settings", + "setting.loginout": "Logout succeeded", + "setting.copy": "Copying succeeded", + "setting.input.search": "Enter a keyword", + "setting.foot.title": "Produced by OpenTiny", + "setting.mode.navbar": "Collapse Header, Footer", + "setting.mode.menu": "Collapse menu", + "setting.mode.footer": "Collapse Footer", + "login.form.mode": "Account and password login", + "login.form.mail": "Email login", + "login.form.title": "Login to Tiny Pro", + "login.form.userName.errMsg": "Username cannot be empty", + "login.form.password.errMsg": "Password cannot be empty", + "login.form.mailName.errMsg": "The mailbox name cannot be empty", + "login.form.mailpassword.errMsg": "The email password cannot be empty", + "login.form.mailpassword2.errMsg": "Confirm password cannot be empty", + "login.form.login.errMsg": "Login error, refresh and try again", + "login.form.login.success": "welcome to use", + "login.form.userName.placeholder": "Username: admin", + "login.form.password.placeholder": "Password: admin", + "login.form.mailName.placeholder": "EmailName:123{'@'}example.com", + "login.form.mailpassword.placeholder": "Password:admin", + "login.form.registerMail.placeholder": "Register Email:", + "login.form.registerPassword.placeholder": "Registration password:", + "login.form.registerConfirmPassword.placeholder": "Confirm Password:", + "login.form.rememberPassword": "Remember password", + "login.form.forgetPassword": "Forgot password", + "login.form.registration": "Sign up", + "login.form.login": "login", + "login.form.register": "register", + "login.form.registerPass": "The verification is successful and the registration is successful", + "login.form.registerError": "Verification failed!", + "login.form.change": "Sign in with", + "login.form.mailInput": "Email:", + "login.form.passwordInput": "Password:", + "login.form.passwordConfirm": "Confirm Password:", + "login.form.checkUsername": "Letters, numbers, underscores, dashes, dots{'@'}Letters, numbers, dashes", + "login.form.checkPassword": "At least eight characters, including at least one uppercase letter, one lowercase letter, and one digit", + "login.form.confirmPassword": "Inconsistent passwords", + "login.banner.slogan1": "Out-of-the-box high-quality template", + "login.banner.subSlogan1": "Rich page templates, covering most typical business scenarios", + "login.banner.slogan2": "Built-in solutions to common problems", + "login.banner.subSlogan2": "Internationalization, routing configuration, state management everything", + "login.banner.slogan3": "Access visualization enhancement tool AUX", + "login.banner.subSlogan3": "Realize flexible block development", + "login.icon.language": "language", + "login.tip.info": "User name: admin; password: admin", + "login.tip.mail": "User name: admin{'@'}example.com; password: admin", + "login.tip.right": "Enter the correct user name and password", + "login.main.text": "TinyPro Mid-Back-End Front-End Solution", + "menu.list.searchTable": "Search Table", + "searchTable.form.number": "Set Number", + "searchTable.form.number.placeholder": "Please enter Set Number", + "searchTable.form.name": "Set Name", + "searchTable.form.name.placeholder": "Please enter Set Name", + "searchTable.form.contentType": "Content Type", + "searchTable.form.contentType.img": "image-text", + "searchTable.form.contentType.horizontalVideo": "Horizontal short video", + "searchTable.form.contentType.verticalVideo": "Vertical short video", + "searchTable.form.filterType": "Filter Type", + "searchTable.form.filterType.artificial": "artificial", + "searchTable.form.filterType.rules": "Rules", + "searchTable.form.createdTime": "Create Date", + "searchTable.form.status": "Status", + "searchTable.form.status.online": "Online", + "searchTable.form.status.offline": "Offline", + "searchTable.form.status.doing": "Ongoing", + "searchTable.form.search": "Search", + "searchTable.form.reset": "Reset", + "searchTable.form.selectDefault": "All", + "searchTable.operation.create": "Create", + "searchTable.operation.import": "Export", + "searchTable.operation.download": "Download", + "searchTable.form.collapse": "Collapse", + "searchTable.form.extend": "Extend", + "searchTable.form.input": "Please enter", + "searchTable.form.create": "Creating a Topic", + "searchTable.columns.number": "ID", + "searchTable.columns.name": "Set Name", + "searchTable.columns.department": "Department", + "searchTable.columns.filterType": "Department Level", + "searchTable.columns.count": "Count", + "searchTable.columns.workname": "Workbench", + "searchTable.columns.enablement": "Enablement", + "searchTable.columns.type": "Person Type", + "searchTable.columns.study": "Institute", + "searchTable.columns.role": "Role", + "searchTable.columns.updatesperson": "Updates Person", + "searchTable.columns.createdTime": "CreatedTime", + "searchTable.columns.status": "Status", + "searchTable.columns.operations": "Operations", + "searchTable.columns.operations.view": "View", + "searchTable.columns.operations.delete": "Delete", + "searchTable.collapse.restores": "restores", + "searchTable.collapse.full": "Full", + "menu.form.step": "Step Form", + "stepForm.button.submit": "Create", + "stepForm.button.cancel": "Cancel", + "stepForm.button.restore": "Restores", + "stepForm.probation.day": "Day", + "stepForm.coaching.process": "Coaching Process", + "stepForm.start.date": "Labor Contract Start Date", + "stepForm.end.date": "Labor Contract End Date", + "stepForm.probation.period": "Probation Period", + "stepForm.probation.start": "Trial Start and End Date", + "stepForm.recruitment.type": "Recruitment Type", + "stepForm.recruitment.position": "Position", + "stepForm.recruitment.department": "Department", + "stepForm.start.coaching": "Start coaching", + "stepForm.immediate.supervisor": "Enter the mentor immediate supervisor", + "stepForm.overall.goals": "Set overall goals", + "stepForm.overall.summary": "Submit the overall summary", + "stepForm.overall.end": "End", + "stepForm.collapse.base": "Coaching Basic Information", + "stepForm.collapse.supervisor": "Entry Supervisor", + "stepForm.collapse.goals": "Set overall goals", + "stepForm.collapse.summary": "Submit the overall summary", + "stepForm.coach.position": "Cultivating Positions", + "stepForm.coach.culture": "Training Department", + "stepForm.coach.mentor": "Mentor", + "stepForm.coach.startTime": "Actual Coaching Start Date", + "stepForm.coach.endTime": "Actual Coaching End Date", + "stepForm.dire.supervisor": "Mentor Supervisor", + "stepForm.dire.remarks": "Mentor Information Remarks", + "stepForm.dire.startTime": "Start Coaching Date", + "stepForm.dire.endTime": "Coaching End Date", + "stepForm.target.list": "Target List", + "stepForm.target.sure": "Set goals", + "stepForm.sum.self": "Self-summarization", + "stepForm.error.target": "At least one item exists on the right", + "stepForm.head.admin": "User name", + "menu.form.base": "Base Form", + "baseForm.form.label.no": "no", + "baseForm.form.label.yes": "yes", + "baseForm.form.label.placeholder": "Please select", + "baseForm.form.label.frequencyone": "By Month", + "baseForm.form.label.frequencytwo": "By Week", + "baseForm.form.label.frequencythree": "By biweekly", + "baseForm.form.label.frequencyfour": "By Quarter", + "baseForm.form.label.personone": "Local employees", + "baseForm.form.label.persontwo": "Non-Employee", + "baseForm.form.label.personthree": "Chinese employees", + "baseForm.form.label.projectone": "Training for new employees of the manufacturing department", + "baseForm.form.label.projecttwo": "On-boarding coaching for new employees", + "baseForm.form.label.projectthree": "UI Automation Test Coaching Project", + "baseForm.form.label.people": "Applicable Populations", + "baseForm.form.label.rank": "Job Level", + "baseForm.form.label.type": "Project Type", + "baseForm.form.label.business": "Service attribute", + "baseForm.form.label.Objectives": "Overall objective", + "baseForm.form.label.culture": "Training Department", + "baseForm.form.label.develop": "develop", + "baseForm.form.label.developone": "Trainees", + "baseForm.form.label.developtwo": "Mentor", + "baseForm.form.label.effective": "Effective Condition", + "baseForm.form.label.effectiveone": "Effective without approval", + "baseForm.form.label.effectivetwo": "Mentor Approval", + "baseForm.form.label.effectivethree": "Immediate supervisor approval", + "baseForm.form.label.effectivefour": "Mentors and immediate supervisors approve the application", + "baseForm.form.label.plan": "Phase Plan", + "baseForm.form.label.confirm": "Whether to develop", + "baseForm.form.label.frequency": "Formulation frequency", + "baseForm.form.label.role": "Goal Setting Role", + "baseForm.form.label.roleone": "Trainees", + "baseForm.form.label.roletwo": "Mentor", + "baseForm.form.label.condition": "Target Effective Condition", + "baseForm.form.label.conditionone": "Effective without approval", + "baseForm.form.label.conditiontwo": "Mentor Approval", + "baseForm.form.label.conditionthree": "Immediate supervisor approval", + "baseForm.form.label.conditionfour": "Mentors and immediate supervisors approve the application", + "baseForm.form.label.staged": "Phase Evaluation", + "baseForm.form.label.stagedone": "Only mentor evaluation is required", + "baseForm.form.label.stagedtwo": "Only immediate supervisor evaluation is required", + "baseForm.form.label.stagedthree": "Need to be evaluated by the mentor and immediate supervisor", + "baseForm.form.label.wholeconfirm": "Whether to develop", + "baseForm.form.label.evaluation": "Overall evaluation", + "baseForm.form.label.evaluationyes": "Mentors and immediate supervisors are required for evaluation", + "baseForm.form.label.evaluationno": "No mentor is required, and the immediate supervisor evaluates it", + "baseForm.form.label.mentortitle": "Mentor Selection", + "baseForm.form.label.mentortip": "Only mentors with valid qualifications can be selected from the mentor resource pool. If you do not select a mentor from the mentor resource pool, the basic qualifications of the mentor will not be verified", + "baseForm.form.label.mentor": "Select Mentor Only from Mentor Resource Pool", + "baseForm.form.label.remindertitle": "Reminder of coaching communication records", + "baseForm.form.label.reminder": "Require Reminder", + "baseForm.form.submit": "Submit", + "baseForm.form.submit.success": "Form submitted successfully", + "baseForm.form.cancel": "Cancel", + "baseForm.form.submit.error": "Please complete the required items first", + "baseForm.form.record": "Version Record", + "baseForm.form.project": "Project Type", + "baseForm.form.get.error": "Failed to obtain data", + "menu.result.success": "Success", + "success.result.title": "The submission result page displays the processing results of a series of operation tasks.", + "menu.result.messageSuccess": "The coaching process is submitted successfully", + "menu.btn.submit": "Start", + "menu.btn.cancel": "Cancel", + "menu.line.process": "Current progress", + "menu.result.messageEnd": "The coaching process has been submitted", + "menu.result.error": "Error", + "error.result.title": "The submission result page displays the processing results of a series of operation tasks", + "menu.result.messageError": "Failed to submit the coaching process", + "error.result.home": "Back", + "menu.exception.403": "403", + "exception.result.403.description": "Access to this resource on the server is denied.", + "exception.result.403.back": "Back", + "exception.result.permissions.403": "Contact the administrator to apply for the permission.。", + "menu.exception.404": "404", + "exception.result.404.description": "Whoops, this page is gone.", + "exception.result.404.retry": "Retry", + "exception.result.404.back": "Back", + "exception.result.permissions.404": "Check the network connection and try to refresh the page.", + "menu.exception.500": "500", + "exception.result.500.description": "Internal server error", + "exception.result.500.back": "Back", + "exception.result.permissions.500": "Check the network connection and try to refresh the page.", + "menu.user.info": "User Center", + "userInfo.tab.one": "My plan", + "userInfo.tab.two": "My mission", + "userInfo.filter.sort": "Sort by Time", + "userInfo.filter.startTime": "Start Date", + "userInfo.filter.endTime": "End Date", + "userInfo.end.positiveOrder": "By end time in positive order", + "userInfo.end.reverseOrder": "In reverse order by end time", + "userInfo.start.positiveOrder": "Start time in positive order", + "userInfo.start.reverseOrder": "Start time in reverse order", + "userInfo.btn.search": "Search", + "userInfo.btn.reset": "Reset", + "userInfo.status.status": "Status", + "userInfo.status.optionA": "Completed", + "userInfo.status.optionB": "Overdue", + "userInfo.status.optionC": "About to expire", + "userInfo.status.optionD": "Unfinished", + "userInfo.type.type": "Type", + "userInfo.type.optionA": "Organizational arrangements", + "userInfo.type.optionB": "Phase Plan", + "userInfo.type.optionC": "autonomous learning", + "userInfo.table.columnA": "Program Name", + "userInfo.table.columnB": "Completion Time", + "userInfo.table.columnC": "Status", + "userInfo.table.columnD": "Type", + "userInfo.week.1": "2 weeks onboarding", + "userInfo.month.1": "1 month onboarding", + "userInfo.month.2": "2 month onboarding", + "userInfo.month.3": "3 month onboarding", + "userInfo.month.4": "4 month onboarding", + "userInfo.month.5": "5 month onboarding", + "userInfo.month.6": "6 month onboarding", + "userInfo.month.7": "7 month onboarding", + "userInfo.month.8": "8 month onboarding", + "userInfo.month.9": "9 month onboarding", + "userInfo.month.10": "10 month onboarding", + "userInfo.month.11": "11 month onboarding", + "userInfo.month.12": "12 month onboarding", + "userInfo.month.13": "13 month onboarding", + "userInfo.month.14": "14 month onboarding", + "userInfo.month.15": "15 month onboarding", + "userInfo.month.16": "16 month onboarding", + "userInfo.month.17": "17 month onboarding", + "userInfo.time.message": "The end time is earlier than the start time", + "userInfo.filter.all": "Please complete all current filters", + "menu.user.setting": "User Setting", + "userSetting.cancel": "Cancel", + "userSetting.reset": "Reset", + "userSetting.department": "Department:", + "userSetting.position": "Position:", + "userSetting.type": "Recruitment Type:", + "userSetting.date": "Trial Start and End Date:", + "userSetting.during": "Probation Period:", + "userSetting.startTime": "Labor Contract Start Date:", + "userSetting.endTime": "Labor Contract End Date:", + "userSetting.first": "Start Time", + "userSetting.last": "End Time", + "menu.plan.department": "Training Department", + "menu.plan.resource": "Human Resource Mgmt Dept", + "menu.plan.job": "Job Level", + "menu.plan.person": "Person Type", + "menu.plan.attribute": "Service attribute", + "menu.plan.develop": "Whether to develop", + "menu.plan.yes": "yes", + "menu.plan.no": "no", + "menu.plan.role": "Develop Roles", + "menu.plan.mentor": "Mentor", + "menu.plan.condition": "Effective Condition", + "menu.plan.approval": "Immediate supervisor approval", + "menu.plan.frequency": "Formulation frequency", + "menu.plan.month": "By Month", + "menu.plan.goal": "Goal Setting Role", + "menu.plan.trainees": "Trainees", + "menu.plan.teacher": "Mentor Approval", + "menu.plan.phase": "Phase Evaluation", + "menu.plan.evaluation": "Need to be evaluated by mentors and immediate supervisors", + "menu.plan.whole": "Overall evaluation", + "menu.plan.pool": "Select Mentor Only from Mentor Resource Pool", + "menu.plan.time": "Update Time", + "menu.plan.version": "Version number", + "menu.plan.operation": "Operation", + "menu.plan.updated": "Updated by", + "work.mock.employees": "Transferred employees", + "work.mock.onboard": "New employee onboarding", + "work.mock.Test": "Test coaching", + "work.mock.week1": "Zero promotion practice (1 weeks)", + "work.mock.week2": "Zero promotion practice (2 weeks)", + "work.mock.week3": "Zero promotion practice (3 weeks)", + "work.mock.network": "Network Reality", + "work.mock.centralized": "Centralized training for new employees", + "work.mock.hardware": "Hardware Installation Practice", + "work.index.learn": "Learning Planning", + "work.index.coach": "Learning coaching", + "work.index.formalization": "Learning Formalization", + "work.index.practiced": "Learning practiced", + "work.index.train": "Centralized training", + "work.index.Inquiry": "Life little helper", + "work.index.Home": "New Employee Home", + "work.index.Guide": "Operation Guide", + "work.index.plans": "Number of plans", + "work.index.Unfinished": "Unfinished", + "work.index.beOverdue": "To Be Overdue", + "work.index.Overdue": "Overdue", + "work.index.trainees": "Number of trainees to start coaching", + "work.index.coachNum": "Number of trainees in coaching", + "work.index.allocated": "Number of trainees to be allocated", + "work.index.start": "Number of trainees to start practice", + "work.index.practice": "Number of trainees in practice", + "work.index.unpark": "Waiting for Start-up to Form", + "work.index.entered": "Evaluation result to be entered", + "work.index.approved": "Evaluation result to be approved", + "work.index.put": "Number of trainees in practice", + "work.index.assign": "Number of trainees to be allocated", + "work.index.prepare": "Prepare for class opening", + "work.index.open": "Open a middle class", + "work.index.classes": "Number of classes to be accepted", + "work.index.policy": "policy", + "work.index.Period": "Probation Period and Development Policy Process for New Employees", + "work.index.Hotline": "Hotline", + "work.index.service": "All kinds of practical hotline service", + "work.index.Attendance": "Attendance", + "work.index.FAQs": "Attendance System and FAQs", + "work.index.Payroll": "Payroll", + "work.index.Tax": "Payroll Tax Q&A", + "work.index.Brave": "Brave New World Landing Program", + "work.index.Growth": "100-day Growth Guide for New Employees", + "work.index.Termbase": "Termbase", + "work.index.lingo": "The latest and hottest terms to help you understand the lingo", + "work.index.Library": "Document Library and Community", + "work.index.domain": "Knowledge document library of the business domain", + "work.index.platform": "Online learning platform", + "work.index.learning": "Online learning", + "work.index.Operation": "New Employee Home Operation Guide", + "work.index.Numbers": "Number", + "work.index.Person": "Person", + "work.index.net": "Net", + "work.index.netonline": "Online consultation", + "home.main.one": "first screen", + "home.main.up": "Page Onload", + "home.main.down": "Sampling PV", + "home.main.day": "yesterday", + "home.curve.trend": "Performance Trends", + "home.curve.play": "Visible on the first screen", + "home.curve.page": "Page Onload", + "home.falls.line": "Load Waterfall Flow", + "home.falls.tcp": "TCP Link", + "home.falls.ssl": "SSL Link", + "home.round.title": "Network speed distribution", + "home.round.unknow": "Unknown", + "home.roundtable.index": "index", + "home.roundtable.space": "Network speed", + "home.roundtable.pv": "Sampling PV (percentage)", + "home.roundtable.play": "Visible on the first screen", + "home.roundtable.page": "Page Onload", + "home.region.title": "Geographical distribution", + "menu.cloud.hello": "Hello World", + "menu.cloud.contracts": "Contract Management", + "menu.cloud.create": "Create Contract", + "menu.cloud.edit": "Edit Contract", + "menu.cloud.del": "Delete Contract", + "menu.cloud.name": "Project Name", + "menu.cloud.id": "Contract No", + "menu.cloud.customer": "Customer Name", + "menu.cloud.description": "Description", + "menu.cloud.updatedAt": "Creation Time", + "menu.cloud.editOpa": "Edits", + "menu.cloud.editDel": "Delete", + "menu.cloud.registerErro": "The project name does not meet the verification rules", + "menu.cloud.sure": "OK", + "menu.cloud.cancel": "Cancel", + "menu.cloud.tip": "The value can contain 3 to 255 characters, including Chinese characters, digits, hyphens (-), underscores (_), dots (.), slashes (/), parentheses (:) and colons (:) in Chinese and English formats, and periods (). The value can start with only English, Chinese characters, and digits.", + "menu.cloud.askDel": "Are you sure you want to delete the following", + "menu.cloud.askContracts": "Contract", + "menu.cloud.askInput": "Input", + "menu.cloud.askSure": "confirm", + "menu.cloud.verification": "Verification failed", + "menu.cloud.editpass": "If the verification is successful, the modification is successful", + "menu.cloud.delpass": "Deleted successfully", + "menu.contracts.name": "The contract name is:", + "http.error.TokenExpire": "Login expired, please log in again", + "http.error.UserNotFound": "user does not exist", + "http.error.UserAlreadyExist": "User already exists", + "http.error.InvalidParameter": "Invalid request parameter", + "http.error.InternalError": "Internal error", + "http.error.ErrorPassword": "Account or password error", + "menu.allUser.info": "All User Info", + "userInfo.table.id": "ID", + "userInfo.table.name": "Name", + "userInfo.table.email": "Email", + "userInfo.table.department": "Department", + "userInfo.table.employeeType": "EmployeeType", + "userInfo.table.job": "Job", + "userInfo.table.probation": "Probation", + "userInfo.table.probationStart": "ProbationStart", + "userInfo.table.probationEnd": "ProbationEnd", + "userInfo.table.probationDuration": "ProbationDuration", + "userInfo.table.protocol": "Protocol", + "userInfo.table.protocolStart": "ProtocolStart", + "userInfo.table.protocolEnd": "ProtocolEnd", + "userInfo.table.address": "Address", + "userInfo.table.status": "Status", + "userInfo.table.createTime": "CreateTime", + "userInfo.table.updateTime": "UpdateTime", + "userInfo.table.operations": "Operation", + "userInfo.table.operations.update": "Update", + "userInfo.table.operations.delete": "Delete", + "userInfo.table.operations.pwdUpdate": "Password", + "userInfo.day": "Day", + "userInfo.modal.title.pwdUpdate": "Update Password", + "userInfo.modal.input.oldPassword": "Old Password", + "userInfo.modal.input.newPassword": "New Password", + "userInfo.modal.input.confirmNewPassword": "Confirm New Password", + "userInfo.modal.message.error": "Confirm New Password Error", + "userInfo.modal.message.notNull": "Password Is Not Null", + "menu.allUser.setting": "User Setting", + "userSetting.name": "UserName", + "userSetting.address": "Address", + "userSetting.status": "Status", + "userInfo.modal.title.add": "Add User", + "userInfo.modal.title.update": "Update User", + "menu.allUser.useradd": "User Add", + "userAdd.cancel": "Cancel", + "userAdd.save": "Save", + "userAdd.email": "Email", + "userAdd.password": "Password", + "userAdd.department": "Department:", + "userAdd.position": "Position:", + "userAdd.type": "Recruitment Type:", + "userAdd.date": "Trial Start and End Date:", + "userAdd.during": "Probation Period:", + "userAdd.startTime": "Labor Contract Start Date:", + "userAdd.endTime": "Labor Contract End Date:", + "userAdd.first": "Start Time", + "userAdd.last": "End Time", + "userAdd.name": "UserName", + "userAdd.address": "Address", + "userAdd.status": "Status", + "menu.allPermission.info": "Permission", + "permissionInfo.table.id": "ID", + "permissionInfo.table.name": "Name", + "permissionInfo.table.desc": "Desc", + "permissionInfo.table.operations": "Operation", + "permissionInfo.table.operations.update": "Update", + "permissionInfo.table.operations.delete": "Delete", + "permissionInfo.modal.title.update": "Update Permission", + "permissionInfo.modal.title.add": "Add Permission", + "permissionInfo.modal.input.permission": "Permission", + "permissionInfo.modal.input.name": "Name", + "permissionInfo.modal.input.id": "ID", + "permissionInfo.modal.message.error": "Error", + "permissionInfo.modal.message.notNull": "Not Null", + "menu.allRole.info": "All Role Info", + "roleInfo.table.id": "ID", + "roleInfo.table.name": "Name", + "roleInfo.table.desc": "Desc", + "roleInfo.table.menu": "Menu", + "roleInfo.table.operations": "Operation", + "roleInfo.table.operations.update": "Update", + "roleInfo.table.operations.delete": "Delete", + "roleInfo.modal.title.update": "Update Role", + "roleInfo.modal.title.add": "Add Role", + "roleInfo.modal.input.id": "ID", + "roleInfo.modal.input.name": "Name", + "roleInfo.modal.input.desc": "Desc", + "roleInfo.modal.input.menu": "Menu", + "roleInfo.modal.message.error": "Error", + "roleInfo.modal.message.notNull": "Not Null", + "roleInfo.permissionTable.id": "ID", + "roleInfo.permissionTable.name": "Name", + "roleInfo.permissionTable.desc": "Description", + "roleInfo.menuUpdate.confirm": "Confirm", + "roleInfo.menuUpdate.cancel": "Cancel", + "menu.allMenu.info": "All Menu Info", + "menuInfo.table.id": "ID", + "menuInfo.table.name": "Name", + "menuInfo.table.order": "Order", + "menuInfo.table.parentId": "ParentID", + "menuInfo.table.menuType": "MenuType", + "menuInfo.table.icon": "Icon", + "menuInfo.table.component": "Component", + "menuInfo.table.path": "Path", + "menuInfo.table.locale": "Locale", + "menuInfo.table.operations": "Operation", + "menuInfo.table.operations.info": "Detail", + "menuInfo.table.operations.update": "Update", + "menuInfo.table.operations.delete": "Delete", + "menuInfo.modal.title.info": "Menu Detail", + "menuInfo.modal.title.update": "Update Menu", + "menuInfo.modal.title.add": "Add Menu", + "menuInfo.modal.message.error": "ParentId is not as same as id", + "menuInfo.modal.message.notNull": "Not Null", + "menu.add.demo": "Menu Demo Page", + "exception.result.demo.description": "This is a new menu demo page!", + "locale.add.btn": "Add Record", + "locale.add.title": "Add Record", + "locale.add.key": "Key", + "locale.add.content": "Content", + "locale.add.lang": "Language", + "lang.add.title": "Name", + "lang.add.btn": "Confirm", + "lang.manage.btn": "Mange Language", + "locale.add.lang.btn": "Add Language", + "lang.manage.title": "Mange Language", + "lang.manage.remove": "Remove", + "locale.remove": "Remove" + }, + "zhCN": { + "en-US": "English", + "zh-CN": "中文", + "zh-TW": "中国台湾", + "hello": "你好 {name}", + "code": "zh-CN", + "yes": "是", + "no": "否", + "menu.board": "看板", + "menu.home": "监控页", + "menu.work": "工作台", + "menu.list": "列表页", + "menu.result": "结果页", + "menu.exception": "异常页", + "menu.form": "表单页", + "menu.profile": "详情页", + "menu.profile.detail": "基础详情页", + "menu.visualization": "数据可视化", + "menu.menuPage": "菜单页", + "menu.menuPage.second": "二级菜单", + "menu.menuPage.third": "菜单demo页", + "menu.user": "个人中心", + "menu.userManager": "用户管理", + "menu.userManager.info": "查看用户", + "menu.userManager.setting": "修改信息", + "menu.userManager.useradd": "添加用户", + "menu.permission": "权限管理", + "menu.permission.info": "查看权限", + "menu.permission.setting": "修改权限", + "menu.permission.permissionAdd": "添加权限", + "menu.role": "角色管理", + "menu.role.info": "查看角色", + "menu.menu": "菜单管理", + "menu.menu.info": "查看菜单", + "navbar.docs": "文档中心", + "navbar.action.locale": "切换为中文", + "messageBox.switchRoles": "切换角色", + "messageBox.userCenter": "用户中心", + "messageBox.userSettings": "用户设置", + "messageBox.logout": "退出登录", + "messageBox.updatePwd": "修改密码", + "menu.cloud": "云服务能力展示", + "menu.btn.confirm": "确认", + "menu.i18n": "国际化管理", + "theme.title.main": "个性化配置", + "theme.title.first": "主题", + "theme.title.default": "默认主题", + "theme.title.honey": "蜜糖主题", + "theme.title.violet": "紫罗兰主题", + "theme.title.deepness": "深邃夜空主题", + "theme.title.deep": "深色主题", + "theme.title.light": "浅色主题", + "theme.title.customization": "自定义主题", + "theme-title-recommend": "推荐主题", + "theme-text-default": "科技、探索、钻研、精尖、包容", + "theme-text-honey": "明快、感性、温暖、积极、活力", + "theme-text-violet": "优雅、浪漫、温柔、神秘、高贵", + "theme-text-deepness": "平稳、中性、空间、力量、坚硬", + "theme-text-dark": "深沉、果断、勇敢、坚韧、向往", + "settings.title": "页面配置", + "settings.themeColor": "主题色", + "settings.content": "内容区域", + "settings.search": "搜索", + "settings.language": "语言", + "settings.navbar": "简约模式", + "settings.menuWidth": "菜单宽度 (px)", + "settings.navbar.alerts": "消息通知", + "settings.navbar.help": "帮助中心", + "settings.menu": "经典模式", + "settings.tabBar": "多页签", + "settings.footer": "时尚模式", + "settings.colorWeek": "主题配置", + "settings.alertContent": "配置之后仅是临时生效,要想真正作用于项目,点击下方的 \"复制配置\" 按钮,将配置替换到 settings.json 中即可。", + "settings.copySettings": "复制配置", + "settings.copySettings.message": "复制成功,请粘贴到 src/settings.json 文件中", + "settings.close": "关闭", + "settings.color.tooltip": "根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)", + "setting.user.set": "用户设置", + "setting.loginout": "登出成功", + "setting.copy": "复制成功", + "setting.input.search": "请输入关键词", + "setting.foot.title": "OpenTiny 出品", + "setting.mode.navbar": "收起页头,页尾", + "setting.mode.menu": "收起菜单", + "setting.mode.footer": "收起页脚", + "login.form.mode": "账号密码登录", + "login.form.mail": "邮箱登录", + "login.form.title": "登录 Tiny Pro", + "login.form.userName.errMsg": "用户名不能为空", + "login.form.password.errMsg": "密码不能为空", + "login.form.mailName.errMsg": "邮箱名不能为空", + "login.form.mailpassword.errMsg": "邮箱密码不能为空", + "login.form.mailpassword2.errMsg": "确认密码不能为空", + "login.form.login.errMsg": "登录出错,轻刷新重试", + "login.form.login.success": "欢迎使用", + "login.form.userName.placeholder": "用户名:admin", + "login.form.password.placeholder": "密码:admin", + "login.form.mailName.placeholder": "邮箱名:123{'@'}example.com", + "login.form.mailpassword.placeholder": "密码:admin", + "login.form.registerMail.placeholder": "注册邮箱:", + "login.form.registerPassword.placeholder": "注册密码:", + "login.form.registerConfirmPassword.placeholder": "确认密码:", + "login.form.rememberPassword": "记住密码", + "login.form.forgetPassword": "忘记密码", + "login.form.registration": "注册账户", + "login.form.login": "登录", + "login.form.register": "注册", + "login.form.registerPass": "校验通过,注册成功", + "login.form.registerError": "校验不通过!", + "login.form.change": "使用已有账户登录", + "login.form.mailInput": "邮箱:", + "login.form.passwordInput": "密码:", + "login.form.passwordConfirm": "确认密码:", + "login.form.checkUsername": "字母、数字、下划线、短线、点号{'@'}字母、数字、短线", + "login.form.checkPassword": "最少八个字符,至少包含一个大写字母,一个小写字母和一个数字", + "login.form.confirmPassword": "密码输入不一致", + "login.banner.slogan1": "开箱即用的高质量模板", + "login.banner.subSlogan1": "丰富的的页面模板,覆盖大多数典型业务场景", + "login.banner.slogan2": "内置了常见问题的解决方案", + "login.banner.subSlogan2": "国际化,路由配置,状态管理应有尽有", + "login.banner.slogan3": "接入可视化增强工具AUX", + "login.banner.subSlogan3": "实现灵活的区块式开发", + "login.icon.language": "语言", + "login.tip.info": "用户名:admin,密码 admin", + "login.tip.mail": "用户名:admin{'@'}no-reply.com,密码 admin", + "login.tip.right": "请输入正确的用户名密码", + "login.main.text": "TinyPro 中后台前端解决方案", + "menu.list.searchTable": "查询表格", + "searchTable.form.number": "集合编号", + "searchTable.form.number.placeholder": "请输入集合编号", + "searchTable.form.name": "集合名称", + "searchTable.form.name.placeholder": "请输入集合名称", + "searchTable.form.contentType": "内容体裁", + "searchTable.form.contentType.img": "图文", + "searchTable.form.contentType.horizontalVideo": "横版短视频", + "searchTable.form.contentType.verticalVideo": "竖版小视频", + "searchTable.form.filterType": "筛选方式", + "searchTable.form.filterType.artificial": "人工筛选", + "searchTable.form.filterType.rules": "规则筛选", + "searchTable.form.createdTime": "创建时间", + "searchTable.form.status": "状态", + "searchTable.form.status.online": "已上线", + "searchTable.form.status.offline": "已下线", + "searchTable.form.status.doing": "进行中", + "searchTable.form.search": "查询", + "searchTable.form.reset": "重置", + "searchTable.form.selectDefault": "全部", + "searchTable.operation.create": "新建", + "searchTable.operation.import": "批量导出", + "searchTable.operation.download": "下载", + "searchTable.form.collapse": "收起", + "searchTable.form.extend": "展开", + "searchTable.form.input": "请输入", + "searchTable.form.create": "创建主题", + "searchTable.columns.number": "工号", + "searchTable.columns.name": "姓名", + "searchTable.columns.department": "部门", + "searchTable.columns.filterType": "部门层级", + "searchTable.columns.count": "内容量", + "searchTable.columns.workname": "工作台名称", + "searchTable.columns.enablement": "赋能项目", + "searchTable.columns.type": "人员类型", + "searchTable.columns.study": "研究所", + "searchTable.columns.role": "角色", + "searchTable.columns.updatesperson": "最后更新人", + "searchTable.columns.createdTime": "创建时间", + "searchTable.columns.status": "状态", + "searchTable.columns.operations": "操作", + "searchTable.columns.operations.view": "查看", + "searchTable.columns.operations.delete": "删除", + "searchTable.collapse.restores": "还原", + "searchTable.collapse.full": "全屏", + "menu.form.step": "分步表单", + "stepForm.button.submit": "创建", + "stepForm.button.cancel": "取消", + "stepForm.button.restore": "重置", + "stepForm.probation.day": "天", + "stepForm.coaching.process": "辅导流程", + "stepForm.start.date": "劳动合同开始日期", + "stepForm.end.date": "劳动合同结束日期", + "stepForm.probation.period": "试用期时长", + "stepForm.probation.start": "试用起止日期", + "stepForm.recruitment.type": "招聘类型", + "stepForm.recruitment.position": "职位", + "stepForm.recruitment.department": "所属部门", + "stepForm.start.coaching": "启动辅导", + "stepForm.immediate.supervisor": "录入主管", + "stepForm.overall.goals": "制定整体目标", + "stepForm.overall.summary": "提交整体总结", + "stepForm.overall.end": "结束", + "stepForm.collapse.base": "辅导基本信息", + "stepForm.collapse.supervisor": "录入主管", + "stepForm.collapse.goals": "制定整体目标", + "stepForm.collapse.summary": "提交整体总结", + "stepForm.coach.position": "培养职位", + "stepForm.coach.culture": "培养部门", + "stepForm.coach.mentor": "导师", + "stepForm.coach.startTime": "实际辅导开始日期", + "stepForm.coach.endTime": "实际辅导结束日期", + "stepForm.dire.supervisor": "导师主管", + "stepForm.dire.remarks": "导师信息备注", + "stepForm.dire.startTime": "开始辅导日期", + "stepForm.dire.endTime": "结束辅导日期", + "stepForm.target.list": "目标列表", + "stepForm.target.sure": "确立目标", + "stepForm.sum.self": "自我总结", + "stepForm.error.target": "右侧至少存在一项", + "stepForm.head.admin": "用户名", + "menu.form.base": "基础表单", + "baseForm.form.label.no": "否", + "baseForm.form.label.yes": "是", + "baseForm.form.label.placeholder": "请选择", + "baseForm.form.label.frequencyone": "按月", + "baseForm.form.label.frequencytwo": "按周", + "baseForm.form.label.frequencythree": "按双周", + "baseForm.form.label.frequencyfour": "按季度", + "baseForm.form.label.personone": "本地员工", + "baseForm.form.label.persontwo": "非雇员", + "baseForm.form.label.personthree": "中方员工", + "baseForm.form.label.projectone": "制造部新员工培训", + "baseForm.form.label.projecttwo": "公司新员工上岗辅导", + "baseForm.form.label.projectthree": "UI自动化测试辅导项目", + "baseForm.form.label.people": "适用人群", + "baseForm.form.label.rank": "职级", + "baseForm.form.label.type": "项目类型", + "baseForm.form.label.business": "业务属性", + "baseForm.form.label.Objectives": "整体目标", + "baseForm.form.label.culture": "培养部门", + "baseForm.form.label.develop": "制定", + "baseForm.form.label.developone": "学员", + "baseForm.form.label.developtwo": "导师", + "baseForm.form.label.effective": "生效条件", + "baseForm.form.label.effectiveone": "无需审批直接生效", + "baseForm.form.label.effectivetwo": "导师审批", + "baseForm.form.label.effectivethree": "直接主管审批", + "baseForm.form.label.effectivefour": "导师,直接主管审批", + "baseForm.form.label.plan": "阶段计划", + "baseForm.form.label.confirm": "是否需制定", + "baseForm.form.label.frequency": "制定频次", + "baseForm.form.label.role": "目标制定角色", + "baseForm.form.label.roleone": "学员", + "baseForm.form.label.roletwo": "导师", + "baseForm.form.label.condition": "目标生效条件", + "baseForm.form.label.conditionone": "无需审批直接生效", + "baseForm.form.label.conditiontwo": "导师审批", + "baseForm.form.label.conditionthree": "直接主管审批", + "baseForm.form.label.conditionfour": "导师,直接主管审批", + "baseForm.form.label.staged": "阶段评价", + "baseForm.form.label.stagedone": "仅需导师评价", + "baseForm.form.label.stagedtwo": "仅需直接主管评价", + "baseForm.form.label.stagedthree": "需导师,直接主管评价", + "baseForm.form.label.wholeconfirm": "是否需制定", + "baseForm.form.label.evaluation": "整体评价", + "baseForm.form.label.evaluationyes": "需要导师,直接主管评价", + "baseForm.form.label.evaluationno": "不需要导师,直接主管评价", + "baseForm.form.label.mentortitle": "导师选择", + "baseForm.form.label.mentortip": "从导师资源池只能选择导师资格有效的导师,如不从导师资源池选择则不对导师做导师基础资质校验。", + "baseForm.form.label.mentor": "是否仅从导师资源池选择导师", + "baseForm.form.label.remindertitle": "辅导沟通记录提醒", + "baseForm.form.label.reminder": "是否需要提醒", + "baseForm.form.submit": "提交", + "baseForm.form.submit.success": "表单提交成功", + "baseForm.form.cancel": "取消", + "baseForm.form.submit.error": "请先完成必填项", + "baseForm.form.record": "版本记录", + "baseForm.form.project": "项目类型", + "baseForm.form.get.error": "获取数据失败", + "menu.result.success": "成功页", + "success.result.title": "提交结果页用于反馈一系列操作任务的处理结果。", + "menu.result.messageSuccess": "辅导流程提交成功!", + "menu.result.messageEnd": "辅导流程已提交结束!", + "menu.btn.submit": "启动新的辅导", + "menu.btn.cancel": "取消", + "menu.line.process": "当前进度", + "menu.result.error": "失败页", + "error.result.title": "提交结果页用于反馈一系列操作任务的处理结果。", + "menu.result.messageError": "辅导流程提交失败", + "error.result.home": "回到首页", + "menu.exception.403": "403", + "exception.result.403.description": "对不起,您没有访问该资源的权限", + "exception.result.403.back": "返回", + "exception.result.permissions.403": "请联系管理员,申请权限。", + "menu.exception.404": "404", + "exception.result.404.description": "抱歉,页面不见了~", + "exception.result.404.retry": "重试", + "exception.result.404.back": "返回", + "exception.result.permissions.404": "请查看网络连接情况,尝试刷新页面", + "menu.exception.500": "500", + "exception.result.500.description": "抱歉,服务器出了点问题~", + "exception.result.500.back": "返回", + "exception.result.permissions.500": "请查看网络连接情况,尝试刷新页面", + "menu.user.info": "用户中心", + "userInfo.tab.one": "我的计划", + "userInfo.tab.two": "我的任务", + "userInfo.filter.sort": "按时间排序", + "userInfo.filter.startTime": "开始日期", + "userInfo.filter.endTime": "结束日期", + "userInfo.end.positiveOrder": "按截止时间正序", + "userInfo.end.reverseOrder": "按截止时间逆序", + "userInfo.start.positiveOrder": "按开始时间正序", + "userInfo.start.reverseOrder": "按开始时间逆序", + "userInfo.btn.search": "查询", + "userInfo.btn.reset": "重置", + "userInfo.status.status": "状态", + "userInfo.status.optionA": "已完成", + "userInfo.status.optionB": "已逾期", + "userInfo.status.optionC": "即将逾期", + "userInfo.status.optionD": "未完成", + "userInfo.type.type": "类型", + "userInfo.type.optionA": "组织安排", + "userInfo.type.optionB": "阶段计划", + "userInfo.type.optionC": "自主学习", + "userInfo.table.columnA": "计划名称", + "userInfo.table.columnB": "完成时间", + "userInfo.table.columnC": "状态", + "userInfo.table.columnD": "类型", + "userInfo.week.1": "入职2周", + "userInfo.month.1": "入职1个月", + "userInfo.month.2": "入职2个月", + "userInfo.month.3": "入职3个月", + "userInfo.month.4": "入职4个月", + "userInfo.month.5": "入职5个月", + "userInfo.month.6": "入职6个月", + "userInfo.month.7": "入职7个月", + "userInfo.month.8": "入职8个月", + "userInfo.month.9": "入职9个月", + "userInfo.month.10": "入职10个月", + "userInfo.month.11": "入职11个月", + "userInfo.month.12": "入职12个月", + "userInfo.month.13": "入职13个月", + "userInfo.month.14": "入职14个月", + "userInfo.month.15": "入职15个月", + "userInfo.month.16": "入职16个月", + "userInfo.month.17": "入职17个月", + "userInfo.time.message": "结束时间小于开始时间", + "userInfo.filter.all": "请完善当前所有筛选条件", + "menu.user.setting": "用户设置", + "userSetting.save": "保存", + "userSetting.cancel": "取消", + "userSetting.department": "所属部门:", + "userSetting.position": "职位:", + "userSetting.type": "招聘类型:", + "userSetting.date": "试用起止日期:", + "userSetting.during": "试用期时长:", + "userSetting.startTime": "劳动合同开始日期:", + "userSetting.endTime": "劳动合同结束日期:", + "userSetting.first": "开始时间", + "userSetting.last": "结束时间", + "menu.plan.department": "培养部门", + "menu.plan.resource": "人力资源管理部", + "menu.plan.job": "职级", + "menu.plan.person": "人员类型", + "menu.plan.attribute": "业务属性", + "menu.plan.develop": "是否需制定", + "menu.plan.yes": "是", + "menu.plan.no": "否", + "menu.plan.role": "制定角色", + "menu.plan.mentor": "导师", + "menu.plan.condition": "生效条件", + "menu.plan.approval": "直接主管审批", + "menu.plan.frequency": "制定频次", + "menu.plan.month": "按月", + "menu.plan.goal": "目标制定角色", + "menu.plan.trainees": "学员", + "menu.plan.teacher": "导师审批", + "menu.plan.phase": "阶段评价", + "menu.plan.evaluation": "需导师、直接主管评价", + "menu.plan.whole": "整体评价", + "menu.plan.pool": "是否仅从导师资源池选择导师", + "menu.plan.time": "更新时间", + "menu.plan.version": "版本号", + "menu.plan.operation": "操作", + "menu.plan.updated": "更新人", + "work.mock.employees": "转岗员工", + "work.mock.onboard": "新员工上岗", + "work.mock.Test": "测试辅导", + "work.mock.week1": "零促实践(1周)", + "work.mock.week2": "零促实践(2周)", + "work.mock.week3": "零促实践(3周)", + "work.mock.network": "网络实践", + "work.mock.centralized": "新员工集中培训", + "work.mock.hardware": "硬装实践", + "work.index.learn": "学习规划", + "work.index.coach": "学习辅导", + "work.index.formalization": "学习转正", + "work.index.practiced": "学习实践", + "work.index.train": "学习集训", + "work.index.Inquiry": "生活小助手", + "work.index.Home": "新员工之家", + "work.index.Guide": "操作指导", + "work.index.plans": "待制定/确认计划数", + "work.index.Unfinished": "未完成", + "work.index.beOverdue": "即将逾期数", + "work.index.Overdue": "已逾期", + "work.index.trainees": "待启动辅导学员数", + "work.index.coachNum": "辅导中学员数", + "work.index.allocated": "待分配学员数", + "work.index.start": "待启动实践学员数", + "work.index.practice": "实践中学员数", + "work.index.unpark": "待启动转正", + "work.index.entered": "待录入评价结果", + "work.index.approved": "待审批评价结果", + "work.index.put": "实践中学员数", + "work.index.assign": "待分配学员数", + "work.index.prepare": "准备开班班级", + "work.index.open": "开班中班班级", + "work.index.classes": "待验收班级数", + "work.index.policy": "政策", + "work.index.Period": "新员工试用期及培养政策流程", + "work.index.Hotline": "热线", + "work.index.service": "各类实用热线服", + "work.index.Attendance": "考勤", + "work.index.FAQs": "考勤制度及常见问题答疑", + "work.index.Payroll": "发薪", + "work.index.Tax": "发薪纳税问答", + "work.index.Brave": "勇敢新世界登陆计划", + "work.index.Growth": "新员工100天成长指南", + "work.index.Termbase": "术语库", + "work.index.lingo": "最新最热术语,助您懂行话", + "work.index.Library": "文档库与社区", + "work.index.domain": "业务领域知识文档库", + "work.index.platform": "在线学习平台", + "work.index.learning": "在线学习", + "work.index.Operation": "新员工之家操作指导", + "work.index.Numbers": "个", + "work.index.Person": "人", + "work.index.net": "网络", + "work.index.netonline": "在线咨询", + "home.main.one": "首屏可见", + "home.main.up": "页面Onload", + "home.main.down": "采样PV", + "home.main.day": "较昨日", + "home.curve.trend": "性能趋势", + "home.curve.play": "首屏可见", + "home.curve.page": "页面Onload", + "home.falls.line": "加载瀑布流", + "home.falls.tcp": "TCP链接", + "home.falls.ssl": "SSL链接", + "home.round.title": "网络速度分布", + "home.round.unknow": "未知", + "home.roundtable.index": "序号", + "home.roundtable.space": "网络速度", + "home.roundtable.pv": "采样PV(占比)", + "home.roundtable.play": "首屏可见", + "home.roundtable.page": "页面Onload", + "home.region.title": "地域分布", + "menu.cloud.hello": "Hello World", + "menu.cloud.contracts": "合同管理", + "menu.cloud.create": "创建合同", + "menu.cloud.edit": "编辑合同", + "menu.cloud.del": "删除合同", + "menu.cloud.name": "项目名称:", + "menu.cloud.id": "合同编号", + "menu.cloud.customer": "客户名称:", + "menu.cloud.description": "项目描述:", + "menu.cloud.updatedAt": "创建时间", + "menu.cloud.editOpa": "编辑", + "menu.cloud.editDel": "删除", + "menu.cloud.registerErro": "项目名称不满足校验规则", + "menu.cloud.sure": "确认", + "menu.cloud.cancel": "取消", + "menu.cloud.tip": "支持汉字、英文、数字、中划线、下划线、点、斜杠、中英文格式下的小括号和冒号、中文格式下的顿号,且只能以英文、汉字和数字开头,3-255个字符。", + "menu.cloud.askDel": "您确定要删除以下", + "menu.cloud.askContracts": "合同", + "menu.cloud.askInput": "输入", + "menu.cloud.askSure": "确认", + "menu.cloud.verification": "校验不通过", + "menu.cloud.editpass": "校验通过, 修改成功", + "menu.cloud.delpass": "删除成功", + "menu.contracts.name": "合同名称为:", + "http.error.TokenExpire": "登录过期,请重新登录", + "http.error.UserNotFound": "用户不存在", + "http.error.UserAlreadyExist": "用户已存在", + "http.error.InvalidParameter": "无效的请求参数", + "http.error.InternalError": "服务器错误", + "http.error.ErrorPassword": "账号或密码错误", + "menu.allUser.info": "查看用户", + "userInfo.table.id": "ID", + "userInfo.table.name": "名称", + "userInfo.table.email": "邮箱", + "userInfo.table.department": "部门", + "userInfo.table.employeeType": "招聘类型", + "userInfo.table.job": "职位", + "userInfo.table.probation": "试用期", + "userInfo.table.probationStart": "试用期开始日期", + "userInfo.table.probationEnd": "试用期结束日期", + "userInfo.table.probationDuration": "试用期时长", + "userInfo.table.protocol": "劳动合同", + "userInfo.table.protocolStart": "劳动合同开始日期", + "userInfo.table.protocolEnd": "劳动合同结束日期", + "userInfo.table.address": "地址", + "userInfo.table.status": "状态", + "userInfo.table.createTime": "创建时间", + "userInfo.table.updateTime": "更新时间", + "userInfo.table.operations": "操作", + "userInfo.table.operations.update": "修改", + "userInfo.table.operations.delete": "删除", + "userInfo.table.operations.pwdUpdate": "密码", + "userInfo.day": "天", + "userInfo.modal.title.pwdUpdate": "修改密码", + "userInfo.modal.input.oldPassword": "旧密码", + "userInfo.modal.input.newPassword": "新密码", + "userInfo.modal.input.confirmNewPassword": "确认新密码", + "userInfo.modal.message.error": "确认新密码错误", + "userInfo.modal.message.notNull": "密码不能为空", + "userInfo.modal.title.add": "添加用户", + "userInfo.modal.title.update": "更新用户", + "menu.allUser.setting": "用户设置", + "userSetting.name": "用户名", + "userSetting.address": "地址", + "userSetting.status": "状态", + "menu.allUser.useradd": "添加用户", + "userAdd.save": "提交", + "userAdd.cancel": "取消", + "userAdd.email": "邮箱", + "userAdd.password": "密码", + "userAdd.department": "所属部门:", + "userAdd.position": "职位:", + "userAdd.type": "招聘类型:", + "userAdd.date": "试用起止日期:", + "userAdd.during": "试用期时长:", + "userAdd.startTime": "劳动合同开始日期:", + "userAdd.endTime": "劳动合同结束日期:", + "userAdd.first": "开始时间", + "userAdd.last": "结束时间", + "userAdd.name": "用户名", + "userAdd.address": "地址", + "userAdd.status": "状态", + "menu.allPermission.info": "查看权限", + "permissionInfo.table.id": "ID", + "permissionInfo.table.name": "名称", + "permissionInfo.table.desc": "权限", + "permissionInfo.table.operations": "操作", + "permissionInfo.table.operations.update": "修改", + "permissionInfo.table.operations.delete": "删除", + "permissionInfo.modal.title.update": "修改权限", + "permissionInfo.modal.title.add": "添加权限", + "permissionInfo.modal.input.permission": "权限", + "permissionInfo.modal.input.name": "名称", + "permissionInfo.modal.input.id": "id", + "permissionInfo.modal.message.error": "错误", + "permissionInfo.modal.message.notNull": "不能为空", + "menu.allRole.info": "查看权限", + "roleInfo.table.id": "ID", + "roleInfo.table.name": "名称", + "roleInfo.table.desc": "权限", + "roleInfo.table.menu": "菜单", + "roleInfo.table.operations": "操作", + "roleInfo.table.operations.update": "修改", + "roleInfo.table.operations.delete": "删除", + "roleInfo.modal.title.update": "修改角色", + "roleInfo.modal.title.add": "添加角色", + "roleInfo.modal.input.id": "ID", + "roleInfo.modal.input.name": "名称", + "roleInfo.modal.input.desc": "权限", + "roleInfo.modal.input.menu": "菜单", + "roleInfo.modal.message.error": "错误", + "roleInfo.modal.message.notNull": "不能为空", + "roleInfo.permissionTable.id": "ID", + "roleInfo.permissionTable.name": "权限名称", + "roleInfo.permissionTable.desc": "权限介绍", + "roleInfo.menuUpdate.confirm": "确认修改", + "roleInfo.menuUpdate.cancel": "取消", + "menu.allMenu.info": "查看菜单", + "menuInfo.table.id": "ID", + "menuInfo.table.name": "名称", + "menuInfo.table.order": "优先级", + "menuInfo.table.parentId": "父菜单ID", + "menuInfo.table.menuType": "菜单类型", + "menuInfo.table.icon": "图标", + "menuInfo.table.component": "组件", + "menuInfo.table.path": "路径", + "menuInfo.table.locale": "国际化", + "menuInfo.table.operations": "操作", + "menuInfo.table.operations.info": "查看", + "menuInfo.table.operations.update": "修改", + "menuInfo.table.operations.delete": "删除", + "menuInfo.modal.title.info": "查看菜单", + "menuInfo.modal.title.update": "修改菜单", + "menuInfo.modal.title.add": "添加菜单", + "menuInfo.modal.message.error": "parentId不能和id相同", + "menuInfo.modal.message.notNull": "不能为空", + "menu.add.demo": "菜单Demo页", + "exception.result.demo.description": "这是一个新增的菜单demo页", + "locale.add.btn": "添加词条", + "locale.add.title": "添加词条", + "locale.add.key": "词条Key", + "locale.add.content": "词条内容", + "locale.add.lang": "词条语言", + "lang.add.title": "语言名称", + "lang.add.btn": "确认", + "lang.manage.btn": "管理语言", + "locale.add.lang.btn": "新增语言", + "lang.manage.title": "管理语言", + "lang.manage.remove": "删除", + "locale.remove": "删除" + } +} diff --git a/packages/toolkits/pro/template/tinyvue/src/main.ts b/packages/toolkits/pro/template/tinyvue/src/main.ts index 7cbdeb7e..89ebda60 100644 --- a/packages/toolkits/pro/template/tinyvue/src/main.ts +++ b/packages/toolkits/pro/template/tinyvue/src/main.ts @@ -1,23 +1,18 @@ import { createApp } from 'vue'; -import * as echarts4 from 'echarts4'; +import { registerMap } from 'echarts'; import { HwcClient } from '@opentiny/hwc-client'; import globalComponents from '@/components'; import router from './router'; import store from './store'; import i18n from './locale'; import directive from './directive'; -import { setupProdMockServer } from './mockProdServer'; -import './mock'; import App from './App.vue'; import '@/api/interceptor'; import '@/assets/style/global.less'; import config from '../hwc-exports.json'; -// eslint-disable-next-line import/extensions -import 'echarts4/map/js/china.js'; import chinaMap from './assets/chaina.json'; -if(import.meta.env.VITE_USE_MOCK) setupProdMockServer(); -echarts4.registerMap('china', chinaMap); +registerMap('china', chinaMap as any); const app = createApp(App); // 增加华为云相关配置 @@ -29,7 +24,7 @@ HwcClient.configure({ app.use(router); app.use(store); -app.use(i18n({ locale: 'zhCN' })); +app.use(i18n({ locale: localStorage.getItem('tiny-locale') })); app.use(globalComponents); app.use(directive); diff --git a/packages/toolkits/pro/template/tinyvue/src/mock/board.ts b/packages/toolkits/pro/template/tinyvue/src/mock/board.ts index c11ffadd..d36d488e 100644 --- a/packages/toolkits/pro/template/tinyvue/src/mock/board.ts +++ b/packages/toolkits/pro/template/tinyvue/src/mock/board.ts @@ -1,6 +1,5 @@ -import { MockMethod } from 'vite-plugin-mock'; import Mock from 'mockjs'; -import { successResponseWrap } from '@/utils/setup-mock'; +import { successResponseWrap } from '../utils/setup-mock'; const initData = Mock.mock({ options: [ @@ -95,4 +94,4 @@ export default [ return result; }, }, -] as MockMethod[]; +] as any; diff --git a/packages/toolkits/pro/template/tinyvue/src/mock/index.ts b/packages/toolkits/pro/template/tinyvue/src/mock/index.ts index 57b5ee67..330c7d08 100644 --- a/packages/toolkits/pro/template/tinyvue/src/mock/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/mock/index.ts @@ -1,9 +1,12 @@ -import user from './user'; +import { createMockServer } from '@gaonengwww/mock-server'; import list from './list'; import froms from '../views/form/step/mock'; import profile from './profile'; import board from './board'; +import user from './user'; -const index = [...user, ...list, ...froms, ...profile, ...board]; +let mockData = [...list, ...froms, ...profile, ...board, ...user] as any; -export default index; +createMockServer({ + mocks: mockData, +}); diff --git a/packages/toolkits/pro/template/tinyvue/src/mock/list.ts b/packages/toolkits/pro/template/tinyvue/src/mock/list.ts index ed2730cd..207e6fb8 100644 --- a/packages/toolkits/pro/template/tinyvue/src/mock/list.ts +++ b/packages/toolkits/pro/template/tinyvue/src/mock/list.ts @@ -1,6 +1,5 @@ -import { MockMethod } from 'vite-plugin-mock'; import Mock from 'mockjs'; -import { successResponseWrap } from '@/utils/setup-mock'; +import { successResponseWrap } from '../utils/setup-mock'; const taskList = Mock.mock({ 'list|60': [ @@ -31,7 +30,7 @@ export default [ { url: '/api/employee/getEmployee', method: 'post', - response: (params) => { + response: (params: { body: any; }) => { const { pageIndex = 1, pageSize = 10 } = JSON.parse( JSON.stringify(params.body) ); @@ -49,4 +48,4 @@ export default [ return successResponseWrap(data); }, }, -] as MockMethod[]; +] as any; diff --git a/packages/toolkits/pro/template/tinyvue/src/mock/profile.ts b/packages/toolkits/pro/template/tinyvue/src/mock/profile.ts index 2227ba97..8d520ea3 100644 --- a/packages/toolkits/pro/template/tinyvue/src/mock/profile.ts +++ b/packages/toolkits/pro/template/tinyvue/src/mock/profile.ts @@ -1,6 +1,5 @@ -import { MockMethod } from 'vite-plugin-mock'; import Mock from 'mockjs'; -import { successResponseWrap } from '@/utils/setup-mock'; +import { successResponseWrap } from '../utils/setup-mock'; const initData = Mock.mock({ Project: [ @@ -62,4 +61,4 @@ export default [ return successResponseWrap(initData); }, }, -] as MockMethod[]; +] as any; diff --git a/packages/toolkits/pro/template/tinyvue/src/mock/user.ts b/packages/toolkits/pro/template/tinyvue/src/mock/user.ts index 9aeb4573..cadaf001 100644 --- a/packages/toolkits/pro/template/tinyvue/src/mock/user.ts +++ b/packages/toolkits/pro/template/tinyvue/src/mock/user.ts @@ -1,10 +1,9 @@ -import { MockMethod } from 'vite-plugin-mock'; import { successResponseWrap, failResponseWrap, initData, -} from '@/utils/setup-mock'; -import { isLogin } from '@/utils/auth'; +} from '../utils/setup-mock'; +import { isLogin } from '../utils/auth'; const positive = JSON.parse(JSON.stringify(initData.tableData)); const negative = JSON.parse(JSON.stringify(initData.tableData.reverse())); @@ -15,7 +14,7 @@ export default [ { url: '/api/user/register', method: 'post', - response: (params) => { + response: (params: { body: any; }) => { localStorage.setItem('registerUser', JSON.stringify(params.body)); return successResponseWrap({ ...userInfo, role: 'admin' }); }, @@ -57,7 +56,7 @@ export default [ { url: '/api/user/login', method: 'post', - response: (params) => { + response: (params: { body: any; }) => { const registerUser = JSON.parse( localStorage.getItem('registerUser') || '{}' ); @@ -145,4 +144,4 @@ export default [ return successResponseWrap(initData); }, }, -] as MockMethod[]; +] as any; diff --git a/packages/toolkits/pro/template/tinyvue/src/mockProdServer.ts b/packages/toolkits/pro/template/tinyvue/src/mockProdServer.ts deleted file mode 100644 index 6d3a2750..00000000 --- a/packages/toolkits/pro/template/tinyvue/src/mockProdServer.ts +++ /dev/null @@ -1,7 +0,0 @@ -// mockProdServer.ts -import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; -import index from './mock/index'; - -export function setupProdMockServer() { - createProdMockServer([...index]); -} diff --git a/packages/toolkits/pro/template/tinyvue/src/router/constant.ts b/packages/toolkits/pro/template/tinyvue/src/router/constant.ts new file mode 100644 index 00000000..8586a7e6 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/constant.ts @@ -0,0 +1,39 @@ +/* eslint-disable prefer-template */ + +import DefaultLayout from '@/layout/default-layout.vue'; +import { RouteRecordRaw } from 'vue-router'; + +export default [ + { + path: '/', + redirect: `${import.meta.env.VITE_CONTEXT}login`, + }, + { + path: import.meta.env.VITE_CONTEXT, + redirect: { path: `${import.meta.env.VITE_CONTEXT}login` }, + }, + { + path: import.meta.env.VITE_CONTEXT + 'login', + name: 'login', + component: () => import('@/views/login/index.vue'), + meta: { + requiresAuth: false, + }, + }, + { + name: 'root', + path: import.meta.env.VITE_CONTEXT, + component: DefaultLayout, + children: [], + }, + { + path: import.meta.env.VITE_CONTEXT + 'preview', + name: 'preview', + component: () => import('@/views/Preview/index.vue'), + }, + { + name: 'redirect', + path: import.meta.env.VITE_CONTEXT + 'redirect', + component: () => import('@/views/redirect.vue'), + }, +] as RouteRecordRaw[]; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/guard/index.ts b/packages/toolkits/pro/template/tinyvue/src/router/guard/index.ts index ce34d576..acbe7720 100644 --- a/packages/toolkits/pro/template/tinyvue/src/router/guard/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/router/guard/index.ts @@ -1,15 +1,18 @@ import type { Router } from 'vue-router'; import { setRouteEmitter } from '@/utils/route-listener'; import setupPermissionGuard from './permission'; +import { setupMenuGuard } from './menu'; +import { setupTabsGuard } from './tabs'; +import setupInfoGuard from './info'; function setupPageGuard(router: Router) { - router.beforeEach(async (to) => { - // emit route change - setRouteEmitter(to); - }); + setupPermissionGuard(router); + setupInfoGuard(router); + setupMenuGuard(router); + setupTabsGuard(router); } export default function createRouteGuard(router: Router) { setupPageGuard(router); - if(import.meta.env.VITE_USE_MOCK) setupPermissionGuard(router); + // if(import.meta.env.VITE_USE_MOCK) setupPermissionGuard(router); } diff --git a/packages/toolkits/pro/template/tinyvue/src/router/guard/info.ts b/packages/toolkits/pro/template/tinyvue/src/router/guard/info.ts new file mode 100644 index 00000000..8b33664d --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/guard/info.ts @@ -0,0 +1,64 @@ +import { getUserInfo } from '@/api/user'; +import { _i18 } from '@/locale'; +import { useUserStore } from '@/store'; +import { useLocales } from '@/store/modules/locales'; +import { Role } from '@/store/modules/user/types'; +import { isLogin, setToken } from '@/utils/auth'; +import NProgress from 'nprogress'; +import { getCurrentInstance } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { LocationQueryRaw, Router } from 'vue-router'; + +export default function setupInfoGuard(router: Router) { + router.beforeEach(async (to, from, next) => { + NProgress.start(); + if (to.name === 'login') { + next(); + NProgress.done(); + return; + } + const userStore = useUserStore(); + const localesStore = useLocales(); + const { data } = (await getUserInfo()) ?? { data: null }; + if (!data) { + next({ + name: 'login', + query: { + redirect: to.name, + ...to.query, + } as LocationQueryRaw, + }); + setToken(''); + NProgress.done(); + return; + } + if (localesStore.shouldFetch) { + await localesStore.fetchLang(); + await localesStore.fetchLocalTable(); + } + if (localesStore.shouldMerge) { + const entries = Object.entries(localesStore.localTable); + for (let i = 0; i < entries.length; i += 1) { + const lang = entries[i][0]; + const value = entries[i][1]; + _i18?.global.mergeLocaleMessage(lang, value); + } + } + localesStore.$patch({ + shouldFetch: false, + shouldMerge: false, + }); + + userStore.setInfo(data); + userStore.setInfo({ + role: data.role[0].name, + job: data.role[0].name, + roleId: data.role[0].id, + }); + userStore.rolePermission = (data.role as unknown as Role[]) + .flatMap((role) => role.permission) + .map((permission) => permission.name); + next(); + NProgress.done(); + }); +} diff --git a/packages/toolkits/pro/template/tinyvue/src/router/guard/menu.ts b/packages/toolkits/pro/template/tinyvue/src/router/guard/menu.ts new file mode 100644 index 00000000..dc7aa76c --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/guard/menu.ts @@ -0,0 +1,127 @@ +import { useMenuStore } from '@/store/modules/router'; +import { nextTick } from 'vue'; +import { Router, RouteRecordRaw } from 'vue-router'; +import NotFound from '@/views/not-found/404/index.vue'; +import constant from '../constant'; + +export interface ITreeNodeData { + // node-key='id' 设置节点的唯一标识 + id: number | string; + // 节点显示文本 + label: string; + // 子节点 + children?: ITreeNodeData[]; + // 链接 + url: string; + // 组件 + component: string; + // 图标 + customIcon: string; + // 类型 + menuType: string; + // 父节点 + parentId: number; + // 排序 + order: number; + // 国际化 + locale: string; +} +const reg = /\.vue$/gim; +let views = {} as any; +if (BUILD_TOOLS === 'WEBPACK') { + views = import.meta.webpackContext('../../views', { + recursive: true, + regExp: /\.vue$/, + mode: 'sync', + }); + views.keys().forEach((path) => { + if (path.endsWith('.vue')) { + views[`../../views/${path.replace('./', '')}`] = views(path).default; + } + }); +} +if (BUILD_TOOLS === 'VITE') { + views = import.meta.glob('../../views/**/*.vue'); +} else if (BUILD_TOOLS === 'RSPACK') { + const components = require.context('../../views', true, reg, 'sync'); + components.keys().forEach((path) => { + if (path.endsWith('.vue')) { + views[`../../views/${path.replace('./', '')}`] = components(path).default; + } + }); +} + +export const flushRouter = async (router: Router) => { + const menuStore = useMenuStore(); + router.clearRoutes(); + constant.forEach((staticRoute) => router.addRoute(staticRoute)); + await menuStore.getMenuList(); + const routes = toRoutes(menuStore.menuList); + routes.forEach((route) => { + router.addRoute('root', route); + }); +}; + +export const toRoutes = (menus: ITreeNodeData[]) => { + const router: RouteRecordRaw[] = []; + for (let i = 0; i < menus.length; i += 1) { + const menu = menus[i]; + const path = `../../views/${menu.component}${menu.component.includes('.vue') ? '' : '.vue'}`; + if (!views[path]) { + router.push({ + name: menu.label, + path: menu.url, + component: NotFound, + children: [...toRoutes(menu.children ?? [])], + meta: { + locale: menu.locale, + requiresAuth: true, + }, + }); + } else { + router.push({ + name: menu.label, + path: menu.url, + component: views[path], + children: [...toRoutes(menu.children ?? [])], + meta: { + locale: menu.locale, + requiresAuth: true, + }, + }); + } + } + return router; +}; + +export const setupMenuGuard = (router: Router) => { + let has404 = false; + router.beforeEach(async (to, from, next) => { + if (to.name?.toString().toLowerCase() === 'login') { + next(); + return; + } + if (!has404) { + has404 = true; + router.addRoute({ + path: `${import.meta.env.VITE_CONTEXT}:pathMatch(.*)*`, + name: 'notFound', + component: () => import('@/views/not-found/index.vue'), + }); + } + await nextTick(); + const menuStore = useMenuStore(); + if (menuStore.menuList.length) { + next(); + return; + } + const data = await menuStore.getMenuList(); + const routes = toRoutes(data); + routes.forEach((route) => { + if (!router.hasRoute(route.name)) { + router.addRoute('root', route); + } + }); + next({ ...to, replace: true }); + }); +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/guard/permission.ts b/packages/toolkits/pro/template/tinyvue/src/router/guard/permission.ts index 03c53fc1..5a68fe02 100644 --- a/packages/toolkits/pro/template/tinyvue/src/router/guard/permission.ts +++ b/packages/toolkits/pro/template/tinyvue/src/router/guard/permission.ts @@ -1,55 +1,25 @@ import type { Router, LocationQueryRaw } from 'vue-router'; import NProgress from 'nprogress'; // progress bar - -import usePermission from '@/hooks/permission'; -import { useUserStore } from '@/store'; import { isLogin } from '@/utils/auth'; -import appRoutes from '../routes'; +import { Modal } from '@opentiny/vue'; +import { nextTick } from 'vue'; +import { t } from '@opentiny/vue-locale'; export default function setupPermissionGuard(router: Router) { router.beforeEach(async (to, from, next) => { NProgress.start(); - const userStore = useUserStore(); - async function crossroads() { - const Permission = usePermission(); - if (Permission.accessRouter(to)) next(); - else { - const destination = Permission.findFirstPermissionRoute( - appRoutes, - userStore.role - ) || { - name: 'notFound', - } || { - name: 'preview', - }; - next(destination); - } - NProgress.done(); - } - if (isLogin()) { - if (userStore.role) { - crossroads(); - } else { - try { - await userStore.info(); - crossroads(); - } catch (error) { - next({ - name: 'login', - query: { - redirect: to.name, - ...to.query, - } as LocationQueryRaw, - }); - NProgress.done(); - } - } - } else { - if (to.name === 'login' || to.name === 'preview') { + if (!isLogin()) { + if (to.name === 'login') { next(); NProgress.done(); return; } + await nextTick(); + Modal.message({ + message: t('http.error.TokenExpire'), + status: 'error', + }); + await nextTick(); next({ name: 'login', query: { @@ -58,6 +28,9 @@ export default function setupPermissionGuard(router: Router) { } as LocationQueryRaw, }); NProgress.done(); + } else { + next(); + NProgress.done(); } }); } diff --git a/packages/toolkits/pro/template/tinyvue/src/router/guard/tabs.ts b/packages/toolkits/pro/template/tinyvue/src/router/guard/tabs.ts new file mode 100644 index 00000000..b9bff5a9 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/guard/tabs.ts @@ -0,0 +1,16 @@ +import { useTabStore } from "@/store"; +import { Router } from "vue-router"; + +export const setupTabsGuard = (router:Router) => { + router.beforeEach((to, from, next) => { + const tabStore = useTabStore(); + if (tabStore.has(to.meta.locale ?? '')) { + tabStore.set(to.meta.locale!); + next(); + return; + } + tabStore.add({name: to.meta.locale!, link: to.fullPath}); + tabStore.set(to.meta.locale!); + next(); + }) +} diff --git a/packages/toolkits/pro/template/tinyvue/src/router/index.ts b/packages/toolkits/pro/template/tinyvue/src/router/index.ts index 728612df..506296ed 100644 --- a/packages/toolkits/pro/template/tinyvue/src/router/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/router/index.ts @@ -1,9 +1,8 @@ /* eslint-disable prefer-template */ import { createRouter, createWebHistory } from 'vue-router'; import NProgress from 'nprogress'; // progress bar -import DefaultLayout from '@/layout/default-layout.vue'; -import appRoutes from './routes'; import createRouteGuard from './guard'; +import constant from './constant'; NProgress.configure({ showSpinner: false }); // NProgress Configuration @@ -11,43 +10,7 @@ const router = createRouter({ history: createWebHistory(), routes: [ // 本地地址 - { - path: '/', - redirect: `${import.meta.env.VITE_CONTEXT}login`, - }, - { - path: '/' + import.meta.env.VITE_CONTEXT, - redirect: `${import.meta.env.VITE_CONTEXT}board/home`, - }, - // 线上地址 - { - path: import.meta.env.VITE_CONTEXT, - redirect: { path: `${import.meta.env.VITE_CONTEXT}login` }, - }, - { - path: import.meta.env.VITE_CONTEXT + 'login', - name: 'login', - component: () => import('@/views/login/index.vue'), - meta: { - requiresAuth: false, - }, - }, - { - name: 'root', - path: import.meta.env.VITE_CONTEXT, - component: DefaultLayout, - children: appRoutes, - }, - { - path: import.meta.env.VITE_CONTEXT + ':pathMatch(.*)*', - name: 'notFound', - component: () => import('@/views/not-found/index.vue'), - }, - { - path: import.meta.env.VITE_CONTEXT + 'preview', - name: 'preview', - component: () => import('@/views/Preview/index.vue'), - }, + ...constant, ], scrollBehavior() { return { top: 0 }; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/routes/index.ts b/packages/toolkits/pro/template/tinyvue/src/router/routes/index.ts index ee0ca2cc..80266bc2 100644 --- a/packages/toolkits/pro/template/tinyvue/src/router/routes/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/router/routes/index.ts @@ -1,6 +1,6 @@ import type { RouteRecordRaw } from 'vue-router'; -const modules = import.meta.glob('./modules/*.ts', { eager: true }); +const modules = import.meta.glob ? import.meta.glob('./modules/*.ts', { eager: true }) : require.context('./module', false, /\.ts$/, 'sync') const appRoutes: RouteRecordRaw[] = []; Object.keys(modules).forEach((key) => { diff --git a/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/menu.ts b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/menu.ts new file mode 100644 index 00000000..8f645538 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/menu.ts @@ -0,0 +1,29 @@ +import { RoleType } from '@/types/roleType'; + +export default { + path: 'menu', + name: 'Menu', + id: 'Menu', + label: 'Menu', + component: () => import('@/views/menu/index.vue'), + meta: { + locale: 'menu.menu', + requiresAuth: true, + order: 9, + roles: [RoleType.admin], + }, + children: [ + { + path: 'allMenu', + name: 'AllMenu', + id: 'AllMenu', + label: 'AllMenu', + component: () => import('@/views/menu/info/index.vue'), + meta: { + locale: 'menu.menu.info', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + ], +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/permission.ts b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/permission.ts new file mode 100644 index 00000000..2b3bc4bf --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/permission.ts @@ -0,0 +1,29 @@ +import { RoleType } from '@/types/roleType'; + +export default { + path: 'permission', + name: 'Permission', + id: 'Permission', + label: 'Permission', + component: () => import('@/views/permission/index.vue'), + meta: { + locale: 'menu.Permission', + requiresAuth: true, + order: 9, + roles: [RoleType.admin], + }, + children: [ + { + path: 'allPermission', + name: 'AllPermission', + id: 'AllPermission', + label: 'AllPermission', + component: () => import('@/views/permission/info/index.vue'), + meta: { + locale: 'menu.permission.info', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + ], +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/role.ts b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/role.ts new file mode 100644 index 00000000..d6e1f749 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/role.ts @@ -0,0 +1,29 @@ +import { RoleType } from '@/types/roleType'; + +export default { + path: 'role', + name: 'Role', + id: 'Role', + label: 'Role', + component: () => import('@/views/role/index.vue'), + meta: { + locale: 'menu.role', + requiresAuth: true, + order: 9, + roles: [RoleType.admin], + }, + children: [ + { + path: 'allRole', + name: 'AllRole', + id: 'AllRole', + label: 'AllRole', + component: () => import('@/views/role/info/index.vue'), + meta: { + locale: 'menu.role.info', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + ], +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/userManager.ts b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/userManager.ts new file mode 100644 index 00000000..4b88e8b3 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/router/routes/modules/userManager.ts @@ -0,0 +1,53 @@ +import { RoleType } from '@/types/roleType'; + +export default { + path: 'userManager', + name: 'UserManager', + id: 'UserManager', + label: 'UserManager', + component: () => import('@/views/userManager/index.vue'), + meta: { + locale: 'menu.userManager', + requiresAuth: true, + order: 9, + roles: [RoleType.admin], + }, + children: [ + { + path: 'allInfo', + name: 'AllInfo', + id: 'AllInfo', + label: 'AllInfo', + component: () => import('@/views/userManager/info/index.vue'), + meta: { + locale: 'menu.userManager.info', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + { + path: 'allSetting', + name: 'AllSetting', + id: 'AllSetting', + label: 'AllSetting', + component: () => import('@/views/userManager/setting/index.vue'), + meta: { + locale: 'menu.userManager.setting', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + { + path: 'userAdd', + name: 'UserAdd', + id: 'UserAdd', + label: 'UserAdd', + component: () => import('@/views/userManager/useradd/index.vue'), + meta: { + locale: 'menu.userManager.useradd', + requiresAuth: true, + roles: [RoleType.admin], + }, + }, + ], +}; diff --git a/packages/toolkits/pro/template/tinyvue/src/store/index.ts b/packages/toolkits/pro/template/tinyvue/src/store/index.ts index 3d3035f7..7bb438a3 100644 --- a/packages/toolkits/pro/template/tinyvue/src/store/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/store/index.ts @@ -2,8 +2,9 @@ import { createPinia } from 'pinia'; import useAppStore from './modules/app'; import useUserStore from './modules/user'; import useTabBarStore from './modules/tab-bar'; +import { useTabStore } from './modules/tabs'; const pinia = createPinia(); -export { useAppStore, useUserStore, useTabBarStore }; +export { useAppStore, useUserStore, useTabBarStore, useTabStore}; export default pinia; diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/app/index.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/app/index.ts index 7bb6bc7b..02a0d040 100644 --- a/packages/toolkits/pro/template/tinyvue/src/store/modules/app/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/app/index.ts @@ -2,8 +2,21 @@ import { defineStore } from 'pinia'; import defaultSettings from '@/config/settings.json'; import { AppState } from './types'; +export const CONSTANT = { + APP_STATE: 'APP_STATE', +}; + +const initState = (): AppState => { + return { + ...defaultSettings, + ...JSON.parse(localStorage.getItem(CONSTANT.APP_STATE) ?? '{}'), + }; +}; + const useAppStore = defineStore('app', { - state: (): AppState => ({ ...defaultSettings }), + state: () => { + return initState(); + }, getters: { appCurrentSetting(state: AppState): AppState { @@ -15,26 +28,52 @@ const useAppStore = defineStore('app', { }, actions: { + get() { + return { + theme: this.theme, + colorWeek: this.colorWeek, + navbar: this.navbar, + menu: this.menu, + hideMenu: this.hideMenu, + menuCollapse: this.menuCollapse, + footer: this.footer, + themelist: this.themelist, + themeColor: this.themeColor, + themeValue: this.themeValue, + themeContent: this.themeContent, + menuWidth: this.menuWidth, + Settings: this.Settings, + device: this.device, + tabBar: this.tabBar, + step: this.step, + themeLightColors: this.themeLightColors, + }; + }, // Update app settings updateSettings(partial: Partial) { // @ts-ignore-next-line this.$patch(partial); + localStorage.setItem(CONSTANT.APP_STATE, JSON.stringify(this.get())); }, // updateStep updateStep(step: number) { this.step = step; + localStorage.setItem(CONSTANT.APP_STATE, JSON.stringify(this.get())); }, toggleDevice(device: string) { this.device = device; + localStorage.setItem(CONSTANT.APP_STATE, JSON.stringify(this.get())); }, toggleMenu(value: boolean) { this.hideMenu = value; + localStorage.setItem(CONSTANT.APP_STATE, JSON.stringify(this.get())); }, setthemeLightColors(themeLightColors: any) { - this.themeLightColors = themeLightColors - } + this.themeLightColors = themeLightColors; + localStorage.setItem(CONSTANT.APP_STATE, JSON.stringify(this.get())); + }, }, }); diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/locales.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/locales.ts new file mode 100644 index 00000000..c4d87646 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/locales.ts @@ -0,0 +1,39 @@ +import { getAllLang, Lang } from '@/api/lang'; +import { CreateLocalReturn, getLocalTable, I18Table, Local } from '@/api/local'; +import { defineStore } from 'pinia'; +import { reactive, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +export const useLocales = defineStore('locals', { + state: () => ({ + shouldFetch: true, + locales: [] as Local[], + lang: [] as Lang[], + localTable: {} as I18Table, + shouldMerge: true, + }), + actions: { + async fetchLocalTable(lang?: string) { + return getLocalTable(lang).then(({ data }) => { + this.localTable = data; + }); + }, + async fetchLang() { + return getAllLang().then(({ data }) => { + this.lang = data; + }); + }, + pushLang(lang: Lang) { + this.lang.push(lang); + }, + pushLocale(data: CreateLocalReturn) { + this.locales.push(data); + }, + flushI18(lang: string, key: string, content: string) { + const i18n = useI18n(); + i18n.mergeLocaleMessage(lang, { + [key]: content, + }); + }, + }, +}); diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/router.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/router.ts new file mode 100644 index 00000000..e9a8dd22 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/router.ts @@ -0,0 +1,36 @@ +import { defineStore } from 'pinia'; +import { getRoleMenu } from '@/api/menu'; +import useUserStore from './user'; + +export const useMenuStore = defineStore('menu', { + state() { + return { + menuList: [] as any[], + flatMenuList: [] as any[], + }; + }, + actions: { + async getMenuList() { + const userStore = useUserStore(); + if (!userStore.email) { + return []; + } + const { data } = await getRoleMenu(userStore.email); + this.menuList = data; + this.menuListFlat(); + return data; + }, + menuListFlat() { + this.flatMenuList = []; + const dfs = (item: any) => { + this.flatMenuList.push(item); + for (let i = 0; i < item.children.length; i += 1) { + dfs(item.children[i]); + } + }; + for (let i = 0; i < this.menuList.length; i += 1) { + dfs(this.menuList[i]); + } + }, + }, +}); diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/tabs.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/tabs.ts new file mode 100644 index 00000000..031bde02 --- /dev/null +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/tabs.ts @@ -0,0 +1,106 @@ +import { defineStore } from 'pinia'; +import { useRoute, useRouter } from 'vue-router'; + +type Tab = { + name: string; + link: string; +}; + +export const TAB_PERSISTENCE_KEYS = { + TABS: 'tiny-pro::tabs', + CURRENT: 'tiny-pro::tabs:current', +}; + +const initTabs = () => { + const tabs = JSON.parse( + localStorage.getItem(TAB_PERSISTENCE_KEYS.TABS) ?? '[]', + ) as Tab[]; + const routes = useRouter() + .getRoutes() + .map((route) => route.path); + const i18n = useRouter().getRoutes(); + return tabs + .filter((tab) => routes.includes(tab.link)) + .map((item) => { + const i18route = i18n.filter((route) => route.path === item.link)[0]; + return { + name: i18route.meta.locale ?? item.name ?? '', + link: item.link, + }; + }); +}; +const initCurrent = () => { + const current = JSON.parse( + localStorage.getItem(TAB_PERSISTENCE_KEYS.CURRENT) ?? '{}', + ) as Tab; + return current; +}; + +export const useTabStore = defineStore('tabs', { + state() { + return { + data: initTabs(), + current: initCurrent(), + }; + }, + actions: { + add(item: Tab) { + if (!item.name) { + return { ...item }; + } + if (!this.has(item.name)) { + this.data.push(item); + } + this.current = item; + localStorage.setItem( + TAB_PERSISTENCE_KEYS.TABS, + JSON.stringify(this.data), + ); + return { ...item }; + }, + set(name: string) { + this.current = this.getByName(name)[0] ?? null; + localStorage.setItem( + TAB_PERSISTENCE_KEYS.CURRENT, + JSON.stringify(this.current), + ); + return this.current; + }, + has(name: string) { + return this.data.some((tab) => tab.name === name); + }, + getByName(name: string) { + return this.data.filter((tab) => tab.name === name); + }, + getByLink(link: string) { + return this.data.filter((tab) => tab.link === link); + }, + delByLink(link: string, endsWith = false) { + let curName = this.current.name; + if (this.data.length === 1) { + return ''; + } + const idx = this.data.findIndex((tab) => + endsWith ? tab.link.endsWith(link) : tab.link === link, + ); + if (idx === -1) { + return ''; + } + const currentIdx = this.data.findIndex( + (tab) => tab.link === this.current.link, + ); + const isDeleteSelf = currentIdx === idx; + const next = this.data[currentIdx + 1]; + const prev = this.data[currentIdx - 1]; + if (isDeleteSelf) { + curName = next?.name ?? prev?.name; + } + this.data.splice(idx, 1); + localStorage.setItem( + TAB_PERSISTENCE_KEYS.TABS, + JSON.stringify(this.data), + ); + return curName; + }, + }, +}); diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/user/index.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/user/index.ts index 09992554..e43db71e 100644 --- a/packages/toolkits/pro/template/tinyvue/src/store/modules/user/index.ts +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/user/index.ts @@ -1,20 +1,28 @@ -import { defineStore } from 'pinia'; +import {defineStore} from 'pinia'; import { login as userLogin, - loginMail as userLoginMail, - getUserInfo, - updateUserInfo, + logout as userLogout, LoginData, LoginDataMail, + loginMail as userLoginMail, + updateUserInfo, + getUserInfo, + getAllUser, } from '@/api/user'; -import { setToken, clearToken } from '@/utils/auth'; -import { removeRouteListener } from '@/utils/route-listener'; -import { UserState, UserInfo } from './types'; +import {getRoleMenu} from "@/api/menu"; +import {clearToken, getToken, setToken} from '@/utils/auth'; +import {removeRouteListener} from '@/utils/route-listener'; +import {useRouter} from "vue-router"; +import {getRoleInfo} from "@/api/role"; +import {UserInfo, UserState} from './types'; + +const router = useRouter(); const useUserStore = defineStore('user', { state: (): UserState => ({ - userId: '10000', - username: 'admin', + id: '10000', + name: 'admin', + email: 'admin@no-reply.com', department: 'Tiny-Vue-Pro', employeeType: 'social recruitment', job: 'Front end', @@ -33,6 +41,8 @@ const useUserStore = defineStore('user', { filterType: [], submit: false, reset: false, + roleId: 0, + rolePermission: [], }), getters: { @@ -66,12 +76,6 @@ const useUserStore = defineStore('user', { this.filterType = []; }, - // Get user's information - async info() { - const res = await getUserInfo(); - this.setInfo(res.data); - }, - async updateInfo(data: UserInfo) { const res = await updateUserInfo(data); this.setInfo(res.data); @@ -81,8 +85,37 @@ const useUserStore = defineStore('user', { async login(loginForm: LoginData) { try { const res = await userLogin(loginForm); - const { token, userInfo } = res.data; + const { token } = res.data; setToken(token); + const userRes = await getUserInfo(loginForm.email) + const userInfo = { + id: userRes.data.id, + name:userRes.data.name, + email:userRes.data.email, + role:'', + department: userRes.data.department, + employeeType: userRes.data.employeeType, + job: '', + probationStart: userRes.data.probationStart, + probationEnd: userRes.data.probationEnd, + probationDuration: userRes.data.probationDuration, + protocolStart: userRes.data.protocolStart, + protocolEnd: userRes.data.protocolEnd, + address: userRes.data.address, + status: userRes.data.status, + roleId: 0, + rolePermission: [] + } + if(userRes.data.role){ + userInfo.role = userRes.data.role[0].name; + userInfo.job = userRes.data.role[0].name; + userInfo.roleId = userRes.data.role[0].id; + } + const {data} = await getRoleInfo(userInfo.roleId) + const permissions = data.permission; + for (let i = 0; i < permissions.length; i += 1) { + userInfo.rolePermission.push(permissions[i].name) + } this.setInfo(userInfo); } catch (err) { clearToken(); @@ -102,6 +135,10 @@ const useUserStore = defineStore('user', { // Logout async logout() { + const data = { + token:getToken() + } + await userLogout(data); this.resetInfo(); clearToken(); removeRouteListener(); diff --git a/packages/toolkits/pro/template/tinyvue/src/store/modules/user/types.ts b/packages/toolkits/pro/template/tinyvue/src/store/modules/user/types.ts index 7954e618..1c93d56c 100644 --- a/packages/toolkits/pro/template/tinyvue/src/store/modules/user/types.ts +++ b/packages/toolkits/pro/template/tinyvue/src/store/modules/user/types.ts @@ -1,7 +1,18 @@ export type RoleType = '' | '*' | 'admin' | 'user'; +export type Role = { + id: number; + name: string; + permission: { + name: string; + desc: string; + id: number; + }[]; + menus: { id: number; name: string }[]; +}; export interface UserInfo { - userId: string; - username: string; + id: string; + name: string; + email: string; department?: string; employeeType?: string; job?: string; @@ -13,6 +24,10 @@ export interface UserInfo { address?: string; status?: string; role: RoleType; + updateTime?: any; + createTime?: any; + roleId?: number; + rolePermission?: string[]; } export interface UserFilterData { sort?: number; diff --git a/packages/toolkits/pro/template/tinyvue/src/types/global.ts b/packages/toolkits/pro/template/tinyvue/src/types/global.ts index ae6ca9ed..ab5beb4e 100644 --- a/packages/toolkits/pro/template/tinyvue/src/types/global.ts +++ b/packages/toolkits/pro/template/tinyvue/src/types/global.ts @@ -1,3 +1,31 @@ +export type IPaginationMeta = { + itemCount: number; + totalItems?: number; + itemsPerPage: number; + totalPages?: number; + currentPage: number; +}; + +export type InputFilterValue = { + text: string; + relation: 'equals' | 'contains' | 'startwith'; +}; + +export type FilterType = { + [key: string]: { + type: string; + value: InputFilterValue | number[]; + }; +}; + +export type Pager = { + currentPage: number; + pageSizes: number[]; + layout: string; + total: number; + pageSize: number; +}; + export interface AnyObject { [key: string]: unknown; } diff --git a/packages/toolkits/pro/template/tinyvue/src/utils/auth.ts b/packages/toolkits/pro/template/tinyvue/src/utils/auth.ts index 1a87c0ad..1f30ee79 100644 --- a/packages/toolkits/pro/template/tinyvue/src/utils/auth.ts +++ b/packages/toolkits/pro/template/tinyvue/src/utils/auth.ts @@ -1,19 +1,19 @@ const TOKEN_KEY = 'token'; const isLogin = () => { - return !!localStorage.getItem(TOKEN_KEY); + return !!sessionStorage.getItem(TOKEN_KEY); }; const getToken = () => { - return localStorage.getItem(TOKEN_KEY); + return sessionStorage.getItem(TOKEN_KEY); }; const setToken = (token: string) => { - localStorage.setItem(TOKEN_KEY, token); + sessionStorage.setItem(TOKEN_KEY, token); }; const clearToken = () => { - localStorage.removeItem(TOKEN_KEY); + sessionStorage.removeItem(TOKEN_KEY); }; export { isLogin, getToken, setToken, clearToken }; diff --git a/packages/toolkits/pro/template/tinyvue/src/utils/hwcClient.service.ts b/packages/toolkits/pro/template/tinyvue/src/utils/hwcClient.service.ts index d32389f4..2a53b7c3 100644 --- a/packages/toolkits/pro/template/tinyvue/src/utils/hwcClient.service.ts +++ b/packages/toolkits/pro/template/tinyvue/src/utils/hwcClient.service.ts @@ -4,18 +4,21 @@ import BaseUtils from '@/utils/base-utils'; export class HwcClientService { static async apiRequest(fnName: string, params: any, apigInfo: ApigInfo) { - try { - const response = await HwcClient.apigClient.exec( - apigInfo.apigGroupName, - apigInfo.apigName, - { - query: { fn_name: fnName }, - body: JSON.stringify(params), + return HwcClient.apigClient + .exec(apigInfo.apigGroupName, apigInfo.apigName, { + query: { fn_name: fnName }, + body: JSON.stringify(params), + }) + .then((rep) => { + if (rep.ok) { + return rep; } - ); - return await response?.json(); - } catch (error) { - return BaseUtils.getErrorMessage(error); - } + throw new Error(); + }) + .then((rep) => rep.json()) + .then((rep) => (rep.error_code ? { data: [] } : rep)) + .catch((err) => { + return { data: [] }; + }); } } diff --git a/packages/toolkits/pro/template/tinyvue/src/views/board/home/components/curve.vue b/packages/toolkits/pro/template/tinyvue/src/views/board/home/components/curve.vue index 17510961..43ca7fd8 100644 --- a/packages/toolkits/pro/template/tinyvue/src/views/board/home/components/curve.vue +++ b/packages/toolkits/pro/template/tinyvue/src/views/board/home/components/curve.vue @@ -1,8 +1,10 @@ @@ -193,7 +195,7 @@ myChart.resize(); }); nextTick(() => { - myChart.resize() + myChart.resize(); }); }); @@ -220,13 +222,17 @@ diff --git a/packages/toolkits/pro/template/tinyvue/src/views/board/work/components/learn-coach.vue b/packages/toolkits/pro/template/tinyvue/src/views/board/work/components/learn-coach.vue index 0b8b6337..db7071ea 100644 --- a/packages/toolkits/pro/template/tinyvue/src/views/board/work/components/learn-coach.vue +++ b/packages/toolkits/pro/template/tinyvue/src/views/board/work/components/learn-coach.vue @@ -1,52 +1,56 @@