feat(role): 添加角色菜单分配功能
- 新增 RoleAssignMenu 结构体用于角色菜单分配参数 - 实现 assign_menu 和 get_role_menu_ids 后端命令接口 - 在前端角色管理页面添加分配菜单弹窗功能 - 集成菜单树形结构展示和多选分配逻辑 - 更新用户角色分配接口为批量分配模式 - 优化表格容器滚动样式为水平自适应
This commit is contained in:
parent
27341c8a53
commit
0ae78b2d6f
|
|
@ -57,6 +57,12 @@ pub struct RoleSaveReq {
|
|||
pub status: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RoleAssignMenu {
|
||||
pub role_id: i64,
|
||||
pub menu_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
#[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<ApiResponse<Vec<Rol
|
|||
let role_list = roles.into_iter().map(|role| RoleInfo::from(role)).collect();
|
||||
Ok(ApiResponse::success(role_list))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn assign_menu(
|
||||
pool: State<'_, MySqlPool>,
|
||||
params: RoleAssignMenu,
|
||||
) -> Result<ApiResponse<()>, 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<ApiResponse<Vec<i64>>, String> {
|
||||
let ids: Vec<i64> =
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl From<SysUser> for UserInfo {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserAssignRole {
|
||||
pub user_id: i64,
|
||||
pub role_id: i64,
|
||||
pub role_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
#[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<UserAssignRole>,
|
||||
params: UserAssignRole,
|
||||
) -> Result<ApiResponse<()>, 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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ const RoleApi = {
|
|||
},
|
||||
roleList: async (): Promise<ApiResponse<RoleInfo[]>> => {
|
||||
return await invoke("role_list")
|
||||
},
|
||||
// 给角色分配菜单
|
||||
assignMenu: async (roleId: number, menuIds: number[]): Promise<ApiResponse<any>> => {
|
||||
return await invoke("assign_menu", {params: {role_id: roleId, menu_ids: menuIds}})
|
||||
},
|
||||
// 获取角色菜单id
|
||||
getRoleMenuIds: async (roleId: number): Promise<ApiResponse<number[]>> => {
|
||||
return await invoke("get_role_menu_ids", {roleId})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ApiResponse<UserInfo>> => {
|
||||
|
|
@ -64,8 +59,8 @@ const UserApi = {
|
|||
return await invoke("get_user_by_id", {id})
|
||||
},
|
||||
// 给用户分配角色
|
||||
assignRole: async (list: AssignRoleReq[]): Promise<ApiResponse<any>> => {
|
||||
return await invoke("assign_role", {list})
|
||||
assignRole: async (userId: number, roleIds: number[]): Promise<ApiResponse<any>> => {
|
||||
return await invoke("assign_role", {params: {user_id: userId, role_ids: roleIds}})
|
||||
},
|
||||
// 获取用户角色id
|
||||
getUserRole: async (userId: number): Promise<ApiResponse<number[]>> => {
|
||||
|
|
@ -74,4 +69,4 @@ const UserApi = {
|
|||
}
|
||||
|
||||
export default UserApi
|
||||
export type {UserInfo, UserSaveReq, UserPageReq, AssignRoleReq}
|
||||
export type {UserInfo, UserSaveReq, UserPageReq}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,29 @@
|
|||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 分配菜单弹窗 -->
|
||||
<n-modal v-model:show="showMenuModal" preset="card" :title="'分配菜单 - ' + selectedMenuRole.role_name" :style="{ width: '500px' }" :mask-closable="false">
|
||||
<div v-if="menuList.length > 0" style="max-height: 420px; overflow-y: auto; padding: 4px 0;">
|
||||
<n-checkbox-group v-model:value="checkedMenuIds">
|
||||
<n-space vertical :size="0">
|
||||
<div v-for="item in flatMenuList" :key="item.id" :style="{ paddingLeft: item._depth * 24 + 'px', paddingTop: '2px', paddingBottom: '2px' }">
|
||||
<n-checkbox :value="item.id">
|
||||
<span :style="{ fontWeight: item.children && item.children.length > 0 ? 600 : 400 }">{{ item.title }}</span>
|
||||
<span style="color: #86909C; font-size: 12px; margin-left: 8px;">{{ item.name }}</span>
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-checkbox-group>
|
||||
</div>
|
||||
<n-empty v-else description="暂无菜单数据" style="padding: 24px 0;" />
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showMenuModal = false">取消</n-button>
|
||||
<n-button type="primary" :loading="menuSubmitting" @click="handleMenuSubmit">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -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<RoleInfo> = [
|
|||
},
|
||||
{ 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<MenuInfo[]>([])
|
||||
const flatMenuList = ref<FlatMenu[]>([])
|
||||
const checkedMenuIds = ref<number[]>([])
|
||||
const selectedMenuRole = ref<RoleInfo>({ 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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -263,7 +352,7 @@ function handleDelete(row: RoleInfo) {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -381,11 +381,7 @@ async function openRoleModal(row: UserInfo) {
|
|||
async function handleRoleSubmit() {
|
||||
roleSubmitting.value = true
|
||||
try {
|
||||
const list = checkedRoleIds.value.map(role_id => ({
|
||||
user_id: selectedRoleUser.value.id,
|
||||
role_id,
|
||||
}))
|
||||
const res = await UserApi.assignRole(list)
|
||||
const res = await UserApi.assignRole(selectedRoleUser.value.id, checkedRoleIds.value)
|
||||
if (res.success) {
|
||||
message.success('分配角色成功')
|
||||
showRoleModal.value = false
|
||||
|
|
@ -435,7 +431,7 @@ async function handleRoleSubmit() {
|
|||
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue