├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── mongo ├── Dockerfile ├── dump │ └── vitetest │ │ ├── Img.bson │ │ ├── Img.metadata.json │ │ ├── Menu.bson │ │ ├── Menu.metadata.json │ │ ├── Pet.bson │ │ ├── Pet.metadata.json │ │ ├── Role.bson │ │ ├── Role.metadata.json │ │ ├── User.bson │ │ └── User.metadata.json ├── export.sh └── import.sh ├── nginx └── conf.d │ └── docker_nginx.conf ├── preview ├── dashboard.png ├── logo.png └── theme.png ├── server ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── public │ ├── files │ │ ├── 内网_1688694163233.bat │ │ ├── 外网_1688694081394.bat │ │ └── 退费核实平台需完善内容0630_1688694316010.docx │ ├── imgs │ │ ├── clearButtonBg_1620282302613.png │ │ ├── close_1620279997443.png │ │ ├── defaultPanel_1620278623571.png │ │ ├── defaultPanel_1620282310246.png │ │ ├── dialogTitle_1620282152240.png │ │ ├── ico-clear_1620282187944.png │ │ ├── imgAdd_1620280226115.png │ │ ├── loading_1620280035600.gif │ │ ├── logout_1620280008925.png │ │ ├── modifyPassword_1620441325045.png │ │ ├── next_1620282322191.png │ │ ├── openPanel_1620280058549.png │ │ ├── prev_1620282558482.png │ │ ├── stretchRight_1620280171414.jpg │ │ ├── tip2_1620280048557.png │ │ ├── tip_1620280019728.png │ │ └── top_1620280078350.jpg │ └── index.html ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── config.ts │ ├── helper │ │ └── base.controller.ts │ ├── main.ts │ ├── modules │ │ ├── file │ │ │ ├── file.controller.ts │ │ │ ├── file.interface.ts │ │ │ ├── file.model.ts │ │ │ ├── file.module.ts │ │ │ └── file.service.ts │ │ ├── img │ │ │ ├── img.controller.ts │ │ │ ├── img.interface.ts │ │ │ ├── img.model.ts │ │ │ ├── img.module.ts │ │ │ └── img.service.ts │ │ ├── pet │ │ │ ├── pet.controller.ts │ │ │ ├── pet.interface.ts │ │ │ ├── pet.model.ts │ │ │ ├── pet.module.ts │ │ │ └── pet.service.ts │ │ └── sys │ │ │ ├── auth │ │ │ ├── auth module.ts │ │ │ ├── auth module.ts1 │ │ │ ├── auth.controller.ts │ │ │ ├── auth.service.ts │ │ │ └── passport │ │ │ │ ├── http.strategy.ts │ │ │ │ └── jwt.strategy.ts │ │ │ ├── menu │ │ │ ├── menu.controller.ts │ │ │ ├── menu.interface.ts │ │ │ ├── menu.model.ts │ │ │ ├── menu.module.ts │ │ │ └── menu.service.ts │ │ │ ├── role │ │ │ ├── role.controller.ts │ │ │ ├── role.interface.ts │ │ │ ├── role.model.ts │ │ │ ├── role.module.ts │ │ │ └── role.service.ts │ │ │ └── user │ │ │ ├── user.controller.ts │ │ │ ├── user.interface.ts │ │ │ ├── user.model.ts │ │ │ ├── user.module.ts │ │ │ └── user.service.ts │ └── util │ │ └── util.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── web ├── .env.dev ├── .env.pro ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── env.d.ts ├── index.html ├── mock │ ├── _createProductionServer.ts │ └── user │ │ └── index.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public │ ├── favicon.ico │ └── logo.png ├── src │ ├── App.vue │ ├── api │ │ └── system │ │ │ ├── menu.ts │ │ │ ├── role.ts │ │ │ └── user.ts │ ├── assets │ │ ├── imgs │ │ │ ├── avatar.jpg │ │ │ └── logo.png │ │ └── svgs │ │ │ ├── 403.svg │ │ │ ├── 404.svg │ │ │ ├── 500.svg │ │ │ ├── add.svg │ │ │ ├── delete.svg │ │ │ ├── edit.svg │ │ │ ├── exit-fullscreen.svg │ │ │ ├── expend.svg │ │ │ ├── full-screen.svg │ │ │ ├── icon.svg │ │ │ ├── lang-select.svg │ │ │ ├── login-bg.svg │ │ │ ├── login-box-bg.svg │ │ │ ├── message.svg │ │ │ ├── money.svg │ │ │ ├── nav-left.svg │ │ │ ├── nav-top.svg │ │ │ ├── password-edit.svg │ │ │ ├── peoples.svg │ │ │ ├── put-away2.svg │ │ │ ├── right.svg │ │ │ ├── role-select.svg │ │ │ ├── search1.svg │ │ │ ├── shopping.svg │ │ │ ├── theme-center.svg │ │ │ ├── triangle-next.svg │ │ │ ├── triangle-prev.svg │ │ │ └── unfold.svg │ ├── common │ │ └── index.ts │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Dialog │ │ │ └── index.vue │ │ ├── Echarts │ │ │ ├── getEcharts.ts │ │ │ └── index.vue │ │ ├── Editor │ │ │ └── index.vue │ │ ├── Form │ │ │ ├── componentMap.ts │ │ │ ├── helper.ts │ │ │ ├── index.vue │ │ │ ├── useForm.ts │ │ │ ├── useRenderCheckbox.tsx │ │ │ ├── useRenderRadio.tsx │ │ │ └── useRenderSelect.tsx │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── HeaderSearch │ │ │ ├── fuse.js │ │ │ └── index.vue │ │ ├── ScreenFull │ │ │ └── index.vue │ │ ├── SearchFields │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ ├── SvgIcon.vue │ │ │ └── index.ts │ │ ├── Table │ │ │ ├── helper.ts │ │ │ └── index.vue │ │ ├── Upload │ │ │ └── index.vue │ │ ├── UserAvatar │ │ │ └── index.vue │ │ └── __tests__ │ │ │ └── HelloWorld.spec.ts │ ├── config.ts │ ├── directives │ │ ├── index.ts │ │ └── waves │ │ │ ├── index.ts │ │ │ ├── waves.css │ │ │ └── waves.ts │ ├── i18n │ │ ├── index.ts │ │ └── langs │ │ │ └── zh-CN.ts │ ├── layout │ │ ├── Dynamic.vue │ │ ├── ExternalLink.vue │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Settings │ │ │ │ ├── LayoutSetting.vue │ │ │ │ ├── MainSetting.vue │ │ │ │ ├── ModifyPasswordDialog.vue │ │ │ │ └── index.vue │ │ │ ├── rightMenu │ │ │ │ └── index.vue │ │ │ ├── sidebar │ │ │ │ ├── AppLink.vue │ │ │ │ ├── Item.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ ├── sidebarH │ │ │ │ ├── AppLink.vue │ │ │ │ ├── Item.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ └── tagsView │ │ │ │ ├── ScrollPane.vue │ │ │ │ └── index.vue │ │ └── index.vue │ ├── main.ts │ ├── permission.ts │ ├── plugins │ │ ├── element.ts │ │ └── nProgress.ts │ ├── router │ │ └── index.ts │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ ├── app.ts │ │ │ ├── app1.ts │ │ │ ├── counter.ts │ │ │ ├── permission.ts │ │ │ ├── tagsView.ts │ │ │ └── user.ts │ ├── styles │ │ ├── elementHack.scss │ │ ├── global.scss │ │ ├── main.scss │ │ ├── sidebar.scss │ │ ├── tagsView.scss │ │ ├── transition.scss │ │ └── var.css │ ├── utils │ │ ├── auth.ts │ │ ├── deepClone.ts │ │ ├── fullScreen.ts │ │ ├── index.ts │ │ ├── is.ts │ │ ├── request.ts │ │ ├── resize.ts │ │ ├── slot.ts │ │ ├── storage.ts │ │ ├── themeSet.ts │ │ ├── title.ts │ │ └── validator.ts │ └── views │ │ ├── ZjView.vue │ │ ├── dashboard │ │ ├── echartOptions.ts │ │ └── index.vue │ │ ├── form │ │ └── demo.vue │ │ ├── icons │ │ ├── element-icons.ts │ │ ├── index.vue │ │ └── svg-icons.ts │ │ ├── login │ │ ├── 404.vue │ │ ├── NoRoutes.vue │ │ ├── components │ │ │ ├── LoginForm.vue │ │ │ └── index.ts │ │ └── index.vue │ │ ├── product │ │ ├── components │ │ │ └── AddDialog.vue │ │ └── list.vue │ │ ├── redirect │ │ └── index.vue │ │ └── system │ │ ├── menu │ │ ├── components │ │ │ └── IconDialog.vue │ │ └── index.vue │ │ ├── role │ │ ├── components │ │ │ ├── AddDialog.vue │ │ │ └── PermissionDialog.vue │ │ └── index.vue │ │ └── user │ │ ├── components │ │ └── AddDialog.vue │ │ └── index.vue ├── tsconfig.json ├── types │ ├── app.d.ts │ ├── form.d.ts │ ├── global.d.ts │ ├── grid.d.ts │ ├── router.d.ts │ ├── searchField.d.ts │ ├── table.d.ts │ └── vue.d.ts └── vite.config.ts └── 备注.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 bigjbigj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://gitee.com/bigjbigj/zj-admin/raw/master/preview/dashboard.png) 2 | ![](https://gitee.com/bigjbigj/zj-admin/raw/master/preview/theme.png) 3 | 4 | # 说明: 5 | - 全栈web开发解决方案,已集成docker并搭配代码生成脚手架工具,方便开发人员快速应用部署,高效开发 6 | - 框架已提供完备的UI交互,以及用户、角色、菜单等基础功能,小而精,无冗余,开箱即用 7 | - 具体配置及应用请移步[帮助文档](https://bigjbigj.gitee.io/zj-admin-preview/)~ 8 | 9 | ## 技术栈 10 | - 前端: vue3+ts+vite+elementplus 11 | - 后端: nestjs+mongoose 12 | - 数据库: mongodb 13 | - node版本: 14及以上 14 | 15 | # 安装: 16 | - 可直接在docker中部署 17 | ```javascript 18 | docker-compose up 19 | ``` 20 | - 单独运行前后端代码(进入server或web文件夹中) 21 | ```javascript 22 | npm install 23 | npm run dev 24 | ``` 25 | ps:由于无mock都是真接口,需要导入基础数据。docker需要在容器中自行导入,自行配置的mongodb可执行mongo\import.sh导入 26 | 27 | # 脚手架使用 28 | ```javascript 29 | npm install zhangjincli -g 30 | ``` 31 | 32 | ## 指令说明 33 | - zj --help 查看所有指令 34 | - zj init templates初始化模板 35 | - zj init models初始化测试数据 36 | - zj config 参考npm的指令增改查脚手架的配置文件 37 | - zj show logs 查看log 38 | - zj compile ./models 编译指定目录,只编译前端vue文件 39 | - zj compilefb ./models 编译指定目录,前后端都编译 40 | - zj compile ./models -v3 指令中添加-v3,则输出为vue3版本 41 | 42 | ### PS 43 | - 前端vue+element,后端nestjs 44 | - .json为前端数据模型,_b.json为后端数据模型 45 | - 前端编译vue版本通过设置-v2或--vue2指定为vue2版本,-v3或--vue3指定为vue3版本 46 | - 数据模型只支持json格式,.json文件为前端模型,_b.json为后端数据模型,zj init models可以生成测试数据 47 | - 支持递归编译 48 | - 生成的后端代码,直接放到server\src\modules目录中,需要手动在app.module.ts中引入模块,需要配置swagger则自行在main.ts中添加 49 | - 生成的前端代码,直接放到web\src\views目录中,在运行起来的项目页面中,系统管理->菜单管理中添加相应路由即可,别忘了去角色管理配置一下菜单权限 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.0' 2 | services: 3 | mongo: 4 | image: mongo 5 | # container_name: zj-server 6 | # build: ./mongo 7 | restart: always 8 | environment: 9 | - MONGO_DATA_DIR=/data/db 10 | - MONGO_LOG_DIR=/data/logs 11 | volumes: 12 | - ./mongo/dump:/data/db 13 | # volumes: 14 | # - ./mongo:/usr/mongo 15 | ports: 16 | - 27017:27017 17 | # command: 18 | # /usr/mongo/import.sh 19 | mongo-express: 20 | image: mongo-express 21 | restart: always 22 | ports: 23 | - 8081:8081 24 | nginx: 25 | restart: always 26 | image: nginx 27 | ports: 28 | - 8092:80 29 | volumes: 30 | - ./nginx/conf.d/:/etc/nginx/conf.d 31 | - ./web/dist:/var/www/html/ 32 | nestjs: 33 | container_name: zj-server 34 | #构建容器 35 | build: ./server 36 | #直接从git拉去 37 | # build: git@github.com:su37josephxia/docker_ci.git#:backend 38 | # 需要链接本地代码时 39 | # volumes: 40 | # - ./server/dist:/usr/src/app/dist 41 | # - ./server:/usr/src/app 42 | ports: 43 | - 3000:3000 -------------------------------------------------------------------------------- /mongo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo 2 | WORKDIR /usr/mongo 3 | ADD . /usr/mongo 4 | EXPOSE 27017 5 | # RUN mongorestore -d zj-server ./dump/zj-server 6 | # 在docker里一直报错,docker exec -it 027 /bin/bash进到伪终端去还原数据,不需要-h了 7 | RUN mongorestore -h mongo:27017 -d zj-server ./dump/zj-server 8 | -------------------------------------------------------------------------------- /mongo/dump/vitetest/Img.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/mongo/dump/vitetest/Img.bson -------------------------------------------------------------------------------- /mongo/dump/vitetest/Img.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"vitetest.Img"},{"v":{"$numberInt":"2"},"unique":true,"key":{"imgname":{"$numberInt":"1"}},"name":"imgname_1","ns":"vitetest.Img","background":true}],"uuid":"d44628500b80402f8488382a5440c80f"} -------------------------------------------------------------------------------- /mongo/dump/vitetest/Menu.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/mongo/dump/vitetest/Menu.bson -------------------------------------------------------------------------------- /mongo/dump/vitetest/Menu.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"vitetest.Menu"},{"v":{"$numberInt":"2"},"unique":true,"key":{"menuKey":{"$numberInt":"1"}},"name":"menuKey_1","ns":"vitetest.Menu","background":true}],"uuid":"772358fcfb6348d0b43fca4a172d66b0"} -------------------------------------------------------------------------------- /mongo/dump/vitetest/Pet.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/mongo/dump/vitetest/Pet.bson -------------------------------------------------------------------------------- /mongo/dump/vitetest/Pet.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"vitetest.Pet"},{"v":{"$numberInt":"2"},"unique":true,"key":{"code":{"$numberInt":"1"}},"name":"code_1","ns":"vitetest.Pet","background":true}],"uuid":"6226cd5f4c4d4b9ca788efeed52ef918"} -------------------------------------------------------------------------------- /mongo/dump/vitetest/Role.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/mongo/dump/vitetest/Role.bson -------------------------------------------------------------------------------- /mongo/dump/vitetest/Role.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"vitetest.Role"},{"v":{"$numberInt":"2"},"unique":true,"key":{"code":{"$numberInt":"1"}},"name":"code_1","ns":"vitetest.Role","background":true}],"uuid":"e6f7f74ae9044cf287dfcc321eb0f697"} -------------------------------------------------------------------------------- /mongo/dump/vitetest/User.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/mongo/dump/vitetest/User.bson -------------------------------------------------------------------------------- /mongo/dump/vitetest/User.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"vitetest.User"},{"v":{"$numberInt":"2"},"unique":true,"key":{"username":{"$numberInt":"1"}},"name":"username_1","ns":"vitetest.User","background":true}],"uuid":"1b7eada3a25348c29de0fc530cdf9982"} -------------------------------------------------------------------------------- /mongo/export.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | mongodump -d vitetest -o ./dump 3 | # mongodump -d nestjs -o e:/data/dump -------------------------------------------------------------------------------- /mongo/import.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | mongorestore -d vitetest ./dump/vitetest 3 | # mongorestore -h mongo:27017 -d zj-server ./dump/zj-server -------------------------------------------------------------------------------- /nginx/conf.d/docker_nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | # server_name www.josephxia.com; 4 | location / { 5 | root /var/www/html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html$args; 8 | } 9 | 10 | location ~ \.(gif|jpg|png)$ { 11 | root /static; 12 | index index.html index.htm; 13 | } 14 | 15 | 16 | # location /api { 17 | # proxy_pass http://app-pm2:3000; 18 | # proxy_redirect off; 19 | # proxy_set_header Host $host; 20 | # proxy_set_header X-Real-IP $remote_addr; 21 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | # } 23 | 24 | 25 | 26 | # location = / { 27 | # rewrite ^(.*) https://www.josephxia.com/$1 permanent; 28 | # } 29 | } 30 | # server { 31 | # listen 443; 32 | # server_name localhost; 33 | # ssl on; 34 | # root html; 35 | # index index.html index.htm; 36 | # ssl_certificate conf.d/cert/www.josephxia.com.pem; 37 | # ssl_certificate_key conf.d/cert/www.josephxia.com.key; 38 | # ssl_session_timeout 5m; 39 | # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; 40 | # ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 41 | # ssl_prefer_server_ciphers on; 42 | # location / { 43 | # root /var/www/html; 44 | # index index.html index.htm; 45 | # } 46 | # } -------------------------------------------------------------------------------- /preview/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/preview/dashboard.png -------------------------------------------------------------------------------- /preview/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/preview/logo.png -------------------------------------------------------------------------------- /preview/theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/preview/theme.png -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 AS development 2 | WORKDIR /usr/src/app 3 | ADD . /usr/src/app 4 | RUN npm config set registry https://registry.npm.taobao.org/ 5 | RUN npm install --only=dev 6 | RUN npm install -g @nestjs/cli 7 | RUN npm install 8 | RUN npm install @types/estree@latest 9 | EXPOSE 3000 10 | #pm2在docker中使用命令为pm2-docker 11 | # CMD ["pm2-runtime", "start", "--json", "process.json"] 12 | # CMD ["pm2-runtime", "start", "process.yml"] 13 | CMD ["npm", "run", "dev"] 14 | # CMD ["npm", "start"] -------------------------------------------------------------------------------- /server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nesttest", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "nest start --watch", 14 | "start:dev": "nest start --watch", 15 | "start:debug": "nest start --debug --watch", 16 | "start:prod": "node dist/main", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^7.5.1", 26 | "@nestjs/core": "^7.5.1", 27 | "@nestjs/jwt": "^7.2.0", 28 | "@nestjs/mongoose": "^7.2.4", 29 | "@nestjs/passport": "^7.1.5", 30 | "@nestjs/platform-express": "^7.5.1", 31 | "@nestjs/swagger": "^4.8.0", 32 | "@types/passport-jwt": "^3.0.5", 33 | "md5": "^2.3.0", 34 | "moment": "^2.29.1", 35 | "mongoose": "^5.12.4", 36 | "passport": "^0.4.1", 37 | "passport-http-bearer": "^1.0.1", 38 | "passport-jwt": "^4.0.0", 39 | "reflect-metadata": "^0.1.13", 40 | "rimraf": "^3.0.2", 41 | "rxjs": "^6.6.3", 42 | "swagger-ui-express": "^4.1.6" 43 | }, 44 | "devDependencies": { 45 | "@nestjs/cli": "^7.5.1", 46 | "@nestjs/schematics": "^7.1.3", 47 | "@nestjs/testing": "^7.5.1", 48 | "@types/express": "^4.17.8", 49 | "@types/jest": "^26.0.15", 50 | "@types/node": "^14.14.6", 51 | "@types/supertest": "^2.0.10", 52 | "@typescript-eslint/eslint-plugin": "^4.6.1", 53 | "@typescript-eslint/parser": "^4.6.1", 54 | "eslint": "^7.12.1", 55 | "eslint-config-prettier": "7.1.0", 56 | "eslint-plugin-prettier": "^3.1.4", 57 | "jest": "^26.6.3", 58 | "prettier": "^2.1.2", 59 | "supertest": "^6.0.0", 60 | "ts-jest": "^26.4.3", 61 | "ts-loader": "^8.0.8", 62 | "ts-node": "^9.0.0", 63 | "tsconfig-paths": "^3.9.0", 64 | "typescript": "^4.0.5" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "src", 73 | "testRegex": ".*\\.spec\\.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "collectCoverageFrom": [ 78 | "**/*.(t|j)s" 79 | ], 80 | "coverageDirectory": "../coverage", 81 | "testEnvironment": "node" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server/public/files/内网_1688694163233.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/files/内网_1688694163233.bat -------------------------------------------------------------------------------- /server/public/files/外网_1688694081394.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/files/外网_1688694081394.bat -------------------------------------------------------------------------------- /server/public/files/退费核实平台需完善内容0630_1688694316010.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/files/退费核实平台需完善内容0630_1688694316010.docx -------------------------------------------------------------------------------- /server/public/imgs/clearButtonBg_1620282302613.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/clearButtonBg_1620282302613.png -------------------------------------------------------------------------------- /server/public/imgs/close_1620279997443.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/close_1620279997443.png -------------------------------------------------------------------------------- /server/public/imgs/defaultPanel_1620278623571.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/defaultPanel_1620278623571.png -------------------------------------------------------------------------------- /server/public/imgs/defaultPanel_1620282310246.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/defaultPanel_1620282310246.png -------------------------------------------------------------------------------- /server/public/imgs/dialogTitle_1620282152240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/dialogTitle_1620282152240.png -------------------------------------------------------------------------------- /server/public/imgs/ico-clear_1620282187944.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/ico-clear_1620282187944.png -------------------------------------------------------------------------------- /server/public/imgs/imgAdd_1620280226115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/imgAdd_1620280226115.png -------------------------------------------------------------------------------- /server/public/imgs/loading_1620280035600.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/loading_1620280035600.gif -------------------------------------------------------------------------------- /server/public/imgs/logout_1620280008925.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/logout_1620280008925.png -------------------------------------------------------------------------------- /server/public/imgs/modifyPassword_1620441325045.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/modifyPassword_1620441325045.png -------------------------------------------------------------------------------- /server/public/imgs/next_1620282322191.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/next_1620282322191.png -------------------------------------------------------------------------------- /server/public/imgs/openPanel_1620280058549.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/openPanel_1620280058549.png -------------------------------------------------------------------------------- /server/public/imgs/prev_1620282558482.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/prev_1620282558482.png -------------------------------------------------------------------------------- /server/public/imgs/stretchRight_1620280171414.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/stretchRight_1620280171414.jpg -------------------------------------------------------------------------------- /server/public/imgs/tip2_1620280048557.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/tip2_1620280048557.png -------------------------------------------------------------------------------- /server/public/imgs/tip_1620280019728.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/tip_1620280019728.png -------------------------------------------------------------------------------- /server/public/imgs/top_1620280078350.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/server/public/imgs/top_1620280078350.jpg -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | zj test 11 | 12 | -------------------------------------------------------------------------------- /server/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | // import { Controller, Get } from '@nestjs/common'; 2 | // import { AppService } from './app.service'; 3 | 4 | // @Controller() 5 | // export class AppController { 6 | // constructor(private readonly appService: AppService) {} 7 | // @Get() 8 | // getHello(): string { 9 | // return this.appService.getHello(); 10 | // } 11 | // @Get("/aaa") 12 | // aaa(): string { 13 | // return "aaa"; 14 | // } 15 | // } 16 | import { UserController } from "./modules/sys/user/user.controller"; 17 | // import { RoleController } from "./controllers/role.controller"; 18 | // export default [UserController, RoleController]; 19 | export default [UserController]; 20 | -------------------------------------------------------------------------------- /server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { RoleModule } from "./modules/sys/role/role.module"; 3 | import { UserModule } from "./modules/sys/user/user.module"; 4 | import { AuthModule } from "./modules/sys/auth/auth module"; 5 | import { MenuModule } from "./modules/sys/menu/menu.module" 6 | import { ImgModule } from "./modules/img/img.module" 7 | import { PetModule } from "./modules/pet/pet.module" 8 | import { FileModule } from "./modules/file/file.module" 9 | import { MongooseModule } from "@nestjs/mongoose"; 10 | // import { AppController } from "./app.controller"; 11 | // import { AppService } from "./app.service"; 12 | import config from "./config" 13 | @Module({ 14 | imports: [ 15 | AuthModule, 16 | RoleModule, 17 | UserModule, 18 | MenuModule, 19 | ImgModule, 20 | PetModule, 21 | FileModule, 22 | MongooseModule.forRoot(config.mongoUrl, {useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false, poolSize: 5}), 23 | ], // 这里可以直接导入module 24 | // controllers: [AppController], 25 | // providers: [AppService], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | // import { Injectable } from '@nestjs/common'; 2 | 3 | // @Injectable() 4 | // export class AppService { 5 | // getHello(): string { 6 | // return 'Hello World!'; 7 | // } 8 | // } 9 | import { UserService } from "./modules/sys/user/user.service"; 10 | // import { RoleService } from "./services/role.service"; 11 | // export default [UserService, RoleService]; 12 | export default [UserService]; -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | HasSalt: ':zj@666!', // 密码盐 3 | mongoUrl: 'mongodb://localhost:27017/vitetest', 4 | }; 5 | -------------------------------------------------------------------------------- /server/src/helper/base.controller.ts: -------------------------------------------------------------------------------- 1 | export default class BaseController { 2 | success(response, data) { 3 | response.send({ 4 | code: 200, 5 | data, 6 | }); 7 | } 8 | message(response, message) { 9 | response.send({ 10 | code: 200, 11 | message, 12 | }); 13 | } 14 | error(response, message, code = -1, errors = {}) { 15 | response.send({ 16 | code, 17 | message, 18 | errors, 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "./app.module"; 3 | import { NestExpressApplication } from "@nestjs/platform-express"; 4 | import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; 5 | import { UserModule } from './modules/sys/user/user.module'; 6 | import { RoleModule } from './modules/sys/role/role.module'; 7 | import { AuthModule } from './modules/sys/auth/auth module' 8 | import { PetModule } from './modules/pet/pet.module' 9 | const path = require("path"); 10 | async function bootstrap() { 11 | // const app = await NestFactory.create(AppModule); 12 | const app = await NestFactory.create(AppModule); 13 | // app.useStaticAssets("public"); 14 | app.useStaticAssets(path.join(__dirname, "..", "public"), { 15 | prefix: "/static/", //设置虚拟路径 16 | }); 17 | // 配置swagger 18 | const authApiOptions = new DocumentBuilder() 19 | .setTitle("Auth API Doc") 20 | .setDescription("Auth API Info") 21 | .setVersion("1.0") 22 | .addBearerAuth() 23 | .addTag("auth") // match tags in controllers 24 | .build(); 25 | const AuthApiDocument = SwaggerModule.createDocument(app, authApiOptions, { 26 | include: [AuthModule], 27 | }); 28 | SwaggerModule.setup("swagger/auth", app, AuthApiDocument); 29 | const userApiOptions = new DocumentBuilder() 30 | .setTitle("User API Doc") 31 | .setDescription("User API Info") 32 | .setVersion("1.0") 33 | .addBearerAuth() 34 | .addTag("/user") // match tags in controllers 35 | .build(); 36 | const userApiDocument = SwaggerModule.createDocument(app, userApiOptions, { 37 | include: [UserModule], 38 | }); 39 | SwaggerModule.setup("swagger/user", app, userApiDocument); 40 | const roleApiOptions = new DocumentBuilder() 41 | .setTitle("Role API Doc") 42 | .setDescription("Role API Info") 43 | .setVersion("1.0") 44 | .addBearerAuth() 45 | .addTag("role") // match tags in controllers 46 | .build(); 47 | const roleApiDocument = SwaggerModule.createDocument(app, roleApiOptions, { 48 | include: [RoleModule], 49 | }); 50 | SwaggerModule.setup("swagger/role", app, roleApiDocument); 51 | const petApiOptions = new DocumentBuilder() 52 | .setTitle("Pet API Doc") 53 | .setDescription("Pet API Info") 54 | .setVersion("1.0") 55 | .addBearerAuth() 56 | .addTag("pet") // match tags in controllers 57 | .build(); 58 | const petApiDocument = SwaggerModule.createDocument(app, petApiOptions, { 59 | include: [PetModule], 60 | }); 61 | SwaggerModule.setup("swagger/pet", app, petApiDocument); 62 | app.enableCors(); 63 | await app.listen(3000); 64 | console.log("service on port 3000"); 65 | } 66 | bootstrap(); 67 | -------------------------------------------------------------------------------- /server/src/modules/file/file.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../../helper/base.controller'; 2 | import { 3 | Controller, 4 | Get, 5 | Post, 6 | Query, 7 | Response, 8 | UseInterceptors, 9 | UploadedFile, 10 | } from '@nestjs/common'; 11 | import { FileService } from './file.service'; 12 | import { AuthGuard } from '@nestjs/passport'; 13 | import { UseGuards } from '@nestjs/common'; 14 | import { FileInterceptor } from '@nestjs/platform-express'; 15 | import { ApiTags } from '@nestjs/swagger'; 16 | import * as path from 'path'; 17 | import * as fs from 'fs'; 18 | const targetDir = path.resolve('public/files'); 19 | const res = fs.existsSync(targetDir); 20 | if (!res) { 21 | // 没有目录就创建一个 22 | fs.mkdirSync(targetDir); 23 | } 24 | @UseGuards(AuthGuard()) 25 | @ApiTags('/file') 26 | @Controller('file') 27 | export class FileController extends BaseController { 28 | constructor(private readonly appService: FileService) { 29 | super(); 30 | } 31 | @Get('/getOneById') // 获取一个 32 | async findOne(@Response() response, @Query() payload) { 33 | const res = await this.appService.findOne(payload); 34 | return this.success(response, res); 35 | } 36 | @Get('/all') // 获取全部 37 | async findAll(@Response() response, @Query() payload) { 38 | const res = await this.appService.findAll(payload); 39 | return this.success(response, res); 40 | } 41 | @Get('/downloadById') // 下载一个 42 | async download(@Response() response, @Query() payload) { 43 | const res = await this.appService.findOne(payload); 44 | const targetPath = targetDir + res.url.replace('/static/files', '') 45 | // const readStream = fs.createReadStream(targetPath); 46 | // response.writeHead(200, { 47 | // 'Content-Type': 'application/force-download', 48 | // 'Content-Disposition': 'attachment; filename=' + res.name, 49 | // }); 50 | // readStream.pipe(response); 51 | response.download(targetPath) 52 | } 53 | @Post('/upload') 54 | @UseInterceptors(FileInterceptor('file')) 55 | async uploadFile(@UploadedFile() file, @Response() response) { 56 | const filename = file.originalname; 57 | const extname = path.extname(filename).toLowerCase(); 58 | const realFilename = filename.replace(extname, ''); 59 | const timeStr = Date.now(); 60 | const target = path.join(targetDir, `${realFilename}_${timeStr}${extname}`); 61 | const serverUrl = `/static/files/${realFilename}_${timeStr}${extname}`; 62 | const res = (await this.appService.upload( 63 | target, 64 | file.buffer, 65 | serverUrl, 66 | filename, 67 | )) as any; 68 | if (res) { 69 | this.success(response, { 70 | serverUrl, 71 | uid: res._id, 72 | name: res.name, 73 | }); 74 | } else { 75 | this.error(response, '文件上传失败'); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /server/src/modules/file/file.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Document } from 'mongoose'; 3 | export interface FileInterface extends Document{ 4 | url: string, 5 | name: string, 6 | } 7 | export class FileDto { 8 | @ApiPropertyOptional({ 9 | description: '地址', 10 | }) 11 | readonly url?: string; 12 | @ApiPropertyOptional({ 13 | description: '文件名', 14 | }) 15 | readonly name?: string; 16 | } -------------------------------------------------------------------------------- /server/src/modules/file/file.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | export const FileModel = new mongoose.Schema( 3 | { 4 | url: { 5 | type: String, 6 | unique: true, 7 | required: true, 8 | }, 9 | name: { 10 | type: String, 11 | }, 12 | }, 13 | { 14 | versionKey: false, 15 | timestamps: { 16 | currentTime: () => Date.now() + 8 * 60 * 60 * 1000, 17 | }, 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /server/src/modules/file/file.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { FileService } from "./file.service"; 3 | import { FileController } from "./file.controller"; 4 | import { PassportModule } from "@nestjs/passport"; 5 | import { MongooseModule } from "@nestjs/mongoose"; 6 | import { FileModel } from "./file.model"; 7 | @Module({ 8 | imports: [ 9 | PassportModule.register({ defaultStrategy: "jwt" }), 10 | MongooseModule.forFeature([ 11 | { name: "File", schema: FileModel, collection: "File" }, 12 | ]), 13 | ], 14 | controllers: [FileController], 15 | providers: [FileService], 16 | exports: [FileService], 17 | }) 18 | export class FileModule {} 19 | -------------------------------------------------------------------------------- /server/src/modules/file/file.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { FileInterface } from "./file.interface"; 3 | import { Model } from "mongoose"; 4 | import { InjectModel } from "@nestjs/mongoose"; 5 | import { filterSearch, formatTime } from "../../util/util"; 6 | import * as fs from "fs" 7 | @Injectable() 8 | export class FileService { 9 | constructor( 10 | @InjectModel("File") private readonly FileModel: Model 11 | ) {} 12 | async findOne(payload) { 13 | const { id } = payload; 14 | return await this.FileModel.findOne({ _id: id }); 15 | } 16 | async findAll(payload) { 17 | let { pageNo = "1", pageSize = "10", orderName = "updatedAt" , orderDir = "desc" } = payload; 18 | let res = []; 19 | let total = 0; 20 | let skip = (Number(pageNo) - 1) * Number(pageSize); 21 | const params = filterSearch(payload); 22 | if(!orderName){ 23 | orderName = "updatedAt" 24 | } 25 | const sortObj = { 26 | [orderName]: orderDir === "desc"? -1: 1 27 | } 28 | res = await this.FileModel.find(params) 29 | .skip(skip) 30 | .limit(Number(pageSize)) 31 | .sort(sortObj) 32 | .exec(); 33 | total = await this.FileModel.countDocuments() 34 | let data = res.map((e) => { 35 | const jsonObject = Object.assign({}, e._doc); 36 | jsonObject.createdAt = formatTime(e.createdAt); 37 | return jsonObject; 38 | }); 39 | return { 40 | total: total, 41 | list: data, 42 | pageSize: Number(pageSize), 43 | pageNo: Number(pageNo), 44 | }; 45 | } 46 | async upload(path, fileBuffer, url, filename){ 47 | return new Promise((resolve, reject) => { 48 | const writeStream = fs.createWriteStream(path); 49 | writeStream.write(fileBuffer) 50 | writeStream.on('error', (err) => { 51 | resolve(false) 52 | }); 53 | writeStream.on('finish', async () => { 54 | const payload = { 55 | name: filename, 56 | url, 57 | } 58 | const res = await this.FileModel.create(payload); 59 | resolve(res) 60 | }); 61 | writeStream.end(); 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/src/modules/img/img.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Document } from 'mongoose'; 3 | export interface ImgInterface extends Document{ 4 | imgname: string, 5 | file: string, 6 | } 7 | export class ImgDto { 8 | @ApiPropertyOptional({ 9 | description: '图片名称', 10 | }) 11 | readonly imgname: string; 12 | @ApiPropertyOptional({ 13 | description: '图片文件', 14 | }) 15 | readonly file: string; 16 | } -------------------------------------------------------------------------------- /server/src/modules/img/img.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from "mongoose"; 2 | export const ImgModel = new mongoose.Schema( 3 | { 4 | imgname: { type: String, unique: true, required: true }, 5 | file: { type: String, required: true }, 6 | }, 7 | { 8 | versionKey: false, 9 | timestamps: true, 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /server/src/modules/img/img.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ImgService } from "./img.service"; 3 | import { ImgController } from "./img.controller"; 4 | import { PassportModule } from "@nestjs/passport"; 5 | import { MongooseModule } from "@nestjs/mongoose"; 6 | import { ImgModel } from "./img.model"; 7 | @Module({ 8 | imports: [ 9 | // PassportModule.register({defaultStrategy: 'bearer'}) 10 | // 指定strategy, 不用再AuthGuard里特别指定 11 | PassportModule.register({ defaultStrategy: "jwt" }), 12 | // TypeOrmModule.forFeature([User, Platform, Role]), 13 | MongooseModule.forFeature([ 14 | { name: "Img", schema: ImgModel, collection: "Img" }, 15 | ]), 16 | ], 17 | controllers: [ImgController], 18 | providers: [ImgService], 19 | exports: [ImgService], 20 | }) 21 | export class ImgModule {} 22 | -------------------------------------------------------------------------------- /server/src/modules/img/img.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { ImgInterface } from "./img.interface"; 3 | import { Model } from "mongoose"; 4 | import { InjectModel } from "@nestjs/mongoose"; 5 | import { filterSearch, formatTime } from "../../util/util"; 6 | import * as fs from "fs" 7 | @Injectable() 8 | export class ImgService { 9 | constructor( 10 | @InjectModel("Img") private readonly ImgModel: Model 11 | ) {} 12 | async findAll(payload) { 13 | const { pageNo = "1", pageSize = "10" } = payload; 14 | let res = []; 15 | let total = 0; 16 | let skip = (Number(pageNo) - 1) * Number(pageSize); 17 | const params = filterSearch(payload); 18 | res = await this.ImgModel.find(params) 19 | .skip(skip) 20 | .limit(Number(pageSize)) 21 | .sort({ updatedAt: -1 }) 22 | .exec(); 23 | // total = await this.UserModel.count(params).exec(); 24 | total = await this.ImgModel.countDocuments() 25 | return { 26 | total: total, 27 | list: res, 28 | pageSize: Number(pageSize), 29 | pageNo: Number(pageNo), 30 | }; 31 | } 32 | async getImgById(id: string): Promise { 33 | return await this.ImgModel.findOne({ _id: id }); 34 | } 35 | async findOneByImgName(imgname: string): Promise { 36 | const res = await this.ImgModel.findOne({ imgname }); 37 | return res; 38 | } 39 | async createOne(body: ImgInterface) { 40 | return await this.ImgModel.create(body); 41 | } 42 | async deleteById(id: string) { 43 | return await this.ImgModel.deleteOne({ _id: id }); 44 | } 45 | async update(id: string, payload: ImgInterface): Promise{ 46 | return await this.ImgModel.findByIdAndUpdate(id, payload); 47 | } 48 | async upload(fileName, fileBuffer){ 49 | return new Promise((resolve, reject) => { 50 | const writeStream = fs.createWriteStream(fileName); 51 | writeStream.write(fileBuffer) 52 | writeStream.on('error', (err) => { 53 | resolve(false) 54 | }); 55 | writeStream.on('finish', () => { 56 | resolve(true) 57 | }); 58 | writeStream.end(); 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/modules/pet/pet.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Document } from 'mongoose'; 3 | export interface PetInterface extends Document{ 4 | code: string, 5 | name: string, 6 | sex: string, 7 | desp: string, 8 | } 9 | export class PetDto { 10 | @ApiPropertyOptional({ 11 | description: '编号', 12 | }) 13 | readonly code?: string; 14 | @ApiPropertyOptional({ 15 | description: '名称', 16 | }) 17 | readonly name?: string; 18 | @ApiPropertyOptional({ 19 | description: '性别', 20 | }) 21 | readonly sex?: string; 22 | @ApiPropertyOptional({ 23 | description: '备注', 24 | }) 25 | readonly desp?: string; 26 | } -------------------------------------------------------------------------------- /server/src/modules/pet/pet.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from "mongoose"; 2 | const obj = { 3 | code: { 4 | type: String, 5 | required: true, 6 | }, 7 | name: { 8 | type: String, 9 | }, 10 | sex: { 11 | type: String, 12 | default: -1, 13 | }, 14 | desp: { 15 | type: String, 16 | }, 17 | }; 18 | const params = { 19 | versionKey: false, 20 | timestamps: true, 21 | }; 22 | export const PetModel = new mongoose.Schema(obj, params); 23 | -------------------------------------------------------------------------------- /server/src/modules/pet/pet.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { PetService } from "./pet.service"; 3 | import { PetController } from "./pet.controller"; 4 | import { PassportModule } from "@nestjs/passport"; 5 | import { MongooseModule } from "@nestjs/mongoose"; 6 | import { PetModel } from "./pet.model"; 7 | @Module({ 8 | imports: [ 9 | PassportModule.register({ defaultStrategy: "jwt" }), 10 | MongooseModule.forFeature([ 11 | { name: "Pet", schema: PetModel, collection: "Pet" }, 12 | ]), 13 | ], 14 | controllers: [PetController], 15 | providers: [PetService], 16 | exports: [PetService], 17 | }) 18 | export class PetModule {} 19 | -------------------------------------------------------------------------------- /server/src/modules/pet/pet.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { PetInterface } from "./pet.interface"; 3 | import { Model } from "mongoose"; 4 | import { InjectModel } from "@nestjs/mongoose"; 5 | import { filterSearch, formatTime } from "../../util/util"; 6 | @Injectable() 7 | export class PetService { 8 | constructor( 9 | @InjectModel("Pet") private readonly PetModel: Model 10 | ) {} 11 | async findAll(payload) { 12 | let { pageNo = "1", pageSize = "10", orderName = "updatedAt" , orderDir = "desc" } = payload; 13 | let res = []; 14 | let total = 0; 15 | let skip = (Number(pageNo) - 1) * Number(pageSize); 16 | const params = filterSearch(payload); 17 | if(!orderName){ 18 | orderName = "updatedAt" 19 | } 20 | const sortObj = { 21 | [orderName]: orderDir === "desc"? -1: 1 22 | } 23 | res = await this.PetModel.find(params) 24 | .skip(skip) 25 | .limit(Number(pageSize)) 26 | .sort(sortObj) 27 | .exec(); 28 | total = await this.PetModel.countDocuments() 29 | let data = res.map((e) => { 30 | const jsonObject = Object.assign({}, e._doc); 31 | jsonObject.createdAt = formatTime(e.createdAt); 32 | return jsonObject; 33 | }); 34 | return { 35 | total: total, 36 | list: data, 37 | pageSize: Number(pageSize), 38 | pageNo: Number(pageNo), 39 | }; 40 | } 41 | async getById(id: string): Promise { 42 | return await this.PetModel.findOne({ _id: id }); 43 | } 44 | async getByName(name: string): Promise { 45 | const res = await this.PetModel.findOne({ name }); 46 | return res; 47 | } 48 | async createOne(body: PetInterface) { 49 | return await this.PetModel.create(body); 50 | } 51 | async deleteById(id: string) { 52 | return await this.PetModel.deleteOne({ _id: id }); 53 | } 54 | async deleteMany(ids: string) { 55 | const payload:string[] = ids.split(",") || []; 56 | return await this.PetModel.deleteMany({_id: {$in: payload}}) 57 | } 58 | async update(id: string, payload: PetInterface): Promise{ 59 | return await this.PetModel.findByIdAndUpdate(id, payload); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/auth module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from '../user/user.module'; 3 | import { AuthService } from './auth.service'; 4 | // import { HttpStrategy } from './passport/http.strategy'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { AuthController } from './auth.controller'; 7 | import { JwtStrategy } from './passport/jwt.strategy'; 8 | @Module({ 9 | imports: [ 10 | JwtModule.register({ 11 | // secretOrPrivateKey: "zjToken", 12 | secret: 'zjToken', 13 | signOptions: { 14 | // expiresIn: 3600, 15 | }, 16 | }), 17 | UserModule, 18 | ], 19 | providers: [AuthService, JwtStrategy], //HttpStrategy 20 | controllers: [AuthController], 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/auth module.ts1: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { UserModule } from "../user/user.module"; 3 | import { AuthService } from "./auth.service"; 4 | import { HttpStrategy } from "./passport/http.strategy"; 5 | @Module({ 6 | imports: [UserModule], 7 | providers: [AuthService, HttpStrategy], 8 | }) 9 | export class AuthModule {} 10 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, Response } from '@nestjs/common'; 2 | import { ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger'; 3 | import { UserService } from '../user/user.service'; 4 | import { UserInterface, UserDto } from '../user/user.interface'; 5 | import { AuthService } from './auth.service'; 6 | import BaseController from '../../../helper/base.controller'; 7 | @ApiTags('auth') 8 | @Controller('auth') 9 | export class AuthController extends BaseController { 10 | constructor( 11 | private readonly authService: AuthService, 12 | private readonly userService: UserService, 13 | ) { 14 | super(); 15 | } 16 | // 默认密码admin, 000000 17 | // 传入name及password取得jwt token 18 | @Post('/login') 19 | @ApiBody({ 20 | type: UserDto, 21 | description: '用户信息', 22 | }) 23 | async getToken( 24 | @Body('username') username: string, 25 | @Body('password') password: string, 26 | @Response() response, 27 | ) { 28 | const res = await this.authService.createToken(username, password); 29 | return this.success(response, res); 30 | } 31 | @Post('/regist') // 创建用户 32 | @ApiBody({ 33 | type: UserDto, 34 | description: '用户名', 35 | }) 36 | async regist(@Body() body: UserInterface, @Response() response) { 37 | const { username } = body; 38 | const targetUser = await this.userService.findOneByUserName(username); 39 | if (targetUser) { 40 | return this.error(response, '用户名已存在'); 41 | } 42 | const res = await this.userService.createOne(body); 43 | if (res._id) { 44 | return this.message(response, '添加新用户成功'); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | UnauthorizedException, 4 | HttpException, 5 | } from "@nestjs/common"; 6 | import { UserService } from "../user/user.service"; 7 | import { JwtService } from "@nestjs/jwt"; 8 | import { UserInterface } from "../user/user.interface"; 9 | import * as md5 from "md5"; 10 | import config from "../../../config" 11 | @Injectable() 12 | export class AuthService { 13 | constructor( 14 | private readonly usersService: UserService, 15 | private readonly jwtService: JwtService 16 | ) {} 17 | async createToken(name: string, password: string) { 18 | const user: UserInterface | null = await this.usersService.findOneByUserName( 19 | name 20 | ); 21 | if (!user) { 22 | throw new UnauthorizedException("用户名不存在。"); 23 | } 24 | if (user.password !== md5(password + config.HasSalt)) { 25 | throw new UnauthorizedException("密码不对。"); 26 | } 27 | const expiration = 60 * 60 * 24 * 7; 28 | const accessToken = this.jwtService.sign( 29 | { id: user._id }, 30 | { 31 | // jwt只加密id 32 | expiresIn: expiration, 33 | } 34 | ); 35 | return { 36 | expiration, 37 | accessToken, 38 | }; 39 | } 40 | async validateUser(payload) { 41 | return await this.usersService.getUserById(payload.id); // 把整个user对象拿回来,会自动放入到req中 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/passport/http.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { AuthService } from '../auth.service'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Strategy } from 'passport-http-bearer'; 5 | @Injectable() 6 | // 要继承@nest/passport下的PassportStrategy并传入passport 7 | // 本列是以http bearer为例 8 | export class HttpStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly authService: AuthService) { 10 | super(); 11 | } 12 | async validate(token: string) { 13 | const user = await this.authService.validateUser(token); 14 | if (!user) { 15 | return new UnauthorizedException(); 16 | } 17 | return user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/modules/sys/auth/passport/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { AuthService } from '../auth.service'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy) { 7 | constructor(private readonly authService: AuthService) { 8 | super({ 9 | // 这里没有intellisense可以用,下面这一段是说 10 | // 要从header取得bearer token 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | // 这里的key就是要跟create token时的key一样 13 | secretOrKey: 'zjToken', 14 | }); 15 | } 16 | // Passport会自动verify jwt,如果key不正确,或是相关信息不正确,如issuer 17 | async validate(payload) { 18 | const user = await this.authService.validateUser(payload); 19 | if (!user) throw new UnauthorizedException(); 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/modules/sys/menu/menu.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Document } from 'mongoose'; 3 | export interface MenuInterface extends Document { 4 | label: string; 5 | code: string; 6 | pid: string; 7 | path: string; 8 | desc?: string; 9 | sort?: number; 10 | } 11 | export class MenuDto { 12 | @ApiPropertyOptional({ 13 | description: '菜单名', 14 | }) 15 | readonly label?: string; 16 | @ApiPropertyOptional({ 17 | description: '菜单编码', 18 | }) 19 | readonly code: string; 20 | @ApiPropertyOptional({ 21 | description: '父菜单编码', 22 | }) 23 | readonly parent: string; 24 | @ApiPropertyOptional({ 25 | description: '菜单路由', 26 | }) 27 | readonly path: string; 28 | @ApiPropertyOptional({ 29 | description: '描述', 30 | }) 31 | readonly desc: string; 32 | } 33 | -------------------------------------------------------------------------------- /server/src/modules/sys/menu/menu.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | export const MenuModel = new mongoose.Schema( 3 | { 4 | menuKey: { type: String, required: true, unique: true }, 5 | menuType: { type: String, required: true }, 6 | label: { type: String, required: true }, 7 | desp: { type: String }, 8 | path: { type: String, required: true }, 9 | name: { type: String }, 10 | aliveName: { type: String, default: '' }, 11 | pid: { type: String }, 12 | pname: { type: String, default: '' }, 13 | component: { type: String }, 14 | dynamicPage: { type: String }, 15 | linkAddress: { type: String }, 16 | icon: { type: String }, 17 | sort: { type: Number }, 18 | }, 19 | { 20 | versionKey: false, 21 | timestamps: { 22 | currentTime: () => Date.now() + 8 * 60 * 60 * 1000, 23 | }, 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /server/src/modules/sys/menu/menu.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { MenuService } from "./menu.service"; 3 | import { MenuController } from "./menu.controller"; 4 | import { PassportModule } from "@nestjs/passport"; 5 | import { MongooseModule } from "@nestjs/mongoose"; 6 | import { MenuModel } from "./menu.model"; 7 | @Module({ 8 | imports: [ 9 | PassportModule.register({ defaultStrategy: "jwt" }), 10 | MongooseModule.forFeature([ 11 | { name: "Menu", schema: MenuModel, collection: "Menu" }, 12 | ]), 13 | ], 14 | controllers: [MenuController], 15 | providers: [MenuService], 16 | exports: [MenuService] 17 | }) 18 | export class MenuModule {} 19 | -------------------------------------------------------------------------------- /server/src/modules/sys/menu/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MenuInterface } from './menu.interface'; 3 | import { Model } from 'mongoose'; 4 | import { InjectModel } from '@nestjs/mongoose'; 5 | import { getTreeData } from '../../../util/util'; 6 | @Injectable() 7 | export class MenuService { 8 | constructor( 9 | @InjectModel('Menu') private readonly MenuModel: Model, 10 | ) {} 11 | async create(payload) { 12 | return await this.MenuModel.create(payload); 13 | } 14 | async findList(): Promise { 15 | // 获取一维数组 16 | const menus = await this.MenuModel.find({}).sort({ sort: 1 }); // 按最新更新时间排序 17 | return menus; 18 | } 19 | async findAll(): Promise { 20 | // 获取菜单树 21 | const menus = await this.MenuModel.find({}).sort({ sort: 1 }); // 按最新更新时间排序 22 | const nodes = getTreeData(menus); 23 | return nodes; 24 | } 25 | async getMenuById(id: string): Promise { 26 | return await this.MenuModel.findOne({ _id: id }); 27 | } 28 | async findOneByMenuKey(menuKey: string): Promise { 29 | return await this.MenuModel.findOne({ menuKey }); 30 | } 31 | async update(id: string, payload: MenuInterface): Promise { 32 | return await this.MenuModel.findByIdAndUpdate(id, payload); 33 | } 34 | async deleteById(id: string) { 35 | const res = await this.MenuModel.deleteOne({ _id: id }); 36 | const children = await this.findChildrenById(id); 37 | const ids = children.map((item) => item._id); 38 | return await this.MenuModel.deleteMany({ _id: { $in: ids } }); 39 | } 40 | async findChildrenById(id: string) { 41 | return await this.MenuModel.find({ pid: id }).sort({ sort: 1 }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/src/modules/sys/role/role.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from "@nestjs/swagger"; 2 | import { Document } from 'mongoose'; 3 | export interface RoleInterface extends Document{ 4 | label: string; 5 | code: string; 6 | desc?: string; 7 | routes: Array 8 | } 9 | export class RoleDto { 10 | @ApiPropertyOptional({ 11 | description: '角色名称', 12 | }) 13 | readonly label?: string; 14 | @ApiPropertyOptional({ 15 | description: '角色编码', 16 | }) 17 | readonly code: string; 18 | @ApiPropertyOptional({ 19 | description: '描述', 20 | }) 21 | readonly desc: string; 22 | @ApiPropertyOptional({ 23 | description: '角色关联', 24 | }) 25 | readonly routes: Array; 26 | } -------------------------------------------------------------------------------- /server/src/modules/sys/role/role.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | export const RoleModel = new mongoose.Schema( 3 | { 4 | label: { type: String, required: true }, 5 | code: { type: String, unique: true, required: true }, 6 | desc: { type: String }, 7 | // routes: { type: Array, required: true }, 8 | routes: [ 9 | { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'Menu', 12 | }, 13 | ], 14 | }, 15 | { 16 | versionKey: false, 17 | timestamps: { 18 | currentTime: () => Date.now() + 8 * 60 * 60 * 1000, 19 | }, 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /server/src/modules/sys/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RoleService } from './role.service'; 3 | import { RoleController } from './role.controller'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | import { RoleModel } from './role.model' 7 | import { MenuModule } from '../menu/menu.module'; 8 | @Module({ 9 | imports: [ 10 | PassportModule.register({ defaultStrategy: 'jwt' }), 11 | MongooseModule.forFeature([ 12 | { name: "Role", schema: RoleModel, collection: "Role" }, 13 | ]), 14 | MenuModule, 15 | ], 16 | controllers: [RoleController], 17 | providers: [RoleService], 18 | exports: [RoleService] 19 | }) 20 | export class RoleModule { 21 | } -------------------------------------------------------------------------------- /server/src/modules/sys/role/role.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import { RoleInterface } from './role.interface'; 5 | import { MenuService } from '../menu/menu.service'; 6 | import { filterSearch, formatTime } from '../../../util/util'; 7 | @Injectable() 8 | export class RoleService { 9 | constructor( 10 | @InjectModel('Role') private readonly RoleModel: Model, 11 | private readonly menuService: MenuService, 12 | ) {} 13 | async findAll(payload) { 14 | let { 15 | pageNo = '1', 16 | pageSize = '10', 17 | orderName = 'updatedAt', 18 | orderDir = 'desc', 19 | } = payload; 20 | let res = []; 21 | let total = 0; 22 | let skip = (Number(pageNo) - 1) * Number(pageSize); 23 | const params = filterSearch(payload); 24 | if (!orderName) { 25 | orderName = 'updatedAt'; 26 | } 27 | const sortObj = { 28 | [orderName]: orderDir === 'ascending' ? 1 : -1, 29 | }; 30 | res = await this.RoleModel.find(params) 31 | .populate('routes') 32 | .skip(skip) 33 | .limit(Number(pageSize)) 34 | .sort(sortObj) 35 | .exec(); 36 | total = await this.RoleModel.countDocuments(); 37 | let data = res.map((e) => { 38 | const jsonObject = Object.assign({}, e._doc); 39 | jsonObject.createdAt = formatTime(e.createdAt); 40 | jsonObject.updatedAt = formatTime(e.updatedAt); 41 | return jsonObject; 42 | }); 43 | return { 44 | total: total, 45 | list: data, 46 | pageSize: Number(pageSize), 47 | pageNo: Number(pageNo), 48 | }; 49 | } 50 | // async findAll(): Promise { 51 | // return await this.RoleModel.find({}) 52 | // .sort({ updatedAt: -1 }) 53 | // .populate('Menu'); // 按最新更新时间排序 54 | // } 55 | async findById(id: string): Promise { 56 | return await this.RoleModel.findOne({ _id: id }).populate('routes'); 57 | } 58 | async findOneByCode(code: string): Promise { 59 | if (code === 'admin') { 60 | // 超管 61 | const allMenus = await this.menuService.findList(); 62 | const menuIds = allMenus.map((item) => item._id); 63 | const adminRole = await this.RoleModel.findOne({ code }); 64 | adminRole.routes = menuIds; 65 | await this.update(adminRole._id, adminRole); 66 | return adminRole; 67 | } 68 | // return await this.RoleModel.findOne({ code }).populate('routes'); 69 | return await this.RoleModel.findOne({ code }); 70 | } 71 | async create(payload) { 72 | return await this.RoleModel.create(payload); 73 | } 74 | async update(id: string, payload: RoleInterface): Promise { 75 | return await this.RoleModel.findByIdAndUpdate(id, payload); 76 | } 77 | async deleteById(id: string) { 78 | return await this.RoleModel.deleteOne({ _id: id }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/modules/sys/user/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Document } from 'mongoose'; 3 | export interface UserInterface extends Document{ 4 | username: string, 5 | password: string, 6 | mobile: string, 7 | name: string, 8 | sex: string, 9 | role: string 10 | } 11 | export class UserDto { 12 | @ApiPropertyOptional({ 13 | description: '姓名', 14 | }) 15 | readonly name?: string; 16 | @ApiPropertyOptional({ 17 | description: '用户名', 18 | }) 19 | readonly username: string; 20 | @ApiPropertyOptional({ 21 | description: '密码', 22 | }) 23 | readonly password: string; 24 | } -------------------------------------------------------------------------------- /server/src/modules/sys/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | export const UserModel = new mongoose.Schema( 3 | { 4 | username: { type: String, unique: true, required: true }, 5 | password: { type: String, required: true }, 6 | mobile: { type: String, required: false }, 7 | name: { type: String, required: false }, 8 | sex: { 9 | type: String, 10 | default: '-1', 11 | }, 12 | role: { type: String, required: true, default: 'admin' }, 13 | }, 14 | { 15 | versionKey: false, 16 | timestamps: { 17 | currentTime: () => Date.now() + 8 * 60 * 60 * 1000, 18 | }, 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /server/src/modules/sys/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { UserService } from "./user.service"; 3 | import { RoleModule } from "../role/role.module" 4 | import { UserController } from "./user.controller"; 5 | import { PassportModule } from "@nestjs/passport"; 6 | import { MongooseModule } from "@nestjs/mongoose"; 7 | import { UserModel } from "./user.model"; 8 | @Module({ 9 | imports: [ 10 | // PassportModule.register({defaultStrategy: 'bearer'}) 11 | // 指定strategy, 不用再AuthGuard里特别指定 12 | PassportModule.register({ defaultStrategy: "jwt" }), 13 | // TypeOrmModule.forFeature([User, Platform, Role]), 14 | MongooseModule.forFeature([ 15 | { name: "User", schema: UserModel, collection: "User" }, 16 | ]), 17 | RoleModule 18 | ], 19 | controllers: [UserController], 20 | providers: [UserService], 21 | exports: [UserService], 22 | }) 23 | export class UserModule {} 24 | -------------------------------------------------------------------------------- /server/src/util/util.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | /** 3 | * @description list数组,每一项为obj,含有code,和parent标记父子关系,获取最终数组 4 | * @param {*} list 5 | * @returns treeNodeArray 6 | */ 7 | function getTreeData(list) { 8 | const objBuffer = { '0': { children: [] } }; 9 | list = list.map((item) => item._doc); 10 | list.forEach((item) => { 11 | if (!item.pid) { 12 | //保障每项都有parent 13 | item.pid = '0'; 14 | } 15 | objBuffer[item._id] = item; 16 | }); 17 | list.forEach((item) => { 18 | // 添加层级关系 19 | const pid = item.pid; 20 | const target = objBuffer[pid]; // objBuffer中的code 21 | if (target) { 22 | if (!target.children) { 23 | target.children = []; 24 | } 25 | target.children.push(item); 26 | } 27 | }); 28 | let nodes = null; 29 | Object.keys(objBuffer).some((key) => { 30 | if (key === '0') { 31 | nodes = objBuffer[key].children; 32 | } 33 | }); 34 | return nodes; 35 | } 36 | /** 37 | * @description 把pageNo, pageSize字段以及值为空字符串或为-1的字段过滤掉 38 | * @param {*} payload 39 | * @param {*} exception 数组,不用添加到条件中,自行去处理逻辑如['stime', 'etime'] 40 | * @returns searchPayload 41 | */ 42 | function filterSearch(payload, exception?): any { 43 | let params = {}; 44 | if (!exception) { 45 | exception = []; 46 | } 47 | exception.push('pageNo'); 48 | exception.push('pageSize'); 49 | exception.push('orderName'); // 按哪个字段排序 50 | exception.push('orderDir'); // 怎么排 asc升序 desc降序 51 | Object.keys(payload).forEach((key) => { 52 | if (!exception.includes(key)) { 53 | // 排除不需要添加到结果中的key 54 | if (payload[key] && payload[key].length > 0 && payload[key] !== '-1') { 55 | // -1为select默认无值时 56 | params[key] = { $regex: payload[key] }; 57 | } 58 | } 59 | }); 60 | return params; 61 | } 62 | /** 63 | * @description 库时间格式化 64 | * @param {*} time 65 | * @returns format time 66 | */ 67 | function formatTime(time) { 68 | return moment(time).format('YYYY-MM-DD HH:mm:ss'); 69 | } 70 | /** 71 | * @description 时间字符串转化为Date类型 72 | * @param {*} dateString 73 | * @returns Date 74 | */ 75 | function stringToDate(dateString) { 76 | if (typeof dateString == 'string') { 77 | return new Date(dateString.length ? dateString : '1970-01-01'); 78 | } else { 79 | return dateString; 80 | } 81 | } 82 | export { getTreeData, filterSearch, formatTime, stringToDate }; 83 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/.env.dev: -------------------------------------------------------------------------------- 1 | # 环境 2 | NODE_ENV=development 3 | 4 | # 接口前缀 5 | VITE_API_BASEPATH=http://localhost:3000/ 6 | # VITE_API_BASEPATH=http://192.168.85.128:3000/ 7 | 8 | # 打包路径 9 | VITE_BASE_PATH=/ 10 | 11 | # 是否删除debugger 12 | VITE_DROP_DEBUGGER=false 13 | 14 | # 是否删除console.log 15 | VITE_DROP_CONSOLE=false 16 | 17 | # 是否sourcemap 18 | VITE_SOURCEMAP=true 19 | 20 | # 输出路径 21 | VITE_OUT_DIR=dev 22 | 23 | # 标题 24 | VITE_APP_TITLE=ElementPlusAdmin_ZjDev 25 | -------------------------------------------------------------------------------- /web/.env.pro: -------------------------------------------------------------------------------- 1 | # 环境 2 | # NODE_ENV=production 3 | 4 | # 接口前缀 5 | # VITE_API_BASEPATH=http://localhost:3000/ 6 | VITE_API_BASEPATH=http://192.168.85.128:3000/ 7 | 8 | # 打包路径 9 | VITE_BASE_PATH=/ 10 | 11 | # 是否删除debugger 12 | VITE_DROP_DEBUGGER=true 13 | 14 | # 是否删除console.log 15 | VITE_DROP_CONSOLE=true 16 | 17 | # 是否sourcemap 18 | VITE_SOURCEMAP=false 19 | 20 | # 输出路径 21 | VITE_OUT_DIR=dist 22 | 23 | # 标题 24 | VITE_APP_TITLE=ElementPlusAdmin_ZJPro 25 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # zjTest 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 43 | 44 | ```sh 45 | npm run test:unit 46 | ``` 47 | -------------------------------------------------------------------------------- /web/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/mock/_createProductionServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 2 | 3 | const modules = import.meta.glob('./**/*.ts', { 4 | import: 'default', 5 | eager: true 6 | }) 7 | 8 | const mockModules: any[] = [] 9 | Object.keys(modules).forEach(async (key) => { 10 | if (key.includes('_')) { 11 | return 12 | } 13 | mockModules.push(...(modules[key] as any)) 14 | }) 15 | 16 | export function setupProdMockServer() { 17 | createProdMockServer(mockModules) 18 | } 19 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zjtest", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "i": "pnpm install", 7 | "dev": "vite --mode dev --open", 8 | "build": "vite build --mode pro", 9 | "preview": "vite preview", 10 | "test:unit": "vitest --environment jsdom --root src/", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false" 13 | }, 14 | "dependencies": { 15 | "@element-plus/icons-vue": "^2.0.10", 16 | "@wangeditor/editor": "^5.1.23", 17 | "@wangeditor/editor-for-vue": "^5.1.12", 18 | "animate.css": "^4.1.1", 19 | "axios": "^1.3.4", 20 | "echarts": "^5.4.2", 21 | "element-plus": "^2.3.3", 22 | "fast-glob": "^3.2.12", 23 | "mockjs": "^1.1.0", 24 | "nprogress": "^0.2.0", 25 | "path-to-regexp": "^6.2.1", 26 | "pinia": "^2.0.28", 27 | "screenfull": "^6.0.2", 28 | "vue": "^3.3.4", 29 | "vue-i18n": "^9.1.10", 30 | "vue-router": "^4.1.6" 31 | }, 32 | "devDependencies": { 33 | "@types/jsdom": "^20.0.1", 34 | "@types/node": "^18.11.12", 35 | "@vitejs/plugin-vue": "^4.0.0", 36 | "@vitejs/plugin-vue-jsx": "^3.0.1", 37 | "@vue/test-utils": "^2.2.6", 38 | "@vue/tsconfig": "^0.1.3", 39 | "jsdom": "^20.0.3", 40 | "npm-run-all": "^4.1.5", 41 | "sass": "^1.58.3", 42 | "typescript": "~4.7.4", 43 | "vite": "^4.4.2", 44 | "vite-plugin-compression": "^0.5.1", 45 | "vite-plugin-html": "^3.2.0", 46 | "vite-plugin-mock": "^2.9.6", 47 | "vite-plugin-svg-icons": "^2.0.1", 48 | "vitest": "^0.25.6", 49 | "vue-tsc": "^1.0.12" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/web/public/logo.png -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 40 | 45 | -------------------------------------------------------------------------------- /web/src/api/system/menu.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | export function getMenuList() { 3 | return request({ 4 | url: "/menu/all", 5 | method: "get", 6 | }); 7 | } 8 | export function addMenu(data) { 9 | return request({ 10 | url: "/menu/create", 11 | method: "post", 12 | data, 13 | }); 14 | } 15 | export function deleteMenu(id) { 16 | return request({ 17 | url: "/menu/delete/" + id, 18 | method: "delete", 19 | }); 20 | } 21 | export function updateMenu(id, data) { 22 | return request({ 23 | url: "/menu/update/" + id, 24 | method: "put", 25 | data, 26 | }); 27 | } 28 | export function sortMenu(data) { 29 | return request({ 30 | url: "/menu/sort", 31 | method: "put", 32 | data, 33 | }); 34 | } 35 | export function getMenuInfo(id) { 36 | return request({ 37 | url: "/api/menu/" + id, 38 | method: "get", 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /web/src/api/system/role.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | export function getRoutesByCode(code) { 3 | return request({ 4 | url: "/role/findByCode/" + code, 5 | method: "get", 6 | }); 7 | } 8 | export function getAllRoutes() { 9 | return request({ 10 | url: "/role/getAllRoutes", 11 | method: "get", 12 | }); 13 | } 14 | export function getRoles(data) { 15 | return request({ 16 | url: "/role/all", 17 | method: "get", 18 | params: data, 19 | }); 20 | } 21 | export function addRole(data) { 22 | return request({ 23 | url: "/role/create", 24 | method: "post", 25 | data, 26 | }); 27 | } 28 | export function updateRole(id, data) { 29 | return request({ 30 | url: "/role/update/" + id, 31 | method: "put", 32 | data, 33 | }); 34 | } 35 | export function deleteRole(id) { 36 | return request({ 37 | url: "/role/delete/" + id, 38 | method: "delete", 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /web/src/api/system/user.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | export function login(data) { 3 | return request({ 4 | url: "/auth/login", 5 | method: "post", 6 | data, 7 | }); 8 | } 9 | export function getCurrentUserInfo() { 10 | return request({ 11 | url: "/user/current", 12 | method: "get", 13 | }); 14 | } 15 | export function getRoles() { 16 | return request({ 17 | url: "/role/all", 18 | method: "get", 19 | }); 20 | } 21 | export function getUserList(data) { 22 | let url = "/user/all"; 23 | return request({ 24 | url, 25 | method: "get", 26 | params: data, 27 | }); 28 | } 29 | export function addUser(data) { 30 | return request({ 31 | url: "/user/create", 32 | method: "post", 33 | data, 34 | }); 35 | } 36 | export function updateUser(id, data) { 37 | return request({ 38 | url: "/user/update/" + id, 39 | method: "put", 40 | data, 41 | }); 42 | } 43 | export function deleteUser(id) { 44 | return request({ 45 | url: "/user/delete/" + id, 46 | method: "delete", 47 | }); 48 | } 49 | export function deleteUsers(ids) { 50 | return request({ 51 | url: "/user/deleteMany", 52 | method: "delete", 53 | data: { 54 | ids, 55 | }, 56 | }); 57 | } 58 | export function changePwd(data) { 59 | return request({ 60 | url: "/user/changePwd", 61 | method: "post", 62 | data, 63 | }); 64 | } 65 | export function getRoutes(id) { 66 | return request({ 67 | url: "/user/getRoutes/" + id, 68 | method: "get", 69 | }); 70 | } 71 | export function resetPwd(id) { 72 | return request({ 73 | url: "/user/resetPwd/" + id, 74 | method: "get", 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /web/src/assets/imgs/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/web/src/assets/imgs/avatar.jpg -------------------------------------------------------------------------------- /web/src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjinzhangjin/zj-admin/a8dd261f416b778221acccb25d5633fde85d3f89/web/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /web/src/assets/svgs/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/expend.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/full-screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/nav-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/src/assets/svgs/nav-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/src/assets/svgs/password-edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/put-away2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/role-select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/search1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/triangle-next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/triangle-prev.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/svgs/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/common/index.ts: -------------------------------------------------------------------------------- 1 | import { ElLoading } from "element-plus"; 2 | import { ElMessage, ElMessageBox } from "element-plus"; 3 | import { i18n } from "@/i18n"; 4 | import { usePermissionStoreWithOut } from "@/store/modules/permission"; 5 | let permissionStore: any = null; 6 | let loading; 7 | const showLoading = () => { 8 | loading = ElLoading.service({ 9 | fullscreen: true, 10 | background: "rgba(255, 255, 255, 0.3)", //遮罩层背景色 11 | }); 12 | }; 13 | const hideLoading = () => { 14 | if (loading) { 15 | loading.close(); 16 | } 17 | }; 18 | const alert = (msg: string, callback?: Fn) => { 19 | const { t } = i18n.global; 20 | ElMessageBox.alert(t(msg), "提示", { 21 | confirmButtonText: "确定", 22 | center: true, 23 | type: "warning", 24 | callback, 25 | }); 26 | }; 27 | const confirm = ( 28 | msg: string, 29 | okCb = () => {}, 30 | cancelCb = () => {}, 31 | header = "提醒", 32 | confirmBtn = "确定", 33 | cancelBtn = "取消" 34 | ) => { 35 | const { t } = i18n.global; 36 | ElMessageBox.confirm(t(msg), header, { 37 | confirmButtonText: confirmBtn, 38 | cancelButtonText: cancelBtn, 39 | type: "warning", 40 | }) 41 | .then(() => { 42 | okCb.call(this); 43 | }) 44 | .catch(() => { 45 | cancelCb.call(this); 46 | }); 47 | }; 48 | const tip = (msg, type = "success") => { 49 | const { t } = i18n.global; 50 | ElMessage({ 51 | showClose: true, 52 | message: t(msg), 53 | type: type as any, 54 | duration: 3 * 1000, 55 | }); 56 | }; 57 | const pagesizeRange: Array = [10, 20, 50, 100]; 58 | const getRouteByPath = (path) => { 59 | if (!permissionStore) { 60 | permissionStore = usePermissionStoreWithOut(); 61 | } 62 | const routes = permissionStore.getRoutes_1d; 63 | const res = routes.find((route) => route.path === path); 64 | if (res) { 65 | return res.path; 66 | } 67 | }; 68 | export default { 69 | alert, 70 | confirm, 71 | tip, 72 | pagesizeRange, 73 | showLoading, 74 | hideLoading, 75 | getRouteByPath, 76 | pageSizes: [10, 20, 50, 100], 77 | pageLayout: "total, prev, pager, next, jumper, sizes", 78 | }; 79 | -------------------------------------------------------------------------------- /web/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 74 | 98 | -------------------------------------------------------------------------------- /web/src/components/Dialog/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 75 | -------------------------------------------------------------------------------- /web/src/components/Echarts/getEcharts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from "echarts/core"; 2 | import { 3 | BarChart, 4 | LineChart, 5 | PieChart, 6 | MapChart, 7 | PictorialBarChart, 8 | RadarChart, 9 | } from "echarts/charts"; 10 | import { 11 | TitleComponent, 12 | TooltipComponent, 13 | GridComponent, 14 | PolarComponent, 15 | AriaComponent, 16 | ParallelComponent, 17 | LegendComponent, 18 | } from "echarts/components"; 19 | import { CanvasRenderer } from "echarts/renderers"; 20 | echarts.use([ 21 | LegendComponent, 22 | TitleComponent, 23 | TooltipComponent, 24 | GridComponent, 25 | PolarComponent, 26 | AriaComponent, 27 | ParallelComponent, 28 | BarChart, 29 | LineChart, 30 | PieChart, 31 | MapChart, 32 | CanvasRenderer, 33 | PictorialBarChart, 34 | RadarChart, 35 | ]); 36 | export default echarts; 37 | -------------------------------------------------------------------------------- /web/src/components/Echarts/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 94 | -------------------------------------------------------------------------------- /web/src/components/Form/componentMap.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue"; 2 | import { 3 | ElCascader, 4 | ElCheckboxGroup, 5 | ElColorPicker, 6 | ElDatePicker, 7 | ElInput, 8 | ElInputNumber, 9 | ElRadioGroup, 10 | ElRate, 11 | ElSelect, 12 | ElSelectV2, 13 | ElSlider, 14 | ElSwitch, 15 | ElTimePicker, 16 | ElTimeSelect, 17 | ElTransfer, 18 | ElAutocomplete, 19 | ElDivider, 20 | } from "element-plus"; 21 | import Editor from "@/components/Editor/index.vue"; 22 | import Upload from "@/components/Upload/index.vue"; 23 | const componentMap: Recordable = { 24 | Radio: ElRadioGroup, 25 | RadioButton: ElRadioGroup, 26 | Checkbox: ElCheckboxGroup, 27 | CheckboxButton: ElCheckboxGroup, 28 | Input: ElInput, 29 | Autocomplete: ElAutocomplete, 30 | InputNumber: ElInputNumber, 31 | Select: ElSelect, 32 | Cascader: ElCascader, 33 | Switch: ElSwitch, 34 | Slider: ElSlider, 35 | TimePicker: ElTimePicker, 36 | DatePicker: ElDatePicker, 37 | Rate: ElRate, 38 | ColorPicker: ElColorPicker, 39 | Transfer: ElTransfer, 40 | Divider: ElDivider, 41 | TimeSelect: ElTimeSelect, 42 | SelectV2: ElSelectV2, 43 | Editor: Editor, 44 | Upload: Upload, 45 | }; 46 | export { componentMap }; 47 | -------------------------------------------------------------------------------- /web/src/components/Form/useRenderCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import { ElCheckbox, ElCheckboxButton } from "element-plus"; 2 | import { defineComponent } from "vue"; 3 | export const useRenderCheckbox = () => { 4 | const renderChcekboxOptions = (item: FormSchema) => { 5 | // 如果有别名,就取别名 6 | const labelAlias = item?.componentProps?.optionsAlias?.labelField; 7 | const valueAlias = item?.componentProps?.optionsAlias?.valueField; 8 | const Com = ( 9 | item.component === "Checkbox" ? ElCheckbox : ElCheckboxButton 10 | ) as ReturnType; 11 | return item?.componentProps?.options?.map((option) => { 12 | return ( 13 | 14 | {option[valueAlias || "label"]} 15 | 16 | ); 17 | }); 18 | }; 19 | return { 20 | renderChcekboxOptions, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /web/src/components/Form/useRenderRadio.tsx: -------------------------------------------------------------------------------- 1 | import { ElRadio, ElRadioButton } from 'element-plus' 2 | import { defineComponent } from 'vue' 3 | export const useRenderRadio = () => { 4 | const renderRadioOptions = (item: FormSchema) => { 5 | // 如果有别名,就取别名 6 | const labelAlias = item?.componentProps?.optionsAlias?.labelField; 7 | const valueAlias = item?.componentProps?.optionsAlias?.valueField; 8 | const Com = ( 9 | item.component === "Radio" ? ElRadio : ElRadioButton 10 | ) as ReturnType; 11 | return item?.componentProps?.options?.map((option) => { 12 | return ( 13 | 14 | {option[valueAlias || "label"]} 15 | 16 | ); 17 | }); 18 | }; 19 | return { 20 | renderRadioOptions, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /web/src/components/Form/useRenderSelect.tsx: -------------------------------------------------------------------------------- 1 | import { getSlot } from "@/utils/slot" 2 | import { Slots } from "vue"; 3 | export const useRenderSelect = (slots: Slots) => { 4 | // 渲染 select options 5 | const renderSelectOptions = (item: FormSchema) => { 6 | // 如果有别名,就取别名 7 | const labelAlias = item?.componentProps?.optionsAlias?.labelField; 8 | return item?.componentProps?.options?.map((option) => { 9 | if (option?.options?.length) { 10 | return ( 11 | 12 | {() => { 13 | return option?.options?.map((v) => { 14 | return renderSelectOptionItem(item, v); 15 | }); 16 | }} 17 | 18 | ); 19 | } else { 20 | return renderSelectOptionItem(item, option); 21 | } 22 | }); 23 | }; 24 | // 渲染 select option item 25 | const renderSelectOptionItem = ( 26 | item: FormSchema, 27 | option: ComponentOptions 28 | ) => { 29 | // 如果有别名,就取别名 30 | const labelAlias = item?.componentProps?.optionsAlias?.labelField; 31 | const valueAlias = item?.componentProps?.optionsAlias?.valueField; 32 | return ( 33 | 37 | {{ 38 | default: () => 39 | // option 插槽名规则,{field}-option 40 | item?.componentProps?.optionsSlot 41 | ? getSlot(slots, `${item.field}-option`, { item: option }) 42 | : undefined, 43 | }} 44 | 45 | ); 46 | }; 47 | 48 | return { 49 | renderSelectOptions, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /web/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 22 | 33 | -------------------------------------------------------------------------------- /web/src/components/HeaderSearch/fuse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * { 3 | * path: '/aaa', title: ['你好', '我好'] 4 | * } 5 | */ 6 | function Fuse(list) { 7 | this.list = list; 8 | } 9 | Fuse.prototype.search = function(query) { 10 | const list = this.list 11 | const res = [] 12 | list.forEach(item => { 13 | let flag; 14 | if(item.title.some(titleItem => titleItem.trim().toLowerCase().indexOf(query?.toLowerCase()) > -1)){ 15 | flag = true 16 | } 17 | if(flag){ 18 | res.push(item) 19 | } 20 | }) 21 | return res 22 | } 23 | Fuse.prototype.reset = function(){ 24 | this.list = []; 25 | } 26 | Fuse.prototype.add = function(list){ 27 | this.list = list; 28 | } 29 | export default Fuse; -------------------------------------------------------------------------------- /web/src/components/ScreenFull/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /web/src/components/SvgIcon/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 33 | 49 | -------------------------------------------------------------------------------- /web/src/components/SvgIcon/index.ts: -------------------------------------------------------------------------------- 1 | import * as ElementPlusIconsVue from "@element-plus/icons-vue"; 2 | import type { App, VNode } from "vue"; 3 | import SvgIcon from './SvgIcon.vue'; 4 | import { h } from 'vue' 5 | interface IconType { 6 | iconClass?: string 7 | className?: string 8 | } 9 | interface ElementIconTypes { 10 | iconName?: string 11 | } 12 | export const setupIcon = async (app: App) => { 13 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 14 | app.component(key, component); 15 | } 16 | app.component('SvgIcon', SvgIcon); 17 | }; 18 | // {iconClass: "right"} 19 | export const useIcon = (props: IconType): VNode => { 20 | return h(SvgIcon, props) 21 | } 22 | // {iconName: "Aim"} 23 | export const useElementIcon = (props: ElementIconTypes): VNode => { 24 | let target:Component = null; 25 | Object.keys(ElementPlusIconsVue).some(key => { 26 | if(key === props.iconName){ 27 | target = ElementPlusIconsVue[key] 28 | return true 29 | } 30 | }) 31 | return h(target,{}) 32 | } -------------------------------------------------------------------------------- /web/src/components/Table/helper.ts: -------------------------------------------------------------------------------- 1 | export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => { 2 | const newIndex = index + 1 3 | if (reserveIndex) { 4 | return size * (current - 1) + newIndex 5 | } else { 6 | return newIndex 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/src/components/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 41 | -------------------------------------------------------------------------------- /web/src/components/__tests__/HelloWorld.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | import HelloWorld from '../HelloWorld.vue' 5 | 6 | describe('HelloWorld', () => { 7 | it('renders properly', () => { 8 | const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) 9 | expect(wrapper.text()).toContain('Hello Vitest') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /web/src/config.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@/utils/storage" 2 | const storage = useStorage(); 3 | export const appModules: AppState = { 4 | showSettings: false, // 设置面板 5 | param: {}, 6 | title: import.meta.env.VITE_APP_TITLE, // 标题 7 | collapse: false, // 折叠菜单 8 | locale: storage.get('locale') || 'zh-CN', // 多语言 9 | logo: true, // logo 10 | greyMode: false, // 是否开始灰色模式,用于特殊悼念日 11 | layout: storage.get('layout') as LayoutType || 'left', // layout布局 12 | size: storage.get('size') as ElememtPlusSize || 'default', // 组件尺寸 13 | theme: storage.get('theme') || "default", 14 | primaryColor: storage.get('primaryColor') || "default", 15 | navHeadBgColor: storage.get('navHeadBgColor') || "white", 16 | } -------------------------------------------------------------------------------- /web/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { setupWaves } from "./waves"; 3 | 4 | export const setupDirectivs = (app: App) => { 5 | setupWaves(app); 6 | }; 7 | -------------------------------------------------------------------------------- /web/src/directives/waves/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import waves from "./waves"; 3 | export const setupWaves = (app: App) => { 4 | app.directive("waves", waves); 5 | }; 6 | -------------------------------------------------------------------------------- /web/src/directives/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /web/src/directives/waves/waves.ts: -------------------------------------------------------------------------------- 1 | import "./waves.css"; 2 | import type { Directive, DirectiveBinding } from "vue"; 3 | const context = "@@wavesContext"; 4 | function handleClick(el, binding) { 5 | function handle(e) { 6 | const customOpts = Object.assign({}, binding.value); 7 | const opts = Object.assign( 8 | { 9 | ele: el, // 波纹作用元素 10 | type: "hit", // hit 点击位置扩散 center中心点扩展 11 | color: "rgba(0, 0, 0, 0.15)", // 波纹颜色 12 | }, 13 | customOpts 14 | ); 15 | const target = opts.ele; 16 | if (target) { 17 | target.style.position = "relative"; 18 | target.style.overflow = "hidden"; 19 | const rect = target.getBoundingClientRect(); 20 | let ripple = target.querySelector(".waves-ripple"); 21 | if (!ripple) { 22 | ripple = document.createElement("span"); 23 | ripple.className = "waves-ripple"; 24 | ripple.style.height = ripple.style.width = 25 | Math.max(rect.width, rect.height) + "px"; 26 | target.appendChild(ripple); 27 | } else { 28 | ripple.className = "waves-ripple"; 29 | } 30 | switch (opts.type) { 31 | case "center": 32 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + "px"; 33 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + "px"; 34 | break; 35 | default: 36 | ripple.style.top = 37 | (e.pageY - 38 | rect.top - 39 | ripple.offsetHeight / 2 - 40 | document.documentElement.scrollTop || document.body.scrollTop) + 41 | "px"; 42 | ripple.style.left = 43 | (e.pageX - 44 | rect.left - 45 | ripple.offsetWidth / 2 - 46 | document.documentElement.scrollLeft || document.body.scrollLeft) + 47 | "px"; 48 | } 49 | ripple.style.backgroundColor = opts.color; 50 | ripple.className = "waves-ripple z-active"; 51 | return false; 52 | } 53 | } 54 | 55 | if (!el[context]) { 56 | el[context] = { 57 | removeHandle: handle, 58 | }; 59 | } else { 60 | el[context].removeHandle = handle; 61 | } 62 | 63 | return handle; 64 | } 65 | const beforeMount = (el: Element, binding: DirectiveBinding) => { 66 | el.addEventListener("click", handleClick(el, binding), false); 67 | }; 68 | const updated = (el: Element, binding: DirectiveBinding) => { 69 | el.removeEventListener("click", el[context].removeHandle, false); 70 | el.addEventListener("click", handleClick(el, binding), false); 71 | }; 72 | const unmounted = (el: Element, binding: DirectiveBinding) => { 73 | el.removeEventListener("click", el[context].removeHandle, false); 74 | el[context] = null; 75 | delete el[context]; 76 | }; 77 | const waves: Directive = { 78 | beforeMount, 79 | updated, 80 | unmounted, 81 | }; 82 | export default waves; 83 | -------------------------------------------------------------------------------- /web/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { useAppStoreWithOut } from "@/store/modules/app"; 2 | import type { App } from "vue"; 3 | import { createI18n } from "vue-i18n"; 4 | import type { I18n, I18nOptions } from "vue-i18n"; 5 | import element_zhCN from "element-plus/es/locale/lang/zh-cn"; 6 | import element_en from "element-plus/es/locale/lang/en"; 7 | export let i18n: ReturnType; 8 | export const elLocaleMap = { 9 | "zh-CN": element_zhCN, 10 | en: element_en, 11 | }; 12 | const appStore = useAppStoreWithOut(); 13 | const locale = appStore.locale; 14 | const setHtmlPageLang = (locale: string) => { 15 | document.querySelector("html")?.setAttribute("lang", locale); 16 | }; 17 | export const setupI18n = async (app: App) => { 18 | setHtmlPageLang(locale); 19 | const defaultLocal = await import(`./langs/${locale}.ts`); 20 | const message = defaultLocal.default ?? {}; 21 | let options: I18nOptions = { 22 | globalInjection: true, 23 | legacy: false, 24 | locale, 25 | fallbackLocale: "zh-CN", 26 | messages: { 27 | [locale]: message, 28 | }, 29 | availableLocales: ["zh-CN", "en"], 30 | sync: true, 31 | silentTranslationWarn: true, 32 | missingWarn: false, 33 | silentFallbackWarn: true, 34 | }; 35 | i18n = createI18n(options) as I18n; 36 | app.use(i18n); 37 | }; 38 | export const setLocaleMessages = async (lang: string, messageObj: object) => { 39 | const defaultLocal = await import(`./langs/${lang}.ts`); 40 | i18n.global.setLocaleMessage(lang, { 41 | ...elLocaleMap[lang], 42 | ...defaultLocal.default, 43 | ...messageObj, 44 | }); 45 | }; 46 | // 修复i18n在ts中引用useI18n的bug 47 | // type I18nGlobalTranslation = { 48 | // (key: string): string 49 | // (key: string, locale: string): string 50 | // (key: string, locale: string, list: unknown[]): string 51 | // (key: string, locale: string, named: Record): string 52 | // (key: string, list: unknown[]): string 53 | // (key: string, named: Record): string 54 | // } 55 | // type I18nTranslationRestParameters = [string, any] 56 | // export const useI18n = (): { 57 | // t: I18nGlobalTranslation 58 | // } => { 59 | // const normalFn = { 60 | // t: (key: string) => { 61 | // return key 62 | // } 63 | // } 64 | // if (!i18n) { 65 | // return normalFn 66 | // } 67 | // const { t, ...methods } = i18n.global 68 | // const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { 69 | // if (!key) return '' 70 | // return t(key, ...(arg as I18nTranslationRestParameters)) 71 | // } 72 | // return { 73 | // ...methods, 74 | // t: tFn 75 | // } 76 | // } 77 | -------------------------------------------------------------------------------- /web/src/i18n/langs/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | hello: "你好" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/layout/Dynamic.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /web/src/layout/ExternalLink.vue: -------------------------------------------------------------------------------- 1 | 19 | 56 | -------------------------------------------------------------------------------- /web/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 16 | 26 | 51 | 52 | 60 | -------------------------------------------------------------------------------- /web/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 14 | 31 | 32 | 61 | -------------------------------------------------------------------------------- /web/src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 32 | -------------------------------------------------------------------------------- /web/src/layout/components/rightMenu/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 19 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebar/AppLink.vue: -------------------------------------------------------------------------------- 1 | 12 | 43 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 7 | 27 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 14 | 22 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 91 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 38 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebarH/AppLink.vue: -------------------------------------------------------------------------------- 1 | 12 | 43 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebarH/Item.vue: -------------------------------------------------------------------------------- 1 | 7 | 27 | -------------------------------------------------------------------------------- /web/src/layout/components/sidebarH/Logo.vue: -------------------------------------------------------------------------------- 1 | 11 | 15 | -------------------------------------------------------------------------------- /web/src/layout/components/tagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 61 | 89 | -------------------------------------------------------------------------------- /web/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 44 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import "animate.css"; 2 | import "@/styles/global.scss"; 3 | import "element-plus/dist/index.css"; 4 | import "virtual:svg-icons-register"; // 引入注册脚本 5 | import { createApp } from "vue"; 6 | import { setupStore } from "./store"; 7 | import { setupRouter } from "./router"; 8 | import { setupI18n } from "./i18n"; 9 | import { setupElementPlus } from "./plugins/element"; 10 | import { setupIcon } from "./components/SvgIcon"; 11 | import { setupDirectivs } from "./directives"; 12 | import App from "./App.vue"; 13 | import "./permission"; 14 | const setup = async (): Promise => { 15 | const app = createApp(App); 16 | await setupI18n(app); 17 | setupStore(app); 18 | setupRouter(app); 19 | setupElementPlus(app); 20 | setupIcon(app); 21 | setupDirectivs(app); 22 | app.mount("#app"); 23 | }; 24 | setup(); 25 | -------------------------------------------------------------------------------- /web/src/permission.ts: -------------------------------------------------------------------------------- 1 | import router from "./router"; 2 | import type { RouteRecordRaw } from "vue-router"; 3 | import { useTitle } from "@/utils/title"; 4 | import { useNProgress } from "@/plugins/nProgress"; 5 | import { usePermissionStoreWithOut } from "@/store/modules/permission"; 6 | // import { useAppStoreWithOut } from "@/store/modules/app"; 7 | import { useUserStoreWithOut } from "@/store/modules/user"; 8 | import { getToken } from "@/utils/auth"; 9 | // import { setLocaleMessages } from "./i18n"; 10 | import common from "@/common/index"; 11 | const permissionStore = usePermissionStoreWithOut(); 12 | const userStore = useUserStoreWithOut(); 13 | // const appStore = useAppStoreWithOut(); 14 | const { start, done } = useNProgress(); 15 | const whiteList = ["/redirect", "/login", "/icons", "/noRoutes", "/404"]; // 不重定向白名单 16 | const appTitle = import.meta.env.VITE_APP_TITLE; // 配置在env文件里 17 | router.beforeEach(async (to, from, next) => { 18 | start(); 19 | const title = (appTitle + to?.meta?.title) as string; 20 | useTitle(title); 21 | let hasToken: null | string = null; 22 | hasToken = getToken(); 23 | if (hasToken) { 24 | if (to.path === "/login") { 25 | // 登录页直接跳转到主页 26 | next({ path: "/" }); 27 | } else { 28 | // 获取权限 29 | const role = userStore.getRole; 30 | if (role && role.length > 0) { 31 | next(); 32 | } else { 33 | try { 34 | const res: any = await userStore.getInfo(); 35 | const accessRoutes = await permissionStore.generateRoutes(res.role); 36 | // 获取当前用户权限可以访问的路由 37 | if (!accessRoutes.length) { 38 | const tipMessage = "当前用户无菜单权限,你需要重新登录"; 39 | common.tip(tipMessage, "error"); 40 | await userStore.logout(); 41 | next(`/noRoutes`); 42 | return; 43 | } else { 44 | // setLocaleMessages(res.param.lang, res.i18n); // 设置多语言 45 | // appStore.setParam(res.param); 46 | accessRoutes.forEach((route) => { 47 | router.addRoute(route as unknown as RouteRecordRaw); // 动态添加可访问路由表 48 | }); 49 | next({ ...to, replace: true }); 50 | } 51 | } catch (error) { 52 | console.log(error); 53 | // 出问题时清空token,然后返回到登录页 54 | const tipMessage = "获取用户信息失败,请联系管理人员"; 55 | common.tip(tipMessage, "error"); 56 | await userStore.logout(); 57 | next(`/login?redirect=${to.path}`); 58 | } 59 | } 60 | } 61 | } else { 62 | /* 没有token,未登录*/ 63 | if (whiteList.indexOf(to.path) !== -1) { 64 | // 白名单 登录和重定向页 65 | next(); 66 | } else { 67 | next(`/login?redirect=${to.path}`); // 不在白名单中,跳转到登录授权 68 | } 69 | } 70 | }); 71 | router.afterEach((to) => { 72 | done(); // 结束Progress 73 | }); 74 | -------------------------------------------------------------------------------- /web/src/plugins/element.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import ElementPlus from 'element-plus' 3 | export const setupElementPlus = async (app: App) => { 4 | app.use(ElementPlus); 5 | }; -------------------------------------------------------------------------------- /web/src/plugins/nProgress.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, unref } from "vue"; 2 | import type { NProgressOptions } from "nprogress"; 3 | import NProgress from "nprogress"; 4 | import "nprogress/nprogress.css"; 5 | import { useCssVar } from "@vueuse/core"; 6 | const primaryColor = useCssVar("--el-color-primary", document.documentElement); 7 | export const useNProgress = () => { 8 | NProgress.configure({ showSpinner: false } as NProgressOptions); 9 | const initColor = async () => { 10 | await nextTick(); 11 | const bar = document 12 | .getElementById("nprogress") 13 | ?.getElementsByClassName("bar")[0] as ElRef; 14 | if (bar) { 15 | bar.style.background = unref(primaryColor.value); 16 | } 17 | }; 18 | initColor(); 19 | const start = () => { 20 | NProgress.start(); 21 | }; 22 | const done = () => { 23 | NProgress.done(); 24 | }; 25 | return { 26 | start, 27 | done, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /web/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import type { RouteRecordRaw } from "vue-router"; 3 | import type { App } from "vue"; 4 | import Layout from "@/layout/index.vue"; 5 | export const constantRouterMap: AppRouteRecord[] = [ 6 | // { 7 | // path: "/zj", 8 | // name: "zj", 9 | // component: () => import("../views/ZjView.vue"), 10 | // }, 11 | { 12 | path: "/redirect", 13 | component: Layout, 14 | hidden: true, 15 | name: "redirect", 16 | children: [ 17 | { 18 | path: "/redirect/:path(.*)", 19 | component: () => import("../views/redirect/index.vue"), 20 | }, 21 | ], 22 | }, 23 | { 24 | path: "/login", 25 | name: "login", 26 | component: () => import("../views/login/index.vue"), 27 | hidden: true, 28 | }, 29 | { 30 | path: "/icons", 31 | name: "icons", 32 | component: Layout, 33 | redirect: "/icons/all", 34 | icon: "money", 35 | label: "图标", 36 | children: [ 37 | { 38 | path: "/icons/all", 39 | component: () => import("@/views/icons/index.vue"), 40 | menuType: "static", 41 | label: "全部图标", 42 | name: "iconsAll", 43 | }, 44 | ], 45 | hidden: true, 46 | }, 47 | { 48 | path: "/404", 49 | component: () => import("@/views/login/404.vue"), 50 | name: "404", 51 | hidden: true, 52 | }, 53 | { 54 | path: "/noRoutes", 55 | component: () => import("@/views/login/NoRoutes.vue"), 56 | name: "noRoutes", 57 | hidden: true, 58 | }, 59 | { 60 | path: "/", 61 | name: "base", 62 | hidden: false, 63 | component: Layout, 64 | redirect: "/work-bench", 65 | icon: "money", 66 | children: [ 67 | { 68 | path: "/work-bench", 69 | component: () => import("@/views/dashboard/index.vue"), 70 | menuType: "static", 71 | label: "工作台", 72 | name: "workbench", 73 | icon: "money", 74 | meta: { affix: true }, 75 | }, 76 | ], 77 | }, 78 | ]; 79 | const router = createRouter({ 80 | history: createWebHashHistory(), 81 | strict: true, 82 | routes: constantRouterMap as RouteRecordRaw[], 83 | scrollBehavior: () => ({ left: 0, top: 0 }), 84 | }); 85 | export const resetRouter = (): void => { 86 | const resetWhiteNameList = [ 87 | "redirect", 88 | "login", 89 | "icons", 90 | "noRoutes", 91 | "404", 92 | "base", 93 | "workbench", 94 | ]; 95 | router.getRoutes().forEach((route) => { 96 | const name = route.name as string; 97 | if (name && !resetWhiteNameList.includes(name as string)) { 98 | router.hasRoute(name) && router.removeRoute(name); 99 | } 100 | }); 101 | }; 102 | export const setupRouter = (app: App) => { 103 | app.use(router); 104 | }; 105 | export default router; 106 | -------------------------------------------------------------------------------- /web/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from "pinia"; 2 | import type { App } from "vue"; 3 | const store = createPinia(); 4 | const setupStore = (app: App) => { 5 | app.use(store); 6 | }; 7 | export { store, setupStore }; 8 | -------------------------------------------------------------------------------- /web/src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "../index"; 3 | import { appModules } from "@/config"; 4 | import { useStorage } from "@/utils/storage"; 5 | const storage = useStorage(); 6 | export const useAppStore = defineStore({ 7 | id: "app", 8 | state: (): AppState => appModules, 9 | getters: { 10 | getShowSettings(): boolean { 11 | return this.showSettings; 12 | }, 13 | getCollapse(): boolean { 14 | return this.collapse; 15 | }, 16 | getSize(): ElememtPlusSize { 17 | return this.size; 18 | }, 19 | getLocale(): string { 20 | return this.locale; 21 | }, 22 | getLogo(): boolean { 23 | return this.logo; 24 | }, 25 | getGreyMode(): boolean { 26 | return this.greyMode; 27 | }, 28 | getLayout(): LayoutType { 29 | return this.layout; 30 | }, 31 | getTitle(): string { 32 | return this.title; 33 | }, 34 | getTheme(): string { 35 | return this.theme; 36 | }, 37 | getPrimaryColor(): string { 38 | return this.primaryColor; 39 | }, 40 | getNavHeadBgColor(): string { 41 | return this.navHeadBgColor; 42 | }, 43 | getParam(): object { 44 | return this.param; 45 | }, 46 | }, 47 | actions: { 48 | setShowSettings(showSettings: boolean) { 49 | this.showSettings = showSettings; 50 | }, 51 | setCollapse(collapse: boolean) { 52 | this.collapse = collapse; 53 | }, 54 | setSize(size: ElememtPlusSize) { 55 | this.size = size; 56 | }, 57 | setLocale(locale: string) { 58 | this.locale = locale; 59 | storage.set("locale", this.locale); 60 | }, 61 | setLogo(logo: boolean) { 62 | this.logo = logo; 63 | }, 64 | setGreyMode(greyMode: boolean) { 65 | this.greyMode = greyMode; 66 | }, 67 | setLayout(layout: LayoutType) { 68 | this.layout = layout; 69 | storage.set("layout", this.layout); 70 | }, 71 | setTitle(title: string) { 72 | this.title = title; 73 | }, 74 | setTheme(theme: string) { 75 | this.theme = theme; 76 | storage.set("theme", this.theme); 77 | }, 78 | setPrimaryColor(primaryColor: string) { 79 | this.primaryColor = primaryColor; 80 | storage.set("primaryColor", this.primaryColor); 81 | }, 82 | setNavHeadBgColor(navHeadBgColor: string) { 83 | this.navHeadBgColor = navHeadBgColor; 84 | storage.set("navHeadBgColor", this.navHeadBgColor); 85 | }, 86 | setParam(param: object) { 87 | this.param = param; 88 | }, 89 | }, 90 | }); 91 | export const useAppStoreWithOut = () => { 92 | return useAppStore(store); 93 | }; 94 | -------------------------------------------------------------------------------- /web/src/store/modules/app1.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "../index"; 3 | import { appModules } from '@/config' 4 | export const useAppStore = defineStore({ 5 | id: "app1", 6 | state() { 7 | return { 8 | ...appModules, 9 | locale: "zh-CN", 10 | app: "zj", 11 | count: 1, 12 | }; 13 | }, 14 | getters: { 15 | doubleCount(): number { 16 | return this.count * 2; 17 | }, 18 | }, 19 | actions: { 20 | setApp(data: string): void { 21 | this.app = data; 22 | }, 23 | setCount(data: number): void { 24 | this.count = data; 25 | }, 26 | }, 27 | }); 28 | export const useAppStoreWithOut = () => { 29 | return useAppStore(store); 30 | }; 31 | -------------------------------------------------------------------------------- /web/src/store/modules/counter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | export const useCounterStore = defineStore('counter', () => { 4 | const count = ref(0) 5 | const doubleCount = computed(() => count.value * 2) 6 | function increment() { 7 | count.value++ 8 | } 9 | return { count, doubleCount, increment } 10 | }) 11 | -------------------------------------------------------------------------------- /web/src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "../index"; 3 | // import { login, getCurrentUserInfo } from "@/api/user/login"; 4 | import { login, getCurrentUserInfo } from "@/api/system/user"; 5 | import { getToken, setToken, removeToken } from "@/utils/auth"; 6 | import { resetRouter } from "@/router"; 7 | import { useTagsViewStoreWithOut } from "@/store/modules/tagsView"; 8 | let tagsViewStore: any = null; 9 | export const useUserStore = defineStore({ 10 | id: "user", 11 | state: () => { 12 | return { 13 | token: getToken() || "", 14 | info: {}, 15 | role: "", 16 | roleList: [], 17 | }; 18 | }, 19 | getters: { 20 | getToken(): string { 21 | return this.token; 22 | }, 23 | getRole(): string { 24 | return this.role; 25 | }, 26 | getRoleList(): object[] { 27 | return this.roleList; 28 | }, 29 | getUserInfo(): any { 30 | return this.info; 31 | }, 32 | }, 33 | actions: { 34 | login(userInfo): Promise { 35 | const { username, password } = userInfo; 36 | return new Promise(async (resolve, reject) => { 37 | try { 38 | const res = await login({ 39 | username: username.trim(), 40 | password: password, 41 | deviceType: "PC", 42 | }); 43 | const { data } = res; 44 | this.token = data.accessToken; 45 | setToken(data.accessToken); 46 | resolve(true); 47 | } catch (error) { 48 | reject(error); 49 | } 50 | }); 51 | }, 52 | getInfo(): Promise { 53 | return new Promise(async (resolve, reject) => { 54 | // const res = await getCurrentUserInfo(this.token); 55 | const res = await getCurrentUserInfo(); 56 | const { data } = res; 57 | if (!data) { 58 | reject("验证失败,请重新登录."); 59 | } 60 | // 测试 61 | this.info = { 62 | ...data, 63 | postionName: "项目经理", 64 | departmentName: "研发中心", 65 | }; 66 | this.role = data.role; 67 | // 角色列表,多角色切换 68 | this.roleList = [ 69 | { id: "admin", label: "管理员" }, 70 | { id: "test", label: "测试员" }, 71 | { id: "editor", label: "编辑员" }, 72 | ] as any; 73 | resolve(data); 74 | }); 75 | }, 76 | logout(): Promise { 77 | return new Promise(async (resolve) => { 78 | this.token = ""; 79 | this.role = ""; 80 | removeToken(); 81 | resetRouter(); 82 | if (!tagsViewStore) { 83 | tagsViewStore = useTagsViewStoreWithOut(); 84 | } 85 | await tagsViewStore.delAllViews(); 86 | resolve(true); 87 | }); 88 | }, 89 | }, 90 | }); 91 | export const useUserStoreWithOut = () => { 92 | return useUserStore(store); 93 | }; 94 | -------------------------------------------------------------------------------- /web/src/styles/elementHack.scss: -------------------------------------------------------------------------------- 1 | .el-dialog__header { 2 | background-color: #f5f5f5; 3 | border-bottom: 1px solid #e6e6e6; 4 | margin-right: 0px !important; 5 | .headerLine { 6 | width: calc(100% - 30px); 7 | } 8 | .dialogTitleIcon { 9 | margin-right: 10px; 10 | } 11 | .fullscreenIcon { 12 | cursor: pointer; 13 | } 14 | .el-dialog__headerbtn { 15 | top: 20px !important; 16 | width: 30px; 17 | height: 30px; 18 | right: 10px; 19 | } 20 | .el-dialog__close { 21 | font-size: 20px; 22 | font-weight: bold; 23 | } 24 | } 25 | .el-form-item__content { 26 | .el-select, 27 | .el-autocomplete, 28 | .el-input-number, 29 | .el-select-v2, 30 | .el-cascader, 31 | .el-switch, 32 | .el-rate, 33 | .el-color-picker, 34 | .el-transfer, 35 | .el-radio-group, 36 | .el-checkbox-group, 37 | .el-slider, 38 | .el-date-editor { 39 | width: 100%!important; 40 | } 41 | } 42 | // wang editor hack 43 | .w-e-full-screen-container { 44 | z-index: 1008611; 45 | } 46 | -------------------------------------------------------------------------------- /web/src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import './var.css'; 2 | @import './transition.scss'; 3 | @import './sidebar.scss'; 4 | @import './tagsView.scss'; 5 | @import 'main.scss'; 6 | @import 'element-plus/theme-chalk/dark/css-vars.css'; 7 | @import './elementHack.scss'; 8 | *{ 9 | position: relative; 10 | margin: 0px; 11 | padding: 0px; 12 | box-sizing: border-box; 13 | } 14 | html, body, #app{ 15 | width: 100%; 16 | height: 100%; 17 | overflow: hidden; 18 | } 19 | a { 20 | overflow: hidden; 21 | text-decoration: none; 22 | background: none; 23 | &:focus, 24 | &:hover { 25 | text-decoration: none; 26 | } 27 | } 28 | ::-webkit-scrollbar { 29 | width: 6px !important; 30 | } 31 | ::-webkit-scrollbar-thumb { 32 | background-color: rgba(220, 228, 243, 1) !important; 33 | border-radius: 4px !important; 34 | } 35 | ::-webkit-scrollbar-track { 36 | border-radius: 0 !important; 37 | } 38 | ::-webkit-scrollbar:horizontal { 39 | height: 6px !important; 40 | background-color: transparent !important; 41 | } 42 | .ub{ 43 | display: flex!important; 44 | } 45 | .uhide{ 46 | display:none !important; 47 | } 48 | .ub-rev{ 49 | flex-direction: column; 50 | } 51 | .ub-as{ 52 | align-items: flex-start; 53 | } 54 | .ub-ac{ 55 | align-items: center; 56 | } 57 | .ub-ae{ 58 | align-items: flex-end; 59 | } 60 | .ub-ps{ 61 | justify-content: flex-start; 62 | } 63 | .ub-pc{ 64 | justify-content: center; 65 | } 66 | .ub-pe{ 67 | justify-content: flex-end; 68 | } 69 | .ub-pj{ 70 | justify-content: space-between; 71 | } 72 | .ub-pa{ 73 | justify-content: space-around; 74 | } 75 | .ub-wrap{ 76 | flex-wrap: wrap; 77 | } 78 | .ub-f1{ 79 | flex: 1; 80 | width: 0px; 81 | } 82 | .ub-f2{ 83 | flex: 2; 84 | width: 0px; 85 | } 86 | $color-gray: #cccccc; -------------------------------------------------------------------------------- /web/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | #app{ 2 | .main-container{ 3 | height: 100%; 4 | overflow: hidden; 5 | transition: margin-left .28s; 6 | margin-left: var(--sideBarWidth); 7 | background-color: #f7f7f7; 8 | &.menuPosTop{ 9 | margin-left: 0px !important; 10 | } 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /web/src/styles/tagsView.scss: -------------------------------------------------------------------------------- 1 | .tags-view-container { 2 | height: 41px; 3 | padding: 6px 16px 0px; 4 | width: 100%; 5 | background: #f7f7f7; 6 | z-index: 999; 7 | .tags-view-wrapper { 8 | .tags-view-item { 9 | display: inline-block; 10 | position: relative; 11 | cursor: pointer; 12 | min-width: 150px; 13 | height: 35px; 14 | line-height: 35px; 15 | text-align: center; 16 | color: #808080; 17 | background-color: #f7f7f7; 18 | padding: 0 26px 0 8px; 19 | font-size: 14px; 20 | border-top-left-radius: 5px; 21 | border-top-right-radius: 5px; 22 | vertical-align: top; 23 | &:last-of-type { 24 | margin-right: 15px; 25 | } 26 | &.active { 27 | box-shadow: 3px 3px 8px 0px rgba(0, 0, 0, 0.1) !important; 28 | color: var(--el-color-primary); 29 | background-color: #fff; 30 | z-index: 200; 31 | &::before { 32 | content: ""; 33 | background: #fff; 34 | position: absolute; 35 | width: 8px; 36 | height: 2px; 37 | bottom: 0px; 38 | left: -5px; 39 | transform: rotateZ(-17deg); 40 | } 41 | &::after { 42 | content: ""; 43 | background-color: #fff; 44 | position: absolute; 45 | width: 8px; 46 | height: 2px; 47 | bottom: 0px; 48 | right: -5px; 49 | transform: rotateZ(19deg); 50 | z-index: 100; 51 | } 52 | } 53 | .el-icon-close { 54 | width: 16px; 55 | height: 16px; 56 | vertical-align: 2px; 57 | border-radius: 50%; 58 | text-align: center; 59 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 60 | transform-origin: 100% 50%; 61 | position: absolute; 62 | right: 10px; 63 | top: 25%; 64 | line-height: 12px; 65 | &:before { 66 | transform: scale(0.6); 67 | display: inline-block; 68 | vertical-align: -2px; 69 | } 70 | &:hover { 71 | background-color: #b4bccc; 72 | color: #fff; 73 | } 74 | } 75 | } 76 | } 77 | .contextmenu { 78 | margin: 0; 79 | background: #fff; 80 | z-index: 3000; 81 | position: absolute; 82 | list-style-type: none; 83 | padding: 5px 0; 84 | border-radius: 4px; 85 | font-size: 12px; 86 | font-weight: 400; 87 | color: #333; 88 | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); 89 | li { 90 | margin: 0; 91 | padding: 7px 16px; 92 | cursor: pointer; 93 | &:hover { 94 | background: #eee; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /web/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /web/src/styles/var.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-bg-color: #293146; 3 | --menuText: #ffffff; 4 | --menuActiveText: #409eff; 5 | --menuBg: #283c55; 6 | --menuHover: #1a293c; 7 | --subMenuBg: #193152; 8 | --subMenuHover: #001528; 9 | --navHeadBgColor: #ffffff; 10 | --navHeadTextColor: #303133; 11 | --sideBarWidth: 210px; 12 | } 13 | -------------------------------------------------------------------------------- /web/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | // import Cookies from 'js-cookie' 2 | const TokenKey = "sys-token"; 3 | export function getToken(): string | null { 4 | //return Cookies.get(TokenKey) 5 | // return window.localStorage.getItem(TokenKey); 6 | return window.sessionStorage.getItem(TokenKey); 7 | } 8 | export function setToken(token) { 9 | //return Cookies.set(TokenKey, token) 10 | // return window.localStorage.setItem(TokenKey, token); 11 | return window.sessionStorage.setItem(TokenKey, token); 12 | } 13 | export function removeToken() { 14 | //return Cookies.remove(TokenKey) 15 | // return window.localStorage.removeItem(TokenKey) 16 | return window.sessionStorage.removeItem(TokenKey); 17 | } 18 | -------------------------------------------------------------------------------- /web/src/utils/deepClone.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(obj) { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | -------------------------------------------------------------------------------- /web/src/utils/fullScreen.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, unref, watch, nextTick } from "vue"; 2 | const isFullscreen: Ref = ref(false); 3 | const toggleFull = () => { 4 | isFullscreen.value = !unref(isFullscreen); 5 | }; 6 | const setFullscreenWatch = (dialogHeight, defaultHeight) => { 7 | watch( 8 | () => isFullscreen.value, 9 | async (val: boolean) => { 10 | await nextTick(); 11 | if (val) { 12 | const windowHeight = document.documentElement.offsetHeight; 13 | dialogHeight.value = `${windowHeight - 55 - 60 - 63}px`; 14 | } else { 15 | dialogHeight.value = defaultHeight; 16 | } 17 | }, 18 | { 19 | immediate: true, 20 | } 21 | ); 22 | }; 23 | const useFullscreen = () => ({ 24 | isFullscreen, 25 | toggleFull, 26 | setFullscreenWatch, 27 | }); 28 | export default useFullscreen; 29 | 30 | -------------------------------------------------------------------------------- /web/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const setCssVar = ( 2 | prop: string, 3 | val: any, 4 | dom = document.documentElement 5 | ) => { 6 | dom.style.setProperty(prop, val); 7 | }; 8 | export const trim = (str: string) => { 9 | return str.replace(/(^\s*)|(\s*$)/g, ""); 10 | }; 11 | function _basePath(path) { 12 | // 若是数组,则直接返回 13 | if (Array.isArray(path)) return path; 14 | return path.split("."); 15 | } 16 | export const lodashSet = function (object, path, value) { 17 | if (typeof object !== "object") return object; 18 | _basePath(path).reduce((o, k, i, _) => { 19 | if (i === _.length - 1) { 20 | // 若遍历结束直接赋值 21 | o[k] = value; 22 | return null; 23 | } else if (k in o) { 24 | // 若存在对应路径,则返回找到的对象,进行下一次遍历 25 | return o[k]; 26 | } else { 27 | // 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象 28 | o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {}; 29 | return o[k]; 30 | } 31 | }, object); 32 | return object; 33 | }; 34 | -------------------------------------------------------------------------------- /web/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | InternalAxiosRequestConfig, 4 | AxiosResponse, 5 | AxiosError, 6 | } from "axios"; 7 | import { getToken } from "@/utils/auth"; 8 | import common from "@/common/index"; 9 | import { i18n } from "@/i18n"; 10 | import { useUserStoreWithOut } from "@/store/modules/user"; 11 | import router from "@/router"; 12 | const baseUrl = import.meta.env.VITE_API_BASEPATH; // 配置在env文件里 13 | // console.log(baseUrl) 14 | const service: AxiosInstance = axios.create({ 15 | baseURL: baseUrl, // 自己的server服务地址 16 | withCredentials: false, // send cookies when cross-domain requests 17 | timeout: 500000000000, // request timeout 18 | }); 19 | service.interceptors.request.use( 20 | (config: InternalAxiosRequestConfig) => { 21 | const token = getToken(); 22 | if (token) { 23 | config.headers["Authorization"] = "Bearer " + token; 24 | } 25 | return config; 26 | }, 27 | (error) => { 28 | return Promise.reject(error); 29 | } 30 | ); 31 | service.interceptors.response.use( 32 | (response: AxiosResponse) => { 33 | const { t } = i18n.global; 34 | // 当前后台将code统一放到response.data中,方便前台直接处理业务逻辑 35 | const res = response.data; 36 | const code = res.code; 37 | if (res.type === "application/octet-stream" || res.type === "application/x-msdownload") return response; // 文件流没有code 38 | if (code !== 200) { 39 | let tipMessage = ""; 40 | res.message.split(";").forEach((res) => { 41 | tipMessage += `

