Compare commits

..

2 Commits

Author SHA1 Message Date
jayhgq
fa88dd8ec2 refactor: 重构权限管理模块为菜单管理模块
1. 前端将权限管理页面替换为菜单管理页面,更新路由配置
2. 后端角色删除逻辑优化,新增角色存在性检查
3. 后端权限删除逻辑增强,自动清理关联数据并调整子权限层级
4. 更新初始化权限数据中的菜单管理配置
2026-06-10 09:47:29 +08:00
jayhgq
12a577f9a3 feat: 实现动态菜单功能,替换硬编码菜单
1.  后端新增menu字段接口和route权限字段,重构权限接口返回菜单树
2.  前端替换硬编码侧边菜单为动态渲染,新增菜单store和图标映射逻辑
3.  调整权限路由元信息和初始化权限数据
2026-06-09 15:57:23 +08:00
9 changed files with 288 additions and 98 deletions

View File

@ -107,6 +107,70 @@ async def get_permission_tree(
return tree
@router.get("/menu", response_model=list[PermissionTreeResponse])
async def get_menu(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取当前用户的菜单树形结构(仅包含有路由的权限)"""
# 获取用户角色的权限
result = await db.execute(
select(Role)
.options(selectinload(Role.permissions))
.filter(Role.id == current_user.role_id)
)
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
# 获取用户有权限的权限编码集合
user_permission_codes = {perm.code for perm in role.permissions}
# 获取所有顶级模块和二级模块权限有route字段的
result = await db.execute(
select(Permission)
.filter(Permission.route.isnot(None))
)
all_menu_permissions = result.scalars().all()
# 过滤用户有权限的菜单
user_menu_permissions = []
for perm in all_menu_permissions:
# 检查用户是否有该权限或其子权限
has_access = False
for user_perm_code in user_permission_codes:
if user_perm_code.startswith(perm.code):
has_access = True
break
if has_access:
user_menu_permissions.append(perm)
# 转换为字典,便于查找
perm_dict = {perm.id: {
"id": perm.id,
"name": perm.name,
"code": perm.code,
"parentid": perm.parentid,
"route": perm.route,
"description": perm.description,
"children": []
} for perm in user_menu_permissions}
# 构建树形结构
tree = []
for perm_id, perm_data in perm_dict.items():
parentid = perm_data["parentid"]
if parentid == 0:
# 顶级节点
tree.append(perm_data)
elif parentid in perm_dict:
# 子节点,添加到父节点的 children
perm_dict[parentid]["children"].append(perm_data)
return tree
@router.get("/{permission_id}", response_model=PermissionResponse)
async def get_permission(
permission_id: int,
@ -154,15 +218,33 @@ async def delete_permission(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""删除权限"""
"""删除权限(自动清理角色关联和子权限关联)"""
# 检查权限是否存在
result = await db.execute(select(Permission).filter(Permission.id == permission_id))
existing_permission = result.scalar_one_or_none()
if not existing_permission:
raise HTTPException(status_code=404, detail="权限不存在")
# 使用FastCRUD删除权限
deleted = await permission_crud.delete(db, id=permission_id)
# 1. 删除角色-权限关联表中的记录
from sqlalchemy import delete as sql_delete
from models import role_permission
stmt = sql_delete(role_permission).where(role_permission.c.permission_id == permission_id)
await db.execute(stmt)
# 2. 处理子权限如果有子权限将子权限的父ID置为0提升为顶级节点
from sqlalchemy import update as sql_update
stmt = (
sql_update(Permission)
.where(Permission.parentid == permission_id)
.values(parentid=0)
)
await db.execute(stmt)
# 3. 使用FastCRUD删除权限本身不提交与前述操作在同一事务中
await permission_crud.delete(db, id=permission_id, commit=False)
await db.commit()
return {"message": "权限删除成功"}
@ -227,7 +309,7 @@ async def get_current_user_permissions(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取当前登录用户的权限列表"""
"""获取当前登录用户的权限列表和菜单"""
# 获取用户角色的权限
result = await db.execute(
select(Role)
@ -239,9 +321,54 @@ async def get_current_user_permissions(
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
# 返回用户权限信息
# 获取用户有权限的权限编码集合
user_permission_codes = {perm.code for perm in role.permissions}
# 获取所有顶级模块和二级模块权限有route字段的
result = await db.execute(
select(Permission)
.filter(Permission.route.isnot(None))
)
all_menu_permissions = result.scalars().all()
# 过滤用户有权限的菜单
user_menu_permissions = []
for perm in all_menu_permissions:
# 检查用户是否有该权限或其子权限
has_access = False
for user_perm_code in user_permission_codes:
if user_perm_code.startswith(perm.code):
has_access = True
break
if has_access:
user_menu_permissions.append(perm)
# 转换为字典,便于查找
perm_dict = {perm.id: {
"id": perm.id,
"name": perm.name,
"code": perm.code,
"parentid": perm.parentid,
"route": perm.route,
"description": perm.description,
"children": []
} for perm in user_menu_permissions}
# 构建树形结构
menu_tree = []
for perm_id, perm_data in perm_dict.items():
parentid = perm_data["parentid"]
if parentid == 0:
# 顶级节点
menu_tree.append(perm_data)
elif parentid in perm_dict:
# 子节点,添加到父节点的 children
perm_dict[parentid]["children"].append(perm_data)
# 返回用户权限信息和菜单
return RolePermissionResponse(
role_id=role.id,
role_name=role.name,
permissions=[PermissionResponse.from_orm(p) for p in role.permissions]
)
permissions=[PermissionResponse.from_orm(p) for p in role.permissions],
menu=menu_tree
)

View File

@ -125,9 +125,13 @@ async def delete_role(
current_user: User = Depends(get_current_user),
):
"""删除角色"""
# 检查是否有用户使用此角色
from sqlalchemy import select
# 检查角色是否存在
result = await db.execute(select(Role).filter(Role.id == role_id))
existing_role = result.scalar_one_or_none()
if not existing_role:
raise HTTPException(status_code=404, detail="角色不存在")
# 检查是否有用户使用此角色
result = await db.execute(select(User).filter(User.role_id == role_id))
users = result.scalars().all()
if users:
@ -135,14 +139,6 @@ async def delete_role(
status_code=400, detail=f"该角色正在被 {len(users)} 个用户使用,无法删除"
)
# 使用原生 SQLAlchemy 删除角色FastCRUD 有问题)
from sqlalchemy import delete as sql_delete
stmt = sql_delete(Role).where(Role.id == role_id)
result = await db.execute(stmt)
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="角色不存在")
await db.commit()
# 使用FastCRUD删除角色
await role_crud.delete(db, id=role_id)
return {"message": "角色删除成功"}

View File

@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from models import Role, User, Permission
from models import Role, User, Permission, SystemSetting
from utils.password import get_password_hash
@ -32,6 +32,14 @@ async def ensure_permission_table_structure(db: AsyncSession):
except Exception as e:
print(f"添加 parentid 字段失败: {e}")
# 添加 route 字段
if 'route' not in existing_columns:
try:
await db.execute(text("ALTER TABLE permission ADD COLUMN IF NOT EXISTS route VARCHAR(255)"))
print("添加 route 字段成功")
except Exception as e:
print(f"添加 route 字段失败: {e}")
# 删除旧字段(如果存在)
for old_column in ['module', 'submodule', 'action']:
if old_column in existing_columns:
@ -53,21 +61,21 @@ async def init_permissions(db: AsyncSession):
# ========================================
# 顶级模块
# ========================================
{"name": "权限管理", "code": "perm", "parentid": 0, "description": "权限管理模块"},
{"name": "系统设置", "code": "system", "parentid": 0, "description": "系统设置模块"},
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块"},
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块"},
# ========================================
# 权限管理 -> 二级子模块
# ========================================
{"name": "用户管理", "code": "perm:user", "parentid": 1, "description": "用户管理子模块"},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "description": "角色管理子模块"},
{"name": "权限配置", "code": "perm:config", "parentid": 1, "description": "权限配置子模块"},
{"name": "用户管理", "code": "perm:user", "parentid": 1, "route": "/admin/users", "description": "用户管理子模块"},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "route": "/admin/roles", "description": "角色管理子模块"},
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块"},
# ========================================
# 系统设置 -> 二级子模块
# ========================================
{"name": "基础配置", "code": "system:basic", "parentid": 2, "description": "系统基础配置"},
{"name": "高级配置", "code": "system:advanced", "parentid": 2, "description": "系统高级配置"},
{"name": "基础配置", "code": "system:basic", "parentid": 2, "route": "/admin/settings", "description": "系统基础配置"},
{"name": "高级配置", "code": "system:advanced", "parentid": 2, "route": "/admin/settings", "description": "系统高级配置"},
# ========================================
# 用户管理操作parentid=3
@ -96,13 +104,13 @@ async def init_permissions(db: AsyncSession):
# ========================================
# 权限配置操作parentid=5
# ========================================
{"name": "查看列表", "code": "perm:config:list", "parentid": 5, "description": "查看权限列表"},
{"name": "查看详情", "code": "perm:config:detail", "parentid": 5, "description": "查看权限详情"},
{"name": "搜索", "code": "perm:config:search", "parentid": 5, "description": "搜索权限"},
{"name": "创建", "code": "perm:config:create", "parentid": 5, "description": "创建新权限"},
{"name": "编辑", "code": "perm:config:edit", "parentid": 5, "description": "编辑权限信息"},
{"name": "删除", "code": "perm:config:delete", "parentid": 5, "description": "删除权限"},
{"name": "导出", "code": "perm:config:export", "parentid": 5, "description": "导岀权限数据"},
{"name": "查看列表", "code": "perm:menu:list", "parentid": 5, "description": "查看权限列表"},
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 5, "description": "查看权限详情"},
{"name": "搜索", "code": "perm:menu:search", "parentid": 5, "description": "搜索权限"},
{"name": "创建", "code": "perm:menu:create", "parentid": 5, "description": "创建新权限"},
{"name": "编辑", "code": "perm:menu:edit", "parentid": 5, "description": "编辑权限信息"},
{"name": "删除", "code": "perm:menu:delete", "parentid": 5, "description": "删除权限"},
{"name": "导出", "code": "perm:menu:export", "parentid": 5, "description": "导岀权限数据"},
# ========================================
# 系统设置 -> 基础配置操作parentid=6
@ -143,6 +151,9 @@ async def init_permissions(db: AsyncSession):
if existing_perm.parentid != perm_data["parentid"]:
existing_perm.parentid = perm_data["parentid"]
update_needed = True
if existing_perm.route != perm_data.get("route"):
existing_perm.route = perm_data.get("route")
update_needed = True
if existing_perm.description != perm_data["description"]:
existing_perm.description = perm_data["description"]
update_needed = True
@ -383,6 +394,60 @@ async def init_test_user(db: AsyncSession):
print("测试用户初始化/更新完成")
async def init_system_settings(db: AsyncSession):
"""初始化系统设置数据"""
# 定义需要初始化的系统设置
system_settings = [
{
"key": "系统标题",
"value": "图书管理系统",
"type": "string",
"description": "系统显示标题"
},
]
# 检查现有系统设置
result = await db.execute(select(SystemSetting))
existing_settings = result.scalars().all()
existing_setting_keys = {setting.key: setting for setting in existing_settings}
# 创建或更新系统设置
created_count = 0
updated_count = 0
for setting_data in system_settings:
setting_key = setting_data["key"]
if setting_key not in existing_setting_keys:
# 设置不存在,需要创建
db.add(SystemSetting(**setting_data))
created_count += 1
else:
# 设置存在,检查是否需要更新
existing_setting = existing_setting_keys[setting_key]
update_needed = False
if existing_setting.value != setting_data["value"]:
existing_setting.value = setting_data["value"]
update_needed = True
if existing_setting.type != setting_data["type"]:
existing_setting.type = setting_data["type"]
update_needed = True
if existing_setting.description != setting_data.get("description"):
existing_setting.description = setting_data.get("description")
update_needed = True
if update_needed:
updated_count += 1
if created_count > 0 or updated_count > 0:
await db.commit()
print(f"创建了 {created_count} 个系统设置,更新了 {updated_count} 个系统设置")
else:
print("系统设置数据已存在且无需更新,跳过初始化")
print("系统设置数据初始化/更新完成")
async def init_all_data(db: AsyncSession):
"""初始化所有数据"""
print("开始初始化数据库数据...")
@ -392,4 +457,5 @@ async def init_all_data(db: AsyncSession):
await init_role_permissions(db)
await init_admin_user(db)
await init_test_user(db)
await init_system_settings(db)
print("数据库数据初始化完成")

View File

@ -59,6 +59,7 @@ class Permission(Base):
name = Column(String(100), nullable=False, comment="权限名称")
code = Column(String(100), nullable=False, unique=True, comment="权限编码")
parentid = Column(Integer, default=0, comment="父权限ID0表示顶级模块")
route = Column(String(255), nullable=True, comment="前端路由路径")
description = Column(String(255), nullable=True, comment="权限描述")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(

View File

@ -139,6 +139,7 @@ class PermissionBase(BaseModel):
name: str = Field(..., max_length=100, description="权限名称")
code: str = Field(..., max_length=100, description="权限编码")
parentid: int = Field(0, description="父权限ID0表示顶级模块")
route: Optional[str] = Field(None, max_length=255, description="前端路由路径")
description: Optional[str] = Field(None, max_length=255, description="权限描述")
@ -165,6 +166,7 @@ class PermissionTreeResponse(BaseModel):
name: str = Field(..., description="权限名称")
code: str = Field(..., description="权限编码")
parentid: int = Field(..., description="父权限ID")
route: Optional[str] = Field(None, description="前端路由路径")
description: Optional[str] = Field(None, description="权限描述")
children: list = Field([], description="子权限列表")
@ -183,6 +185,7 @@ class RolePermissionResponse(BaseModel):
role_id: int = Field(..., description="角色ID")
role_name: str = Field(..., description="角色名称")
permissions: list[PermissionResponse] = Field(..., description="权限列表")
menu: list = Field([], description="菜单树形结构")
# 系统设置相关模型

View File

@ -9,47 +9,34 @@
text-color="#e2e8f0"
active-text-color="#38bdf8"
>
<!-- 权限管理一级菜单 -->
<el-sub-menu
v-if="hasAnyPermission"
index="/admin/permissions"
>
<template #title>
<el-icon><Key /></el-icon>
<span>权限管理</span>
</template>
<el-menu-item
v-if="hasUserPermission"
index="/admin/users"
:route="{ name: 'admin-users' }"
<!-- 动态渲染菜单 -->
<template v-for="menu in auth.menu" :key="menu.id">
<!-- 有子菜单的顶级菜单 -->
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
:index="menu.route"
>
<span>用户管理</span>
</el-menu-item>
<template #title>
<el-icon><component :is="getIcon(menu.code)" /></el-icon>
<span>{{ menu.name }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.id"
:index="child.route"
>
<span>{{ child.name }}</span>
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单的顶级菜单 -->
<el-menu-item
v-if="hasRolePermission"
index="/admin/roles"
:route="{ name: 'admin-roles' }"
v-else
:index="menu.route"
>
<span>角色管理</span>
<el-icon><component :is="getIcon(menu.code)" /></el-icon>
<span>{{ menu.name }}</span>
</el-menu-item>
<el-menu-item
v-if="hasPermissionPermission"
index="/admin/permissions"
:route="{ name: 'admin-permissions' }"
>
<span>权限管理</span>
</el-menu-item>
</el-sub-menu>
<!-- 系统设置一级菜单 -->
<el-menu-item
v-if="hasSystemPermission"
index="/admin/settings"
:route="{ name: 'admin-settings' }"
>
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<el-container>
@ -72,9 +59,9 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computed, onMounted, ref, markRaw } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Key, Setting } from "@element-plus/icons-vue";
import { Key, Setting, Folder } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { useAuthStore } from "@/modules/admin/stores/auth";
import axios from "axios";
@ -87,25 +74,21 @@ const sysTitle = ref("");
const active = computed(() => route.path);
const pageTitle = computed(() => (route.meta.title as string) || "");
const hasUserPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:user"));
});
//
const iconMap: Record<string, any> = {
"perm": markRaw(Key),
"system": markRaw(Setting),
};
const hasRolePermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:role"));
});
const hasPermissionPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:config"));
});
const hasSystemPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("system:"));
});
const hasAnyPermission = computed(() => {
return hasUserPermission.value || hasRolePermission.value || hasPermissionPermission.value;
});
function getIcon(code: string) {
//
for (const key in iconMap) {
if (code.startsWith(key)) {
return iconMap[key];
}
}
return Folder;
}
async function getSysTitle() {
const baseURL = import.meta.env.VITE_API_BASE || "/api";
@ -115,10 +98,9 @@ async function getSysTitle() {
}
}
onMounted(async () => {
try {
getSysTitle();
await getSysTitle();
if (!auth.user) {
await auth.fetchProfile();
}

View File

@ -19,16 +19,28 @@ export type Permission = {
description?: string | null;
};
export type MenuItem = {
id: number;
name: string;
code: string;
parentid: number;
route: string;
description?: string;
children: MenuItem[];
};
export type RolePermissionResponse = {
role_id: number;
role_name: string;
permissions: Permission[];
menu: MenuItem[];
};
export const useAuthStore = defineStore("auth", () => {
const token = ref<string>(localStorage.getItem("access_token") || "");
const user = ref<UserProfile | null>(null);
const permissions = ref<Permission[]>([]);
const menu = ref<MenuItem[]>([]);
const isAuthenticated = computed(() => Boolean(token.value));
@ -52,7 +64,8 @@ export const useAuthStore = defineStore("auth", () => {
async function fetchPermissions() {
const { data } = await http.get<RolePermissionResponse>("/permissions/user/me");
permissions.value = data.permissions || [];
return permissions.value;
menu.value = data.menu || [];
return { permissions: permissions.value, menu: menu.value };
}
async function loginAndFetchData() {
@ -76,6 +89,7 @@ export const useAuthStore = defineStore("auth", () => {
}
user.value = null;
permissions.value = [];
menu.value = [];
setToken("");
}
@ -83,6 +97,7 @@ export const useAuthStore = defineStore("auth", () => {
token,
user,
permissions,
menu,
permissionCodes,
isAuthenticated,
setToken,

View File

@ -39,11 +39,11 @@ const router = createRouter({
},
{
path: "permissions",
name: "admin-permissions",
meta: { title: "权限管理", permission: "perm:config" },
path: "menu",
name: "admin-menu",
meta: { title: "菜单管理", permission: "perm:menu" },
component: () =>
import("@/modules/admin/views/permissions/PermissionManage.vue"),
import("@/modules/admin/views/permissions/MenuManage.vue"),
},
{
path: "settings",