├── backend └── app │ ├── core │ ├── __init__.py │ ├── logger.py │ └── constants.py │ ├── db │ ├── __init__.py │ ├── session.py │ ├── cache.py │ └── mongo.py │ ├── common │ ├── __init__.py │ ├── security.py │ ├── resp.py │ └── error_code.py │ ├── log │ └── .gitignore │ ├── utils │ ├── __init__.py │ ├── transform.py │ ├── encrypt.py │ └── captcha_code.py │ ├── apps │ ├── user │ │ ├── curd │ │ │ └── __init__.py │ │ ├── schemas │ │ │ ├── __init__.py │ │ │ ├── token_schemas.py │ │ │ └── user_info_schemas.py │ │ └── __init__.py │ ├── base_template │ │ ├── __init__.py │ │ ├── views.py │ │ ├── curd │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── model_base.py │ │ └── schemas │ │ │ └── __init__.py │ ├── system │ │ ├── curd │ │ │ ├── __init__.py │ │ │ └── curd_dict_detail.py │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── config_settings.py │ │ │ └── dictionaries.py │ │ └── schemas.py │ ├── permission │ │ ├── curd │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── perm_label.py │ │ │ ├── role.py │ │ │ ├── menu.py │ │ │ └── user.py │ │ └── schemas.py │ └── __init__.py │ ├── configs │ ├── .gitignore │ ├── supervisor.conf.example │ ├── .env.example │ └── logging_config.conf │ ├── media │ ├── .gitignore │ └── images │ │ └── avatar │ │ └── default │ │ └── avatar.jpg │ ├── email-templates │ ├── register.html │ └── forget-password.html │ ├── workers │ ├── __init__.py │ ├── celeryconfig.py │ └── celery_tasks.py │ └── requirements.txt ├── frontend └── dashboard │ ├── .eslintignore │ ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── utils │ │ ├── param2Obj.spec.js │ │ ├── formatTime.spec.js │ │ ├── validate.spec.js │ │ └── parseTime.spec.js │ │ └── components │ │ ├── SvgIcon.spec.js │ │ └── Hamburger.spec.js │ ├── postcss.config.js │ ├── .travis.yml │ ├── public │ ├── alipay.jpg │ ├── favicon.ico │ ├── qqqun.jpg │ ├── wechatpay.jpg │ └── index.html │ ├── src │ ├── assets │ │ ├── 401_images │ │ │ └── 401.gif │ │ ├── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ │ ├── images │ │ │ ├── loading.gif │ │ │ └── login-background.jpg │ │ └── custom-theme │ │ │ └── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ ├── App.vue │ ├── icons │ │ ├── svg │ │ │ ├── chart.svg │ │ │ ├── size.svg │ │ │ ├── link.svg │ │ │ ├── guide.svg │ │ │ ├── component.svg │ │ │ ├── money.svg │ │ │ ├── email.svg │ │ │ ├── drag.svg │ │ │ ├── documentation.svg │ │ │ ├── fullscreen.svg │ │ │ ├── user.svg │ │ │ ├── lock.svg │ │ │ ├── excel.svg │ │ │ ├── example.svg │ │ │ ├── slider.svg │ │ │ ├── star.svg │ │ │ ├── search.svg │ │ │ ├── table.svg │ │ │ ├── password.svg │ │ │ ├── education.svg │ │ │ ├── tab.svg │ │ │ ├── message.svg │ │ │ ├── switch.svg │ │ │ ├── theme.svg │ │ │ ├── druid.svg │ │ │ ├── code.svg │ │ │ ├── peoples.svg │ │ │ ├── input.svg │ │ │ ├── server.svg │ │ │ ├── textarea.svg │ │ │ ├── time.svg │ │ │ ├── edit.svg │ │ │ ├── nested.svg │ │ │ ├── row.svg │ │ │ ├── monitor.svg │ │ │ ├── tree-table.svg │ │ │ ├── eye.svg │ │ │ ├── build.svg │ │ │ ├── clipboard.svg │ │ │ ├── list.svg │ │ │ ├── download.svg │ │ │ ├── icon.svg │ │ │ ├── international.svg │ │ │ ├── question.svg │ │ │ ├── wechat.svg │ │ │ ├── skill.svg │ │ │ ├── people.svg │ │ │ ├── post.svg │ │ │ ├── checkbox.svg │ │ │ ├── language.svg │ │ │ ├── eye-open.svg │ │ │ ├── validCode.svg │ │ │ ├── radio.svg │ │ │ ├── select.svg │ │ │ ├── upload.svg │ │ │ ├── 404.svg │ │ │ ├── zip.svg │ │ │ ├── phone.svg │ │ │ ├── log.svg │ │ │ ├── bug.svg │ │ │ ├── github.svg │ │ │ ├── pdf.svg │ │ │ ├── logininfor.svg │ │ │ ├── rate.svg │ │ │ ├── job.svg │ │ │ ├── exit-fullscreen.svg │ │ │ ├── tree.svg │ │ │ └── swagger.svg │ │ ├── index.js │ │ └── svgo.yml │ ├── components │ │ ├── ImageCropper │ │ │ └── utils │ │ │ │ ├── mimes.js │ │ │ │ ├── data2blob.js │ │ │ │ └── effectRipple.js │ │ ├── README.md │ │ ├── IconSelect │ │ │ ├── requireIcons.js │ │ │ └── index.vue │ │ ├── Tinymce │ │ │ ├── toolbar.js │ │ │ ├── plugins.js │ │ │ └── dynamicLoadScript.js │ │ ├── MarkdownEditor │ │ │ └── default-options.js │ │ ├── Screenfull │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── SizeSelect │ │ │ └── index.vue │ │ ├── Charts │ │ │ └── mixins │ │ │ │ └── resize.js │ │ ├── DragSelect │ │ │ └── index.vue │ │ └── JsonEditor │ │ │ └── index.vue │ ├── layout │ │ ├── components │ │ │ ├── index.js │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Link.vue │ │ │ │ ├── Item.vue │ │ │ │ └── index.vue │ │ │ └── AppMain.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── utils │ │ ├── get-page-title.js │ │ ├── auth.js │ │ ├── permission.js │ │ ├── clipboard.js │ │ ├── error-log.js │ │ ├── jsencrypt.js │ │ ├── open-window.js │ │ └── scroll-to.js │ ├── directive │ │ ├── waves │ │ │ ├── index.js │ │ │ └── waves.css │ │ ├── el-drag-dialog │ │ │ └── index.js │ │ ├── clipboard │ │ │ ├── index.js │ │ │ └── clipboard.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ ├── el-table │ │ │ ├── index.js │ │ │ └── adaptive.js │ │ └── button_permission │ │ │ ├── index.js │ │ │ ├── hasRole.js │ │ │ └── hasPermi.js │ ├── views │ │ ├── redirect │ │ │ └── index.vue │ │ ├── user │ │ │ ├── login │ │ │ │ └── auth-redirect.vue │ │ │ └── layout.vue │ │ ├── dashboard │ │ │ ├── index.vue │ │ │ └── admin │ │ │ │ └── components │ │ │ │ └── mixins │ │ │ │ └── resize.js │ │ └── profile │ │ │ └── index.vue │ ├── store │ │ ├── modules │ │ │ ├── errorLog.js │ │ │ ├── settings.js │ │ │ └── app.js │ │ ├── index.js │ │ └── getters.js │ ├── vendor │ │ └── Export2Zip.js │ ├── styles │ │ ├── variables.scss │ │ ├── element-variables.scss │ │ ├── transition.scss │ │ ├── element-ui.scss │ │ ├── mixin.scss │ │ └── btn.scss │ ├── settings.js │ ├── api │ │ ├── permission │ │ │ ├── label.js │ │ │ ├── menu.js │ │ │ └── role.js │ │ └── system │ │ │ ├── dict │ │ │ ├── data.js │ │ │ └── detail.js │ │ │ └── parameter.js │ └── filters │ │ └── index.js │ ├── .env.staging │ ├── jsconfig.json │ ├── babel.config.js │ ├── .env.production │ ├── plop-templates │ ├── utils.js │ ├── store │ │ ├── index.hbs │ │ └── prompt.js │ ├── view │ │ ├── index.hbs │ │ └── prompt.js │ └── component │ │ ├── index.hbs │ │ └── prompt.js │ ├── .editorconfig │ ├── .gitignore │ ├── plopfile.js │ ├── .env.development │ ├── jest.config.js │ ├── build │ └── index.js │ └── LICENSE ├── .gitattributes ├── makefile ├── .gitignore ├── .dockerignore ├── .env.example ├── .conf.docker └── app.nginx.conf ├── Dockerfile.frontend ├── nginx.conf.example └── Dockerfile.backend /backend/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/log/.gitignore: -------------------------------------------------------------------------------- 1 | ./* -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/user/curd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/configs/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /backend/app/apps/base_template/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/base_template/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/system/curd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/user/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/base_template/curd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/permission/curd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/base_template/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/base_template/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as user_api -------------------------------------------------------------------------------- /backend/app/apps/system/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as system_api -------------------------------------------------------------------------------- /backend/app/apps/permission/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router as permission_api -------------------------------------------------------------------------------- /frontend/dashboard/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /backend/app/media/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | !./imagess 3 | !/images/avatar/default 4 | /* 5 | !.gitignore -------------------------------------------------------------------------------- /frontend/dashboard/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py linguist-language=python 2 | *.vue linguist-language=python 3 | *.js linguist-language=vue 4 | -------------------------------------------------------------------------------- /frontend/dashboard/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/app/email-templates/register.html: -------------------------------------------------------------------------------- 1 |
2 |
请点击以下链接完成账号验证
3 | {{ url }} 4 |
-------------------------------------------------------------------------------- /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/public/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/public/alipay.jpg -------------------------------------------------------------------------------- /frontend/dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /frontend/dashboard/public/qqqun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/public/qqqun.jpg -------------------------------------------------------------------------------- /backend/app/email-templates/forget-password.html: -------------------------------------------------------------------------------- 1 |
2 |
请点击以下链接设置新密码
3 | {{ url }} 4 |
-------------------------------------------------------------------------------- /frontend/dashboard/public/wechatpay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/public/wechatpay.jpg -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/src/assets/images/loading.gif -------------------------------------------------------------------------------- /backend/app/media/images/avatar/default/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/backend/app/media/images/avatar/default/avatar.jpg -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/images/login-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/frontend/dashboard/src/assets/images/login-background.jpg -------------------------------------------------------------------------------- /frontend/dashboard/src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDoe1996/fastAPI-vue/HEAD/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/HEAD/frontend/dashboard/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /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/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /frontend/dashboard/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /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/src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/.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/plop-templates/utils.js: -------------------------------------------------------------------------------- 1 | exports.notEmpty = name => { 2 | return v => { 3 | if (!v || v.trim === '') { 4 | return `${name} is required` 5 | } else { 6 | return true 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/components/README.md: -------------------------------------------------------------------------------- 1 | ##### 用于各组件功能点解释,避免重复组件引入 2 | ``` 3 | BackToTop 返回顶部 4 | Breadcrumb pass 5 | Charts 图表 6 | DndList 7 | DragSelect 拖拽表单 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/dashboard/plop-templates/store/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if state}} 2 | const state = {} 3 | {{/if}} 4 | 5 | {{#if mutations}} 6 | const mutations = {} 7 | {{/if}} 8 | 9 | {{#if actions}} 10 | const actions = {} 11 | {{/if}} 12 | 13 | export default { 14 | namespaced: true, 15 | {{options}} 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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/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/.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/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/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/drag.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/.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /frontend/dashboard/src/views/user/login/auth-redirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /frontend/dashboard/plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('./plop-templates/view/prompt') 2 | const componentGenerator = require('./plop-templates/component/prompt') 3 | const storeGenerator = require('./plop-templates/store/prompt.js') 4 | 5 | module.exports = function(plop) { 6 | plop.setGenerator('view', viewGenerator) 7 | plop.setGenerator('component', componentGenerator) 8 | plop.setGenerator('store', storeGenerator) 9 | } 10 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/plop-templates/view/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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")) -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= webpackConfig.name %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/slider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/src/icons/svg/switch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/icons/svg/druid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /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/icons/svg/input.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/icons/svg/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/textarea.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/icons/svg/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/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/icons/svg/row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/src/icons/svg/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /frontend/dashboard/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /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")) -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/icons/svg/build.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/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/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | ) -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/checkbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/validCode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/radio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/src/icons/svg/select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/" -------------------------------------------------------------------------------- /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/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /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/icons/svg/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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: '