feat(项目管理): 项目管理成员本地联调&封装公共组件tag&tagGroup

This commit is contained in:
xinxin.wu 2023-09-01 18:06:12 +08:00 committed by fit2-zhao
parent 2a24e2938d
commit c1108b48db
19 changed files with 670 additions and 296 deletions

View File

@ -0,0 +1,51 @@
import MSR from '@/api/http/index';
import {
GetProjectMemberListUrl,
AddMemberToProjectUrl,
EditProjectMemberUrl,
BatchAddUserGroup,
BatchRemoveMemberUrl,
RemoveProjectMemberUrl,
ProjectUserGroupUrl,
ProjectMemberOptions,
} from '@/api/requrls/project-management/projectMember';
import type { ProjectMemberItem, ActionProjectMember } from '@/models/projectManagement/projectAndPermission';
import type { TableQueryParams, CommonList } from '@/models/common';
// 获取项目成员列表
export function getProjectMemberList(data: TableQueryParams) {
return MSR.post<CommonList<ProjectMemberItem>>({ url: GetProjectMemberListUrl, data });
}
// 添加项目成员&编辑项目成员
export function addOrUpdateProjectMember(data: ActionProjectMember) {
if (data.userId) {
return MSR.post({ url: EditProjectMemberUrl, data });
}
return MSR.post({ url: AddMemberToProjectUrl, data });
}
// 添加项目成员到用户组
export function addProjectUserGroup(data: ActionProjectMember) {
return MSR.post({ url: BatchAddUserGroup, data });
}
// 批量移除项目成员
export function batchRemoveMember(data: ActionProjectMember) {
return MSR.post({ url: BatchRemoveMemberUrl, data });
}
// 移除项目成员
export function removeProjectMember(projectId: string, userId: string) {
return MSR.get({ url: RemoveProjectMemberUrl, params: `${projectId}/${userId}` });
}
// 获取用户组下拉
export function getProjectUserGroup(projectId: string) {
return MSR.get({ url: ProjectUserGroupUrl, params: projectId });
}
// 项目成员下拉选项
export function getProjectMemberOptions(projectId: string) {
return MSR.get({ url: ProjectMemberOptions, params: projectId });
}

View File

@ -0,0 +1,8 @@
export const GetProjectMemberListUrl = '/project/member/list';
export const AddMemberToProjectUrl = '/project/member/add';
export const EditProjectMemberUrl = '/project/member/update';
export const BatchRemoveMemberUrl = '/project/member/batch/remove';
export const RemoveProjectMemberUrl = '/project/member/remove';
export const BatchAddUserGroup = '/project/member/add-role';
export const ProjectUserGroupUrl = '/project/member/get-role/option';
export const ProjectMemberOptions = '/project/member/get-member/option';

View File

