├── backend ├── app │ ├── __init__.py │ ├── init │ │ ├── __init__.py │ │ ├── routers.py │ │ ├── mount.py │ │ ├── cors.py │ │ └── middleware.py │ ├── schemas │ │ ├── __init__.py │ │ ├── system │ │ │ ├── __init__.py │ │ │ ├── file.py │ │ │ ├── roles.py │ │ │ ├── lookup.py │ │ │ └── menu.py │ │ └── base.py │ ├── services │ │ ├── __init__.py │ │ └── system │ │ │ ├── __init__.py │ │ │ └── role.py │ ├── exceptions │ │ ├── __init__.py │ │ └── exceptions.py │ ├── apis │ │ ├── system │ │ │ ├── __init__.py │ │ │ ├── id_center.py │ │ │ ├── roles.py │ │ │ ├── file.py │ │ │ ├── menu.py │ │ │ ├── lookup.py │ │ │ └── notify.py │ │ ├── __init__.py │ │ ├── api_router.py │ │ └── deps.py │ ├── corelibs │ │ ├── __init__.py │ │ ├── local.py │ │ ├── consts.py │ │ ├── custom_router.py │ │ ├── codes.py │ │ └── logger.py │ ├── utils │ │ ├── common.py │ │ ├── __init__.py │ │ ├── context.py │ │ ├── current_user.py │ │ ├── create_dir.py │ │ └── serialize.py │ ├── db │ │ └── __init__.py │ └── models │ │ └── __init__.py ├── celery_worker │ ├── __init__.py │ ├── tasks │ │ ├── __init__.py │ │ └── common.py │ ├── scheduler │ │ ├── __init__.py │ │ ├── literals.py │ │ └── session.py │ └── base.py ├── requirements ├── db_script │ └── my.cnf ├── .env.example ├── start.sh ├── .gitignore └── main.py ├── frontend ├── src │ ├── stores │ │ ├── index.ts │ │ ├── requestOldRoutes.ts │ │ ├── tagsViewRoutes.ts │ │ ├── auth.ts │ │ ├── routesList.ts │ │ ├── lookup.ts │ │ ├── menu.ts │ │ ├── user.ts │ │ ├── setup.ts │ │ └── keepAliveNames.ts │ ├── layout │ │ ├── navBars │ │ │ ├── global-search │ │ │ │ └── index.ts │ │ │ ├── topBar │ │ │ │ ├── settings │ │ │ │ │ └── component │ │ │ │ │ │ ├── icons │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── setting.vue │ │ │ │ │ │ └── full-content.vue │ │ │ │ │ │ └── general.vue │ │ │ │ └── closeFull.vue │ │ │ └── index.vue │ │ ├── component │ │ │ ├── header.vue │ │ │ └── main.vue │ │ ├── footer │ │ │ └── index.vue │ │ ├── navMenu │ │ │ └── subItem.vue │ │ ├── index.vue │ │ └── main │ │ │ ├── transverse.vue │ │ │ ├── classic.vue │ │ │ ├── columns.vue │ │ │ └── defaults.vue │ ├── assets │ │ ├── weixin.png │ │ ├── logo-mini.png │ │ ├── bakgrounImage │ │ │ ├── bj_hc.png │ │ │ └── bakgrounImage.jpg │ │ └── fonts │ │ │ └── HarmonyOS_Sans_SC_Medium.ttf │ ├── icons │ │ ├── index.ts │ │ ├── svg │ │ │ ├── icons │ │ │ │ ├── add.svg │ │ │ │ ├── back.svg │ │ │ │ ├── code.svg │ │ │ │ ├── forking.svg │ │ │ │ ├── gold_medal.svg │ │ │ │ ├── mysql_icon.svg │ │ │ │ ├── silver_medal.svg │ │ │ │ ├── bronze_medal.svg │ │ │ │ ├── case_svg.svg │ │ │ │ ├── exc_count.svg │ │ │ │ ├── UI.svg │ │ │ │ ├── case_run_count.svg │ │ │ │ ├── add_case.svg │ │ │ │ ├── project_svg.svg │ │ │ │ ├── websocket.svg │ │ │ │ ├── user_login.svg │ │ │ │ ├── loop.svg │ │ │ │ └── robots.svg │ │ │ ├── index.ts │ │ │ └── load.ts │ │ ├── create-icon.ts │ │ ├── iconify │ │ │ └── index.ts │ │ └── lucide │ │ │ └── index.ts │ ├── theme │ │ ├── jacoco │ │ │ ├── up.gif │ │ │ ├── down.gif │ │ │ ├── sort.gif │ │ │ ├── bundle.gif │ │ │ ├── class.gif │ │ │ ├── group.gif │ │ │ ├── method.gif │ │ │ ├── package.gif │ │ │ ├── redbar.gif │ │ │ ├── report.gif │ │ │ ├── session.gif │ │ │ ├── source.gif │ │ │ ├── branchfc.gif │ │ │ ├── branchnc.gif │ │ │ ├── branchpc.gif │ │ │ ├── greenbar.gif │ │ │ └── prettify.css │ │ ├── iconfont │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ └── iconfont.css │ │ ├── media │ │ │ ├── cityLinkage.scss │ │ │ ├── tagsView.scss │ │ │ ├── dialog.scss │ │ │ ├── pagination.scss │ │ │ ├── personal.scss │ │ │ ├── media.scss │ │ │ ├── index.scss │ │ │ ├── home.scss │ │ │ ├── date.scss │ │ │ ├── form.scss │ │ │ ├── error.scss │ │ │ ├── layout.scss │ │ │ ├── scrollbar.scss │ │ │ ├── login.scss │ │ │ └── chart.scss │ │ ├── zerorunner │ │ │ └── case.scss │ │ ├── index.scss │ │ ├── tableTool.scss │ │ ├── splitpanes.scss │ │ ├── iconSelector.scss │ │ ├── common │ │ │ ├── default.scss │ │ │ └── test-case-default.scss │ │ ├── mixins │ │ │ └── index.scss │ │ ├── other.scss │ │ └── loading.scss │ ├── utils │ │ ├── index.ts │ │ ├── mitt.ts │ │ ├── unique.ts │ │ ├── config.ts │ │ ├── lookup.ts │ │ ├── urlHandler.ts │ │ ├── authFunction.ts │ │ ├── setIconfont.ts │ │ ├── loading.ts │ │ ├── storage.ts │ │ ├── watermark.ts │ │ ├── wartermark.ts │ │ ├── arrayOperation.ts │ │ ├── mersenneTwister.js │ │ ├── commonFunction.ts │ │ ├── request.ts │ │ ├── tree.ts │ │ └── common.ts │ ├── types │ │ ├── terminal.d.ts │ │ ├── axios.d.ts │ │ ├── layout.d.ts │ │ └── mitt.d.ts │ ├── directive │ │ ├── clickOutside.ts │ │ ├── index.ts │ │ └── authDirective.ts │ ├── components │ │ ├── monaco │ │ │ └── index.d.ts │ │ ├── Z-Table │ │ │ └── expand.vue │ │ ├── ZeroCard │ │ │ └── index.vue │ │ ├── svgIcon │ │ │ └── index.vue │ │ └── iconSelector │ │ │ └── list.vue │ ├── api │ │ ├── useSystemApi │ │ │ ├── statistic.ts │ │ │ ├── idCenter.ts │ │ │ ├── roles.ts │ │ │ ├── file.ts │ │ │ ├── menu.ts │ │ │ ├── user.ts │ │ │ └── lookup.ts │ │ ├── login │ │ │ └── index.ts │ │ └── menu │ │ │ └── index.ts │ ├── main.ts │ └── views │ │ ├── login │ │ └── component │ │ │ ├── scan.vue │ │ │ └── mobile.vue │ │ └── error │ │ └── 401.vue ├── .env.development ├── .env.production ├── .eslintignore ├── .gitignore ├── .env ├── index.html ├── README.md ├── LICENSE ├── .prettierrc.js ├── CHANGELOG.md └── package.json ├── static └── img │ ├── func.png │ ├── index.png │ ├── report.png │ ├── weixin.jpg │ └── weixin.png ├── .gitattributes ├── .gitignore └── README.md /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/init/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/services/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/schemas/system/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/app/services/system/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /backend/celery_worker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | -------------------------------------------------------------------------------- /backend/celery_worker/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | -------------------------------------------------------------------------------- /frontend/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './setup'; 2 | export { storeToRefs } from 'pinia'; 3 | -------------------------------------------------------------------------------- /static/img/func.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/static/img/func.png -------------------------------------------------------------------------------- /backend/requirements: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/backend/requirements -------------------------------------------------------------------------------- /static/img/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/static/img/index.png -------------------------------------------------------------------------------- /static/img/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/static/img/report.png -------------------------------------------------------------------------------- /static/img/weixin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/static/img/weixin.jpg -------------------------------------------------------------------------------- /static/img/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/static/img/weixin.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts linguist-language=python 2 | *.js linguist-language=python 3 | *.html linguist-language=python -------------------------------------------------------------------------------- /backend/app/apis/system/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from . import user 5 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/global-search/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GlobalSearch } from './global-search.vue'; 2 | -------------------------------------------------------------------------------- /frontend/src/assets/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/assets/weixin.png -------------------------------------------------------------------------------- /frontend/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lucide'; 2 | export * from './svg/index'; 3 | export * from './iconify/index'; 4 | -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/up.gif -------------------------------------------------------------------------------- /backend/app/corelibs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from .local import g 5 | 6 | __all__ = ["g"] -------------------------------------------------------------------------------- /frontend/src/assets/logo-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/assets/logo-mini.png -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/down.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/sort.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/sort.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/bundle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/bundle.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/class.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/class.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/group.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/group.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/method.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/method.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/package.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/package.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/redbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/redbar.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/report.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/report.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/session.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/session.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/source.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/source.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/branchfc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/branchfc.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/branchnc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/branchnc.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/branchpc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/branchpc.gif -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/greenbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/jacoco/greenbar.gif -------------------------------------------------------------------------------- /frontend/src/theme/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /frontend/src/theme/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/iconfont/iconfont.woff -------------------------------------------------------------------------------- /frontend/src/theme/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/theme/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unique'; 2 | export * from './tree'; 3 | export * from './inference'; 4 | export * from './common'; 5 | -------------------------------------------------------------------------------- /frontend/src/assets/bakgrounImage/bj_hc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/assets/bakgrounImage/bj_hc.png -------------------------------------------------------------------------------- /backend/app/apis/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from .api_router import app_router 5 | 6 | __all__ = ["app_router"] 7 | -------------------------------------------------------------------------------- /backend/app/apis/system/id_center.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from fastapi import APIRouter 4 | 5 | router = APIRouter() 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/types/terminal.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare interface TerminalState { 3 | term: any 4 | wsTime: any, 5 | ws: any, 6 | command: string, 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | ENV='development' 3 | 4 | # 本地环境接口地址 5 | VITE_API_BASE_URL='http://localhost:9100' 6 | 7 | # 后端接口前缀 8 | VITE_API_PREFIX='/api' -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | ENV = 'production' 3 | 4 | # 线上环境接口地址 5 | VITE_API_BASE_URL = 'https://localhost' 6 | 7 | # 后端接口前缀 8 | VITE_API_PREFIX = '/api' -------------------------------------------------------------------------------- /frontend/src/assets/bakgrounImage/bakgrounImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/assets/bakgrounImage/bakgrounImage.jpg -------------------------------------------------------------------------------- /frontend/src/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/vue-fastapi-admin/HEAD/frontend/src/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf -------------------------------------------------------------------------------- /backend/app/utils/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import uuid 4 | 5 | 6 | def get_str_uuid(): 7 | return str(uuid.uuid4()).replace("-", "") 8 | -------------------------------------------------------------------------------- /backend/app/db/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from app.db.redis import RedisPool 4 | from config import config 5 | 6 | redis_pool = RedisPool() 7 | -------------------------------------------------------------------------------- /backend/celery_worker/tasks/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from celery_worker.worker import celery 5 | 6 | 7 | @celery.task 8 | def add(i): 9 | return 1 + i 10 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from .create_dir import create_dir 5 | from .current_user import current_user 6 | 7 | __all__ = ["create_dir", "current_user"] 8 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | lib 5 | *.md 6 | *.scss 7 | *.woff 8 | *.ttf 9 | .vscode 10 | .idea 11 | dist 12 | mock 13 | public 14 | bin 15 | build 16 | config 17 | index.html 18 | src/assets -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | // https://www.npmjs.com/package/mitt 2 | import mitt, { Emitter } from 'mitt'; 3 | 4 | // 类型 5 | const emitter: Emitter = mitt(); 6 | 7 | // 导出 8 | export default emitter; 9 | -------------------------------------------------------------------------------- /frontend/src/directive/clickOutside.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue'; 2 | import {ClickOutside} from "element-plus"; 3 | 4 | /** 5 | 点击外部区域触发 6 | */ 7 | export function clickOutside(app: App) { 8 | app.directive('click-outside', ClickOutside) 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/theme/media/cityLinkage.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-cascader__dropdown.el-popper { 7 | overflow: auto; 8 | max-width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/theme/media/tagsView.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .tags-view-form { 7 | .tags-view-form-col { 8 | margin-bottom: 20px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/theme/media/dialog.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于800px 4 | ------------------------------- */ 5 | @media screen and (max-width: 800px) { 6 | .el-dialog { 7 | width: 90% !important; 8 | } 9 | .el-dialog.is-fullscreen { 10 | width: 100% !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/celery_worker/scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # flake8:noqa 3 | 4 | from .session import SessionManager 5 | from .models import ( 6 | PeriodicTask, PeriodicTaskChanged, 7 | CrontabSchedule, IntervalSchedule, 8 | SolarSchedule, 9 | ) 10 | from .schedulers import DatabaseScheduler 11 | -------------------------------------------------------------------------------- /frontend/src/types/axios.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as axios from 'axios'; 3 | 4 | // 扩展 axios 数据返回类型,可自行扩展 5 | declare module 'axios' { 6 | export interface AxiosResponse { 7 | code: number; 8 | data: T; 9 | message: string; 10 | type?: string; 11 | [key: string]: T; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/theme/zerorunner/case.scss: -------------------------------------------------------------------------------- 1 | 2 | .method-color-get { 3 | color: #61affe 4 | } 5 | 6 | .method-color-post { 7 | color: #49cc90 8 | } 9 | 10 | .method-color-delete { 11 | color: #f93e3d 12 | } 13 | 14 | .method-color-put { 15 | color: #fca130 16 | } 17 | 18 | .method-color-na { 19 | color: #f56c6c 20 | } -------------------------------------------------------------------------------- /frontend/src/components/monaco/index.d.ts: -------------------------------------------------------------------------------- 1 | // import SQLSnippets from "/@/components/monaco/core/sql"; 2 | 3 | declare interface MonacoStateData { 4 | sqlSnippets: null | SQLSnippets, 5 | contentBackup: null | string, 6 | isSettingContent: boolean, 7 | jsonPath: null | string, 8 | options: { 9 | [key: string]: T; 10 | } 11 | } -------------------------------------------------------------------------------- /backend/app/init/routers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from fastapi import FastAPI 5 | 6 | from app.apis import app_router 7 | from config import config 8 | 9 | 10 | def init_router(app: FastAPI): 11 | """ 注册路由 """ 12 | # 权限(权限在每个接口上) 13 | app.include_router(app_router, prefix=config.API_PREFIX) 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/src/theme/media/pagination.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-pager, 7 | .el-pagination__jump { 8 | display: none !important; 9 | } 10 | // 默认居中对齐 11 | .el-pagination, 12 | .table-footer { 13 | justify-content: center !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | # port 端口号 2 | VITE_PORT=8893 3 | 4 | # open 运行 npm run dev 时自动打开浏览器 5 | VITE_OPEN=true 6 | 7 | # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可 8 | VITE_PUBLIC_PATH= 9 | 10 | # 网站主标题(菜单导航、浏览器当前网页标题) 11 | VITE_GLOBAL_TITLE=fast-element-admin 12 | #网站副标题(登录页顶部文字) 13 | VITE_GLOBAL_VICE_TITLE=fast-element-admin 14 | 15 | VITE_APP_NAMESPACE=fast-element-admin -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/statistic.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 统计接口 5 | */ 6 | export function useStatisticsApi() { 7 | return { 8 | countStatistic: () => { 9 | return request({ 10 | url: '/statistic/countStatistic', 11 | method: 'POST', 12 | data: {} 13 | }); 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/theme/media/personal.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .personal-info { 7 | padding-left: 0 !important; 8 | margin-top: 15px; 9 | } 10 | .personal-recommend-col { 11 | margin-bottom: 15px; 12 | &:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/idCenter.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 菜单接口 5 | * @method getId 获取id 6 | */ 7 | export function useIdCenterApi() { 8 | return { 9 | // 获取id 10 | getId: () => { 11 | return request({ 12 | url: '/idCenter/getId', 13 | method: 'GET', 14 | data: {}, 15 | }); 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/db_script/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | pid-file = /var/run/mysqld/mysqld.pid 3 | socket = /var/run/mysqld/mysqld.sock 4 | datadir = /var/lib/mysql 5 | 6 | symbolic-links=0 7 | # 1GB 8 | max_allowed_packet=1073741824 9 | # 大小写不敏感 10 | 11 | # lower_case_table_names=1 12 | 13 | # 慢查询 14 | slow_query_log = ON 15 | slow_query_log_file = /var/lib/mysql/slow.log 16 | long_query_time = 3 -------------------------------------------------------------------------------- /frontend/src/icons/create-icon.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue'; 2 | 3 | import { Icon } from '@iconify/vue'; 4 | 5 | function createIconifyIcon(icon: string) { 6 | return defineComponent({ 7 | name: `Icon-${icon}`, 8 | setup(props, { attrs }) { 9 | return () => h(Icon, { icon, ...props, ...attrs }); 10 | }, 11 | }); 12 | } 13 | 14 | export { createIconifyIcon }; 15 | -------------------------------------------------------------------------------- /frontend/src/theme/jacoco/prettify.css: -------------------------------------------------------------------------------- 1 | /* Pretty printing styles. Used with prettify.js. */ 2 | 3 | .str { color: #2A00FF; } 4 | .kwd { color: #7F0055; font-weight:bold; } 5 | .com { color: #3F5FBF; } 6 | .typ { color: #606; } 7 | .lit { color: #066; } 8 | .pun { color: #660; } 9 | .pln { color: #000; } 10 | .tag { color: #008; } 11 | .atn { color: #606; } 12 | .atv { color: #080; } 13 | .dec { color: #606; } 14 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | 2 | # redis 3 | REDIS_URI=redis://:redis@localhost:6379/4 4 | 5 | # mysql 异步 6 | MYSQL_DATABASE_URI=mysql+asyncmy://root:123456@localhost:3306/zerorunner?charset=UTF8MB4 7 | 8 | # celery 9 | CELERY_BROKER_URL=redis://:redis@localhost:6379/5 10 | CELERY_RESULT_BACKEND=redis://:redis@localhost:6379/5 11 | CELERY_BEAT_DB_URL=mysql+pymysql://root:123456@localhost:3306/zerorunner?charset=UTF8MB4 12 | -------------------------------------------------------------------------------- /frontend/src/theme/media/media.scss: -------------------------------------------------------------------------------- 1 | @import './login.scss'; 2 | @import './error.scss'; 3 | @import './layout.scss'; 4 | @import './personal.scss'; 5 | @import './tagsView.scss'; 6 | @import './home.scss'; 7 | @import './chart.scss'; 8 | @import './form.scss'; 9 | @import './scrollbar.scss'; 10 | @import './pagination.scss'; 11 | @import './dialog.scss'; 12 | @import './cityLinkage.scss'; 13 | @import './date.scss'; 14 | -------------------------------------------------------------------------------- /frontend/src/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import './app.scss'; 2 | @import 'common/transition.scss'; 3 | @import 'common/default.scss'; 4 | @import 'common/test-case-default.scss'; 5 | @import './other.scss'; 6 | @import './element.scss'; 7 | @import './media/media.scss'; 8 | @import './waves.scss'; 9 | @import './dark.scss'; 10 | @import "./splitpanes.scss"; 11 | @import "./iconfont/iconfont.css"; 12 | @import "./zerorunner/case.scss"; -------------------------------------------------------------------------------- /frontend/src/theme/media/index.scss: -------------------------------------------------------------------------------- 1 | /* 栅格布局(媒体查询变量) 2 | * https://developer.mozilla.org/zh-CN/docs/Learn/CSS/CSS_layout/Media_queries 3 | * $us ≥376px 响应式栅格 4 | * $xs ≥576px 响应式栅格 5 | * $sm ≥768px 响应式栅格 6 | * $md ≥992px 响应式栅格 7 | * $lg ≥1200px 响应式栅格 8 | * $xl ≥1920px 响应式栅格 9 | ------------------------------- */ 10 | $us: 376px; 11 | $xs: 576px; 12 | $sm: 768px; 13 | $md: 992px; 14 | $lg: 1200px; 15 | $xl: 1920px; 16 | -------------------------------------------------------------------------------- /frontend/src/utils/unique.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据指定字段对对象数组进行去重 3 | * @param arr 要去重的对象数组 4 | * @param key 去重依据的字段名 5 | * @returns 去重后的对象数组 6 | */ 7 | function uniqueByField(arr: T[], key: keyof T): T[] { 8 | const seen = new Map(); 9 | return arr.filter((item) => { 10 | const value = item[key]; 11 | return seen.has(value) ? false : (seen.set(value, item), true); 12 | }); 13 | } 14 | 15 | export { uniqueByField }; 16 | -------------------------------------------------------------------------------- /frontend/src/stores/requestOldRoutes.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | /** 4 | * 后端返回原始路由(未处理时) 5 | * @methods setCacheKeepAlive 设置接口原始路由数据 6 | */ 7 | export const useRequestOldRoutes = defineStore('requestOldRoutes', { 8 | state: (): RequestOldRoutesState => ({ 9 | requestOldRoutes: [], 10 | }), 11 | actions: { 12 | async setRequestOldRoutes(routes: Array) { 13 | this.requestOldRoutes = routes; 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /backend/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import asyncio 4 | 5 | from loguru import logger 6 | from app.db.sqlalchemy import engine 7 | from app.models.base import Base 8 | 9 | 10 | async def init_db(): 11 | """ 12 | 初始化数据库 13 | :return: 14 | """ 15 | async with engine.begin() as conn: 16 | await conn.run_sync(Base.metadata.drop_all) 17 | await conn.run_sync(Base.metadata.create_all) 18 | -------------------------------------------------------------------------------- /backend/app/init/mount.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from fastapi import FastAPI 4 | from fastapi.staticfiles import StaticFiles 5 | 6 | from config import config 7 | 8 | 9 | def init_mount(app: FastAPI): 10 | """ 挂载静态文件 -- https://fastapi.tiangolo.com/zh/tutorial/static-files/ """ 11 | 12 | # 第一个参数为url路径参数, 第二参数为静态文件目录的路径, 第三个参数是FastAPI内部使用的名字 13 | app.mount(f"/{config.STATIC_DIR}", StaticFiles(directory=config.STATIC_DIR), name=config.STATIC_DIR) 14 | -------------------------------------------------------------------------------- /frontend/src/theme/media/home.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .home-media, 7 | .home-media-sm { 8 | margin-top: 15px; 9 | } 10 | } 11 | 12 | /* 页面宽度小于1200px 13 | ------------------------------- */ 14 | @media screen and (max-width: $lg) { 15 | .home-media-lg { 16 | margin-top: 15px; 17 | } 18 | .home-monitor { 19 | .flex-warp-item { 20 | width: 33.33% !important; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/topBar/settings/component/icons/index.ts: -------------------------------------------------------------------------------- 1 | import HeaderNav from './header-nav.vue'; 2 | 3 | export { default as ContentCompact } from './content-compact.vue'; 4 | export { default as FullContent } from './full-content.vue'; 5 | export { default as MixedNav } from './mixed-nav.vue'; 6 | export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue'; 7 | export { default as SidebarNav } from './sidebar-nav.vue'; 8 | 9 | const ContentWide = HeaderNav; 10 | export { ContentWide, HeaderNav }; 11 | -------------------------------------------------------------------------------- /backend/app/schemas/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from pydantic import BaseModel, field_validator 4 | 5 | 6 | class BaseSchema(BaseModel): 7 | def model_dump(self, *args, **kwargs): 8 | if "exclude_none" not in kwargs: 9 | kwargs["exclude_none"] = True 10 | return super(BaseSchema, self).model_dump(*args, **kwargs) 11 | 12 | @field_validator('*', mode="before") 13 | def blank_strings(cls, v): 14 | if v == "": 15 | return None 16 | return v 17 | -------------------------------------------------------------------------------- /backend/celery_worker/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import asyncio 4 | import typing 5 | 6 | 7 | def run_async(func: typing.Union[typing.Coroutine, typing.Awaitable]) -> typing.Any: 8 | """ 9 | 异步函数调用时使用 10 | :param func: 11 | :return: 12 | """ 13 | # 单线程 14 | try: 15 | loop = asyncio.get_event_loop() 16 | return loop.run_until_complete(func) 17 | except Exception as err: 18 | asyncio.set_event_loop(asyncio.new_event_loop()) 19 | return asyncio.run(func) 20 | -------------------------------------------------------------------------------- /frontend/src/utils/config.ts: -------------------------------------------------------------------------------- 1 | export function getEnv() { 2 | return import.meta.env.ENV 3 | } 4 | 5 | export function getApiBaseUrl() { 6 | return import.meta.env.VITE_API_BASE_URL + import.meta.env.VITE_API_PREFIX || "http://localhost:9100" + import.meta.env.VITE_API_PREFIX 7 | } 8 | 9 | export function getWebSocketUrl() { 10 | return ((window.location.protocol === 'https:') ? 'wss' : 'ws') + '://' + import.meta.env.VITE_WBE_SOCKET_URL 11 | } 12 | 13 | 14 | export function getBaseApiUrl() { 15 | return import.meta.env.VITE_API_BASE_URL 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/layout/component/header.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /backend/app/init/cors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from fastapi import FastAPI 4 | from starlette.middleware.cors import CORSMiddleware 5 | 6 | from config import config 7 | 8 | 9 | def init_cors(app: FastAPI): 10 | """ 跨域请求 -- https://fastapi.tiangolo.com/zh/tutorial/cors/ """ 11 | 12 | app.add_middleware( 13 | CORSMiddleware, 14 | allow_origins=[str(origin) for origin in config.CORS_ORIGINS], 15 | allow_credentials=True, 16 | allow_methods=["GET", "POST", "PUT", "DELETE"], 17 | allow_headers=["*"], 18 | ) 19 | -------------------------------------------------------------------------------- /backend/celery_worker/scheduler/literals.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from app.config import config 3 | 4 | DAYS = 'days' 5 | HOURS = 'hours' 6 | MINUTES = 'minutes' 7 | SECONDS = 'seconds' 8 | MICROSECONDS = 'microseconds' 9 | 10 | 11 | # This scheduler must wake up more frequently than the 12 | # regular of 5 minutes because it needs to take external 13 | # changes to the schedule into account. 14 | DEFAULT_MAX_INTERVAL = 5 # seconds 15 | 16 | DEFAULT_BEAT_DBURI = config.beat_dburi 17 | 18 | ADD_ENTRY_ERROR = """\ 19 | Cannot add entry %r to database schedule: %r. Contents: %r 20 | """ 21 | -------------------------------------------------------------------------------- /frontend/src/utils/lookup.ts: -------------------------------------------------------------------------------- 1 | import {useLookupStore} from "/@/stores/lookup"; 2 | import {storeToRefs} from "/@/stores"; 3 | /** 4 | * 返回数据字典 5 | * @param code 数据字典编码 6 | * @param lookup_code 数据字典值编码 7 | * @returns return 对应的字符串,否则返回原值 8 | */ 9 | export function formatLookup(code: any, lookup_code: string): string { 10 | const lookupStore = useLookupStore() 11 | const {lookupDict} = storeToRefs(lookupStore); 12 | 13 | let lookup = lookupDict.value[code] 14 | if (!lookup) return lookup_code; 15 | if (lookup[lookup_code]) return lookup[lookup_code]; 16 | return lookup_code; 17 | } -------------------------------------------------------------------------------- /frontend/src/api/login/index.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参) 5 | * 6 | * 登录api接口集合 7 | * @method signIn 用户登录 8 | * @method signOut 用户退出登录 9 | */ 10 | export function useLoginApi() { 11 | return { 12 | signIn: (data: object) => { 13 | return request({ 14 | url: '/user/signIn', 15 | method: 'post', 16 | data, 17 | }); 18 | }, 19 | signOut: (data: object) => { 20 | return request({ 21 | url: '/user/signOut', 22 | method: 'post', 23 | data, 24 | }); 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /backend/app/utils/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: 小白 3 | from contextvars import ContextVar 4 | from typing import Optional 5 | 6 | from fastapi.requests import Request 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | 9 | AccessToken: ContextVar[Optional[str]] = ContextVar('AccessToken', default=None) 10 | AppTraceId: ContextVar[Optional[str]] = ContextVar('AppTraceId', default=None) 11 | SQLAlchemySession: ContextVar[Optional[AsyncSession]] = ContextVar('SQLAlchemySession', default=None) 12 | 13 | FastApiRequest: ContextVar[Optional[Request]] = ContextVar('fastApiRequest', default=None) 14 | -------------------------------------------------------------------------------- /frontend/src/components/Z-Table/expand.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /backend/app/utils/current_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import typing 4 | 5 | from app.corelibs.consts import TEST_USER_INFO 6 | from app.db import redis_pool 7 | from app.exceptions.exceptions import AccessTokenFail 8 | from app.utils.context import AccessToken 9 | 10 | 11 | async def current_user(token: str = None) -> typing.Union[typing.Dict[typing.Text, typing.Any], None]: 12 | """根据token获取用户信息""" 13 | user_info = await redis_pool.redis.get(TEST_USER_INFO.format(AccessToken.get() if not token else token)) 14 | if not user_info: 15 | raise AccessTokenFail() 16 | return user_info 17 | -------------------------------------------------------------------------------- /frontend/src/utils/urlHandler.ts: -------------------------------------------------------------------------------- 1 | export function handlerRedirectUrl() { 2 | try { 3 | let localUrl = window.location.href.replace("/#", ''); 4 | const newLocalUrl = new URL(localUrl); 5 | let params: any = {} 6 | for (let p of newLocalUrl.searchParams) { 7 | params[p[0]] = p[1] 8 | } 9 | let redirectUrl = `/#/login?redirect=${newLocalUrl.pathname}`; 10 | if (Object.keys(params).length > 0) { 11 | redirectUrl += `¶ms=${JSON.stringify(params)}` 12 | } else { 13 | redirectUrl += `¶ms={}` 14 | } 15 | return redirectUrl 16 | } catch (error) { 17 | console.log(error) 18 | return null 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /frontend/src/layout/footer/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /frontend/src/theme/media/date.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 时间选择器适配 7 | .el-date-range-picker { 8 | width: 100vw; 9 | .el-picker-panel__body { 10 | min-width: 100%; 11 | .el-date-range-picker__content { 12 | .el-date-range-picker__header div { 13 | margin-left: 22px; 14 | margin-right: 0px; 15 | } 16 | & + .el-date-range-picker__content { 17 | .el-date-range-picker__header div { 18 | margin-left: 0px; 19 | margin-right: 22px; 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue'; 2 | import {authDirective} from '/@/directive/authDirective'; 3 | import {wavesDirective, dragDirective} from '/@/directive/customDirective'; 4 | import {clickOutside} from '/@/directive/clickOutside'; 5 | 6 | 7 | /** 8 | * 导出指令方法:v-xxx 9 | * @methods authDirective 用户权限指令,用法:v-auth 10 | * @methods wavesDirective 按钮波浪指令,用法:v-waves 11 | * @methods dragDirective 自定义拖动指令,用法:v-drag 12 | */ 13 | export function directive(app: App) { 14 | // 用户权限指令 15 | authDirective(app); 16 | // 按钮波浪指令 17 | wavesDirective(app); 18 | // 自定义拖动指令 19 | dragDirective(app); 20 | // 点击外部区域 21 | clickOutside(app) 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/theme/tableTool.scss: -------------------------------------------------------------------------------- 1 | .table-tool-popper { 2 | padding: 0 !important; 3 | .tool-box { 4 | display: flex; 5 | border-bottom: 1px solid var(--el-border-color-lighter); 6 | box-sizing: border-box; 7 | color: var(--el-text-color-primary); 8 | height: 40px; 9 | align-items: center; 10 | } 11 | .tool-sortable { 12 | max-height: 303px; 13 | .tool-sortable-item { 14 | display: flex; 15 | box-sizing: border-box; 16 | color: var(--el-text-color-primary); 17 | align-items: center; 18 | padding: 0 12px; 19 | &:hover { 20 | background: var(--el-fill-color-lighter); 21 | } 22 | i { 23 | opacity: 0.7; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | port=$2 4 | if [ "$port" = "" ]; then 5 | port=8101 6 | fi 7 | 8 | if [ $1 = "app" ]; then 9 | echo "start app" 10 | /usr/local/bin/python -m gunicorn main:app --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$port 11 | fi 12 | 13 | if [ $1 = "celery-worker" ]; then 14 | echo "start celery worker" 15 | /usr/local/bin/python -m celery -A celery_worker.worker.celery worker --pool=gevent -c 10 -l INFO 16 | fi 17 | 18 | if [ $1 = "celery-beat" ]; then 19 | echo "start celery beat" 20 | /usr/local/bin/python -m celery -A celery_worker.worker.celery beat -S celery_worker.scheduler.schedulers:DatabaseScheduler -l INFO 21 | fi -------------------------------------------------------------------------------- /backend/app/apis/api_router.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from fastapi import APIRouter 4 | 5 | from app.apis.system import user, menu, roles, lookup, id_center, file 6 | 7 | app_router = APIRouter() 8 | 9 | # system 10 | app_router.include_router(user.router, prefix="/user", tags=["user"]) 11 | app_router.include_router(menu.router, prefix="/menu", tags=["menu"]) 12 | app_router.include_router(roles.router, prefix="/roles", tags=["roles"]) 13 | app_router.include_router(lookup.router, prefix="/lookup", tags=["lookup"]) 14 | app_router.include_router(id_center.router, prefix="/idCenter", tags=["idCenter"]) 15 | app_router.include_router(file.router, prefix="/file", tags=["file"]) 16 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | fast-element-admin 17 | 18 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/stores/tagsViewRoutes.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {Session} from '/@/utils/storage'; 3 | 4 | /** 5 | * TagsView 路由列表 6 | * @methods setTagsViewRoutes 设置 TagsView 路由列表 7 | * @methods setCurrenFullscreen 设置开启/关闭全屏时的 boolean 状态 8 | */ 9 | export const useTagsViewRoutes = defineStore('tagsViewRoutes', { 10 | state: (): TagsViewRoutesState => ({ 11 | tagsViewRoutes: [], 12 | isTagsViewCurrenFull: false, 13 | }), 14 | actions: { 15 | async setTagsViewRoutes(data: Array) { 16 | this.tagsViewRoutes = data; 17 | }, 18 | setCurrenFullscreen(bool: Boolean) { 19 | Session.set('isTagsViewCurrenFull', bool); 20 | this.isTagsViewCurrenFull = bool; 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import { directive } from '/@/directive/index'; 5 | import other from '/@/utils/other'; 6 | 7 | import ElementPlus from 'element-plus'; 8 | import 'element-plus/dist/index.css'; 9 | import '/@/theme/index.scss'; 10 | import { initStores } from "/@/stores"; 11 | 12 | async function initApplication() { 13 | const app = createApp(App); 14 | 15 | const namespace = `${import.meta.env.VITE_APP_NAMESPACE}`; 16 | await initStores(app, { namespace }) 17 | 18 | directive(app); 19 | other.apiPublicAssembly(app) 20 | app.use(router) 21 | app.use(ElementPlus) 22 | app.mount('#app'); 23 | } 24 | 25 | initApplication() -------------------------------------------------------------------------------- /frontend/src/theme/splitpanes.scss: -------------------------------------------------------------------------------- 1 | @import "splitpanes/dist/splitpanes.css"; 2 | 3 | .splitpanes.default-theme .splitpanes__pane { 4 | background-color: #ffffff !important; 5 | background-color: var(--el-color-white) !important; 6 | } 7 | 8 | .splitpanes.default-theme .splitpanes__splitter { 9 | background-color: #ffffff !important; 10 | background-color: var(--el-color-white) !important; 11 | border-left: 1px solid var(--el-border-color)!important; 12 | } 13 | 14 | .default-theme.splitpanes--horizontal>.splitpanes__splitter, .default-theme .splitpanes--horizontal>.splitpanes__splitter { 15 | border-top: 1px solid #eee; 16 | border-top: 1px solid var(--el-border-color); 17 | } 18 | 19 | .splitpanes__pane .content { 20 | padding: 0 6px; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/theme/media/form.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-form-item__label { 7 | width: 100% !important; 8 | text-align: left !important; 9 | // 移动端 label 右对齐问题 10 | justify-content: flex-start !important; 11 | } 12 | .el-form-item__content { 13 | margin-left: 0 !important; 14 | } 15 | .el-form-item { 16 | // 响应式表单时,登录页需要重新处理 17 | display: unset !important; 18 | } 19 | // 表格演示中的表单筛选 20 | .table-form-btn { 21 | display: flex !important; 22 | .el-form-item__label { 23 | width: auto !important; 24 | } 25 | } 26 | // 表格演示中的表单筛选最大高度,适配移动端 27 | .table-search-container { 28 | max-height: 160px; 29 | overflow: auto; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/theme/iconSelector.scss: -------------------------------------------------------------------------------- 1 | /* Popover 弹出框(图标选择器) 2 | ------------------------------- */ 3 | .icon-selector-popper { 4 | padding: 0 !important; 5 | .icon-selector-warp { 6 | height: 260px; 7 | overflow: hidden; 8 | position: relative; 9 | .icon-selector-warp-title { 10 | position: absolute; 11 | height: 40px; 12 | line-height: 40px; 13 | left: 15px; 14 | } 15 | .el-tabs__header { 16 | display: flex; 17 | justify-content: flex-end; 18 | padding: 0 15px; 19 | border-bottom: 1px solid var(--el-border-color-light); 20 | margin: 0 !important; 21 | .el-tabs__nav-wrap { 22 | &::after { 23 | height: 0 !important; 24 | } 25 | .el-tabs__item { 26 | padding: 0 5px !important; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/roles.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useRolesApi() { 10 | return { 11 | getList: (data?: object) => { 12 | return request({ 13 | url: '/roles/list', 14 | method: 'POST', 15 | data, 16 | }); 17 | }, 18 | saveOrUpdate(data?: object) { 19 | return request({ 20 | url: '/roles/saveOrUpdate', 21 | method: 'POST', 22 | data 23 | }) 24 | }, 25 | deleted(data?: object) { 26 | return request({ 27 | url: '/roles/deleted', 28 | method: 'POST', 29 | data 30 | }) 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /backend/app/schemas/system/file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import uuid 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class FileIn(BaseModel): 9 | id: str = Field(default=str(uuid.uuid4()).replace("-", ""), description="文件id") 10 | name: str = Field(None, description="存储的文件名") 11 | file_path: str = Field(None, description="文件路径") 12 | extend_name: str = Field(None, description="文件后缀名") 13 | original_name: str = Field(None, description="文件原名称") 14 | content_type: str = Field(None, description="文件类型") 15 | file_size: str = Field(None, description="文件大小") 16 | 17 | 18 | class FileDown(BaseModel): 19 | path: str = Field(..., description="文件路径") 20 | 21 | 22 | class FileId(BaseModel): 23 | id: int = Field(..., description="文件id") 24 | -------------------------------------------------------------------------------- /backend/app/utils/create_dir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | from pathlib import Path 5 | 6 | 7 | def create_dir(file_name: str) -> Path: 8 | """ 创建文件夹 """ 9 | path = Path(file_name).absolute().parent / file_name # 拼接日志文件夹的路径 10 | if not Path(path).exists(): # 文件是否存在 11 | Path.mkdir(path) 12 | 13 | return path 14 | 15 | # import os 16 | 17 | 18 | # # 请不要随意移动该文件,创建文件夹是根据当前文件位置来创建 19 | # def create_dir(file_name: str) -> str: 20 | # """ 创建文件夹 """ 21 | # current_path = os.path.dirname(__file__) # 获取当前文件夹 22 | 23 | # base_path = os.path.abspath(os.path.join(current_path, "..")) # 获取当前文件夹的上一层文件 24 | 25 | # path = base_path + os.sep + file_name + os.sep # 拼接日志文件夹的路径 26 | 27 | # os.makedirs(path, exist_ok=True) # 如果文件夹不存在就创建 28 | 29 | # return path 30 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/file.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useFileApi() { 10 | return { 11 | upload: (data: object) => { 12 | return request({ 13 | url: '/file/upload', 14 | method: 'POST', 15 | headers: {"Content-Type": "multipart/form-data"}, 16 | data, 17 | }); 18 | }, 19 | download: (path: string) => { 20 | return request({ 21 | url: '/file/download/' + path, 22 | method: 'GET', 23 | }); 24 | }, 25 | deleted: (data: object) => { 26 | return request({ 27 | url: '/file/deleted', 28 | method: 'POST', 29 | data, 30 | }); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/index.ts: -------------------------------------------------------------------------------- 1 | import {createIconifyIcon} from "/@/icons/create-icon"; 2 | 3 | import './load'; 4 | 5 | const SvgAvatar1Icon = createIconifyIcon('svg:avatar-1'); 6 | const SvgAvatar2Icon = createIconifyIcon('svg:avatar-2'); 7 | const SvgAvatar3Icon = createIconifyIcon('svg:avatar-3'); 8 | const SvgAvatar4Icon = createIconifyIcon('svg:avatar-4'); 9 | const SvgDownloadIcon = createIconifyIcon('svg:download'); 10 | const SvgCardIcon = createIconifyIcon('svg:card'); 11 | const SvgBellIcon = createIconifyIcon('svg:bell'); 12 | const SvgCakeIcon = createIconifyIcon('svg:cake'); 13 | const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo'); 14 | 15 | export { 16 | SvgAntdvLogoIcon, 17 | SvgAvatar1Icon, 18 | SvgAvatar2Icon, 19 | SvgAvatar3Icon, 20 | SvgAvatar4Icon, 21 | SvgBellIcon, 22 | SvgCakeIcon, 23 | SvgCardIcon, 24 | SvgDownloadIcon, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/app/apis/system/roles.py: -------------------------------------------------------------------------------- 1 | from app.corelibs.custom_router import APIRouter 2 | from app.utils.response import HttpResponse 3 | from app.schemas.system.roles import RoleQuery, RoleIn, RoleDel 4 | from app.services.system.role import RolesService 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.post('/list', description="获取角色列表") 10 | async def all_roles(params: RoleQuery): 11 | data = await RolesService.list(params) 12 | return await HttpResponse.success(data) 13 | 14 | 15 | @router.post('/saveOrUpdate', description="新增或更新角色") 16 | async def save_or_update(params: RoleIn): 17 | data = await RolesService.save_or_update(params) 18 | return await HttpResponse.success(data) 19 | 20 | 21 | @router.post('/deleted', description="删除角色") 22 | async def deleted(params: RoleDel): 23 | data = await RolesService.deleted(params) 24 | return await HttpResponse.success(data) 25 | -------------------------------------------------------------------------------- /frontend/src/stores/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useUserStore } from "/@/stores/user"; 3 | import { useUserApi } from "/@/api/useSystemApi/user"; 4 | import { Session } from "/@/utils/storage"; 5 | import { initBackEndControlRoutes } from "/@/router/backEnd"; 6 | import { resetAllStores } from "/@/stores/setup"; 7 | 8 | export const useAuthStore = defineStore("auth", () => { 9 | const userStore = useUserStore(); 10 | 11 | 12 | async function Login(params: any) { 13 | const { data } = await useUserApi().signIn(params) 14 | const token = data?.token 15 | Session.set('token', token); 16 | await userStore.setUserInfos(); 17 | await initBackEndControlRoutes(); 18 | } 19 | 20 | async function Logout() { 21 | await useUserApi().logout(); 22 | Session.clear(); 23 | resetAllStores() 24 | } 25 | 26 | return { 27 | Login, 28 | Logout 29 | } 30 | 31 | }) -------------------------------------------------------------------------------- /frontend/src/stores/routesList.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | 3 | /** 4 | * 路由列表 5 | * @methods setRoutesList 设置路由数据 6 | * @methods setColumnsMenuHover 设置分栏布局菜单鼠标移入 boolean 7 | * @methods setColumnsNavHover 设置分栏布局最左侧导航鼠标移入 boolean 8 | */ 9 | export const useRoutesList = defineStore('routesList', { 10 | state: (): RoutesListState => ({ 11 | routesList: [], 12 | isGet: false, 13 | isColumnsMenuHover: false, 14 | isColumnsNavHover: false, 15 | }), 16 | actions: { 17 | async setRoutesList(data: Array) { 18 | this.routesList = data; 19 | }, 20 | async setColumnsMenuHover(bool: Boolean) { 21 | this.isColumnsMenuHover = bool; 22 | }, 23 | async setColumnsNavHover(bool: Boolean) { 24 | this.isColumnsNavHover = bool; 25 | }, 26 | async setIsGet(bool: Boolean) { 27 | this.isGet = bool; 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /frontend/src/api/menu/index.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 以下为模拟接口地址,gitee 的不通,就换自己的真实接口地址 5 | * 6 | * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参) 7 | * 8 | * 后端控制菜单模拟json,路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu 9 | * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由 10 | * @method getAdminMenu 获取后端动态路由菜单(admin) 11 | * @method getTestMenu 获取后端动态路由菜单(test) 12 | */ 13 | export function useMenuApi() { 14 | return { 15 | getAdminMenu: (params?: object) => { 16 | return request({ 17 | url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json', 18 | method: 'get', 19 | params, 20 | }); 21 | }, 22 | getTestMenu: (params?: object) => { 23 | return request({ 24 | url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json', 25 | method: 'get', 26 | params, 27 | }); 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/stores/lookup.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {useLookupApi} from "/@/api/useSystemApi/lookup" 3 | import {Session} from "/@/utils/storage"; 4 | 5 | /** 6 | * 数据字典 7 | * @methods setLookup 设置数据字典 8 | * @methods setColumnsMenuHover 设置分栏布局菜单鼠标移入 boolean 9 | * @methods setColumnsNavHover 设置分栏布局最左侧导航鼠标移入 boolean 10 | */ 11 | export const useLookupStore = defineStore('lookupDict', { 12 | state: (): LookUpState => ({ 13 | lookupDict: [] 14 | }), 15 | actions: { 16 | async setLookup() { 17 | if (Session.get('lookupDict')) { 18 | // pass 19 | } else { 20 | let res = await useLookupApi().getAllLookup() 21 | Session.set("lookupDict", res.data) 22 | } 23 | this.lookupDict = Session.get('lookupDict') 24 | }, 25 | getLookup() { 26 | if (Session.get('lookupDict')) { 27 | this.lookupDict = Session.get('lookupDict') 28 | } 29 | return this.lookupDict 30 | } 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apis/deps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | 5 | import typing 6 | 7 | from fastapi.security import OAuth2PasswordBearer 8 | from sqlalchemy.ext.asyncio import AsyncSession 9 | from starlette.requests import Request 10 | from config import config 11 | from app.db.redis import MyRedis 12 | from app.db.session import async_session 13 | 14 | get_token = OAuth2PasswordBearer(tokenUrl=f"{config.API_PREFIX}/login") 15 | 16 | 17 | async def get_db() -> typing.AsyncGenerator[AsyncSession, None]: 18 | """ sql连接会话 """ 19 | async with async_session() as session: 20 | try: 21 | yield session 22 | await session.commit() 23 | except Exception: 24 | await session.rollback() 25 | raise 26 | finally: 27 | await session.close() 28 | 29 | 30 | async def get_redis(request: Request) -> MyRedis: 31 | """ redis连接对象 """ 32 | return await request.app.state.redis 33 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/forking.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/theme/media/error.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .error { 7 | .error-flex { 8 | flex-direction: column-reverse !important; 9 | height: auto !important; 10 | width: 100% !important; 11 | } 12 | .right, 13 | .left { 14 | flex: unset !important; 15 | display: flex !important; 16 | } 17 | .left-item { 18 | margin: auto !important; 19 | } 20 | .right img { 21 | max-width: 450px !important; 22 | @extend .left-item; 23 | } 24 | } 25 | } 26 | 27 | /* 页面宽度大于768px小于992px 28 | ------------------------------- */ 29 | @media screen and (min-width: $sm) and (max-width: $md) { 30 | .error { 31 | .error-flex { 32 | padding-left: 30px !important; 33 | } 34 | } 35 | } 36 | 37 | /* 页面宽度小于1200px 38 | ------------------------------- */ 39 | @media screen and (max-width: $lg) { 40 | .error { 41 | .error-flex { 42 | padding: 0 30px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/app/apis/system/file.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, UploadFile, File 2 | 3 | from app.utils.response import HttpResponse 4 | from app.schemas.system.file import FileId 5 | from app.services.system.file import FileService 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.post('/upload', description="文件上传") 11 | async def upload(file: UploadFile = File(...)): 12 | result = await FileService.upload(file) 13 | return await HttpResponse.success(result) 14 | 15 | 16 | @router.get('/download/{file_id}', description="文件下载") 17 | async def download(file_id: str): 18 | result = await FileService.download(file_id) 19 | return result 20 | 21 | 22 | @router.get('/getFileById', description="根据id获取文件下载地址") 23 | async def get_file_by_id(params: FileId): 24 | return await FileService.get_file_by_id(params) 25 | 26 | 27 | @router.post('/deleted', description="文件删除") 28 | async def deleted(params: FileId): 29 | data = await FileService.get_file_by_id(params) 30 | return await HttpResponse.success(data) 31 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | #### 🌈 介绍 2 | 3 | 基于 vite + vue3 + element-plus 4 | 5 | - 使用软件版本 6 | - node version 18.15.0 7 | - vue version 3.2.45 8 | - element-plus version 2.2.26 9 | 10 | #### 💒 平台地址地址 11 | - github 12 | https://github.com/baizunxian/fast-element-admin 13 | - gitee 14 | 15 | #### 🚧 安装 cnpm、yarn 16 | 17 | ```bash 18 | # node 版本 19 | node -v 20 | v18.15.0 21 | ``` 22 | 23 | - 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org` 24 | - 复制代码(桌面 cmd 运行) `npm install -g yarn` 25 | 26 | ```bash 27 | # 克隆项目 28 | git clone https://github.com/baizunxian/fast-element-admin.git 29 | 30 | # 进入项目 31 | cd fast-element-admin/frontend 32 | 33 | # 安装依赖 34 | cnpm install 35 | # 或者 36 | yarn insatll 37 | 38 | # 运行项目 39 | cnpm run dev 40 | # 或者 41 | yarn dev 42 | 43 | # 打包发布 44 | cnpm run build 45 | # 或者 46 | yarn build 47 | ``` 48 | 49 | #### 💌 支持作者 50 | 51 | 如果觉得框架不错,或者已经在使用了,希望你可以去 Github 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持, 平台会持续迭代更新。 52 | -------------------------------------------------------------------------------- /frontend/src/components/ZeroCard/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/stores/menu.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {Session} from '/@/utils/storage'; 3 | import {useUserApi} from "/@/api/useSystemApi/user"; 4 | 5 | /** 6 | * 用户信息 7 | * @methods 设置菜单信息 8 | */ 9 | export const useMenuInfo = defineStore('useMenuInfo', { 10 | state: (): MenuDataState => ({ 11 | menuData: [], 12 | }), 13 | actions: { 14 | 15 | async setUserInfos() { 16 | if (Session.get('menuData')) { 17 | this.menuData = Session.get('menuData'); 18 | } else { 19 | this.menuData = await this.getMenuData(); 20 | } 21 | }, 22 | 23 | async getMenuData() { 24 | let data 25 | if (Session.get('menuData')) { 26 | data = Session.get('menuData'); 27 | } else { 28 | let res = await useUserApi().getMenuByToken() 29 | this.menuData = data = res.data 30 | 31 | Session.set("menuData", this.menuData) 32 | } 33 | return data 34 | }, 35 | 36 | getMenuList() { 37 | if (Session.get('menuData')) { 38 | return Session.get('menuData'); 39 | } 40 | } 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/menu.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 菜单接口 5 | * @method getAllMenus 获取菜单接口,路由格式 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useMenuApi() { 10 | return { 11 | // 获取所有菜单,嵌套 12 | getAllMenus: () => { 13 | return request({ 14 | url: '/menu/getAllMenus', 15 | method: 'POST', 16 | data: {}, 17 | }); 18 | }, 19 | //后去所有菜单,平铺 20 | allMenu: (data?: object) => { 21 | return request({ 22 | url: '/menu/allMenu', 23 | method: 'POST', 24 | data, 25 | }); 26 | }, 27 | // 新增修改 28 | saveOrUpdate(data?: object) { 29 | return request({ 30 | url: '/menu/saveOrUpdate', 31 | method: 'POST', 32 | data 33 | }) 34 | }, 35 | // 删除 36 | deleted(data?: object) { 37 | return request({ 38 | url: '/menu/deleted', 39 | method: 'POST', 40 | data 41 | }) 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/topBar/settings/component/icons/setting.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/gold_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/utils/authFunction.ts: -------------------------------------------------------------------------------- 1 | import { useUserInfo } from '/@/stores/userInfo'; 2 | import { judementSameArr } from '/@/utils/arrayOperation'; 3 | 4 | /** 5 | * 单个权限验证 6 | * @param value 权限值 7 | * @returns 有权限,返回 `true`,反之则反 8 | */ 9 | export function auth(value: string): boolean { 10 | const stores = useUserInfo(); 11 | return stores.userInfos.authBtnList.some((v: string) => v === value); 12 | } 13 | 14 | /** 15 | * 多个权限验证,满足一个则为 true 16 | * @param value 权限值 17 | * @returns 有权限,返回 `true`,反之则反 18 | */ 19 | export function auths(value: Array): boolean { 20 | let flag = false; 21 | const stores = useUserInfo(); 22 | stores.userInfos.authBtnList.map((val: string) => { 23 | value.map((v: string) => { 24 | if (val === v) flag = true; 25 | }); 26 | }); 27 | return flag; 28 | } 29 | 30 | /** 31 | * 多个权限验证,全部满足则为 true 32 | * @param value 权限值 33 | * @returns 有权限,返回 `true`,反之则反 34 | */ 35 | export function authAll(value: Array): boolean { 36 | const stores = useUserInfo(); 37 | return judementSameArr(value, stores.userInfos.authBtnList); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/icons/iconify/index.ts: -------------------------------------------------------------------------------- 1 | import {createIconifyIcon} from "/@/icons/create-icon"; 2 | 3 | 4 | export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc'); 5 | 6 | export const MdiWechat = createIconifyIcon('mdi:wechat'); 7 | 8 | export const MdiGithub = createIconifyIcon('mdi:github'); 9 | 10 | export const MdiGoogle = createIconifyIcon('mdi:google'); 11 | 12 | export const MdiQqchat = createIconifyIcon('mdi:qqchat'); 13 | export const VsCodePython = createIconifyIcon('vscode-icons:file-type-python'); 14 | export const VsCodeJs = createIconifyIcon('vscode-icons:file-type-js-official'); 15 | export const PsRightC = createIconifyIcon('icon-park-solid:right-c'); 16 | export const BxsRightArrow = createIconifyIcon('bxs:right-arrow'); 17 | export const BxsLeftArrow = createIconifyIcon('bxs:left-arrow'); 18 | export const flowBiteClose = createIconifyIcon('flowbite:close-circle-solid'); 19 | export const BxHome = createIconifyIcon('bx:home-heart'); 20 | export const SolarStop = createIconifyIcon('fluent:stop-16-filled'); 21 | export const AIFill = createIconifyIcon('mingcute:ai-fill'); 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/mysql_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/silver_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {Session} from '/@/utils/storage'; 3 | import {useUserApi} from "/@/api/useSystemApi/user"; 4 | 5 | /** 6 | * 用户信息 7 | * @methods setUserInfos 设置用户信息 8 | */ 9 | export const useUserStore = defineStore('userInfo', { 10 | state: (): UserInfosState => ({ 11 | userInfos: { 12 | id: null, 13 | authBtnList: [], 14 | avatar: '', 15 | roles: [], 16 | time: 0, 17 | username: '', 18 | nickname: '', 19 | user_type: null, 20 | login_time: "", 21 | lastLoginTime: "" 22 | }, 23 | }), 24 | 25 | actions: { 26 | async setUserInfos() { 27 | if (Session.get('userInfo')) { 28 | this.userInfos = Session.get('userInfo'); 29 | } else { 30 | this.userInfos = await this.getApiUserInfo(); 31 | Session.set("userInfo", this.userInfos) 32 | } 33 | }, 34 | async getApiUserInfo() { 35 | let {data} = await useUserApi().getUserInfoByToken() 36 | return data 37 | }, 38 | async updateUserInfo(data: UserInfos) { 39 | this.userInfos = data 40 | Session.set("userInfo", data) 41 | } 42 | } 43 | }); -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | .idea/ 7 | # C extensions 8 | *.so 9 | .env 10 | 11 | # Distribution / packaging 12 | .Python 13 | .pytest_cache/ 14 | venv/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | *.pytest_cache/ 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python db_script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | *.html 58 | *.pyc 59 | *.xml 60 | *.iml 61 | 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/bronze_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lyt-Top 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. -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多多少个字符 3 | printWidth: 150, 4 | // 指定每个缩进级别的空格数 5 | tabWidth: 2, 6 | // 使用制表符而不是空格缩进行 7 | useTabs: true, 8 | // 在语句末尾打印分号 9 | semi: true, 10 | // 使用单引号而不是双引号 11 | singleQuote: true, 12 | // 更改引用对象属性的时间 可选值"" 13 | quoteProps: 'as-needed', 14 | // 在JSX中使用单引号而不是双引号 15 | jsxSingleQuote: false, 16 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 17 | trailingComma: 'es5', 18 | // 在对象文字中的括号之间打印空格 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x 23 | arrowParens: 'always', 24 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 指定要使用的解析器,不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 always\never\preserve 32 | proseWrap: 'preserve', 33 | // 指定HTML文件的全局空格敏感度 css\strict\ignore 34 | htmlWhitespaceSensitivity: 'css', 35 | // Vue文件脚本和样式标签缩进 36 | vueIndentScriptAndStyle: false, 37 | // 换行符使用 lf 结尾是 可选值"" 38 | endOfLine: 'lf', 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/src/stores/setup.ts: -------------------------------------------------------------------------------- 1 | import type { Pinia } from 'pinia'; 2 | 3 | import type { App } from 'vue'; 4 | 5 | import { createPinia } from 'pinia'; 6 | 7 | let pinia: Pinia; 8 | 9 | export interface InitStoreOptions { 10 | /** 11 | * @zh_CN 应用名,由于 stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名,应用名将被用于持久化的前缀 12 | */ 13 | namespace: string; 14 | } 15 | 16 | /** 17 | * @zh_CN 初始化pinia 18 | */ 19 | export async function initStores(app: App, options: InitStoreOptions) { 20 | const { createPersistedState } = await import('pinia-plugin-persistedstate'); 21 | pinia = createPinia(); 22 | const { namespace } = options; 23 | pinia.use( 24 | createPersistedState({ 25 | // key $appName-$store.id 26 | key: (storeKey) => `${namespace}-${storeKey}`, 27 | storage: localStorage, 28 | }), 29 | ); 30 | app.use(pinia); 31 | return pinia; 32 | } 33 | 34 | export function resetAllStores() { 35 | if (!pinia) { 36 | console.error('Pinia is not installed'); 37 | return; 38 | } 39 | const allStores = (pinia as any)._s; 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 42 | for (const [_key, store] of allStores) { 43 | store.$reset(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/case_svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/corelibs/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from contextvars import ContextVar 4 | import typing 5 | 6 | 7 | class Local: 8 | __slots__ = ("_storage",) 9 | 10 | def __init__(self) -> None: 11 | object.__setattr__(self, "_storage", ContextVar("local_storage")) 12 | 13 | def __iter__(self) -> typing.Iterator[typing.Tuple[int, typing.Any]]: 14 | return iter(self._storage.get({}).items()) 15 | 16 | def __release_local__(self) -> None: 17 | self._storage.set({}) 18 | 19 | def __getattr__(self, name: str) -> typing.Any: 20 | values = self._storage.get({}) 21 | try: 22 | return values[name] 23 | except KeyError: 24 | return None 25 | 26 | def __setattr__(self, name: str, value: typing.Any) -> None: 27 | values = self._storage.get({}).copy() 28 | values[name] = value 29 | self._storage.set(values) 30 | 31 | def __delattr__(self, name: str) -> None: 32 | values = self._storage.get({}).copy() 33 | try: 34 | del values[name] 35 | self._storage.set(values) 36 | except KeyError: 37 | ... 38 | 39 | 40 | g = Local() 41 | -------------------------------------------------------------------------------- /frontend/src/icons/lucide/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ArrowDown, 3 | ArrowLeft, 4 | ArrowLeftFromLine as MdiMenuOpen, 5 | ArrowLeftToLine, 6 | ArrowRightFromLine as MdiMenuClose, 7 | ArrowRightLeft, 8 | ArrowRight, 9 | ArrowRightToLine, 10 | ArrowUp, 11 | ArrowUpToLine, 12 | Bell, 13 | BookOpenText, 14 | ChevronDown, 15 | ChevronLeft, 16 | ChevronRight, 17 | ChevronsLeft, 18 | ChevronsRight, 19 | CircleHelp, 20 | Copy, 21 | CornerDownLeft, 22 | Ellipsis, 23 | Expand, 24 | ExternalLink, 25 | Eye, 26 | EyeOff, 27 | FoldHorizontal, 28 | Fullscreen, 29 | Github, 30 | Info, 31 | InspectionPanel, 32 | Languages, 33 | LoaderCircle, 34 | LockKeyhole, 35 | LogOut, 36 | MailCheck, 37 | Maximize, 38 | Menu as IconDefault, 39 | Menu, 40 | Minimize, 41 | Minimize2, 42 | MoonStar, 43 | Palette, 44 | PanelLeft, 45 | PanelRight, 46 | Pin, 47 | PinOff, 48 | RotateCw, 49 | Search, 50 | SearchX, 51 | Settings, 52 | Shrink, 53 | Sun, 54 | SunMoon, 55 | SwatchBook, 56 | UserRoundPen, 57 | MousePointerClick, 58 | MoveUp, 59 | X, 60 | Trash2, 61 | Link, 62 | Unlink, 63 | CircleArrowLeft, 64 | ArrowDownToDot, 65 | } from 'lucide-vue-next'; 66 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/exc_count.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/theme/common/default.scss: -------------------------------------------------------------------------------- 1 | .ui-badge-circle { 2 | font-size: 12px; 3 | display: inline-flex; 4 | margin-left: 4px; 5 | justify-content: center; 6 | align-items: center; 7 | max-width: 16px; 8 | max-height: 16px; 9 | border-radius: 100px; 10 | padding: 0 4px; 11 | color: var(--el-bg-color); 12 | background-color: var(--el-color-primary); 13 | } 14 | 15 | .ui-badge-status-dot { 16 | position: relative; 17 | top: -1px; 18 | display: inline-block; 19 | width: 6px; 20 | height: 6px; 21 | margin-left: 5px; 22 | vertical-align: middle; 23 | border-radius: 50%; 24 | background: var(--el-color-primary); 25 | } 26 | 27 | .block-title { 28 | position: relative; 29 | padding-left: 11px; 30 | font-size: 14px; 31 | font-weight: 600; 32 | height: 24px; 33 | line-height: 24px; 34 | background: #f7f7fc; 35 | color: #333333; 36 | border-left: 2px solid #409eff; 37 | margin-bottom: 5px; 38 | display: flex; 39 | justify-content: space-between; 40 | } 41 | 42 | .default-card-info { 43 | padding: 15px 16px; 44 | background-color: #ffffff; 45 | border-radius: 10px; 46 | border-left: 5px solid #409eff; 47 | margin-bottom: 20px; 48 | box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); 49 | } -------------------------------------------------------------------------------- /backend/app/schemas/system/roles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import typing 4 | 5 | from pydantic import BaseModel, Field, model_validator 6 | 7 | from app.schemas.base import BaseSchema 8 | 9 | 10 | class RoleIn(BaseModel): 11 | id: int = Field(None, description="角色id") 12 | name: str = Field(..., description="角色名称") 13 | role_type: int = Field(default=10, description="角色类型") 14 | menus: str = Field(..., description="菜单列表") 15 | description: typing.Optional[str] = Field(None, description="描述") 16 | status: typing.Optional[int] = Field(default=10, description="状态 10 启用 20 禁用") 17 | 18 | @model_validator(mode="before") 19 | def root_validator(cls, data: typing.Dict[typing.Text, typing.Any]): 20 | menus = data.get("menus", []) 21 | if menus: 22 | data["menus"] = ','.join(list(map(str, menus))) 23 | return data 24 | 25 | 26 | class RoleQuery(BaseSchema): 27 | id: typing.Optional[int] = Field(None, description="角色id") 28 | name: typing.Optional[str] = Field(None, description="角色名称") 29 | role_type: typing.Optional[str] = Field(10, description="角色类型") 30 | 31 | 32 | class RoleDel(BaseModel): 33 | id: int = Field(..., description="角色id") 34 | -------------------------------------------------------------------------------- /frontend/src/utils/setIconfont.ts: -------------------------------------------------------------------------------- 1 | // 字体图标 url 2 | const cssCdnUrlList: Array = [ 3 | // '//at.alicdn.com/t/c/font_2298093_rnp72ifj3ba.css', 4 | // '//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 5 | ]; 6 | // 第三方 js url 7 | const jsCdnUrlList: Array = []; 8 | 9 | // 动态批量设置字体图标 10 | export function setCssCdn() { 11 | if (cssCdnUrlList.length <= 0) return false; 12 | cssCdnUrlList.map((v) => { 13 | let link = document.createElement('link'); 14 | link.rel = 'stylesheet'; 15 | link.href = v; 16 | link.crossOrigin = 'anonymous'; 17 | document.getElementsByTagName('head')[0].appendChild(link); 18 | }); 19 | } 20 | 21 | // 动态批量设置第三方js 22 | export function setJsCdn() { 23 | if (jsCdnUrlList.length <= 0) return false; 24 | jsCdnUrlList.map((v) => { 25 | let link = document.createElement('script'); 26 | link.src = v; 27 | document.body.appendChild(link); 28 | }); 29 | } 30 | 31 | /** 32 | * 批量设置字体图标、动态js 33 | * @method cssCdn 动态批量设置字体图标 34 | * @method jsCdn 动态批量设置第三方js 35 | */ 36 | const setIntroduction = { 37 | // 设置css 38 | cssCdn: () => { 39 | setCssCdn(); 40 | }, 41 | // 设置js 42 | jsCdn: () => { 43 | setJsCdn(); 44 | }, 45 | }; 46 | 47 | // 导出函数方法 48 | export default setIntroduction; 49 | -------------------------------------------------------------------------------- /frontend/src/theme/media/layout.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | // MessageBox 弹框 7 | .el-message-box { 8 | width: 80% !important; 9 | } 10 | } 11 | 12 | /* 页面宽度小于768px 13 | ------------------------------- */ 14 | @media screen and (max-width: $sm) { 15 | // Breadcrumb 面包屑 16 | .layout-navbars-breadcrumb-hide { 17 | display: none; 18 | } 19 | // 外链视图 20 | .layout-view-link { 21 | a { 22 | max-width: 80%; 23 | text-align: center; 24 | } 25 | } 26 | // 菜单搜索 27 | .layout-search-dialog { 28 | .el-autocomplete { 29 | width: 80% !important; 30 | } 31 | } 32 | } 33 | 34 | /* 页面宽度小于1000px 35 | ------------------------------- */ 36 | @media screen and (max-width: 1000px) { 37 | // 布局配置 38 | .layout-drawer-content-flex { 39 | position: relative; 40 | &::after { 41 | content: '手机版不支持切换布局'; 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | bottom: 0; 46 | left: 0; 47 | z-index: 1; 48 | text-align: center; 49 | height: 140px; 50 | line-height: 140px; 51 | background: rgba(255, 255, 255, 0.9); 52 | color: #666666; 53 | } 54 | } 55 | // pagination 分页中的工具栏 56 | .table-footer-tool { 57 | display: none !important; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/UI.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /backend/app/corelibs/consts.py: -------------------------------------------------------------------------------- 1 | # 公共 2 | DEFAULT_PAGE = 1 3 | DEFAULT_PER_PAGE = 10 4 | DEFAULT_FAIL = -1 5 | 6 | # Cache Time 7 | CACHE_FIVE_SECONDS = 5 8 | CACHE_MINUTE = 60 9 | CACHE_THREE_MINUTE = 60 * 3 10 | CACHE_FIVE_MINUTE = 60 * 5 11 | CACHE_TEN_MINUTE = 60 * 10 12 | CACHE_HALF_HOUR = 60 * 30 13 | CACHE_HOUR = 60 * 60 14 | CACHE_THREE_HOUR = 60 * 60 * 3 15 | CACHE_TWELVE_HOUR = 60 * 60 * 12 16 | CACHE_DAY = 60 * 60 * 24 17 | CACHE_WEEK = 60 * 60 * 24 * 7 18 | CACHE_MONTH = 60 * 60 * 24 * 30 19 | 20 | # Cache 21 | TEST_USER_INFO = 'zero:user_token:{0}' # 用户token缓存 22 | TEST_EXECUTE_SET = 'zero:test_execute_set:case:{}' # 用例执行集合 23 | TEST_EXECUTE_STATS = 'zero:test_execute_set:stats:{}' # 用例执行统计 24 | TEST_EXECUTE_TASK = 'zero:test_execute_set:task:{}' # 运行任务数 25 | TEST_EXECUTE_PARAMETER = 'zero:test_execute_set:extract_parameter:{}' # 变量 26 | DATA_STRUCTURE_CASE_UPDATE = 'zero:data_structure:user:{}' # 数据构造用户变更的接口信息 27 | TEST_USER_LOGIN_TIME = 'zero:user_login_time:{}' # 数据构造用户变更的接口信息 28 | 29 | # 性能 30 | PREFORMANCE_RUN_STATUS = 'performance_test:status' 31 | PREFORMANCE_FREE = 0 32 | PREFORMANCE_INIT = 10 33 | PREFORMANCE_BUSY = 20 34 | PREFORMANCE_ABORT = 30 35 | 36 | PREFORMANCE_CODE = 'code' 37 | PREFORMANCE_SIGN_CODE = 'sign_code' 38 | 39 | THREAD_MAXMUM = 100 40 | RUN_NUMBER_MAXMUM = 1000000 41 | DEBUG_MAXMUM = 100 42 | -------------------------------------------------------------------------------- /frontend/src/theme/mixins/index.scss: -------------------------------------------------------------------------------- 1 | /* 第三方图标字体间距/大小设置 2 | ------------------------------- */ 3 | @mixin generalIcon { 4 | font-size: 14px !important; 5 | display: inline-block; 6 | vertical-align: middle; 7 | margin-right: 5px; 8 | width: 24px; 9 | text-align: center; 10 | justify-content: center; 11 | } 12 | 13 | /* 文本不换行 14 | ------------------------------- */ 15 | @mixin text-no-wrap() { 16 | text-overflow: ellipsis; 17 | overflow: hidden; 18 | white-space: nowrap; 19 | } 20 | 21 | /* 多行文本溢出 22 | ------------------------------- */ 23 | @mixin text-ellipsis($line: 2) { 24 | overflow: hidden; 25 | word-break: break-all; 26 | text-overflow: ellipsis; 27 | display: -webkit-box; 28 | -webkit-line-clamp: $line; 29 | -webkit-box-orient: vertical; 30 | } 31 | 32 | /* 滚动条(页面未使用) div 中使用: 33 | ------------------------------- */ 34 | // .test { 35 | // @include scrollBar; 36 | // } 37 | @mixin scrollBar { 38 | // 滚动条凹槽的颜色,还可以设置边框属性 39 | &::-webkit-scrollbar-track-piece { 40 | background-color: #f8f8f8; 41 | } 42 | // 滚动条的宽度 43 | &::-webkit-scrollbar { 44 | width: 9px; 45 | height: 9px; 46 | } 47 | // 滚动条的设置 48 | &::-webkit-scrollbar-thumb { 49 | background-color: #dddddd; 50 | background-clip: padding-box; 51 | min-height: 28px; 52 | } 53 | &::-webkit-scrollbar-thumb:hover { 54 | background-color: #bbb; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/app/apis/system/menu.py: -------------------------------------------------------------------------------- 1 | from app.corelibs.custom_router import APIRouter 2 | from app.utils.response import HttpResponse 3 | from app.schemas.system.menu import MenuIn, MenuDel, MenuViews 4 | from app.services.system.menu import MenuService 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.post('/allMenu', description="获取所有菜单数据") 10 | async def all_menu(): 11 | data = await MenuService.all_menu() 12 | return await HttpResponse.success(data) 13 | 14 | 15 | @router.post('/getAllMenus', description="获取菜单嵌套结构") 16 | async def get_all_menus(): 17 | data = await MenuService.all_menu_nesting() 18 | return await HttpResponse.success(data) 19 | 20 | 21 | @router.post('/saveOrUpdate', description="新增或者更新menu") 22 | async def save_or_update(params: MenuIn): 23 | # return await HttpResponse.success(code=codes.PARTNER_CODE_FAIL, msg="演示环境不保存!") 24 | await MenuService.save_or_update(params) 25 | return await HttpResponse.success() 26 | 27 | 28 | @router.post('/deleted', description="删除菜单") 29 | async def delete_menu(params: MenuDel): 30 | data = await MenuService.deleted(params) 31 | return await HttpResponse.success(data) 32 | 33 | 34 | @router.post('/setMenuViews', description="设置菜单访问量") 35 | async def set_menu_views(params: MenuViews): 36 | await MenuService.set_menu_views(params) 37 | return await HttpResponse.success() 38 | -------------------------------------------------------------------------------- /frontend/src/directive/authDirective.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { useUserStore } from '/@/stores/user'; 3 | import { judementSameArr } from '/@/utils/arrayOperation'; 4 | 5 | /** 6 | * 用户权限指令 7 | * @directive 单个权限验证(v-auth="xxx") 8 | * @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 9 | * @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 10 | */ 11 | export function authDirective(app: App) { 12 | // 单个权限验证(v-auth="xxx") 13 | app.directive('auth', { 14 | mounted(el, binding) { 15 | const stores = useUserStore(); 16 | if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el); 17 | }, 18 | }); 19 | // 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 20 | app.directive('auths', { 21 | mounted(el, binding) { 22 | let flag = false; 23 | const stores = useUserStore(); 24 | stores.userInfos.authBtnList.map((val: string) => { 25 | binding.value.map((v: string) => { 26 | if (val === v) flag = true; 27 | }); 28 | }); 29 | if (!flag) el.parentNode.removeChild(el); 30 | }, 31 | }); 32 | // 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 33 | app.directive('auth-all', { 34 | mounted(el, binding) { 35 | const stores = useUserStore(); 36 | const flag = judementSameArr(binding.value, stores.userInfos.authBtnList); 37 | if (!flag) el.parentNode.removeChild(el); 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/types/layout.d.ts: -------------------------------------------------------------------------------- 1 | // aside 2 | declare type AsideState = { 3 | menuList: RouteRecordRaw[]; 4 | clientWidth: number; 5 | }; 6 | 7 | // columnsAside 8 | declare type ColumnsAsideState = { 9 | columnsAsideList: T[]; 10 | liIndex: number; 11 | liOldIndex: null | number; 12 | liHoverIndex: null | number; 13 | liOldPath: null | string; 14 | difference: number; 15 | routeSplit: string[]; 16 | }; 17 | 18 | // navBars breadcrumb 19 | declare type BreadcrumbState = { 20 | breadcrumbList: T[]; 21 | routeSplit: string[]; 22 | routeSplitFirst: string; 23 | routeSplitIndex: number; 24 | }; 25 | 26 | // navBars search 27 | declare type SearchState = { 28 | isShowSearch: boolean; 29 | menuQuery: string; 30 | tagsViewList: T[]; 31 | }; 32 | 33 | // navBars tagsView 34 | declare type TagsViewState = { 35 | routeActive: string | T; 36 | routePath: string | unknown; 37 | dropdown: { 38 | x: string | number; 39 | y: string | number; 40 | }; 41 | sortable: T; 42 | tagsRefsIndex: number; 43 | tagsViewList: T[]; 44 | tagsViewRoutesList: T[]; 45 | }; 46 | 47 | // navBars parent 48 | declare type ParentViewState = { 49 | refreshRouterViewKey: string; 50 | iframeRefreshKey: string; 51 | keepAliveNameList: string[]; 52 | iframeList: T[]; 53 | }; 54 | 55 | // navBars link 56 | declare type LinkViewState = { 57 | title: string; 58 | isLink: string; 59 | }; 60 | -------------------------------------------------------------------------------- /frontend/src/theme/common/test-case-default.scss: -------------------------------------------------------------------------------- 1 | .method-color-get { 2 | color: #61affe 3 | } 4 | 5 | .method-color-post { 6 | color: #49cc90 7 | } 8 | 9 | .method-color-delete { 10 | color: #f93e3d 11 | } 12 | 13 | .method-color-put { 14 | color: #fca130 15 | } 16 | 17 | .method-color-na { 18 | color: #f56c6c 19 | } 20 | 21 | 22 | @mixin priority-icon { 23 | flex-grow: 0; 24 | box-sizing: border-box; 25 | width: 10px; 26 | height: 10px; 27 | margin-right: 4px; 28 | border-radius: 50%; 29 | } 30 | 31 | .priority-0 { 32 | @include priority-icon; 33 | background: #fde9e7; 34 | border: 2px solid #f8a59f; 35 | } 36 | 37 | .priority-1 { 38 | @include priority-icon; 39 | background: #fdede4; 40 | border: 2px solid #f7b794; 41 | } 42 | 43 | .priority-2 { 44 | @include priority-icon; 45 | background: #e6f2fe; 46 | border: 2px solid #9bcafd; 47 | } 48 | 49 | .priority-3 { 50 | @include priority-icon; 51 | background: #f9fafb; 52 | border: 2px solid #d0d5dd; 53 | } 54 | 55 | .priority-4 { 56 | @include priority-icon; 57 | background: #fde9e7; 58 | border: 2px solid #f8a59f; 59 | } 60 | 61 | .priority-5 { 62 | @include priority-icon; 63 | background: #fde9e7; 64 | border: 2px solid #f8a59f; 65 | } 66 | 67 | .case-status-dot { 68 | position: relative; 69 | top: -1px; 70 | display: inline-block; 71 | width: 6px; 72 | height: 6px; 73 | vertical-align: middle; 74 | border-radius: 50%; 75 | } -------------------------------------------------------------------------------- /frontend/src/utils/loading.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import '/@/theme/loading.scss'; 3 | 4 | /** 5 | * 页面全局 Loading 6 | * @method start 创建 loading 7 | * @method done 移除 loading 8 | */ 9 | export const NextLoading = { 10 | // 创建 loading 11 | start: () => { 12 | const bodys: Element = document.body; 13 | const div = document.createElement('div'); 14 | div.setAttribute('class', 'loading-next'); 15 | const htmls = ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | `; 30 | div.innerHTML = htmls; 31 | bodys.insertBefore(div, bodys.childNodes[0]); 32 | window.nextLoading = true; 33 | }, 34 | // 移除 loading 35 | done: (time: number = 0) => { 36 | nextTick(() => { 37 | setTimeout(() => { 38 | window.nextLoading = false; 39 | const el = document.querySelector('.loading-next'); 40 | el?.parentNode?.removeChild(el); 41 | }, time); 42 | }); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | 26 | # ---> Python 27 | # Byte-compiled / optimized / DLL files 28 | __pycache__/ 29 | *.py[cod] 30 | *$py.class 31 | .idea/ 32 | # C extensions 33 | *.so 34 | *.env 35 | 36 | # Distribution / packaging 37 | .Python 38 | .pytest_cache/ 39 | venv/ 40 | build/ 41 | develop-eggs/ 42 | dist/ 43 | downloads/ 44 | eggs/ 45 | .eggs/ 46 | lib/ 47 | lib64/ 48 | parts/ 49 | sdist/ 50 | var/ 51 | *.egg-info/ 52 | .installed.cfg 53 | *.egg 54 | *.pytest_cache/ 55 | 56 | # PyInstaller 57 | # Usually these files are written by a python script from a template 58 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 59 | *.manifest 60 | *.spec 61 | 62 | # Installer logs 63 | pip-log.txt 64 | pip-delete-this-directory.txt 65 | 66 | # Unit test / coverage reports 67 | htmlcov/ 68 | .tox/ 69 | .coverage 70 | .coverage.* 71 | .cache 72 | nosetests.xml 73 | coverage.xml 74 | *,cover 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | *.html 83 | *.pyc 84 | *.xml 85 | *.iml 86 | 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | -------------------------------------------------------------------------------- /frontend/src/theme/other.scss: -------------------------------------------------------------------------------- 1 | /* wangeditor 富文本编辑器 2 | ------------------------------- */ 3 | .editor-container { 4 | z-index: 10; // 用于 wangeditor 点击全屏时 5 | .w-e-toolbar { 6 | border: 1px solid var(--el-border-color-light, #ebeef5) !important; 7 | border-bottom: 1px solid var(--el-border-color-light, #ebeef5) !important; 8 | border-top-left-radius: 3px; 9 | border-top-right-radius: 3px; 10 | z-index: 2 !important; 11 | } 12 | .w-e-text-container { 13 | border: 1px solid var(--el-border-color-light, #ebeef5) !important; 14 | border-top: none !important; 15 | border-bottom-left-radius: 3px; 16 | border-bottom-right-radius: 3px; 17 | z-index: 1 !important; 18 | } 19 | } 20 | 21 | [data-theme='dark'] { 22 | // textarea - css vars 23 | --w-e-textarea-bg-color: var(--el-color-white) !important; 24 | --w-e-textarea-color: var(--el-text-color-primary) !important; 25 | 26 | // toolbar - css vars 27 | --w-e-toolbar-color: var(--el-text-color-primary) !important; 28 | --w-e-toolbar-bg-color: var(--el-color-white) !important; 29 | --w-e-toolbar-active-color: var(--el-text-color-primary) !important; 30 | --w-e-toolbar-active-bg-color: var(--next-color-menu-hover) !important; 31 | --w-e-toolbar-border-color: var(--el-border-color-light, #ebeef5) !important; 32 | 33 | // modal - css vars 34 | --w-e-modal-button-bg-color: var(--el-color-primary) !important; 35 | --w-e-modal-button-border-color: var(--el-color-primary) !important; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/user.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useUserApi() { 10 | return { 11 | signIn: (data: object) => { 12 | return request({ 13 | url: '/user/login', 14 | method: 'POST', 15 | data, 16 | }); 17 | }, 18 | logout: () => { 19 | return request({ 20 | url: '/user/logout', 21 | method: 'POST', 22 | data: {} 23 | }); 24 | }, 25 | getList: (data?: object) => { 26 | return request({ 27 | url: '/user/list', 28 | method: 'POST', 29 | data, 30 | }); 31 | }, 32 | saveOrUpdate(data?: object) { 33 | return request({ 34 | url: '/user/saveOrUpdate', 35 | method: 'POST', 36 | data 37 | }) 38 | }, 39 | deleted(data?: object) { 40 | return request({ 41 | url: '/user/deleted', 42 | method: 'POST', 43 | data 44 | }) 45 | }, 46 | getMenuByToken() { 47 | return request({ 48 | url: '/user/getMenuByToken', 49 | method: 'POST', 50 | data: {} 51 | }) 52 | }, 53 | getUserInfoByToken() { 54 | return request({ 55 | url: '/user/getUserInfoByToken', 56 | method: 'POST', 57 | data: {} 58 | }) 59 | } 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/stores/keepAliveNames.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | 3 | /** 4 | * 路由缓存列表 5 | * @methods setCacheKeepAlive 设置要缓存的路由 names(开启 Tagsview) 6 | * @methods addCachedView 添加要缓存的路由 names(关闭 Tagsview) 7 | * @methods delCachedView 删除要缓存的路由 names(关闭 Tagsview) 8 | * @methods delOthersCachedViews 右键菜单`关闭其它`,删除要缓存的路由 names(关闭 Tagsview) 9 | * @methods delAllCachedViews 右键菜单`全部关闭`,删除要缓存的路由 names(关闭 Tagsview) 10 | */ 11 | export const useKeepALiveNames = defineStore('keepALiveNames', { 12 | state: (): KeepAliveNamesState => ({ 13 | keepAliveNames: [], 14 | cachedViews: [], 15 | }), 16 | actions: { 17 | setCacheKeepAlive(data: Array) { 18 | this.keepAliveNames = data; 19 | }, 20 | updateCacheKeepAlive(name: string) { 21 | const index = this.keepAliveNames.indexOf(name); 22 | index == -1 && this.keepAliveNames.push(name); 23 | }, 24 | 25 | async addCachedView(isKeepAlive: boolean, fullPath: string) { 26 | const index = this.cachedViews.indexOf(fullPath); 27 | if (isKeepAlive && index == -1) this.cachedViews?.push(fullPath); 28 | }, 29 | async delCachedView(fullPath: string) { 30 | const index = this.cachedViews.indexOf(fullPath); 31 | index > -1 && this.cachedViews.splice(index, 1); 32 | }, 33 | async delOthersCachedViews(view: any) { 34 | if (view.meta.isKeepAlive) this.cachedViews = [view.name]; 35 | else this.cachedViews = []; 36 | }, 37 | async delAllCachedViews() { 38 | this.cachedViews = []; 39 | }, 40 | }, 41 | }); -------------------------------------------------------------------------------- /frontend/src/layout/navBars/topBar/closeFull.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/case_run_count.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/theme/media/scrollbar.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 滚动条的宽度 7 | ::-webkit-scrollbar { 8 | width: 3px !important; 9 | height: 3px !important; 10 | } 11 | ::-webkit-scrollbar-track-piece { 12 | background-color: var(--next-bg-main-color); 13 | } 14 | // 滚动条的设置 15 | ::-webkit-scrollbar-thumb { 16 | background-color: rgba(144, 147, 153, 0.3); 17 | background-clip: padding-box; 18 | min-height: 28px; 19 | border-radius: 5px; 20 | transition: 0.3s background-color; 21 | } 22 | ::-webkit-scrollbar-thumb:hover { 23 | background-color: rgba(144, 147, 153, 0.5); 24 | } 25 | // element plus scrollbar 26 | .el-scrollbar__bar.is-vertical { 27 | width: 2px !important; 28 | } 29 | .el-scrollbar__bar.is-horizontal { 30 | height: 2px !important; 31 | } 32 | } 33 | 34 | /* 页面宽度大于768px 35 | ------------------------------- */ 36 | @media screen and (min-width: 769px) { 37 | // 滚动条的宽度 38 | ::-webkit-scrollbar { 39 | width: 7px; 40 | height: 7px; 41 | } 42 | ::-webkit-scrollbar-track-piece { 43 | background-color: var(--next-bg-main-color); 44 | } 45 | // 滚动条的设置 46 | ::-webkit-scrollbar-thumb { 47 | background-color: rgba(144, 147, 153, 0.3); 48 | background-clip: padding-box; 49 | min-height: 28px; 50 | border-radius: 5px; 51 | transition: 0.3s background-color; 52 | } 53 | ::-webkit-scrollbar-thumb:hover { 54 | background-color: rgba(144, 147, 153, 0.5); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/add_case.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from contextlib import asynccontextmanager 4 | 5 | import uvicorn 6 | from fastapi import FastAPI 7 | 8 | from app.corelibs.logger import init_logger, logger 9 | from app.db import redis_pool 10 | from app.init.cors import init_cors 11 | from app.init.exception import init_exception 12 | from app.init.middleware import init_middleware 13 | from app.init.routers import init_router 14 | from config import config 15 | 16 | 17 | @asynccontextmanager 18 | async def start_app(app: FastAPI): 19 | """ 注册中心 """ 20 | # register_mount(app) # 挂载静态文件 21 | redis_pool.init_by_config(config=config) 22 | init_logger() 23 | logger.info("日志初始化成功!!!") # 初始化日志 24 | 25 | yield 26 | 27 | await redis_pool.redis.close() 28 | 29 | 30 | def create_app() -> FastAPI: 31 | app: FastAPI = FastAPI(title="vue-fastapi-admin", 32 | config=config, 33 | description=config.SERVER_DESC, 34 | version=config.SERVER_VERSION, 35 | lifespan=start_app) 36 | init_exception(app) # 注册捕获全局异常 37 | init_router(app) # 注册路由 38 | init_middleware(app) # 注册请求响应拦截 39 | init_cors(app) # 初始化跨域 40 | 41 | return app 42 | 43 | 44 | app = create_app() 45 | 46 | # gunicorn main:app --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8101 47 | if __name__ == '__main__': 48 | uvicorn.run(app='main:app', host="127.0.0.1", port=9100, reload=True) 49 | -------------------------------------------------------------------------------- /frontend/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | /** 4 | * window.localStorage 浏览器永久缓存 5 | * @method set 设置永久缓存 6 | * @method get 获取永久缓存 7 | * @method remove 移除永久缓存 8 | * @method clear 移除全部永久缓存 9 | */ 10 | export const Local = { 11 | // 设置永久缓存 12 | set(key: string, val: any) { 13 | window.localStorage.setItem(key, JSON.stringify(val)); 14 | }, 15 | // 获取永久缓存 16 | get(key: string) { 17 | let json = window.localStorage.getItem(key); 18 | return JSON.parse(json); 19 | }, 20 | // 移除永久缓存 21 | remove(key: string) { 22 | window.localStorage.removeItem(key); 23 | }, 24 | // 移除全部永久缓存 25 | clear() { 26 | window.localStorage.clear(); 27 | }, 28 | }; 29 | 30 | /** 31 | * window.sessionStorage 浏览器临时缓存 32 | * @method set 设置临时缓存 33 | * @method get 获取临时缓存 34 | * @method remove 移除临时缓存 35 | * @method clear 移除全部临时缓存 36 | */ 37 | export const Session = { 38 | // 设置临时缓存 39 | set(key: string, val: any) { 40 | if (key === 'token') return Cookies.set(key, val); 41 | window.sessionStorage.setItem(key, JSON.stringify(val)); 42 | }, 43 | // 获取临时缓存 44 | get(key: string) { 45 | if (key === 'token') return Cookies.get(key); 46 | let json = window.sessionStorage.getItem(key); 47 | return JSON.parse(json); 48 | }, 49 | // 移除临时缓存 50 | remove(key: string) { 51 | if (key === 'token') return Cookies.remove(key); 52 | window.sessionStorage.removeItem(key); 53 | }, 54 | // 移除全部临时缓存 55 | clear() { 56 | Cookies.remove('token'); 57 | window.sessionStorage.clear(); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /frontend/src/layout/navMenu/subItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ val.meta.title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ val.meta.title }} 15 | 16 | 17 | 18 | 19 | {{ val.meta.title }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 50 | -------------------------------------------------------------------------------- /backend/app/services/system/role.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import typing 3 | 4 | from loguru import logger 5 | 6 | from app.models.system_models import Roles, User 7 | from app.schemas.system.roles import RoleQuery, RoleIn, RoleDel 8 | 9 | 10 | class RolesService: 11 | """角色类""" 12 | 13 | @staticmethod 14 | async def list(params: RoleQuery) -> typing.Dict[str, typing.Any]: 15 | data = await Roles.get_list(params) 16 | for row in data.get("rows", []): 17 | row["menus"] = list(map(int, (row["menus"].split(",")))) if row["menus"] else [] 18 | return data 19 | 20 | @staticmethod 21 | async def save_or_update(params: RoleIn) -> int: 22 | 23 | if params.id: 24 | role_info = await Roles.get(params.id) 25 | if role_info.name != params.name: 26 | if await Roles.get_roles_by_name(params.name): 27 | raise ValueError('角色名已存在!') 28 | else: 29 | if await Roles.get_roles_by_name(params.name): 30 | raise ValueError('角色名已存在!') 31 | result = await Roles.create_or_update(params.dict()) 32 | return result 33 | 34 | @staticmethod 35 | async def deleted(params: RoleDel) -> int: 36 | try: 37 | relation_data = await User.get_user_by_roles(params.id) 38 | if relation_data: 39 | raise ValueError('有用户关联了当前角色,不允许删除!') 40 | return await Roles.delete(params.id) 41 | except Exception as err: 42 | logger.error(traceback.format_exc()) 43 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/project_svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/types/mitt.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * mitt 事件类型定义 3 | * 4 | * @method openSetingsDrawer 打开布局设置弹窗 5 | * @method restoreDefault 分栏布局,鼠标移入、移出数据显示 6 | * @method setSendColumnsChildren 分栏布局,鼠标移入、移出菜单数据传入到 navMenu 下的菜单中 7 | * @method setSendClassicChildren 经典布局,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中 8 | * @method getBreadcrumbIndexSetFilterRoutes 布局设置弹窗,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中 9 | * @method layoutMobileResize 浏览器窗口改变时,用于适配移动端界面显示 10 | * @method openOrCloseSortable 布局设置弹窗,开启 TagsView 拖拽 11 | * @method openShareTagsView 布局设置弹窗,开启 TagsView 共用 12 | * @method onTagsViewRefreshRouterView tagsview 刷新界面 13 | * @method onCurrentContextmenuClick tagsview 右键菜单每项点击时 14 | * @method getColumnList 数据查询工具,获取字段列表 15 | * @method setSourceInfo 数据查询工具,设置数据源信息 16 | * @method setSql 数据查询工具,设置sql 17 | * @method setSql 数据查询工具,设置执行结果 18 | */ 19 | declare type MittType = { 20 | openSetingsDrawer?: string; 21 | restoreDefault?: string; 22 | setSendColumnsChildren: T; 23 | setSendClassicChildren: T; 24 | getBreadcrumbIndexSetFilterRoutes?: string; 25 | layoutMobileResize: T; 26 | openOrCloseSortable?: string; 27 | openShareTagsView?: string; 28 | onTagsViewRefreshRouterView?: T; 29 | onCurrentContextmenuClick?: T; 30 | // dataSrouce 31 | getColumnList?: T; 32 | setSourceInfo?: T; 33 | setSql?: T; 34 | setExecuteResult?: T; 35 | }; 36 | 37 | // mitt 参数类型定义 38 | declare type LayoutMobileResize = { 39 | layout: string; 40 | clientWidth: number; 41 | }; 42 | 43 | // mitt 参数菜单类型 44 | declare type MittMenu = { 45 | children: RouteRecordRaw[]; 46 | item?: RouteItem; 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/topBar/settings/component/icons/full-content.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 29 | 37 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/theme/loading.scss: -------------------------------------------------------------------------------- 1 | .loading-next { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | .loading-next .loading-next-box { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | .loading-next .loading-next-box-warp { 12 | width: 80px; 13 | height: 80px; 14 | } 15 | .loading-next .loading-next-box-warp .loading-next-box-item { 16 | width: 33.333333%; 17 | height: 33.333333%; 18 | background: var(--el-color-primary); 19 | float: left; 20 | animation: loading-next-animation 1.2s infinite ease; 21 | border-radius: 1px; 22 | } 23 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) { 24 | animation-delay: 0s; 25 | } 26 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4), 27 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) { 28 | animation-delay: 0.1s; 29 | } 30 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1), 31 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5), 32 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) { 33 | animation-delay: 0.2s; 34 | } 35 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2), 36 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) { 37 | animation-delay: 0.3s; 38 | } 39 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) { 40 | animation-delay: 0.4s; 41 | } 42 | @keyframes loading-next-animation { 43 | 0%, 44 | 70%, 45 | 100% { 46 | transform: scale3D(1, 1, 1); 47 | } 48 | 35% { 49 | transform: scale3D(0, 0, 1); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/utils/watermark.ts: -------------------------------------------------------------------------------- 1 | // 页面添加水印效果 2 | const setWatermark = (str: string) => { 3 | const id = '1.23452384164.123412416'; 4 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 5 | const can = document.createElement('canvas'); 6 | can.width = 200; 7 | can.height = 130; 8 | const cans = can.getContext('2d'); 9 | cans.rotate((-20 * Math.PI) / 180); 10 | cans.font = '12px Vedana'; 11 | cans.fillStyle = 'rgba(200, 200, 200, 0.30)'; 12 | cans.textBaseline = 'middle'; 13 | cans.fillText(str, can.width / 10, can.height / 2); 14 | const div = document.createElement('div'); 15 | div.id = id; 16 | div.style.pointerEvents = 'none'; 17 | div.style.top = '0px'; 18 | div.style.left = '0px'; 19 | div.style.position = 'fixed'; 20 | div.style.zIndex = '10000000'; 21 | div.style.width = `${document.documentElement.clientWidth}px`; 22 | div.style.height = `${document.documentElement.clientHeight}px`; 23 | div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`; 24 | document.body.appendChild(div); 25 | return id; 26 | }; 27 | 28 | /** 29 | * 页面添加水印效果 30 | * @method set 设置水印 31 | * @method del 删除水印 32 | */ 33 | const watermark = { 34 | // 设置水印 35 | set: (str: string) => { 36 | let id = setWatermark(str); 37 | if (document.getElementById(id) === null) id = setWatermark(str); 38 | }, 39 | // 删除水印 40 | del: () => { 41 | let id = '1.23452384164.123412416'; 42 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 43 | }, 44 | }; 45 | 46 | // 导出方法 47 | export default watermark; -------------------------------------------------------------------------------- /frontend/src/utils/wartermark.ts: -------------------------------------------------------------------------------- 1 | // 页面添加水印效果 2 | const setWatermark = (str: string) => { 3 | const id = '1.23452384164.123412416'; 4 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 5 | const can = document.createElement('canvas'); 6 | can.width = 200; 7 | can.height = 130; 8 | const cans = can.getContext('2d'); 9 | cans.rotate((-20 * Math.PI) / 180); 10 | cans.font = '12px Vedana'; 11 | cans.fillStyle = 'rgba(200, 200, 200, 0.30)'; 12 | cans.textBaseline = 'middle'; 13 | cans.fillText(str, can.width / 10, can.height / 2); 14 | const div = document.createElement('div'); 15 | div.id = id; 16 | div.style.pointerEvents = 'none'; 17 | div.style.top = '0px'; 18 | div.style.left = '0px'; 19 | div.style.position = 'fixed'; 20 | div.style.zIndex = '10000000'; 21 | div.style.width = `${document.documentElement.clientWidth}px`; 22 | div.style.height = `${document.documentElement.clientHeight}px`; 23 | div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`; 24 | document.body.appendChild(div); 25 | return id; 26 | }; 27 | 28 | /** 29 | * 页面添加水印效果 30 | * @method set 设置水印 31 | * @method del 删除水印 32 | */ 33 | const watermark = { 34 | // 设置水印 35 | set: (str: string) => { 36 | let id = setWatermark(str); 37 | if (document.getElementById(id) === null) id = setWatermark(str); 38 | }, 39 | // 删除水印 40 | del: () => { 41 | let id = '1.23452384164.123412416'; 42 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 43 | }, 44 | }; 45 | 46 | // 导出方法 47 | export default watermark; 48 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/websocket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/theme/media/login.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于1200px 4 | ------------------------------- */ 5 | @media screen and (max-width: $lg) and (min-width: $xs) { 6 | .login-container { 7 | .login-left { 8 | .login-left-img { 9 | top: 90% !important; 10 | left: 12% !important; 11 | width: 30% !important; 12 | height: 18% !important; 13 | } 14 | } 15 | .login-right { 16 | position: absolute; 17 | top: 50%; 18 | left: 50%; 19 | transform: translate(-50%, -50%); 20 | } 21 | } 22 | } 23 | 24 | /* 页面宽度小于576px 25 | ------------------------------- */ 26 | @media screen and (max-width: $xs) { 27 | .login-container { 28 | .login-left { 29 | display: none; 30 | } 31 | .login-right { 32 | width: 100% !important; 33 | .login-right-warp { 34 | width: 100% !important; 35 | height: 100% !important; 36 | border: none !important; 37 | .login-right-warp-mian { 38 | .el-form-item { 39 | display: flex !important; 40 | } 41 | .login-right-warp-main-title { 42 | font-size: 20px !important; 43 | } 44 | } 45 | .login-right-warp-one { 46 | &::after { 47 | right: 0 !important; 48 | } 49 | } 50 | .login-right-warp-two { 51 | &::before { 52 | bottom: 1px !important; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | /* 页面宽度小于375px 61 | ------------------------------- */ 62 | @media screen and (max-width: $us) { 63 | .login-container { 64 | .login-right { 65 | .login-right-warp { 66 | .login-right-warp-mian { 67 | .login-right-warp-main-title { 68 | font-size: 18px !important; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/api/useSystemApi/lookup.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 数据字典 5 | * @method getLookupList 获取数据字典列表 6 | * @method saveOrUpdateLookup 更新保存数据字典 7 | * @method delLookup 删除数据字典 8 | * @method getLookupValue 获取数据字典列值 9 | * @method saveOrUpdateLookupValue 更新保存数据字典值 10 | * @method delLookupValue 删除数据字典值 11 | */ 12 | export function useLookupApi() { 13 | return { 14 | getAllLookup: () => { 15 | return request({ 16 | url: '/lookup/getAllLookup', 17 | method: 'POST', 18 | data: {} 19 | }); 20 | }, 21 | getLookupList: (data: object) => { 22 | return request({ 23 | url: '/lookup/getLookupList', 24 | method: 'POST', 25 | data, 26 | }); 27 | }, 28 | saveOrUpdateLookup: (data: object) => { 29 | return request({ 30 | url: '/lookup/saveOrUpdateLookup', 31 | method: 'POST', 32 | data 33 | }); 34 | }, 35 | delLookup: (data?: object) => { 36 | return request({ 37 | url: '/lookup/delLookup', 38 | method: 'POST', 39 | data, 40 | }); 41 | }, 42 | getLookupValue(data?: object) { 43 | return request({ 44 | url: '/lookup/getLookupValue', 45 | method: 'POST', 46 | data 47 | }) 48 | }, 49 | saveOrUpdateLookupValue(data?: object) { 50 | return request({ 51 | url: '/lookup/saveOrUpdateLookupValue', 52 | method: 'POST', 53 | data 54 | }) 55 | }, 56 | delLookupValue(data?: object) { 57 | return request({ 58 | url: '/lookup/delLookupValue', 59 | method: 'POST', 60 | data 61 | }) 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /backend/app/schemas/system/lookup.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | from app.schemas.base import BaseSchema 6 | 7 | 8 | class LookupIn(BaseModel): 9 | """字典保存""" 10 | 11 | id: typing.Optional[int] = Field(None, description="字典id") 12 | code: str = Field(..., description="字典code") 13 | description: typing.Optional[str] = Field(None, description="字典描述") 14 | 15 | 16 | class LookupQuery(BaseSchema): 17 | code: typing.Optional[str] = Field(None, description="字典code") 18 | 19 | 20 | class LookupId(BaseSchema): 21 | id: int = Field(None, description="字典id") 22 | 23 | 24 | class LookupValueQuery(BaseSchema): 25 | code: typing.Optional[str] = Field(None, description="字典code") 26 | lookup_id: typing.Optional[str] = Field(None, description="lookup_id") 27 | 28 | # @root_validator(pre=True) 29 | # def root_validator(cls, data): 30 | # code = data.get('code', None) 31 | # lookup_id = data.get('lookup_id', None) 32 | # if not code: 33 | # raise ParameterError('编码不能为空!') 34 | # if not lookup_id: 35 | # raise ParameterError('数据字典id不能为空!') 36 | # return data 37 | 38 | 39 | class LookupValueIn(BaseModel): 40 | """字典值保存""" 41 | id: int = Field(None, description="字典id") 42 | lookup_id: int = Field(None, description="字典id") 43 | lookup_code: typing.Optional[str] = Field(..., description="字典code") 44 | lookup_value: typing.Optional[str] = Field(..., description="字典value") 45 | ext: typing.Optional[str] = Field(None, description="备注") 46 | display_sequence: typing.Optional[int] = Field(None, description="显示顺序") 47 | description: typing.Optional[str] = Field(None, description="描述") 48 | -------------------------------------------------------------------------------- /frontend/src/views/login/component/scan.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 打开手机扫一扫,快速登录/注册 7 | 8 | 9 | 10 | 11 | 36 | 37 | 67 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/load.ts: -------------------------------------------------------------------------------- 1 | import { addIcon, type IconifyIcon } from '@iconify/vue'; 2 | 3 | let loaded = false; 4 | if (!loaded) { 5 | loadSvgIcons(); 6 | loaded = true; 7 | } 8 | 9 | function parseSvg(svgData: string): IconifyIcon { 10 | const parser = new DOMParser(); 11 | const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml'); 12 | const svgElement = xmlDoc.documentElement; 13 | 14 | const svgContent = [...svgElement.childNodes] 15 | .filter((node) => node.nodeType === Node.ELEMENT_NODE) 16 | .map((node) => new XMLSerializer().serializeToString(node)) 17 | .join(''); 18 | 19 | const viewBoxValue = svgElement.getAttribute('viewBox') || ''; 20 | const [left, top, width, height] = viewBoxValue.split(' ').map((val) => { 21 | const num = Number(val); 22 | return Number.isNaN(num) ? undefined : num; 23 | }); 24 | 25 | return { 26 | body: svgContent, 27 | height, 28 | left, 29 | top, 30 | width, 31 | }; 32 | } 33 | 34 | /** 35 | * 自定义的svg图片转化为组件 36 | * @example ./svg/avatar.svg 37 | * 38 | */ 39 | async function loadSvgIcons() { 40 | const svgEagers = import.meta.glob('./icons/**', { 41 | eager: true, 42 | query: '?raw', 43 | }); 44 | 45 | await Promise.all( 46 | Object.entries(svgEagers).map((svg) => { 47 | const [key, body] = svg as [string, { default: string } | string]; 48 | 49 | // ./icons/xxxx.svg => xxxxxx 50 | const start = key.lastIndexOf('/') + 1; 51 | const end = key.lastIndexOf('.'); 52 | const iconName = key.slice(start, end); 53 | 54 | return addIcon(`svg:${iconName}`, { 55 | ...parseSvg(typeof body === 'object' ? body.default : body), 56 | }); 57 | }), 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | -------------------------------------------------------------------------------- /frontend/src/layout/main/transverse.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /backend/app/schemas/system/menu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import typing 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | from app.schemas.base import BaseSchema 8 | 9 | 10 | class MenuIn(BaseModel): 11 | id: int = Field(None, description='id') 12 | path: typing.Optional[str] = Field(..., description='路径') 13 | name: typing.Optional[str] = Field(..., description='组件名称') 14 | component: typing.Optional[str] = Field(..., description='组件路径') 15 | title: typing.Optional[str] = Field(..., description='路由名称') 16 | isLink: typing.Optional[bool] = Field(False, 17 | description='是否是链接 开启外链条件,`1、isLink: true 2、链接地址不为空(meta.isLink) 3、isIframe: false`') 18 | isHide: typing.Optional[bool] = Field(False, description='菜单是否隐藏(菜单不显示在界面,但可以进行跳转)') 19 | isKeepAlive: typing.Optional[bool] = Field(True, description='菜单是否隐藏(菜单不显示在界面,但可以进行跳转)') 20 | isAffix: typing.Optional[bool] = Field(False, description='是否固定') 21 | isIframe: typing.Optional[bool] = Field(False, description='是否内嵌') 22 | icon: typing.Optional[str] = Field("", description='菜单图标') 23 | parent_id: typing.Optional[str] = Field(..., description='父级菜单id') 24 | redirect: typing.Optional[str] = Field(None, description='重定向路由') 25 | sort: typing.Optional[int] = Field(0, description='排序') 26 | menu_type: typing.Optional[int] = Field(..., description='菜单类型') 27 | active_menu: typing.Optional[str] = Field(None, description='显示页签') 28 | 29 | 30 | class MenuUpdate(MenuIn): 31 | pass 32 | 33 | 34 | class MenuDel(BaseModel): 35 | id: int = Field(..., title="id", description='id') 36 | 37 | 38 | class MenuViews(BaseModel): 39 | menu_id: int = Field(..., title="id", description='菜单id') 40 | 41 | 42 | class Menu(BaseSchema): 43 | name: typing.Optional[str] = Field(None, description='组件名称') 44 | -------------------------------------------------------------------------------- /frontend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vue-next-admin-template(不带国际化) 更新日志 2 | 3 | 🎉🎉🔥 `vue-next-admin-template` 基于 (vue-next-admin-v2.4.21 版本) vue3.x 、Typescript、vite、Element plus 等,适配手机、平板、pc 的后台开源免费模板库(vue2.x 请切换 vue-prev-admin 分支) 4 | 5 | ## 2.4.21 6 | 7 | `2022.12.12` 8 | 9 | - 🎉 同步 master 分支 v2.4.21 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 10 | 11 | ## 2.4.2 12 | 13 | `2022.12.10` 14 | 15 | - 🎉 同步 master 分支 v2.4.2 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 16 | 17 | ## 2.4.1 18 | 19 | `2022.11.30` 20 | 21 | - 🎉 同步 master 分支 v2.4.1 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 22 | 23 | ## 2.3.0 24 | 25 | `2022.11.16` 26 | 27 | - 🎉 同步 master 分支 v2.3.0 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 28 | 29 | ## 2.2.0 30 | 31 | `2022.07.11` 32 | 33 | - 🎉 同步 master 分支 v2.2.0 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 34 | 35 | ## 2.1.1 36 | 37 | - 🎉 同步 master 分支 v2.1.1 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md) 38 | 39 | ## 2.0.2 40 | 41 | - 🎉 同步 master 分支 v2.0.2 版本内容,具体查看 master CHANGELOG.md 42 | 43 | ## 0.2.2 44 | 45 | `2021.12.21` 46 | 47 | - 🎉 同步 master 分支 v1.2.2 版本内容,具体查看 master CHANGELOG.md 48 | 49 | ## 0.2.1 50 | 51 | `2021.12.12` 52 | 53 | - 🌟 更新 依赖更新最新版本 54 | - 🐞 修复 浏览器标题问题 55 | - 🐞 修复 element plus svg 图标引入 56 | - 🐞 修复 默认显示英文问题,改成默认显示中文 57 | 58 | ## 0.2.0 59 | 60 | `2021.12.04` 61 | 62 | - 🎉 同步 master 分支 v1.2.0 版本内容,具体查看 master CHANGELOG.md 63 | 64 | ## 0.1.0 65 | 66 | `2021.10.17` 67 | 68 | - 🎉 新增 vue-next-admin-template 基础版本(不带国际化),切换 `vue-next-admin-template` 分支 69 | -------------------------------------------------------------------------------- /backend/app/corelibs/custom_router.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: 小白 3 | from typing import Callable 4 | 5 | import fastapi 6 | from fastapi.requests import Request 7 | from fastapi.responses import Response 8 | from fastapi.routing import APIRoute 9 | 10 | from app.utils.context import FastApiRequest 11 | 12 | 13 | class ContextIncludedRoute(APIRoute): 14 | def get_route_handler(self) -> Callable: 15 | original_route_handler = super().get_route_handler() 16 | 17 | async def custom_route_handler(request: Request) -> Response: 18 | # 请求日志的初始化操作 19 | try: 20 | body_form = await request.form() 21 | except: 22 | body_form = None 23 | 24 | body = None 25 | try: 26 | body_bytes = await request.body() 27 | if body_bytes: 28 | try: 29 | body = await request.json() 30 | except: 31 | pass 32 | if body_bytes: 33 | try: 34 | body = body_bytes.decode('utf-8') 35 | except: 36 | body = body_bytes.decode('gb2312') 37 | except: 38 | pass 39 | 40 | request.scope.setdefault("request_form", body_form) 41 | request.scope.setdefault("request_body", body) 42 | FastApiRequest.set(request) 43 | 44 | # 这里记录下请求入口的时候相关的日志信息 45 | 46 | response: Response = await original_route_handler(request) 47 | 48 | return response 49 | 50 | return custom_route_handler 51 | 52 | 53 | class APIRouter(fastapi.APIRouter): 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | self.route_class = ContextIncludedRoute 57 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/user_login.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/utils/arrayOperation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断两数组字符串是否相同(用于按钮权限验证),数组字符串中存在相同时会自动去重(按钮权限标识不会重复) 3 | * @param news 新数据 4 | * @param old 源数据 5 | * @returns 两数组相同返回 `true`,反之则反 6 | */ 7 | export function judementSameArr(newArr: unknown[] | string[], oldArr: string[]): boolean { 8 | const news = removeDuplicate(newArr); 9 | const olds = removeDuplicate(oldArr); 10 | let count = 0; 11 | const leng = news.length; 12 | for (let i in olds) { 13 | for (let j in news) { 14 | if (olds[i] === news[j]) count++; 15 | } 16 | } 17 | return count === leng ? true : false; 18 | } 19 | 20 | /** 21 | * 判断两个对象是否相同 22 | * @param a 要比较的对象一 23 | * @param b 要比较的对象二 24 | * @returns 相同返回 true,反之则反 25 | */ 26 | export function isObjectValueEqual(a: T, b: T): boolean { 27 | if (!a || !b) return false; 28 | let aProps = Object.getOwnPropertyNames(a); 29 | let bProps = Object.getOwnPropertyNames(b); 30 | if (aProps.length != bProps.length) return false; 31 | for (let i = 0; i < aProps.length; i++) { 32 | let propName = aProps[i]; 33 | let propA = a[propName]; 34 | let propB = b[propName]; 35 | if (!b.hasOwnProperty(propName)) return false; 36 | if (propA instanceof Object) { 37 | if (!isObjectValueEqual(propA, propB)) return false; 38 | } else if (propA !== propB) { 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | /** 46 | * 数组、数组对象去重 47 | * @param arr 数组内容 48 | * @param attr 需要去重的键值(数组对象) 49 | * @returns 50 | */ 51 | export function removeDuplicate(arr: EmptyArrayType, attr?: string) { 52 | if (!Object.keys(arr).length) { 53 | return arr; 54 | } else { 55 | if (attr) { 56 | const obj: EmptyObjectType = {}; 57 | return arr.reduce((cur: EmptyArrayType[], item: EmptyArrayType) => { 58 | obj[item[attr]] ? '' : (obj[item[attr]] = true && item[attr] && cur.push(item)); 59 | return cur; 60 | }, []); 61 | } else { 62 | return [...new Set(arr)]; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/loop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/apis/system/lookup.py: -------------------------------------------------------------------------------- 1 | from app.corelibs.custom_router import APIRouter 2 | from app.schemas.system.lookup import LookupIn, LookupValueQuery, LookupValueIn, LookupQuery, LookupId 3 | from app.services.system.lookup import LookupValueService, LookupService 4 | from app.utils.response import HttpResponse 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.post('/getAllLookup', description="获取所有数据字典") 10 | async def get_all_lookup(): 11 | data = await LookupValueService.get_all_lookup() 12 | return await HttpResponse.success(data) 13 | 14 | 15 | @router.post('/getLookupList', description="获取数据字典列表") 16 | async def lookup_list(params: LookupQuery): 17 | data = await LookupService.list(params) 18 | return await HttpResponse.success(data) 19 | 20 | 21 | @router.post('/saveOrUpdateLookup', description="新增或更新字典") 22 | async def save_or_update_lookup(params: LookupIn): 23 | data = await LookupService.save_or_update(params) 24 | return await HttpResponse.success(data) 25 | 26 | 27 | @router.post('/delLookup', description="删除字典") 28 | async def del_lookup(params: LookupId): 29 | data = await LookupService.deleted(params) 30 | return await HttpResponse.success(data) 31 | 32 | 33 | @router.post('/getLookupValue', description="获取字典值") 34 | async def get_lookup_value(params: LookupValueQuery): 35 | """获取字典值""" 36 | data = await LookupValueService.get_lookup_value(params) 37 | return await HttpResponse.success(data) 38 | 39 | 40 | @router.post('/saveOrUpdateLookupValue', description="保存或更新字典值") 41 | async def save_or_update_lookup_value(params: LookupValueIn): 42 | """保存或更新字典值""" 43 | data = await LookupValueService.save_or_update(params) 44 | return await HttpResponse.success(data) 45 | 46 | 47 | @router.post('/delLookupValue', description="删除字典值") 48 | async def del_lookup_value(params: LookupId): 49 | """删除字典值""" 50 | data = await LookupValueService.deleted(params) 51 | return await HttpResponse.success(data) 52 | -------------------------------------------------------------------------------- /frontend/src/layout/navBars/topBar/settings/component/general.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 通用 14 | 15 | 16 | 开启水印 17 | 18 | 19 | 20 | 21 | 22 | 水印文案 23 | 24 | 26 | 27 | 28 | 29 | 动画 30 | 31 | 32 | 主页面切换动画 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/utils/mersenneTwister.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 为了保证相同的内容每次生成的随机数都是一样的,我们可以使用一个伪随机数生成器(PRNG),并使用内容的哈希值作为种子。以下是一个使用Mersenne Twister算法的PRNG的实现: 3 | * 4 | * @param {*} seed 5 | */ 6 | 7 | export default function MersenneTwister(seed) { 8 | this.N = 624 9 | this.M = 397 10 | this.MATRIX_A = 0x9908b0df 11 | this.UPPER_MASK = 0x80000000 12 | this.LOWER_MASK = 0x7fffffff 13 | 14 | this.mt = new Array(this.N) 15 | this.mti = this.N + 1 16 | 17 | this.init_genrand(seed) 18 | } 19 | 20 | MersenneTwister.prototype.init_genrand = function (s) { 21 | this.mt[0] = s >>> 0 22 | for (this.mti = 1; this.mti < this.N; this.mti++) { 23 | s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30) 24 | this.mt[this.mti] = 25 | ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + 26 | (s & 0x0000ffff) * 1812433253 + 27 | this.mti 28 | this.mt[this.mti] >>>= 0 29 | } 30 | } 31 | 32 | MersenneTwister.prototype.genrand_int32 = function () { 33 | var y 34 | var mag01 = new Array(0x0, this.MATRIX_A) 35 | 36 | if (this.mti >= this.N) { 37 | var kk 38 | 39 | if (this.mti == this.N + 1) this.init_genrand(5489) 40 | 41 | for (kk = 0; kk < this.N - this.M; kk++) { 42 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK) 43 | this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1] 44 | } 45 | 46 | for (; kk < this.N - 1; kk++) { 47 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK) 48 | this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1] 49 | } 50 | 51 | y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK) 52 | this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1] 53 | 54 | this.mti = 0 55 | } 56 | 57 | y = this.mt[this.mti++] 58 | 59 | y ^= y >>> 11 60 | y ^= (y << 7) & 0x9d2c5680 61 | y ^= (y << 15) & 0xefc60000 62 | y ^= y >>> 18 63 | 64 | return y >>> 0 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/utils/commonFunction.ts: -------------------------------------------------------------------------------- 1 | // 通用函数 2 | import useClipboard from 'vue-clipboard3'; 3 | import { ElMessage } from 'element-plus'; 4 | import { formatDate } from '/@/utils/formatTime'; 5 | 6 | export default function () { 7 | const { toClipboard } = useClipboard(); 8 | 9 | // 百分比格式化 10 | const percentFormat = (row: EmptyArrayType, column: number, cellValue: string) => { 11 | return cellValue ? `${cellValue}%` : '-'; 12 | }; 13 | // 列表日期时间格式化 14 | const dateFormatYMD = (row: EmptyArrayType, column: number, cellValue: string) => { 15 | if (!cellValue) return '-'; 16 | return formatDate(new Date(cellValue), 'YYYY-mm-dd'); 17 | }; 18 | // 列表日期时间格式化 19 | const dateFormatYMDHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 20 | if (!cellValue) return '-'; 21 | return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS'); 22 | }; 23 | // 列表日期时间格式化 24 | const dateFormatHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 25 | if (!cellValue) return '-'; 26 | let time = 0; 27 | if (typeof row === 'number') time = row; 28 | if (typeof cellValue === 'number') time = cellValue; 29 | return formatDate(new Date(time * 1000), 'HH:MM:SS'); 30 | }; 31 | // 小数格式化 32 | const scaleFormat = (value: string = '0', scale: number = 4) => { 33 | return Number.parseFloat(value).toFixed(scale); 34 | }; 35 | // 小数格式化 36 | const scale2Format = (value: string = '0') => { 37 | return Number.parseFloat(value).toFixed(2); 38 | }; 39 | // 点击复制文本 40 | const copyText = (text: string) => { 41 | return new Promise((resolve, reject) => { 42 | try { 43 | // 复制 44 | toClipboard(text); 45 | // 下面可以设置复制成功的提示框等操作 46 | ElMessage.success('复制成功!'); 47 | resolve(text); 48 | } catch (e) { 49 | // 复制失败 50 | ElMessage.error('复制失败!'); 51 | reject(e); 52 | } 53 | }); 54 | }; 55 | return { 56 | percentFormat, 57 | dateFormatYMD, 58 | dateFormatYMDHMS, 59 | dateFormatHMS, 60 | scaleFormat, 61 | scale2Format, 62 | copyText, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /backend/app/init/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import time 4 | 5 | from fastapi import FastAPI, Request 6 | from loguru import logger 7 | 8 | from app.corelibs import g 9 | from app.corelibs.consts import TEST_USER_INFO, CACHE_DAY 10 | from app.db import redis_pool 11 | from app.exceptions.exceptions import AccessTokenFail 12 | from app.utils.common import get_str_uuid 13 | from app.utils.context import AccessToken 14 | from app.utils.response import HttpResponse 15 | from config import config 16 | 17 | 18 | async def login_verification(request: Request): 19 | """ 20 | 登录校验 21 | :param request: 路径 22 | :return: 23 | """ 24 | token = request.headers.get("token", None) 25 | router: str = request.scope.get('path', "") 26 | if router.startswith("/api") and not router.startswith("/api/file") and router not in config.WHITE_ROUTER: 27 | if not token: 28 | raise AccessTokenFail() 29 | user_info = await redis_pool.redis.get(TEST_USER_INFO.format(token)) 30 | if not user_info: 31 | raise AccessTokenFail() 32 | # 重置token时间 33 | await redis_pool.redis.set(TEST_USER_INFO.format(token), user_info, CACHE_DAY) 34 | 35 | 36 | def init_middleware(app: FastAPI): 37 | """""" 38 | 39 | @app.middleware("http") 40 | async def intercept(request: Request, call_next): 41 | g.trace_id = get_str_uuid() 42 | start_time = time.time() 43 | token = request.headers.get("token", None) 44 | AccessToken.set(token) 45 | remote_addr = request.headers.get("X-Real-IP", request.client.host) 46 | logger.info(f"访问记录:IP:{remote_addr}-method:{request.method}-url:{request.url}") 47 | # 登录校验 48 | try: 49 | await login_verification(request) 50 | except AccessTokenFail as err: 51 | return await HttpResponse.success(code=err.code, msg=err.msg) 52 | response = await call_next(request) 53 | response.headers["X-request-id"] = g.trace_id 54 | logger.info(f"请求耗时: {time.time() - start_time}") 55 | return response 56 | -------------------------------------------------------------------------------- /frontend/src/components/svgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 63 | -------------------------------------------------------------------------------- /backend/app/exceptions/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import typing 4 | 5 | from app.corelibs.codes import CodeEnum 6 | 7 | 8 | class MyBaseException(Exception): 9 | def __init__(self, err_or_code: typing.Union[CodeEnum, str]): 10 | if isinstance(err_or_code, CodeEnum): 11 | code = err_or_code.code 12 | msg = err_or_code.msg 13 | else: 14 | code = CodeEnum.PARTNER_CODE_FAIL.code 15 | msg = err_or_code 16 | self.code = code 17 | self.msg = msg 18 | 19 | def __str__(self): 20 | return f"{self.code}:{self.msg}" 21 | 22 | def __repr__(self): 23 | return f"{self.code}:{self.msg}" 24 | 25 | 26 | class IpError(MyBaseException): 27 | """ ip错误 """ 28 | 29 | def __init__(self): 30 | super(IpError, self).__init__("ip 错误") 31 | 32 | 33 | class SetRedis(MyBaseException): 34 | """ Redis存储失败 """ 35 | 36 | def __init__(self): 37 | super(SetRedis, self).__init__("Redis存储失败") 38 | 39 | 40 | class IdNotExist(MyBaseException): 41 | """ 查询id不存在 """ 42 | 43 | def __init__(self): 44 | super(IdNotExist, self).__init__("查询id不存在") 45 | 46 | 47 | class UserNotExist(MyBaseException): 48 | """ 用户不存在 """ 49 | 50 | def __init__(self): 51 | super(UserNotExist, self).__init__("用户不存在") 52 | 53 | 54 | class AccessTokenFail(MyBaseException): 55 | """ 访问令牌失败 """ 56 | 57 | def __init__(self): 58 | super(AccessTokenFail, self).__init__(CodeEnum.PARTNER_CODE_TOKEN_EXPIRED_FAIL) 59 | 60 | 61 | class ErrorUser(MyBaseException): 62 | """ 错误的用户名或密码 """ 63 | 64 | def __init__(self): 65 | super(ErrorUser, self).__init__("错误的用户名或密码") 66 | 67 | 68 | class PermissionNotEnough(MyBaseException): 69 | """ 权限不足,拒绝访问 """ 70 | 71 | def __init__(self): 72 | super(PermissionNotEnough, self).__init__("权限不足,拒绝访问") 73 | 74 | 75 | class ParameterError(MyBaseException): 76 | """ 参数错误 """ 77 | 78 | def __init__(self, err_code: typing.Union[CodeEnum, str]): 79 | super(ParameterError, self).__init__(err_code) 80 | 81 | -------------------------------------------------------------------------------- /frontend/src/views/login/component/mobile.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 获取验证码 21 | 22 | 23 | 24 | 25 | 登 录 26 | 27 | 28 | 29 | * 温馨提示:建议使用谷歌、Microsoft Edge,版本 79.0.1072.62 及以上浏览器,360浏览器请使用极速模式 30 | 31 | 32 | 33 | 34 | 45 | 46 | 73 | -------------------------------------------------------------------------------- /frontend/src/components/iconSelector/list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 44 | 45 | 85 | -------------------------------------------------------------------------------- /backend/app/corelibs/codes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | 4 | 5 | from enum import Enum 6 | 7 | 8 | class CodeEnum(Enum): 9 | 10 | @property 11 | def code(self): 12 | """获取状态吗""" 13 | return self.value[0] 14 | 15 | @property 16 | def msg(self): 17 | """获取状态码信息""" 18 | return self.value[1] 19 | 20 | # 业务状态码 21 | PARTNER_CODE_OK = (0, "OK") 22 | PARTNER_CODE_FAIL = (-1, "操作失败") 23 | 24 | # 10000 - 11000 账号体系 25 | WRONG_USER_NAME_OR_PASSWORD = (10001, "账号或者密码错误!😱") # 账号或密码错误 26 | PARTNER_CODE_EMPLOYEE_FAIL = (10002, "账号错误!") # 账号错误 27 | WRONG_USER_NAME_OR_PASSWORD_LOCK = (10003, "密码输入错误超过次数,请5分钟后再登录!😭") 28 | USERNAME_OR_EMAIL_IS_REGISTER = (10004, "用户名已被注册") 29 | USER_ID_IS_NULL = (10005, "用户id不能为空") 30 | PASSWORD_TWICE_IS_NOT_AGREEMENT = (10006, "两次输入的密码不一致") 31 | NEW_PWD_NO_OLD_PWD_EQUAL = (10007, "新密码不能与旧密码相同") 32 | OLD_PASSWORD_ERROR = (10008, "旧密码错误") 33 | 34 | # 用户状态 验证 11000 - 12000 35 | PARTNER_CODE_TOKEN_EXPIRED_FAIL = (11000, "用户信息以已过期 😂") # token已过期 36 | 37 | # 参数类型 12000 - 13000 38 | PARTNER_CODE_PARAMS_FAIL = (12000, "必填参数不能为空 😅") # 必填参数不能为空 39 | 40 | # project 项目 13000 - 14000 41 | PROJECT_HAS_MODULE_ASSOCIATION = (13000, "项目有模块或用例关联,不能删除") 42 | PROJECT_NAME_EXIST = (13001, "项目名已存在") # 项目名以存在 43 | 44 | # module 模块 14000 - 15000 45 | MODULE_HAS_CASE_ASSOCIATION = (14000, " 模块有用例关联, 请删除对于模块下的用例") # 模块有用例关联 46 | MODULE_NAME_EXIST = (14001, "模块名已存在") # 模块名以存在 47 | 48 | # case 用例/配置 15000 - 16000 49 | CASE_NAME_EXIST = (15000, "用例名已存在,请重新命名") 50 | SUITE_NAME_EXIST = (15001, "套件名已存在,请重新命名") 51 | CASE_NOT_EXIST = (15002, "用例不存在") 52 | CASE_UPLOAD_FROM_POSTMAN = (15003, "导入失败") 53 | 54 | # 菜单管理 16000 - 17000 55 | MENU_HAS_MODULE_ASSOCIATION = (16000, "当前菜单下管理的子菜单,不能删除!") # 56 | MENU_NAME_EXIST = (16001, "菜单名称已存在") # 菜单名以存在 57 | 58 | # lookup 数据字典 17000 - 18000 59 | LOOKUP_CODE_NOT_EMPTY = (17000, "字典code不能为空!") # 菜单名以存在 60 | LOOKUP_NOT_EXIST = (17001, "字典不存在!") # 菜单名以存在 61 | LOOKUP_CODE_EXIST = (17002, "字典code已存在!") # 菜单名以存在 62 | 63 | # task 定时任务 18000 - 19000 64 | TASK_NAME_EXIST = (18000, "定时任务名称以存在") 65 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-element-admin", 3 | "version": "2.0.1", 4 | "description": "一个自动化测试平台", 5 | "author": "xiaobai", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vite --force", 9 | "build": "vite build", 10 | "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" 11 | }, 12 | "dependencies": { 13 | "@element-plus/icons-vue": "^2.0.10", 14 | "@iconify/vue": "^4.1.2", 15 | "axios": "^1.2.1", 16 | "echarts": "^5.4.1", 17 | "element-plus": "^2.7.4", 18 | "js-cookie": "^3.0.1", 19 | "lucide-vue-next": "^0.454.0", 20 | "mitt": "^3.0.0", 21 | "nprogress": "^0.2.0", 22 | "pinia": "^2.0.28", 23 | "pinia-plugin-persistedstate": "^4.1.2", 24 | "qrcodejs2-fixes": "^0.0.2", 25 | "qs": "^6.11.0", 26 | "screenfull": "^6.0.2", 27 | "sortablejs": "^1.15.0", 28 | "vue": "3.5.8", 29 | "vue-clipboard3": "^2.0.0", 30 | "vue-router": "^4.1.6" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^18.11.13", 34 | "@types/nprogress": "^0.2.0", 35 | "@typescript-eslint/eslint-plugin": "^5.46.0", 36 | "@typescript-eslint/parser": "^5.46.0", 37 | "@vitejs/plugin-vue": "5.0.4", 38 | "@vue/compiler-sfc": "^3.2.45", 39 | "cropperjs": "^1.5.13", 40 | "eslint": "8.22.0", 41 | "eslint-plugin-vue": "^9.8.0", 42 | "monaco-editor": "^0.34.1", 43 | "prettier": "^2.8.1", 44 | "sass": "^1.56.2", 45 | "splitpanes": "^3.1.5", 46 | "typescript": "^4.9.4", 47 | "vite": "^5.4.8", 48 | "vite-plugin-monaco-editor": "1.0.5", 49 | "vite-plugin-vue-setup-extend": "^0.4.0", 50 | "vue-eslint-parser": "^9.1.0", 51 | "vuedraggable": "^4.1.0", 52 | "xterm": "^5.1.0", 53 | "xterm-addon-fit": "^0.7.0" 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions", 58 | "not dead" 59 | ], 60 | "bugs": { 61 | "url": "https://github.com/baizunxian/zerorunner/issues" 62 | }, 63 | "engines": { 64 | "node": ">=16.0.0", 65 | "npm": ">= 7.0.0" 66 | }, 67 | "keywords": [ 68 | "vue", 69 | "vue3", 70 | "vuejs/vue-next", 71 | "vuejs/vue-next-template", 72 | "element-ui", 73 | "element-plus", 74 | "vue-next-admin", 75 | "next-admin" 76 | ], 77 | "repository": { 78 | "type": "git", 79 | "url": "https://github.com/baizunxian/zerorunner.git" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosInstance, AxiosRequestConfig} from 'axios'; 2 | import {ElMessage, ElMessageBox} from 'element-plus'; 3 | import {Session} from '/@/utils/storage'; 4 | import qs from 'qs'; 5 | import {getApiBaseUrl} from "/@/utils/config"; 6 | import {handlerRedirectUrl} from "/@/utils/urlHandler"; 7 | 8 | const cancelToken = axios.CancelToken 9 | const source = cancelToken.source() 10 | 11 | // 配置新建一个 axios 实例 12 | const service: AxiosInstance = axios.create({ 13 | baseURL: getApiBaseUrl(), 14 | timeout: 50000, 15 | headers: {'Content-Type': 'application/json'}, 16 | paramsSerializer: { 17 | serialize(params) { 18 | return qs.stringify(params, {allowDots: true}); 19 | }, 20 | }, 21 | }); 22 | 23 | // 添加请求拦截器 24 | service.interceptors.request.use( 25 | (config: AxiosRequestConfig) => { 26 | // 在发送请求之前做些什么 token 27 | if (Session.get('token')) { 28 | config.headers!['token'] = `${Session.get('token')}`; 29 | } 30 | return config; 31 | }, 32 | (error) => { 33 | // 对请求错误做些什么 34 | return Promise.reject(error); 35 | } 36 | ); 37 | 38 | // 添加响应拦截器 39 | service.interceptors.response.use( 40 | (response) => { 41 | // 对响应数据做点什么 42 | const res = response.data; 43 | if (res.code && res.code !== 0) { 44 | // `token` 过期或者账号已在别处登录 45 | if (res.code === 11000) { 46 | ElMessageBox.confirm('登录信息已失效,是否重新登录?', '提示', { 47 | confirmButtonText: '确认', 48 | cancelButtonText: '取消', 49 | type: 'warning', 50 | }) 51 | .then(() => { 52 | Session.clear(); // 清除浏览器全部临时缓存 53 | window.location.href = handlerRedirectUrl() || '/'; // 去登录页 54 | }) 55 | .catch(() => { 56 | }); 57 | } else { 58 | ElMessage.error(res.msg || '接口错误!'); 59 | } 60 | return Promise.reject(service.interceptors.response); 61 | } else { 62 | return response.data; 63 | } 64 | }, 65 | (error) => { 66 | // 对响应错误做点什么 67 | if (error.message.indexOf('timeout') != -1) { 68 | ElMessage.error('网络超时'); 69 | } else if (error.message == 'Network Error') { 70 | ElMessage.error('网络连接错误'); 71 | } else { 72 | 73 | if (error.response?.data) ElMessage.error(error.response.statusText); 74 | else ElMessage.error('接口路径找不到'); 75 | } 76 | return Promise.reject(error); 77 | } 78 | ); 79 | 80 | // 导出 axios 实例 81 | export default service; 82 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icons/robots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/layout/main/classic.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | -------------------------------------------------------------------------------- /backend/celery_worker/scheduler/session.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """SQLAlchemy session.""" 3 | 4 | from contextlib import contextmanager 5 | 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import sessionmaker 9 | from sqlalchemy.pool import NullPool 10 | 11 | from kombu.utils.compat import register_after_fork 12 | 13 | ModelBase = declarative_base() 14 | 15 | 16 | @contextmanager 17 | def session_cleanup(session): 18 | try: 19 | yield 20 | except Exception: 21 | session.rollback() 22 | raise 23 | finally: 24 | session.close() 25 | 26 | 27 | def _after_fork_cleanup_session(session): 28 | session._after_fork() 29 | 30 | 31 | class SessionManager(object): 32 | """Manage SQLAlchemy sessions.""" 33 | 34 | def __init__(self): 35 | self._engines = {} 36 | self._sessions = {} 37 | self.forked = False 38 | self.prepared = False 39 | if register_after_fork is not None: 40 | register_after_fork(self, _after_fork_cleanup_session) 41 | 42 | def _after_fork(self): 43 | self.forked = True 44 | 45 | def get_engine(self, dburi, **kwargs): 46 | if self.forked: 47 | try: 48 | return self._engines[dburi] 49 | except KeyError: 50 | engine = self._engines[dburi] = create_engine(dburi, **kwargs) 51 | return engine 52 | else: 53 | return create_engine(dburi, poolclass=NullPool) 54 | 55 | def create_session(self, dburi, short_lived_sessions=False, **kwargs): 56 | engine = self.get_engine(dburi, **kwargs) 57 | if self.forked: 58 | if short_lived_sessions or dburi not in self._sessions: 59 | self._sessions[dburi] = sessionmaker(bind=engine) 60 | return engine, self._sessions[dburi] 61 | else: 62 | return engine, sessionmaker(bind=engine) 63 | 64 | def prepare_models(self, engine): 65 | if not self.prepared: 66 | ModelBase.metadata.create_all(engine) 67 | self.prepared = True 68 | 69 | def session_factory(self, dburi, **kwargs): 70 | engine, session = self.create_session(dburi, **kwargs) 71 | self.prepare_models(engine) 72 | return session() 73 | -------------------------------------------------------------------------------- /frontend/src/theme/media/chart.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .big-data-down-left { 7 | width: 100% !important; 8 | flex-direction: unset !important; 9 | flex-wrap: wrap; 10 | .flex-warp-item { 11 | min-height: 196.24px; 12 | padding: 0 7.5px 15px 15px !important; 13 | .flex-warp-item-box { 14 | border: none !important; 15 | border-bottom: 1px solid #ebeef5 !important; 16 | } 17 | } 18 | } 19 | .big-data-down-center { 20 | width: 100% !important; 21 | .big-data-down-center-one, 22 | .big-data-down-center-two { 23 | min-height: 196.24px; 24 | padding-left: 15px !important; 25 | .big-data-down-center-one-content { 26 | border: none !important; 27 | border-bottom: 1px solid #ebeef5 !important; 28 | } 29 | .flex-warp-item-box { 30 | @extend .big-data-down-center-one-content; 31 | } 32 | } 33 | } 34 | .big-data-down-right { 35 | .flex-warp-item { 36 | .flex-warp-item-box { 37 | border: none !important; 38 | border-bottom: 1px solid #ebeef5 !important; 39 | } 40 | &:nth-of-type(2) { 41 | padding-left: 15px !important; 42 | } 43 | &:last-of-type { 44 | .flex-warp-item-box { 45 | border: none !important; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | /* 页面宽度大于768px小于1200px 53 | ------------------------------- */ 54 | @media screen and (min-width: $sm) and (max-width: $lg) { 55 | .chart-warp-bottom { 56 | .big-data-down-left { 57 | width: 50% !important; 58 | } 59 | .big-data-down-center { 60 | width: 50% !important; 61 | } 62 | .big-data-down-right { 63 | .flex-warp-item { 64 | width: 50% !important; 65 | &:nth-of-type(2) { 66 | padding-left: 7.5px !important; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | /* 页面宽度小于1200px 74 | ------------------------------- */ 75 | @media screen and (max-width: $lg) { 76 | .chart-warp-top { 77 | .up-left { 78 | display: none; 79 | } 80 | } 81 | .chart-warp-bottom { 82 | overflow-y: auto !important; 83 | flex-wrap: wrap; 84 | .big-data-down-right { 85 | width: 100% !important; 86 | flex-direction: unset !important; 87 | flex-wrap: wrap; 88 | .flex-warp-item { 89 | min-height: 196.24px; 90 | padding: 0 7.5px 15px 15px !important; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /frontend/src/layout/main/columns.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | -------------------------------------------------------------------------------- /frontend/src/layout/main/defaults.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 76 | -------------------------------------------------------------------------------- /frontend/src/layout/component/main.vue: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 71 | -------------------------------------------------------------------------------- /backend/app/apis/system/notify.py: -------------------------------------------------------------------------------- 1 | # import traceback 2 | # 3 | # from flask import Blueprint, request 4 | # from loguru import logger 5 | # 6 | # from app.exc import codes 7 | # from app.forms.form import SendMessageSchema 8 | # from app.models.sys_models import Notify, User 9 | # from app.services.system_service.notify_service import notify 10 | # from app.utils.api import partner_success, login_verification, parse_pagination 11 | # 12 | # bp = Blueprint('notify', __name__, url_prefix='/api') 13 | # 14 | # 15 | # @bp.route('/notifyList', methods=['POST']) 16 | # @login_verification 17 | # @json_required 18 | # def notify_list(): 19 | # parsed_data = request.json 20 | # user_id = parsed_data.get('user_id', None) 21 | # send_status = parsed_data.get('send_status', None) 22 | # read_status = parsed_data.get('read_status', None) 23 | # data = parse_pagination(Notify.get_list(user_id=user_id, send_status=send_status, read_status=read_status)) 24 | # _result, pagination = data.get('result'), data.get('pagination') 25 | # _result = SendMessageSchema().dump(_result, many=True) 26 | # result = { 27 | # 'rows': _result 28 | # } 29 | # result.update(pagination) 30 | # return HttpResponse.successdata=result) 31 | # 32 | # 33 | # @bp.route('/notifyInfo', methods=['POST']) 34 | # @login_verification 35 | # @json_required 36 | # def get_notify_info(): 37 | # parsed_data = request.json 38 | # n_id = parsed_data.get('id', None) 39 | # notify_info = Notify.get(n_id) 40 | # if notify_info: 41 | # notify_info.read_status = 20 42 | # notify_info.save() 43 | # n_info = SendMessageSchema().dump(notify_info) 44 | # return HttpResponse.successn_info) 45 | # 46 | # 47 | # @bp.route('/sendMessage', methods=['POST']) 48 | # @login_verification 49 | # def send_message(): 50 | # """ 51 | # :return: 52 | # """ 53 | # parsed_data = request.json 54 | # message = parsed_data.get('message', None) 55 | # group = parsed_data.get('group', 'notify') 56 | # user_ids = parsed_data.get('user_ids', []) 57 | # try: 58 | # data = notify.send_message(group=group, message=message, user_ids=user_ids) 59 | # return data 60 | # except Exception as err: 61 | # logger.error(traceback.format_exc()) 62 | # return HttpResponse.successcode=codes.PARTNER_CODE_FAIL, msg=str(err)) 63 | # 64 | # 65 | # @bp.route('/getGroupInfo', methods=['POST']) 66 | # @login_verification 67 | # def get_group_info(): 68 | # """ 69 | # :return: 70 | # """ 71 | # data = notify.get_group_info() 72 | # return data 73 | -------------------------------------------------------------------------------- /frontend/src/views/error/401.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 401 8 | 您未被授权,没有操作权限~ 9 | 10 | 重新授权 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 34 | 98 | -------------------------------------------------------------------------------- /frontend/src/theme/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3756939 */ 3 | src: url('iconfont.woff2?t=1715062237868') format('woff2'), 4 | url('iconfont.woff?t=1715062237868') format('woff'), 5 | url('iconfont.ttf?t=1715062237868') format('truetype'), 6 | url('iconfont.svg?t=1715062237868#iconfont') format('svg'); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-UI:before { 18 | content: "\e699"; 19 | } 20 | 21 | .icon-suffix-sql:before { 22 | content: "\e624"; 23 | } 24 | 25 | .icon-a-case-o1:before { 26 | content: "\e625"; 27 | } 28 | 29 | .icon-fenzhijiedian:before { 30 | content: "\e622"; 31 | } 32 | 33 | .icon-c158API:before { 34 | content: "\e623"; 35 | } 36 | 37 | .icon-fuhao-ziti:before { 38 | content: "\e61d"; 39 | } 40 | 41 | .icon-tuichuquanping:before { 42 | content: "\e61e"; 43 | } 44 | 45 | .icon-zhuti:before { 46 | content: "\e61f"; 47 | } 48 | 49 | .icon-quanping:before { 50 | content: "\e620"; 51 | } 52 | 53 | .icon-github:before { 54 | content: "\e621"; 55 | } 56 | 57 | .icon-setting:before { 58 | content: "\e61c"; 59 | } 60 | 61 | .icon-remove:before { 62 | content: "\e61a"; 63 | } 64 | 65 | .icon-add_circle_outline_black_24dp:before { 66 | content: "\e61b"; 67 | } 68 | 69 | .icon-add_circle_black_48dp:before { 70 | content: "\e618"; 71 | } 72 | 73 | .icon-remove_circle_black_48dp:before { 74 | content: "\e619"; 75 | } 76 | 77 | .icon-favorite:before { 78 | content: "\e613"; 79 | } 80 | 81 | .icon-alarm_FILL0_wght400_GRAD0_opsz48:before { 82 | content: "\e614"; 83 | } 84 | 85 | .icon-drive_file:before { 86 | content: "\e615"; 87 | } 88 | 89 | .icon-alt_route:before { 90 | content: "\e616"; 91 | } 92 | 93 | .icon-text_fields:before { 94 | content: "\e617"; 95 | } 96 | 97 | .icon-add:before { 98 | content: "\e612"; 99 | } 100 | 101 | .icon-case-o:before { 102 | content: "\e60e"; 103 | } 104 | 105 | .icon-sql:before { 106 | content: "\e60f"; 107 | } 108 | 109 | .icon-changjingguanli:before { 110 | content: "\e610"; 111 | } 112 | 113 | .icon-time:before { 114 | content: "\e611"; 115 | } 116 | 117 | .icon-fenzhi:before { 118 | content: "\e60b"; 119 | } 120 | 121 | .icon-loop:before { 122 | content: "\e60c"; 123 | } 124 | 125 | .icon-code:before { 126 | content: "\e60a"; 127 | } 128 | 129 | -------------------------------------------------------------------------------- /frontend/src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | interface TreeConfigOptions { 2 | // 子属性的名称,默认为'children' 3 | childProps: string; 4 | } 5 | 6 | /** 7 | * @zh_CN 遍历树形结构,并返回所有节点中指定的值。 8 | * @param tree 树形结构数组 9 | * @param getValue 获取节点值的函数 10 | * @param options 作为子节点数组的可选属性名称。 11 | * @returns 所有节点中指定的值的数组 12 | */ 13 | function traverseTreeValues( 14 | tree: T[], 15 | getValue: (node: T) => V, 16 | options?: TreeConfigOptions, 17 | ): V[] { 18 | const result: V[] = []; 19 | const { childProps } = options || { 20 | childProps: 'children', 21 | }; 22 | 23 | const dfs = (treeNode: T) => { 24 | const value = getValue(treeNode); 25 | result.push(value); 26 | const children = (treeNode as Record)?.[childProps]; 27 | if (!children) { 28 | return; 29 | } 30 | if (children.length > 0) { 31 | for (const child of children) { 32 | dfs(child); 33 | } 34 | } 35 | }; 36 | 37 | for (const treeNode of tree) { 38 | dfs(treeNode); 39 | } 40 | return result.filter(Boolean); 41 | } 42 | 43 | /** 44 | * 根据条件过滤给定树结构的节点,并以原有顺序返回所有匹配节点的数组。 45 | * @param tree 要过滤的树结构的根节点数组。 46 | * @param filter 用于匹配每个节点的条件。 47 | * @param options 作为子节点数组的可选属性名称。 48 | * @returns 包含所有匹配节点的数组。 49 | */ 50 | function filterTree>( 51 | tree: T[], 52 | filter: (node: T) => boolean, 53 | options?: TreeConfigOptions, 54 | ): T[] { 55 | const { childProps } = options || { 56 | childProps: 'children', 57 | }; 58 | 59 | const _filterTree = (nodes: T[]): T[] => { 60 | return nodes.filter((node: Record) => { 61 | if (filter(node as T)) { 62 | if (node[childProps]) { 63 | node[childProps] = _filterTree(node[childProps]); 64 | } 65 | return true; 66 | } 67 | return false; 68 | }); 69 | }; 70 | 71 | return _filterTree(tree); 72 | } 73 | 74 | /** 75 | * 根据条件重新映射给定树结构的节 76 | * @param tree 要过滤的树结构的根节点数组。 77 | * @param mapper 用于map每个节点的条件。 78 | * @param options 作为子节点数组的可选属性名称。 79 | */ 80 | function mapTree>( 81 | tree: T[], 82 | mapper: (node: T) => V, 83 | options?: TreeConfigOptions, 84 | ): V[] { 85 | const { childProps } = options || { 86 | childProps: 'children', 87 | }; 88 | return tree.map((node) => { 89 | const mapperNode: Record = mapper(node); 90 | if (mapperNode[childProps]) { 91 | mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options); 92 | } 93 | return mapperNode as V; 94 | }); 95 | } 96 | 97 | export { filterTree, mapTree, traverseTreeValues }; 98 | -------------------------------------------------------------------------------- /backend/app/corelibs/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | import os 4 | import sys 5 | import logging 6 | from loguru import logger 7 | 8 | from config import config 9 | from app.corelibs.local import g 10 | from app.utils import create_dir 11 | 12 | # 创建日志文件名 13 | from app.utils.common import get_str_uuid 14 | 15 | 16 | def logger_file() -> str: 17 | """ 创建日志文件名 """ 18 | log_path = create_dir(config.LOGGER_DIR) 19 | 20 | """ 保留日志文件夹下最大个数(本地调试用) 21 | 本地调式需要多次重启, 日志轮转片不会生效 """ 22 | file_list = os.listdir(log_path) 23 | if len(file_list) > 3: 24 | os.remove(os.path.join(log_path, file_list[0])) 25 | 26 | # 日志输出路径 27 | return os.path.join(log_path, config.LOGGER_NAME) 28 | 29 | 30 | def correlation_id_filter(record): 31 | if not g.trace_id: 32 | g.trace_id = get_str_uuid() 33 | record['trace_id'] = g.trace_id 34 | return record 35 | 36 | 37 | # 详见: https://loguru.readthedocs.io/en/stable/overview.html#features 38 | fmt = "{time:YYYY-MM-DD HH:mm:ss.SSS}| {thread} | {level: <8} | {trace_id} | {name}:{function}:{line} | {message}" 39 | logger.remove() 40 | logger.add( 41 | # logger_file(), 42 | sys.stdout, 43 | # encoding=config.GLOBAL_ENCODING, 44 | level=config.LOGGER_LEVEL, 45 | colorize=True, 46 | # rotation=config.LOGGER_ROTATION, 47 | # retention=config.LOGGER_RETENTION, 48 | filter=correlation_id_filter, 49 | format=fmt, 50 | # enqueue=True 51 | ) 52 | 53 | 54 | class InterceptHandler(logging.Handler): 55 | def emit(self, record): 56 | # Get corresponding Loguru level if it exists 57 | try: 58 | level = logger.level(record.levelname).name 59 | except ValueError: 60 | level = record.levelno 61 | 62 | logger_opt = logger.opt(depth=6, exception=record.exc_info) 63 | logger_opt.log(level, record.getMessage()) 64 | 65 | 66 | def init_logger(): 67 | logger_name_list = [name for name in logging.root.manager.loggerDict] 68 | 69 | for logger_name in logger_name_list: 70 | """获取所有logger""" 71 | effective_level = logging.getLogger(logger_name).getEffectiveLevel() 72 | if effective_level < logging.getLevelName(config.LOGGER_LEVEL.upper()): 73 | logging.getLogger(logger_name).setLevel(config.LOGGER_LEVEL.upper()) 74 | if '.' not in logger_name: 75 | logging.getLogger(logger_name).handlers = [] 76 | logging.getLogger(logger_name).addHandler(InterceptHandler()) 77 | -------------------------------------------------------------------------------- /backend/app/utils/serialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @author: xiaobai 3 | from datetime import datetime 4 | import typing 5 | from fastapi.encoders import jsonable_encoder 6 | from sqlalchemy import Select, select, func, literal_column, Row 7 | from sqlalchemy.orm import noload, DeclarativeMeta 8 | 9 | T = typing.TypeVar("T", Select, "Query[Any]") 10 | 11 | 12 | def count_query(query: Select) -> Select: 13 | """ 14 | 获取count sql 15 | :param query: sql 16 | :return: 17 | """ 18 | count_subquery = typing.cast(typing.Any, query.order_by(None)).options(noload("*")).subquery() 19 | return select(func.count(literal_column("*"))).select_from(count_subquery) 20 | 21 | 22 | def paginate_query(query: T, page: int, page_size: int) -> T: 23 | """ 24 | 获取分页sql 25 | :param query: 26 | :param page: 页数 27 | :param page_size: 每页大小 28 | :return: 29 | """ 30 | return query.limit(page_size).offset(page_size * (page - 1)) 31 | 32 | 33 | def len_or_none(obj: typing.Any) -> typing.Optional[int]: 34 | """有数据返回长度 没数据返回None""" 35 | try: 36 | return len(obj) 37 | except TypeError: 38 | return None 39 | 40 | 41 | def unwrap_scalars(items: typing.Union[typing.Sequence[Row], Row]) -> typing.Union[ 42 | typing.List[typing.Dict[typing.Text, typing.Any]], typing.Dict[str, typing.Any]]: 43 | """ 44 | 数据库Row对象数据序列化为字典 45 | :param items: 数据返回数据 [Row(...)] 46 | :return: 47 | """ 48 | if isinstance(items, typing.Iterable) and not isinstance(items, Row): 49 | return [default_serialize(item) for item in items] 50 | return default_serialize(items) 51 | 52 | 53 | def default_serialize(obj): 54 | """默认序序列化""" 55 | try: 56 | if isinstance(obj, int) and len(str(obj)) > 15: 57 | return str(obj) 58 | if isinstance(obj, dict): 59 | return {key: default_serialize(value) for key, value in obj.items()} 60 | if isinstance(obj, list): 61 | return [default_serialize(i) for i in obj] 62 | if isinstance(obj, datetime): 63 | return obj.strftime("%Y-%m-%d %H:%M:%S") 64 | if isinstance(obj, Row): 65 | data = dict(zip(obj._fields, obj._data)) 66 | return {key: default_serialize(value) for key, value in data.items()} 67 | if hasattr(obj, "__class__") and isinstance(obj.__class__, DeclarativeMeta): 68 | return {c.name: default_serialize(getattr(obj, c.name)) for c in obj.__table__.columns} 69 | if isinstance(obj, typing.Callable): 70 | return repr(obj) 71 | return jsonable_encoder(obj) 72 | except TypeError as err: 73 | return repr(obj) 74 | -------------------------------------------------------------------------------- /frontend/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | // 通用函数 2 | import useClipboard from 'vue-clipboard3'; 3 | import {ElMessage} from 'element-plus'; 4 | import {formatDate} from '/@/utils/formatTime'; 5 | import MersenneTwister from './mersenneTwister.js' 6 | 7 | 8 | export const {toClipboard} = useClipboard(); 9 | 10 | // 百分比格式化 11 | export const percentFormat = (row: EmptyArrayType, column: number, cellValue: string) => { 12 | return cellValue ? `${cellValue}%` : '-'; 13 | }; 14 | 15 | 16 | // 列表日期时间格式化 17 | export const dateFormatYMD = (row: EmptyArrayType, column: number, cellValue: string) => { 18 | if (!cellValue) return '-'; 19 | return formatDate(new Date(cellValue), 'YYYY-mm-dd'); 20 | }; 21 | // 列表日期时间格式化 22 | export const dateFormatYMDHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 23 | if (!cellValue) return '-'; 24 | return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS'); 25 | }; 26 | // 列表日期时间格式化 27 | export const dateFormatHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 28 | if (!cellValue) return '-'; 29 | let time = 0; 30 | if (typeof row === 'number') time = row; 31 | if (typeof cellValue === 'number') time = cellValue; 32 | return formatDate(new Date(time * 1000), 'HH:MM:SS'); 33 | }; 34 | // 小数格式化 35 | export const scaleFormat = (value: string = '0', scale: number = 4) => { 36 | return Number.parseFloat(value).toFixed(scale); 37 | }; 38 | // 小数格式化 39 | export const scale2Format = (value: string = '0') => { 40 | return Number.parseFloat(value).toFixed(2); 41 | }; 42 | // 点击复制文本 43 | export const copyText = (text: string, message?: string | null) => { 44 | return new Promise((resolve, reject) => { 45 | try { 46 | // 复制 47 | toClipboard(text); 48 | // 下面可以设置复制成功的提示框等操作 49 | ElMessage.success(message || '复制成功!'); 50 | resolve(text); 51 | } catch (e) { 52 | // 复制失败 53 | ElMessage.error('复制失败!'); 54 | reject(e); 55 | } 56 | }); 57 | }; 58 | 59 | export const getUuid = () => { 60 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 61 | var r = (Math.random() * 16) | 0, 62 | v = c == 'x' ? r : (r & 0x3) | 0x8; 63 | return v.toString(16); 64 | }); 65 | }; 66 | 67 | 68 | // 根据内容生成颜色 69 | export const generateColorByContent = (str: string) => { 70 | let hash = 0 71 | for (let i = 0; i < str.length; i++) { 72 | hash = str.charCodeAt(i) + ((hash << 5) - hash) 73 | } 74 | // 这里使用伪随机数的原因是因为 75 | // 1. 如果字符串的内容差不多,根据hash生产的颜色就比较相近,不好区分,比如v1.1 v1.2,所以需要加入随机数来使得颜色能够区分开 76 | // 2. 普通的随机数每次数值不一样,就会导致每次新增标签原来的标签颜色就会发生改变,所以加入了这个方法,使得内容不变随机数也不变 77 | const rng = new MersenneTwister(hash) 78 | const h = rng.genrand_int32() % 360 79 | return 'hsla(' + h + ', 50%, 50%, 1)' 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 🌈 介绍 2 | 3 | #### 后端 4 | - 基于 python + fastApi + celery + sqlalchemy + redis 5 | 6 | - 使用软件版本 7 | - python version <= 3.13 8 | - mysql version 8.0.23 9 | - redis version 6.0.9 10 | - node version 18.15.0 11 | 12 | #### 前端 13 | 14 | - 基于 vite + vue3 + element-plus 15 | 16 | - 使用软件版本 17 | - node version 18.15.0 18 | - vue version 3.2.45 19 | - element-plus version 2.2.26 20 | 21 | 22 | #### 💒 平台地址地址 23 | - github 24 | https://github.com/baizunxian/fast-element-admin 25 | - gitee 26 | https://gitee.com/xb_xiaobai/fast-element-admin 27 | 28 | #### ⛱️ 线上预览 29 | 30 | - ZERO AUTOTEST 31 | 自动化测试平台在线预览 https://zerorunner.cn 32 | 33 | - 首页 34 |  35 | - 报告页面 36 |  37 | - 自定义函数 38 |  39 | 40 | #### 🚧 项目启动初始化-后端 41 | 42 | ```bash 43 | # 克隆项目 44 | git clone https://github.com/baizunxian/vue-fastapi-admin.git 45 | 46 | # 数据库脚本 将内容复制数据库执行 需要新建数据库 zerorunner 47 | backend/script/db_init.sql 48 | 49 | # 修改对应的数据库地址,redis 地址 50 | backend/config.py 51 | # 或者 52 | backend/.env # 环境文件中的地址修改 53 | 54 | # 安装依赖 55 | pip install -r requirements 56 | 57 | # 运行项目 zerorunner/backend 目录下执行 58 | python main.py 59 | 60 | # 异步任务依赖 celery 启动命令 61 | 62 | # windows 启动,只能单线程 zerorunner/backend 目录下执行 63 | celery -A celery_worker.worker.celery worker --pool=solo -l INFO 64 | 65 | # linux 启动 66 | elery -A celery_worker.worker.celery worker --loglevel=INFO -c 10 -P solo -n zerorunner-celery-worker 67 | 68 | # 定时任务启动 69 | celery -A celery_worker.worker.celery beat -S celery_worker.scheduler.schedulers:DatabaseScheduler -l INFO 70 | 71 | # 定时任务心跳启动 72 | celery -A celery_worker.worker.celery beat -l INFO 73 | 74 | ``` 75 | 76 | #### 🚧 项目启动初始化-前端 77 | 78 | ```bash 79 | # node 版本 80 | node -v 81 | v18.15.0 82 | ``` 83 | 84 | - 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org` 85 | - 复制代码(桌面 cmd 运行) `npm install -g yarn` 86 | 87 | ```bash 88 | # 克隆项目 89 | git clone https://github.com/baizunxian/vue-fastapi-admin.git 90 | 91 | # 进入项目 92 | cd zerorunner/frontend 93 | 94 | # 安装依赖 95 | cnpm install 96 | # 或者 97 | yarn insatll 98 | 99 | # 运行项目 100 | cnpm run dev 101 | # 或者 102 | yarn dev 103 | 104 | # 打包发布 105 | cnpm run build 106 | # 或者 107 | yarn build 108 | ``` 109 | 110 | #### 💯 学习交流加 微信 群 111 | 112 | - 或者添加我的微信,我可以拉你们进入交流群 113 |  114 | 115 | #### 💌 支持作者 116 | 117 | 如果觉得框架不错,或者已经在使用了,希望你可以去 118 | Github 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持, 平台会持续迭代更新。 119 | --------------------------------------------------------------------------------