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,
|
pub status: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RoleAssignMenu {
|
||||||
|
pub role_id: i64,
|
||||||
|
pub menu_ids: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[auto_collect_command]
|
#[auto_collect_command]
|
||||||
pub async fn role_save(
|
pub async fn role_save(
|
||||||
|
|
@ -72,14 +78,15 @@ pub async fn role_save(
|
||||||
if exist > 0 {
|
if exist > 0 {
|
||||||
return Ok(ApiResponse::error(2001, "角色_key已存在"));
|
return Ok(ApiResponse::error(2001, "角色_key已存在"));
|
||||||
}
|
}
|
||||||
let result = sqlx::query("INSERT INTO sys_role (role_key, role_name, sort, status) VALUES (?, ?, ?, ?)")
|
let result =
|
||||||
.bind(¶ms.role_key)
|
sqlx::query("INSERT INTO sys_role (role_key, role_name, sort, status) VALUES (?, ?, ?, ?)")
|
||||||
.bind(¶ms.role_name)
|
.bind(¶ms.role_key)
|
||||||
.bind(¶ms.sort)
|
.bind(¶ms.role_name)
|
||||||
.bind(¶ms.status)
|
.bind(¶ms.sort)
|
||||||
.execute(&*pool)
|
.bind(¶ms.status)
|
||||||
.await
|
.execute(&*pool)
|
||||||
.map_err(|e| e.to_string())?;
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
let id = result.last_insert_id() as i64;
|
let id = result.last_insert_id() as i64;
|
||||||
Ok(ApiResponse::success(id))
|
Ok(ApiResponse::success(id))
|
||||||
}
|
}
|
||||||
|
|
@ -181,15 +188,17 @@ pub async fn role_update(
|
||||||
return Ok(ApiResponse::error(2001, "角色_key已存在"));
|
return Ok(ApiResponse::error(2001, "角色_key已存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("UPDATE sys_role SET role_key = ?, role_name = ?, sort = ?, status = ? WHERE id = ?")
|
sqlx::query(
|
||||||
.bind(¶ms.role_key)
|
"UPDATE sys_role SET role_key = ?, role_name = ?, sort = ?, status = ? WHERE id = ?",
|
||||||
.bind(¶ms.role_name)
|
)
|
||||||
.bind(¶ms.sort)
|
.bind(¶ms.role_key)
|
||||||
.bind(¶ms.status)
|
.bind(¶ms.role_name)
|
||||||
.bind(¶ms.id)
|
.bind(¶ms.sort)
|
||||||
.execute(&*pool)
|
.bind(¶ms.status)
|
||||||
.await
|
.bind(¶ms.id)
|
||||||
.map_err(|e| e.to_string())?;
|
.execute(&*pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(ApiResponse::success(()))
|
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();
|
let role_list = roles.into_iter().map(|role| RoleInfo::from(role)).collect();
|
||||||
Ok(ApiResponse::success(role_list))
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct UserAssignRole {
|
pub struct UserAssignRole {
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub role_id: i64,
|
pub role_ids: Vec<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -264,23 +264,27 @@ pub async fn get_user_by_id(
|
||||||
#[auto_collect_command]
|
#[auto_collect_command]
|
||||||
pub async fn assign_role(
|
pub async fn assign_role(
|
||||||
pool: State<'_, MySqlPool>,
|
pool: State<'_, MySqlPool>,
|
||||||
list: Vec<UserAssignRole>,
|
params: UserAssignRole,
|
||||||
) -> Result<ApiResponse<()>, String> {
|
) -> 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 = ?")
|
sqlx::query("DELETE FROM sys_user_role WHERE user_id = ?")
|
||||||
.bind(user_id)
|
.bind(params.user_id)
|
||||||
.execute(&*pool)
|
.execute(&*pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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)");
|
let mut query_builder = QueryBuilder::new("INSERT INTO sys_user_role (user_id, role_id)");
|
||||||
query_builder.push_values(&list, |mut b, item| {
|
query_builder.push_values(¶ms.role_ids, |mut b, role_id| {
|
||||||
b.push_bind(item.user_id).push_bind(item.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())
|
Ok(ApiResponse::<()>::success_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ const RoleApi = {
|
||||||
},
|
},
|
||||||
roleList: async (): Promise<ApiResponse<RoleInfo[]>> => {
|
roleList: async (): Promise<ApiResponse<RoleInfo[]>> => {
|
||||||
return await invoke("role_list")
|
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
|
creater: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssignRoleReq {
|
|
||||||
user_id: number
|
|
||||||
role_id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserApi = {
|
const UserApi = {
|
||||||
// 登录
|
// 登录
|
||||||
login: async (userName: string, password: string): Promise<ApiResponse<UserInfo>> => {
|
login: async (userName: string, password: string): Promise<ApiResponse<UserInfo>> => {
|
||||||
|
|
@ -64,8 +59,8 @@ const UserApi = {
|
||||||
return await invoke("get_user_by_id", {id})
|
return await invoke("get_user_by_id", {id})
|
||||||
},
|
},
|
||||||
// 给用户分配角色
|
// 给用户分配角色
|
||||||
assignRole: async (list: AssignRoleReq[]): Promise<ApiResponse<any>> => {
|
assignRole: async (userId: number, roleIds: number[]): Promise<ApiResponse<any>> => {
|
||||||
return await invoke("assign_role", {list})
|
return await invoke("assign_role", {params: {user_id: userId, role_ids: roleIds}})
|
||||||
},
|
},
|
||||||
// 获取用户角色id
|
// 获取用户角色id
|
||||||
getUserRole: async (userId: number): Promise<ApiResponse<number[]>> => {
|
getUserRole: async (userId: number): Promise<ApiResponse<number[]>> => {
|
||||||
|
|
@ -74,4 +69,4 @@ const UserApi = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default 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);
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid var(--color-border, #E5E6EB);
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card :deep(.n-data-table) {
|
.table-card :deep(.n-data-table) {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,29 @@
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -70,8 +93,9 @@
|
||||||
import { ref, reactive, computed, h } from 'vue'
|
import { ref, reactive, computed, h } from 'vue'
|
||||||
import { NButton, NIcon, useMessage, useDialog } from 'naive-ui'
|
import { NButton, NIcon, useMessage, useDialog } from 'naive-ui'
|
||||||
import type { DataTableColumns, FormInst, FormRules } 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 RoleApi, { type RoleInfo, type RoleSaveReq } from '@/api/system/role'
|
||||||
|
import MenuApi, { type MenuInfo } from '@/api/system/menu'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
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: '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) {
|
render(row) {
|
||||||
return h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'center' } }, [
|
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, {
|
h(NButton, {
|
||||||
text: true, size: 'tiny', type: 'primary',
|
text: true, size: 'tiny', type: 'primary',
|
||||||
onClick: (e: Event) => { e.stopPropagation(); openEdit(row) },
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -263,7 +352,7 @@ function handleDelete(row: RoleInfo) {
|
||||||
background: var(--color-surface, #fff);
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid var(--color-border, #E5E6EB);
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card :deep(.n-data-table) {
|
.table-card :deep(.n-data-table) {
|
||||||
|
|
|
||||||
|
|
@ -381,11 +381,7 @@ async function openRoleModal(row: UserInfo) {
|
||||||
async function handleRoleSubmit() {
|
async function handleRoleSubmit() {
|
||||||
roleSubmitting.value = true
|
roleSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
const list = checkedRoleIds.value.map(role_id => ({
|
const res = await UserApi.assignRole(selectedRoleUser.value.id, checkedRoleIds.value)
|
||||||
user_id: selectedRoleUser.value.id,
|
|
||||||
role_id,
|
|
||||||
}))
|
|
||||||
const res = await UserApi.assignRole(list)
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
message.success('分配角色成功')
|
message.success('分配角色成功')
|
||||||
showRoleModal.value = false
|
showRoleModal.value = false
|
||||||
|
|
@ -435,7 +431,7 @@ async function handleRoleSubmit() {
|
||||||
background: var(--color-surface, #fff);
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid var(--color-border, #E5E6EB);
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card :deep(.n-data-table) {
|
.table-card :deep(.n-data-table) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue