From 0ae78b2d6f636bcb35df9f5b63b3b67114fd46e4 Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Thu, 30 Jul 2026 14:38:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(role):=20=E6=B7=BB=E5=8A=A0=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E8=8F=9C=E5=8D=95=E5=88=86=E9=85=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RoleAssignMenu 结构体用于角色菜单分配参数 - 实现 assign_menu 和 get_role_menu_ids 后端命令接口 - 在前端角色管理页面添加分配菜单弹窗功能 - 集成菜单树形结构展示和多选分配逻辑 - 更新用户角色分配接口为批量分配模式 - 优化表格容器滚动样式为水平自适应 --- src-tauri/src/commands/role.rs | 86 ++++++++++++++++++++++++------ src-tauri/src/commands/user.rs | 24 +++++---- src/api/system/role.ts | 8 +++ src/api/system/user.ts | 11 ++-- src/views/system/SysMenu.vue | 2 +- src/views/system/SysRole.vue | 95 ++++++++++++++++++++++++++++++++-- src/views/system/SysUser.vue | 8 +-- 7 files changed, 189 insertions(+), 45 deletions(-) diff --git a/src-tauri/src/commands/role.rs b/src-tauri/src/commands/role.rs index ddc6f8d..0644d02 100644 --- a/src-tauri/src/commands/role.rs +++ b/src-tauri/src/commands/role.rs @@ -57,6 +57,12 @@ pub struct RoleSaveReq { pub status: bool, } +#[derive(Debug, Deserialize)] +pub struct RoleAssignMenu { + pub role_id: i64, + pub menu_ids: Vec, +} + #[tauri::command] #[auto_collect_command] pub async fn role_save( @@ -72,14 +78,15 @@ pub async fn role_save( if exist > 0 { return Ok(ApiResponse::error(2001, "角色_key已存在")); } - let result = sqlx::query("INSERT INTO sys_role (role_key, role_name, sort, status) VALUES (?, ?, ?, ?)") - .bind(¶ms.role_key) - .bind(¶ms.role_name) - .bind(¶ms.sort) - .bind(¶ms.status) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; + let result = + sqlx::query("INSERT INTO sys_role (role_key, role_name, sort, status) VALUES (?, ?, ?, ?)") + .bind(¶ms.role_key) + .bind(¶ms.role_name) + .bind(¶ms.sort) + .bind(¶ms.status) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; let id = result.last_insert_id() as i64; Ok(ApiResponse::success(id)) } @@ -181,15 +188,17 @@ pub async fn role_update( return Ok(ApiResponse::error(2001, "角色_key已存在")); } - sqlx::query("UPDATE sys_role SET role_key = ?, role_name = ?, sort = ?, status = ? WHERE id = ?") - .bind(¶ms.role_key) - .bind(¶ms.role_name) - .bind(¶ms.sort) - .bind(¶ms.status) - .bind(¶ms.id) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; + sqlx::query( + "UPDATE sys_role SET role_key = ?, role_name = ?, sort = ?, status = ? WHERE id = ?", + ) + .bind(¶ms.role_key) + .bind(¶ms.role_name) + .bind(¶ms.sort) + .bind(¶ms.status) + .bind(¶ms.id) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; Ok(ApiResponse::success(())) } @@ -214,3 +223,46 @@ pub async fn role_list(pool: State<'_, MySqlPool>) -> Result, + params: RoleAssignMenu, +) -> Result, String> { + sqlx::query("DELETE FROM sys_role_menu WHERE role_id = ?") + .bind(params.role_id) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + + if params.menu_ids.is_empty() { + return Ok(ApiResponse::<()>::success_empty()); + } + + let mut query_builder = QueryBuilder::new("INSERT INTO sys_role_menu (role_id, menu_id)"); + query_builder.push_values(¶ms.menu_ids, |mut b, menu_id| { + b.push_bind(params.role_id).push_bind(menu_id); + }); + let _ = query_builder + .build() + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::<()>::success_empty()) +} + +#[tauri::command] +#[auto_collect_command] +pub async fn get_role_menu_ids( + pool: State<'_, MySqlPool>, + role_id: i64, +) -> Result>, String> { + let ids: Vec = + sqlx::query_scalar::<_, i64>("SELECT menu_id FROM sys_role_menu WHERE role_id = ?") + .bind(role_id) + .fetch_all(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::success(ids)) +} diff --git a/src-tauri/src/commands/user.rs b/src-tauri/src/commands/user.rs index d336d37..161285e 100644 --- a/src-tauri/src/commands/user.rs +++ b/src-tauri/src/commands/user.rs @@ -71,7 +71,7 @@ impl From for UserInfo { #[derive(Debug, Deserialize)] pub struct UserAssignRole { pub user_id: i64, - pub role_id: i64, + pub role_ids: Vec, } #[tauri::command] @@ -264,23 +264,27 @@ pub async fn get_user_by_id( #[auto_collect_command] pub async fn assign_role( pool: State<'_, MySqlPool>, - list: Vec, + params: UserAssignRole, ) -> Result, String> { - let user_id = match list.first() { - Some(first) => first.user_id, - None => return Ok(ApiResponse::<()>::success_empty()), - }; sqlx::query("DELETE FROM sys_user_role WHERE user_id = ?") - .bind(user_id) + .bind(params.user_id) .execute(&*pool) .await .map_err(|e| e.to_string())?; + if params.role_ids.is_empty() { + return Ok(ApiResponse::<()>::success_empty()); + } + let mut query_builder = QueryBuilder::new("INSERT INTO sys_user_role (user_id, role_id)"); - query_builder.push_values(&list, |mut b, item| { - b.push_bind(item.user_id).push_bind(item.role_id); + query_builder.push_values(¶ms.role_ids, |mut b, role_id| { + b.push_bind(params.user_id).push_bind(role_id); }); - let _ = query_builder.build().execute(&*pool).await.map_err(|e| e.to_string())?; + let _ = query_builder + .build() + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; Ok(ApiResponse::<()>::success_empty()) } diff --git a/src/api/system/role.ts b/src/api/system/role.ts index 7d55b9a..72f4fe5 100644 --- a/src/api/system/role.ts +++ b/src/api/system/role.ts @@ -41,6 +41,14 @@ const RoleApi = { }, roleList: async (): Promise> => { return await invoke("role_list") + }, + // 给角色分配菜单 + assignMenu: async (roleId: number, menuIds: number[]): Promise> => { + return await invoke("assign_menu", {params: {role_id: roleId, menu_ids: menuIds}}) + }, + // 获取角色菜单id + getRoleMenuIds: async (roleId: number): Promise> => { + return await invoke("get_role_menu_ids", {roleId}) } } diff --git a/src/api/system/user.ts b/src/api/system/user.ts index 0cfc89f..34c5033 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -33,11 +33,6 @@ interface UserSaveReq { creater: number } -interface AssignRoleReq { - user_id: number - role_id: number -} - const UserApi = { // 登录 login: async (userName: string, password: string): Promise> => { @@ -64,8 +59,8 @@ const UserApi = { return await invoke("get_user_by_id", {id}) }, // 给用户分配角色 - assignRole: async (list: AssignRoleReq[]): Promise> => { - return await invoke("assign_role", {list}) + assignRole: async (userId: number, roleIds: number[]): Promise> => { + return await invoke("assign_role", {params: {user_id: userId, role_ids: roleIds}}) }, // 获取用户角色id getUserRole: async (userId: number): Promise> => { @@ -74,4 +69,4 @@ const UserApi = { } export default UserApi -export type {UserInfo, UserSaveReq, UserPageReq, AssignRoleReq} \ No newline at end of file +export type {UserInfo, UserSaveReq, UserPageReq} \ No newline at end of file diff --git a/src/views/system/SysMenu.vue b/src/views/system/SysMenu.vue index 53b7adc..aab0bfa 100644 --- a/src/views/system/SysMenu.vue +++ b/src/views/system/SysMenu.vue @@ -360,7 +360,7 @@ function handleDelete(row: MenuInfo) { background: var(--color-surface, #fff); border: 1px solid var(--color-border, #E5E6EB); border-radius: 10px; - overflow: hidden; + overflow-x: auto; } .table-card :deep(.n-data-table) { diff --git a/src/views/system/SysRole.vue b/src/views/system/SysRole.vue index 6e04d44..9ed321f 100644 --- a/src/views/system/SysRole.vue +++ b/src/views/system/SysRole.vue @@ -63,6 +63,29 @@ + + + +
+ + +
+ + {{ item.title }} + {{ item.name }} + +
+
+
+
+ + +
@@ -70,8 +93,9 @@ import { ref, reactive, computed, h } from 'vue' import { NButton, NIcon, useMessage, useDialog } from 'naive-ui' import type { DataTableColumns, FormInst, FormRules } from 'naive-ui' -import { SearchOutline, AddOutline, CreateOutline, TrashOutline } from '@vicons/ionicons5' +import { SearchOutline, AddOutline, CreateOutline, TrashOutline, AppsOutline } from '@vicons/ionicons5' import RoleApi, { type RoleInfo, type RoleSaveReq } from '@/api/system/role' +import MenuApi, { type MenuInfo } from '@/api/system/menu' const message = useMessage() const dialog = useDialog() @@ -104,9 +128,13 @@ const columns: DataTableColumns = [ }, { title: '创建时间', key: 'create_time', width: 170, render(row) { return row.create_time ?? '-' } }, { - title: '操作', key: 'actions', width: 150, align: 'center', + title: '操作', key: 'actions', width: 220, align: 'center', render(row) { return h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'center' } }, [ + h(NButton, { + text: true, size: 'tiny', type: 'info', + onClick: (e: Event) => { e.stopPropagation(); openMenuModal(row) }, + }, { icon: () => h(NIcon, null, { default: () => h(AppsOutline) }), default: () => '菜单' }), h(NButton, { text: true, size: 'tiny', type: 'primary', onClick: (e: Event) => { e.stopPropagation(); openEdit(row) }, @@ -226,6 +254,67 @@ function handleDelete(row: RoleInfo) { }, }) } + +// ─── 分配菜单 ─── +type FlatMenu = MenuInfo & { _depth: number } + +const showMenuModal = ref(false) +const menuSubmitting = ref(false) +const menuList = ref([]) +const flatMenuList = ref([]) +const checkedMenuIds = ref([]) +const selectedMenuRole = ref({ id: 0, role_key: '', role_name: '', sort: 0, status: true }) + +function flattenMenuTree(menus: MenuInfo[], depth: number = 0): FlatMenu[] { + const result: FlatMenu[] = [] + for (const menu of menus) { + result.push({ ...menu, _depth: depth }) + if (menu.children && menu.children.length > 0) { + result.push(...flattenMenuTree(menu.children, depth + 1)) + } + } + return result +} + +async function openMenuModal(row: RoleInfo) { + selectedMenuRole.value = row + try { + const [menuRes, menuIdsRes] = await Promise.all([ + MenuApi.menuList({}), + RoleApi.getRoleMenuIds(row.id), + ]) + if (menuRes.success && menuRes.data) { + menuList.value = menuRes.data + flatMenuList.value = flattenMenuTree(menuRes.data) + } + if (menuIdsRes.success && menuIdsRes.data) { + checkedMenuIds.value = menuIdsRes.data + } else { + checkedMenuIds.value = [] + } + } catch (e: any) { + message.error(e?.toString() ?? '加载菜单信息失败') + return + } + showMenuModal.value = true +} + +async function handleMenuSubmit() { + menuSubmitting.value = true + try { + const res = await RoleApi.assignMenu(selectedMenuRole.value.id, checkedMenuIds.value) + if (res.success) { + message.success('分配菜单成功') + showMenuModal.value = false + } else { + message.error(res.message) + } + } catch (e: any) { + message.error(e?.toString() ?? '分配菜单失败') + } finally { + menuSubmitting.value = false + } +}