commit
39851544ce
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,7 +5,7 @@ venv/
|
||||
.vscode
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
|
||||
migrations/
|
||||
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
15
Makefile
15
Makefile
@ -74,4 +74,17 @@ test: ## Run the test suite
|
||||
$(eval include .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
|
||||
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
||||
|
||||
#### 方法一(推荐):[Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) 安装依赖
|
||||
1. 创建虚拟环境
|
||||
```sh
|
||||
poetry shell
|
||||
@ -100,6 +100,24 @@ poetry install
|
||||
```sh
|
||||
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文档
|
||||
|
||||
#### 前端
|
||||
|
||||
@ -5,8 +5,7 @@ from tortoise import Tortoise
|
||||
|
||||
from app.core.exceptions import SettingNotFound
|
||||
from app.core.init_app import (
|
||||
init_menus,
|
||||
init_superuser,
|
||||
init_data,
|
||||
make_middlewares,
|
||||
register_exceptions,
|
||||
register_routers,
|
||||
@ -20,10 +19,7 @@ except ImportError:
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await Tortoise.init(config=settings.TORTOISE_ORM)
|
||||
await Tortoise.generate_schemas()
|
||||
await init_superuser()
|
||||
await init_menus()
|
||||
await init_data()
|
||||
yield
|
||||
await Tortoise.close_connections()
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ from fastapi import APIRouter
|
||||
from app.core.dependency import DependPermisson
|
||||
|
||||
from .apis import apis_router
|
||||
from .auditlog import auditlog_router
|
||||
from .base import base_router
|
||||
from .depts import depts_router
|
||||
from .menus import menus_router
|
||||
from .roles import roles_router
|
||||
from .users import users_router
|
||||
from .auditlog import auditlog_router
|
||||
|
||||
v1_router = APIRouter()
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
from fastapi import APIRouter, Query
|
||||
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.apis import *
|
||||
from app.core.dependency import DependPermisson
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get('/list', summary="查看操作日志", dependencies=[DependPermisson])
|
||||
|
||||
@router.get("/list", summary="查看操作日志")
|
||||
async def get_audit_log_list(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -18,7 +18,6 @@ async def get_audit_log_list(
|
||||
start_time: str = Query("", description="开始时间"),
|
||||
end_time: str = Query("", description="结束时间"),
|
||||
):
|
||||
|
||||
q = Q()
|
||||
if username:
|
||||
q &= Q(username__icontains=username)
|
||||
@ -32,7 +31,7 @@ async def get_audit_log_list(
|
||||
q &= Q(created_at__gte=start_time)
|
||||
elif 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")
|
||||
total = await AuditLog.filter(q).count()
|
||||
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="重置密码")
|
||||
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)
|
||||
return Success(msg="密码已重置为123456")
|
||||
|
||||
@ -16,7 +16,8 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
||||
# 删除废弃API数据
|
||||
all_api_list = []
|
||||
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))
|
||||
delete_api = []
|
||||
for api in await Api.all():
|
||||
@ -28,7 +29,7 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
||||
await Api.filter(method=method, path=path).delete()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||
method = list(route.methods)[0]
|
||||
path = route.path_format
|
||||
summary = route.summary
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
from aerich import Command
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.api import api_router
|
||||
from app.controllers.api import api_controller
|
||||
from app.controllers.user import UserCreate, user_controller
|
||||
from app.core.exceptions import (
|
||||
DoesNotExist,
|
||||
@ -16,7 +19,7 @@ from app.core.exceptions import (
|
||||
ResponseValidationError,
|
||||
ResponseValidationHandle,
|
||||
)
|
||||
from app.models.admin import Menu
|
||||
from app.models.admin import Api, Menu, Role
|
||||
from app.schemas.menus import MenuType
|
||||
from app.settings.config import settings
|
||||
|
||||
@ -152,29 +155,69 @@ async def init_menus():
|
||||
is_hidden=False,
|
||||
component="/system/auditlog",
|
||||
keepalive=False,
|
||||
)
|
||||
),
|
||||
]
|
||||
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(
|
||||
menu_type=MenuType.MENU,
|
||||
name="一级菜单",
|
||||
path="top-menu",
|
||||
order=1,
|
||||
parent_id=parent_menu.id,
|
||||
icon="mdi-fan-speed-1",
|
||||
path="/top-menu",
|
||||
order=2,
|
||||
parent_id=0,
|
||||
icon="material-symbols:featured-play-list-outline",
|
||||
is_hidden=False,
|
||||
component="/top-menu",
|
||||
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:
|
||||
table = "user"
|
||||
|
||||
class PydanticMeta:
|
||||
# todo
|
||||
# computed = ["full_name"]
|
||||
...
|
||||
|
||||
|
||||
class Role(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
|
||||
|
||||
@ -20,18 +20,34 @@ class BaseModel(models.Model):
|
||||
if isinstance(value, datetime):
|
||||
value = value.strftime(settings.DATETIME_FORMAT)
|
||||
d[field] = value
|
||||
|
||||
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)
|
||||
for field, values in results:
|
||||
d[field] = values
|
||||
|
||||
return d
|
||||
|
||||
async def __fetch_m2m_field(self, field):
|
||||
values = [value for value in await getattr(self, field).all().values()]
|
||||
async def __fetch_m2m_field(self, field, exclude_fields):
|
||||
values = await getattr(self, field).all().values()
|
||||
formatted_values = []
|
||||
|
||||
for value in values:
|
||||
value.update((k, v.strftime(settings.DATETIME_FORMAT)) for k, v in value.items() if isinstance(v, datetime))
|
||||
return field, values
|
||||
formatted_value = {}
|
||||
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:
|
||||
abstract = True
|
||||
|
||||
@ -16,42 +16,77 @@ class Settings(BaseSettings):
|
||||
CORS_ALLOW_HEADERS: typing.List = ["*"]
|
||||
|
||||
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))
|
||||
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 = 24 * 7 # 7 day
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day
|
||||
TORTOISE_ORM: dict = {
|
||||
"connections": {
|
||||
# SQLite configuration
|
||||
"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": {
|
||||
"models": {
|
||||
"models": ["app.models"],
|
||||
"models": ["app.models", "aerich.models"],
|
||||
"default_connection": "sqlite",
|
||||
},
|
||||
},
|
||||
"use_tz": False,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"use_tz": False, # Whether to use timezone-aware datetimes
|
||||
"timezone": "Asia/Shanghai", # Timezone setting
|
||||
}
|
||||
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"
|
||||
uvicorn = "^0.30.1"
|
||||
h11 = "^0.14.0"
|
||||
aerich = "^0.7.2"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
@ -48,4 +49,8 @@ extend-select = [
|
||||
ignore = [
|
||||
"F403",
|
||||
"F405",
|
||||
]
|
||||
]
|
||||
[tool.aerich]
|
||||
tortoise_orm = "app.settings.TORTOISE_ORM"
|
||||
location = "./migrations"
|
||||
src_folder = "./."
|
||||
|
||||
@ -1,28 +1,21 @@
|
||||
aiosqlite==0.17.0
|
||||
annotated-types==0.6.0
|
||||
anyio==4.3.0
|
||||
anyio==4.4.0
|
||||
argon2-cffi==23.1.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
black==23.12.1
|
||||
blinker==1.7.0
|
||||
certifi==2024.2.2
|
||||
cffi==1.16.0
|
||||
certifi==2024.7.4
|
||||
cffi==1.17.0
|
||||
click==8.1.7
|
||||
dep-logic==0.2.0
|
||||
distlib==0.3.8
|
||||
dnspython==2.6.1
|
||||
email_validator==2.1.1
|
||||
email_validator==2.2.0
|
||||
fastapi==0.111.0
|
||||
fastapi-cli==0.0.4
|
||||
filelock==3.13.4
|
||||
findpython==0.6.0
|
||||
gunicorn==21.2.0
|
||||
fastapi-cli==0.0.5
|
||||
h11==0.14.0
|
||||
hishel==0.0.26
|
||||
httpcore==1.0.5
|
||||
httptools==0.6.1
|
||||
httpx==0.27.0
|
||||
idna==3.7
|
||||
installer==0.7.0
|
||||
idna==3.8
|
||||
iso8601==1.1.0
|
||||
isort==5.13.2
|
||||
Jinja2==3.1.4
|
||||
@ -30,40 +23,35 @@ loguru==0.7.2
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==2.1.5
|
||||
mdurl==0.1.2
|
||||
msgpack==1.0.8
|
||||
mypy-extensions==1.0.0
|
||||
orjson==3.10.3
|
||||
packaging==24.0
|
||||
orjson==3.10.7
|
||||
packaging==24.1
|
||||
passlib==1.7.4
|
||||
pathspec==0.12.1
|
||||
pbs-installer==2024.4.1
|
||||
pdm==2.14.0
|
||||
platformdirs==4.2.2
|
||||
pycparser==2.22
|
||||
pydantic==2.7.1
|
||||
pydantic-settings==2.2.1
|
||||
pydantic_core==2.18.2
|
||||
pydantic-settings==2.4.0
|
||||
pydantic_core==2.23.0
|
||||
Pygments==2.18.0
|
||||
PyJWT==2.8.0
|
||||
PyJWT==2.9.0
|
||||
pypika-tortoise==0.1.6
|
||||
pyproject_hooks==1.0.0
|
||||
python-dotenv==1.0.1
|
||||
python-multipart==0.0.9
|
||||
pytz==2024.1
|
||||
resolvelib==1.0.1
|
||||
rich==13.7.1
|
||||
PyYAML==6.0.2
|
||||
rich==13.8.0
|
||||
ruff==0.0.281
|
||||
setuptools==70.3.0
|
||||
shellingham==1.5.4
|
||||
sniffio==1.3.1
|
||||
socksio==1.0.0
|
||||
starlette==0.37.2
|
||||
tomlkit==0.12.4
|
||||
tortoise-orm==0.20.1
|
||||
truststore==0.8.0
|
||||
typer==0.12.3
|
||||
typing_extensions==4.11.0
|
||||
typer==0.12.5
|
||||
typing_extensions==4.12.2
|
||||
ujson==5.10.0
|
||||
unearth==0.15.1
|
||||
uvicorn==0.30.1
|
||||
virtualenv==20.25.1
|
||||
zstandard==0.22.0
|
||||
uvicorn==0.30.6
|
||||
uvloop==0.20.0
|
||||
watchfiles==0.23.0
|
||||
websockets==13.0
|
||||
aerich==0.7.2
|
||||
|
||||
@ -13,6 +13,7 @@ export default {
|
||||
createUser: (data = {}) => request.post('/user/create', data),
|
||||
updateUser: (data = {}) => request.post('/user/update', data),
|
||||
deleteUser: (params = {}) => request.delete(`/user/delete`, { params }),
|
||||
resetPassword: (data = {}) => request.post(`/user/reset_password`, data),
|
||||
// role
|
||||
getRoleList: (params = {}) => request.get('/role/list', { params }),
|
||||
createRole: (data = {}) => request.post('/role/create', data),
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
<template>
|
||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||
<slot name="queryBar" />
|
||||
</QueryBar>
|
||||
<div v-bind="$attrs">
|
||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||
<slot name="queryBar" />
|
||||
</QueryBar>
|
||||
|
||||
<n-data-table
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:scroll-x="scrollX"
|
||||
:row-key="(row) => row[rowKey]"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
<n-data-table
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:scroll-x="scrollX"
|
||||
:row-key="(row) => row[rowKey]"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@ -17,7 +17,7 @@ export const basicRoutes = [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/workbench/index.vue'),
|
||||
name: t('views.workbench.label_workbench'),
|
||||
name: `${t('views.workbench.label_workbench')}Default`,
|
||||
meta: {
|
||||
title: t('views.workbench.label_workbench'),
|
||||
icon: 'icon-park-outline:workbench',
|
||||
@ -36,7 +36,7 @@ export const basicRoutes = [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/profile/index.vue'),
|
||||
name: t('views.profile.label_profile'),
|
||||
name: `${t('views.profile.label_profile')}Default`,
|
||||
meta: {
|
||||
title: t('views.profile.label_profile'),
|
||||
icon: 'user',
|
||||
|
||||
@ -5,33 +5,56 @@ import api from '@/api'
|
||||
|
||||
// * 后端路由相关函数
|
||||
// 根据后端传来数据构建出前端路由
|
||||
|
||||
function buildRoutes(routes = []) {
|
||||
return routes.map((e) => ({
|
||||
name: e.name,
|
||||
path: e.path, // 处理目录是一级菜单的情况
|
||||
component: shallowRef(Layout), // ? 不使用 shallowRef 控制台会有 warning
|
||||
isHidden: e.is_hidden,
|
||||
redirect: e.redirect,
|
||||
meta: {
|
||||
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,
|
||||
return routes.map((e) => {
|
||||
const route = {
|
||||
name: e.name,
|
||||
path: e.path,
|
||||
component: shallowRef(Layout),
|
||||
isHidden: e.is_hidden,
|
||||
redirect: e.redirect,
|
||||
meta: {
|
||||
title: e_child.name,
|
||||
icon: e_child.icon,
|
||||
order: e_child.order,
|
||||
keepAlive: e_child.keepalive,
|
||||
title: e.name,
|
||||
icon: e.icon,
|
||||
order: e.order,
|
||||
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', {
|
||||
|
||||
@ -156,6 +156,7 @@ const columns = [
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-right: 8px;',
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
|
||||
@ -90,7 +90,7 @@ const columns = [
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-right: 8px;',
|
||||
style: 'margin-left: 8px;',
|
||||
onClick: () => {
|
||||
console.log('row', row.parent_id)
|
||||
if (row.parent_id === 0) {
|
||||
@ -122,6 +122,7 @@ const columns = [
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 8px;',
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
|
||||
@ -9,6 +9,8 @@ import {
|
||||
NPopconfirm,
|
||||
NSwitch,
|
||||
NTreeSelect,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
} from 'naive-ui'
|
||||
|
||||
import CommonPage from '@/components/page/CommonPage.vue'
|
||||
@ -26,7 +28,6 @@ defineOptions({ name: '菜单管理' })
|
||||
const $table = ref(null)
|
||||
const queryItems = ref({})
|
||||
const vPermission = resolveDirective('permission')
|
||||
const menuDisabled = ref(false)
|
||||
|
||||
// 表单初始化内容
|
||||
const initForm = {
|
||||
@ -126,12 +127,11 @@ const columns = [
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
type: 'primary',
|
||||
style: `display: ${row.children ? '' : 'none'};`,
|
||||
style: `display: ${row.children && row.menu_type !== 'menu' ? '' : 'none'};`,
|
||||
onClick: () => {
|
||||
initForm.parent_id = row.id
|
||||
initForm.menu_type = 'menu'
|
||||
showMenuType.value = false
|
||||
menuDisabled.value = false
|
||||
handleAdd()
|
||||
},
|
||||
},
|
||||
@ -216,7 +216,6 @@ function handleClickAdd() {
|
||||
initForm.order = 1
|
||||
initForm.keepalive = true
|
||||
showMenuType.value = true
|
||||
menuDisabled.value = true
|
||||
handleAdd()
|
||||
}
|
||||
|
||||
@ -263,6 +262,12 @@ async function getTreeSelect() {
|
||||
:label-width="80"
|
||||
: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">
|
||||
<NTreeSelect
|
||||
v-model:value="modalForm.parent_id"
|
||||
@ -270,7 +275,6 @@ async function getTreeSelect() {
|
||||
label-field="name"
|
||||
:options="menuOptions"
|
||||
default-expand-all="true"
|
||||
:disabled="menuDisabled"
|
||||
/>
|
||||
</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',
|
||||
type: 'error',
|
||||
style: 'margin-right: 8px;',
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
@ -203,6 +204,40 @@ const columns = [
|
||||
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