@ -618,18 +618,6 @@
font-weight: 500;
}
/** 标签 **/
.arco-tag {
.arco-icon {
font-size: 14px;
}
.arco-tag-close-btn {
.arco-icon {
color: var(--color-text-brand);
}
}
}
/** 虚拟列表 **/
.arco-virtual-list {
.ms-scroll-bar();

View File

@ -3,33 +3,28 @@
<template #title>
{{ batchTitle }}
<div class="text-[var(--color-text-4)]">
{{ t('msbatchmodal.batchModalSubTitle', { count: props.tableSelected.length }) }}
{{ t('msBatchModal.batchModalSubTitle', { count: props.tableSelected.length }) }}
</div>
</template>
<a-alert v-if="props.action === 'batchAddProject'" class="mb-[16px]">
{{ t('msbatchmodal.batchModalTip') }}
</a-alert>
<a-transfer
v-model="target"
:title="[t('msbatchmodal.batchOptional'), t('msbatchmodal.batchChosen')]"
:data="transferData"
show-search
>
<template #source="{ data, selectedKeys, onSelect }">
<a-tree
:checkable="true"
checked-strategy="child"
:checked-keys="selectedKeys"
:data="getTreeData(data)"
block-node
@check="onSelect"
/>
</template>
</a-transfer>
<a-spin :loading="loading">
<a-alert v-if="props.action === 'batchAddProject'" class="mb-[16px]">
{{ t('msBatchModal.batchModalTip') }}
</a-alert>
<MsTransfer
v-model="target"
:data="treeList"
:tree-filed="{
key: 'id',
title: 'name',
children: 'children',
disabled: 'disabled',
}"
/>
</a-spin>
<template #footer>
<a-button type="secondary" @click="cancelBatch">{{ t('msbatchmodal.batchModalCancel') }}</a-button>
<a-button type="primary" :loading="batchLoading" @click="confirmBatch">
{{ t('msbatchmodal.batchModalConfirm') }}
<a-button type="secondary" @click="cancelBatch">{{ t('msBatchModal.batchModalCancel') }}</a-button>
<a-button type="primary" :loading="batchLoading" :disabled="target.length < 1" @click="confirmBatch">
{{ t('msBatchModal.batchModalConfirm') }}
</a-button>
</template>
</a-modal>
@ -40,6 +35,7 @@
import { useI18n } from '@/hooks/useI18n';
import { Message } from '@arco-design/web-vue';
import type { BatchModel } from './types';
import MsTransfer from '@/components/pure/ms-transfer/index.vue';
const { t } = useI18n();
@ -49,18 +45,11 @@
children?: TreeDataItem[];
}
interface TransferDataItem {
value: string;
label: string;
disabled: boolean;
}
const props = withDefaults(
defineProps<{
tableSelected: (string | number)[];
visible: boolean;
action: string;
treeData: TreeDataItem[];
}>(),
{
visible: false,
@ -70,7 +59,7 @@
(e: 'update:visible', val: boolean): void;
(e: 'addProject', targetValue: string[], type: string): void;
(e: 'addUserGroup', targetValue: string[], type: string): void;
(e: 'addOrgnization', targetValue: string[], type: string): void;
(e: 'addOrganization', targetValue: string[], type: string): void;
}>();
const showBatchModal = ref(false);
@ -78,6 +67,7 @@
const batchTitle = ref('');
const target = ref<string[]>([]);
const treeList = ref<TreeDataItem[]>([]);
const loading = ref<boolean>(false);
function handleTableBatch(action: string) {
switch (action) {
@ -115,78 +105,38 @@
}
);
/**
* 获取穿梭框数据根据树结构获取
* @param _treeData 树结构
* @param transferDataSource 穿梭框数组
*/
const getTransferData = (_treeData: TreeDataItem[], transferDataSource: TransferDataItem[]) => {
_treeData.forEach((item) => {
if (item.children) getTransferData(item.children, transferDataSource);
else transferDataSource.push({ label: item.title, value: item.key, disabled: false });
});
return transferDataSource;
};
/**
* 获取树结构数据根据穿梭框过滤的数据获取
*/
const getTreeData = (data: TransferDataItem[]) => {
const values = data.map((item) => item.value);
const travel = (_treeData: TreeDataItem[]) => {
const treeDataSource: TreeDataItem[] = [];
_treeData.forEach((item) => {
// push 穿
const allSelected = item.children?.every((child) => target.value.includes(child.key));
if (!allSelected && (item.children || values.includes(item.key))) {
treeDataSource.push({
title: item.title,
key: item.key,
children: item.children ? travel(item.children) : [],
});
}
});
return treeDataSource;
};
return travel(treeList.value);
};
let transferData: TransferDataItem[] = [];
function cancelBatch() {
showBatchModal.value = false;
target.value = [];
}
const batchRequestFun = async (reqFun: any, params: BatchModel) => {
batchLoading.value = true;
loading.value = true;
try {
await reqFun(params);
Message.success(t('organization.member.batchModalSuccess'));
Message.success(t('msBatchModal.batchModalSuccess'));
showBatchModal.value = false;
target.value = [];
} catch (error) {
console.log(error);
} finally {
batchLoading.value = false;
loading.value = false;
}
};
const confirmBatch = async () => {
batchLoading.value = true;
if (target.value.length < 1) {
return;
}
try {
switch (props.action) {
case 'batchAddProject':
emit('addProject', target.value, 'project');
break;
case 'batchAddUserGroup':
emit('addUserGroup', target.value, 'usergroup');
emit('addUserGroup', target.value, 'userGroup');
break;
case 'batchAddOrganization':
emit('addOrgnization', target.value, 'orgnization');
emit('addOrganization', target.value, 'organization');
break;
default:
break;
@ -195,17 +145,21 @@
console.log(error);
}
};
watch(
() => props.treeData,
(newVal) => {
treeList.value = newVal;
transferData = getTransferData(treeList.value, []);
},
{ deep: true, immediate: true }
);
const getTreeList = async (callBack: (orgId: string) => Promise<any>, orgId: string) => {
loading.value = true;
try {
treeList.value = await callBack(orgId);
loading.value = false;
} catch (error) {
console.log(error);
}
};
defineExpose({
batchLoading,
batchRequestFun,
getTreeList,
});
</script>

View File

@ -1,8 +1,10 @@
export default {
'msbatchmodal.batchModalSubTitle': '(Selected {count} users)',
'msbatchmodal.batchModalTip': 'Add project member usergroup as member by default',
'msbatchmodal.batchOptional': 'Optional',
'msbatchmodal.batchChosen': 'Chosen',
'msbatchmodal.batchModalCancel': 'Cancel',
'msbatchmodal.batchModalConfirm': 'Add',
'msBatchModal.batchModalSubTitle': '(Selected {count} users)',
'msBatchModal.batchModalTip': 'Add project member userGroup as member by default',
'msBatchModal.batchOptional': 'Optional',
'msBatchModal.batchChosen': 'Chosen',
'msBatchModal.batchModalCancel': 'Cancel',
'msBatchModal.batchModalConfirm': 'Add',
'msBatchModal.batchModalEmpty': 'The selection can not be empty',
'msBatchModal.batchModalSuccess': 'Successfully added',
};

View File

@ -1,8 +1,10 @@
export default {
'msbatchmodal.batchModalSubTitle': '已选 {count} 个用户)',
'msbatchmodal.batchModalTip': '默认为成员添加项目成员用户组',
'msbatchmodal.batchOptional': '可选',
'msbatchmodal.batchChosen': '已选',
'msbatchmodal.batchModalCancel': '取消',
'msbatchmodal.batchModalConfirm': '添加',
'msBatchModal.batchModalSubTitle': '已选 {count} 个用户)',
'msBatchModal.batchModalTip': '默认为成员添加项目成员用户组',
'msBatchModal.batchOptional': '可选',
'msBatchModal.batchChosen': '已选',
'msBatchModal.batchModalCancel': '取消',
'msBatchModal.batchModalConfirm': '添加',
'msBatchModal.batchModalEmpty': '选择内容不能为空',
'msBatchModal.batchModalSuccess': '添加成功',
};

View File

@ -49,7 +49,7 @@
</template>
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue';
import { ref, useAttrs, watch, watchEffect } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
@ -94,7 +94,11 @@
//
const switchEnable = ref<boolean>(false);
const dialogVisible = ref<boolean>(props.visible);
const dialogVisible = ref<boolean>(false);
watchEffect(() => {
dialogVisible.value = props.visible;
});
watch(
() => props.visible,
@ -102,6 +106,12 @@
dialogVisible.value = val;
}
);
watch(
() => dialogVisible.value,
(val) => {
emits('update:visible', val);
}
);
watch(
() => props.switchProps?.enable,

View File

@ -0,0 +1,29 @@
<template>
<a-tooltip :content="(props.tagList||[]).map((e: any) => e.name).join('')">
<div class="float-left flex min-h-[22px] max-w-[456px]">
<MsTag v-for="tag of props.tagList.slice(0, props.showNum)" :key="tag.id" v-bind="attrs">
<slot :tag="tag"> </slot>
</MsTag>
<MsTag v-if="props.tagList.length > props.showNum" v-bind="attrs"> +{{ props.showNum }}</MsTag>
</div>
</a-tooltip>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue';
import MsTag from './ms-tag.vue';
const props = withDefaults(
defineProps<{
tagList: any;
showNum?: number;
}>(),
{
showNum: 2,
}
);
const attrs = useAttrs();
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,121 @@
<template>
<a-tag
v-bind="attrs"
:type="props.type"
defer
:style="{ ...typeStyle, 'margin': tagMargin, 'max-width': '144px' }"
:size="props.size"
class="my-1"
>
<slot name="icon"></slot>
<span class="one-line-text"> <slot></slot></span>
</a-tag>
</template>
<script setup lang="ts">
import { computed, ref, useAttrs, watchEffect } from 'vue';
export type TagType = 'default' | 'primary' | 'danger' | 'warning' | 'success';
export type Size = 'small' | 'medium' | 'large';
export type Theme = 'dark' | 'light' | 'outline' | 'lightOutLine';
const attrs = useAttrs();
const props = withDefaults(
defineProps<{
type?: TagType; // tag
size?: Size; // tag
theme?: Theme; // tag
selfStyle?: any; //
}>(),
{
type: 'default',
theme: 'dark',
size: 'medium',
}
);
//
const tagMargin = computed(() => {
switch (props.size) {
case 'medium':
return '3px';
case 'large':
return '4px';
default:
return '2px';
}
});
//
const typeList: any = {
dark: {
'color': 'white',
'border-color': 'rgb(var(--#{}-5))',
'background': 'rgb(var(--#{}-5))',
},
light: {
color: 'rgb(var(--#{}-5))',
background: 'rgb(var(--#{}-1)',
},
outline: {
'border-color': 'rgb(var(--#{}-5))',
'color': 'rgb(var(--#{}-5))',
'background': 'transparent',
},
lightOutLine: {
'border-color': 'rgb(var(--#{}-5))',
'color': 'rgb(var(--#{}-5))',
'background': 'rgb(var(--#{}-1)',
},
default: {
'color': 'var(--color-text-1)',
'background': props.theme !== 'outline' ? 'var(--color-text-n8)' : 'white',
'border-color':
props.theme === 'lightOutLine' || props.theme === 'outline'
? 'var(--color-text-input-border)'
: 'var(--color-text-n8)',
},
};
const typeConst = ref<string>('');
const typeStyle = ref<string[]>();
// : > default >
const getTagType = (type: string, theme: string) => {
if (props.selfStyle && Object.keys(props.selfStyle).length > 0) {
typeStyle.value = props.selfStyle;
} else {
if (type === 'default') {
typeStyle.value = typeList.default;
return;
}
//
if (type === 'primary') {
typeConst.value = 'primary';
//
} else {
typeConst.value = type;
}
// style
if (theme !== 'default' && type !== 'default') {
const themeStyle = typeList[theme];
Object.keys(themeStyle).forEach((item) => {
themeStyle[item] = themeStyle[item].replace('#{}', typeConst.value);
});
typeStyle.value = themeStyle;
}
}
};
watchEffect(() => {
if (props.type && props.theme) {
getTagType(props.type, props.theme);
}
});
</script>
<style scoped lang="less">
:deep(.arco-icon) {
font-size: 14px;
color: var(--color-text-brand);
}
</style>

View File

@ -159,6 +159,51 @@ export const pathMap = [
route: RouteEnum.PROJECT_MANAGEMENT,
permission: [],
level: MENU_LEVEL[2],
children: [],
children: [
{
key: 'PROJECT_MANAGEMENT_PERMISSION', // 项目管理-项目与权限
locale: 'menu.projectManagement.projectPermission',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION,
permission: [],
level: MENU_LEVEL[0],
children: [
{
key: 'PROJECT_MANAGEMENT_PERMISSION_BASIC_INFO', // 项目管理-项目与权限-基本信息
locale: 'project.permission.basicInfo',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION_BASIC_INFO,
permission: [],
level: MENU_LEVEL[0],
},
{
key: 'PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT', // 项目管理-项目与权限-菜单管理
locale: 'project.permission.menuManagement',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT,
permission: [],
level: MENU_LEVEL[0],
},
{
key: 'PROJECT_MANAGEMENT_PERMISSION_VERSION', // 项目管理-项目与权限-项目版本
locale: 'project.permission.projectVersion',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION_VERSION,
permission: [],
level: MENU_LEVEL[0],
},
{
key: 'PROJECT_MANAGEMENT_PERMISSION_MEMBER', // 项目管理-项目与权限-成员
locale: 'project.permission.member',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION_MEMBER,
permission: [],
level: MENU_LEVEL[0],
},
{
key: 'PROJECT_MANAGEMENT_PERMISSION_USER_GROUP', // 项目管理-项目与权限-用户组
locale: 'project.permission.userGroup',
route: RouteEnum.PROJECT_MANAGEMENT_PERMISSION_USER_GROUP,
permission: [],
level: MENU_LEVEL[0],
},
],
},
],
},
];

View File

@ -3,3 +3,55 @@ export interface ProjectTreeData {
title: string;
children?: ProjectTreeData[];
}
export interface UserGroupItem {
id: string;
name: string;
description: string;
internal: boolean;
type: string;
createTime: number | string;
updateTime: number | string;
createUser: string;
scopeId: string;
}
export interface ProjectMemberItem {
id?: string;
name: string;
email: string;
password: string;
enable: boolean; // 是否启用
createTime: number | string;
updateTime: number | string;
language: string;
lastOrganizationId: string; // 组织id
phone: string;
source: string;
lastProjectId: string; // 项目id
createUser: string;
updateUser: string;
deleted: true; // 是否被删除
userRoles: UserGroupItem[];
showUserSelect?: boolean; // 是否可以选择
selectUserList?: string[]; // 已选择项目用户组
}
export interface ActionProjectMember {
userId?: string;
projectId?: string; // 项目ID
userIds?: (string | number)[]; // 用户ID集合
roleIds?: string[]; // 用户组ID集合
}
export interface ProjectUserOption {
id: string;
name: string;
}
export interface SearchParams {
filter: {
roleIds: string[];
};
projectId: string;
keyword: '';
}

View File

@ -17,6 +17,7 @@ export interface UserState {
certification?: number;
role: RoleType;
lastOrganizationId?: string;
lastProjectId?: string;
// 盐
salt: string;
}

View File

@ -9,8 +9,8 @@
</MsTableMoreAction>
</div>
<div class="project-info mb-6 h-[112px] bg-white p-1">
<div class="inner-wrapper p-4">
<div class="detail-info flex flex-col justify-between p-4">
<div class="inner-wrapper rounded-md p-4">
<div class="detail-info flex flex-col justify-between rounded-md p-4">
<div class="flex items-center">
<span class="mr-1 font-medium text-[var(--color-text-000)]">具体的项目名称</span>
<span v-if="!isDelete" class="button enable-button mr-1">{{ t('project.basicInfo.enable') }}</span>
@ -55,6 +55,10 @@
label: 'project.basicInfo.edit',
eventTag: 'edit',
},
{
label: 'project.basicInfo.enable',
eventTag: 'enable',
},
{
label: 'project.basicInfo.finish',
eventTag: 'finish',

View File

@ -3,30 +3,30 @@
v-model:visible="visible"
dialog-size="medium"
title="project.member.addMember"
:close="closeHandler"
:confirm="confirmFunHandler"
ok-text="project.member.add"
:confirm="confirmHandler"
:close="closeHandler"
>
<div class="form">
<a-form ref="memberFormRef" :model="form" size="large" layout="vertical">
<a-form-item
field="memberIds"
field="userIds"
:label="t('project.member.member')"
asterisk-position="end"
:rules="[{ required: true, message: t('project.member.selectMemberEmptyTip') }]"
>
<a-select v-model="form.memberIds" multiple :placeholder="t('project.member.selectMemberScope')" allow-clear>
<a-select v-model="form.userIds" multiple :placeholder="t('project.member.selectMemberScope')" allow-clear>
<a-option v-for="item of memberList" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item
field="userRoleIds"
field="roleIds"
:label="t('project.member.tableColumnUserGroup')"
asterisk-position="end"
:rules="[{ required: true, message: t('project.member.selectUserEmptyTip') }]"
>
<a-select v-model="form.userRoleIds" multiple allow-clear :placeholder="t('project.member.selectUserScope')">
<a-option v-for="item of userGroupOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
<a-select v-model="form.roleIds" multiple allow-clear :placeholder="t('project.member.selectUserScope')">
<a-option v-for="item of props.userGroupOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</a-form-item>
</a-form>
@ -35,49 +35,87 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, watch } from 'vue';
import MsDialog from '@/components/pure/ms-dialog/index.vue';
import { getProjectMemberOptions, addOrUpdateProjectMember } from '@/api/modules/project-management/projectMember';
import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store';
import { FormInstance, Message } from '@arco-design/web-vue';
import type { ProjectUserOption, ActionProjectMember } from '@/models/projectManagement/projectAndPermission';
const { t } = useI18n();
const userStore = useUserStore();
const lastProjectId = userStore.$state?.lastProjectId;
const props = defineProps<{
userGroupOptions: ProjectUserOption[];
}>();
const emits = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'success'): void;
}>();
const visible = ref<boolean>(false);
const initFormValue = {
userRoleIds: [],
memberIds: [],
};
const form = ref({ ...initFormValue });
const memberList = ref([
{
id: '',
name: '全部',
},
]);
const userGroupOptions = ref([
{
id: '',
name: '全部',
},
]);
const initFormValue: ActionProjectMember = {
roleIds: ['project_member'],
userIds: [],
projectId: lastProjectId,
};
const form = ref<ActionProjectMember>({ ...initFormValue });
const memberFormRef = ref<FormInstance | null>(null);
const confirmFunHandler = async () => {
const closeHandler = () => {
memberFormRef.value?.resetFields();
visible.value = false;
form.value = { ...initFormValue };
};
//
const confirmHandler = async () => {
await memberFormRef.value?.validate().then(async (error) => {
if (!error) {
console.log(error);
try {
await addOrUpdateProjectMember(form.value);
Message.success(t('project.member.batchModalSuccess'));
closeHandler();
emits('success');
} catch (e) {
console.log(e);
}
} else {
return false;
}
});
};
const closeHandler = () => {
memberFormRef.value?.resetFields();
visible.value = false;
const memberList = ref<ProjectUserOption[]>([]);
//
const initProjectMemberOptions = async () => {
try {
if (lastProjectId) {
const result = await getProjectMemberOptions(lastProjectId);
memberList.value = result;
}
} catch (error) {
console.log(error);
}
};
watch(
() => visible.value,
(val) => {
emits('update:visible', val);
}
);
defineExpose({
initProjectMemberOptions,
});
</script>
<style scoped></style>

View File

@ -4,10 +4,8 @@
><a-button class="mr-3" type="primary" @click="addMember">{{ t('project.member.addMember') }}</a-button></div
>
<div>
<a-select v-model="searchParams.userId">
<a-option v-for="item of userGroupListOptions" :key="item.value" :value="item.value">{{
t(item.name)
}}</a-option>
<a-select v-model="roleIds" allow-search @change="changeSelect">
<a-option v-for="item of userGroupAll" :key="item.id" :value="item.id">{{ t(item.name) }}</a-option>
<template #prefix
><span>{{ t('project.member.tableColumnUserGroup') }}</span></template
>
@ -15,6 +13,7 @@
>
<div>
<a-input-search
v-model="searchParams.keyword"
:max-length="250"
:placeholder="t('project.member.searchMember')"
allow-clear
@ -31,25 +30,33 @@
@batch-action="handleTableBatch"
>
<template #userRole="{ record }">
<a-tooltip :content="(record.userRoleIdNameMap||[]).map((e: any) => e.name).join(',')">
<a-tooltip :content="(record.userRoles||[]).map((e: any) => e.name).join(',')">
<div v-if="!record.showUserSelect">
<a-tag
v-for="org of (record.userRoleIdNameMap || []).slice(0, 3)"
v-for="org of (record.userRoles || []).slice(0, 3)"
:key="org"
class="mr-[4px] border-[rgb(var(--primary-5))] bg-transparent !text-[rgb(var(--primary-5))]"
bordered
@click="changeUser(record)"
>
{{ org.name }}
</a-tag>
<a-tag
v-if="record.userRoleIdNameMap.length > 3"
v-if="(record.userRoles || []).length > 3"
class="mr-[4px] border-[rgb(var(--primary-5))] bg-transparent !text-[rgb(var(--primary-5))]"
bordered
@click="changeUser(record)"
>
+{{ record.userRoleIdNameMap.length - 3 }}
+{{ (record.userRoles || []).length - 3 }}
</a-tag>
</div>
<a-select v-else v-model="record.selectUserList" multiple :max-tag-count="2">
<a-select
v-else
v-model="record.selectUserList"
multiple
:max-tag-count="2"
@popup-visible-change="(value) => userGroupChange(value, record)"
>
<a-option v-for="item of userGroupOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</a-tooltip>
@ -69,28 +76,40 @@
position="br"
:title="t('project.member.deleteMemberTip', { name: characterLimit(record.name) })"
:sub-title-tip="t('project.member.subTitle')"
@ok="removeMember(record)"
/>
</template>
</ms-base-table>
<AddMemberModal v-model:visible="addMemberVisible" />
<AddMemberModal
ref="projectMemberRef"
v-model:visible="addMemberVisible"
:user-group-options="userGroupOptions"
@success="loadList()"
/>
<MSBatchModal
ref="batchModalRef"
v-model:visible="batchVisible"
:table-selected="tableSelected"
:action="batchAction"
:tree-data="treeData"
:select-data="selectData"
@add-user-group="addUserGroup"
/>
</template>
<script setup lang="ts">
import { ref, onBeforeMount } from 'vue';
import { ref, onBeforeMount, onMounted } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import useTable from '@/components/pure/ms-table/useTable';
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
import { getMemberList } from '@/api/modules/setting/member';
import {
getProjectMemberList,
getProjectUserGroup,
addOrUpdateProjectMember,
batchRemoveMember,
removeProjectMember,
addProjectUserGroup,
} from '@/api/modules/project-management/projectMember';
import { TableKeyEnum } from '@/enums/tableEnum';
import { useTableStore, useUserStore } from '@/store';
import useModal from '@/hooks/useModal';
@ -98,27 +117,22 @@
import { characterLimit } from '@/utils';
import AddMemberModal from './components/addMemberModal.vue';
import MSBatchModal from '@/components/business/ms-batch-modal/index.vue';
import type { ProjectTreeData } from '@/models/projectManagement/projectAndPermission';
import { Message } from '@arco-design/web-vue';
import type {
ProjectTreeData,
ProjectUserOption,
ActionProjectMember,
ProjectMemberItem,
SearchParams,
} from '@/models/projectManagement/projectAndPermission';
const { t } = useI18n();
const { openModal } = useModal();
const tableStore = useTableStore();
const userStore = useUserStore();
const lastOrganizationId = userStore?.$state.lastOrganizationId;
const lastProjectId = userStore?.$state?.lastProjectId;
const userGroupListOptions = ref([
{
id: '',
name: '全部',
value: '',
},
{
id: '1001',
name: '用户组1',
value: '1001',
},
]);
const columns: MsTableColumn = [
{
title: 'project.member.tableColumnEmail',
@ -180,7 +194,7 @@
tableSelected.value = selectArr;
};
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getMemberList, {
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getProjectMemberList, {
tableKey: TableKeyEnum.PROJECT_MEMBER,
selectable: true,
showSetting: true,
@ -189,19 +203,31 @@
},
});
const searchParams = ref<SearchParams>({
filter: {
roleIds: [],
},
projectId: lastProjectId as string,
keyword: '',
});
const roleIds = ref<string>('');
const initData = async () => {
setLoadListParams({ organizationId: lastOrganizationId });
setLoadListParams({ ...searchParams.value });
await loadList();
};
const searchParams = ref({
userId: '',
});
const searchHandler = () => {
initData();
};
const searchHandler = () => {};
const changeSelect = () => {
searchParams.value.filter.roleIds = roleIds.value ? [roleIds.value] : [];
initData();
};
//
const batchRemoveMember = () => {
//
const batchRemoveHandler = () => {
openModal({
type: 'error',
title: t('project.member.batchRemoveTip', { number: tableSelected.value.length }),
@ -211,44 +237,128 @@
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {},
onBeforeOk: async () => {
try {
const params: ActionProjectMember = {
projectId: lastProjectId,
userIds: tableSelected.value,
};
await batchRemoveMember(params);
Message.success(t('project.member.deleteMemberSuccess'));
loadList();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
};
//
const removeMember = async (record: ProjectMemberItem) => {
try {
if (lastProjectId && record.id) {
await removeProjectMember(lastProjectId, record.id);
Message.success(t('project.member.deleteMemberSuccess'));
loadList();
}
} catch (error) {
console.log(error);
}
};
const batchVisible = ref<boolean>(false);
const selectData = ref<string[]>([]);
const batchAction = ref('');
const treeData = ref<ProjectTreeData[]>([]);
const userGroupOptions = ref<ProjectUserOption[]>([]);
const batchModalRef = ref();
//
const addUserGroup = () => {};
const addUserGroup = async (target: string[]) => {
const params = {
projectId: lastProjectId,
userIds: tableSelected.value,
roleIds: target,
};
try {
await batchModalRef.value.batchRequestFun(addProjectUserGroup, params);
loadList();
} catch (error) {
console.log(error);
}
};
//
const handleTableBatch = (actionItem: any) => {
if (actionItem.eventTag === 'batchActionRemove') {
batchRemoveMember();
batchRemoveHandler();
}
if (actionItem.eventTag === 'batchAddUserGroup') {
batchVisible.value = true;
addUserGroup();
batchAction.value = actionItem.eventTag;
batchModalRef.value.getTreeList(getProjectUserGroup, lastProjectId);
}
};
const userGroupOptions = ref([
{
id: '',
name: '',
},
]);
//
const addMemberVisible = ref<boolean>(false);
const projectMemberRef = ref();
const addMember = () => {
addMemberVisible.value = true;
projectMemberRef.value.initProjectMemberOptions();
};
onBeforeMount(() => {
//
const editProjectMember = async (record: ProjectMemberItem) => {
const params: ActionProjectMember = {
projectId: lastProjectId,
userId: record.id,
roleIds: record.selectUserList,
};
try {
await addOrUpdateProjectMember(params);
Message.success(t('project.member.batchUpdateSuccess'));
record.showUserSelect = false;
loadList();
} catch (error) {
console.log(error);
}
};
//
const changeUser = (record: ProjectMemberItem) => {
if (record.enable) {
record.showUserSelect = true;
record.selectUserList = (record.userRoles || []).map((item) => item.id);
}
};
//
const userGroupChange = (visible: boolean, record: ProjectMemberItem) => {
if (visible) {
return;
}
if ((record.selectUserList || []).length < 1) {
Message.warning(t('project.member.selectUserEmptyTip'));
return;
}
editProjectMember(record);
};
const userGroupAll = ref<ProjectUserOption[]>([]);
onBeforeMount(async () => {
initData();
userGroupOptions.value = await getProjectUserGroup(lastProjectId as string);
userGroupAll.value = [
{
id: '',
name: '全部',
},
...userGroupOptions.value,
];
});
</script>

View File

@ -5,7 +5,6 @@ export default {
'project.member.remove': 'Remove',
'project.member.edit': 'Edit',
'project.member.add': 'Add',
'project.member.batchActionAddProject': 'Add to project',
'project.member.batchActionAddUserGroup': 'Add to usergroup',
'project.member.tableEnable': 'Enabled',
'project.member.tableDisable': 'Disabled',
@ -28,7 +27,7 @@ export default {
'system.user.deleteUserTip': 'Are you sure to delete the user `{name}` ?',
'project.member.deleteMemberConfirm': 'Delete',
'project.member.deleteMemberCancel': 'Cancel',
'project.member.deleteMemberSuccess': 'Delete successful',
'project.member.deleteMemberSuccess': 'Remove successful',
'project.member.batchModalSuccess': 'Successfully added',
'project.member.batchUpdateSuccess': 'Successfully updated',
'project.member.project': 'Project',

View File

@ -5,7 +5,6 @@ export default {
'project.member.remove': '移除',
'project.member.edit': '编辑',
'project.member.add': '添加',
'project.member.batchActionAddProject': '添加至项目',
'project.member.batchActionAddUserGroup': '添加至用户组',
'project.member.tableEnable': '正常',
'project.member.tableDisable': '禁用',
@ -27,7 +26,7 @@ export default {
'project.member.deleteMemberTip': '确认移除 {name} 这个成员吗?',
'project.member.deleteMemberConfirm': '确认删除',
'project.member.deleteMemberCancel': '取消',
'project.member.deleteMemberSuccess': '除成功',
'project.member.deleteMemberSuccess': '除成功',
'project.member.batchModalSuccess': '添加成功',
'project.member.batchUpdateSuccess': '更新成功',
'project.member.project': '项目',

View File

@ -5,6 +5,7 @@
class="ms-modal-form ms-modal-medium"
:ok-text="t('organization.member.Confirm')"
:cancel-text="t('organization.member.Cancel')"
@close="handleCancel"
>
<template #title>
{{

View File

@ -24,71 +24,52 @@
@batch-action="handleTableBatch"
>
<template #project="{ record }">
<a-tooltip :content="(record.projectIdNameMap||[]).map((e: any) => e.name).join(',')">
<div v-if="!record.showProjectSelect">
<a-tag
v-for="pro of (record.projectIdNameMap || []).slice(0, 3)"
:key="pro.id"
class="mr-[4px] bg-transparent"
bordered
@click="changeUserOrProject(record, 'project')"
>
{{ pro.name }}
</a-tag>
<a-tag
v-if="(record.projectIdNameMap || []).length > 3"
class="mr-[4px] bg-transparent"
bordered
@click="changeUserOrProject(record, 'project')"
>
+{{ (record.projectIdNameMap || []).length - 3 }}
</a-tag>
</div>
<a-select
v-else
v-model="record.selectProjectList"
multiple
:max-tag-count="2"
size="small"
@change="(value) => selectUserOrProject(value, record, 'project')"
@popup-visible-change="visibleChange($event, record, 'project')"
>
<a-option v-for="item of projectOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</a-tooltip>
<MsTagGroup
v-if="!record.showProjectSelect"
:tag-list="record.projectIdNameMap || []"
:show-num="2"
theme="outline"
@click="changeUserOrProject(record, 'project')"
>
<template #default="{ tag }">
{{ tag.name }}
</template>
</MsTagGroup>
<a-select
v-else
v-model="record.selectProjectList"
multiple
:max-tag-count="2"
size="small"
@change="(value) => selectUserOrProject(value, record, 'project')"
@popup-visible-change="visibleChange($event, record, 'project')"
>
<a-option v-for="item of projectOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</template>
<template #userRole="{ record }">
<a-tooltip :content="(record.userRoleIdNameMap||[]).map((e: any) => e.name).join(',')">
<div v-if="!record.showUserSelect">
<a-tag
v-for="org of (record.userRoleIdNameMap || []).slice(0, 3)"
:key="org"
class="mr-[4px] border-[rgb(var(--primary-5))] bg-transparent !text-[rgb(var(--primary-5))]"
bordered
@click="changeUserOrProject(record, 'user')"
>
{{ org.name }}
</a-tag>
<a-tag
v-if="record.userRoleIdNameMap.length > 3"
class="mr-[4px] border-[rgb(var(--primary-5))] bg-transparent !text-[rgb(var(--primary-5))]"
bordered
@click="changeUserOrProject(record, 'user')"
>
+{{ record.userRoleIdNameMap.length - 3 }}
</a-tag>
</div>
<a-select
v-else
v-model="record.selectUserList"
multiple
:max-tag-count="2"
@change="(value) => selectUserOrProject(value, record, 'user')"
@popup-visible-change="(value) => visibleChange(value, record, 'user')"
>
<a-option v-for="item of userGroupOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</a-tooltip>
<MsTagGroup
v-if="!record.showUserSelect"
:tag-list="record.userRoleIdNameMap || []"
:show-num="2"
type="primary"
theme="outline"
@click="changeUserOrProject(record, 'user')"
>
<template #default="{ tag }">
{{ tag.name }}
</template>
</MsTagGroup>
<a-select
v-else
v-model="record.selectUserList"
multiple
:max-tag-count="2"
@change="(value) => selectUserOrProject(value, record, 'user')"
@popup-visible-change="(value) => visibleChange(value, record, 'user')"
>
<a-option v-for="item of userGroupOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</template>
<template #enable="{ record }">
<div v-if="record.enable" class="flex items-center">
@ -119,12 +100,10 @@
@success="initData()"
/>
<MSBatchModal
v-if="treeData.length > 0"
ref="batchModalRef"
v-model:visible="showBatchModal"
:table-selected="tableSelected"
:action="batchAction"
:tree-data="treeData"
:select-data="selectData"
@add-project="addProjectOrAddUserGroup"
@add-user-group="addProjectOrAddUserGroup"
@ -154,19 +133,14 @@
import MSBatchModal from '@/components/business/ms-batch-modal/index.vue';
import { useTableStore, useUserStore } from '@/store';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type {
MemberItem,
AddorUpdateMemberModel,
LinkList,
LinkItem,
BatchAddProjectModel,
} from '@/models/setting/member';
import type { MemberItem, AddorUpdateMemberModel, LinkList, BatchAddProjectModel } from '@/models/setting/member';
import { characterLimit } from '@/utils';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
const tableStore = useTableStore();
const { t } = useI18n();
const userStore = useUserStore();
const lastOrganizationId = userStore.$state?.lastOrganizationId as string;
const lastOrganizationId = userStore.$state?.lastOrganizationId;
const columns: MsTableColumn = [
{
@ -237,13 +211,6 @@
const tableSelected = ref<(string | number)[]>([]);
const selectData = ref<string[]>([]);
interface TreeDataItem {
key: string;
title: string;
children?: TreeDataItem[];
}
const batchAction = ref('');
const initData = async () => {
setLoadListParams({ keyword: keyword.value, organizationId: lastOrganizationId });
await loadList();
@ -264,7 +231,7 @@
};
const deleteMember = async (record: MemberItem) => {
try {
await deleteMemberReq(lastOrganizationId, record.id);
if (lastOrganizationId) await deleteMemberReq(lastOrganizationId, record.id);
Message.success(t('organization.member.deleteMemberSuccess'));
initData();
} catch (error) {
@ -275,23 +242,12 @@
tableSelected.value = selectArr;
};
const treeData = ref<TreeDataItem[]>([]);
const getData = async (callBack: any) => {
try {
const links = await callBack(lastOrganizationId);
treeData.value = links.map((item: LinkItem) => {
return {
title: item.name,
key: item.id,
id: item.id,
};
});
} catch (error) {
console.log(error);
}
const batchModalRef = ref();
const getData = (callBack: any) => {
batchModalRef.value.getTreeList(callBack, lastOrganizationId);
};
const batchModalRef = ref();
const showBatchModal = ref(false);
const batchList = [
@ -300,10 +256,13 @@
request: batchAddProject,
},
{
type: 'usergroup',
type: 'userGroup',
request: batchAddUserGroup,
},
];
const batchAction = ref('');
//
const addProjectOrAddUserGroup = async (target: string[], type: string) => {
const currentType = batchList.find((item) => item.type === type);
@ -322,7 +281,6 @@
//
const handleTableBatch = (actionItem: any) => {
showBatchModal.value = true;
treeData.value = [];
batchAction.value = actionItem.eventTag;
if (actionItem.eventTag === 'batchAddProject') getData(getProjectList);
if (actionItem.eventTag === 'batchAddUserGroup') getData(getGlobalUserGroup);
@ -383,8 +341,10 @@
const userGroupOptions = ref<LinkList>([]);
const projectOptions = ref<LinkList>([]);
const getLinkList = async () => {
userGroupOptions.value = await getGlobalUserGroup(lastOrganizationId);
projectOptions.value = await getProjectList(lastOrganizationId);
if (lastOrganizationId) {
userGroupOptions.value = await getGlobalUserGroup(lastOrganizationId);
projectOptions.value = await getProjectList(lastOrganizationId);
}
};
onBeforeMount(() => {
initData();