${t(res)}

`; 42 | }); 43 | common.tip(tipMessage, "error"); 44 | if (code === 10004) { 45 | // to re-login 46 | setTimeout(() => { 47 | backToLogin(); 48 | }, 1000); 49 | } else { 50 | common.hideLoading(); 51 | } 52 | return Promise.reject( 53 | new Error("程序发现错误---" + res.message || "出错啦") 54 | ); 55 | } else { 56 | return res; 57 | } 58 | }, 59 | (error: AxiosError) => { 60 | common.hideLoading(); 61 | if (!error.response) { 62 | // 连接超时 63 | common.tip("连接超时~是不是服务没启??", "error"); 64 | } else if (error.response && error.response.data) { 65 | let message = (error.response.data as any).message; 66 | common.tip(message, "error"); 67 | } 68 | setTimeout(() => { 69 | backToLogin(); 70 | }, 2000); 71 | return Promise.reject(error); 72 | } 73 | ); 74 | function backToLogin() { 75 | const userStore = useUserStoreWithOut(); 76 | userStore.logout(); 77 | common.hideLoading(); 78 | router.push("/login"); 79 | location.reload(); 80 | } 81 | export default service; 82 | -------------------------------------------------------------------------------- /web/src/utils/resize.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener, useWindowSize } from '@vueuse/core' 2 | import { onMounted } from 'vue' 3 | export const useGridResize = (tableHeight, offset) => { 4 | const handleResize = () => { 5 | const { height } = useWindowSize() 6 | tableHeight.value = height.value - offset 7 | } 8 | onMounted(() => { 9 | useEventListener('resize', handleResize) 10 | setTimeout(() => { 11 | handleResize() 12 | }, 100) 13 | }) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /web/src/utils/slot.ts: -------------------------------------------------------------------------------- 1 | import type { Slots } from "vue"; 2 | import { isFunction } from "@/utils/is"; 3 | /* 4 | slots setup函数的slots,所有的一级插槽 5 | slot 具体的插槽名,template #aaa='scope'中的aaa 6 | data 上面模板中的scope,比如el-table-column中template指定的scope,里面有row,column,$index这些 7 | */ 8 | export const getSlot = (slots: Slots, slot = "default", data?: Recordable) => { 9 | // Reflect.has 判断一个对象是否存在某个属性 10 | if (!slots || !Reflect.has(slots, slot)) { 11 | return null; 12 | } 13 | // slots[slot]必须是函数 14 | if (!isFunction(slots[slot])) { 15 | console.error(`${slot} is not a function!`); 16 | return null; 17 | } 18 | const slotFn = slots[slot]; 19 | if (!slotFn) return null; 20 | return slotFn(data); 21 | }; 22 | -------------------------------------------------------------------------------- /web/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export const useStorage = (type = "localStorage") => { 2 | const storage = window[type]; 3 | return { 4 | set(key, value) { 5 | if (typeof value == "object") { 6 | value = JSON.stringify(value); 7 | } 8 | storage.setItem(key, value); 9 | }, 10 | get(key) { 11 | let value = storage.getItem(key); 12 | try { 13 | let res = JSON.parse(value); 14 | return res; 15 | } catch (error) { 16 | return value; 17 | } 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /web/src/utils/title.ts: -------------------------------------------------------------------------------- 1 | import { watch, ref } from 'vue' 2 | import { isString } from '@/utils/is' 3 | import { useAppStoreWithOut } from '@/store/modules/app' 4 | const appStore = useAppStoreWithOut() 5 | export const useTitle = (newTitle?: string) => { 6 | const title = ref( 7 | newTitle ? `${appStore.getTitle} - ${newTitle as string}` : appStore.getTitle 8 | ) 9 | watch( 10 | title, 11 | (n, o) => { 12 | if (isString(n) && n !== o && document) { 13 | document.title = n 14 | } 15 | }, 16 | { immediate: true } 17 | ) 18 | return title 19 | } 20 | -------------------------------------------------------------------------------- /web/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | type Callback = (error?: string | Error | undefined) => void 2 | interface LengthRange { 3 | min: number 4 | max: number 5 | message: string 6 | } 7 | export const useValidator = () => { 8 | const required = (message?: string) => { 9 | return { 10 | required: true, 11 | message: message || '必填项' 12 | } 13 | } 14 | const lengthRange = (val: any, callback: Callback, options: LengthRange) => { 15 | const { min, max, message } = options 16 | if (val.length < min || val.length > max) { 17 | callback(new Error(message)) 18 | } else { 19 | callback() 20 | } 21 | } 22 | const notSpace = (val: any, callback: Callback, message: string) => { 23 | // 用户名不能有空格 24 | if (val.indexOf(' ') !== -1) { 25 | callback(new Error(message)) 26 | } else { 27 | callback() 28 | } 29 | } 30 | const notSpecialCharacters = (val: any, callback: Callback, message: string) => { 31 | // 密码不能是特殊字符 32 | if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { 33 | callback(new Error(message)) 34 | } else { 35 | callback() 36 | } 37 | } 38 | // 两个字符串是否想等 39 | const isEqual = (val1: string, val2: string, callback: Callback, message: string) => { 40 | if (val1 === val2) { 41 | callback() 42 | } else { 43 | callback(new Error(message)) 44 | } 45 | } 46 | return { 47 | required, 48 | lengthRange, 49 | notSpace, 50 | notSpecialCharacters, 51 | isEqual 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /web/src/views/ZjView.vue: -------------------------------------------------------------------------------- 1 | 12 | 22 | 23 | -------------------------------------------------------------------------------- /web/src/views/icons/element-icons.ts: -------------------------------------------------------------------------------- 1 | import * as ElementPlusIconsVue from "@element-plus/icons-vue"; 2 | const modules: any = []; 3 | Object.keys(ElementPlusIconsVue).forEach(key => { 4 | modules.push(key) 5 | }) 6 | export default modules 7 | -------------------------------------------------------------------------------- /web/src/views/icons/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 64 | 100 | -------------------------------------------------------------------------------- /web/src/views/icons/svg-icons.ts: -------------------------------------------------------------------------------- 1 | const svgs = import.meta.glob('../../assets/svgs/*.svg') 2 | const modules: any = []; 3 | Object.keys(svgs).forEach((key: string) => { 4 | const splitBuffer = key.split("/"); 5 | const svgName = splitBuffer[splitBuffer.length - 1]; 6 | const name = svgName.split(".")[0] 7 | modules.push(name) 8 | }) 9 | export default modules -------------------------------------------------------------------------------- /web/src/views/login/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 8 | -------------------------------------------------------------------------------- /web/src/views/login/NoRoutes.vue: -------------------------------------------------------------------------------- 1 | 6 | 8 | -------------------------------------------------------------------------------- /web/src/views/login/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 66 | 84 | -------------------------------------------------------------------------------- /web/src/views/login/components/index.ts: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm.vue' 2 | export { LoginForm } 3 | -------------------------------------------------------------------------------- /web/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 30 | 71 | -------------------------------------------------------------------------------- /web/src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 26 | -------------------------------------------------------------------------------- /web/src/views/system/menu/components/IconDialog.vue: -------------------------------------------------------------------------------- 1 | 13 | 38 | 65 | -------------------------------------------------------------------------------- /web/src/views/system/role/components/PermissionDialog.vue: -------------------------------------------------------------------------------- 1 | 13 | 63 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "baseUrl": ".", 15 | "forceConsistentCasingInFileNames": true, 16 | "allowSyntheticDefaultImports": true, 17 | "extendedDiagnostics": true, 18 | "strictFunctionTypes": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "experimentalDecorators": true, 22 | "noImplicitAny": false, 23 | "skipLibCheck": true, 24 | "paths": { 25 | "@/*": ["src/*"] 26 | }, 27 | "types": [ 28 | "vite/client", 29 | "element-plus/global", 30 | "vite-plugin-svg-icons/client" 31 | ], 32 | "typeRoots": ["./node_modules/@types/", "./types"] 33 | }, 34 | "include": ["src/**/*", "types/**/*.d.ts", "mock/**/*.ts"], 35 | "exclude": ["dist", "node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /web/types/app.d.ts: -------------------------------------------------------------------------------- 1 | declare type LayoutType = 'top' | 'left' 2 | declare interface AppState { 3 | param: object, 4 | collapse: boolean 5 | locale: string 6 | logo: boolean 7 | greyMode: boolean 8 | layout: LayoutType 9 | title: string 10 | size: ElememtPlusSize 11 | theme: string, 12 | primaryColor: string, 13 | navHeadBgColor: string, 14 | showSettings: boolean 15 | } 16 | -------------------------------------------------------------------------------- /web/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Fn { 2 | (...arg: T[]): T 3 | } 4 | declare type Nullable = T | null 5 | declare type ElRef = Nullable 6 | declare type ElememtPlusSize = 'default' | 'small' | 'large' 7 | declare type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger' 8 | declare type Recordable = Record 9 | declare type ComponentRef = InstanceType 10 | declare type LocaleType = 'zh-CN' | 'en' 11 | declare type AxiosHeaders = 12 | | 'application/json' 13 | | 'application/x-www-form-urlencoded' 14 | | 'multipart/form-data' 15 | declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put' 16 | declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' 17 | declare interface AxiosConfig { 18 | params?: any 19 | data?: any 20 | url?: string 21 | method?: AxiosMethod 22 | headersType?: string 23 | responseType?: AxiosResponseType 24 | } 25 | declare interface IResponse { 26 | code: string 27 | data: T extends any ? T : T & any 28 | } 29 | -------------------------------------------------------------------------------- /web/types/grid.d.ts: -------------------------------------------------------------------------------- 1 | declare type AlignName = 2 | | "left" 3 | | "center" 4 | | "right"; 5 | declare type GridColumn = { 6 | prop: string 7 | label: string 8 | width?: string | number, 9 | sortable?: boolean, 10 | align?: AlignName 11 | } -------------------------------------------------------------------------------- /web/types/router.d.ts: -------------------------------------------------------------------------------- 1 | type Component = 2 | | ReturnType 3 | | (() => Promise) 4 | | (() => Promise); 5 | declare interface AppRouteRecord { 6 | aliveName?: string; 7 | name?: string; 8 | menuType?: string; 9 | label?: string; 10 | component?: string | Component; 11 | path?: string; 12 | icon?: string; 13 | redirect?: string; 14 | hidden?: boolean; 15 | children?: AppRouteRecord[]; 16 | meta?: object; 17 | linkAddress?: string; 18 | dynamicPage?: string; 19 | } 20 | -------------------------------------------------------------------------------- /web/types/searchField.d.ts: -------------------------------------------------------------------------------- 1 | declare type SearchFieldComponentName = 2 | | "Input" 3 | | "Select" 4 | | "TimePicker" 5 | | "DatePicker"; 6 | // YYYY-MM-DD 7 | declare type extendPropsObj = { 8 | options?: OptionProp[]; 9 | format?: string; 10 | // formatter?: Fn; 11 | }; 12 | // select,radio,checkbox这些 13 | declare type OptionProp = { 14 | value: string | number; 15 | label: string | number; 16 | }; 17 | declare type SearchFieldSchema = { 18 | // 唯一值 19 | field: string; 20 | // 标题 21 | label?: string; 22 | // 渲染的组件 23 | component?: ComponentName; 24 | // 初始值 25 | value?: FormValueType; 26 | // 远程加载下拉项 27 | api?: () => AxiosPromise; 28 | // 宽度占位 29 | span?: number; 30 | // 非通用选项 31 | extendProps?: extendPropsObj; 32 | // 标签宽度 33 | labelWidth?: string | number; 34 | // placeholder 35 | placeholder?: string; 36 | }; -------------------------------------------------------------------------------- /web/types/table.d.ts: -------------------------------------------------------------------------------- 1 | declare type TableColumn = { 2 | field: string 3 | label?: string 4 | children?: TableColumn[] 5 | } & Recordable 6 | declare type PaginationProps = { 7 | pageNo: number 8 | pageSize: number 9 | total: number 10 | } 11 | declare type SortProps = { 12 | orderName: string 13 | orderDir: string 14 | } 15 | declare type TableSlotDefault = { 16 | row: Recordable 17 | column: TableColumn 18 | $index: number 19 | } & Recordable 20 | declare interface TableSetPropsType { 21 | field: string 22 | path: string 23 | value: any 24 | } 25 | -------------------------------------------------------------------------------- /web/types/vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const component: ReturnType 4 | export default component 5 | } -------------------------------------------------------------------------------- /备注.txt: -------------------------------------------------------------------------------- 1 | 未搞定mongo,目前启动容器后需要手动进到mongo容器还原数据 2 | mongorestore -d vitetest ./dump/vitetest --------------------------------------------------------------------------------