first commit

This commit is contained in:
mizhexiaxiao 2023-08-15 16:03:24 +08:00 committed by mizhexiaoxiao
parent 03efc0558d
commit 0586412ffa
167 changed files with 16669 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
__pycache__/
.idea/
venv/
.mypy_cache/
.vscode
.ruff_cache/
.pytest_cache/
db.sqlite3
db.sqlite3-journal
db.sqlite3-shm
db.sqlite3-wal
.DS_Store

80
Makefile Normal file
View File

@ -0,0 +1,80 @@
# Build configuration
# -------------------
APP_NAME := `sed -n 's/^ *name.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
APP_VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
GIT_REVISION = `git rev-parse HEAD`
# Introspection targets
# ---------------------
.PHONY: help
help: header targets
.PHONY: header
header:
@echo "\033[34mEnvironment\033[0m"
@echo "\033[34m---------------------------------------------------------------\033[0m"
@printf "\033[33m%-23s\033[0m" "APP_NAME"
@printf "\033[35m%s\033[0m" $(APP_NAME)
@echo ""
@printf "\033[33m%-23s\033[0m" "APP_VERSION"
@printf "\033[35m%s\033[0m" $(APP_VERSION)
@echo ""
@printf "\033[33m%-23s\033[0m" "GIT_REVISION"
@printf "\033[35m%s\033[0m" $(GIT_REVISION)
@echo "\n"
.PHONY: targets
targets:
@echo "\033[34mDevelopment Targets\033[0m"
@echo "\033[34m---------------------------------------------------------------\033[0m"
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}'
# Development targets
# -------------
.PHONY: install
install: ## Install dependencies
poetry install
.PHONY: run
run: start
.PHONY: start
start: ## Starts the server
$(eval include .env)
$(eval export $(sh sed 's/=.*//' .env))
poetry run python run.py
# Check, lint and format targets
# ------------------------------
.PHONY: check
check: check-format lint
.PHONY: check-format
check-format: ## Dry-run code formatter
poetry run black ./ --check
poetry run isort ./ --profile black --check
.PHONY: lint
lint: ## Run ruff
poetry run ruff check ./app
.PHONY: format
format: ## Run code formatter
poetry run black ./
poetry run isort ./ --profile black
.PHONY: check-lockfile
check-lockfile: ## Compares lock file with pyproject.toml
poetry lock --check
.PHONY: test
test: ## Run the test suite
$(eval include .env)
$(eval export $(sh sed 's/=.*//' .env))
poetry run pytest -vv -s --cache-clear ./

39
app/__init__.py Normal file
View File

@ -0,0 +1,39 @@
from fastapi import FastAPI
from app.core.exceptions import SettingNotFound
from app.core.init_app import (
init_menus,
init_superuser,
make_middlewares,
register_db,
register_exceptions,
register_routers,
)
try:
from app.settings.config import settings
except ImportError:
raise SettingNotFound("Can not import settings. Create settings file from template.config.py")
def create_app() -> FastAPI:
app = FastAPI(
title=settings.APP_TITLE,
description=settings.APP_DESCRIPTION,
version=settings.VERSION,
openapi_url="/openapi.json",
middleware=make_middlewares(),
)
register_db(app)
register_exceptions(app)
register_routers(app, prefix="/api")
return app
app = create_app()
@app.on_event("startup")
async def startup_event():
await init_superuser()
await init_menus()

9
app/api/__init__.py Normal file
View File

@ -0,0 +1,9 @@
from fastapi import APIRouter
from .v1 import v1_router
api_router = APIRouter()
api_router.include_router(v1_router, prefix="/v1")
__all__ = ["api_router"]

17
app/api/v1/__init__.py Normal file
View File

@ -0,0 +1,17 @@
from fastapi import APIRouter
from app.core.dependency import DependPermisson
from .apis import apis_router
from .base import base_router
from .menus import menus_router
from .roles import roles_router
from .users import users_router
v1_router = APIRouter()
v1_router.include_router(base_router, prefix="/base")
v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermisson])
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermisson])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson])

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .apis import router
apis_router = APIRouter()
apis_router.include_router(router, tags=["API模块"])
__all__ = ["apis_router"]

101
app/api/v1/apis/apis.py Normal file
View File

@ -0,0 +1,101 @@
from fastapi import APIRouter, Query
from fastapi.routing import APIRoute
from tortoise.expressions import Q
from app.controllers.api import api_controller
from app.log import logger
from app.models.admin import Api
from app.schemas import Success, SuccessExtra
from app.schemas.apis import *
router = APIRouter()
@router.get("/list", summary="查看API列表")
async def list_api(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
path: str = Query(None, description="API路径"),
summary: str = Query(None, description="API简介"),
tags: str = Query(None, description="API模块"),
):
q = Q()
if path:
q &= Q(path__contains=path)
if summary:
q &= Q(summary__contains=summary)
if tags:
q &= Q(tags__contains=tags)
total, api_objs = await api_controller.list(page=page, page_size=page_size, search=q, order=["id"])
result = []
for api in api_objs:
api_dict = await api.to_dict(m2m=False)
result.append(api_dict)
return SuccessExtra(data=result, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看Api")
async def get_api(
id: int = Query(..., description="Api"),
):
api_obj = await api_controller.get(id=id)
api_dict = await api_obj.to_dict()
return Success(code=200, data=api_dict)
@router.post("/create", summary="创建Api")
async def create_api(
api_in: ApiCreate,
):
new_api = await api_controller.create(obj_in=api_in)
return Success(msg="Created Successfully", data=new_api)
@router.post("/update", summary="更新Api")
async def update_api(
api_in: ApiUpdate,
):
await api_controller.update(id=api_in.id, obj_in=api_in.update_dict())
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除Api")
async def delete_api(
api_id: int = Query(..., description="ApiID"),
):
await api_controller.remove(id=api_id)
return Success(msg="Deleted Success")
@router.post("/refresh", summary="刷新API列表")
async def refresh_api():
from app import app
# 删除废弃API数据
all_api_list = []
for route in app.routes:
if isinstance(route, APIRoute):
all_api_list.append((list(route.methods)[0], route.path_format))
delete_api = []
for api in await Api.all():
if (api.method, api.path) not in all_api_list:
delete_api.append((api.method, api.path))
for item in delete_api:
method, path = item
logger.debug(f"API Deleted {method} {path}")
await Api.filter(method=method, path=path).delete()
for route in app.routes:
if isinstance(route, APIRoute):
method = list(route.methods)[0]
path = route.path_format
summary = route.summary
tags = list(route.tags)[0]
api_obj = await Api.filter(method=method, path=path).first()
if api_obj:
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
else:
logger.debug(f"API Created {method} {path}")
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
return Success(msg="OK")

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .base import router
base_router = APIRouter()
base_router.include_router(router, tags=["基础模块"])
__all__ = ["base_router"]

106
app/api/v1/base/base.py Normal file
View File

@ -0,0 +1,106 @@
from datetime import datetime, timedelta
from fastapi import APIRouter
from app.controllers.user import UserController, user_controller
from app.core.ctx import CTX_USER_ID
from app.core.dependency import DependAuth
from app.models.admin import Api, Menu, Role, User
from app.schemas.base import BaseResponse, Fail, Success
from app.schemas.login import *
from app.schemas.users import UpdatePassword
from app.settings import settings
from app.utils.jwt import create_access_token
from app.utils.password import get_password_hash, verify_password
router = APIRouter()
@router.post("/access_token", summary="获取token")
async def login_access_token(credentials: CredentialsSchema) -> BaseResponse:
user: User = await user_controller.authenticate(credentials)
await user_controller.update_last_login(user.id)
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + access_token_expires
result = JWTOut(
access_token=create_access_token(
data=JWTPayload(
user_id=user.id,
username=user.username,
is_superuser=user.is_superuser,
exp=expire,
)
),
username=user.username,
)
return BaseResponse(code=200, data=result)
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth])
async def get_userinfo():
user_id = CTX_USER_ID.get()
user_obj = await user_controller.get(id=user_id)
to_dict = await user_obj.to_dict()
to_dict.pop("password")
to_dict["avatar"] = "https://avatars.githubusercontent.com/u/54677442?v=4"
return BaseResponse(code=200, data=to_dict)
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth])
async def get_user_menu() -> BaseResponse:
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
menus: list[Menu] = []
if user_obj.is_superuser:
menus = await Menu.all()
else:
role_objs: list[Role] = await user_obj.roles
for role_obj in role_objs:
menu = await role_obj.menus
menus.extend(menu)
menus = list(set(menus))
parent_menus: list[Menu] = []
for menu in menus:
if menu.parent_id == 0:
parent_menus.append(menu)
res = []
for parent_menu in parent_menus:
parent_menu_dict = await parent_menu.to_dict()
parent_menu_dict["children"] = []
for menu in menus:
if menu.parent_id == parent_menu.id:
parent_menu_dict["children"].append(await menu.to_dict())
res.append(parent_menu_dict)
return Success(data=res)
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth])
async def get_user_api() -> BaseResponse:
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
if user_obj.is_superuser:
api_objs: list[Api] = await Api.all()
apis = [api.method.lower() + api.path for api in api_objs]
return Success(data=apis)
role_objs: list[Role] = await user_obj.roles
apis = []
for role_obj in role_objs:
api_objs: list[Api] = await role_obj.apis
apis.extend([api.method.lower() + api.path for api in api_objs])
apis = list(set(apis))
return Success(data=apis)
@router.post("/update_password", summary="更新用户密码", dependencies=[DependAuth])
async def update_user_password(req_in: UpdatePassword) -> BaseResponse:
user_controller = UserController()
user = await user_controller.get(req_in.id)
verified = verify_password(req_in.old_password, user.password)
if not verified:
return Fail(msg="旧密码验证错误!")
user.password = get_password_hash(req_in.new_password)
await user.save()
return Success(msg="修改成功")

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .menus import router
menus_router = APIRouter()
menus_router.include_router(router, tags=["菜单模块"])
__all__ = ["menus_router"]

61
app/api/v1/menus/menus.py Normal file
View File

@ -0,0 +1,61 @@
import logging
from fastapi import APIRouter, Query
from app.controllers.menu import menu_controller
from app.schemas.base import BaseResponse, Fail, Success
from app.schemas.menus import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看菜单列表")
async def list_menu(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
) -> MenuOutList:
parent_menus = await menu_controller.model.filter(parent_id=0).order_by("order")
res_menu = []
for menu in parent_menus:
child_menu = await menu_controller.model.filter(parent_id=menu.id).order_by("order")
menu_dict = await menu.to_dict()
menu_dict["children"] = child_menu
res_menu.append(menu_dict)
return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size)
@router.get("/get", summary="查看菜单")
async def get_menu(
menu_id: int = Query(..., description="菜单id"),
) -> BaseResponse:
result = await menu_controller.get(id=menu_id)
return Success(data=result)
@router.post("/create", summary="创建菜单")
async def create_menu(
menu_in: MenuCreate,
) -> BaseResponse:
await menu_controller.create(obj_in=menu_in)
return Success(msg="Created Success")
@router.post("/update", summary="更新菜单")
async def update_menu(
menu_in: MenuUpdate,
) -> BaseResponse:
await menu_controller.update(id=menu_in.id, obj_in=menu_in.update_dict())
return Success(msg="Updated Success")
@router.delete("/delete", summary="删除菜单")
async def delete_menu(
id: int = Query(..., description="菜单id"),
) -> BaseResponse:
child_menu_count = await menu_controller.model.filter(parent_id=id).count()
if child_menu_count > 0:
return Fail(msg="Cannot delete a menu with child menus")
await menu_controller.remove(id=id)
return Success(msg="Deleted Success")

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .roles import router
roles_router = APIRouter()
roles_router.include_router(router, tags=["角色模块"])
__all__ = ["roles_router"]

74
app/api/v1/roles/roles.py Normal file
View File

@ -0,0 +1,74 @@
import logging
from fastapi import APIRouter, Query
from fastapi.exceptions import HTTPException
from tortoise.expressions import Q
from app.controllers import role_controller
from app.schemas.base import BaseResponse, Success, SuccessExtra
from app.schemas.roles import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看角色列表")
async def list_role(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
role_name: str = Query("", description="角色名称,用于查询"),
) -> RoleOutList:
q = Q()
if role_name:
q = Q(name__contains=role_name)
total, result = await role_controller.list(page=page, page_size=page_size, search=q)
return SuccessExtra(data=result, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看角色")
async def get_role(
role_id: int = Query(..., description="角色ID"),
) -> RoleOut:
role_obj = await role_controller.get(id=role_id)
return Success(data=role_obj)
@router.post("/create", summary="创建角色")
async def create_role(
role_in: RoleCreate,
) -> BaseResponse:
if await role_controller.is_exist(name=role_in.name):
raise HTTPException(
status_code=400,
detail="The role with this rolename already exists in the system.",
)
await role_controller.create(obj_in=role_in)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新角色")
async def update_role(role_in: RoleUpdate) -> BaseResponse:
await role_controller.update(id=role_in.id, obj_in=role_in.update_dict())
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除角色")
async def delete_role(
role_id: int = Query(..., description="角色ID"),
) -> BaseResponse:
await role_controller.remove(id=role_id)
return Success(msg="Deleted Success")
@router.get("/authorized", summary="查看角色权限")
async def get_role_authorized(id: int = Query(..., description="角色ID")) -> BaseResponse:
role_obj = await role_controller.get(id=id)
role_dict = await role_obj.to_dict()
return Success(data=role_dict)
@router.post("/authorized", summary="更新角色权限")
async def update_role_authorized(role_in: RoleUpdateMenusApis) -> BaseResponse:
role_obj = await role_controller.get(id=role_in.id)
await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
return Success(msg="Updated Successfully")

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .users import router
users_router = APIRouter()
users_router.include_router(router, tags=["用户模块"])
__all__ = ["users_router"]

81
app/api/v1/users/users.py Normal file
View File

@ -0,0 +1,81 @@
import logging
from fastapi import APIRouter, Query
from fastapi.exceptions import HTTPException
from tortoise.expressions import Q
from app.controllers.user import UserController
from app.core.dependency import DependPermisson
from app.schemas.base import BaseResponse, Success, SuccessExtra
from app.schemas.users import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看用户列表", dependencies=[DependPermisson])
async def list_user(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
username: str = Query("", description="用户名称,用于搜索"),
email: str = Query("", description="邮箱地址"),
) -> UserOutList:
user_controller = UserController()
q = Q()
if username:
q &= Q(username__contains=username)
if email:
q &= Q(email__contains=email)
total, user_objs = await user_controller.list(page=page, page_size=page_size, search=q)
result = []
for user in user_objs:
user_dict = await user.to_dict()
result.append(user_dict)
return SuccessExtra(data=result, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看用户", dependencies=[DependPermisson])
async def get_user(
user_id: int = Query(..., description="用户ID"),
) -> UserOut:
user_controller = UserController()
user_obj = await user_controller.get(id=user_id)
user_dict = await user_obj.to_dict()
return Success(code=200, data=user_dict)
@router.post("/create", summary="创建用户", dependencies=[DependPermisson])
async def create_user(
user_in: UserCreate,
) -> BaseResponse:
user_controller = UserController()
user = await user_controller.get_by_email(user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
new_user = await user_controller.create(obj_in=user_in)
await user_controller.update_roles(new_user, user_in.roles)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新用户", dependencies=[DependPermisson])
async def update_user(
user_in: UserUpdate,
) -> BaseResponse:
print(user_in.dict())
user_controller = UserController()
user = await user_controller.update(obj_in=user_in)
await user_controller.update_roles(user, user_in.roles)
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除用户", dependencies=[DependPermisson])
async def delete_user(
user_id: int = Query(..., description="用户ID"),
) -> BaseResponse:
user_controller = UserController()
await user_controller.remove(id=user_id)
return Success(msg="Deleted Successfully")

View File

@ -0,0 +1,2 @@
from .role import role_controller as role_controller
from .user import user_controller as user_controller

11
app/controllers/api.py Normal file
View File

@ -0,0 +1,11 @@
from app.core.crud import CRUDBase
from app.models.admin import Api
from app.schemas.apis import ApiCreate, ApiUpdate
class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
def __init__(self):
super().__init__(model=Api)
api_controller = ApiController()

16
app/controllers/menu.py Normal file
View File

@ -0,0 +1,16 @@
from typing import Optional
from app.core.crud import CRUDBase
from app.models.admin import Menu
from app.schemas.menus import MenuCreate, MenuUpdate
class MenuController(CRUDBase[Menu, MenuCreate, MenuUpdate]):
def __init__(self):
super().__init__(model=Menu)
async def get_by_menu_path(self, path: str) -> Optional["Menu"]:
return await self.model.filter(path=path).first()
menu_controller = MenuController()

27
app/controllers/role.py Normal file
View File

@ -0,0 +1,27 @@
from typing import List
from app.core.crud import CRUDBase
from app.models.admin import Api, Menu, Role
from app.schemas.roles import RoleCreate, RoleUpdate
class RoleController(CRUDBase[Role, RoleCreate, RoleUpdate]):
def __init__(self):
super().__init__(model=Role)
async def is_exist(self, name: str) -> bool:
return await self.model.filter(name=name).exists()
async def update_roles(self, role: Role, menu_ids: List[int], api_infos: List[dict]) -> None:
await role.menus.clear()
for menu_id in menu_ids:
menu_obj = await Menu.filter(id=menu_id).first()
await role.menus.add(menu_obj)
await role.apis.clear()
for item in api_infos:
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
await role.apis.add(api_obj)
role_controller = RoleController()

56
app/controllers/user.py Normal file
View File

@ -0,0 +1,56 @@
from datetime import datetime
from typing import List, Optional
from fastapi.exceptions import HTTPException
from app.core.crud import CRUDBase
from app.models.admin import User
from app.schemas.login import CredentialsSchema
from app.schemas.users import UserCreate, UserUpdate
from app.utils.password import get_password_hash, verify_password
from .role import role_controller
class UserController(CRUDBase[User, UserCreate, UserUpdate]):
def __init__(self):
super().__init__(model=User)
async def get_by_email(self, email: str) -> Optional[User]:
return await self.model.filter(email=email).first()
async def get_by_username(self, username: str) -> Optional[User]:
return await self.model.filter(username=username).first()
async def create(self, obj_in: UserCreate) -> User:
obj_in.password = get_password_hash(password=obj_in.password)
obj = await super().create(obj_in.create_dict())
return obj
async def update(self, obj_in: UserUpdate) -> User:
return await super().update(id=obj_in.id, obj_in=obj_in.update_dict())
async def update_last_login(self, id: int) -> None:
user = await self.model.get(id=id)
user.last_login = datetime.now()
await user.save()
async def authenticate(self, credentials: CredentialsSchema) -> Optional["User"]:
user = await self.model.filter(username=credentials.username).first()
if not user:
raise HTTPException(status_code=400, detail="无效的用户名")
verified = verify_password(credentials.password, user.password)
if not verified:
raise HTTPException(status_code=400, detail="密码错误!")
if not user.is_active:
raise HTTPException(status_code=400, detail="用户已被禁用")
return user
async def update_roles(self, user: User, roles: List[int]) -> None:
await user.roles.clear()
for role_id in roles:
role_obj = await role_controller.get(id=role_id)
await user.roles.add(role_obj)
user_controller = UserController()

31
app/core/bgtask.py Normal file
View File

@ -0,0 +1,31 @@
from starlette.background import BackgroundTasks
from .ctx import CTX_BG_TASKS
class BgTasks:
"""后台任务统一管理"""
@classmethod
async def init_bg_tasks_obj(cls):
"""实例化后台任务,并设置到上下文"""
bg_tasks = BackgroundTasks()
CTX_BG_TASKS.set(bg_tasks)
@classmethod
async def get_bg_tasks_obj(cls):
"""从上下文中获取后台任务实例"""
return CTX_BG_TASKS.get()
@classmethod
async def add_task(cls, func, *args, **kwargs):
"""添加后台任务"""
bg_tasks = await cls.get_bg_tasks_obj()
bg_tasks.add_task(func, *args, **kwargs)
@classmethod
async def execute_tasks(cls):
"""执行后台任务,一般是请求结果返回之后执行"""
bg_tasks = await cls.get_bg_tasks_obj()
if bg_tasks.tasks:
await bg_tasks()

45
app/core/crud.py Normal file
View File

@ -0,0 +1,45 @@
from typing import Any, Dict, Generic, List, NewType, Tuple, Type, TypeVar, Union
from pydantic import BaseModel
from tortoise.expressions import Q
from tortoise.models import Model
Total = NewType("Total", int)
ModelType = TypeVar("ModelType", bound=Model)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
self.model = model
async def get(self, id: int) -> ModelType:
return await self.model.get(id=id)
async def list(self, page: int, page_size: int, search: Q = Q(), order: list = []) -> Tuple[Total, List[ModelType]]:
query = self.model.filter(search)
return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)
async def create(self, obj_in: CreateSchemaType) -> ModelType:
if isinstance(obj_in, Dict):
obj_dict = obj_in
else:
obj_dict = obj_in.dict()
obj = self.model(**obj_dict)
await obj.save()
return obj
async def update(self, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType:
if isinstance(obj_in, Dict):
obj_dict = obj_in
else:
obj_dict = obj_in.dict(exclude_unset=True)
obj = await self.get(id=id)
obj = obj.update_from_dict(obj_dict)
await obj.save()
return obj
async def remove(self, id: int) -> None:
obj = await self.get(id=id)
await obj.delete()

6
app/core/ctx.py Normal file
View File

@ -0,0 +1,6 @@
import contextvars
from starlette.background import BackgroundTasks
CTX_USER_ID: contextvars.ContextVar[int] = contextvars.ContextVar("user_id", default=0)
CTX_BG_TASKS: contextvars.ContextVar[BackgroundTasks] = contextvars.ContextVar("bg_task", default=None)

53
app/core/dependency.py Normal file
View File

@ -0,0 +1,53 @@
from typing import Optional
import jwt
from fastapi import Depends, Header, HTTPException, Request
from app.core.ctx import CTX_USER_ID
from app.models import Role, User
from app.settings import settings
class AuthControl:
@classmethod
async def is_authed(cls, token: str = Header(..., description="token验证")) -> Optional["User"]:
try:
if token == "dev":
user = await User.filter().first()
user_id = user.id
else:
decode_data = jwt.decode(token, settings.SECRET_KEY, algorithms=settings.JWT_ALGORITHM)
user_id = decode_data.get("user_id")
user = await User.filter(id=user_id).first()
if not user:
raise HTTPException(status_code=401, detail="Authentication failed")
CTX_USER_ID.set(int(user_id))
return user
except jwt.DecodeError:
raise HTTPException(status_code=401, detail="无效的Token")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="登录已过期")
except Exception as e:
raise HTTPException(status_code=500, detail=f"{repr(e)}")
class PermissionControl:
@classmethod
async def has_permission(cls, request: Request, current_user: User = Depends(AuthControl.is_authed)) -> None:
if current_user.is_superuser:
return
method = request.method
path = request.url.path
roles: list[Role] = await current_user.roles
if not roles:
raise HTTPException(status_code=403, detail="The user is not bound to a role")
apis = [await role.apis for role in roles]
permission_apis = list(set((api.method, api.path) for api in sum(apis, [])))
# path = "/api/v1/auth/userinfo"
# method = "GET"
if (method, path) not in permission_apis:
raise HTTPException(status_code=403, detail=f"Permission denied method:{method} path:{path}")
DependAuth = Depends(AuthControl.is_authed)
DependPermisson = Depends(PermissionControl.has_permission)

43
app/core/exceptions.py Normal file
View File

@ -0,0 +1,43 @@
from fastapi.exceptions import (
HTTPException,
RequestValidationError,
ResponseValidationError,
)
from fastapi.requests import Request
from fastapi.responses import JSONResponse
from tortoise.exceptions import DoesNotExist, IntegrityError
class SettingNotFound(Exception):
pass
async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse:
content = dict(
code=404,
msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}",
)
return JSONResponse(content=content, status_code=404)
async def IntegrityHandle(_: Request, exc: IntegrityError) -> JSONResponse:
content = dict(
code=500,
msg=f"IntegrityError{exc}",
)
return JSONResponse(content=content, status_code=500)
async def HttpExcHandle(_: Request, exc: HTTPException) -> JSONResponse:
content = dict(code=exc.status_code, msg=exc.detail, data=None)
return JSONResponse(content=content, status_code=exc.status_code)
async def RequestValidationHandle(_: Request, exc: RequestValidationError) -> JSONResponse:
content = dict(code=422, msg=f"RequestValidationError, {exc}")
return JSONResponse(content=content, status_code=422)
async def ResponseValidationHandle(_: Request, exc: ResponseValidationError) -> JSONResponse:
content = dict(code=500, msg=f"ResponseValidationError, {exc}")
return JSONResponse(content=content, status_code=500)

138
app/core/init_app.py Normal file
View File

@ -0,0 +1,138 @@
from fastapi import FastAPI
from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise
from app.api import api_router
from app.controllers.user import UserCreate, user_controller
from app.core.exceptions import (
DoesNotExist,
DoesNotExistHandle,
HTTPException,
HttpExcHandle,
IntegrityError,
IntegrityHandle,
RequestValidationError,
RequestValidationHandle,
ResponseValidationError,
ResponseValidationHandle,
)
from app.models.admin import Menu
from app.schemas.menus import MenuType
from app.settings.config import settings
from .middlewares import BackGroundTaskMiddleware
def make_middlewares():
middleware = [
Middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
allow_methods=settings.CORS_ALLOW_METHODS,
allow_headers=settings.CORS_ALLOW_HEADERS,
),
Middleware(BackGroundTaskMiddleware),
]
return middleware
def register_db(app: FastAPI, db_url=None):
register_tortoise(
app,
# db_url='sqlite://db.sqlite3',
# modules={'models':['app.models', "aerich.models"]},
config=settings.TORTOISE_ORM,
generate_schemas=True,
)
def register_exceptions(app: FastAPI):
app.add_exception_handler(DoesNotExist, DoesNotExistHandle)
app.add_exception_handler(HTTPException, HttpExcHandle)
app.add_exception_handler(IntegrityError, IntegrityHandle)
app.add_exception_handler(RequestValidationError, RequestValidationHandle)
app.add_exception_handler(ResponseValidationError, ResponseValidationHandle)
def register_routers(app: FastAPI, prefix: str = "/api"):
app.include_router(api_router, prefix=prefix)
async def init_superuser():
user = await user_controller.model.exists()
if not user:
await user_controller.create(
UserCreate(
username="admin",
email="admin@admin.com",
password="123456",
is_active=True,
is_superuser=True,
)
)
async def init_menus():
menus = await Menu.exists()
if not menus:
parent_menu = await Menu.create(
menu_type=MenuType.CATALOG,
name="系统管理",
path="/system",
order=1,
parent_id=0,
icon="carbon:gui-management",
is_hidden=False,
component="Layout",
keepalive=True,
redirect="/system/user",
)
children_menu = [
Menu(
menu_type=MenuType.MENU,
name="用户管理",
path="user",
order=1,
parent_id=parent_menu.id,
icon="material-symbols:person-outline-rounded",
is_hidden=False,
component="/system/user",
keepalive=True,
),
Menu(
menu_type=MenuType.MENU,
name="角色管理",
path="role",
order=2,
parent_id=parent_menu.id,
icon="carbon:user-role",
is_hidden=False,
component="/system/role",
keepalive=True,
),
Menu(
menu_type=MenuType.MENU,
name="菜单管理",
path="menu",
order=3,
parent_id=parent_menu.id,
icon="material-symbols:list-alt-outline",
is_hidden=False,
component="/system/menu",
keepalive=True,
),
Menu(
menu_type=MenuType.MENU,
name="API管理",
path="api",
order=4,
parent_id=parent_menu.id,
icon="ant-design:api-outlined",
is_hidden=False,
component="/system/api",
keepalive=True,
),
]
await Menu.bulk_create(children_menu)

34
app/core/middlewares.py Normal file
View File

@ -0,0 +1,34 @@
from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send
from .bgtask import BgTasks
class SimpleBaseMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request = Request(scope, receive=receive)
response = await self.before_request(request) or self.app
await response(request.scope, request.receive, send)
await self.after_request(request)
async def before_request(self, request: Request):
return self.app
async def after_request(self, request: Request):
return None
class BackGroundTaskMiddleware(SimpleBaseMiddleware):
async def before_request(self, request):
await BgTasks.init_bg_tasks_obj()
async def after_request(self, request):
await BgTasks.execute_tasks()

1
app/log/__init__.py Normal file
View File

@ -0,0 +1 @@
from .log import logger as logger

25
app/log/log.py Normal file
View File

@ -0,0 +1,25 @@
import sys
from loguru import logger as loguru_logger
from app.settings import settings
class Loggin:
def __init__(self) -> None:
debug = settings.DEBUG
if debug:
self.level = "DEBUG"
else:
self.level = "INFO"
def setup_logger(self):
loguru_logger.remove()
loguru_logger.add(sink=sys.stdout, level=self.level)
# logger.add("my_project.log", level=level, rotation="100 MB") # Output log messages to a file
return loguru_logger
loggin = Loggin()
logger = loggin.setup_logger()

1
app/models/__init__.py Normal file
View File

@ -0,0 +1 @@
from .admin import *

74
app/models/admin.py Normal file
View File

@ -0,0 +1,74 @@
from tortoise import fields
from app.schemas.menus import MenuType
from .base import BaseModel, TimestampMixin
from .enums import MethodType
class User(BaseModel, TimestampMixin):
username = fields.CharField(max_length=20, unique=True, description="用户名称")
alias = fields.CharField(max_length=30, null=True, description="姓名")
email = fields.CharField(max_length=255, unique=True, description="邮箱")
phone = fields.CharField(max_length=20, null=True, description="电话")
password = fields.CharField(max_length=128, null=True, description="密码")
is_active = fields.BooleanField(default=True, description="是否激活")
is_superuser = fields.BooleanField(default=False, description="是否为超级管理员")
last_login = fields.DatetimeField(null=True, description="最后登录时间")
roles = fields.ManyToManyField("models.Role", related_name="user_roles")
class Meta:
table = "user"
class PydanticMeta:
# todo
# computed = ["full_name"]
...
class Role(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, unique=True, description="角色名称")
desc = fields.CharField(max_length=500, null=True, blank=True, description="角色描述")
menus = fields.ManyToManyField("models.Menu", related_name="role_menus")
apis = fields.ManyToManyField("models.Api", related_name="role_apis")
class Meta:
table = "role"
class Api(BaseModel, TimestampMixin):
path = fields.CharField(max_length=100, description="API路径")
method = fields.CharEnumField(MethodType, description="请求方法")
summary = fields.CharField(max_length=500, description="请求简介")
tags = fields.CharField(max_length=100, description="API标签")
class Meta:
table = "api"
class Menu(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, description="菜单名称")
remark = fields.JSONField(null=True, description="保留字段", blank=True)
menu_type = fields.CharEnumField(MenuType, null=True, blank=True, description="菜单类型")
icon = fields.CharField(max_length=100, null=True, blank=True, description="菜单图标")
path = fields.CharField(max_length=100, description="菜单路径")
order = fields.IntField(default=0, description="排序")
parent_id = fields.IntField(default=0, max_length=10, description="父菜单ID")
is_hidden = fields.BooleanField(default=False, description="是否隐藏")
component = fields.CharField(max_length=100, description="组件")
keepalive = fields.BooleanField(default=True, description="存活")
redirect = fields.CharField(max_length=100, null=True, blank=True, description="重定向")
class Meta:
table = "menu"
class Dept(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, unique=True, description="部门名称")
desc = fields.CharField(max_length=500, null=True, blank=True, description="菜单描述")
is_deleted = fields.BooleanField(default=False, description="软删除标记")
order = fields.IntField(default=0, description="排序")
parent_id = fields.IntField(default=0, max_length=10, description="父部门ID")
class Meta:
table = "dept"

26
app/models/base.py Normal file
View File

@ -0,0 +1,26 @@
from tortoise import fields, models
class BaseModel(models.Model):
id = fields.BigIntField(pk=True, index=True)
async def to_dict(self, m2m=True):
d = {}
for field in self._meta.db_fields:
d[field] = getattr(self, field)
if m2m:
for field in self._meta.m2m_fields:
d[field] = await getattr(self, field).all().values()
return d
class Meta:
abstract = True
class UUIDModel:
uuid = fields.UUIDField(unique=True, pk=False)
class TimestampMixin:
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)

27
app/models/enums.py Normal file
View File

@ -0,0 +1,27 @@
from enum import Enum
class EnumBase(Enum):
@classmethod
def get_member_values(cls):
return [item.value for item in cls._member_map_.values()]
@classmethod
def get_member_names(cls):
return [name for name in cls._member_names_]
class IntEnum(int, EnumBase):
...
class StrEnum(str, EnumBase):
...
class MethodType(StrEnum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"

1
app/schemas/__init__.py Normal file
View File

@ -0,0 +1 @@
from .base import *

21
app/schemas/apis.py Normal file
View File

@ -0,0 +1,21 @@
from pydantic import BaseModel, Field
from app.models.enums import MethodType
class BaseApi(BaseModel):
path: str = Field(..., description="API路径", example="/api/v1/user/list")
summary: str = Field(None, description="API简介", example="查看用户列表")
method: MethodType = Field(..., description="API方法", example="GET")
tags: str = Field(..., description="API标签", example="User")
class ApiCreate(BaseApi):
...
class ApiUpdate(BaseApi):
id: int
def update_dict(self):
return self.dict(exclude_unset=True, exclude={"id"})

26
app/schemas/base.py Normal file
View File

@ -0,0 +1,26 @@
from typing import Generic, Optional, TypeVar
from pydantic import BaseModel
from pydantic.generics import GenericModel
DataT = TypeVar("DataT")
class BaseResponse(GenericModel, BaseModel, Generic[DataT]):
code: int
msg: str = ""
data: Optional[DataT] = None
class Success(BaseResponse):
code: int = 200
class Fail(BaseResponse):
code: int = -1
class SuccessExtra(Success):
total: int
page: int
page_size: int

21
app/schemas/login.py Normal file
View File

@ -0,0 +1,21 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class CredentialsSchema(BaseModel):
username: str = Field(..., description="用户名称", example="admin")
password: str = Field(..., description="密码", example="123456")
class JWTOut(BaseModel):
access_token: str
username: str
class JWTPayload(BaseModel):
user_id: int
username: str
is_superuser: bool
exp: datetime

70
app/schemas/menus.py Normal file
View File

@ -0,0 +1,70 @@
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field
from .base import BaseResponse, SuccessExtra
class MenuType(str, Enum):
CATALOG = "catalog"
MENU = "menu"
class BaseMenu(BaseModel):
id: int
name: str
path: str
remark: Optional[dict]
menu_type: Optional[MenuType]
icon: Optional[str]
order: int
parent_id: int
is_hidden: bool
component: str
keepalive: bool
redirect: Optional[str]
children: Optional[list["BaseMenu"]]
class MenuCreate(BaseModel):
menu_type: MenuType = Field(default=MenuType.CATALOG.value)
name: str = Field(example="用户管理")
remark: Optional[dict] = Field(example={})
icon: Optional[str] = "ph:user-list-bold"
path: str = Field(example="/system/user")
order: Optional[int] = Field(example=1)
parent_id: Optional[int] = Field(example=0, default=0)
is_hidden: Optional[bool] = False
component: str = Field(default="Layout", example="/system/user")
keepalive: Optional[bool] = True
redirect: Optional[str] = ""
class MenuUpdate(BaseModel):
id: int
menu_type: Optional[MenuType] = Field(example=MenuType.CATALOG.value)
name: Optional[str] = Field(example="用户管理")
remark: Optional[dict] = Field(example={})
icon: Optional[str] = "ph:user-list-bold"
path: Optional[str] = Field(example="/system/user")
order: Optional[int] = Field(example=1)
parent_id: Optional[int] = Field(example=0)
is_hidden: Optional[bool] = False
component: str = Field(example="/system/user")
keepalive: Optional[bool] = False
redirect: Optional[str] = ""
def update_dict(self):
return self.dict(exclude_unset=True, exclude={"id"})
"""Response"""
class MenuOutList(SuccessExtra):
data: Optional[List[BaseMenu]]
class MenuOut(BaseResponse):
data: Optional[BaseMenu]

48
app/schemas/roles.py Normal file
View File

@ -0,0 +1,48 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from .base import BaseResponse, SuccessExtra
class BaseRole(BaseModel):
id: int
name: str
desc: Optional[str]
users: Optional[list]
menus: Optional[list]
apis: Optional[list]
created_at: Optional[datetime]
updated_at: Optional[datetime]
class RoleCreate(BaseModel):
name: str = Field(example="管理员")
desc: Optional[str] = Field(example="管理员角色")
class RoleUpdate(BaseModel):
id: int = Field(example=1)
name: Optional[str] = Field(example="管理员")
desc: Optional[str] = Field(example="管理员角色")
def update_dict(self):
return self.dict(exclude_unset=True, exclude={"id"})
class RoleUpdateMenusApis(BaseModel):
id: int
menu_ids: List[int] = []
api_infos: List[dict] = []
"""Response"""
class RoleOutList(SuccessExtra):
data: Optional[List[BaseRole]]
class RoleOut(BaseResponse):
data: Optional[BaseRole]

60
app/schemas/users.py Normal file
View File

@ -0,0 +1,60 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr, Field
from app.schemas.base import BaseResponse, SuccessExtra
class BaseUser(BaseModel):
id: int
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
created_at: Optional[datetime]
updated_at: Optional[datetime]
last_login: Optional[datetime]
roles: Optional[list] = []
class UserCreate(BaseModel):
email: EmailStr = Field(example="admin@qq.com")
username: str = Field(example="admin")
password: str = Field(example="123456")
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
roles: Optional[List[int]] = []
def create_dict(self):
return self.dict(exclude_unset=True, exclude={"roles"})
class UserUpdate(BaseModel):
id: int
password: Optional[str]
email: Optional[EmailStr]
username: Optional[str]
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
roles: Optional[List[int]] = []
def update_dict(self):
return self.dict(exclude_unset=True, exclude={"roles", "id"})
class UpdatePassword(BaseModel):
id: int = Field(description="用户ID")
old_password: str = Field(description="旧密码")
new_password: str = Field(description="新密码")
"""Response"""
class UserOutList(SuccessExtra):
data: Optional[List[BaseUser]]
class UserOut(BaseResponse):
data: Optional[BaseUser]

3
app/settings/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .config import settings as settings
TORTOISE_ORM = settings.TORTOISE_ORM

56
app/settings/config.py Normal file
View File

@ -0,0 +1,56 @@
import os
import typing
from pydantic import BaseSettings
class Settings(BaseSettings):
VERSION: str = "0.1.0"
APP_TITLE: str = "Template Application"
PROJECT_NAME: str = "Template Application"
APP_DESCRIPTION: str = "Description"
CORS_ORIGINS: typing.List = ["*"]
CORS_ALLOW_CREDENTIALS: bool = True
CORS_ALLOW_METHODS: typing.List = ["*"]
CORS_ALLOW_HEADERS: typing.List = ["*"]
DEBUG = True
DB_URL: str = "sqlite://db.sqlite3"
DB_CONNECTIONS: dict = {
"default": {
"engine": "tortoise.backends.sqlite",
"db_url": DB_URL,
"credentials": {
"host": "",
"port": "",
"user": "",
"password": "",
"database": "",
},
},
}
PROJECT_ROOT: str = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
BASE_DIR: str = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir))
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 # 7 day
TORTOISE_ORM = {
"connections": {
"sqlite": {
"engine": "tortoise.backends.sqlite",
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"},
}
},
"apps": {
"models": {
"models": ["app.models"],
"default_connection": "sqlite",
},
},
}
settings = Settings()

10
app/utils/jwt.py Normal file
View File

@ -0,0 +1,10 @@
import jwt
from app.schemas.login import JWTPayload
from app.settings.config import settings
def create_access_token(*, data: JWTPayload):
payload = data.dict().copy()
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt

16
app/utils/password.py Normal file
View File

@ -0,0 +1,16 @@
from passlib import pwd
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def generate_password() -> str:
return pwd.genword()

578
poetry.lock generated Normal file
View File

@ -0,0 +1,578 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "aiosqlite"
version = "0.17.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.6"
files = [
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
]
[package.dependencies]
typing_extensions = ">=3.7.2"
[[package]]
name = "anyio"
version = "3.7.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.7"
files = [
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (<0.22)"]
[[package]]
name = "bcrypt"
version = "4.0.1"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.6"
files = [
{file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"},
{file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"},
{file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"},
{file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"},
]
[package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"]
[[package]]
name = "black"
version = "23.7.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
{file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
{file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
{file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
{file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
{file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
{file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
{file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
{file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
{file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
{file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
{file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
{file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
{file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
{file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
{file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
{file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
{file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
{file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
{file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
{file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
{file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
{file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
]
[[package]]
name = "click"
version = "8.1.5"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.5-py3-none-any.whl", hash = "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"},
{file = "click-8.1.5.tar.gz", hash = "sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "dnspython"
version = "2.4.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.8,<4.0"
files = [
{file = "dnspython-2.4.0-py3-none-any.whl", hash = "sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a"},
{file = "dnspython-2.4.0.tar.gz", hash = "sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c"},
]
[package.dependencies]
httpcore = {version = ">=0.17.3", markers = "python_version >= \"3.8\""}
sniffio = ">=1.1,<2.0"
[package.extras]
dnssec = ["cryptography (>=2.6,<42.0)"]
doh = ["h2 (>=4.1.0)", "httpx (>=0.24.1)"]
doq = ["aioquic (>=0.9.20)"]
idna = ["idna (>=2.1,<4.0)"]
trio = ["trio (>=0.14,<0.23)"]
wmi = ["wmi (>=1.5.1,<2.0.0)"]
[[package]]
name = "email-validator"
version = "2.0.0.post2"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.7"
files = [
{file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"},
{file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]]
name = "fastapi"
version = "0.100.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.7"
files = [
{file = "fastapi-0.100.0-py3-none-any.whl", hash = "sha256:271662daf986da8fa98dc2b7c7f61c4abdfdccfb4786d79ed8b2878f172c6d5f"},
{file = "fastapi-0.100.0.tar.gz", hash = "sha256:acb5f941ea8215663283c10018323ba7ea737c571b67fc7e88e9469c7eb1d12e"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<3.0.0"
starlette = ">=0.27.0,<0.28.0"
typing-extensions = ">=4.5.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "0.17.3"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.7"
files = [
{file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
{file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
]
[package.dependencies]
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
sniffio = "==1.*"
[package.extras]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "iso8601"
version = "1.1.0"
description = "Simple module to parse ISO 8601 dates"
optional = false
python-versions = ">=3.6.2,<4.0"
files = [
{file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"},
{file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"},
]
[[package]]
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "loguru"
version = "0.7.0"
description = "Python logging made (stupidly) simple"
optional = false
python-versions = ">=3.5"
files = [
{file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"},
{file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"},
]
[package.dependencies]
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v0.990)", "pre-commit (==3.2.1)", "pytest (==6.1.2)", "pytest (==7.2.1)", "pytest-cov (==2.12.1)", "pytest-cov (==4.0.0)", "pytest-mypy-plugins (==1.10.1)", "pytest-mypy-plugins (==1.9.3)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.2.0)", "tox (==3.27.1)", "tox (==4.4.6)"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
[[package]]
name = "passlib"
version = "1.7.4"
description = "comprehensive password hashing framework supporting over 30 schemes"
optional = false
python-versions = "*"
files = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]]
name = "pathspec"
version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
]
[[package]]
name = "platformdirs"
version = "3.9.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"},
{file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"},
]
[package.extras]
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
[[package]]
name = "pydantic"
version = "1.10.11"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f"},
{file = "pydantic-1.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e"},
{file = "pydantic-1.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151"},
{file = "pydantic-1.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7"},
{file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588"},
{file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f"},
{file = "pydantic-1.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847"},
{file = "pydantic-1.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb"},
{file = "pydantic-1.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b"},
{file = "pydantic-1.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae"},
{file = "pydantic-1.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66"},
{file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216"},
{file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c"},
{file = "pydantic-1.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b"},
{file = "pydantic-1.10.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6"},
{file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713"},
{file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c"},
{file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248"},
{file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36"},
{file = "pydantic-1.10.11-cp37-cp37m-win_amd64.whl", hash = "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629"},
{file = "pydantic-1.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3"},
{file = "pydantic-1.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f"},
{file = "pydantic-1.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb"},
{file = "pydantic-1.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d"},
{file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f"},
{file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e"},
{file = "pydantic-1.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19"},
{file = "pydantic-1.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622"},
{file = "pydantic-1.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1"},
{file = "pydantic-1.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999"},
{file = "pydantic-1.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303"},
{file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604"},
{file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13"},
{file = "pydantic-1.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e"},
{file = "pydantic-1.10.11-py3-none-any.whl", hash = "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e"},
{file = "pydantic-1.10.11.tar.gz", hash = "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528"},
]
[package.dependencies]
typing-extensions = ">=4.2.0"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyjwt"
version = "2.7.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "PyJWT-2.7.0-py3-none-any.whl", hash = "sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1"},
{file = "PyJWT-2.7.0.tar.gz", hash = "sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074"},
]
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pypika-tortoise"
version = "0.1.6"
description = "Forked from pypika and streamline just for tortoise-orm"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"},
{file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"},
]
[[package]]
name = "pytz"
version = "2023.3"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
files = [
{file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"},
{file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"},
]
[[package]]
name = "ruff"
version = "0.0.281"
description = "An extremely fast Python linter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.281-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:418fbddfd3dba4d7b11e4e016eacc40d321ff0b7d3637c7ba9ad3ee0474c9a35"},
{file = "ruff-0.0.281-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c086bf3968d5cb2b4f31a586fc73bc42cb688c32f4c992ff161d4ce19f551cf2"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0162b149a94f6007768820bcdf4ccb7e90a21655aac829ace49f4682d0565fdb"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3495175e6d85a01d3da409a079461a5a3c15b70237cc82550ad8c1f091002c8"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae0b836c03a7010527bb56384a4e3718e0958e32bea64459879aacdcb65c4945"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6d34cae6ef6c6b6fd6d4f09271fbf635db49e6b788da1b2e1dea11a29f1c2a11"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd3c94260a148e955fb46f41d4bcecd857c75794e9f06ebfa7f9be65cfed9621"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ccb875a4000bcba6cc61cb9d3cd5969d6b0921b5234f0ef99ad75f74e8935ef"},
{file = "ruff-0.0.281-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5b8ccaabad61e2d50494df820b7bafd94eac13f10d2d8b831994c1618801a9"},
{file = "ruff-0.0.281-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cbf279fd9c2ca674896656df2d82831010afd336a6703a060fe08d6f2358e47b"},
{file = "ruff-0.0.281-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24d0defeb2c6a1b16a4230840d1138e08bc4ef2318496fa6ff7ddbf3a443626f"},
{file = "ruff-0.0.281-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54bab7128167057ee5987bbd9f925fbf105071068de9d8474ca7c38f684b8463"},
{file = "ruff-0.0.281-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:29a22b7a6433ce0b4e601897e8a5dd58a75c75c01afee9b8922ebbdd1fe51e51"},
{file = "ruff-0.0.281-py3-none-win32.whl", hash = "sha256:7b781f6a7ed35196e6565ed32f57d07b852b0dcd7158c6c7669c8b5d0f8cf97a"},
{file = "ruff-0.0.281-py3-none-win_amd64.whl", hash = "sha256:70f921438bf09f04c0547cf64c137c87ef33cbec2b64be12b8caa87df261a016"},
{file = "ruff-0.0.281-py3-none-win_arm64.whl", hash = "sha256:42a92a62fc841f7444821444553fd6e1e700bb55348f24e8ec39afdd4e3d0312"},
{file = "ruff-0.0.281.tar.gz", hash = "sha256:bab2cdfa78754315cccc2b4d46ad6181aabb29e89747a3b135a4b85e11baa025"},
]
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "starlette"
version = "0.27.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.7"
files = [
{file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"},
{file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"},
]
[package.dependencies]
anyio = ">=3.4.0,<5"
[package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
[[package]]
name = "tortoise-orm"
version = "0.19.3"
description = "Easy async ORM for python, built with relations in mind"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "tortoise_orm-0.19.3-py3-none-any.whl", hash = "sha256:9e368820c70a0866ef9c521d43aa5503485bd7a20a561edc0933b7b0f7036fbc"},
{file = "tortoise_orm-0.19.3.tar.gz", hash = "sha256:ca574bca5191f55608f9013314b1f5d1c6ffd4165a1fcc2f60f6c902f529b3b6"},
]
[package.dependencies]
aiosqlite = ">=0.16.0,<0.18.0"
iso8601 = ">=1.0.2,<2.0.0"
pypika-tortoise = ">=0.1.6,<0.2.0"
pytz = "*"
[package.extras]
accel = ["ciso8601", "orjson", "uvloop"]
aiomysql = ["aiomysql"]
asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"]
asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"]
asyncpg = ["asyncpg"]
psycopg = ["psycopg[binary,pool] (==3.0.12)"]
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
optional = false
python-versions = ">=3.7"
files = [
{file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
[[package]]
name = "uvicorn"
version = "0.23.1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"},
{file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
[package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "win32-setctime"
version = "1.1.0"
description = "A small Python utility to set file creation time on Windows"
optional = false
python-versions = ">=3.5"
files = [
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
]
[package.extras]
dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "9da7bb387eefe738f40f5b1b879e621e924379162f7034f1b0af8e03c14f60f1"

41
pyproject.toml Normal file
View File

@ -0,0 +1,41 @@
[tool.poetry]
name = "vue-fastapi-admin"
version = "0.1.0"
description = "Vue Fastapi admin"
authors = ["王津校 <jinxiao.wang@tenclass.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.100.0"
uvicorn = "^0.23.1"
tortoise-orm = "^0.19.3"
pydantic = "^1.10.5"
email-validator = "^2.0.0.post2"
passlib = "^1.7.4"
pyjwt = "^2.7.0"
bcrypt = "^4.0.1"
black = "^23.7.0"
isort = "^5.12.0"
ruff = "^0.0.281"
loguru = "^0.7.0"
[tool.black]
line-length = 120
target-version = ["py310", "py311"]
[tool.ruff]
line-length = 120
extend-select = [
"I", # isort
# "B", # flake8-bugbear
# "C4", # flake8-comprehensions
# "PGH", # pygrep-hooks
"RUF", # ruff
# "W", # pycodestyle
# "YTT", # flake8-2020
]
ignore = [
"F403",
"F405",
]

4
run.py Normal file
View File

@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True)

3
run.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/zsh
uvicorn app:app --reload --host 0.0.0.0 --port 9999

3
web/.env Normal file
View File

@ -0,0 +1,3 @@
VITE_TITLE = 'Vue FastAPI Admin'
VITE_PORT = 3100

8
web/.env.development Normal file
View File

@ -0,0 +1,8 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# 是否启用代理
VITE_USE_PROXY = true
# 代理类型 'dev' | 'test' | 'prod'
VITE_PROXY_TYPE = 'dev'

11
web/.env.production Normal file
View File

@ -0,0 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# base api
VITE_BASE_API = '/api'
# 是否启用压缩
VITE_USE_COMPRESS = true
# 压缩类型
VITE_COMPRESS_TYPE = gzip

View File

@ -0,0 +1,62 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"defineOptions": true,
"$dialog": true,
"$notification": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

4
web/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
public
package.json

25
web/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
web/.prettierignore Normal file
View File

@ -0,0 +1,3 @@
/node_modules/**
/dist/*
/public/*

6
web/.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf"
}

16
web/README.md Normal file
View File

@ -0,0 +1,16 @@
## 快速开始
进入前端目录
```sh
cd web
```
安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
```sh
npm i -g pnpm # 已安装可忽略
pnpm i # 或者 npm i
```
启动
```sh
pnpm dev
```

View File

@ -0,0 +1,13 @@
import dayjs from 'dayjs'
/**
* * 此处定义的是全局常量启动或打包后将添加到window中
* https://vitejs.cn/config/#define
*/
// 项目构建时间
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
export const viteDefine = {
_BUILD_TIME_,
}

View File

@ -0,0 +1,2 @@
export * from './define'
export * from './proxy'

15
web/build/config/proxy.js Normal file
View File

@ -0,0 +1,15 @@
import { getProxyConfig } from '../../settings'
export function createViteProxy(isUseProxy = true, proxyType) {
if (!isUseProxy) return undefined
const proxyConfig = getProxyConfig(proxyType)
const proxy = {
[proxyConfig.prefix]: {
target: proxyConfig.target,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${proxyConfig.prefix}`), ''),
},
}
return proxy
}

