login page
This commit is contained in:
commit
a1c23c8cf8
142
backend/.gitignore
vendored
Normal file
142
backend/.gitignore
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
.idea
|
||||
.pdm.toml
|
||||
__pypackages__/
|
17
backend/controller/__init__.py
Normal file
17
backend/controller/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
from core.security import check_token
|
||||
|
||||
|
||||
def register_routers(app: FastAPI):
|
||||
from controller.common import common
|
||||
from controller.menu import menu
|
||||
from controller.role import role
|
||||
from controller.user import user
|
||||
|
||||
app.include_router(router=common)
|
||||
app.include_router(
|
||||
router=user,
|
||||
)
|
||||
app.include_router(router=menu)
|
||||
app.include_router(router=role)
|
16
backend/controller/common.py
Normal file
16
backend/controller/common.py
Normal file
@ -0,0 +1,16 @@
|
||||
from core.resp import Response
|
||||
from core.router import Router
|
||||
from core.security import generate_token, verify_password
|
||||
from dbhelper.user import get_user
|
||||
from schemas.common import LoginForm, LoginResult
|
||||
|
||||
common = Router(tags=["公共接口"])
|
||||
|
||||
|
||||
@common.post("/login", summary="登录")
|
||||
async def login(auth_data: LoginForm) -> Response[LoginResult]:
|
||||
user_obj = await get_user({"username": auth_data.username})
|
||||
if user_obj:
|
||||
if verify_password(auth_data.password, user_obj.password):
|
||||
return dict(id=user_obj.id, access_token=generate_token(user_obj.username))
|
||||
return Response(msg="账号或密码错误")
|
31
backend/controller/menu.py
Normal file
31
backend/controller/menu.py
Normal file
@ -0,0 +1,31 @@
|
||||
from core.resp import Response
|
||||
from core.router import Router
|
||||
from schemas.common import QueryData
|
||||
from schemas.menu import MenuIn, MenuRead
|
||||
|
||||
menu = Router(prefix="/menu", tags=["菜单管理"])
|
||||
|
||||
|
||||
@menu.post("", summary="菜单添加")
|
||||
async def menu_add(data: MenuIn) -> Response[MenuRead]:
|
||||
pass
|
||||
|
||||
|
||||
@menu.get("/{pk}", summary="菜单详情")
|
||||
async def menu_info(pk: int) -> Response[MenuRead]:
|
||||
pass
|
||||
|
||||
|
||||
@menu.delete("/{pk}", summary="删除菜单")
|
||||
async def menu_del(pk: int) -> Response:
|
||||
pass
|
||||
|
||||
|
||||
@menu.put("/{pk}", summary="编辑菜单")
|
||||
async def menu_put(pk: int, data: MenuIn) -> Response[MenuRead]:
|
||||
pass
|
||||
|
||||
|
||||
@menu.post("/list", summary="查询菜单列表")
|
||||
async def menu_list(data: QueryData) -> Response[list[MenuRead]]:
|
||||
pass
|
53
backend/controller/role.py
Normal file
53
backend/controller/role.py
Normal file
@ -0,0 +1,53 @@
|
||||
import json
|
||||
|
||||
from core.resp import Response
|
||||
from core.router import Router
|
||||
from core.utils import list_to_tree
|
||||
from dbhelper.role import get_role_menus
|
||||
from schemas.common import QueryData
|
||||
from schemas.role import RoleAdd, RoleInfo
|
||||
|
||||
role = Router(prefix="/role", tags=["角色管理"])
|
||||
|
||||
|
||||
@role.post("", summary="角色添加")
|
||||
async def role_add(data: RoleAdd) -> Response[RoleInfo]:
|
||||
pass
|
||||
|
||||
|
||||
@role.get("/{pk}", summary="角色详情")
|
||||
async def role_info(pk: int) -> Response[RoleInfo]:
|
||||
pass
|
||||
|
||||
|
||||
@role.delete("/{pk}", summary="删除角色")
|
||||
async def role_del(pk: int) -> Response:
|
||||
pass
|
||||
|
||||
|
||||
@role.put("/{pk}", summary="编辑角色")
|
||||
async def role_put(pk: int, data: RoleAdd) -> Response[RoleInfo]:
|
||||
pass
|
||||
|
||||
|
||||
@role.post("/list", summary="查询角色列表")
|
||||
async def role_list(data: QueryData) -> Response[list[RoleInfo]]:
|
||||
pass
|
||||
|
||||
|
||||
@role.get("/{pk}/menu", summary="查询角色菜单权限")
|
||||
async def role_menu(pk: int):
|
||||
menus = await get_role_menus(pk)
|
||||
for obj in menus:
|
||||
obj["meta"] = json.loads(obj["meta"]) if obj["meta"] is not None else None
|
||||
return Response(data=list_to_tree(menus))
|
||||
|
||||
|
||||
@role.get("/{pk}/menuIds", summary="查询角色菜单ids")
|
||||
async def role_menus_id():
|
||||
pass
|
||||
|
||||
|
||||
@role.get("/assign", summary="分配权限")
|
||||
async def role_assign():
|
||||
pass
|
41
backend/controller/user.py
Normal file
41
backend/controller/user.py
Normal file
@ -0,0 +1,41 @@
|
||||
from core.resp import Response
|
||||
from core.router import Router
|
||||
from dbhelper.user import get_user_info, get_users, insert_user
|
||||
from schemas.common import ListAll
|
||||
from schemas.user import UserAdd, UserInfo, UserList, UserQuery
|
||||
|
||||
user = Router(prefix="/users", tags=["用户管理"])
|
||||
|
||||
|
||||
@user.post("", summary="用户添加")
|
||||
async def user_add(data: UserAdd) -> Response[UserInfo]:
|
||||
roles = data.rids
|
||||
del data.rids
|
||||
return await insert_user(data, roles)
|
||||
|
||||
|
||||
@user.get("/{pk}", summary="用户详情")
|
||||
async def user_info(pk: int) -> Response[UserInfo]:
|
||||
try:
|
||||
return Response(data=await get_user_info(pk))
|
||||
except Exception as e:
|
||||
return Response(msg=f"用户不存在 {e}")
|
||||
|
||||
|
||||
@user.delete("/{pk}", summary="删除用户")
|
||||
async def user_del(pk: int) -> Response:
|
||||
pass
|
||||
|
||||
|
||||
@user.put("/{pk}", summary="编辑用户")
|
||||
async def user_put(pk: int, data: UserAdd) -> Response[UserInfo]:
|
||||
pass
|
||||
|
||||
|
||||
@user.post("/list", summary="查询用户列表")
|
||||
async def user_list(query: UserQuery) -> Response[ListAll[UserList]]:
|
||||
limit = query.size
|
||||
skip = (query.offset - 1) * limit
|
||||
del query.offset, query.size
|
||||
users, count = await get_users(skip, limit, query.dict())
|
||||
return Response(data=ListAll(total=count, items=users))
|
0
backend/core/__init__.py
Normal file
0
backend/core/__init__.py
Normal file
35
backend/core/enums.py
Normal file
35
backend/core/enums.py
Normal file
@ -0,0 +1,35 @@
|
||||
import enum
|
||||
|
||||
|
||||
class Status(enum.IntEnum):
|
||||
"""
|
||||
数据库状态枚举值
|
||||
9 删除 5 无效 1 有效 3 使用
|
||||
"""
|
||||
|
||||
DELETED: int = 9
|
||||
INACTIVE: int = 5
|
||||
ACTIVE: int = 1
|
||||
SELECTED: int = 3
|
||||
|
||||
|
||||
class UserType(enum.IntEnum):
|
||||
"""
|
||||
数据库超级管理员枚举
|
||||
0 超级管理员 1用户
|
||||
"""
|
||||
|
||||
ADMIN: int = 0
|
||||
USER: int = 1
|
||||
|
||||
|
||||
class MenuType(enum.IntEnum):
|
||||
"""
|
||||
菜单类型枚举
|
||||
目录 0
|
||||
组件 1 按钮 2
|
||||
"""
|
||||
|
||||
DIR = 0
|
||||
CPN = 1
|
||||
BTN = 2
|
13
backend/core/events.py
Normal file
13
backend/core/events.py
Normal file
@ -0,0 +1,13 @@
|
||||
from tortoise import Tortoise
|
||||
|
||||
|
||||
async def init_orm():
|
||||
"""初始化orm"""
|
||||
await Tortoise.init(
|
||||
db_url="sqlite://mini.db", modules={"models": ["models"]}
|
||||
)
|
||||
|
||||
|
||||
async def close_orm():
|
||||
"""关闭orm"""
|
||||
await Tortoise.close_connections()
|
6
backend/core/exceptions.py
Normal file
6
backend/core/exceptions.py
Normal file
@ -0,0 +1,6 @@
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
|
||||
class TokenAuthFailure(HTTPException):
|
||||
status_code = 401
|
||||
detail = "认证失败"
|
15
backend/core/log.py
Normal file
15
backend/core/log.py
Normal file
@ -0,0 +1,15 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
fmt = logging.Formatter(
|
||||
fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setLevel(logging.DEBUG)
|
||||
sh.setFormatter(fmt)
|
||||
|
||||
# will print debug sql
|
||||
logger_db_client = logging.getLogger("mini-rbac")
|
||||
logger_db_client.setLevel(logging.DEBUG)
|
||||
logger_db_client.addHandler(sh)
|
12
backend/core/middleware.py
Normal file
12
backend/core/middleware.py
Normal file
@ -0,0 +1,12 @@
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
middlewares = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
]
|
32
backend/core/resp.py
Normal file
32
backend/core/resp.py
Normal file
@ -0,0 +1,32 @@
|
||||
import enum
|
||||
from typing import Generic, Optional, TypeVar, Union
|
||||
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Status(enum.IntEnum):
|
||||
OK = 200
|
||||
CREATED = 201
|
||||
ACCEPTED = 202
|
||||
NO_CONTENT = 204
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
INTERNAL_SERVER_ERROR = 500
|
||||
NOT_IMPLEMENTED = 501
|
||||
BAD_GATEWAY = 502
|
||||
SERVICE_UNAVAILABLE = 503
|
||||
|
||||
|
||||
class Msg(enum.Enum):
|
||||
OK = "OK"
|
||||
FAIL = "FAIL"
|
||||
|
||||
|
||||
class Response(GenericModel, Generic[T]):
|
||||
code: Status = Status.OK
|
||||
msg: Union[Msg, str] = Msg.OK
|
||||
data: Optional[T]
|
48
backend/core/router.py
Normal file
48
backend/core/router.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import TYPE_CHECKING, Any, Callable, get_type_hints
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
"""
|
||||
根据类型标注自动返回模型
|
||||
https://github.com/tiangolo/fastapi/issues/620
|
||||
"""
|
||||
|
||||
|
||||
class Router(APIRouter):
|
||||
"""
|
||||
装饰器用法
|
||||
@app.get("/")
|
||||
def index() -> User:
|
||||
"""
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
def add_api_route(
|
||||
self, path: str, endpoint: Callable[..., Any], **kwargs: Any
|
||||
) -> None:
|
||||
if kwargs.get("response_model") is None:
|
||||
kwargs["response_model"] = get_type_hints(endpoint).get("return")
|
||||
return super().add_api_route(path, endpoint, **kwargs)
|
||||
|
||||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
class Route(APIRoute):
|
||||
"""
|
||||
Django挂载视图方法
|
||||
def index() -> User:
|
||||
pass
|
||||
Route("/", endpoint=index)
|
||||
"""
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
def __init__(self, path: str, endpoint: Callable[..., Any], **kwargs: Any):
|
||||
if kwargs.get("response_model") is None:
|
||||
kwargs["response_model"] = get_type_hints(endpoint).get("return")
|
||||
super(Route, self).__init__(path=path, endpoint=endpoint, **kwargs)
|
||||
|
||||
else: # pragma: no cover
|
||||
pass
|
68
backend/core/security.py
Normal file
68
backend/core/security.py
Normal file
@ -0,0 +1,68 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from core.exceptions import TokenAuthFailure
|
||||
from dbhelper.user import get_user
|
||||
|
||||
# JWT
|
||||
SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU"
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 *24 * 7
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
bearer = HTTPBearer()
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
验证明文密码 vs hash密码
|
||||
:param plain_password: 明文密码
|
||||
:param hashed_password: hash密码
|
||||
:return:
|
||||
"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""
|
||||
加密明文
|
||||
:param password: 明文密码
|
||||
:return:
|
||||
"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def generate_token(username: str, expires_delta: Optional[timedelta] = None):
|
||||
"""生成token"""
|
||||
to_encode = {"sub": username}.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode.update(dict(exp=expire))
|
||||
encoded_jwt = jwt.encode(
|
||||
to_encode, SECRET_KEY, algorithm=ALGORITHM
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)):
|
||||
"""检查用户token"""
|
||||
token = security.credentials
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, SECRET_KEY, algorithms=[ALGORITHM]
|
||||
)
|
||||
username: str = payload.get("sub")
|
||||
return await get_user({"username": username})
|
||||
except JWTError:
|
||||
raise TokenAuthFailure
|
19
backend/core/table.py
Normal file
19
backend/core/table.py
Normal file
@ -0,0 +1,19 @@
|
||||
from tortoise import fields, models
|
||||
|
||||
from core.enums import Status
|
||||
|
||||
|
||||
class Table(models.Model):
|
||||
"""
|
||||
抽象模型
|
||||
"""
|
||||
|
||||
id = fields.IntField(pk=True, description="主键")
|
||||
status = fields.IntEnumField(Status, description="状态", default=Status.ACTIVE)
|
||||
created = fields.DatetimeField(auto_now_add=True, description="创建时间", null=True)
|
||||
modified = fields.DatetimeField(auto_now=True, description="更新时间", null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ["-created"]
|
||||
indexes = ("status",)
|
51
backend/core/utils.py
Normal file
51
backend/core/utils.py
Normal file
@ -0,0 +1,51 @@
|
||||
def list_to_tree(
|
||||
menus, parent_flag: str = "pid", children_key: str = "children"
|
||||
) -> list:
|
||||
"""
|
||||
list 结构转 树结构
|
||||
:param menus: [{id:1, pid: 3}]
|
||||
:param parent_flag: 节点关系字段
|
||||
:param children_key: 生成树结构的子节点字段
|
||||
:return: list 类型的 树嵌套数据
|
||||
""" ""
|
||||
# 先转成字典 id作为key, 数据作为value
|
||||
menu_map = {menu["id"]: menu for menu in menus}
|
||||
arr = []
|
||||
for menu in menus:
|
||||
|
||||
# 有父级
|
||||
if mid := menu.get(parent_flag):
|
||||
# 有 子项的情况
|
||||
if result := menu_map[mid].get(children_key):
|
||||
result.append(menu)
|
||||
else:
|
||||
# 无子项的情况
|
||||
menu_map[mid][children_key] = [menu]
|
||||
else:
|
||||
arr.append(menu)
|
||||
return arr
|
||||
|
||||
|
||||
def menu_table():
|
||||
"""生成菜单表数据"""
|
||||
from models import MenuModel
|
||||
MenuModel.bulk_create([
|
||||
MenuModel(name="系统管理",
|
||||
meta={"icon": "Grid"},
|
||||
path="/system",
|
||||
type=0),
|
||||
MenuModel(name="系统设置",
|
||||
meta={"icon": "Setting"},
|
||||
path="/setting",
|
||||
type=0),
|
||||
MenuModel(name="菜单管理",
|
||||
meta={"icon": "Menu"},
|
||||
path="/system/menu",
|
||||
type=1,
|
||||
component="/system/menu",
|
||||
pid=1,
|
||||
api="/menu",
|
||||
method="{'GET}",
|
||||
regx="^/menu$"
|
||||
)
|
||||
])
|
0
backend/dbhelper/__init__.py
Normal file
0
backend/dbhelper/__init__.py
Normal file
15
backend/dbhelper/role.py
Normal file
15
backend/dbhelper/role.py
Normal file
@ -0,0 +1,15 @@
|
||||
from tortoise import connections
|
||||
|
||||
|
||||
async def get_role_menus(rid: int):
|
||||
"""
|
||||
根据角色id 获取菜单
|
||||
"""
|
||||
db = connections.get("default")
|
||||
return await db.execute_query_dict(
|
||||
"""
|
||||
select m.id, m.name, m.meta, m.path, m.type, m.component, m.pid, m.identifier, m.api_regx,m.api, m.method, m.sort
|
||||
FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid
|
||||
AND sys_role_menu.rid = (%s) AND m.`status` = 1 ORDER BY m.sort""",
|
||||
[rid],
|
||||
)
|
72
backend/dbhelper/user.py
Normal file
72
backend/dbhelper/user.py
Normal file
@ -0,0 +1,72 @@
|
||||
from tortoise.transactions import atomic
|
||||
|
||||
from core.enums import Status
|
||||
from models import RoleModel, UserModel, UserRoleModel
|
||||
from schemas.user import UserRole
|
||||
|
||||
|
||||
async def get_user(kwargs):
|
||||
"""
|
||||
根据条件查询到第一条符合结果的数据
|
||||
Args:
|
||||
kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return await UserModel.filter(**kwargs).first()
|
||||
|
||||
|
||||
async def get_user_info(pk: int):
|
||||
"""
|
||||
根据id查用户角色列表,当前激活角色
|
||||
"""
|
||||
user = await UserModel.get(pk=pk).values(
|
||||
"id", "username", "nickname", "identity", "created", "modified"
|
||||
)
|
||||
role = (
|
||||
await UserRoleModel.filter(uid=pk, status__not_in=[9, 5])
|
||||
.all()
|
||||
.values("rid", "status")
|
||||
)
|
||||
active_rid = role[0].get("rid")
|
||||
rids = []
|
||||
for obj in role:
|
||||
if obj.get("status") == Status.SELECTED:
|
||||
active_rid = obj.get("rid")
|
||||
rids.append(obj.get("rid"))
|
||||
return {**user, "active_rid": active_rid, "rids": rids}
|
||||
|
||||
|
||||
async def get_users(skip: int, limit: int, kwargs: dict):
|
||||
"""
|
||||
分页获取用户并且支持字段模糊查询
|
||||
Args:
|
||||
skip: 偏移量
|
||||
limit: 数量
|
||||
kwargs: 查询字典
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
kwargs = {f"{k}__contains": v for k, v in kwargs.items()}
|
||||
result = (
|
||||
UserModel.filter(status__not_in=[9, 5], **kwargs).all().order_by("-created")
|
||||
)
|
||||
return await result.offset(skip).limit(limit), await result.count()
|
||||
|
||||
|
||||
@atomic()
|
||||
async def insert_user(user, roles):
|
||||
for index, rid in enumerate(roles):
|
||||
# 1. 查角色表是否有该角色
|
||||
await RoleModel.get(pk=rid)
|
||||
# 创建用户
|
||||
obj = await UserModel.create(**user.dict())
|
||||
|
||||
user_role = UserRole(rid=rid, uid=obj.id)
|
||||
if index == 0:
|
||||
user_role.status = Status.SELECTED
|
||||
# 第一个角色默认, 添加到关系表
|
||||
await UserRoleModel.create(**user_role.dict())
|
||||
return user
|
41
backend/main.py
Normal file
41
backend/main.py
Normal file
@ -0,0 +1,41 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
|
||||
from controller import register_routers
|
||||
from core.events import close_orm, init_orm
|
||||
from core.log import logger_db_client
|
||||
from core.utils import menu_table
|
||||
|
||||
app = FastAPI(
|
||||
on_startup=[init_orm, menu_table],
|
||||
on_shutdown=[close_orm],
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
)
|
||||
|
||||
register_routers(app)
|
||||
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title + " - Swagger UI",
|
||||
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
||||
swagger_js_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/4.10.3/swagger-ui-bundle.js",
|
||||
swagger_css_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/4.10.3/swagger-ui.css",
|
||||
)
|
||||
|
||||
|
||||
for i in app.routes:
|
||||
logger_db_client.debug(i.__dict__)
|
||||
logger_db_client.info(f"{i.path}, {i.methods}, {i.path_regex}")
|
||||
"""
|
||||
'path_regex': re.compile('^/role/(?P<
|
||||
pk>[^/]+)/menu$'), 'path_format': '/role/{pk}/menu',
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", reload=True)
|
BIN
backend/mini.db
Normal file
BIN
backend/mini.db
Normal file
Binary file not shown.
4
backend/models/__init__.py
Normal file
4
backend/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from models.menu import MenuModel
|
||||
from models.relation import RoleMenuModel, UserRoleModel
|
||||
from models.role import RoleModel
|
||||
from models.user import UserModel
|
25
backend/models/menu.py
Normal file
25
backend/models/menu.py
Normal file
@ -0,0 +1,25 @@
|
||||
from core.enums import MenuType
|
||||
from core.table import Table, fields
|
||||
|
||||
|
||||
class MenuModel(Table):
|
||||
"""
|
||||
菜单表
|
||||
"""
|
||||
|
||||
name = fields.CharField(max_length=20, description="名称", null=True)
|
||||
meta = fields.JSONField(description="元数据信息", null=True)
|
||||
path = fields.CharField(max_length=128, description="菜单url", null=True)
|
||||
type = fields.IntEnumField(MenuType, description="菜单类型")
|
||||
component = fields.CharField(max_length=128, description="组件地址", null=True)
|
||||
pid = fields.IntField(description="父id", null=True)
|
||||
identifier = fields.CharField(max_length=30, description="权限标识 user:add", null=True)
|
||||
api = fields.CharField(max_length=128, description="接口地址", null=True)
|
||||
method = fields.CharField(max_length=10, description="接口请求方式", null=True)
|
||||
regx = fields.CharField(max_length=50, description="接口地址正则表达式", null=True)
|
||||
|
||||
class Meta:
|
||||
table = "sys_menu"
|
||||
table_description = "菜单表"
|
||||
# 非唯一的索引
|
||||
indexes = ("type", "name")
|
25
backend/models/relation.py
Normal file
25
backend/models/relation.py
Normal file
@ -0,0 +1,25 @@
|
||||
from core.table import Table, fields
|
||||
|
||||
|
||||
class RoleRelationMixin:
|
||||
rid = fields.IntField(description="角色id")
|
||||
|
||||
|
||||
class UserRoleModel(Table, RoleRelationMixin):
|
||||
"""用户角色关系表"""
|
||||
|
||||
uid = fields.IntField(description="用户id")
|
||||
|
||||
class Meta:
|
||||
table = "sys_user_role"
|
||||
indexes = ("uid", "rid")
|
||||
|
||||
|
||||
class RoleMenuModel(Table, RoleRelationMixin):
|
||||
"""角色菜单(权限)关系表"""
|
||||
|
||||
mid = fields.IntField(description="菜单ID")
|
||||
|
||||
class Meta:
|
||||
table = "sys_role_menu"
|
||||
indexes = ("mid", "rid")
|
14
backend/models/role.py
Normal file
14
backend/models/role.py
Normal file
@ -0,0 +1,14 @@
|
||||
from core.table import Table, fields
|
||||
|
||||
|
||||
class RoleModel(Table):
|
||||
"""
|
||||
角色表
|
||||
"""
|
||||
|
||||
name = fields.CharField(max_length=20, description="角色名称")
|
||||
remark = fields.CharField(max_length=200, description="角色描述")
|
||||
|
||||
class Meta:
|
||||
table = "sys_role"
|
||||
table_description = "角色表"
|
19
backend/models/user.py
Normal file
19
backend/models/user.py
Normal file
@ -0,0 +1,19 @@
|
||||
from core.enums import UserType
|
||||
from core.table import Table, fields
|
||||
|
||||
|
||||
class UserModel(Table):
|
||||
"""
|
||||
用户模型类 > user table
|
||||
"""
|
||||
|
||||
username = fields.CharField(max_length=16, description="账号", unique=True)
|
||||
nickname = fields.CharField(max_length=20, description="姓名", null=True)
|
||||
password = fields.CharField(max_length=128, description="密码")
|
||||
|
||||
class Meta:
|
||||
table = "sys_user"
|
||||
table_description = "用户表"
|
||||
# 索引
|
||||
unique_together = ("username",)
|
||||
|
23
backend/requirements.txt
Normal file
23
backend/requirements.txt
Normal file
@ -0,0 +1,23 @@
|
||||
aiosqlite==0.17.0
|
||||
anyio==3.6.1
|
||||
bcrypt==4.0.0
|
||||
click==8.1.3
|
||||
colorama==0.4.5
|
||||
ecdsa==0.18.0
|
||||
fastapi==0.82.0
|
||||
h11==0.13.0
|
||||
idna==3.3
|
||||
iso8601==1.0.2
|
||||
passlib==1.7.4
|
||||
pyasn1==0.4.8
|
||||
pydantic==1.10.2
|
||||
pypika-tortoise==0.1.6
|
||||
python-jose==3.3.0
|
||||
pytz==2022.2.1
|
||||
rsa==4.9
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
starlette==0.19.1
|
||||
tortoise-orm==0.19.2
|
||||
typing-extensions==4.3.0
|
||||
uvicorn==0.18.3
|
0
backend/schemas/__init__.py
Normal file
0
backend/schemas/__init__.py
Normal file
36
backend/schemas/common.py
Normal file
36
backend/schemas/common.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""公共模型"""
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class LoginForm(BaseModel):
|
||||
"""用户登录参数"""
|
||||
|
||||
username: str = Field(..., description="账号", max_length=12, min_length=3)
|
||||
password: str = Field(..., description="密码", min_length=6, max_length=16)
|
||||
|
||||
|
||||
class LoginResult(BaseModel):
|
||||
"""登录响应模型"""
|
||||
|
||||
id: int = Field(..., description="用户ID")
|
||||
access_token: str = Field(..., description="token 串")
|
||||
token_type: str = Field("Bearer", description="token 类型")
|
||||
|
||||
|
||||
class QueryData(BaseModel):
|
||||
"""分页查询基础数据"""
|
||||
|
||||
offset: int = 1
|
||||
size: int = 10
|
||||
|
||||
|
||||
class ListAll(GenericModel, Generic[T]):
|
||||
"""查列表时的模型"""
|
||||
|
||||
total: int = Field(..., description="总数")
|
||||
items: T = Field(..., description="数据列表")
|
6
backend/schemas/menu.py
Normal file
6
backend/schemas/menu.py
Normal file
@ -0,0 +1,6 @@
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
|
||||
from models import MenuModel
|
||||
|
||||
MenuRead = pydantic_model_creator(MenuModel, name="MenuOut")
|
||||
MenuIn = pydantic_model_creator(MenuModel, name="MenuIn", exclude_readonly=True)
|
16
backend/schemas/role.py
Normal file
16
backend/schemas/role.py
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
from pydantic import Field
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
|
||||
from models import RoleModel
|
||||
|
||||
RoleRed = pydantic_model_creator(RoleModel, name="RoleOut")
|
||||
RoleIn = pydantic_model_creator(RoleModel, name="RoleIn", exclude_readonly=True)
|
||||
|
||||
|
||||
class RoleAdd(RoleIn):
|
||||
menus: list[int] = Field(..., description="菜单列表")
|
||||
|
||||
|
||||
class RoleInfo(RoleRed):
|
||||
pass
|
29
backend/schemas/user.py
Normal file
29
backend/schemas/user.py
Normal file
@ -0,0 +1,29 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import Field
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
|
||||
from models import UserModel, UserRoleModel
|
||||
from schemas.common import QueryData
|
||||
|
||||
UserRead = pydantic_model_creator(UserModel, name="UserOut", exclude=("password",))
|
||||
UserIn = pydantic_model_creator(UserModel, name="UserIn", exclude_readonly=True)
|
||||
|
||||
UserRole = pydantic_model_creator(UserRoleModel, name="UserRole", exclude_readonly=True)
|
||||
|
||||
|
||||
class UserInfo(UserRead):
|
||||
active_rid: int = Field(..., description="用户当前激活角色")
|
||||
rids: List[int] = Field(..., description="用户拥有角色")
|
||||
|
||||
|
||||
class UserAdd(UserIn):
|
||||
rids: List[int] = Field(..., description="用户角色列表")
|
||||
|
||||
|
||||
class UserQuery(QueryData):
|
||||
username: Optional[str] = Field("", description="用户名")
|
||||
nickname: Optional[str] = Field("", description="姓名")
|
||||
|
||||
|
||||
UserList = List[UserRead]
|
1
frontend/.env.development
Normal file
1
frontend/.env.development
Normal file
@ -0,0 +1 @@
|
||||
VITE_BASE_URL = /api
|
1
frontend/.env.production
Normal file
1
frontend/.env.production
Normal file
@ -0,0 +1 @@
|
||||
VITE_BASE_URL = http://127.0.0.1:8000/api/
|
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# 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
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
39
frontend/README.md
Normal file
39
frontend/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
### 极简的权限管理
|
||||
1. 前端项目参考Vue-elment-admin
|
||||
|
||||
#### 系统管理
|
||||
- [] 用户管理
|
||||
- [] 角色管理
|
||||
- [] 菜单管理
|
||||
|
||||
#### 系统设置
|
||||
- [] 系统监控
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
10
frontend/jsconfig.json
Normal file
10
frontend/jsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
2883
frontend/package-lock.json
generated
Normal file
2883
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"element-plus": "^2.2.16",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.21",
|
||||
"pinia-plugin-persistedstate": "^2.2.0",
|
||||
"vue": "^3.2.38",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"unplugin-vue-components": "^0.22.4",
|
||||
"vite": "^3.0.9"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
12
frontend/src/App.vue
Normal file
12
frontend/src/App.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
6
frontend/src/assets/base.css
Normal file
6
frontend/src/assets/base.css
Normal file
@ -0,0 +1,6 @@
|
||||
body,html, #app{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
15
frontend/src/main.js
Normal file
15
frontend/src/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import 'normalize.css'
|
||||
import '@/assets/base.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
17
frontend/src/router/index.js
Normal file
17
frontend/src/router/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login.vue')
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: routes
|
||||
})
|
||||
|
||||
export default router
|
36
frontend/src/service/request.js
Normal file
36
frontend/src/service/request.js
Normal file
@ -0,0 +1,36 @@
|
||||
import axios from "axios";
|
||||
import {message, ElLoading} from 'element-plus'
|
||||
import userStore from '@/stores/user'
|
||||
|
||||
|
||||
const store = userStore()
|
||||
|
||||
export default (config) => {
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
instance.interceptors.request.use(config => {
|
||||
ElLoading.service({
|
||||
title: '请求中.'
|
||||
})
|
||||
config.headers.Authorization = store.accessToken
|
||||
return config
|
||||
})
|
||||
|
||||
instance.interceptors.response.use(res => {
|
||||
if (res.data.code !== 20000 ){
|
||||
message.error(res.data.msg)
|
||||
}
|
||||
ElLoading.close()
|
||||
return res.data
|
||||
}, err => {
|
||||
message.error(err)
|
||||
ElLoading.close()
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
return instance(config)
|
||||
}
|
23
frontend/src/stores/user.js
Normal file
23
frontend/src/stores/user.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const userStore = defineStore('user', () => {
|
||||
const info = ref({})
|
||||
const token = ref("")
|
||||
const accessToken = computed(() => 'Bearer ' + token)
|
||||
|
||||
return { info, token, accessToken }
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
||||
|
||||
// export const userStore = defineStore('user',{
|
||||
// state: () => ({
|
||||
// token: ""
|
||||
// }),
|
||||
// getters: {
|
||||
// accessToken() {
|
||||
// return `Bearer ${this.token}`
|
||||
// }
|
||||
// }
|
||||
// })
|
79
frontend/src/views/login.vue
Normal file
79
frontend/src/views/login.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
import {ref,reactive} from 'vue'
|
||||
|
||||
// 表单配置
|
||||
const rules = {
|
||||
username: [
|
||||
{required: true, message: '请输入用户名', trigger: 'blur'},
|
||||
{min:5, max:20, message: '5~20', trigger: 'blur'}
|
||||
],
|
||||
password: [
|
||||
{required: true, message: '请输入密码', trigger: 'blur'},
|
||||
{min:6, max:12, message: '6~12', trigger: 'blur'}
|
||||
]
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
|
||||
// 事件
|
||||
const submitForm = (formEl) => {
|
||||
if (!formEl) return
|
||||
formEl.validate( valid => {
|
||||
if (valid) {
|
||||
// 验证通过
|
||||
console.log('submit!')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="continer">
|
||||
<h1>Mini RBAC</h1>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" status-icon>
|
||||
<el-form-item prop="username">
|
||||
<el-input placeholder="用户名" clearable :prefix-icon="User"
|
||||
v-model.trim="formData.username"/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input placeholder="密码" show-password :prefix-icon="Lock"
|
||||
v-model.trim="formData.password"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm(formRef)" >登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
display: flex;
|
||||
background-color: #2d3a4b;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.continer {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
.continer h1{
|
||||
color: #fff;
|
||||
}
|
||||
.continer .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
34
frontend/vite.config.js
Normal file
34
frontend/vite.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}), Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: { // 代理
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'ws://localhost:5000',
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user