first commit
15
.gitignore
vendored
Normal 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
@ -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
@ -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
@ -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
@ -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])
|
||||||
8
app/api/v1/apis/__init__.py
Normal 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
@ -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")
|
||||||
8
app/api/v1/base/__init__.py
Normal 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
@ -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="修改成功")
|
||||||
8
app/api/v1/menus/__init__.py
Normal 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
@ -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")
|
||||||
8
app/api/v1/roles/__init__.py
Normal 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
@ -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")
|
||||||
8
app/api/v1/users/__init__.py
Normal 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
@ -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")
|
||||||
2
app/controllers/__init__.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
|||||||
|
from .log import logger as logger
|
||||||
25
app/log/log.py
Normal 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
@ -0,0 +1 @@
|
|||||||
|
from .admin import *
|
||||||
74
app/models/admin.py
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
|||||||
|
from .base import *
|
||||||
21
app/schemas/apis.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
|||||||
|
from .config import settings as settings
|
||||||
|
|
||||||
|
TORTOISE_ORM = settings.TORTOISE_ORM
|
||||||
56
app/settings/config.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
|
||||||
|
uvicorn app:app --reload --host 0.0.0.0 --port 9999
|
||||||
8
web/.env.development
Normal 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
@ -0,0 +1,11 @@
|
|||||||
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VITE_BASE_API = '/api'
|
||||||
|
|
||||||
|
# 是否启用压缩
|
||||||
|
VITE_USE_COMPRESS = true
|
||||||
|
|
||||||
|
# 压缩类型
|
||||||
|
VITE_COMPRESS_TYPE = gzip
|
||||||
62
web/.eslint-global-variables.json
Normal 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
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
public
|
||||||
|
package.json
|
||||||
25
web/.gitignore
vendored
Normal 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
@ -0,0 +1,3 @@
|
|||||||
|
/node_modules/**
|
||||||
|
/dist/*
|
||||||
|
/public/*
|
||||||
6
web/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
16
web/README.md
Normal 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
|
||||||
|
```
|
||||||
13
web/build/config/define.js
Normal 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_,
|
||||||
|
}
|
||||||
2
web/build/config/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './define'
|
||||||
|
export * from './proxy'
|
||||||
15
web/build/config/proxy.js
Normal 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
@ -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
@ -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
|
||||||
|
}
|
||||||
46
web/build/plugin/unplugin.js
Normal 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__',
|
||||||
|
}),
|
||||||
|
]
|
||||||
15
web/build/script/build-cname.js
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
1
web/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
91
web/public/resource/loading.css
Normal 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;
|
||||||
|
}
|
||||||
25
web/public/resource/loading.js
Normal file
2
web/settings/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './theme.json'
|
||||||
|
export * from './proxy-config'
|
||||||
18
web/settings/proxy-config.js
Normal 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
@ -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
@ -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
@ -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),
|
||||||
|
}
|
||||||
BIN
web/src/assets/images/login_bg.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
230
web/src/assets/js/icons.js
Normal 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',
|
||||||
|
]
|
||||||
1
web/src/assets/svg/forbidden.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
1
web/src/assets/svg/front-page.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
1
web/src/assets/svg/logo.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
web/src/assets/svg/network-error.svg
Normal file
|
After Width: | Height: | Size: 86 KiB |
1
web/src/assets/svg/no-data.svg
Normal file
|
After Width: | Height: | Size: 92 KiB |
1
web/src/assets/svg/not-found.svg
Normal file
|
After Width: | Height: | Size: 71 KiB |
1
web/src/assets/svg/server-error.svg
Normal file
|
After Width: | Height: | Size: 87 KiB |
1
web/src/assets/svg/service-unavailable.svg
Normal file
|
After Width: | Height: | Size: 91 KiB |
1
web/src/assets/svg/unauthorized.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
4
web/src/components/common/AppFooter.vue
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<footer f-c-c flex-col text-14 color="#6a6a6a">
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
67
web/src/components/common/AppProvider.vue
Normal 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] || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载naive组件的方法至window, 以便在全局使用
|
||||||
|
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>
|
||||||
82
web/src/components/common/LoadingEmptyWrapper.vue
Normal 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>
|
||||||
160
web/src/components/common/ScrollX.vue
Normal 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>
|
||||||
22
web/src/components/icon/CustomIcon.vue
Normal 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>
|
||||||
71
web/src/components/icon/IconPicker.vue
Normal 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>
|
||||||
24
web/src/components/icon/SvgIcon.vue
Normal 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>
|
||||||
22
web/src/components/icon/TheIcon.vue
Normal 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>
|
||||||
18
web/src/components/page/AppPage.vue
Normal 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>
|
||||||
33
web/src/components/page/CommonPage.vue
Normal 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>
|
||||||
27
web/src/components/query-bar/QueryBar.vue
Normal 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>
|
||||||