15
web/build/plugin/html.js Normal file
View File

@ -0,0 +1,15 @@
import { createHtmlPlugin } from 'vite-plugin-html'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_TITLE,
},
},
})
return htmlPlugin
}

35
web/build/plugin/index.js Normal file
View File

@ -0,0 +1,35 @@
import vue from '@vitejs/plugin-vue'
/**
* * unocss插件原子css
* https://github.com/antfu/unocss
*/
import Unocss from 'unocss/vite'
// rollup打包分析插件
import visualizer from 'rollup-plugin-visualizer'
// 压缩
import viteCompression from 'vite-plugin-compression'
import { configHtmlPlugin } from './html'
import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv.VITE_USE_COMPRESS) {
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
}
if (isBuild) {
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
)
}
return plugins
}

View File

@ -0,0 +1,46 @@
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
/**
* * unplugin-icons插件自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [
AutoImport({
imports: ['vue', 'vue-router'],
dts: false,
}),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
],
dts: false,
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}),
]

View File

@ -0,0 +1,15 @@
import { resolve } from 'path'
import chalk from 'chalk'
import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() {
const { VITE_CNAME } = getEnvConfig()
if (!VITE_CNAME) return
try {
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}
}

14
web/build/script/index.js Normal file
View File

@ -0,0 +1,14 @@
import chalk from 'chalk'
import { runBuildCNAME } from './build-cname'
export const runBuild = async () => {
try {
runBuildCNAME()
console.log(`${chalk.cyan('build successfully!')}`)
} catch (error) {
console.log(chalk.red('vite build error:\n' + error))
process.exit(1)
}
}
runBuild()

70
web/build/utils.js Normal file
View File

@ -0,0 +1,70 @@
import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
/**
* * 项目根路径
* @descrition 结尾不带/
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
/**
* * 项目src路径
* @param srcName src目录名称(默认: "src")
* @descrition 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
}
export function convertEnv(envOptions) {
const result = {}
if (!envOptions) return result
for (const envKey in envOptions) {
let envVal = envOptions[envKey]
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
result[envKey] = envVal
}
return result
}
/**
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script
const reg = new RegExp('--mode ([a-z_\\d]+)')
const result = reg.exec(script)
if (result) {
const mode = result[1]
return ['.env', '.env.local', `.env.${mode}`]
}
return ['.env', '.env.local', '.env.production']
}
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
if (fs.existsSync(path.resolve(process.cwd(), item))) {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
envConfig = { ...envConfig, ...env }
}
} catch (e) {
console.error(`Error in parsing ${item}`, e)
}
})
const reg = new RegExp(`^(${match})`)
Object.keys(envConfig).forEach((key) => {
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key)
}
})
return envConfig
}

35
web/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" />
<link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title>
</head>
<body>
<div id="app">
<!-- 白屏时的loading效果 -->
<div class="loading-container">
<div id="loadingLogo" class="loading-svg"></div>
<div class="loading-spin__container">
<div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div>
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
</div>
</div>
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

14
web/jsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"~/*": ["./*"],
"@/*": ["src/*"]
},
"jsx": "preserve",
"allowJs": true
},
"exclude": ["node_modules", "dist"]
}

55
web/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "vue-fastapi-admin-web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged"
},
"dependencies": {
"@iconify/json": "^2.2.101",
"@iconify/vue": "^4.1.1",
"@unocss/eslint-config": "^0.55.0",
"@vueuse/core": "^10.3.0",
"@zclzone/eslint-config": "^0.0.4",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"dotenv": "^16.3.1",
"eslint": "^8.46.0",
"lodash-es": "^4.17.21",
"naive-ui": "^2.34.4",
"pinia": "^2.1.6",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.65.1",
"typescript": "^5.1.6",
"unocss": "^0.55.0",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.6"
},
"lint-staged": {
"*.{js,vue}": [
"eslint --ext .js,.vue ."
]
},
"eslintConfig": {
"extends": [
"@zclzone",
"@unocss",
".eslint-global-variables.json"
]
}
}

4077
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
web/public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,91 @@
.loading-container {
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loading-svg {
width: 128px;
height: 128px;
color: var(--primary-color);
}
.loading-spin__container {
width: 56px;
height: 56px;
margin: 36px 0;
}
.loading-spin {
position: relative;
height: 100%;
animation: loadingSpin 1s linear infinite;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.top-0 {
top: 0;
}
.bottom-0 {
bottom: 0;
}
.loading-spin-item {
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primary-color);
border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes loadingSpin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
.loading-delay-500 {
-webkit-animation-delay: 500ms;
animation-delay: 500ms;
}
.loading-delay-1000 {
-webkit-animation-delay: 1000ms;
animation-delay: 1000ms;
}
.loading-delay-1500 {
-webkit-animation-delay: 1500ms;
animation-delay: 1500ms;
}
.loading-title {
font-size: 28px;
font-weight: 500;
color: #6a6a6a;
}

File diff suppressed because one or more lines are too long

2
web/settings/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from './theme.json'
export * from './proxy-config'

View File

@ -0,0 +1,18 @@
const proxyConfigMappings = {
dev: {
prefix: '/url-patten',
target: 'http://127.0.0.1:9999/api/v1',
},
test: {
prefix: '/url-patten',
target: 'http://127.0.0.1:9999',
},
prod: {
prefix: '/url-patten',
target: 'http://127.0.0.1:9999',
},
}
export function getProxyConfig(envType = 'dev') {
return proxyConfigMappings[envType]
}

37
web/settings/theme.json Normal file
View File

@ -0,0 +1,37 @@
{
"header": {
"height": 60
},
"tags": {
"visible": true,
"height": 50
},
"naiveThemeOverrides": {
"common": {
"primaryColor": "#F4511E",
"primaryColorHover": "#F4511E",
"primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#F4511E",
"infoColor": "#2080F0FF",
"infoColorHover": "#4098FCFF",
"infoColorPressed": "#1060C9FF",
"infoColorSuppl": "#4098FCFF",
"successColor": "#18A058FF",
"successColorHover": "#F4511E",
"successColorPressed": "#0C7A43FF",
"successColorSuppl": "#F4511E",
"warningColor": "#F0A020FF",
"warningColorHover": "#FCB040FF",
"warningColorPressed": "#C97C10FF",
"warningColorSuppl": "#FCB040FF",
"errorColor": "#D03050FF",
"errorColorHover": "#DE576DFF",
"errorColorPressed": "#AB1F3FFF",
"errorColorSuppl": "#DE576DFF"
}
}
}

11
web/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<AppProvider>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</AppProvider>
</template>
<script setup>
import AppProvider from '@/components/common/AppProvider.vue'
</script>

34
web/src/api/index.js Normal file
View File

@ -0,0 +1,34 @@
import { request } from '@/utils'
export default {
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
getUserInfo: () => request.get('/base/userinfo'),
getUserMenu: () => request.get('/base/usermenu'),
getUserApi: () => request.get('/base/userapi'),
// profile
updatePassword: (data = {}) => request.post('/base/update_password', data),
// users
getUserList: (params = {}) => request.get('/user/list', { params }),
getUserById: (params = {}) => request.get('/user/get', { params }),
createUser: (data = {}) => request.post('/user/create', data),
updateUser: (data = {}) => request.post('/user/update', data),
deleteUser: (params = {}) => request.delete(`/user/delete`, { params }),
// role
getRoleList: (params = {}) => request.get('/role/list', { params }),
createRole: (data = {}) => request.post('/role/create', data),
updateRole: (data = {}) => request.post('/role/update', data),
deleteRole: (params = {}) => request.delete('/role/delete', { params }),
updateRoleAuthorized: (data = {}) => request.post('/role/authorized', data),
getRoleAuthorized: (params = {}) => request.get('/role/authorized', { params }),
// menus
getMenus: (params = {}) => request.get('/menu/list', { params }),
createMenu: (data = {}) => request.post('/menu/create', data),
updateMenu: (data = {}) => request.post('/menu/update', data),
deleteMenu: (params = {}) => request.delete('/menu/delete', { params }),
// apis
getApis: (params = {}) => request.get('/api/list', { params }),
createApi: (data = {}) => request.post('/api/create', data),
updateApi: (data = {}) => request.post('/api/update', data),
deleteApi: (params = {}) => request.delete('/api/delete', { params }),
refreshApi: (data = {}) => request.post('/api/refresh', data),
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

230
web/src/assets/js/icons.js Normal file
View File

@ -0,0 +1,230 @@
export default [
'mdi-air-humidifier-off',
'mdi-chili-off',
'mdi-cigar-off',
'mdi-clock-time-eight',
'mdi-clock-time-eight-outline',
'mdi-clock-time-eleven',
'mdi-clock-time-eleven-outline',
'mdi-clock-time-five',
'mdi-clock-time-five-outline',
'mdi-clock-time-four',
'mdi-clock-time-four-outline',
'mdi-clock-time-nine',
'mdi-clock-time-nine-outline',
'mdi-clock-time-one',
'mdi-clock-time-one-outline',
'mdi-clock-time-seven',
'mdi-clock-time-seven-outline',
'mdi-clock-time-six',
'mdi-clock-time-six-outline',
'mdi-clock-time-ten',
'mdi-clock-time-ten-outline',
'mdi-clock-time-three',
'mdi-clock-time-three-outline',
'mdi-clock-time-twelve',
'mdi-clock-time-twelve-outline',
'mdi-clock-time-two',
'mdi-clock-time-two-outline',
'mdi-cog-refresh',
'mdi-cog-refresh-outline',
'mdi-cog-sync',
'mdi-cog-sync-outline',
'mdi-content-save-cog',
'mdi-content-save-cog-outline',
'mdi-cosine-wave',
'mdi-cube-off',
'mdi-cube-off-outline',
'mdi-dome-light',
'mdi-download-box',
'mdi-download-box-outline',
'mdi-download-circle',
'mdi-download-circle-outline',
'mdi-fan-alert',
'mdi-fan-chevron-down',
'mdi-fan-chevron-up',
'mdi-fan-minus',
'mdi-fan-plus',
'mdi-fan-remove',
'mdi-fan-speed-1',
'mdi-fan-speed-2',
'mdi-fan-speed-3',
'mdi-food-drumstick',
'mdi-food-drumstick-off',
'mdi-food-drumstick-off-outline',
'mdi-food-drumstick-outline',
'mdi-food-steak',
'mdi-food-steak-off',
'mdi-fuse-alert',
'mdi-fuse-off',
'mdi-heart-minus',
'mdi-heart-minus-outline',
'mdi-heart-off-outline',
'mdi-heart-plus',
'mdi-heart-plus-outline',
'mdi-heart-remove',
'mdi-heart-remove-outline',
'mdi-hours-24',
'mdi-incognito-circle',
'mdi-incognito-circle-off',
'mdi-lingerie',
'mdi-microwave-off',
'mdi-minus-circle-off',
'mdi-minus-circle-off-outline',
'mdi-motion-sensor-off',
'mdi-pail-minus',
'mdi-pail-minus-outline',
'mdi-pail-off',
'mdi-pail-off-outline',
'mdi-pail-outline',
'mdi-pail-plus',
'mdi-pail-plus-outline',
'mdi-pail-remove',
'mdi-pail-remove-outline',
'mdi-pine-tree-fire',
'mdi-power-plug-off-outline',
'mdi-power-plug-outline',
'mdi-printer-eye',
'mdi-printer-search',
'mdi-puzzle-check',
'mdi-puzzle-check-outline',
'mdi-rug',
'mdi-sawtooth-wave',
'mdi-set-square',
'mdi-smoking-pipe-off',
'mdi-spoon-sugar',
'mdi-square-wave',
'mdi-table-split-cell',
'mdi-ticket-percent-outline',
'mdi-triangle-wave',
'mdi-waveform',
'mdi-wizard-hat',
'mdi-ab-testing',
'mdi-abjad-arabic',
'mdi-abjad-hebrew',
'mdi-abugida-devanagari',
'mdi-abugida-thai',
'mdi-access-point',
'mdi-access-point-network',
'mdi-access-point-network-off',
'mdi-account',
'mdi-account-alert',
'mdi-account-alert-outline',
'mdi-account-arrow-left',
'mdi-account-arrow-left-outline',
'mdi-account-arrow-right',
'mdi-account-arrow-right-outline',
'mdi-account-box',
'mdi-account-box-multiple',
'mdi-account-box-multiple-outline',
'mdi-account-box-outline',
'mdi-account-cancel',
'mdi-account-cancel-outline',
'mdi-account-cash',
'mdi-account-cash-outline',
'mdi-account-check',
'mdi-account-check-outline',
'mdi-account-child',
'mdi-account-child-circle',
'mdi-account-child-outline',
'mdi-account-circle',
'mdi-account-circle-outline',
'mdi-account-clock',
'mdi-account-clock-outline',
'mdi-account-cog',
'mdi-account-cog-outline',
'mdi-account-convert',
'mdi-account-convert-outline',
'mdi-account-cowboy-hat',
'mdi-account-details',
'mdi-account-details-outline',
'mdi-account-edit',
'mdi-account-edit-outline',
'mdi-account-group',
'mdi-account-group-outline',
'mdi-account-hard-hat',
'mdi-account-heart',
'mdi-account-heart-outline',
'mdi-account-key',
'mdi-account-key-outline',
'mdi-account-lock',
'mdi-account-lock-outline',
'mdi-account-minus',
'mdi-account-minus-outline',
'mdi-account-multiple',
'mdi-account-multiple-check',
'mdi-account-multiple-check-outline',
'mdi-account-multiple-minus',
'mdi-account-multiple-minus-outline',
'mdi-account-multiple-outline',
'mdi-account-multiple-plus',
'mdi-account-multiple-plus-outline',
'mdi-account-multiple-remove',
'mdi-account-multiple-remove-outline',
'mdi-account-music',
'mdi-account-music-outline',
'mdi-account-network',
'mdi-account-network-outline',
'mdi-account-off',
'mdi-account-off-outline',
'mdi-account-outline',
'mdi-account-plus',
'mdi-account-plus-outline',
'mdi-account-question',
'mdi-account-question-outline',
'mdi-account-remove',
'mdi-account-remove-outline',
'mdi-account-search',
'mdi-account-search-outline',
'mdi-account-settings',
'mdi-account-settings-outline',
'mdi-account-star',
'mdi-account-star-outline',
'mdi-account-supervisor',
'mdi-account-supervisor-circle',
'mdi-account-supervisor-outline',
'mdi-account-switch',
'mdi-account-switch-outline',
'mdi-account-tie',
'mdi-account-tie-outline',
'mdi-account-tie-voice',
'mdi-account-tie-voice-off',
'mdi-account-tie-voice-off-outline',
'mdi-account-tie-voice-outline',
'mdi-account-voice',
'mdi-adjust',
'mdi-adobe',
'mdi-adobe-acrobat',
'mdi-air-conditioner',
'mdi-air-filter',
'mdi-air-horn',
'mdi-air-humidifier',
'mdi-air-purifier',
'mdi-airbag',
'mdi-airballoon',
'mdi-airballoon-outline',
'mdi-airplane',
'mdi-airplane-landing',
'mdi-airplane-off',
'mdi-airplane-takeoff',
'mdi-airport',
'mdi-alarm',
'mdi-alarm-bell',
'mdi-alarm-check',
'mdi-alarm-light',
'mdi-alarm-light-outline',
'mdi-alarm-multiple',
'mdi-alarm-note',
'mdi-alarm-note-off',
'mdi-alarm-off',
'mdi-alarm-plus',
'mdi-alarm-snooze',
'mdi-album',
'mdi-alert',
'mdi-alert-box',
'mdi-alert-box-outline',
'mdi-alert-circle',
'mdi-alert-circle-check',
'mdi-alert-circle-check-outline',
'mdi-alert-circle-outline',
]

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 87 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,4 @@
<template>
<footer f-c-c flex-col text-14 color="#6a6a6a">
</footer>
</template>

View File

@ -0,0 +1,67 @@
<template>
<n-config-provider
wh-full
:locale="zhCN"
:date-locale="dateZhCN"
:theme="appStore.isDark ? darkTheme : undefined"
:theme-overrides="naiveThemeOverrides"
>
<n-loading-bar-provider>
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<slot></slot>
<NaiveProviderContent />
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>
<script setup>
import { defineComponent, h } from 'vue'
import {
zhCN,
dateZhCN,
darkTheme,
useLoadingBar,
useDialog,
useMessage,
useNotification,
} from 'naive-ui'
import { useCssVar } from '@vueuse/core'
import { kebabCase } from 'lodash-es'
import { setupMessage, setupDialog } from '@/utils'
import { naiveThemeOverrides } from '~/settings'
import { useAppStore } from '@/store'
const appStore = useAppStore()
function setupCssVar() {
const common = naiveThemeOverrides.common
for (const key in common) {
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
}
}
// naivewindow, 便使
function setupNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$notification = useNotification()
window.$message = setupMessage(useMessage())
window.$dialog = setupDialog(useDialog())
}
const NaiveProviderContent = defineComponent({
setup() {
setupCssVar()
setupNaiveTools()
},
render() {
return h('div')
},
})
</script>

View File

@ -0,0 +1,82 @@
<template>
<div v-if="reloadFlag" class="relative">
<slot></slot>
<div v-show="showPlaceholder" class="absolute-lt h-full w-full" :class="placeholderClass">
<div v-show="loading" class="absolute-center">
<n-spin :show="true" :size="loadingSize" />
</div>
<div v-show="isEmpty" class="absolute-center">
<div class="relative">
<icon-custom-no-data :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ emptyDesc }}</p>
</div>
</div>
<div v-show="!network" class="absolute-center">
<div
class="relative"
:class="{ 'cursor-pointer': showNetworkReload }"
@click="handleReload"
>
<icon-custom-network-error :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ networkErrorDesc }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, watch, onUnmounted } from 'vue'
defineOptions({ name: 'LoadingEmptyWrapper' })
const NETWORK_ERROR_MSG = '网络似乎开了小差~'
const props = {
loading: false,
empty: false,
loadingSize: 'medium',
placeholderClass: 'bg-white dark:bg-dark transition-background-color duration-300 ease-in-out',
emptyDesc: '暂无数据',
iconClass: 'text-320px text-primary',
descClass: 'text-16px text-#666',
showNetworkReload: false,
}
//
const network = ref(window.navigator.onLine)
const reloadFlag = ref(true)
//
const isEmpty = computed(() => props.empty && !props.loading && network.value)
const showPlaceholder = computed(() => props.loading || isEmpty.value || !network.value)
const networkErrorDesc = computed(() =>
props.showNetworkReload ? `${NETWORK_ERROR_MSG}, 点击重试` : NETWORK_ERROR_MSG
)
function handleReload() {
if (!props.showNetworkReload) return
reloadFlag.value = false
nextTick(() => {
reloadFlag.value = true
})
}
const stopHandle = watch(
() => props.loading,
(newValue) => {
//
if (!newValue) {
network.value = window.navigator.onLine
}
}
)
onUnmounted(() => {
stopHandle()
})
</script>
<style scoped></style>

View File

@ -0,0 +1,160 @@
<template>
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
<icon-ic:baseline-keyboard-arrow-left />
</div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right />
</div>
</template>
<div
ref="content"
v-resize="refreshIsOverflow"
class="content"
:class="{ overflow: isOverflow && showArrow }"
:style="{
transform: `translateX(${translateX}px)`,
}"
>
<slot />
</div>
</div>
</template>
<script setup>
import { debounce, useResize } from '@/utils'
defineProps({
showArrow: {
type: Boolean,
default: true,
},
})
const translateX = ref(0)
const content = ref(null)
const wrapper = ref(null)
const isOverflow = ref(false)
const refreshIsOverflow = debounce(() => {
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
isOverflow.value = contentWidth > wrapperWidth
resetTranslateX(wrapperWidth, contentWidth)
}, 200)
function handleMouseWheel(e) {
const { wheelDelta } = e
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
/**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值
* @wrapperWidth 容器的宽度
* @contentWidth 内容的宽度
*/
if (wheelDelta < 0) {
if (wrapperWidth > contentWidth && translateX.value < -10) return
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
}
if (wheelDelta > 0 && translateX.value > 10) {
return
}
translateX.value += wheelDelta
resetTranslateX(wrapperWidth, contentWidth)
}
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
if (!isOverflow.value) {
translateX.value = 0
} else if (-translateX.value > contentWidth - wrapperWidth) {
translateX.value = wrapperWidth - contentWidth
} else if (translateX.value > 0) {
translateX.value = 0
}
}, 200)
const observer = ref(null)
onMounted(() => {
refreshIsOverflow()
observer.value = useResize(document.body, refreshIsOverflow)
})
onBeforeUnmount(() => {
observer.value?.disconnect()
})
function handleScroll(x, width) {
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
if (contentWidth <= wrapperWidth) return
// x
if (x < -translateX.value + 150) {
translateX.value = -(x - 150)
resetTranslateX(wrapperWidth, contentWidth)
}
// x
if (x + width > -translateX.value + wrapperWidth) {
translateX.value = wrapperWidth - (x + width)
resetTranslateX(wrapperWidth, contentWidth)
}
}
defineExpose({
handleScroll,
})
</script>
<style lang="scss" scoped>
.wrapper {
display: flex;
background-color: #fff;
z-index: 9;
overflow: hidden;
position: relative;
.content {
padding: 0 10px;
display: flex;
align-items: center;
flex-wrap: nowrap;
transition: transform 0.5s;
&.overflow {
padding-left: 30px;
padding-right: 30px;
}
}
.left,
.right {
background-color: #fff;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
width: 20px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid #e0e0e6;
border-radius: 2px;
z-index: 2;
cursor: pointer;
}
.left {
left: 0;
}
.right {
right: 0;
}
}
</style>

