update
This commit is contained in:
parent
6123e6f971
commit
3e00fc440c
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,7 +5,7 @@ venv/
|
|||||||
.vscode
|
.vscode
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
migrations/
|
||||||
|
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
|
|||||||
15
Makefile
15
Makefile
@ -74,4 +74,17 @@ test: ## Run the test suite
|
|||||||
$(eval include .env)
|
$(eval include .env)
|
||||||
$(eval export $(sh sed 's/=.*//' .env))
|
$(eval export $(sh sed 's/=.*//' .env))
|
||||||
|
|
||||||
poetry run pytest -vv -s --cache-clear ./
|
poetry run pytest -vv -s --cache-clear ./
|
||||||
|
|
||||||
|
.PHONY: clean-db
|
||||||
|
clean-db: ## 删除migrations文件夹和db.sqlite3
|
||||||
|
find . -type d -name "migrations" -exec rm -rf {} +
|
||||||
|
rm -f db.sqlite3 db.sqlite3-shm db.sqlite3-wal
|
||||||
|
|
||||||
|
.PHONY: migrate
|
||||||
|
migrate: ## 运行aerich migrate命令生成迁移文件
|
||||||
|
poetry run aerich migrate
|
||||||
|
|
||||||
|
.PHONY: upgrade
|
||||||
|
upgrade: ## 运行aerich upgrade命令应用迁移
|
||||||
|
poetry run aerich upgrade
|
||||||
20
README.md
20
README.md
@ -86,8 +86,8 @@ password:123456
|
|||||||
#### 后端
|
#### 后端
|
||||||
启动项目需要以下环境:
|
启动项目需要以下环境:
|
||||||
- Python 3.11
|
- Python 3.11
|
||||||
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
|
||||||
|
|
||||||
|
#### 方法一(推荐):[Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) 安装依赖
|
||||||
1. 创建虚拟环境
|
1. 创建虚拟环境
|
||||||
```sh
|
```sh
|
||||||
poetry shell
|
poetry shell
|
||||||
@ -100,6 +100,24 @@ poetry install
|
|||||||
```sh
|
```sh
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
#### 方法二:Pip 安装依赖
|
||||||
|
1. 创建虚拟环境
|
||||||
|
```sh
|
||||||
|
python3.11 -m venv venv
|
||||||
|
```
|
||||||
|
2. 激活虚拟环境
|
||||||
|
```sh
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
3. 安装依赖
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
```
|
||||||
|
3. 启动服务
|
||||||
|
```sh
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
服务现在应该正在运行,访问 http://localhost:9999/docs 查看API文档
|
服务现在应该正在运行,访问 http://localhost:9999/docs 查看API文档
|
||||||
|
|
||||||
#### 前端
|
#### 前端
|
||||||
|
|||||||
@ -5,8 +5,7 @@ from tortoise import Tortoise
|
|||||||
|
|
||||||
from app.core.exceptions import SettingNotFound
|
from app.core.exceptions import SettingNotFound
|
||||||
from app.core.init_app import (
|
from app.core.init_app import (
|
||||||
init_menus,
|
init_data,
|
||||||
init_superuser,
|
|
||||||
make_middlewares,
|
make_middlewares,
|
||||||
register_exceptions,
|
register_exceptions,
|
||||||
register_routers,
|
register_routers,
|
||||||
@ -20,10 +19,7 @@ except ImportError:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
await Tortoise.init(config=settings.TORTOISE_ORM)
|
await init_data()
|
||||||
await Tortoise.generate_schemas()
|
|
||||||
await init_superuser()
|
|
||||||
await init_menus()
|
|
||||||
yield
|
yield
|
||||||
await Tortoise.close_connections()
|
await Tortoise.close_connections()
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,12 @@ from fastapi import APIRouter
|
|||||||
from app.core.dependency import DependPermisson
|
from app.core.dependency import DependPermisson
|
||||||
|
|
||||||
from .apis import apis_router
|
from .apis import apis_router
|
||||||
|
from .auditlog import auditlog_router
|
||||||
from .base import base_router
|
from .base import base_router
|
||||||
from .depts import depts_router
|
from .depts import depts_router
|
||||||
from .menus import menus_router
|
from .menus import menus_router
|
||||||
from .roles import roles_router
|
from .roles import roles_router
|
||||||
from .users import users_router
|
from .users import users_router
|
||||||
from .auditlog import auditlog_router
|
|
||||||
|
|
||||||
v1_router = APIRouter()
|
v1_router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
from tortoise.expressions import Q
|
from tortoise.expressions import Q
|
||||||
from app.models.admin import AuditLog
|
|
||||||
|
|
||||||
|
from app.models.admin import AuditLog
|
||||||
from app.schemas import SuccessExtra
|
from app.schemas import SuccessExtra
|
||||||
from app.schemas.apis import *
|
from app.schemas.apis import *
|
||||||
from app.core.dependency import DependPermisson
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get('/list', summary="查看操作日志", dependencies=[DependPermisson])
|
|
||||||
|
@router.get("/list", summary="查看操作日志")
|
||||||
async def get_audit_log_list(
|
async def get_audit_log_list(
|
||||||
page: int = Query(1, description="页码"),
|
page: int = Query(1, description="页码"),
|
||||||
page_size: int = Query(10, description="每页数量"),
|
page_size: int = Query(10, description="每页数量"),
|
||||||
@ -18,7 +18,6 @@ async def get_audit_log_list(
|
|||||||
start_time: str = Query("", description="开始时间"),
|
start_time: str = Query("", description="开始时间"),
|
||||||
end_time: str = Query("", description="结束时间"),
|
end_time: str = Query("", description="结束时间"),
|
||||||
):
|
):
|
||||||
|
|
||||||
q = Q()
|
q = Q()
|
||||||
if username:
|
if username:
|
||||||
q &= Q(username__icontains=username)
|
q &= Q(username__icontains=username)
|
||||||
@ -32,7 +31,7 @@ async def get_audit_log_list(
|
|||||||
q &= Q(created_at__gte=start_time)
|
q &= Q(created_at__gte=start_time)
|
||||||
elif end_time:
|
elif end_time:
|
||||||
q &= Q(created_at__lte=end_time)
|
q &= Q(created_at__lte=end_time)
|
||||||
|
|
||||||
audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at")
|
audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at")
|
||||||
total = await AuditLog.filter(q).count()
|
total = await AuditLog.filter(q).count()
|
||||||
data = [await audit_log.to_dict() for audit_log in audit_log_objs]
|
data = [await audit_log.to_dict() for audit_log in audit_log_objs]
|
||||||
|
|||||||
@ -76,6 +76,6 @@ async def delete_user(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/reset_password", summary="重置密码")
|
@router.post("/reset_password", summary="重置密码")
|
||||||
async def reset_password(user_id: int = Body(..., description="用户ID")):
|
async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
|
||||||
await user_controller.reset_password(user_id)
|
await user_controller.reset_password(user_id)
|
||||||
return Success(msg="密码已重置为123456")
|
return Success(msg="密码已重置为123456")
|
||||||
|
|||||||
@ -16,7 +16,8 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
|||||||
# 删除废弃API数据
|
# 删除废弃API数据
|
||||||
all_api_list = []
|
all_api_list = []
|
||||||
for route in app.routes:
|
for route in app.routes:
|
||||||
if isinstance(route, APIRoute):
|
# 只更新有鉴权的API
|
||||||
|
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||||
all_api_list.append((list(route.methods)[0], route.path_format))
|
all_api_list.append((list(route.methods)[0], route.path_format))
|
||||||
delete_api = []
|
delete_api = []
|
||||||
for api in await Api.all():
|
for api in await Api.all():
|
||||||
@ -28,7 +29,7 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
|||||||
await Api.filter(method=method, path=path).delete()
|
await Api.filter(method=method, path=path).delete()
|
||||||
|
|
||||||
for route in app.routes:
|
for route in app.routes:
|
||||||
if isinstance(route, APIRoute):
|
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||||
method = list(route.methods)[0]
|
method = list(route.methods)[0]
|
||||||
path = route.path_format
|
path = route.path_format
|
||||||
summary = route.summary
|
summary = route.summary
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
from aerich import Command
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware import Middleware
|
from fastapi.middleware import Middleware
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from tortoise.expressions import Q
|
||||||
|
|
||||||
from app.api import api_router
|
from app.api import api_router
|
||||||
|
from app.controllers.api import api_controller
|
||||||
from app.controllers.user import UserCreate, user_controller
|
from app.controllers.user import UserCreate, user_controller
|
||||||
from app.core.exceptions import (
|
from app.core.exceptions import (
|
||||||
DoesNotExist,
|
DoesNotExist,
|
||||||
@ -16,7 +19,7 @@ from app.core.exceptions import (
|
|||||||
ResponseValidationError,
|
ResponseValidationError,
|
||||||
ResponseValidationHandle,
|
ResponseValidationHandle,
|
||||||
)
|
)
|
||||||
from app.models.admin import Menu
|
from app.models.admin import Api, Menu, Role
|
||||||
from app.schemas.menus import MenuType
|
from app.schemas.menus import MenuType
|
||||||
from app.settings.config import settings
|
from app.settings.config import settings
|
||||||
|
|
||||||
@ -152,29 +155,69 @@ async def init_menus():
|
|||||||
is_hidden=False,
|
is_hidden=False,
|
||||||
component="/system/auditlog",
|
component="/system/auditlog",
|
||||||
keepalive=False,
|
keepalive=False,
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
await Menu.bulk_create(children_menu)
|
await Menu.bulk_create(children_menu)
|
||||||
parent_menu = await Menu.create(
|
|
||||||
menu_type=MenuType.CATALOG,
|
|
||||||
name="一级菜单",
|
|
||||||
path="/",
|
|
||||||
order=2,
|
|
||||||
parent_id=0,
|
|
||||||
icon="mdi-fan-speed-1",
|
|
||||||
is_hidden=False,
|
|
||||||
component="Layout",
|
|
||||||
keepalive=False,
|
|
||||||
redirect="",
|
|
||||||
)
|
|
||||||
await Menu.create(
|
await Menu.create(
|
||||||
menu_type=MenuType.MENU,
|
menu_type=MenuType.MENU,
|
||||||
name="一级菜单",
|
name="一级菜单",
|
||||||
path="top-menu",
|
path="/top-menu",
|
||||||
order=1,
|
order=2,
|
||||||
parent_id=parent_menu.id,
|
parent_id=0,
|
||||||
icon="mdi-fan-speed-1",
|
icon="material-symbols:featured-play-list-outline",
|
||||||
is_hidden=False,
|
is_hidden=False,
|
||||||
component="/top-menu",
|
component="/top-menu",
|
||||||
keepalive=False,
|
keepalive=False,
|
||||||
|
redirect="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_apis():
|
||||||
|
apis = await api_controller.model.exists()
|
||||||
|
if not apis:
|
||||||
|
await api_controller.refresh_api()
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
command = Command(tortoise_config=settings.TORTOISE_ORM)
|
||||||
|
try:
|
||||||
|
await command.init_db(safe=True)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await command.init()
|
||||||
|
await command.migrate()
|
||||||
|
await command.upgrade(run_in_transaction=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_roles():
|
||||||
|
roles = await Role.exists()
|
||||||
|
if not roles:
|
||||||
|
admin_role = await Role.create(
|
||||||
|
name="管理员",
|
||||||
|
desc="管理员角色",
|
||||||
|
)
|
||||||
|
user_role = await Role.create(
|
||||||
|
name="普通用户",
|
||||||
|
desc="普通用户角色",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 分配所有API给管理员角色
|
||||||
|
all_apis = await Api.all()
|
||||||
|
await admin_role.apis.add(*all_apis)
|
||||||
|
# 分配所有菜单给管理员和普通用户
|
||||||
|
all_menus = await Menu.all()
|
||||||
|
await admin_role.menus.add(*all_menus)
|
||||||
|
await user_role.menus.add(*all_menus)
|
||||||
|
|
||||||
|
# 为普通用户分配基本API
|
||||||
|
basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块"))
|
||||||
|
await user_role.apis.add(*basic_apis)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_data():
|
||||||
|
await init_db()
|
||||||
|
await init_superuser()
|
||||||
|
await init_menus()
|
||||||
|
await init_apis()
|
||||||
|
await init_roles()
|
||||||
|
|||||||
@ -21,11 +21,6 @@ class User(BaseModel, TimestampMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
table = "user"
|
table = "user"
|
||||||
|
|
||||||
class PydanticMeta:
|
|
||||||
# todo
|
|
||||||
# computed = ["full_name"]
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Role(BaseModel, TimestampMixin):
|
class Role(BaseModel, TimestampMixin):
|
||||||
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
|
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
|
||||||
|
|||||||
@ -20,18 +20,34 @@ class BaseModel(models.Model):
|
|||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
value = value.strftime(settings.DATETIME_FORMAT)
|
value = value.strftime(settings.DATETIME_FORMAT)
|
||||||
d[field] = value
|
d[field] = value
|
||||||
|
|
||||||
if m2m:
|
if m2m:
|
||||||
tasks = [self.__fetch_m2m_field(field) for field in self._meta.m2m_fields if field not in exclude_fields]
|
tasks = [
|
||||||
|
self.__fetch_m2m_field(field, exclude_fields)
|
||||||
|
for field in self._meta.m2m_fields
|
||||||
|
if field not in exclude_fields
|
||||||
|
]
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
for field, values in results:
|
for field, values in results:
|
||||||
d[field] = values
|
d[field] = values
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
async def __fetch_m2m_field(self, field):
|
async def __fetch_m2m_field(self, field, exclude_fields):
|
||||||
values = [value for value in await getattr(self, field).all().values()]
|
values = await getattr(self, field).all().values()
|
||||||
|
formatted_values = []
|
||||||
|
|
||||||
for value in values:
|
for value in values:
|
||||||
value.update((k, v.strftime(settings.DATETIME_FORMAT)) for k, v in value.items() if isinstance(v, datetime))
|
formatted_value = {}
|
||||||
return field, values
|
for k, v in value.items():
|
||||||
|
if k not in exclude_fields:
|
||||||
|
if isinstance(v, datetime):
|
||||||
|
formatted_value[k] = v.strftime(settings.DATETIME_FORMAT)
|
||||||
|
else:
|
||||||
|
formatted_value[k] = v
|
||||||
|
formatted_values.append(formatted_value)
|
||||||
|
|
||||||
|
return field, formatted_values
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|||||||
@ -16,42 +16,77 @@ class Settings(BaseSettings):
|
|||||||
CORS_ALLOW_HEADERS: typing.List = ["*"]
|
CORS_ALLOW_HEADERS: typing.List = ["*"]
|
||||||
|
|
||||||
DEBUG: bool = True
|
DEBUG: bool = 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))
|
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))
|
BASE_DIR: str = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir))
|
||||||
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
|
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
|
||||||
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
|
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
|
||||||
JWT_ALGORITHM: str = "HS256"
|
JWT_ALGORITHM: str = "HS256"
|
||||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 24 * 7 # 7 day
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day
|
||||||
TORTOISE_ORM: dict = {
|
TORTOISE_ORM: dict = {
|
||||||
"connections": {
|
"connections": {
|
||||||
|
# SQLite configuration
|
||||||
"sqlite": {
|
"sqlite": {
|
||||||
"engine": "tortoise.backends.sqlite",
|
"engine": "tortoise.backends.sqlite",
|
||||||
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"},
|
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
|
||||||
}
|
},
|
||||||
|
# MySQL/MariaDB configuration
|
||||||
|
# Install with: tortoise-orm[asyncmy]
|
||||||
|
# "mysql": {
|
||||||
|
# "engine": "tortoise.backends.mysql",
|
||||||
|
# "credentials": {
|
||||||
|
# "host": "localhost", # Database host address
|
||||||
|
# "port": 3306, # Database port
|
||||||
|
# "user": "yourusername", # Database username
|
||||||
|
# "password": "yourpassword", # Database password
|
||||||
|
# "database": "yourdatabase", # Database name
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# PostgreSQL configuration
|
||||||
|
# Install with: tortoise-orm[asyncpg]
|
||||||
|
# "postgres": {
|
||||||
|
# "engine": "tortoise.backends.asyncpg",
|
||||||
|
# "credentials": {
|
||||||
|
# "host": "localhost", # Database host address
|
||||||
|
# "port": 5432, # Database port
|
||||||
|
# "user": "yourusername", # Database username
|
||||||
|
# "password": "yourpassword", # Database password
|
||||||
|
# "database": "yourdatabase", # Database name
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# MSSQL/Oracle configuration
|
||||||
|
# Install with: tortoise-orm[asyncodbc]
|
||||||
|
# "oracle": {
|
||||||
|
# "engine": "tortoise.backends.asyncodbc",
|
||||||
|
# "credentials": {
|
||||||
|
# "host": "localhost", # Database host address
|
||||||
|
# "port": 1433, # Database port
|
||||||
|
# "user": "yourusername", # Database username
|
||||||
|
# "password": "yourpassword", # Database password
|
||||||
|
# "database": "yourdatabase", # Database name
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# SQLServer configuration
|
||||||
|
# Install with: tortoise-orm[asyncodbc]
|
||||||
|
# "sqlserver": {
|
||||||
|
# "engine": "tortoise.backends.asyncodbc",
|
||||||
|
# "credentials": {
|
||||||
|
# "host": "localhost", # Database host address
|
||||||
|
# "port": 1433, # Database port
|
||||||
|
# "user": "yourusername", # Database username
|
||||||
|
# "password": "yourpassword", # Database password
|
||||||
|
# "database": "yourdatabase", # Database name
|
||||||
|
# },
|
||||||
|
# },
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"models": {
|
"models": {
|
||||||
"models": ["app.models"],
|
"models": ["app.models", "aerich.models"],
|
||||||
"default_connection": "sqlite",
|
"default_connection": "sqlite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"use_tz": False,
|
"use_tz": False, # Whether to use timezone-aware datetimes
|
||||||
"timezone": "Asia/Shanghai",
|
"timezone": "Asia/Shanghai", # Timezone setting
|
||||||
}
|
}
|
||||||
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
|
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|||||||
1033
poetry.lock
generated
1033
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@ annotated-types = "^0.6.0"
|
|||||||
setuptools = "^70.0.0"
|
setuptools = "^70.0.0"
|
||||||
uvicorn = "^0.30.1"
|
uvicorn = "^0.30.1"
|
||||||
h11 = "^0.14.0"
|
h11 = "^0.14.0"
|
||||||
|
aerich = "^0.7.2"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
@ -48,4 +49,8 @@ extend-select = [
|
|||||||
ignore = [
|
ignore = [
|
||||||
"F403",
|
"F403",
|
||||||
"F405",
|
"F405",
|
||||||
]
|
]
|
||||||
|
[tool.aerich]
|
||||||
|
tortoise_orm = "app.settings.TORTOISE_ORM"
|
||||||
|
location = "./migrations"
|
||||||
|
src_folder = "./."
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
aiosqlite==0.17.0
|
aiosqlite==0.17.0
|
||||||
annotated-types==0.6.0
|
annotated-types==0.6.0
|
||||||
anyio==4.3.0
|
anyio==4.4.0
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==23.1.0
|
||||||
argon2-cffi-bindings==21.2.0
|
argon2-cffi-bindings==21.2.0
|
||||||
black==23.12.1
|
black==23.12.1
|
||||||
blinker==1.7.0
|
certifi==2024.7.4
|
||||||
certifi==2024.2.2
|
cffi==1.17.0
|
||||||
cffi==1.16.0
|
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
dep-logic==0.2.0
|
|
||||||
distlib==0.3.8
|
|
||||||
dnspython==2.6.1
|
dnspython==2.6.1
|
||||||
email_validator==2.1.1
|
email_validator==2.2.0
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
fastapi-cli==0.0.4
|
fastapi-cli==0.0.5
|
||||||
filelock==3.13.4
|
|
||||||
findpython==0.6.0
|
|
||||||
gunicorn==21.2.0
|
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
hishel==0.0.26
|
|
||||||
httpcore==1.0.5
|
httpcore==1.0.5
|
||||||
|
httptools==0.6.1
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
idna==3.7
|
idna==3.8
|
||||||
installer==0.7.0
|
|
||||||
iso8601==1.1.0
|
iso8601==1.1.0
|
||||||
isort==5.13.2
|
isort==5.13.2
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.4
|
||||||
@ -30,40 +23,35 @@ loguru==0.7.2
|
|||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
MarkupSafe==2.1.5
|
MarkupSafe==2.1.5
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
msgpack==1.0.8
|
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
orjson==3.10.3
|
orjson==3.10.7
|
||||||
packaging==24.0
|
packaging==24.1
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
pathspec==0.12.1
|
pathspec==0.12.1
|
||||||
pbs-installer==2024.4.1
|
|
||||||
pdm==2.14.0
|
|
||||||
platformdirs==4.2.2
|
platformdirs==4.2.2
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
pydantic==2.7.1
|
pydantic==2.7.1
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.4.0
|
||||||
pydantic_core==2.18.2
|
pydantic_core==2.23.0
|
||||||
Pygments==2.18.0
|
Pygments==2.18.0
|
||||||
PyJWT==2.8.0
|
PyJWT==2.9.0
|
||||||
pypika-tortoise==0.1.6
|
pypika-tortoise==0.1.6
|
||||||
pyproject_hooks==1.0.0
|
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
pytz==2024.1
|
pytz==2024.1
|
||||||
resolvelib==1.0.1
|
PyYAML==6.0.2
|
||||||
rich==13.7.1
|
rich==13.8.0
|
||||||
ruff==0.0.281
|
ruff==0.0.281
|
||||||
|
setuptools==70.3.0
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
socksio==1.0.0
|
|
||||||
starlette==0.37.2
|
starlette==0.37.2
|
||||||
tomlkit==0.12.4
|
|
||||||
tortoise-orm==0.20.1
|
tortoise-orm==0.20.1
|
||||||
truststore==0.8.0
|
typer==0.12.5
|
||||||
typer==0.12.3
|
typing_extensions==4.12.2
|
||||||
typing_extensions==4.11.0
|
|
||||||
ujson==5.10.0
|
ujson==5.10.0
|
||||||
unearth==0.15.1
|
uvicorn==0.30.6
|
||||||
uvicorn==0.30.1
|
uvloop==0.20.0
|
||||||
virtualenv==20.25.1
|
watchfiles==0.23.0
|
||||||
zstandard==0.22.0
|
websockets==13.0
|
||||||
|
aerich==0.7.2
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default {
|
|||||||
createUser: (data = {}) => request.post('/user/create', data),
|
createUser: (data = {}) => request.post('/user/create', data),
|
||||||
updateUser: (data = {}) => request.post('/user/update', data),
|
updateUser: (data = {}) => request.post('/user/update', data),
|
||||||
deleteUser: (params = {}) => request.delete(`/user/delete`, { params }),
|
deleteUser: (params = {}) => request.delete(`/user/delete`, { params }),
|
||||||
|
resetPassword: (data = {}) => request.post(`/user/reset_password`, data),
|
||||||
// role
|
// role
|
||||||
getRoleList: (params = {}) => request.get('/role/list', { params }),
|
getRoleList: (params = {}) => request.get('/role/list', { params }),
|
||||||
createRole: (data = {}) => request.post('/role/create', data),
|
createRole: (data = {}) => request.post('/role/create', data),
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
<div v-bind="$attrs">
|
||||||
<slot name="queryBar" />
|
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||||
</QueryBar>
|
<slot name="queryBar" />
|
||||||
|
</QueryBar>
|
||||||
|
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:remote="remote"
|
:remote="remote"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:scroll-x="scrollX"
|
:scroll-x="scrollX"
|
||||||
:row-key="(row) => row[rowKey]"
|
:row-key="(row) => row[rowKey]"
|
||||||
:pagination="isPagination ? pagination : false"
|
:pagination="isPagination ? pagination : false"
|
||||||
@update:checked-row-keys="onChecked"
|
@update:checked-row-keys="onChecked"
|
||||||
@update:page="onPageChange"
|
@update:page="onPageChange"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export const basicRoutes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: () => import('@/views/workbench/index.vue'),
|
component: () => import('@/views/workbench/index.vue'),
|
||||||
name: t('views.workbench.label_workbench'),
|
name: `${t('views.workbench.label_workbench')}Default`,
|
||||||
meta: {
|
meta: {
|
||||||
title: t('views.workbench.label_workbench'),
|
title: t('views.workbench.label_workbench'),
|
||||||
icon: 'icon-park-outline:workbench',
|
icon: 'icon-park-outline:workbench',
|
||||||
@ -36,7 +36,7 @@ export const basicRoutes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: () => import('@/views/profile/index.vue'),
|
component: () => import('@/views/profile/index.vue'),
|
||||||
name: t('views.profile.label_profile'),
|
name: `${t('views.profile.label_profile')}Default`,
|
||||||
meta: {
|
meta: {
|
||||||
title: t('views.profile.label_profile'),
|
title: t('views.profile.label_profile'),
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
|
|||||||
@ -5,33 +5,56 @@ import api from '@/api'
|
|||||||
|
|
||||||
// * 后端路由相关函数
|
// * 后端路由相关函数
|
||||||
// 根据后端传来数据构建出前端路由
|
// 根据后端传来数据构建出前端路由
|
||||||
|
|
||||||
function buildRoutes(routes = []) {
|
function buildRoutes(routes = []) {
|
||||||
return routes.map((e) => ({
|
return routes.map((e) => {
|
||||||
name: e.name,
|
const route = {
|
||||||
path: e.path, // 处理目录是一级菜单的情况
|
name: e.name,
|
||||||
component: shallowRef(Layout), // ? 不使用 shallowRef 控制台会有 warning
|
path: e.path,
|
||||||
isHidden: e.is_hidden,
|
component: shallowRef(Layout),
|
||||||
redirect: e.redirect,
|
isHidden: e.is_hidden,
|
||||||
meta: {
|
redirect: e.redirect,
|
||||||
title: e.name,
|
|
||||||
icon: e.icon,
|
|
||||||
order: e.order,
|
|
||||||
keepAlive: e.keepalive,
|
|
||||||
},
|
|
||||||
children: e.children.map((e_child) => ({
|
|
||||||
name: e_child.name,
|
|
||||||
path: e_child.path, // 父路径 + 当前菜单路径
|
|
||||||
// ! 读取动态加载的路由模块
|
|
||||||
component: vueModules[`/src/views${e_child.component}/index.vue`],
|
|
||||||
isHidden: e_child.is_hidden,
|
|
||||||
meta: {
|
meta: {
|
||||||
title: e_child.name,
|
title: e.name,
|
||||||
icon: e_child.icon,
|
icon: e.icon,
|
||||||
order: e_child.order,
|
order: e.order,
|
||||||
keepAlive: e_child.keepalive,
|
keepAlive: e.keepalive,
|
||||||
},
|
},
|
||||||
})),
|
children: [],
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
if (e.children && e.children.length > 0) {
|
||||||
|
// 有子菜单
|
||||||
|
route.children = e.children.map((e_child) => ({
|
||||||
|
name: e_child.name,
|
||||||
|
path: e_child.path,
|
||||||
|
component: vueModules[`/src/views${e_child.component}/index.vue`],
|
||||||
|
isHidden: e_child.is_hidden,
|
||||||
|
meta: {
|
||||||
|
title: e_child.name,
|
||||||
|
icon: e_child.icon,
|
||||||
|
order: e_child.order,
|
||||||
|
keepAlive: e_child.keepalive,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// 没有子菜单,创建一个默认的子路由
|
||||||
|
route.children.push({
|
||||||
|
name: `${e.name}Default`,
|
||||||
|
path: '',
|
||||||
|
component: vueModules[`/src/views${e.component}/index.vue`],
|
||||||
|
isHidden: true,
|
||||||
|
meta: {
|
||||||
|
title: e.name,
|
||||||
|
icon: e.icon,
|
||||||
|
order: e.order,
|
||||||
|
keepAlive: e.keepalive,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePermissionStore = defineStore('permission', {
|
export const usePermissionStore = defineStore('permission', {
|
||||||
|
|||||||
@ -156,6 +156,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
style: 'margin-right: 8px;',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
style: 'margin-right: 8px;',
|
style: 'margin-left: 8px;',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
console.log('row', row.parent_id)
|
console.log('row', row.parent_id)
|
||||||
if (row.parent_id === 0) {
|
if (row.parent_id === 0) {
|
||||||
@ -122,6 +122,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
style: 'margin-left: 8px;',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import {
|
|||||||
NPopconfirm,
|
NPopconfirm,
|
||||||
NSwitch,
|
NSwitch,
|
||||||
NTreeSelect,
|
NTreeSelect,
|
||||||
|
NRadio,
|
||||||
|
NRadioGroup,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
|
||||||
import CommonPage from '@/components/page/CommonPage.vue'
|
import CommonPage from '@/components/page/CommonPage.vue'
|
||||||
@ -26,7 +28,6 @@ defineOptions({ name: '菜单管理' })
|
|||||||
const $table = ref(null)
|
const $table = ref(null)
|
||||||
const queryItems = ref({})
|
const queryItems = ref({})
|
||||||
const vPermission = resolveDirective('permission')
|
const vPermission = resolveDirective('permission')
|
||||||
const menuDisabled = ref(false)
|
|
||||||
|
|
||||||
// 表单初始化内容
|
// 表单初始化内容
|
||||||
const initForm = {
|
const initForm = {
|
||||||
@ -126,12 +127,11 @@ const columns = [
|
|||||||
size: 'tiny',
|
size: 'tiny',
|
||||||
quaternary: true,
|
quaternary: true,
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
style: `display: ${row.children ? '' : 'none'};`,
|
style: `display: ${row.children && row.menu_type !== 'menu' ? '' : 'none'};`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
initForm.parent_id = row.id
|
initForm.parent_id = row.id
|
||||||
initForm.menu_type = 'menu'
|
initForm.menu_type = 'menu'
|
||||||
showMenuType.value = false
|
showMenuType.value = false
|
||||||
menuDisabled.value = false
|
|
||||||
handleAdd()
|
handleAdd()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -216,7 +216,6 @@ function handleClickAdd() {
|
|||||||
initForm.order = 1
|
initForm.order = 1
|
||||||
initForm.keepalive = true
|
initForm.keepalive = true
|
||||||
showMenuType.value = true
|
showMenuType.value = true
|
||||||
menuDisabled.value = true
|
|
||||||
handleAdd()
|
handleAdd()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +262,12 @@ async function getTreeSelect() {
|
|||||||
:label-width="80"
|
:label-width="80"
|
||||||
:model="modalForm"
|
:model="modalForm"
|
||||||
>
|
>
|
||||||
|
<NFormItem label="菜单类型" path="menu_type">
|
||||||
|
<NRadioGroup v-model:value="modalForm.menu_type">
|
||||||
|
<NRadio label="目录" value="catalog" />
|
||||||
|
<NRadio label="菜单" value="menu" />
|
||||||
|
</NRadioGroup>
|
||||||
|
</NFormItem>
|
||||||
<NFormItem label="上级菜单" path="parent_id">
|
<NFormItem label="上级菜单" path="parent_id">
|
||||||
<NTreeSelect
|
<NTreeSelect
|
||||||
v-model:value="modalForm.parent_id"
|
v-model:value="modalForm.parent_id"
|
||||||
@ -270,7 +275,6 @@ async function getTreeSelect() {
|
|||||||
label-field="name"
|
label-field="name"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
default-expand-all="true"
|
default-expand-all="true"
|
||||||
:disabled="menuDisabled"
|
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem
|
<NFormItem
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
// const Layout = () => import('@/layout/index.vue')
|
|
||||||
|
|
||||||
// export default {
|
|
||||||
// name: 'System',
|
|
||||||
// path: '/system',
|
|
||||||
// component: Layout,
|
|
||||||
// redirect: '/system/user',
|
|
||||||
// meta: {
|
|
||||||
// title: '系统管理',
|
|
||||||
// icon: 'ph:user-list-bold',
|
|
||||||
// order: 5,
|
|
||||||
// // role: ['admin'],
|
|
||||||
// // requireAuth: true,
|
|
||||||
// },
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// name: 'User',
|
|
||||||
// path: 'user',
|
|
||||||
// component: () => import('./user/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// title: '用户列表',
|
|
||||||
// icon: 'mdi:account',
|
|
||||||
// keepAlive: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Menu',
|
|
||||||
// path: 'menu',
|
|
||||||
// component: () => import('./menu/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// title: '菜单列表',
|
|
||||||
// icon: 'ic:twotone-menu-book',
|
|
||||||
// keepAlive: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// }
|
|
||||||
@ -192,6 +192,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
style: 'margin-right: 8px;',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
@ -203,6 +204,40 @@ const columns = [
|
|||||||
default: () => h('div', {}, '确定删除该用户吗?'),
|
default: () => h('div', {}, '确定删除该用户吗?'),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
!row.is_superuser && h(
|
||||||
|
NPopconfirm,
|
||||||
|
{
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await api.resetPassword({ user_id: row.id });
|
||||||
|
$message.success('密码已成功重置为123456');
|
||||||
|
await $table.value?.handleSearch();
|
||||||
|
} catch (error) {
|
||||||
|
$message.error('重置密码失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: () =>
|
||||||
|
withDirectives(
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
style: 'margin-right: 8px;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => '重置密码',
|
||||||
|
icon: renderIcon('material-symbols:delete-outline', { size: 16 }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[[vPermission, 'post/api/v1/user/reset_password']]
|
||||||
|
),
|
||||||
|
default: () => h('div', {}, '确定重置用户密码为123456吗?'),
|
||||||
|
}
|
||||||
|
),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
const Layout = () => import('@/layout/index.vue')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Dashboard',
|
|
||||||
path: '/',
|
|
||||||
component: Layout,
|
|
||||||
redirect: '/workbench',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'Workbench',
|
|
||||||
path: 'workbench',
|
|
||||||
component: () => import('./index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '工作台',
|
|
||||||
icon: 'mdi:home',
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user