feat(@vben/web-antd): 项目成员管理改 Drawer + 分页 + 增量
从 Modal 多选改为 Drawer 分页表,更接近"成员管理"语义: - 原 assign-user-form.vue 重写为 Drawer + Vxe 分页表 - 新增 add-user-modal.vue 子弹窗用于添加用户(过滤已是成员) - 每行一个"移除"popConfirm 按钮,调 removeProjectUser 单删 - 顶部 keyword 搜索,按 username/nickname/mobile 模糊 - 底部提示:超管不在此列表(后端已过滤) - data.ts 新增 useProjectMemberGridColumns - api 新增 getProjectUserPage / addProjectUsers / removeProjectUser - project/index.vue 接入点改 useVbenDrawer Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemUserProjectApi {
|
||||
@@ -9,6 +13,14 @@ export namespace SystemUserProjectApi {
|
||||
projectId: number;
|
||||
userIds: number[];
|
||||
}
|
||||
export interface AddProjectUsersReq {
|
||||
projectId: number;
|
||||
userIds: number[];
|
||||
}
|
||||
export interface ProjectUserPageReq extends PageParam {
|
||||
projectId: number;
|
||||
keyword?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 给用户覆盖式分配项目 */
|
||||
@@ -44,3 +56,26 @@ export function getUserIdsByProjectId(projectId: number) {
|
||||
`/system/user-project/list-user-ids-by-project?projectId=${projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 分页查询项目成员(已自动过滤超级管理员) */
|
||||
export function getProjectUserPage(params: SystemUserProjectApi.ProjectUserPageReq) {
|
||||
return requestClient.get<PageResult<SystemUserApi.User>>(
|
||||
'/system/user-project/project-user-page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 增量给项目添加成员 */
|
||||
export function addProjectUsers(data: SystemUserProjectApi.AddProjectUsersReq) {
|
||||
return requestClient.post<boolean>(
|
||||
'/system/user-project/add-project-users',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 从项目中移除单个成员 */
|
||||
export function removeProjectUser(projectId: number, userId: number) {
|
||||
return requestClient.delete<boolean>(
|
||||
`/system/user-project/remove-project-user?projectId=${projectId}&userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,53 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
];
|
||||
}
|
||||
|
||||
/** 管理成员的表单(项目 → 多用户) */
|
||||
/** 项目成员管理抽屉的表格列 */
|
||||
export function useProjectMemberGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '用户编号',
|
||||
minWidth: 90,
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: '用户名',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '昵称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'deptId',
|
||||
title: '部门编号',
|
||||
minWidth: 90,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 90,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 管理成员的表单(项目 → 多用户)— 保留给未来"覆盖写入"场景,当前 UI 已改抽屉 */
|
||||
export function useAssignUserFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SystemProjectApi } from '#/api/system/project';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { confirm, Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
@@ -14,7 +14,7 @@ import { deleteProject, getProjectPage } from '#/api/system/project';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import AssignUserForm from './modules/assign-user-form.vue';
|
||||
import AssignUserDrawer from './modules/assign-user-form.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
@@ -22,8 +22,8 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [AssignUserModal, assignUserModalApi] = useVbenModal({
|
||||
connectedComponent: AssignUserForm,
|
||||
const [MemberDrawer, memberDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: AssignUserDrawer,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ function handleEdit(row: SystemProjectApi.Project) {
|
||||
|
||||
/** 管理成员 */
|
||||
function handleAssignUser(row: SystemProjectApi.Project) {
|
||||
assignUserModalApi.setData(row).open();
|
||||
memberDrawerApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除项目 */
|
||||
@@ -125,7 +125,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<AssignUserModal @success="handleRefresh" />
|
||||
<MemberDrawer @success="handleRefresh" />
|
||||
<Grid table-title="项目列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message, Select, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { addProjectUsers } from '#/api/system/user-project';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface AddUserModalData {
|
||||
projectId: number;
|
||||
projectName?: string;
|
||||
excludedUserIds: number[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const allUsers = ref<SystemUserApi.User[]>([]);
|
||||
const selectedUserIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const currentData = ref<AddUserModalData | null>(null);
|
||||
|
||||
/** 可选项:全量用户 - 已是成员的用户 */
|
||||
const selectOptions = computed(() => {
|
||||
const excluded = new Set(currentData.value?.excludedUserIds ?? []);
|
||||
return allUsers.value
|
||||
.filter((u) => u.id && !excluded.has(u.id))
|
||||
.map((u) => ({
|
||||
value: u.id,
|
||||
label: `${u.nickname || u.username}(${u.username})`,
|
||||
}));
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!currentData.value?.projectId) return;
|
||||
if (selectedUserIds.value.length === 0) {
|
||||
message.warning('请至少选择一个用户');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await addProjectUsers({
|
||||
projectId: currentData.value.projectId,
|
||||
userIds: selectedUserIds.value,
|
||||
});
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
allUsers.value = [];
|
||||
selectedUserIds.value = [];
|
||||
currentData.value = null;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<AddUserModalData>();
|
||||
if (!data) return;
|
||||
currentData.value = data;
|
||||
loading.value = true;
|
||||
try {
|
||||
allUsers.value = await getSimpleUserList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="
|
||||
currentData?.projectName
|
||||
? `添加用户到项目 - ${currentData.projectName}`
|
||||
: '添加用户'
|
||||
"
|
||||
class="w-[560px]"
|
||||
>
|
||||
<Spin :spinning="loading">
|
||||
<div class="px-4 py-2">
|
||||
<div class="mb-2 text-sm">选择要加入项目的用户(已在项目中的用户会自动过滤)</div>
|
||||
<Select
|
||||
v-model:value="selectedUserIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择用户"
|
||||
:options="selectOptions"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
"
|
||||
show-search
|
||||
allow-clear
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,98 +1,175 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemProjectApi } from '#/api/system/project';
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { message, Modal as AntModal } from 'ant-design-vue';
|
||||
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { Button, Input, message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
assignProjectUsers,
|
||||
getProjectUserPage,
|
||||
getUserIdsByProjectId,
|
||||
removeProjectUser,
|
||||
} from '#/api/system/user-project';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useAssignUserFormSchema } from '../data';
|
||||
import AddUserModal from './add-user-modal.vue';
|
||||
import { useProjectMemberGridColumns } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useAssignUserFormSchema(),
|
||||
showDefaultActions: false,
|
||||
/** 当前打开抽屉的项目 */
|
||||
const currentProject = ref<SystemProjectApi.Project | null>(null);
|
||||
/** 搜索关键字 */
|
||||
const keyword = ref<string>('');
|
||||
|
||||
/** 添加用户子弹窗 */
|
||||
const [AddUserModalCmp, addUserModalApi] = useVbenModal({
|
||||
connectedComponent: AddUserModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const values = await formApi.getValues();
|
||||
const userIds: number[] = values.userIds ?? [];
|
||||
/** 移除单个成员 */
|
||||
async function handleRemove(row: SystemUserApi.User) {
|
||||
if (!currentProject.value?.id || !row.id) return;
|
||||
const hideLoading = message.loading({
|
||||
content: `正在从项目中移除 ${row.nickname || row.username}...`,
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await removeProjectUser(currentProject.value.id, row.id);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
gridApi.query();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 空集二次确认:清空该项目所有成员是高危操作
|
||||
if (userIds.length === 0) {
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
AntModal.confirm({
|
||||
title: '确认清空项目成员?',
|
||||
content: `即将清空项目【${values.name}】的所有成员,保存后除超管外所有用户都将无法访问该项目。确认继续?`,
|
||||
okText: '确认清空',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/** 打开添加用户弹窗 */
|
||||
async function handleOpenAddUser() {
|
||||
if (!currentProject.value?.id) return;
|
||||
// 把当前已是成员的 userIds 传给子 Modal 用于过滤
|
||||
const excludedIds = await getUserIdsByProjectId(currentProject.value.id);
|
||||
addUserModalApi
|
||||
.setData({
|
||||
projectId: currentProject.value.id,
|
||||
projectName: currentProject.value.name,
|
||||
excludedUserIds: excludedIds,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
await assignProjectUsers({
|
||||
projectId: values.id,
|
||||
userIds,
|
||||
});
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
/** 子弹窗保存后刷新表格 */
|
||||
function handleAddUserSuccess() {
|
||||
gridApi.query();
|
||||
emit('success');
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useProjectMemberGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
pageSize: 10,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!currentProject.value?.id) {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
return await getProjectUserPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
projectId: currentProject.value.id,
|
||||
keyword: keyword.value || undefined,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemUserApi.User>,
|
||||
});
|
||||
|
||||
function handleSearch() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
currentProject.value = null;
|
||||
keyword.value = '';
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<SystemProjectApi.Project>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const userIds = await getUserIdsByProjectId(data.id);
|
||||
await formApi.setValues({
|
||||
...data,
|
||||
userIds,
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
const data = drawerApi.getData<SystemProjectApi.Project>();
|
||||
if (!data || !data.id) return;
|
||||
currentProject.value = data;
|
||||
// 等 currentProject 赋值后再 query
|
||||
gridApi.query();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="管理成员">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
<Drawer
|
||||
class="w-[800px]"
|
||||
:title="currentProject ? `项目成员管理 - ${currentProject.name}` : '项目成员管理'"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<AddUserModalCmp @success="handleAddUserSuccess" />
|
||||
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Input
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索用户名 / 昵称 / 手机号"
|
||||
allow-clear
|
||||
class="w-64"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button type="primary" @click="handleOpenAddUser">
|
||||
<template #icon>
|
||||
<span class="iconify ant-design--plus-outlined" />
|
||||
</template>
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Grid>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '移除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['system:project:assign-user'],
|
||||
popConfirm: {
|
||||
title: `确认将 ${row.nickname || row.username} 从项目中移除?`,
|
||||
confirm: handleRemove.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-500">
|
||||
提示:超级管理员通过角色天然拥有所有项目权限,不会出现在此列表中。
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user