View File

@ -0,0 +1,22 @@
<script setup>
/** 自定义图标 */
const props = defineProps({
/** 图标名称(assets/svg下的文件名) */
icon: {
type: String,
required: true,
},
size: {
type: Number,
default: 14,
},
color: {
type: String,
default: undefined,
},
})
</script>
<template>
<TheIcon type="custom" v-bind="props" />
</template>

View File

@ -0,0 +1,71 @@
<script setup>
import { ref } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { NInput, NPopover } from 'naive-ui'
import TheIcon from './TheIcon.vue'
import iconData from '@/assets/js/icons'
const props = defineProps({ value: String })
const emit = defineEmits(['update:value'])
const choosed = ref(props.value) //
const icons = ref(iconData)
// const icons = ref(iconData.filter((icon) => icon.includes(choosed.value))) //
function filterIcons() {
console.log('filterIcons', choosed.value)
icons.value = iconData.filter((item) => item.includes(choosed.value))
}
function selectIcon(icon) {
choosed.value = icon
emit('update:value', choosed.value)
}
watchDebounced(
choosed,
() => {
filterIcons()
emit('update:value', choosed.value)
},
{ debounce: 200 }
)
</script>
<template>
<div class="w-full">
<NPopover trigger="click" placement="bottom-start">
<template #trigger>
<NInput v-model:value="choosed" placeholder="请输入图标名称" @update:value="filterIcons">
<template #prefix>
<span class="i-mdi:magnify text-18" />
</template>
<template #suffix>
<TheIcon :icon="choosed" :size="18" />
</template>
</NInput>
</template>
<template #footer>
更多图标去
<a class="text-blue" target="_blank" href="https://icones.js.org/collection/all">
Icones
</a>
查看
</template>
<ul v-if="icons.length" class="h-150 w-300 overflow-y-scroll">
<li
v-for="(icon, index) in icons"
:key="index"
class="mx-5 inline-block cursor-pointer hover:text-cyan"
@click="selectIcon(icon)"
>
<TheIcon :icon="icon" :size="18" />
</li>
</ul>
<div v-else>
<TheIcon :icon="choosed" :size="18" />
</div>
</NPopover>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup>
const props = defineProps({
icon: {
type: String,
required: true,
},
prefix: {
type: String,
default: 'icon-custom',
},
color: {
type: String,
default: 'currentColor',
},
})
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
</script>
<template>
<svg aria-hidden="true" width="1em" height="1em">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>

