├── .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 |
--------------------------------------------------------------------------------
/backend/app/email-templates/register.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
3 |
4 |
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: '',
17 | value: 'template',
18 | checked: true
19 | },
20 | {
21 | name: '
20 | {{/if}}
21 |
22 | {{#if style}}
23 |
26 | {{/if}}
27 |
--------------------------------------------------------------------------------
/frontend/dashboard/plop-templates/view/prompt.js:
--------------------------------------------------------------------------------
1 | const { notEmpty } = require('../utils.js')
2 |
3 | module.exports = {
4 | description: 'generate a view',
5 | prompts: [{
6 | type: 'input',
7 | name: 'name',
8 | message: 'view name please',
9 | validate: notEmpty('name')
10 | },
11 | {
12 | type: 'checkbox',
13 | name: 'blocks',
14 | message: 'Blocks:',
15 | choices: [{
16 | name: '',
17 | value: 'template',
18 | checked: true
19 | },
20 | {
21 | name: '
12 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/permission/label.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 查询b权限标签列表
4 | export function listPermLabel(query) {
5 | return request({
6 | url: '/permission/perm-label',
7 | method: 'get',
8 | params: query
9 | })
10 | }
11 |
12 |
13 | // 查询权限标签详细
14 | export function getPermLabel(labelId) {
15 | return request({
16 | url: '/permission/perm-label/' + labelId,
17 | method: 'get'
18 | })
19 | }
20 |
21 | // 新增权限标签
22 | export function addPermLabel(data) {
23 | console.log(data)
24 | return request({
25 | url: '/permission/perm-label',
26 | method: 'post',
27 | data: data
28 | })
29 | }
30 |
31 | // 修改权限标签
32 | export function setPermLabel(labelId, data) {
33 | return request({
34 | url: '/permission/perm-label/' + labelId,
35 | method: 'put',
36 | data: data
37 | })
38 | }
39 |
40 | // 删除权限标签
41 | export function delPermLabel(labelId) {
42 | return request({
43 | url: '/permission/perm-label/' + labelId,
44 | method: 'delete'
45 | })
46 | }
47 |
48 |
49 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/permission/menu.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 查询菜单列表
4 | export function listMenu(query) {
5 | return request({
6 | url: '/permission/menu',
7 | method: 'get',
8 | params: query
9 | })
10 | }
11 |
12 | // 查询菜单详细
13 | export function getMenu(id) {
14 | return request({
15 | url: '/permission/menu/' + id,
16 | method: 'get'
17 | })
18 | }
19 |
20 | // 查询菜单下拉树结构
21 | export function treeselect() {
22 | return request({
23 | url: '/permission/menu/simple/tree',
24 | method: 'get'
25 | })
26 | }
27 |
28 | // 查询菜单下列表结构
29 | export function listselect() {
30 | return request({
31 | url: '/permission/menu/simple/list',
32 | method: 'get'
33 | })
34 | }
35 |
36 |
37 | // 新增菜单
38 | export function addMenu(data) {
39 | return request({
40 | url: '/permission/menu/',
41 | method: 'post',
42 | data: data
43 | })
44 | }
45 |
46 | // 修改菜单
47 | export function updateMenu(id, data) {
48 | return request({
49 | url: '/permission/menu/' + id,
50 | method: 'put',
51 | data: data
52 | })
53 | }
54 |
55 | // 删除菜单
56 | export function delMenu(id) {
57 | return request({
58 | url: '/permission/menu/' + id,
59 | method: 'delete'
60 | })
61 | }
62 |
63 | // 获取最排序
64 | export function getMaxOrderNum(parent_id) {
65 | return request({
66 | url: "/permission/menu/" + parent_id + "/max-order-num",
67 | method: 'get'
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/permission/role.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 查询角色列表
4 | export function listRole(query) {
5 | return request({
6 | url: '/permission/role',
7 | method: 'get',
8 | params: query
9 | })
10 | }
11 |
12 |
13 | // 获取最大排序
14 | export function getRoleMaxOrderNum() {
15 | return request({
16 | url: "/permission/role/max-order-num",
17 | method: "get"
18 | })
19 | }
20 |
21 | // 查询角色详细
22 | export function getRole(roleId) {
23 | return request({
24 | url: '/permission/role/' + roleId,
25 | method: 'get'
26 | })
27 | }
28 |
29 | // 新增角色
30 | export function addRole(data) {
31 | return request({
32 | url: '/permission/role',
33 | method: 'post',
34 | data: data
35 | })
36 | }
37 |
38 | // 修改角色
39 | export function setRole(roleId, data) {
40 | return request({
41 | url: '/permission/role/' + roleId,
42 | method: 'put',
43 | data: data
44 | })
45 | }
46 |
47 |
48 | // 修改角色菜单
49 | export function setRoleMenus(roleId, menus) {
50 | console.log(menus)
51 | return request({
52 | url: "/permission/role/" + roleId + "/menu",
53 | method: "put",
54 | data: {menu_ids: menus}
55 | })
56 | }
57 |
58 |
59 | // 删除角色
60 | export function delRole(roleId) {
61 | return request({
62 | url: '/permission/role/' + roleId,
63 | method: 'delete'
64 | })
65 | }
66 |
67 |
68 | // 获取角色选择列表
69 | export function getRoleSelectList() {
70 | return request({
71 | url: "/permission/role/select/list",
72 | method: "get"
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/system/dict/data.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取字典
4 | export function getDicts(type) {
5 | return request({
6 | url: "/system/dict/type/" + type,
7 | method: 'get'
8 | })
9 | }
10 |
11 |
12 | // 查询字典类型列表
13 | export function listDictData(query) {
14 | return request({
15 | url: '/system/dict/data',
16 | method: 'get',
17 | params: query
18 | })
19 | }
20 |
21 | // 查询字典类型详细
22 | export function getDictData(id) {
23 | return request({
24 | url: '/system/dict/data/' + id,
25 | method: 'get'
26 | })
27 | }
28 |
29 | // 新增字典类型
30 | export function addDictData(data) {
31 | return request({
32 | url: '/system/dict/data',
33 | method: 'post',
34 | data: data
35 | })
36 | }
37 |
38 | // 修改字典类型
39 | export function setDictData(id, data) {
40 | return request({
41 | url: '/system/dict/data/' + id,
42 | method: 'put',
43 | data: data
44 | })
45 | }
46 |
47 | // 删除字典类型
48 | export function delDictData(id) {
49 | return request({
50 | url: '/system/dict/data/' + id,
51 | method: 'delete'
52 | })
53 | }
54 |
55 |
56 | export function getDictDataMaxOrderNum(){
57 | return request({
58 | url: "/system/dict/data/max-order-num",
59 | method: "get"
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/system/dict/detail.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 |
4 | // 查询字典数据列表
5 | export function listDetail(query) {
6 | return request({
7 | url: '/system/dict/detail',
8 | method: 'get',
9 | params: query
10 | })
11 | }
12 |
13 | // 查询字典数据详细
14 | export function getDetail(id) {
15 | return request({
16 | url: '/system/dict/detail/' + id,
17 | method: 'get'
18 | })
19 | }
20 |
21 | // 根据字典类型查询字典数据信息 用于user获取字典
22 | export function getDicts(dictType) {
23 | return request({
24 | url: '/system/dict/data/type_code/' + dictType,
25 | method: 'get'
26 | })
27 | }
28 |
29 | // 新增字典数据
30 | export function addDetail(data) {
31 | return request({
32 | url: '/system/dict/detail',
33 | method: 'post',
34 | data: data
35 | })
36 | }
37 |
38 | // 修改字典数据
39 | export function setDetail(id, data) {
40 | return request({
41 | url: '/system/dict/detail/' + id,
42 | method: 'put',
43 | data: data
44 | })
45 | }
46 |
47 | // 删除字典数据
48 | export function delDetail(id) {
49 | return request({
50 | url: '/system/dict/detail/' + id,
51 | method: 'delete'
52 | })
53 | }
54 |
55 |
56 | export function getDetailMaxOrderNum(dictDataID) {
57 | return request({
58 | url: "/system/dict/detail/max-order-num/" + dictDataID,
59 | method: "get"
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/api/system/parameter.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取参数
4 | export function getParameter(key) {
5 | return request({
6 | url: "/system/config-setting/key/" + key,
7 | method: 'get'
8 | })
9 | }
10 |
11 |
12 | // 查询参数列表
13 | export function listConfigSettings(query) {
14 | return request({
15 | url: '/system/config-setting',
16 | method: 'get',
17 | params: query
18 | })
19 | }
20 |
21 |
22 | // 获取参数
23 | export function getConfigSetting(id) {
24 | return request({
25 | url: "/system/config-setting/" + id,
26 | method: "get"
27 | })
28 | }
29 |
30 |
31 | // 获取最大排序
32 | export function getConfigSettingMaxOrderNum(){
33 | return request({
34 | url: "/system/config-setting/max-order-num",
35 | method: "get"
36 | })
37 | }
38 |
39 |
40 | // 添加参数
41 | export function addConfigSetting(data){
42 | return request({
43 | url: "/system/config-setting",
44 | method: "post",
45 | data: data
46 | })
47 | }
48 |
49 |
50 | // 更新参数
51 | export function setConfigSetting(id, data) {
52 | return request({
53 | url: "/system/config-setting/" + id,
54 | method: "put",
55 | data: data
56 | })
57 | }
58 |
59 |
60 | // 删除参数
61 | export function delConfigSetting(id) {
62 | return request({
63 | url: "/system/config-setting/" + id,
64 | method: 'delete'
65 | })
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/401_images/401.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/401_images/401.gif
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/custom-theme/fonts/element-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/custom-theme/fonts/element-icons.ttf
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/custom-theme/fonts/element-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/custom-theme/fonts/element-icons.woff
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/images/loading.gif
--------------------------------------------------------------------------------
/frontend/dashboard/src/assets/images/login-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/fe4437ae7622a3545348f9cdaff37f64922725e9/frontend/dashboard/src/assets/images/login-background.jpg
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Charts/mixins/resize.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '@/utils'
2 |
3 | export default {
4 | data() {
5 | return {
6 | $_sidebarElm: null,
7 | $_resizeHandler: null
8 | }
9 | },
10 | mounted() {
11 | this.initListener()
12 | },
13 | activated() {
14 | if (!this.$_resizeHandler) {
15 | // avoid duplication init
16 | this.initListener()
17 | }
18 |
19 | // when keep-alive chart activated, auto resize
20 | this.resize()
21 | },
22 | beforeDestroy() {
23 | this.destroyListener()
24 | },
25 | deactivated() {
26 | this.destroyListener()
27 | },
28 | methods: {
29 | // use $_ for mixins properties
30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
31 | $_sidebarResizeHandler(e) {
32 | if (e.propertyName === 'width') {
33 | this.$_resizeHandler()
34 | }
35 | },
36 | initListener() {
37 | this.$_resizeHandler = debounce(() => {
38 | this.resize()
39 | }, 100)
40 | window.addEventListener('resize', this.$_resizeHandler)
41 |
42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
44 | },
45 | destroyListener() {
46 | window.removeEventListener('resize', this.$_resizeHandler)
47 | this.$_resizeHandler = null
48 |
49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
50 | },
51 | resize() {
52 | const { chart } = this
53 | chart && chart.resize()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/DragSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
51 |
66 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/IconSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ item }}
11 |
12 |
13 |
14 |
15 |
16 |
44 |
45 |
69 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/IconSelect/requireIcons.js:
--------------------------------------------------------------------------------
1 |
2 | const req = require.context('@/icons/svg', false, /\.svg$/)
3 | const requireAll = requireContext => requireContext.keys()
4 |
5 | const re = /\.\/(.*)\.svg/
6 |
7 | const icons = requireAll(req).map(i => {
8 | return i.match(re)[1]
9 | })
10 |
11 | export default icons
12 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/ImageCropper/utils/data2blob.js:
--------------------------------------------------------------------------------
1 | /**
2 | * database64文件格式转换为2进制
3 | *
4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
5 | * @param {[String]} mime [description]
6 | * @return {[blob]} [description]
7 | */
8 | export default function(data, mime) {
9 | data = data.split(',')[1]
10 | data = window.atob(data)
11 | var ia = new Uint8Array(data.length)
12 | for (var i = 0; i < data.length; i++) {
13 | ia[i] = data.charCodeAt(i)
14 | }
15 | // canvas.toDataURL 返回的默认格式就是 image/png
16 | return new Blob([ia], {
17 | type: mime
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/ImageCropper/utils/effectRipple.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 点击波纹效果
3 | *
4 | * @param {[event]} e [description]
5 | * @param {[Object]} arg_opts [description]
6 | * @return {[bollean]} [description]
7 | */
8 | export default function(e, arg_opts) {
9 | var opts = Object.assign({
10 | ele: e.target, // 波纹作用元素
11 | type: 'hit', // hit点击位置扩散center中心点扩展
12 | bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
13 | }, arg_opts)
14 | var target = opts.ele
15 | if (target) {
16 | var rect = target.getBoundingClientRect()
17 | var ripple = target.querySelector('.e-ripple')
18 | if (!ripple) {
19 | ripple = document.createElement('span')
20 | ripple.className = 'e-ripple'
21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
22 | target.appendChild(ripple)
23 | } else {
24 | ripple.className = 'e-ripple'
25 | }
26 | switch (opts.type) {
27 | case 'center':
28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
30 | break
31 | default:
32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
34 | }
35 | ripple.style.backgroundColor = opts.bgc
36 | ripple.className = 'e-ripple z-active'
37 | return false
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/ImageCropper/utils/mimes.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'jpg': 'image/jpeg',
3 | 'png': 'image/png',
4 | 'gif': 'image/gif',
5 | 'svg': 'image/svg+xml',
6 | 'psd': 'image/photoshop'
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/MarkdownEditor/default-options.js:
--------------------------------------------------------------------------------
1 | // doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
2 | export default {
3 | minHeight: '200px',
4 | previewStyle: 'vertical',
5 | useCommandShortcut: true,
6 | useDefaultHTMLSanitizer: true,
7 | usageStatistics: false,
8 | hideModeSwitch: false,
9 | toolbarItems: [
10 | 'heading',
11 | 'bold',
12 | 'italic',
13 | 'strike',
14 | 'divider',
15 | 'hr',
16 | 'quote',
17 | 'divider',
18 | 'ul',
19 | 'ol',
20 | 'task',
21 | 'indent',
22 | 'outdent',
23 | 'divider',
24 | 'table',
25 | 'image',
26 | 'link',
27 | 'divider',
28 | 'code',
29 | 'codeblock'
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/README.md:
--------------------------------------------------------------------------------
1 | ##### 用于各组件功能点解释,避免重复组件引入
2 | ```
3 | BackToTop 返回顶部
4 | Breadcrumb pass
5 | Charts 图表
6 | DndList
7 | DragSelect 拖拽表单
8 |
9 | ```
10 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Screenfull/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
51 |
61 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/SizeSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{
9 | item.label }}
10 |
11 |
12 |
13 |
14 |
15 |
58 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Tinymce/dynamicLoadScript.js:
--------------------------------------------------------------------------------
1 | let callbacks = []
2 |
3 | function loadedTinymce() {
4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
5 | // check is successfully downloaded script
6 | return window.tinymce
7 | }
8 |
9 | const dynamicLoadScript = (src, callback) => {
10 | const existingScript = document.getElementById(src)
11 | const cb = callback || function() {}
12 |
13 | if (!existingScript) {
14 | const script = document.createElement('script')
15 | script.src = src // src url for the third-party library being loaded.
16 | script.id = src
17 | document.body.appendChild(script)
18 | callbacks.push(cb)
19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
20 | onEnd(script)
21 | }
22 |
23 | if (existingScript && cb) {
24 | if (loadedTinymce()) {
25 | cb(null, existingScript)
26 | } else {
27 | callbacks.push(cb)
28 | }
29 | }
30 |
31 | function stdOnEnd(script) {
32 | script.onload = function() {
33 | // this.onload = null here is necessary
34 | // because even IE9 works not like others
35 | this.onerror = this.onload = null
36 | for (const cb of callbacks) {
37 | cb(null, script)
38 | }
39 | callbacks = null
40 | }
41 | script.onerror = function() {
42 | this.onerror = this.onload = null
43 | cb(new Error('Failed to load ' + src), script)
44 | }
45 | }
46 |
47 | function ieOnEnd(script) {
48 | script.onreadystatechange = function() {
49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
50 | this.onreadystatechange = null
51 | for (const cb of callbacks) {
52 | cb(null, script) // there is no way to catch loading errors in IE8
53 | }
54 | callbacks = null
55 | }
56 | }
57 | }
58 |
59 | export default dynamicLoadScript
60 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Tinymce/plugins.js:
--------------------------------------------------------------------------------
1 | // Any plugins you want to use has to be imported
2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/
3 | // Custom builds see https://www.tinymce.com/download/custom-builds/
4 |
5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
6 |
7 | export default plugins
8 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/components/Tinymce/toolbar.js:
--------------------------------------------------------------------------------
1 | // Here is a list of the toolbar
2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
3 |
4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
5 |
6 | export default toolbar
7 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/button_permission/hasPermi.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 操作权限处理
3 | * Copyright (c) 2019 ruoyi
4 | */
5 |
6 | import store from '@/store'
7 |
8 | export default {
9 | inserted(el, binding, vnode) {
10 | const { value } = binding
11 | const all_permission = '*:*:*'
12 | const permissions = store.getters && store.getters.permissions
13 |
14 | if (value && value instanceof Array && value.length > 0) {
15 | const permissionFlag = value
16 |
17 | const hasPermissions = permissions.some(permission => {
18 | return all_permission === permission || permissionFlag.includes(permission)
19 | })
20 |
21 | if (!hasPermissions) {
22 | el.parentNode && el.parentNode.removeChild(el)
23 | }
24 | } else {
25 | throw new Error(`请设置操作权限标签值`)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/button_permission/hasRole.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 角色权限处理
3 | * Copyright (c) 2019 ruoyi
4 | */
5 |
6 | import store from '@/store'
7 |
8 | export default {
9 | inserted(el, binding, vnode) {
10 | const { value } = binding
11 | const super_admin = 'admin'
12 | const roles = store.getters && store.getters.roles
13 |
14 | if (value && value instanceof Array && value.length > 0) {
15 | const roleFlag = value
16 |
17 | const hasRole = roles.some(role => {
18 | return super_admin === role || roleFlag.includes(role)
19 | })
20 |
21 | if (!hasRole) {
22 | el.parentNode && el.parentNode.removeChild(el)
23 | }
24 | } else {
25 | throw new Error(`请设置角色权限标签值"`)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/button_permission/index.js:
--------------------------------------------------------------------------------
1 | import hasRole from './hasRole'
2 | import hasPermi from './hasPermi'
3 |
4 | const install = function(Vue) {
5 | Vue.directive('hasRole', hasRole)
6 | Vue.directive('hasPermi', hasPermi)
7 | }
8 |
9 | if (window.Vue) {
10 | window['hasRole'] = hasRole
11 | window['hasPermi'] = hasPermi
12 | Vue.use(install); // eslint-disable-line
13 | }
14 |
15 | export default install
16 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/clipboard/clipboard.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/Inndy/vue-clipboard2
2 | const Clipboard = require('clipboard')
3 | if (!Clipboard) {
4 | throw new Error('you should npm install `clipboard` --save at first ')
5 | }
6 |
7 | export default {
8 | bind(el, binding) {
9 | if (binding.arg === 'success') {
10 | el._v_clipboard_success = binding.value
11 | } else if (binding.arg === 'error') {
12 | el._v_clipboard_error = binding.value
13 | } else {
14 | const clipboard = new Clipboard(el, {
15 | text() { return binding.value },
16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
17 | })
18 | clipboard.on('success', e => {
19 | const callback = el._v_clipboard_success
20 | callback && callback(e) // eslint-disable-line
21 | })
22 | clipboard.on('error', e => {
23 | const callback = el._v_clipboard_error
24 | callback && callback(e) // eslint-disable-line
25 | })
26 | el._v_clipboard = clipboard
27 | }
28 | },
29 | update(el, binding) {
30 | if (binding.arg === 'success') {
31 | el._v_clipboard_success = binding.value
32 | } else if (binding.arg === 'error') {
33 | el._v_clipboard_error = binding.value
34 | } else {
35 | el._v_clipboard.text = function() { return binding.value }
36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
37 | }
38 | },
39 | unbind(el, binding) {
40 | if (binding.arg === 'success') {
41 | delete el._v_clipboard_success
42 | } else if (binding.arg === 'error') {
43 | delete el._v_clipboard_error
44 | } else {
45 | el._v_clipboard.destroy()
46 | delete el._v_clipboard
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/clipboard/index.js:
--------------------------------------------------------------------------------
1 | import Clipboard from './clipboard'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('Clipboard', Clipboard)
5 | }
6 |
7 | if (window.Vue) {
8 | window.clipboard = Clipboard
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | Clipboard.install = install
13 | export default Clipboard
14 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/el-drag-dialog/index.js:
--------------------------------------------------------------------------------
1 | import drag from './drag'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-drag-dialog', drag)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-drag-dialog'] = drag
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | drag.install = install
13 | export default drag
14 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/el-table/adaptive.js:
--------------------------------------------------------------------------------
1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
2 |
3 | /**
4 | * How to use
5 | * ...
6 | * el-table height is must be set
7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page.
8 | */
9 |
10 | const doResize = (el, binding, vnode) => {
11 | const { componentInstance: $table } = vnode
12 |
13 | const { value } = binding
14 |
15 | if (!$table.height) {
16 | throw new Error(`el-$table must set the height. Such as height='100px'`)
17 | }
18 | const bottomOffset = (value && value.bottomOffset) || 30
19 |
20 | if (!$table) return
21 |
22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
23 | $table.layout.setHeight(height)
24 | $table.doLayout()
25 | }
26 |
27 | export default {
28 | bind(el, binding, vnode) {
29 | el.resizeListener = () => {
30 | doResize(el, binding, vnode)
31 | }
32 | // parameter 1 is must be "Element" type
33 | addResizeListener(window.document.body, el.resizeListener)
34 | },
35 | inserted(el, binding, vnode) {
36 | doResize(el, binding, vnode)
37 | },
38 | unbind(el) {
39 | removeResizeListener(window.document.body, el.resizeListener)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/el-table/index.js:
--------------------------------------------------------------------------------
1 | import adaptive from './adaptive'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-height-adaptive-table', adaptive)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-height-adaptive-table'] = adaptive
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | adaptive.install = install
13 | export default adaptive
14 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/permission/index.js:
--------------------------------------------------------------------------------
1 | import permission from './permission'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('permission', permission)
5 | }
6 |
7 | if (window.Vue) {
8 | window['permission'] = permission
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | permission.install = install
13 | export default permission
14 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/permission/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | function checkPermission(el, binding) {
4 | const { value } = binding
5 | const roles = store.getters && store.getters.roles
6 |
7 | if (value && value instanceof Array) {
8 | if (value.length > 0) {
9 | const permissionRoles = value
10 |
11 | const hasPermission = roles.some(role => {
12 | return permissionRoles.includes(role)
13 | })
14 |
15 | if (!hasPermission) {
16 | el.parentNode && el.parentNode.removeChild(el)
17 | }
18 | }
19 | } else {
20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`)
21 | }
22 | }
23 |
24 | export default {
25 | inserted(el, binding) {
26 | checkPermission(el, binding)
27 | },
28 | update(el, binding) {
29 | checkPermission(el, binding)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/waves/index.js:
--------------------------------------------------------------------------------
1 | import waves from './waves'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('waves', waves)
5 | }
6 |
7 | if (window.Vue) {
8 | window.waves = waves
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | waves.install = install
13 | export default waves
14 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/directive/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 | }
--------------------------------------------------------------------------------
/frontend/dashboard/src/filters/index.js:
--------------------------------------------------------------------------------
1 | // import parseTime, formatTime and set to filter
2 | export { parseTime, formatTime } from '@/utils'
3 |
4 | /**
5 | * Show plural label if time is plural number
6 | * @param {number} time
7 | * @param {string} label
8 | * @return {string}
9 | */
10 | function pluralize(time, label) {
11 | if (time === 1) {
12 | return time + label
13 | }
14 | return time + label + 's'
15 | }
16 |
17 | /**
18 | * @param {number} time
19 | */
20 | export function timeAgo(time) {
21 | const between = Date.now() / 1000 - Number(time)
22 | if (between < 3600) {
23 | return pluralize(~~(between / 60), ' minute')
24 | } else if (between < 86400) {
25 | return pluralize(~~(between / 3600), ' hour')
26 | } else {
27 | return pluralize(~~(between / 86400), ' day')
28 | }
29 | }
30 |
31 | /**
32 | * Number formatting
33 | * like 10000 => 10k
34 | * @param {number} num
35 | * @param {number} digits
36 | */
37 | export function numberFormatter(num, digits) {
38 | const si = [
39 | { value: 1E18, symbol: 'E' },
40 | { value: 1E15, symbol: 'P' },
41 | { value: 1E12, symbol: 'T' },
42 | { value: 1E9, symbol: 'G' },
43 | { value: 1E6, symbol: 'M' },
44 | { value: 1E3, symbol: 'k' }
45 | ]
46 | for (let i = 0; i < si.length; i++) {
47 | if (num >= si[i].value) {
48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
49 | }
50 | }
51 | return num.toString()
52 | }
53 |
54 | /**
55 | * 10000 => "10,000"
56 | * @param {number} num
57 | */
58 | export function toThousandFilter(num) {
59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
60 | }
61 |
62 | /**
63 | * Upper case first char
64 | * @param {String} string
65 | */
66 | export function uppercaseFirst(string) {
67 | return string.charAt(0).toUpperCase() + string.slice(1)
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/404.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/bug.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/build.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/chart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/checkbox.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/clipboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/component.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/documentation.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/drag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/druid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/education.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/email.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/excel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/exit-fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/guide.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/input.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/international.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/job.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/language.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/lock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/log.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/logininfor.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/message.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/money.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/monitor.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/pdf.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/people.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/peoples.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/phone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/post.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/question.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/radio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/rate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/row.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/select.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/server.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/size.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/skill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/slider.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/star.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/swagger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/switch.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/textarea.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/theme.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/time.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/tree-table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/upload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/validCode.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/wechat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svg/zip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
24 |
25 |
49 |
50 |
58 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
55 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as AppMain } from './AppMain'
2 | export { default as Navbar } from './Navbar'
3 | export { default as Settings } from './Settings'
4 | export { default as Sidebar } from './Sidebar/index.vue'
5 | export { default as TagsView } from './TagsView/index.vue'
6 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | const { body } = document
4 | const WIDTH = 992 // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route(route) {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler)
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler)
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile()
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile')
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect()
32 | return rect.width - 1 < WIDTH
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile()
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'Vue Element Admin',
3 |
4 | /**
5 | * @type {boolean} true | false
6 | * @description Whether show the settings right-panel
7 | */
8 | showSettings: false,
9 |
10 | /**
11 | * @type {boolean} true | false
12 | * @description Whether need tagsView
13 | */
14 | tagsView: true,
15 |
16 | /**
17 | * @type {boolean} true | false
18 | * @description Whether fix the header
19 | */
20 | fixedHeader: false,
21 |
22 | /**
23 | * @type {boolean} true | false
24 | * @description Whether show the logo in sidebar
25 | */
26 | sidebarLogo: true,
27 |
28 | /**
29 | * @type {string | array} 'production' | ['production', 'development']
30 | * @description Need show err logs component.
31 | * The default is only used in the production env
32 | * If you want to also use it in dev, you can pass ['production', 'development']
33 | */
34 | errorLog: 'production'
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | size: state => state.app.size,
4 | device: state => state.app.device,
5 | visitedViews: state => state.tagsView.visitedViews,
6 | cachedViews: state => state.tagsView.cachedViews,
7 | token: state => state.user.token,
8 | avatar: state => state.user.avatar,
9 | username: state => state.user.username,
10 | nickname: state => state.user.nickname,
11 | email: state => state.user.email,
12 | phone: state => state.user.phone,
13 | sex: state => state.user.sex,
14 | roles: state => state.user.roles,
15 | roles_name: state => state.user.roles_name,
16 | permissions: state => state.user.permissions,
17 | permission_routes: state => state.permission.routes,
18 | errorLogs: state => state.errorLog.logs
19 | }
20 | export default getters
21 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 |
5 | Vue.use(Vuex)
6 |
7 | // https://webpack.js.org/guides/dependency-management/#requirecontext
8 | const modulesFiles = require.context('./modules', true, /\.js$/)
9 |
10 | // you do not need `import app from './modules/app'`
11 | // it will auto require all vuex module from modules file
12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => {
13 | // set './app.js' => 'app'
14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
15 | const value = modulesFiles(modulePath)
16 | modules[moduleName] = value.default
17 | return modules
18 | }, {})
19 |
20 | const store = new Vuex.Store({
21 | modules,
22 | getters
23 | })
24 |
25 | export default store
26 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop',
9 | size: Cookies.get('size') || 'medium'
10 | }
11 |
12 | const mutations = {
13 | TOGGLE_SIDEBAR: state => {
14 | state.sidebar.opened = !state.sidebar.opened
15 | state.sidebar.withoutAnimation = false
16 | if (state.sidebar.opened) {
17 | Cookies.set('sidebarStatus', 1)
18 | } else {
19 | Cookies.set('sidebarStatus', 0)
20 | }
21 | },
22 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
23 | Cookies.set('sidebarStatus', 0)
24 | state.sidebar.opened = false
25 | state.sidebar.withoutAnimation = withoutAnimation
26 | },
27 | TOGGLE_DEVICE: (state, device) => {
28 | state.device = device
29 | },
30 | SET_SIZE: (state, size) => {
31 | state.size = size
32 | Cookies.set('size', size)
33 | }
34 | }
35 |
36 | const actions = {
37 | toggleSideBar({ commit }) {
38 | commit('TOGGLE_SIDEBAR')
39 | },
40 | closeSideBar({ commit }, { withoutAnimation }) {
41 | commit('CLOSE_SIDEBAR', withoutAnimation)
42 | },
43 | toggleDevice({ commit }, device) {
44 | commit('TOGGLE_DEVICE', device)
45 | },
46 | setSize({ commit }, size) {
47 | commit('SET_SIZE', size)
48 | }
49 | }
50 |
51 | export default {
52 | namespaced: true,
53 | state,
54 | mutations,
55 | actions
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/store/modules/errorLog.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | logs: []
3 | }
4 |
5 | const mutations = {
6 | ADD_ERROR_LOG: (state, log) => {
7 | state.logs.push(log)
8 | },
9 | CLEAR_ERROR_LOG: (state) => {
10 | state.logs.splice(0)
11 | }
12 | }
13 |
14 | const actions = {
15 | addErrorLog({ commit }, log) {
16 | commit('ADD_ERROR_LOG', log)
17 | },
18 | clearErrorLog({ commit }) {
19 | commit('CLEAR_ERROR_LOG')
20 | }
21 | }
22 |
23 | export default {
24 | namespaced: true,
25 | state,
26 | mutations,
27 | actions
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import variables from '@/styles/element-variables.scss'
2 | import defaultSettings from '@/settings'
3 |
4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
5 |
6 | const state = {
7 | theme: variables.theme,
8 | showSettings: showSettings,
9 | tagsView: tagsView,
10 | fixedHeader: fixedHeader,
11 | sidebarLogo: sidebarLogo
12 | }
13 |
14 | const mutations = {
15 | CHANGE_SETTING: (state, { key, value }) => {
16 | if (state.hasOwnProperty(key)) {
17 | state[key] = value
18 | }
19 | }
20 | }
21 |
22 | const actions = {
23 | changeSetting({ commit }, data) {
24 | commit('CHANGE_SETTING', data)
25 | }
26 | }
27 |
28 | export default {
29 | namespaced: true,
30 | state,
31 | mutations,
32 | actions
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/styles/btn.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 |
3 | @mixin colorBtn($color) {
4 | background: $color;
5 |
6 | &:hover {
7 | color: $color;
8 |
9 | &:before,
10 | &:after {
11 | background: $color;
12 | }
13 | }
14 | }
15 |
16 | .blue-btn {
17 | @include colorBtn($blue)
18 | }
19 |
20 | .light-blue-btn {
21 | @include colorBtn($light-blue)
22 | }
23 |
24 | .red-btn {
25 | @include colorBtn($red)
26 | }
27 |
28 | .pink-btn {
29 | @include colorBtn($pink)
30 | }
31 |
32 | .green-btn {
33 | @include colorBtn($green)
34 | }
35 |
36 | .tiffany-btn {
37 | @include colorBtn($tiffany)
38 | }
39 |
40 | .yellow-btn {
41 | @include colorBtn($yellow)
42 | }
43 |
44 | .pan-btn {
45 | font-size: 14px;
46 | color: #fff;
47 | padding: 14px 36px;
48 | border-radius: 8px;
49 | border: none;
50 | outline: none;
51 | transition: 600ms ease all;
52 | position: relative;
53 | display: inline-block;
54 |
55 | &:hover {
56 | background: #fff;
57 |
58 | &:before,
59 | &:after {
60 | width: 100%;
61 | transition: 600ms ease all;
62 | }
63 | }
64 |
65 | &:before,
66 | &:after {
67 | content: '';
68 | position: absolute;
69 | top: 0;
70 | right: 0;
71 | height: 2px;
72 | width: 0;
73 | transition: 400ms ease all;
74 | }
75 |
76 | &::after {
77 | right: inherit;
78 | top: inherit;
79 | left: 0;
80 | bottom: 0;
81 | }
82 | }
83 |
84 | .custom-button {
85 | display: inline-block;
86 | line-height: 1;
87 | white-space: nowrap;
88 | cursor: pointer;
89 | background: #fff;
90 | color: #fff;
91 | -webkit-appearance: none;
92 | text-align: center;
93 | box-sizing: border-box;
94 | outline: 0;
95 | margin: 0;
96 | padding: 10px 15px;
97 | font-size: 14px;
98 | border-radius: 4px;
99 | }
100 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 | .cell {
19 | .el-tag {
20 | margin-right: 0px;
21 | }
22 | }
23 |
24 | .small-padding {
25 | .cell {
26 | padding-left: 5px;
27 | padding-right: 5px;
28 | }
29 | }
30 |
31 | .fixed-width {
32 | .el-button--mini {
33 | padding: 7px 10px;
34 | min-width: 60px;
35 | }
36 | }
37 |
38 | .status-col {
39 | .cell {
40 | padding: 0 10px;
41 | text-align: center;
42 |
43 | .el-tag {
44 | margin-right: 0px;
45 | }
46 | }
47 | }
48 |
49 | // to fixed https://github.com/ElemeFE/element/issues/2461
50 | .el-dialog {
51 | transform: none;
52 | left: 0;
53 | position: relative;
54 | margin: 0 auto;
55 | }
56 |
57 | // refine element ui upload
58 | .upload-container {
59 | .el-upload {
60 | width: 100%;
61 |
62 | .el-upload-dragger {
63 | width: 100%;
64 | height: 200px;
65 | }
66 | }
67 | }
68 |
69 | // dropdown
70 | .el-dropdown-menu {
71 | a {
72 | display: block
73 | }
74 | }
75 |
76 | // fix date-picker ui bug in filter-item
77 | .el-range-editor.el-input__inner {
78 | display: inline-flex !important;
79 | }
80 |
81 | // to fix el-date-picker css style
82 | .el-range-separator {
83 | box-sizing: content-box;
84 | }
85 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/styles/element-variables.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * I think element-ui's default theme color is too light for long-term use.
3 | * So I modified the default color and you can modify it to your liking.
4 | **/
5 |
6 | /* theme color */
7 | $--color-primary: #1890ff;
8 | $--color-success: #13ce66;
9 | $--color-warning: #ffba00;
10 | $--color-danger: #ff4949;
11 | // $--color-info: #1E1E1E;
12 |
13 | $--button-font-weight: 400;
14 |
15 | // $--color-text-regular: #1f2d3d;
16 |
17 | $--border-color-light: #dfe4ed;
18 | $--border-color-lighter: #e6ebf5;
19 |
20 | $--table-border: 1px solid #dfe6ec;
21 |
22 | /* icon font path, required */
23 | $--font-path: "~element-ui/lib/theme-chalk/fonts";
24 |
25 | @import "~element-ui/packages/theme-chalk/src/index";
26 |
27 | // the :export directive is the magic sauce for webpack
28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
29 | :export {
30 | theme: $--color-primary;
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
30 | @mixin pct($pct) {
31 | width: #{$pct};
32 | position: relative;
33 | margin: 0 auto;
34 | }
35 |
36 | @mixin triangle($width, $height, $color, $direction) {
37 | $width: $width/2;
38 | $color-border-style: $height solid $color;
39 | $transparent-border-style: $width solid transparent;
40 | height: 0;
41 | width: 0;
42 |
43 | @if $direction==up {
44 | border-bottom: $color-border-style;
45 | border-left: $transparent-border-style;
46 | border-right: $transparent-border-style;
47 | }
48 |
49 | @else if $direction==right {
50 | border-left: $color-border-style;
51 | border-top: $transparent-border-style;
52 | border-bottom: $transparent-border-style;
53 | }
54 |
55 | @else if $direction==down {
56 | border-top: $color-border-style;
57 | border-left: $transparent-border-style;
58 | border-right: $transparent-border-style;
59 | }
60 |
61 | @else if $direction==left {
62 | border-right: $color-border-style;
63 | border-top: $transparent-border-style;
64 | border-bottom: $transparent-border-style;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/dashboard/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 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // base color
2 | $blue:#324157;
3 | $light-blue:#3A71A8;
4 | $red:#C03639;
5 | $pink: #E65D6E;
6 | $green: #30B08F;
7 | $tiffany: #4AB7BD;
8 | $yellow:#FEC171;
9 | $panGreen: #30B08F;
10 |
11 | // sidebar
12 | $menuText:#bfcbd9;
13 | $menuActiveText:#409EFF;
14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
15 |
16 | $menuBg:#304156;
17 | $menuHover:#263445;
18 |
19 | $subMenuBg:#1f2d3d;
20 | $subMenuHover:#001528;
21 |
22 | $sideBarWidth: 210px;
23 |
24 | // the :export directive is the magic sauce for webpack
25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
26 | :export {
27 | menuText: $menuText;
28 | menuActiveText: $menuActiveText;
29 | subMenuActiveText: $subMenuActiveText;
30 | menuBg: $menuBg;
31 | menuHover: $menuHover;
32 | subMenuBg: $subMenuBg;
33 | subMenuHover: $subMenuHover;
34 | sideBarWidth: $sideBarWidth;
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'Admin-Token'
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken(token) {
10 | return Cookies.set(TokenKey, token)
11 | }
12 |
13 | export function removeToken() {
14 | return Cookies.remove(TokenKey)
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/clipboard.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Clipboard from 'clipboard'
3 |
4 | function clipboardSuccess() {
5 | Vue.prototype.$message({
6 | message: 'Copy successfully',
7 | type: 'success',
8 | duration: 1500
9 | })
10 | }
11 |
12 | function clipboardError() {
13 | Vue.prototype.$message({
14 | message: 'Copy failed',
15 | type: 'error'
16 | })
17 | }
18 |
19 | export default function handleClipboard(text, event) {
20 | const clipboard = new Clipboard(event.target, {
21 | text: () => text
22 | })
23 | clipboard.on('success', () => {
24 | clipboardSuccess()
25 | clipboard.destroy()
26 | })
27 | clipboard.on('error', () => {
28 | clipboardError()
29 | clipboard.destroy()
30 | })
31 | clipboard.onClick(event)
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/error-log.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '@/store'
3 | import { isString, isArray } from '@/utils/validate'
4 | import settings from '@/settings'
5 |
6 | // you can set in settings.js
7 | // errorLog:'production' | ['production', 'development']
8 | const { errorLog: needErrorLog } = settings
9 |
10 | function checkNeed() {
11 | const env = process.env.NODE_ENV
12 | if (isString(needErrorLog)) {
13 | return env === needErrorLog
14 | }
15 | if (isArray(needErrorLog)) {
16 | return needErrorLog.includes(env)
17 | }
18 | return false
19 | }
20 |
21 | if (checkNeed()) {
22 | Vue.config.errorHandler = function(err, vm, info, a) {
23 | // Don't ask me why I use Vue.nextTick, it just a hack.
24 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
25 | Vue.nextTick(() => {
26 | store.dispatch('errorLog/addErrorLog', {
27 | err,
28 | vm,
29 | info,
30 | url: window.location.href
31 | })
32 | console.error(err, info)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'Vue Element Admin'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/jsencrypt.js:
--------------------------------------------------------------------------------
1 | import JSEncrypt from "jsencrypt/bin/jsencrypt.min";
2 |
3 | // 密钥对生成 http://web.chacuo.net/netrsakeypair
4 |
5 | const publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n" +
6 | "nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==";
7 |
8 | const privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n" +
9 | "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n" +
10 | "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n" +
11 | "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n" +
12 | "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n" +
13 | "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n" +
14 | "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n" +
15 | "UP8iWi1Qw0Y=";
16 |
17 | // 加密
18 | export function encrypt(txt) {
19 | const encryptor = new JSEncrypt();
20 | encryptor.setPublicKey(publicKey); // 设置公钥
21 | return encryptor.encrypt(txt); // 对数据进行加密
22 | }
23 |
24 | // 解密
25 | export function decrypt(txt) {
26 | const encryptor = new JSEncrypt();
27 | encryptor.setPrivateKey(privateKey); // 设置私钥
28 | return encryptor.decrypt(txt); // 对数据进行解密
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/open-window.js:
--------------------------------------------------------------------------------
1 | /**
2 | *Created by PanJiaChen on 16/11/29.
3 | * @param {Sting} url
4 | * @param {Sting} title
5 | * @param {Number} w
6 | * @param {Number} h
7 | */
8 | export default function openWindow(url, title, w, h) {
9 | // Fixes dual-screen position Most browsers Firefox
10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
12 |
13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
15 |
16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft
17 | const top = ((height / 2) - (h / 2)) + dualScreenTop
18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
19 |
20 | // Puts focus on the newWindow
21 | if (window.focus) {
22 | newWindow.focus()
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | /**
4 | * @param {Array} value
5 | * @returns {Boolean}
6 | * @example see @/views/permission/directive.vue
7 | */
8 | export default function checkPermission(value) {
9 | if (value && value instanceof Array && value.length > 0) {
10 | const roles = store.getters && store.getters.roles
11 | const permissionRoles = value
12 |
13 | const hasPermission = roles.some(role => {
14 | return permissionRoles.includes(role)
15 | })
16 |
17 | if (!hasPermission) {
18 | return false
19 | }
20 | return true
21 | } else {
22 | console.error(`need roles! Like v-permission="['admin','editor']"`)
23 | return false
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/utils/scroll-to.js:
--------------------------------------------------------------------------------
1 | Math.easeInOutQuad = function(t, b, c, d) {
2 | t /= d / 2
3 | if (t < 1) {
4 | return c / 2 * t * t + b
5 | }
6 | t--
7 | return -c / 2 * (t * (t - 2) - 1) + b
8 | }
9 |
10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
11 | var requestAnimFrame = (function() {
12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
13 | })()
14 |
15 | /**
16 | * Because it's so fucking difficult to detect the scrolling element, just move them all
17 | * @param {number} amount
18 | */
19 | function move(amount) {
20 | document.documentElement.scrollTop = amount
21 | document.body.parentNode.scrollTop = amount
22 | document.body.scrollTop = amount
23 | }
24 |
25 | function position() {
26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
27 | }
28 |
29 | /**
30 | * @param {number} to
31 | * @param {number} duration
32 | * @param {Function} callback
33 | */
34 | export function scrollTo(to, duration, callback) {
35 | const start = position()
36 | const change = to - start
37 | const increment = 20
38 | let currentTime = 0
39 | duration = (typeof (duration) === 'undefined') ? 500 : duration
40 | var animateScroll = function() {
41 | // increment the time
42 | currentTime += increment
43 | // find the value with the quadratic in-out easing function
44 | var val = Math.easeInOutQuad(currentTime, start, change, duration)
45 | // move the document.body
46 | move(val)
47 | // do the animation unless its over
48 | if (currentTime < duration) {
49 | requestAnimFrame(animateScroll)
50 | } else {
51 | if (callback && typeof (callback) === 'function') {
52 | // the animation is done so lets callback
53 | callback()
54 | }
55 | }
56 | }
57 | animateScroll()
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/vendor/Export2Zip.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { saveAs } from 'file-saver'
3 | import JSZip from 'jszip'
4 |
5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) {
6 | const zip = new JSZip()
7 | const txt_name = txtName || 'file'
8 | const zip_name = zipName || 'file'
9 | const data = jsonData
10 | let txtData = `${th}\r\n`
11 | data.forEach((row) => {
12 | let tempStr = ''
13 | tempStr = row.toString()
14 | txtData += `${tempStr}\r\n`
15 | })
16 | zip.file(`${txt_name}.txt`, txtData)
17 | zip.generateAsync({
18 | type: "blob"
19 | }).then((blob) => {
20 | saveAs(blob, `${zip_name}.zip`)
21 | }, (err) => {
22 | alert('导出失败')
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/dashboard/admin/components/mixins/resize.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '@/utils'
2 |
3 | export default {
4 | data() {
5 | return {
6 | $_sidebarElm: null,
7 | $_resizeHandler: null
8 | }
9 | },
10 | mounted() {
11 | this.$_resizeHandler = debounce(() => {
12 | if (this.chart) {
13 | this.chart.resize()
14 | }
15 | }, 100)
16 | this.$_initResizeEvent()
17 | this.$_initSidebarResizeEvent()
18 | },
19 | beforeDestroy() {
20 | this.$_destroyResizeEvent()
21 | this.$_destroySidebarResizeEvent()
22 | },
23 | // to fixed bug when cached by keep-alive
24 | // https://github.com/PanJiaChen/vue-element-admin/issues/2116
25 | activated() {
26 | this.$_initResizeEvent()
27 | this.$_initSidebarResizeEvent()
28 | },
29 | deactivated() {
30 | this.$_destroyResizeEvent()
31 | this.$_destroySidebarResizeEvent()
32 | },
33 | methods: {
34 | // use $_ for mixins properties
35 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
36 | $_initResizeEvent() {
37 | window.addEventListener('resize', this.$_resizeHandler)
38 | },
39 | $_destroyResizeEvent() {
40 | window.removeEventListener('resize', this.$_resizeHandler)
41 | },
42 | $_sidebarResizeHandler(e) {
43 | if (e.propertyName === 'width') {
44 | this.$_resizeHandler()
45 | }
46 | },
47 | $_initSidebarResizeEvent() {
48 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
49 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
50 | },
51 | $_destroySidebarResizeEvent() {
52 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
32 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/profile/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
73 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/redirect/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/user/layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
25 |
26 |
80 |
81 |
--------------------------------------------------------------------------------
/frontend/dashboard/src/views/user/login/auth-redirect.vue:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/components/Hamburger.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import Hamburger from '@/components/Hamburger/index.vue'
3 | describe('Hamburger.vue', () => {
4 | it('toggle click', () => {
5 | const wrapper = shallowMount(Hamburger)
6 | const mockFn = jest.fn()
7 | wrapper.vm.$on('toggleClick', mockFn)
8 | wrapper.find('.hamburger').trigger('click')
9 | expect(mockFn).toBeCalled()
10 | })
11 | it('prop isActive', () => {
12 | const wrapper = shallowMount(Hamburger)
13 | wrapper.setProps({ isActive: true })
14 | expect(wrapper.contains('.is-active')).toBe(true)
15 | wrapper.setProps({ isActive: false })
16 | expect(wrapper.contains('.is-active')).toBe(false)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/components/SvgIcon.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import SvgIcon from '@/components/SvgIcon/index.vue'
3 | describe('SvgIcon.vue', () => {
4 | it('iconClass', () => {
5 | const wrapper = shallowMount(SvgIcon, {
6 | propsData: {
7 | iconClass: 'test'
8 | }
9 | })
10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11 | })
12 | it('className', () => {
13 | const wrapper = shallowMount(SvgIcon, {
14 | propsData: {
15 | iconClass: 'test'
16 | }
17 | })
18 | expect(wrapper.classes().length).toBe(1)
19 | wrapper.setProps({ className: 'test' })
20 | expect(wrapper.classes().includes('test')).toBe(true)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/utils/formatTime.spec.js:
--------------------------------------------------------------------------------
1 | import { formatTime } from '@/utils/index.js'
2 | describe('Utils:formatTime', () => {
3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
4 | const retrofit = 5 * 1000
5 |
6 | it('ten digits timestamp', () => {
7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
8 | })
9 | it('test now', () => {
10 | expect(formatTime(+new Date() - 1)).toBe('刚刚')
11 | })
12 | it('less two minute', () => {
13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
14 | })
15 | it('less two hour', () => {
16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
17 | })
18 | it('less one day', () => {
19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
20 | })
21 | it('more than one day', () => {
22 | expect(formatTime(d)).toBe('7月13日17时54分')
23 | })
24 | it('format', () => {
25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/utils/param2Obj.spec.js:
--------------------------------------------------------------------------------
1 | import { param2Obj } from '@/utils/index.js'
2 | describe('Utils:param2Obj', () => {
3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
4 |
5 | it('param2Obj test', () => {
6 | expect(param2Obj(url)).toEqual({
7 | name: 'bill',
8 | age: '29',
9 | sex: '1',
10 | field: window.btoa('test'),
11 | key: '测试'
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
1 | import { parseTime } from '@/utils/index.js'
2 |
3 | describe('Utils:parseTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | it('timestamp', () => {
6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7 | })
8 |
9 | it('timestamp string', () => {
10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
11 | })
12 |
13 | it('ten digits timestamp', () => {
14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
15 | })
16 | it('new Date', () => {
17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
18 | })
19 | it('format', () => {
20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
23 | })
24 | it('get the day of the week', () => {
25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
26 | })
27 | it('get the day of the week', () => {
28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
29 | })
30 | it('empty argument', () => {
31 | expect(parseTime()).toBeNull()
32 | })
33 |
34 | it('null', () => {
35 | expect(parseTime(null)).toBeNull()
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/frontend/dashboard/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js'
2 | describe('Utils:validate', () => {
3 | it('validUsername', () => {
4 | expect(validUsername('admin')).toBe(true)
5 | expect(validUsername('editor')).toBe(true)
6 | expect(validUsername('xxxx')).toBe(false)
7 | })
8 | it('validURL', () => {
9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false)
12 | })
13 | it('validLowerCase', () => {
14 | expect(validLowerCase('abc')).toBe(true)
15 | expect(validLowerCase('Abc')).toBe(false)
16 | expect(validLowerCase('123abc')).toBe(false)
17 | })
18 | it('validUpperCase', () => {
19 | expect(validUpperCase('ABC')).toBe(true)
20 | expect(validUpperCase('Abc')).toBe(false)
21 | expect(validUpperCase('123ABC')).toBe(false)
22 | })
23 | it('validAlphabets', () => {
24 | expect(validAlphabets('ABC')).toBe(true)
25 | expect(validAlphabets('Abc')).toBe(true)
26 | expect(validAlphabets('123aBC')).toBe(false)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 |
2 | build_backend:
3 | docker build -f ./Dockerfile.backend . -t fastapi_vue/backend:latest
4 |
5 | build_frontend:
6 | docker build -f ./Dockerfile.frontend . -t fastapi_vue/frontend:latest
7 |
8 | up:
9 | docker-compose up -d
10 |
11 | up_local:
12 | docker-compose up -env ./.env.local -d
13 |
--------------------------------------------------------------------------------
/nginx.conf.example:
--------------------------------------------------------------------------------
1 | upstream fastapi-backend {
2 | server 127.0.0.1:8100 weight=20 max_fails=200 fail_timeout=20s;
3 | server 127.0.0.1:8101 weight=20 max_fails=200 fail_timeout=20s;
4 | }
5 |
6 | server {
7 | listen 80;
8 | server_name localhost; # 改为api的(二级)域名
9 | client_max_body_size 5m;
10 | error_log /home/ubuntu/log/nginx/fastapi-backend.error.log;
11 | access_log /home/ubuntu/log/nginx/fastapi-backend.access.log;
12 |
13 | location / {
14 | proxy_set_header Host $host;
15 | proxy_set_header X-Real-IP $remote_addr;
16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
17 | proxy_set_header X-User-Agent $http_user_agent;
18 | proxy_pass http://fastapi-backend;
19 | }
20 | }
21 |
22 | server {
23 | listen 80;
24 | server_name localhost; # 改为前端使用的域名
25 | client_max_body_size 5m;
26 | error_log /home/ubuntu/log/nginx/fastapi-frontend.error.log;
27 | access_log /home/ubuntu/log/nginx/fastapi-frontend.access.log;
28 |
29 | location / {
30 | root /home/ubuntu/opt/fastAPI-vue/frontend/dist;
31 | try_files $uri $uri/ /index.html;
32 | index index.html index.htm;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------