feat: 角色页成员维护弹窗与用户角色清空

- 角色页新增「成员」动作,弹窗展示姓名/账号/所属组织/状态,支持搜索添加启用用户与移除
- api/system/role 增加 pageRoleMembers/addRoleMember/removeRoleMember,走 THINK_API_URL
- api/system/user 的 updateUser 改走 THINK_API_URL,空角色由后端清空
- 用户编辑表单允许角色为空,保存后可删除该用户全部角色关联
This commit is contained in:
2026-10-30 19:59:48 +08:00
parent 80d7eec4f8
commit 634e1c12af
6 changed files with 321 additions and 13 deletions
@@ -0,0 +1,230 @@
<!-- 角色成员维护弹窗 -->
<template>
<ele-modal
:width="720"
title="角色成员"
:visible="visible"
:body-style="{ paddingBottom: '8px' }"
:footer="null"
@update:visible="updateVisible"
>
<div style="margin-bottom: 12px">
<a-space>
<a-select
v-model:value="selectedUserId"
show-search
allow-clear
:filter-option="false"
:options="userOptions"
:loading="userLoading"
placeholder="输入姓名或账号搜索启用用户"
style="width: 320px"
@search="searchUsers"
/>
<a-button
type="primary"
:loading="adding"
:disabled="!selectedUserId"
@click="addOne"
>
添加成员
</a-button>
</a-space>
<div class="ele-text-secondary" style="margin-top: 6px">
角色:{{ data?.roleName }}({{ data?.roleCode }})
</div>
</div>
<a-table
size="small"
row-key="userId"
:columns="columns"
:data-source="members"
:loading="loading"
:pagination="false"
:scroll="{ y: '46vh' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userName'">
{{ record.realName || record.nickname || '-' }}
</template>
<template v-else-if="column.key === 'organizationName'">
{{ record.organizationName || '-' }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '停用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-popconfirm
placement="topRight"
title="确定要移除该成员吗?"
@confirm="removeOne(record)"
>
<a class="ele-text-danger">移除</a>
</a-popconfirm>
</template>
</template>
</a-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
import {
addRoleMember,
pageRoleMembers,
removeRoleMember
} from '@/api/system/role';
import type { Role, RoleMember } from '@/api/system/role/model';
import { pageUsers } from '@/api/system/user';
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 当前角色数据
data?: Role | null;
}>();
// 表格列配置
const columns = ref<ColumnItem[]>([
{ title: '姓名', key: 'userName', width: 120 },
{ title: '账号', dataIndex: 'username', width: 150 },
{ title: '所属组织', key: 'organizationName' },
{ title: '状态', key: 'status', width: 90, align: 'center' },
{ title: '操作', key: 'action', width: 90, align: 'center' }
]);
// 成员列表
const members = ref<RoleMember[]>([]);
// 成员请求状态
const loading = ref(false);
// 添加状态
const adding = ref(false);
// 候选用户请求状态
const userLoading = ref(false);
// 候选用户下拉选项
const userOptions = ref<{ label: string; value: number }[]>([]);
// 选中的候选用户
const selectedUserId = ref<number | undefined>(undefined);
// 用户搜索关键词
const keyword = ref('');
// 当前角色id
const roleId = computed(() => props.data?.roleId);
// 已是成员的候选用户id
const memberIds = computed(() => new Set(members.value.map((d) => d.userId)));
/* 查询角色成员 */
const queryMembers = () => {
if (!roleId.value) {
members.value = [];
return;
}
loading.value = true;
pageRoleMembers({ roleId: roleId.value, page: 1, limit: 2000 })
.then((res) => {
loading.value = false;
members.value = res?.list ?? [];
})
.catch((e) => {
loading.value = false;
members.value = [];
message.error(e.message);
});
};
/* 搜索启用用户作为候选 */
const searchUsers = (value?: string) => {
keyword.value = value ?? '';
userLoading.value = true;
pageUsers({
keywords: keyword.value || undefined,
status: 0,
page: 1,
limit: 20
})
.then((res) => {
userLoading.value = false;
userOptions.value = (res?.list ?? [])
.filter((d) => d.userId != null && !memberIds.value.has(d.userId))
.map((d) => ({
label: `${d.realName || d.nickname || d.username}(${d.username})`,
value: d.userId as number
}));
})
.catch((e) => {
userLoading.value = false;
userOptions.value = [];
message.error(e.message);
});
};
/* 添加成员 */
const addOne = () => {
if (!roleId.value || selectedUserId.value == null) {
return;
}
adding.value = true;
addRoleMember({ roleId: roleId.value, userId: selectedUserId.value })
.then((msg) => {
adding.value = false;
selectedUserId.value = undefined;
message.success(msg);
queryMembers();
searchUsers(keyword.value);
})
.catch((e) => {
adding.value = false;
message.error(e.message);
});
};
/* 移除成员 */
const removeOne = (row: RoleMember) => {
if (!roleId.value || row.userId == null) {
return;
}
removeRoleMember(roleId.value, row.userId)
.then((msg) => {
message.success(msg);
queryMembers();
searchUsers(keyword.value);
})
.catch((e) => {
message.error(e.message);
});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
members.value = [];
userOptions.value = [];
selectedUserId.value = undefined;
keyword.value = '';
queryMembers();
searchUsers('');
}
}
);
</script>
+15 -1
View File
@@ -39,6 +39,8 @@
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a @click="openMember(record)">成员</a>
<a-divider type="vertical" />
<a @click="openAuth(record)">分配权限</a>
<a-divider type="vertical" />
<a-popconfirm
@@ -57,6 +59,8 @@
<role-edit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 权限分配弹窗 -->
<role-auth v-model:visible="showAuth" :data="current" />
<!-- 成员维护弹窗 -->
<role-member v-model:visible="showMember" :data="current" />
</div>
</template>
@@ -77,6 +81,7 @@
import RoleSearch from './components/role-search.vue';
import RoleEdit from './components/role-edit.vue';
import RoleAuth from './components/role-auth.vue';
import RoleMember from './components/role-member.vue';
import { pageRoles, removeRole, removeRoles } from '@/api/system/role';
import type { Role, RoleParam } from '@/api/system/role/model';
@@ -120,7 +125,7 @@
{
title: '操作',
key: 'action',
width: 200,
width: 260,
align: 'center'
}
]);
@@ -137,6 +142,9 @@
// 是否显示权限分配弹窗
const showAuth = ref(false);
// 是否显示成员维护弹窗
const showMember = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageRoles({ ...where, ...orders, page, limit });
@@ -160,6 +168,12 @@
showAuth.value = true;
};
/* 打开成员维护弹窗 */
const openMember = (row?: Role) => {
current.value = row ?? null;
showMember.value = true;
};
/* 删除单个 */
const remove = (row: Role) => {
const hide = messageLoading('请求中..', 0);
@@ -255,14 +255,8 @@
// trigger: 'blur'
// }
// ],
roles: [
{
required: true,
message: '请选择角色',
type: 'array',
trigger: 'blur'
}
],
// 角色可以为空:清空并保存表示删除该用户的全部角色关联
roles: [],
email: [
{
pattern: emailReg,