View File

@ -0,0 +1,22 @@
<script setup>
import { renderIcon } from '@/utils'
defineProps({
icon: {
type: String,
required: true,
},
size: {
type: Number,
default: 14,
},
color: {
type: String,
default: undefined,
},
})
</script>
<template>
<component :is="renderIcon(icon, { size, color })" />
</template>

View File

@ -0,0 +1,18 @@
<template>
<transition name="fade-slide" mode="out-in" appear>
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
<slot />
<AppFooter v-if="showFooter" mt-15 />
<n-back-top :bottom="20" />
</section>
</transition>
</template>
<script setup>
defineProps({
showFooter: {
type: Boolean,
default: false,
},
})
</script>

View File

@ -0,0 +1,33 @@
<template>
<AppPage :show-footer="showFooter">
<header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
<slot v-if="$slots.header" name="header" />
<template v-else>
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
<slot name="action" />
</template>
</header>
<n-card flex-1 rounded-10>
<slot />
</n-card>
</AppPage>
</template>
<script setup>
defineProps({
showFooter: {
type: Boolean,
default: false,
},
showHeader: {
type: Boolean,
default: true,
},
title: {
type: String,
default: undefined,
},
})
const route = useRoute()
</script>

View File

@ -0,0 +1,27 @@
<template>
<div
bg="#fafafc"
min-h-60
flex
items-start
justify-between
b-1
rounded-8
p-15
bc-ccc
dark:bg-black
>
<n-space wrap :size="[35, 15]">
<slot />
</n-space>
<div flex-shrink-0>
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['search', 'reset'])
</script>

Some files were not shown because too many files have changed in this diff Show More