This commit is contained in:
mizhexiaoxiao 2024-09-10 17:17:27 +08:00
parent 6123e6f971
commit 3e00fc440c
25 changed files with 1109 additions and 451 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -86,8 +86,8 @@ password123456
#### 后端 #### 后端
启动项目需要以下环境: 启动项目需要以下环境:
- 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文档
#### 前端 #### 前端

View File

@ -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()

View File

@ -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()

View File

@ -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]

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 = "./."

View File

@ -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

View File

@ -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),

View File

@ -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>

View File

@ -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',

View File

@ -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', {

View File

@ -156,6 +156,7 @@ const columns = [
{ {
size: 'small', size: 'small',
type: 'error', type: 'error',
style: 'margin-right: 8px;',
}, },
{ {
default: () => '删除', default: () => '删除',

View File

@ -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: () => '删除',

View File

@ -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

View File

@ -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,
// },
// },
// ],
// }

View File

@ -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吗?'),
}
),
] ]
}, },
}, },

View File

@ -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,
},
},
],
}