├── .conf.docker ├── app.nginx.conf └── init_data.sql ├── .dockerignore ├── .env.example ├── .gitattributes ├── .gitignore ├── Dockerfile.backend ├── Dockerfile.frontend ├── README.md ├── backend └── app │ ├── apps │ ├── __init__.py │ ├── base_template │ │ ├── __init__.py │ │ ├── curd │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── model_base.py │ │ ├── schemas │ │ │ └── __init__.py │ │ └── views.py │ ├── permission │ │ ├── __init__.py │ │ ├── curd │ │ │ ├── __init__.py │ │ │ ├── curd_menu.py │ │ │ ├── curd_perm_label.py │ │ │ ├── curd_role.py │ │ │ └── curd_user.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── menu.py │ │ │ ├── perm_label.py │ │ │ ├── role.py │ │ │ └── user.py │ │ ├── schemas.py │ │ └── views.py │ ├── system │ │ ├── __init__.py │ │ ├── curd │ │ │ ├── __init__.py │ │ │ ├── curd_config_setting.py │ │ │ ├── curd_dict_data.py │ │ │ └── curd_dict_detail.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── config_settings.py │ │ │ └── dictionaries.py │ │ ├── schemas.py │ │ └── views.py │ └── user │ │ ├── __init__.py │ │ ├── curd │ │ ├── __init__.py │ │ └── curd_user.py │ │ ├── schemas │ │ ├── __init__.py │ │ ├── token_schemas.py │ │ └── user_info_schemas.py │ │ └── views.py │ ├── common │ ├── __init__.py │ ├── curd_base.py │ ├── deps.py │ ├── error_code.py │ ├── exceptions.py │ ├── middleware.py │ ├── resp.py │ └── security.py │ ├── configs │ ├── .env.example │ ├── .gitignore │ ├── logging_config.conf │ └── supervisor.conf.example │ ├── core │ ├── __init__.py │ ├── config.py │ ├── constants.py │ └── logger.py │ ├── db │ ├── __init__.py │ ├── base_class.py │ ├── cache.py │ ├── mongo.py │ └── session.py │ ├── email-templates │ ├── forget-password.html │ └── register.html │ ├── log │ └── .gitignore │ ├── main.py │ ├── media │ ├── .gitignore │ └── images │ │ └── avatar │ │ └── default │ │ └── avatar.jpg │ ├── requirements.txt │ ├── utils │ ├── __init__.py │ ├── captcha_code.py │ ├── email.py │ ├── encrypt.py │ ├── loggers.py │ └── transform.py │ └── workers │ ├── __init__.py │ ├── celery_tasks.py │ └── celeryconfig.py ├── docker-compose.yaml ├── frontend └── dashboard │ ├── .editorconfig │ ├── .env.development │ ├── .env.production │ ├── .env.staging │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .travis.yml │ ├── LICENSE │ ├── babel.config.js │ ├── build │ └── index.js │ ├── jest.config.js │ ├── jsconfig.json │ ├── package.json │ ├── plop-templates │ ├── component │ │ ├── index.hbs │ │ └── prompt.js │ ├── store │ │ ├── index.hbs │ │ └── prompt.js │ ├── utils.js │ └── view │ │ ├── index.hbs │ │ └── prompt.js │ ├── plopfile.js │ ├── postcss.config.js │ ├── public │ ├── alipay.jpg │ ├── favicon.ico │ ├── index.html │ ├── qqqun.jpg │ └── wechatpay.jpg │ ├── src │ ├── App.vue │ ├── api │ │ ├── permission │ │ │ ├── label.js │ │ │ ├── menu.js │ │ │ ├── role.js │ │ │ └── user.js │ │ ├── system │ │ │ ├── dict │ │ │ │ ├── data.js │ │ │ │ └── detail.js │ │ │ └── parameter.js │ │ └── user.js │ ├── assets │ │ ├── 401_images │ │ │ └── 401.gif │ │ ├── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ │ ├── custom-theme │ │ │ ├── fonts │ │ │ │ ├── element-icons.ttf │ │ │ │ └── element-icons.woff │ │ │ └── index.css │ │ └── images │ │ │ ├── loading.gif │ │ │ └── login-background.jpg │ ├── components │ │ ├── BackToTop │ │ │ └── index.vue │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Charts │ │ │ ├── Keyboard.vue │ │ │ ├── LineMarker.vue │ │ │ ├── MixChart.vue │ │ │ └── mixins │ │ │ │ └── resize.js │ │ ├── DndList │ │ │ └── index.vue │ │ ├── DragSelect │ │ │ └── index.vue │ │ ├── Dropzone │ │ │ └── index.vue │ │ ├── ErrorLog │ │ │ └── index.vue │ │ ├── GithubCorner │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── HeaderSearch │ │ │ └── index.vue │ │ ├── IconSelect │ │ │ ├── index.vue │ │ │ └── requireIcons.js │ │ ├── ImageCropper │ │ │ ├── index.vue │ │ │ └── utils │ │ │ │ ├── data2blob.js │ │ │ │ ├── effectRipple.js │ │ │ │ ├── language.js │ │ │ │ └── mimes.js │ │ ├── JsonEditor │ │ │ └── index.vue │ │ ├── Kanban │ │ │ └── index.vue │ │ ├── MDinput │ │ │ └── index.vue │ │ ├── MarkdownEditor │ │ │ ├── default-options.js │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ ├── PanThumb │ │ │ └── index.vue │ │ ├── README.md │ │ ├── RightPanel │ │ │ └── index.vue │ │ ├── Screenfull │ │ │ └── index.vue │ │ ├── Share │ │ │ └── DropdownMenu.vue │ │ ├── SizeSelect │ │ │ └── index.vue │ │ ├── Sticky │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── TextHoverEffect │ │ │ └── Mallki.vue │ │ ├── ThemePicker │ │ │ └── index.vue │ │ ├── Tinymce │ │ │ ├── components │ │ │ │ └── EditorImage.vue │ │ │ ├── dynamicLoadScript.js │ │ │ ├── index.vue │ │ │ ├── plugins.js │ │ │ └── toolbar.js │ │ ├── Upload │ │ │ ├── SingleImage.vue │ │ │ ├── SingleImage2.vue │ │ │ └── SingleImage3.vue │ │ └── UploadExcel │ │ │ └── index.vue │ ├── directive │ │ ├── button_permission │ │ │ ├── hasPermi.js │ │ │ ├── hasRole.js │ │ │ └── index.js │ │ ├── clipboard │ │ │ ├── clipboard.js │ │ │ └── index.js │ │ ├── el-drag-dialog │ │ │ ├── drag.js │ │ │ └── index.js │ │ ├── el-table │ │ │ ├── adaptive.js │ │ │ └── index.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ ├── sticky.js │ │ └── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ ├── filters │ │ └── index.js │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── 404.svg │ │ │ ├── bug.svg │ │ │ ├── build.svg │ │ │ ├── cascader.svg │ │ │ ├── chart.svg │ │ │ ├── checkbox.svg │ │ │ ├── clipboard.svg │ │ │ ├── code.svg │ │ │ ├── color.svg │ │ │ ├── component.svg │ │ │ ├── dashboard.svg │ │ │ ├── date-range.svg │ │ │ ├── date.svg │ │ │ ├── dict.svg │ │ │ ├── documentation.svg │ │ │ ├── download.svg │ │ │ ├── drag.svg │ │ │ ├── druid.svg │ │ │ ├── edit.svg │ │ │ ├── education.svg │ │ │ ├── email.svg │ │ │ ├── example.svg │ │ │ ├── excel.svg │ │ │ ├── exit-fullscreen.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── form.svg │ │ │ ├── fullscreen.svg │ │ │ ├── github.svg │ │ │ ├── guide.svg │ │ │ ├── icon.svg │ │ │ ├── input.svg │ │ │ ├── international.svg │ │ │ ├── job.svg │ │ │ ├── language.svg │ │ │ ├── link.svg │ │ │ ├── list.svg │ │ │ ├── lock.svg │ │ │ ├── log.svg │ │ │ ├── logininfor.svg │ │ │ ├── message.svg │ │ │ ├── money.svg │ │ │ ├── monitor.svg │ │ │ ├── nested.svg │ │ │ ├── number.svg │ │ │ ├── online.svg │ │ │ ├── password.svg │ │ │ ├── pdf.svg │ │ │ ├── people.svg │ │ │ ├── peoples.svg │ │ │ ├── phone.svg │ │ │ ├── post.svg │ │ │ ├── qq.svg │ │ │ ├── question.svg │ │ │ ├── radio.svg │ │ │ ├── rate.svg │ │ │ ├── row.svg │ │ │ ├── search.svg │ │ │ ├── select.svg │ │ │ ├── server.svg │ │ │ ├── shopping.svg │ │ │ ├── size.svg │ │ │ ├── skill.svg │ │ │ ├── slider.svg │ │ │ ├── star.svg │ │ │ ├── swagger.svg │ │ │ ├── switch.svg │ │ │ ├── system.svg │ │ │ ├── tab.svg │ │ │ ├── table.svg │ │ │ ├── textarea.svg │ │ │ ├── theme.svg │ │ │ ├── time-range.svg │ │ │ ├── time.svg │ │ │ ├── tool.svg │ │ │ ├── tree-table.svg │ │ │ ├── tree.svg │ │ │ ├── upload.svg │ │ │ ├── user.svg │ │ │ ├── validCode.svg │ │ │ ├── wechat.svg │ │ │ └── zip.svg │ │ └── svgo.yml │ ├── layout │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Settings │ │ │ │ └── index.vue │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ ├── TagsView │ │ │ │ ├── ScrollPane.vue │ │ │ │ └── index.vue │ │ │ └── index.js │ │ ├── index.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── main.js │ ├── permission.js │ ├── router │ │ └── index.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ ├── errorLog.js │ │ │ ├── permission.js │ │ │ ├── settings.js │ │ │ ├── tagsView.js │ │ │ └── user.js │ ├── styles │ │ ├── btn.scss │ │ ├── element-ui.scss │ │ ├── element-variables.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── ruoyi.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── auth.js │ │ ├── clipboard.js │ │ ├── error-log.js │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── jsencrypt.js │ │ ├── open-window.js │ │ ├── permission.js │ │ ├── request.js │ │ ├── ruoyi.js │ │ ├── scroll-to.js │ │ └── validate.js │ ├── vendor │ │ ├── Export2Excel.js │ │ └── Export2Zip.js │ └── views │ │ ├── dashboard │ │ ├── admin │ │ │ ├── components │ │ │ │ ├── BarChart.vue │ │ │ │ ├── BoxCard.vue │ │ │ │ ├── LineChart.vue │ │ │ │ ├── PanelGroup.vue │ │ │ │ ├── PieChart.vue │ │ │ │ ├── RaddarChart.vue │ │ │ │ ├── TodoList │ │ │ │ │ ├── Todo.vue │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.vue │ │ │ │ └── mixins │ │ │ │ │ └── resize.js │ │ │ └── index.vue │ │ ├── editor │ │ │ └── index.vue │ │ └── index.vue │ │ ├── error-page │ │ ├── 401.vue │ │ └── 404.vue │ │ ├── permission │ │ ├── label │ │ │ └── index.vue │ │ ├── menu │ │ │ └── index.vue │ │ ├── role │ │ │ └── index.vue │ │ └── user │ │ │ ├── AvatarUpload.vue │ │ │ └── index.vue │ │ ├── profile │ │ ├── components │ │ │ ├── Account.vue │ │ │ ├── ResetPwd.vue │ │ │ ├── UserCard.vue │ │ │ └── userAvatar.vue │ │ └── index.vue │ │ ├── redirect │ │ └── index.vue │ │ ├── system │ │ ├── dict │ │ │ ├── detail │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ └── parameter │ │ │ └── index.vue │ │ └── user │ │ ├── forgetPassword │ │ ├── SetPassword.vue │ │ └── index.vue │ │ ├── layout.vue │ │ ├── login │ │ ├── auth-redirect.vue │ │ └── index.vue │ │ └── register │ │ ├── index.vue │ │ └── verify.vue │ ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── components │ │ ├── Hamburger.spec.js │ │ └── SvgIcon.spec.js │ │ └── utils │ │ ├── formatTime.spec.js │ │ ├── param2Obj.spec.js │ │ ├── parseTime.spec.js │ │ └── validate.spec.js │ └── vue.config.js ├── init_data.sql ├── makefile └── nginx.conf.example /.conf.docker/app.nginx.conf: -------------------------------------------------------------------------------- 1 | upstream fastapi-backend { 2 | server app1:8888 weight=20 max_fails=200 fail_timeout=20s; 3 | server app2:8888 weight=20 max_fails=200 fail_timeout=20s; 4 | } 5 | 6 | server { 7 | listen 80; 8 | server_name localhost; 9 | client_max_body_size 5m; 10 | error_log /var/log/nginx/fastapi_vue.error.log; 11 | access_log /var/log/nginx/fastapi_vue.access.log; 12 | 13 | location / { 14 | root /usr/share/nginx/dashboard; 15 | try_files $uri $uri/ /index.html; 16 | index index.html index.htm; 17 | } 18 | 19 | location ~* ^/(api|docs|media|openapi.json) { 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-User-Agent $http_user_agent; 24 | proxy_pass http://fastapi-backend; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/webpack-stats.jsonon 3 | **/pip-selfcheck.json 4 | **/.idea/ 5 | **/.vscode/ 6 | **/package-lock.json 7 | **/.DS_Store 8 | 9 | **/__pycache__/ 10 | **/.env 11 | 12 | **/.git 13 | **/.github 14 | **/.git* 15 | 16 | **/venv 17 | 18 | **/log/* 19 | **/logs/* 20 | **/*.log 21 | **/*.log.* 22 | 23 | **/data/ 24 | **/media/* 25 | 26 | **/*.tar 27 | **/*.zip 28 | **/volumes/ 29 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DB_DATA=./volumes/DB 2 | DB_NAME=fastapi_vue 3 | DB_PWD=123456 4 | DB_PORT=4316 5 | INIT_SQL=./init_data.sql 6 | 7 | REDIS_PORT=6389 8 | 9 | SERBVER_PORT=800 10 | DASHBPARD_PORT=8001 11 | CONF_DIR=./.conf.docker/ 12 | DASHBPARD_DIST=./frontend/dashboard/dist 13 | 14 | APP1_PORT=8898 15 | APP2_PORT=8899 16 | APP_MEDIA_DIR=./backend/app/media/ 17 | APP_LOG_DIR=./backend/app/log/ 18 | APP_DIR=./backend/app/ 19 | DB_DATA=/Users/beginner/Projects/temp/fastAPI-vue-2/data/DB 20 | DB_NAME=fastapi_vue 21 | DB_PWD=123456 22 | DB_PORT=4316 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py linguist-language=python 2 | *.vue linguist-language=python 3 | *.js linguist-language=vue 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/.fleet 3 | **/venv 4 | **/__pycache__ 5 | *.pyc 6 | *.pyo 7 | *.log 8 | *.log.* 9 | 10 | **.yaml 11 | !docker-compose.yaml 12 | **/.DS_Store 13 | 14 | 15 | **/node_modules 16 | **/dist 17 | 18 | .env 19 | **/.env 20 | **/.env.local 21 | **/.env.*.local 22 | 23 | 24 | **/volumes/ 25 | -------------------------------------------------------------------------------- /Dockerfile.backend: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | 6 | # # 使用阿里云国内apt镜像 7 | # RUN cp /etc/apt/sources.list /etc/apt/sources.list.bak \ 8 | # && touch /etc/apt/sources.list \ 9 | # && sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ 10 | # && sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ 11 | # && sed -i 's/ports.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list 12 | 13 | # 安装必要的软件 14 | RUN apt-get update && apt-get install python3 python3-pip -y 15 | 16 | 17 | # 创建代码目录和复制代码到容器内 18 | WORKDIR /projects 19 | 20 | COPY ./backend/ ./ 21 | 22 | # # 直接使用系统中的python Debian 10+ 系统包含了Python,系统默认禁止直接pip防止导致与系统使用的库冲突, 下面命令解除此限制或使用虚拟环境 23 | # RUN mv /usr/lib/python3.11/EXTERNALLY-MANAGED /usr/lib/python3.11/EXTERNALLY-MANAGED.bak 24 | 25 | # 安装程序用到的python库 26 | RUN pip3 install -r /projects/app/requirements.txt # -i https://pypi.tuna.tsinghua.edu.cn/simple 27 | 28 | 29 | # 启动命令 gunicorn main:app --chdir /projects/app -w (程序异步数量) -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8888 --log-config ./configs/logging_config.conf " 30 | ENTRYPOINT ["python3", "-m", "gunicorn", "main:app", "--chdir", "/projects/app", "-w", "2", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8888", "--log-config", "/projects/app/configs/logging_config.conf"] 31 | -------------------------------------------------------------------------------- /Dockerfile.frontend: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # node版本高于16 需要添加此环境变量 7 | ENV NODE_OPTIONS=--openssl-legacy-provider 8 | 9 | # # 使用阿里云国内apt镜像 10 | # RUN cp /etc/apt/sources.list /etc/apt/sources.list.bak \ 11 | # && touch /etc/apt/sources.list \ 12 | # && sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ 13 | # && sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ 14 | # && sed -i 's/ports.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list 15 | 16 | # 安装必要的软件 17 | RUN apt-get update && apt-get install build-essential python3 python3-pip curl git make -y 18 | 19 | # 安装nodejs npm 20 | RUN curl --silent --location https://deb.nodesource.com/setup_22.x | bash - \ 21 | && apt-get install nodejs -y 22 | 23 | # 创建代码目录和复制代码到容器内 24 | WORKDIR /projects 25 | 26 | COPY ./frontend/ ./ 27 | 28 | # 安装程序依赖 29 | RUN cd /projects/dashboard/ && npm i # --registry https://registry.npm.taobao.org 30 | 31 | 32 | 33 | CMD ["/bin/bash", "-c", "cd /projects/dashboard/ && npm run dev --port=80"] 34 | -------------------------------------------------------------------------------- /backend/app/apps/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi.routing import APIRouter 2 | 3 | from .user import user_api 4 | from .permission import permission_api 5 | from .system import system_api 6 | 7 | 8 | api_router = APIRouter() 9 | 10 | api_router.include_router(user_api, prefix="/user") 11 | api_router.include_router(system_api, prefix="/system") 12 | api_router.include_router(permission_api, prefix="/permission") 13 | 14 | 15 | __all__ = ['api_router'] -------------------------------------------------------------------------------- /backend/app/apps/base_template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/base_template/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/base_template/curd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/base_template/curd/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/base_template/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/base_template/models/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/base_template/models/model_base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 2 | from sqlalchemy.orm import relationship 3 | 4 | from db.base_class import Base 5 | 6 | 7 | class BaseTemplate(Base): 8 | """ 模型模板 """ 9 | __tablename__ = "base_tb" 10 | -------------------------------------------------------------------------------- /backend/app/apps/base_template/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/base_template/schemas/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/base_template/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/base_template/views.py -------------------------------------------------------------------------------- /backend/app/apps/permission/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as permission_api -------------------------------------------------------------------------------- /backend/app/apps/permission/curd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/permission/curd/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/permission/models/__init__.py: -------------------------------------------------------------------------------- 1 | from db.base_class import Base 2 | from db.session import engine 3 | from .menu import Menus 4 | from .role import Roles, RoleMenu 5 | from .user import Users, UserRole 6 | from .perm_label import PermLabel, PermLabelRole 7 | 8 | 9 | __all__ = ['Menus', 'Roles', 'RoleMenu', 'Users', 'UserRole', 'PermLabel', 'PermLabelRole'] 10 | 11 | 12 | Base.metadata.create_all(engine) -------------------------------------------------------------------------------- /backend/app/apps/permission/models/menu.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from db.base_class import Base 5 | 6 | 7 | class Menus(Base): 8 | """ 菜单表 """ 9 | path = Column(String(128), default='', server_default="", comment="路由") 10 | component = Column(String(128), default="", server_default="", comment="组件") 11 | is_frame = Column(Boolean, default=False, server_default='0', comment="是否外链") 12 | hidden = Column(Boolean(), default=False, server_default='0', comment="是否隐藏") 13 | status = Column(Integer, default=0, server_default='0', comment="菜单状态") # 0: 正常 1 停用 14 | order_num = Column(Integer, default=0, server_default='0', comment="显示排序") 15 | # meta 16 | name = Column(String(32), default="", server_default="", comment="唯一标识用于页面缓存,否则keep-alive会出问题") # index组件的name 17 | title = Column(String(32), default="", server_default="", comment="标题") 18 | icon = Column(String(32), default="", server_default="", comment="图标") 19 | no_cache = Column(Boolean, default=False, server_default="0", comment="是否缓存") 20 | parent_id = Column(Integer, default=0, server_default="0", comment="上级菜单") # 0代表上级菜单就是根目录 21 | -------------------------------------------------------------------------------- /backend/app/apps/permission/models/perm_label.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from core.config import settings 5 | from db.base_class import Base 6 | 7 | 8 | class PermLabel(Base): 9 | """ 权限标签 """ 10 | label = Column(String(128), server_default='', comment='标签') 11 | remark = Column(String(256), server_default='', default='', comment="备注") 12 | status = Column(Integer, server_default='0', default=0, comment='状态') 13 | 14 | label_role = relationship("Roles", secondary=f"{settings.SQL_TABLE_PREFIX}perm_label_role", backref="perm_label") 15 | 16 | 17 | class PermLabelRole(Base): 18 | """用户-权限组-中间表""" 19 | label_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}perm_label.id", ondelete='CASCADE')) 20 | role_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}roles.id")) -------------------------------------------------------------------------------- /backend/app/apps/permission/models/role.py: -------------------------------------------------------------------------------- 1 | from email.policy import default 2 | from xml.etree.ElementTree import Comment 3 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 4 | from sqlalchemy.orm import relationship 5 | from core.config import settings 6 | 7 | from db.base_class import Base 8 | 9 | 10 | class Roles(Base): 11 | """角色""" 12 | key = Column(String(64), unique=True, index=True, nullable=False, comment="权限标识") 13 | name = Column(String(256), default="", server_default="", comment="权限名称") 14 | order_num = Column(Integer, default=0, server_default="0", comment="顺序") 15 | status = Column(Integer, default=0, server_default="0", comment="状态(0: 正常, 1: 停用)") 16 | 17 | role_menu = relationship("Menus", backref="role", secondary=f"{settings.SQL_TABLE_PREFIX}role_menu") 18 | 19 | 20 | class RoleMenu(Base): 21 | """角色-菜单-中间表""" 22 | role_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}roles.id", ondelete='CASCADE')) 23 | menu_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}menus.id")) 24 | -------------------------------------------------------------------------------- /backend/app/apps/permission/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 2 | from sqlalchemy.orm import relationship 3 | from core.config import settings 4 | 5 | from db.base_class import Base 6 | 7 | 8 | class Users(Base): 9 | """用户表""" 10 | username = Column(String(32), unique=True, index=True, nullable=False, comment="用户名") 11 | nickname = Column(String(32), default='', server_default="", nullable=False, comment="姓名") 12 | sex = Column(Integer, default=0, server_default='0', comment="性别") # 0: 未知, 1: 男, 2: 女 13 | phone = Column(String(32), nullable=False, comment="手机号") 14 | email = Column(String(256), nullable=False, comment="邮箱") 15 | hashed_password = Column(String(128), nullable=False, comment="密码") 16 | avatar = Column(String(128), default="", server_default="", comment="头像") 17 | status = Column(Integer, default=0, server_default='0', nullable=False, comment="状态") # 0: 正常 1: 停用 18 | is_active = Column(Boolean(), default=False, server_default='0', comment="是否已经验证用户") 19 | is_superuser = Column(Boolean(), default=False, server_default='0', comment="是否超级管理员") 20 | 21 | user_role = relationship("Roles", secondary=f"{settings.SQL_TABLE_PREFIX}user_role", backref="user") 22 | 23 | 24 | class UserRole(Base): 25 | """用户-权限组-中间表""" 26 | user_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}users.id", ondelete='CASCADE')) 27 | role_id = Column(Integer, ForeignKey(f"{settings.SQL_TABLE_PREFIX}roles.id")) 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /backend/app/apps/permission/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | from pydantic import BaseModel, AnyHttpUrl, conint 3 | 4 | 5 | class UserSchema(BaseModel): 6 | username: str 7 | nickname: str = "" 8 | sex: int = 0 9 | phone: str 10 | email: str 11 | avatar: str = "" 12 | is_active: bool = True 13 | status: int = 0 14 | roles: List[int] = [] 15 | 16 | 17 | class UserIsActiveSchema(BaseModel): 18 | is_active: bool 19 | 20 | 21 | class UserSetPasswordSchema(BaseModel): 22 | password: str 23 | 24 | 25 | class RoleSchema(BaseModel): 26 | name: str 27 | key: str 28 | order_num: int = 0 29 | status: int = 0 30 | menus: List[int] = [] 31 | 32 | 33 | class MenuSchema(BaseModel): 34 | path: str = "" 35 | component: str = "" 36 | is_frame: bool = False 37 | hidden: bool = False 38 | status: int = 0 39 | name: str = "" 40 | title: str = "" 41 | icon: str = "" 42 | order_num: int = 0 43 | no_cache: bool = True 44 | parent_id: int = 0 45 | 46 | 47 | class RoleMenuSchema(BaseModel): 48 | menu_ids: List[int] 49 | 50 | 51 | class PremLabelSchema(BaseModel): 52 | label: str 53 | remark: str = "" 54 | status: int = 0 55 | roles: List[int] = [] 56 | -------------------------------------------------------------------------------- /backend/app/apps/system/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as system_api -------------------------------------------------------------------------------- /backend/app/apps/system/curd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/system/curd/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/system/curd/curd_dict_detail.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | from sqlalchemy.orm import Session 3 | from sqlalchemy.sql import func 4 | from common.curd_base import CRUDBase 5 | from ..models.dictionaries import DictDetails, DictData 6 | 7 | 8 | class CURDDictDetail(CRUDBase): 9 | 10 | def get(self, db: Session, _id: int, to_dict: bool = True): 11 | """ 通过id获取 """ 12 | row = db.query(*self.query_columns, DictData.dict_name, DictData.dict_type).outerjoin( # outerjoin() == LEFT JOIN, join() == INNER JOIN, 不支持 RIGHT JOIN (可以考虑表顺序实现) 13 | DictData).filter(self.model.id ==_id, self.model.is_deleted == 0).first() 14 | return dict(row or {}) if to_dict else row 15 | 16 | def get_max_order_num(self, db: Session, *, dict_data_id: int ) -> int: 17 | res = db.query(func.max(DictDetails.order_num).label('max_order_num')).filter( 18 | DictDetails.dict_data_id == dict_data_id, DictDetails.is_deleted == 0 19 | ).first() 20 | return res['max_order_num'] or 0 21 | 22 | 23 | curd_dict_detail = CURDDictDetail(DictDetails) -------------------------------------------------------------------------------- /backend/app/apps/system/models/__init__.py: -------------------------------------------------------------------------------- 1 | from db.base_class import Base 2 | from db.session import engine 3 | from .config_settings import ConfigSettings 4 | from .dictionaries import DictData, DictDetails 5 | 6 | 7 | __all__ = ['DictData', 'DictDetails', 'ConfigSettings'] 8 | 9 | 10 | Base.metadata.create_all(engine) -------------------------------------------------------------------------------- /backend/app/apps/system/models/config_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date 2 | from sqlalchemy.orm import relationship 3 | 4 | from db.base_class import Base 5 | 6 | 7 | class ConfigSettings(Base): 8 | """ 配置参数 """ 9 | name = Column(String(64), unique=True, index=True, nullable=False, default="", server_default="", comment="参数名称") 10 | key = Column(String(128), nullable=False, comment="参数键名") 11 | value = Column(String(128), nullable=False, comment="参数键值") 12 | remark = Column(String(256), default="", server_default="", comment="备注") 13 | status = Column(Integer, default=0, server_default='0', comment="状态 0: 正常 1:停用") 14 | order_num = Column(Integer, default=0, server_default='0', comment="排序") 15 | -------------------------------------------------------------------------------- /backend/app/apps/system/models/dictionaries.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Date, asc, desc 2 | from sqlalchemy.orm import relationship 3 | from core.config import settings 4 | from db.base_class import Base 5 | 6 | 7 | class DictDetails(Base): 8 | """ 字典值表 """ 9 | dict_label = Column(String(128), nullable=False, comment="字典标签") 10 | dict_value = Column(String(128), nullable=False, comment="字典键值") 11 | remark = Column(String(256), default="", server_default="", comment="备注") 12 | is_default = Column(Boolean, nullable=False, default=False, server_default="0", comment="是否默认值") 13 | status = Column(Integer, default=0, server_default='0', comment="状态 0: 正常 1:停用") 14 | order_num = Column(Integer, default=0, server_default='0', comment="排序") 15 | # FK dict_data 16 | dict_data_id = Column(Integer, ForeignKey(f'{settings.SQL_TABLE_PREFIX}dict_data.id', ondelete="CASCADE")) 17 | dict_data = relationship("DictData", back_populates='dict_detail') 18 | 19 | 20 | class DictData(Base): 21 | """ 字典表 """ 22 | dict_type = Column(String(64), unique=True, index=True, nullable=False, comment="字典类型") 23 | dict_name = Column(String(64), default="", server_default="", comment="字典名称") 24 | remark = Column(String(256), default="", server_default="", comment="备注") 25 | status = Column(Integer, default=0, server_default='0', comment="状态 0: 正常 1:停用") 26 | order_num = Column(Integer, default=0, server_default='0', comment="排序") 27 | # one to many 28 | dict_detail = relationship("DictDetails", back_populates="dict_data", lazy='dynamic', order_by=asc(DictDetails.order_num)) 29 | -------------------------------------------------------------------------------- /backend/app/apps/system/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | from pydantic import BaseModel, AnyHttpUrl, conint 3 | 4 | 5 | class ConfigSettingSchema(BaseModel): 6 | name: str 7 | key: str 8 | value: str 9 | remark: str = "" 10 | status: int = 0 11 | order_num: int = 0 12 | 13 | 14 | class DictDataSchema(BaseModel): 15 | dict_type: str 16 | dict_name: str = "" 17 | remark: str = "" 18 | status: int = 0 19 | order_num: int = 0 20 | 21 | 22 | class DictDetailSchema(BaseModel): 23 | dict_label: str 24 | dict_value: str 25 | dict_data_id: int 26 | remark: str = "" 27 | is_default: bool = False 28 | status: int = 0 29 | order_num: int = 0 -------------------------------------------------------------------------------- /backend/app/apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as user_api -------------------------------------------------------------------------------- /backend/app/apps/user/curd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/user/curd/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/user/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/apps/user/schemas/__init__.py -------------------------------------------------------------------------------- /backend/app/apps/user/schemas/token_schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel 3 | 4 | 5 | class Token(BaseModel): 6 | token: str 7 | 8 | 9 | class TokenPayload(BaseModel): 10 | sub: Optional[int] = None 11 | -------------------------------------------------------------------------------- /backend/app/apps/user/schemas/user_info_schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | from pydantic import BaseModel, AnyHttpUrl, conint 3 | 4 | 5 | class LoginUserInfoSchema(BaseModel): 6 | user: str 7 | password: str 8 | code: str = "" 9 | key: str = "" 10 | 11 | 12 | class RegisterUserInfoSchema(BaseModel): 13 | username: str 14 | email: str 15 | phone: str 16 | password: str 17 | sex: int = 0 18 | nickname: str = "" 19 | avatar: str = "" 20 | code: str = "" 21 | key: str = "" 22 | 23 | 24 | class ForgetPasswordSubmitSchema(BaseModel): 25 | email: str 26 | code: str = "" 27 | key: str = "" 28 | 29 | 30 | class ForgetPasswordSetPasswordSchema(BaseModel): 31 | password: str 32 | code: str = "" 33 | key: str = "" 34 | 35 | 36 | class ChangeUserInfoSchema(BaseModel): 37 | nickname: str 38 | email: str 39 | phone: str 40 | sex: str 41 | 42 | 43 | class ChangePasswordSchema(BaseModel): 44 | old_password: str 45 | new_password: str 46 | 47 | 48 | class UserAvailabilitySchema(BaseModel): 49 | data: str 50 | exclude_user_id: int = None 51 | -------------------------------------------------------------------------------- /backend/app/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/common/__init__.py -------------------------------------------------------------------------------- /backend/app/common/error_code.py: -------------------------------------------------------------------------------- 1 | from socket import MsgFlag 2 | from pydantic import BaseModel 3 | 4 | 5 | class ErrorBase(BaseModel): 6 | code: int 7 | msg: str = "" 8 | 9 | 10 | # 报错 11 | ERROR_INTERNAL = ErrorBase(code=500, msg="内部错误") 12 | # 找不到路径 13 | ERROR_NOT_FOUND = ErrorBase(code=404, msg="api 路径错误") 14 | # 参数错误 15 | ERROR_PARAMETER_ERROR = ErrorBase(code=400, msg="参数错误") 16 | 17 | # 用户相关 18 | ERROR_USER_TOKEN_FAILURE = ErrorBase(code=5004, msg="未登录或登录过期") 19 | ERROR_USER_NOT_FOUND = ErrorBase(code=5004, msg="用户不存在") 20 | ERROR_USER_PASSWORD_ERROR = ErrorBase(code=5005, msg="密码错误") 21 | ERROR_USER_NOT_ACTIVATE = ErrorBase(code=5006, msg="用户账号尚未") 22 | ERROR_USER_ACCOUNT_EXISTS = ErrorBase(code=5007, msg="账号已存在") 23 | ERROR_USER_EMAIL_NOT_EXISTS = ErrorBase(code=5008, msg="邮箱不存在") 24 | ERROR_FORGET_PWD_TOKEN_ERROR = ErrorBase(code=5009, msg="重置密码链接错误或已过期") 25 | ERROR_USER_REGISTER_TOKEN_ERROR = ErrorBase(code=5031, msg="注册验证链接已过期或不存在") 26 | ERROR_USER_REGISTER_EXISTS = ErrorBase(code=5032, msg="注册失败,可能账号已存在。") 27 | ERROR_USER_REGISTER_ERROR = ErrorBase(code=5033, msg="注册失败,请重试。") 28 | ERROR_USER_REGISTER_TO_OFTEN = ErrorBase(code=5034, msg="提交注册太频繁,请稍后重试") 29 | ERROR_USER_EMAIL_EXISTS = ErrorBase(code=5011, msg="邮箱不可用") 30 | ERROR_USER_PHONE_EXISTS = ErrorBase(code=5012, msg="手机号码不可用") 31 | ERROR_USER_USERNAME_EXISTS = ErrorBase(code=5013, msg="用户名不可用") 32 | ERROR_USER_CAPTCHA_CODE_ERROR = ErrorBase(code=5021, msg="验证码错误") 33 | ERROR_USER_CAPTCHA_CODE_INVALID = ErrorBase(code=5022, msg="验证码已失效,请重试。") 34 | ERROR_USER_PREM_ADD_ERROR = ErrorBase(code=5031, msg="权限标识添加失败") 35 | ERROR_USER_PREM_ERROR = ErrorBase(code=5403, msg="权限不足") 36 | -------------------------------------------------------------------------------- /backend/app/common/resp.py: -------------------------------------------------------------------------------- 1 | from fastapi import status 2 | from fastapi.responses import JSONResponse # , ORJSONResponse 3 | from pydantic import BaseModel 4 | from typing import Union, Optional 5 | 6 | from common.error_code import ErrorBase 7 | 8 | 9 | class respJsonBase(BaseModel): 10 | code: int 11 | msg: str 12 | data: Union[dict, list] 13 | 14 | 15 | def respSuccessJson(data: Union[list, dict, str] = None, msg: str = "Success"): 16 | """ 接口成功返回 """ 17 | return JSONResponse( 18 | status_code=status.HTTP_200_OK, 19 | content={ 20 | 'code': 0, 21 | 'msg': msg, 22 | 'data': data or {} 23 | } 24 | ) 25 | 26 | 27 | def respErrorJson(error: ErrorBase, *, msg: Optional[str] = None, msg_append: str = "", 28 | data: Union[list, dict, str] = None, status_code: int = status.HTTP_200_OK): 29 | """ 错误接口返回 """ 30 | return JSONResponse( 31 | status_code=status_code, 32 | content={ 33 | 'code': error.code, 34 | 'msg': (msg or error.msg) + msg_append, 35 | 'data': data or {} 36 | } 37 | ) -------------------------------------------------------------------------------- /backend/app/common/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from typing import Any, Union 3 | 4 | from jose import jwt 5 | from passlib.context import CryptContext 6 | import redis 7 | 8 | from core.config import settings 9 | 10 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 11 | 12 | 13 | 14 | def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str: 15 | expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) 16 | to_encode = {"exp": expire, "sub": str(subject)} 17 | return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) 18 | 19 | 20 | def verify_password(plain_password: str, hashed_password: str) -> bool: 21 | return pwd_context.verify(plain_password, hashed_password) 22 | 23 | 24 | def get_password_hash(password: str) -> str: 25 | return pwd_context.hash(password) 26 | -------------------------------------------------------------------------------- /backend/app/configs/.env.example: -------------------------------------------------------------------------------- 1 | ECHO_SQL=true 2 | AUTO_ADD_PERM_LABEL=true 3 | PORT=9898 4 | 5 | PROJECT_NAME=fastAPI-vue 6 | USE_CAPTCHA=true 7 | SECRET_KEY=DFGG45645674GHFGHFH 8 | 9 | SQL_HOST=127.0.0.1 10 | SQL_PORT=3306 11 | SQL_USERNAME=root 12 | SQL_PASSWORD=123456 13 | SQL_DATABASE=fastapi_vue 14 | 15 | 16 | REDIS_HOST=127.0.0.1 17 | REDIS_PORT=6379 18 | REDIS_PASSWORD= 19 | REDIS_DB=1 20 | 21 | # CELERY_BROKER=redis://127.0.0.1:7379/2 22 | # CELERY_BACKEND=redis://127.0.0.1:7379/3 23 | 24 | 25 | SMTP_HOST=smtp.qq.com 26 | SMTP_USER=john_doe_1996@foxmail.com 27 | SMTP_PASSWORD=ndkhgrgoimyvbhei 28 | EMAIL_FROM_EMAIL=john_doe_1996@foxmail.com 29 | -------------------------------------------------------------------------------- /backend/app/configs/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /backend/app/configs/logging_config.conf: -------------------------------------------------------------------------------- 1 | 2 | [loggers] 3 | keys=root,api,gunicorn.error,gunicorn.access,uvicorn.error,uvicorn.access 4 | 5 | [handlers] 6 | keys=console,error,info,access 7 | 8 | [formatters] 9 | keys=default,access 10 | 11 | [logger_root] 12 | level=INFO 13 | handlers=console 14 | 15 | [logger_api] 16 | level=INFO 17 | handlers=info,error 18 | propagate=1 19 | qualname=api 20 | 21 | [logger_gunicorn.error] 22 | level=INFO 23 | handlers=info,error 24 | propagate=1 25 | qualname=gunicorn.error 26 | 27 | [logger_gunicorn.access] 28 | level=INFO 29 | handlers=info,access 30 | propagate=0 31 | qualname=gunicorn.access 32 | 33 | [logger_uvicorn.error] 34 | level=INFO 35 | handlers=info,error 36 | propagate=1 37 | qualname=uvicorn.error 38 | 39 | [logger_uvicorn.access] 40 | level=INFO 41 | handlers=info,access 42 | propagate=0 43 | qualname=uvicorn.access 44 | 45 | [handler_console] 46 | class=StreamHandler 47 | level=DEBUG 48 | formatter=default 49 | args=(sys.stderr,) 50 | 51 | [handler_access] 52 | class=StreamHandler 53 | level=INFO 54 | formatter=access 55 | args=(sys.stdout,) 56 | 57 | [handler_error] 58 | class=logging.handlers.TimedRotatingFileHandler 59 | level=ERROR 60 | formatter=default 61 | kwargs={'filename':'./log/error.log', 'when': 'D', "backupCount": 5} 62 | 63 | [handler_info] 64 | class=logging.handlers.TimedRotatingFileHandler 65 | level=INFO 66 | formatter=default 67 | kwargs={'filename':'./log/info.log', 'when': 'D', "backupCount": 10} 68 | 69 | [formatter_default] 70 | class=uvicorn.logging.DefaultFormatter 71 | format=%(asctime)s [%(levelname)s] %(message)s 72 | datefmt=%Y-%m-%d %H:%M:%S 73 | 74 | [formatter_access] 75 | class=uvicorn.logging.AccessFormatter 76 | format=%(asctime)s [%(levelname)s] %(client_addr)s - "%(request_line)s" %(status_code)s 77 | datefmt=%Y-%m-%d %H:%M:%S 78 | -------------------------------------------------------------------------------- /backend/app/configs/supervisor.conf.example: -------------------------------------------------------------------------------- 1 | [program: fastapi-backend] 2 | command=/home/ubuntu/opt/fastAPI-vue/backend/app/venv/bin/python -m gunicorn main:app -w 2 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:81%(process_num)02d --log-config ./configs/logging_config.conf 3 | directory=/home/ubuntu/opt/fastAPI-vue/backend/app 4 | numprocs=2 5 | process_name=81%(process_num)02d 6 | autostart=false 7 | autorestart=true 8 | startretries = 5 9 | user=ubuntu 10 | redirect_stderr=true 11 | stdout_logfile=/home/ubuntu/log/supervisor/fastapi-backend.log 12 | stdout_logfile_maxbytes=20MB 13 | stdout_logfile_backups=10 -------------------------------------------------------------------------------- /backend/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/core/__init__.py -------------------------------------------------------------------------------- /backend/app/core/constants.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import os 3 | 4 | BASE_DIR = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')) 5 | DEFAULT_ENV_FILE = os.path.abspath(os.path.join(BASE_DIR, "./configs/.env")) # default env file path: './configs/.env' when run "python main.py", you can change in this if you want 6 | 7 | 8 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 9 | REGISTER_TOKEN_EXPIRE_HOURS = 24 10 | USER_CAPTCHA_CODE_EXPIRE_MINUTES = 15 11 | USER_REGISTER_SUBMIT_NUM_LIMIT = 5 12 | USER_REGISTER_SUBMIT_EXPIRE_MINUTES = 5 13 | FORGET_PWD_TOKEN_EXPIRE_HOURS = 24 14 | USER_FORGET_PWD_SUBMIT_NUM_LIMIT = 2 15 | USER_FORGET_PWD_SUBMIT_EXPIRE_MINUTES = 5 16 | USER_PERM_LABEL_CACHE_EXPIRE_MINUTES = 3 17 | 18 | CELERY_PRINT_DATETIME = timedelta(seconds=10) 19 | 20 | 21 | REDIS_KEY_LOGIN_TOKEN_KEY_PREFIX = "user_login_token_" 22 | REDIS_KEY_REGISTER_TOKEN_KEY_PREFIX = "user_register_token_" 23 | REDIS_KEY_FORGET_PWD_TOKEN_KEY_PREFIX = "user_forget_pwd_token_" 24 | REDIS_KEY_USER_CAPTCHA_CODE_KEY_PREFIX = "user_captcha_code_" 25 | REDIS_KEY_USER_REGISTER_NUM_OF_TIME = "user_register_time_num_IP_" 26 | REDIS_KEY_USER_FORGET_PWD_NUM_OF_TIME = "user_forget_pwd_time_num_EMAIL_" 27 | REDIS_KEY_USER_PERM_LABEL_CACHE = "user_perm_label_cache_" 28 | 29 | 30 | MEDIA_BASE_PATH = os.path.join(BASE_DIR, 'media/') 31 | MEDIA_AVATAR_BASE_DIR = "images/avatar/" -------------------------------------------------------------------------------- /backend/app/core/logger.py: -------------------------------------------------------------------------------- 1 | from core.config import settings 2 | from utils.loggers import Logging 3 | 4 | 5 | _path = str(settings.LOGGING_CONFIG_FILE) 6 | Logging(_path) 7 | 8 | 9 | logging = Logging 10 | logger = logging.use("api") 11 | 12 | -------------------------------------------------------------------------------- /backend/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/db/__init__.py -------------------------------------------------------------------------------- /backend/app/db/cache.py: -------------------------------------------------------------------------------- 1 | try: 2 | from redis import asyncio as aioredis 3 | except ImportError: 4 | import aioredis 5 | from fastapi import FastAPI 6 | import redis 7 | 8 | from core.config import settings 9 | 10 | 11 | def registerRedis(app: FastAPI) -> None: 12 | """ 13 | 把redis挂载到app对象上面, 异步redis 14 | :param app: 15 | :return: 16 | """ 17 | 18 | @app.on_event('startup') 19 | async def startup_event(): 20 | """ 21 | 获取链接 22 | :return: 23 | """ 24 | app.state.redis = await aioredis.from_url(settings.getRedisURL()) 25 | 26 | @app.on_event('shutdown') 27 | async def shutdown_event(): 28 | """ 29 | 关闭 30 | :return: 31 | """ 32 | await app.state.redis.close() 33 | 34 | 35 | def get_redis() -> redis.Redis: 36 | """ 37 | get_redis 同步的redis 38 | 39 | :return redis.Redis 40 | """ 41 | pool = redis.ConnectionPool.from_url(settings.getRedisURL()) 42 | return redis.Redis(connection_pool=pool) -------------------------------------------------------------------------------- /backend/app/db/mongo.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pymongo import MongoClient, database 3 | 4 | from core.config import settings 5 | 6 | 7 | def registerMongo(app: FastAPI) -> None: 8 | """ 9 | 把MongoDB挂载到app对象上面 10 | :param app: 11 | :return: 12 | """ 13 | 14 | @app.on_event('startup') 15 | async def startup_event(): 16 | """ 17 | 获取链接 18 | :return: 19 | """ 20 | 21 | app.mongodb_client = None if not settings.MONGODB_HOST else MongoClient( 22 | settings.getMongoURL(), serverSelectionTimeoutMS=10000, connectTimeoutMS=10000) 23 | app.mongo = app.mongodb_client and app.mongodb_client.get_database(settings.MONGODB_DB_NAME or "db") 24 | 25 | @app.on_event('shutdown') 26 | async def shutdown_event(): 27 | """ 28 | 关闭 29 | :return: 30 | """ 31 | if app.mongodb_client: 32 | app.mongodb_client.close() 33 | 34 | 35 | def get_mongo(db_name: str = settings.MONGODB_DB_NAME) -> database.Database: 36 | """ 37 | get_mongo 获取MongoDB数据库连接 38 | 39 | :param str db_name: 选择的数据库名称 40 | :return database.Database: 41 | """ 42 | if not settings.MONGODB_HOST: 43 | return None 44 | return MongoClient(settings.getMongoURL())[db_name or "db"] 45 | -------------------------------------------------------------------------------- /backend/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from core.config import settings 5 | # from db.base_class import Base 6 | 7 | 8 | engine = create_engine(settings.getSqlalchemyURL(), pool_pre_ping=True, echo=settings.ECHO_SQL) 9 | print(engine) 10 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 11 | 12 | 13 | # Base.metadata.create_all(engine) 14 | -------------------------------------------------------------------------------- /backend/app/email-templates/forget-password.html: -------------------------------------------------------------------------------- 1 |
2 |
请点击以下链接设置新密码
3 | {{ url }} 4 |
-------------------------------------------------------------------------------- /backend/app/email-templates/register.html: -------------------------------------------------------------------------------- 1 |
2 |
请点击以下链接完成账号验证
3 | {{ url }} 4 |
-------------------------------------------------------------------------------- /backend/app/log/.gitignore: -------------------------------------------------------------------------------- 1 | ./* -------------------------------------------------------------------------------- /backend/app/media/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | !./imagess 3 | !/images/avatar/default 4 | /* 5 | !.gitignore -------------------------------------------------------------------------------- /backend/app/media/images/avatar/default/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/media/images/avatar/default/avatar.jpg -------------------------------------------------------------------------------- /backend/app/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==2.0.1 2 | redis==4.3.4 3 | captcha==0.5 4 | click==8.1.3 5 | fastapi==0.86.0 6 | Jinja2==3.1.2 7 | msgpack==0.6.2 8 | openpyxl==3.0.10 9 | prometheus-client==0.15.0 10 | pydantic==1.10.2 11 | PyMySQL==1.0.2 12 | python-dotenv==0.21.0 13 | requests==2.22.0 14 | SQLAlchemy==1.4.43 15 | starlette==0.20.4 16 | python-jose==3.3.0 17 | passlib==1.7.4 18 | redis==4.3.4 19 | uvicorn==0.19.0 20 | python-multipart==0.0.5 21 | gunicorn==20.1.0 22 | celery==5.2.1 23 | pymongo==3.12.3 24 | cryptography -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/backend/app/utils/__init__.py -------------------------------------------------------------------------------- /backend/app/utils/captcha_code.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from captcha.image import ImageCaptcha 3 | import random 4 | import base64 5 | 6 | 7 | _NUM_WORDS = [ 8 | #'0', '1', # 容易混淆 O 和 l 9 | '2', '3', '4', '5', '6', '7', '8', # '9', # 容易混淆q 10 | ] * 3 # 数字*3 增加出现数字的概率 11 | 12 | _UPPER_WORDS = [ 13 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', # 'I', # 容易与 l 和 1 混淆 14 | 'J', 'K', 'L', 'M', 'N', # 'O', # 容易和 0 混淆 15 | 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', # 'Z' 容易和 2 混淆 16 | ] 17 | 18 | _LOWER_WORDS = [ 19 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', # 'l', # 混淆 1,I 20 | 'm', 'n', # 'o', # 混淆 0 21 | 'p', # 'q', # 混淆 9 22 | 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', # 'z', # 混淆 2 23 | ] 24 | 25 | _OTHER_WORDS = [ 26 | '#', '%', 27 | ] * 5 28 | 29 | WORDS = _NUM_WORDS + _UPPER_WORDS + _LOWER_WORDS + _OTHER_WORDS 30 | 31 | 32 | def create_code(k: int, *, img_width: int = 100, img_height: int = 50, font_sizes:List[int] = None): 33 | if not font_sizes: 34 | font_sizes = [35, 30, 33] 35 | elif isinstance(font_sizes, int): 36 | font_sizes = [font_sizes] 37 | image = ImageCaptcha(width=img_width, height=img_height, font_sizes=font_sizes) 38 | codes = ''.join(random.choices(WORDS, k=k)) 39 | captcha_img = image.generate(codes) 40 | return captcha_img.read(), codes 41 | 42 | 43 | def create_base64_code(k: int, *, img_width: int = 100, img_height: int = 50, font_sizes:List[int] = None): 44 | captcha_img, codes = create_code(k, img_width=img_width, img_height=img_height, font_sizes=font_sizes) 45 | return "data:image/png;base64," + base64.b64encode(captcha_img).decode('utf-8'), codes 46 | 47 | 48 | if __name__ == '__main__': 49 | print(create_base64_code(4)) 50 | -------------------------------------------------------------------------------- /backend/app/utils/encrypt.py: -------------------------------------------------------------------------------- 1 | import string 2 | import uuid 3 | import random 4 | 5 | 6 | def get_uuid(res_type: str = 'str'): 7 | """ 8 | 生成uuid 9 | :param res_type: :type str 返回类型, 默认str 10 | """ 11 | res_type = res_type.lower() 12 | obj = uuid.uuid4() 13 | if res_type.startswith('obj'): 14 | return obj 15 | elif res_type.startswith('h'): 16 | return obj.hex 17 | elif res_type.startswith('int'): 18 | return obj.int 19 | elif res_type.startswith('field'): 20 | return obj.fields 21 | else: 22 | return str(obj) 23 | 24 | 25 | 26 | def get_random_string(length: int, number: bool = True, uppercase: bool = True, lowercase: bool = True) -> str: 27 | """ 28 | 获取随机字符串 29 | :param length: :type int 字符串长度 30 | :param number: :type bool 是否含有数字 default: True 31 | :param uppercase: :type bool 是否含有大写英文字母 default: True 32 | :param lowercase: :type bool 是否含有小写英文字母 default: True 33 | :return: :type str 返回生成的随机字符串 default 34 | """ 35 | if type(length) != int: 36 | raise TypeError("length must be int") 37 | scope = "" 38 | if number: 39 | scope += string.digits 40 | if uppercase: 41 | scope += string.ascii_uppercase 42 | if lowercase: 43 | scope += string.ascii_lowercase 44 | if scope: 45 | return ''.join(random.choice(scope) for _ in range(length)) 46 | else: 47 | raise ValueError("number / uppercase / lowercase not all False") -------------------------------------------------------------------------------- /backend/app/utils/transform.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def camel_case_2_underscore(name: str, /, *, symbol: str = '_') -> str: 5 | """ 6 | 驼峰命名转下划线命名MallUser, mailName 替换成 mall_user 7 | :param name: 8 | :param symbol: 链接符号, 默认是下划线 9 | :return: 10 | """ 11 | name_list = re.findall(r"[A-Z][a-z\d]*", name[0].upper() + name[1:]) 12 | return symbol.join(name_list).lower() 13 | -------------------------------------------------------------------------------- /backend/app/workers/__init__.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | from celery.signals import after_setup_logger 3 | 4 | from utils.loggers import Logging 5 | 6 | 7 | app = Celery('tasks') 8 | app.config_from_object('workers.celeryconfig') 9 | 10 | 11 | @after_setup_logger.connect 12 | def setup_loggers(logger, *args, **kwargs): 13 | logger.addHandler(Logging.getAccessHandler("./log/celery_worker.log")) 14 | logger.addHandler(Logging.getErrorHandler("./log/celery_worker.err.log")) -------------------------------------------------------------------------------- /backend/app/workers/celery_tasks.py: -------------------------------------------------------------------------------- 1 | 2 | from redis import Redis 3 | from common.deps import get_db 4 | from core.constants import * 5 | from db.cache import get_redis 6 | from db.session import SessionLocal 7 | from . import app 8 | import traceback 9 | from app.apps.user.curd.curd_user import curd_user 10 | from core.logger import logger 11 | 12 | 13 | @app.task 14 | def taskPrintDatetime(): 15 | try: 16 | db = next(get_db()) # type: SessionLocal 17 | r = get_redis() # type: Redis 18 | dt = db.execute("SELECT now();") 19 | logger.info(dt.fetchall()) 20 | logger.info(f"===== {dt.t} =====") 21 | users = curd_user.query(db) 22 | logger.info(users) 23 | r.incr("taskPrintDatetimeRunCounter") 24 | except: 25 | traceback.print_exc() -------------------------------------------------------------------------------- /backend/app/workers/celeryconfig.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from core.config import settings 3 | from core.constants import * 4 | 5 | 6 | broker_url = settings.CELERY_BROKER 7 | result_backend = settings.CELERY_BACKEND 8 | 9 | task_serializer = 'json' 10 | result_serializer = 'json' 11 | accept_content = ['json'] 12 | 13 | 14 | # 导入任务所在文件 15 | imports = [ 16 | 'workers.celery_tasks', 17 | ] 18 | 19 | # 需要执行任务的配置 20 | beat_schedule = { 21 | 'test1': { 22 | 'task': 'workers.celery_tasks.taskPrintDatetime', 23 | # 设置定时的时间 24 | 'schedule': CELERY_PRINT_DATETIME, 25 | 'args': () 26 | } 27 | } -------------------------------------------------------------------------------- /frontend/dashboard/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /frontend/dashboard/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV='development' 3 | 4 | # base api 5 | VUE_APP_BASE_API='http://127.0.0.1:9898/api/v1/' 6 | VUE_APP_MEDIA_BASE='http://127.0.0.1:9898/media/' 7 | 8 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 9 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 10 | # It only does one thing by converting all import() to require(). 11 | # This configuration can significantly increase the speed of hot updates, 12 | # when you have a large number of pages. 13 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 14 | 15 | # VUE_CLI_BABEL_TRANSPILE_MODULES = true 16 | -------------------------------------------------------------------------------- /frontend/dashboard/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV='production' 3 | 4 | # base api 5 | VUE_APP_BASE_API='http://fastapi-vue-api.beginner2020.top/api/v1/' 6 | VUE_APP_MEDIA_BASE='http://fastapi-vue-api.beginner2020.top/media/' 7 | -------------------------------------------------------------------------------- /frontend/dashboard/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | 3 | # just a flag 4 | ENV='staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API='/api/v1/' 8 | VUE_APP_MEDIA_BASE='/media/' 9 | 10 | -------------------------------------------------------------------------------- /frontend/dashboard/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /frontend/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | tests/**/coverage/ 10 | tests/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.local 21 | 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /frontend/dashboard/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /frontend/dashboard/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 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 | -------------------------------------------------------------------------------- /frontend/dashboard/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ], 5 | env: { 6 | development: { 7 | plugins: ['dynamic-import-node'] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/dashboard/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/dashboard/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/dashboard/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /frontend/dashboard/plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /frontend/dashboard/plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate vue component', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'component name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '