From acd41555f9c647a5279db9bbcc3e024759cb799b Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Mon, 18 May 2026 22:25:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(projects):=201=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=B0=E5=A2=9E=20=E4=BA=A7=E5=93=81=E5=92=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B9=E7=9B=AE=EF=BC=9B2=E3=80=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=A7=92=E8=89=B2=E6=8F=90=E7=A4=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/api/product-shared.ts | 3 - src/service/api/product.ts | 22 + src/service/api/project-shared.ts | 3 - src/service/api/project.ts | 22 + src/typings/api/product.d.ts | 30 +- src/typings/api/project.d.ts | 31 +- src/typings/api/system-manage.d.ts | 4 +- src/typings/components.d.ts | 4 + .../product-create-team-member-dialog.vue | 98 +- .../list/modules/product-create-team-step.vue | 316 ++--- .../list/modules/product-operate-dialog.vue | 228 +++- src/views/product/setting/index.vue | 81 +- .../modules/member-batch-remove-dialog.vue | 75 ++ .../setting/modules/setting-team-panel.vue | 117 +- .../components/product-team-batch-dialog.vue | 1157 +++++++++++++++++ .../project-create-team-member-dialog.vue | 64 +- .../list/modules/project-create-team-step.vue | 316 ++--- .../list/modules/project-operate-dialog.vue | 231 +++- src/views/project/project/setting/index.vue | 81 +- .../modules/member-batch-remove-dialog.vue | 81 ++ .../setting/modules/setting-team-panel.vue | 110 +- .../components/project-team-batch-dialog.vue | 1157 +++++++++++++++++ 22 files changed, 3588 insertions(+), 643 deletions(-) create mode 100644 src/views/product/setting/modules/member-batch-remove-dialog.vue create mode 100644 src/views/product/shared/components/product-team-batch-dialog.vue create mode 100644 src/views/project/project/setting/modules/member-batch-remove-dialog.vue create mode 100644 src/views/project/shared/components/project-team-batch-dialog.vue diff --git a/src/service/api/product-shared.ts b/src/service/api/product-shared.ts index ca8b373..6a02a8d 100644 --- a/src/service/api/product-shared.ts +++ b/src/service/api/product-shared.ts @@ -33,8 +33,6 @@ interface ProductMemberResponse { roleId: string | number; roleName: string; roleCode: string; - /** 多角色合并展示的非主角色名列表 */ - additionalRoleNames?: string[] | null; managerFlag: boolean; status: 0 | 1; joinedTime: string; @@ -76,7 +74,6 @@ export function normalizeProductMember(response: ProductMemberResponse): Api.Pro roleId: normalizeStringId(response.roleId), roleName: response.roleName || '', roleCode: response.roleCode || '', - additionalRoleNames: response.additionalRoleNames ?? [], managerFlag: Boolean(response.managerFlag), status: response.status, joinedTime: response.joinedTime, diff --git a/src/service/api/product.ts b/src/service/api/product.ts index c74363b..076b362 100644 --- a/src/service/api/product.ts +++ b/src/service/api/product.ts @@ -574,6 +574,19 @@ export async function fetchCreateProductMember(id: string, data: Api.Product.Cre return mapServiceResult(result as ServiceRequestResult, normalizeStringId); } +export async function fetchBatchCreateProductMembers(id: string, data: Api.Product.BatchCreateProductMembersParams) { + const result = await request>({ + ...safeJsonRequestConfig, + url: `${PRODUCT_PREFIX}/${id}/members/batch`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult>, list => + Array.isArray(list) ? list.map(normalizeStringId) : [] + ); +} + export function fetchUpdateProductMember(id: string, memberId: string, data: Api.Product.UpdateProductMemberParams) { return request({ ...safeJsonRequestConfig, @@ -583,6 +596,15 @@ export function fetchUpdateProductMember(id: string, memberId: string, data: Api }); } +export function fetchBatchInactiveProductMembers(id: string, data: Api.Product.BatchInactiveProductMembersParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PRODUCT_PREFIX}/${id}/members/batch/inactive`, + method: 'post', + data + }); +} + export function fetchInactiveProductMember( id: string, memberId: string, diff --git a/src/service/api/project-shared.ts b/src/service/api/project-shared.ts index a0dd2a5..b709815 100644 --- a/src/service/api/project-shared.ts +++ b/src/service/api/project-shared.ts @@ -147,8 +147,6 @@ export interface ProjectMemberResponse { roleId: string | number; roleName: string; roleCode: string; - /** 多角色合并展示的非主角色名列表 */ - additionalRoleNames?: string[] | null; managerFlag: boolean; status: 0 | 1; joinedTime: string; @@ -227,7 +225,6 @@ export function normalizeProjectMember(response: ProjectMemberResponse): Api.Pro roleId: normalizeStringId(response.roleId), roleName: response.roleName || '', roleCode: response.roleCode || '', - additionalRoleNames: response.additionalRoleNames ?? [], managerFlag: Boolean(response.managerFlag), status: response.status, joinedTime: response.joinedTime, diff --git a/src/service/api/project.ts b/src/service/api/project.ts index bd7ef75..de92e98 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -284,6 +284,28 @@ export function fetchInactiveProjectMember( }); } +export async function fetchBatchCreateProjectMembers(id: string, data: Api.Project.BatchCreateProjectMembersParams) { + const result = await request>({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members/batch`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult>, list => + Array.isArray(list) ? list.map(normalizeStringId) : [] + ); +} + +export function fetchBatchInactiveProjectMembers(id: string, data: Api.Project.BatchInactiveProjectMembersParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members/batch/inactive`, + method: 'post', + data + }); +} + /** 获取项目设置 */ export async function fetchGetProjectSettings(id: string) { const result = await fetchGetProject(id); diff --git a/src/typings/api/product.d.ts b/src/typings/api/product.d.ts index f9bc3d4..556ebea 100644 --- a/src/typings/api/product.d.ts +++ b/src/typings/api/product.d.ts @@ -99,15 +99,10 @@ declare namespace Api { userNickname: string; /** 角色 ID */ roleId: string; - /** 角色名称(主角色) */ + /** 角色名称 */ roleName: string; - /** 角色编码(主角色) */ + /** 角色编码 */ roleCode: string; - /** - * 非主角色的中文名列表(多角色合并展示用,按字典序升序) - * 单角色时为空数组 [];典型场景:创建者 + 经理重合时,主行 manager,creator 名进此列表 - */ - additionalRoleNames: string[]; /** 是否当前产品经理 */ managerFlag: boolean; /** 成员状态 */ @@ -215,6 +210,20 @@ declare namespace Api { previousManagerRoleId?: string | null; } + /** + * 批量新增产品成员参数 + * + * 刻意不复用 CreateProductMemberParams:批量接口不承担「产品经理交接」语义, + * 后端兜底拒绝 roleId 为产品经理角色的项。 + */ + interface BatchCreateProductMembersParams { + members: Array<{ + userId: string; + roleId: string; + remark?: string | null; + }>; + } + /** * 产品创建(含初始团队)原子接口参数 * @@ -223,7 +232,7 @@ declare namespace Api { interface CreateProductWithTeamParams { product: SaveProductParams; members: CreateProductMemberParams[]; - /** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */ + /** 关注人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */ watcherUserIds?: string[]; } @@ -239,6 +248,11 @@ declare namespace Api { reason?: string | null; } + interface BatchInactiveProductMembersParams { + memberIds: string[]; + reason?: string | null; + } + // ========== 产品需求相关类型定义 ========== /** 需求状态编码 */ type RequirementStatusCode = diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 15d1358..ea7bd72 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -519,15 +519,10 @@ declare namespace Api { userNickname: string; /** 角色 ID */ roleId: string; - /** 角色名称(主角色) */ + /** 角色名称 */ roleName: string; - /** 角色编码(主角色) */ + /** 角色编码 */ roleCode: string; - /** - * 非主角色的中文名列表(多角色合并展示用,按字典序升序) - * 单角色时为空数组 [];典型场景:创建者 + 负责人重合时,主行 manager,creator 名进此列表 - */ - additionalRoleNames: string[]; /** 是否项目负责人 */ managerFlag: boolean; /** 成员状态 */ @@ -625,6 +620,26 @@ declare namespace Api { reason: string | null; } + /** + * 批量新增项目成员参数 + * + * 刻意不复用 CreateProjectMemberParams:批量接口不承担"项目负责人交接"语义, + * 后端兜底拒绝 roleId 为项目负责人角色的项。 + */ + interface BatchCreateProjectMembersParams { + members: Array<{ + userId: string; + roleId: string; + remark?: string | null; + }>; + } + + /** 批量移出项目成员参数 */ + interface BatchInactiveProjectMembersParams { + memberIds: string[]; + reason?: string | null; + } + /** * 项目创建(含初始团队)原子接口参数 * @@ -633,7 +648,7 @@ declare namespace Api { interface CreateProjectWithTeamParams { project: SaveProjectParams; members: CreateProjectMemberParams[]; - /** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */ + /** 关注人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */ watcherUserIds?: string[]; } diff --git a/src/typings/api/system-manage.d.ts b/src/typings/api/system-manage.d.ts index 7fbcbbb..92a57dc 100644 --- a/src/typings/api/system-manage.d.ts +++ b/src/typings/api/system-manage.d.ts @@ -47,6 +47,8 @@ declare namespace Api { type: RoleType; /** remark */ remark?: string | null; + /** 是否在前端选择面板可见:0 不可见 / 1 可见,缺省视作可见 */ + visible?: 0 | 1 | null; /** create time */ createTime: number; } @@ -226,7 +228,7 @@ declare namespace Api { type PostList = PageResult; - type RoleSimple = Pick; + type RoleSimple = Pick; type RoleSimpleList = RoleSimple[]; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 90fe525..3497532 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -100,6 +100,10 @@ declare module 'vue' { IconCarbonStop: typeof import('~icons/carbon/stop')['default'] 'IconCharm:download': typeof import('~icons/charm/download')['default'] 'IconEp:arrowDown': typeof import('~icons/ep/arrow-down')['default'] + 'IconEp:box': typeof import('~icons/ep/box')['default'] + 'IconEp:files': typeof import('~icons/ep/files')['default'] + 'IconEp:infoFilled': typeof import('~icons/ep/info-filled')['default'] + 'IconEp:plus': typeof import('~icons/ep/plus')['default'] IconEpRemoveFilled: typeof import('~icons/ep/remove-filled')['default'] IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default'] 'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default'] diff --git a/src/views/product/list/modules/product-create-team-member-dialog.vue b/src/views/product/list/modules/product-create-team-member-dialog.vue index 7c677b4..bf46329 100644 --- a/src/views/product/list/modules/product-create-team-member-dialog.vue +++ b/src/views/product/list/modules/product-create-team-member-dialog.vue @@ -1,14 +1,10 @@ diff --git a/src/views/product/list/modules/product-create-team-step.vue b/src/views/product/list/modules/product-create-team-step.vue index 144cb9a..9fa3cb1 100644 --- a/src/views/product/list/modules/product-create-team-step.vue +++ b/src/views/product/list/modules/product-create-team-step.vue @@ -1,8 +1,9 @@ @@ -326,16 +262,65 @@ defineExpose({ validate: runValidate }); display: flex; flex-direction: column; gap: 14px; + height: 100%; min-height: 0; } -.team-step__toolbar { +.team-step__add { display: flex; - justify-content: flex-end; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 44px; + padding: 0 16px; + border: 1px dashed var(--el-border-color-darker); + border-radius: 6px; + background: transparent; + color: var(--el-text-color-regular); + font-size: 13px; + cursor: pointer; + transition: + border-color 0.2s ease, + color 0.2s ease, + background-color 0.2s ease; + flex-shrink: 0; +} + +.team-step__add:hover:not(:disabled) { + border-color: var(--el-color-primary); + color: var(--el-color-primary); + background: var(--el-color-primary-light-9); +} + +.team-step__add:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.team-step__add-icon { + font-size: 16px; +} + +.team-step__add-hint { + color: var(--el-text-color-placeholder); + font-size: 12px; + font-weight: 400; +} + +.team-step__add:hover:not(:disabled) .team-step__add-hint { + color: var(--el-color-primary); + opacity: 0.7; } .team-step__alert { margin: 0; + flex-shrink: 0; +} + +.team-step__table-wrap { + flex: 1 1 auto; + min-height: 0; } .team-step__actions { @@ -343,27 +328,4 @@ defineExpose({ validate: runValidate }); align-items: center; gap: 12px; } - -.watcher-row { - display: flex; - align-items: center; - gap: 10px; -} - -.watcher-row__label { - flex: 0 0 auto; - font-size: 13px; - font-weight: 500; - color: rgb(60 70 95 / 96%); -} - -.watcher-row__optional { - color: rgb(140 150 170 / 96%); - font-weight: 400; -} - -.watcher-row__select { - flex: 1 1 auto; - min-width: 0; -} diff --git a/src/views/product/list/modules/product-operate-dialog.vue b/src/views/product/list/modules/product-operate-dialog.vue index 64b796a..e50c922 100644 --- a/src/views/product/list/modules/product-operate-dialog.vue +++ b/src/views/product/list/modules/product-operate-dialog.vue @@ -1,7 +1,8 @@ + + diff --git a/src/views/product/setting/modules/setting-team-panel.vue b/src/views/product/setting/modules/setting-team-panel.vue index 23fa53d..1a50826 100644 --- a/src/views/product/setting/modules/setting-team-panel.vue +++ b/src/views/product/setting/modules/setting-team-panel.vue @@ -1,5 +1,6 @@ + + + + diff --git a/src/views/project/list/modules/project-create-team-member-dialog.vue b/src/views/project/list/modules/project-create-team-member-dialog.vue index c90d653..84f3b22 100644 --- a/src/views/project/list/modules/project-create-team-member-dialog.vue +++ b/src/views/project/list/modules/project-create-team-member-dialog.vue @@ -1,14 +1,10 @@ diff --git a/src/views/project/list/modules/project-operate-dialog.vue b/src/views/project/list/modules/project-operate-dialog.vue index 28dd27c..4a7d6eb 100644 --- a/src/views/project/list/modules/project-operate-dialog.vue +++ b/src/views/project/list/modules/project-operate-dialog.vue @@ -2,8 +2,14 @@ import { computed, nextTick, ref, watch } from 'vue'; import { ElCol, ElDatePicker, ElFormItem, ElInput, ElRow } from 'element-plus'; import dayjs from 'dayjs'; +import { PROJECT_MANAGER_ROLE_CODE } from '@/constants/business'; import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict'; -import { fetchCreateProjectWithTeam, fetchGetProductPage, fetchUpdateProject } from '@/service/api'; +import { + fetchCreateProjectWithTeam, + fetchGetProductPage, + fetchGetRoleSimpleList, + fetchUpdateProject +} from '@/service/api'; import { useDict } from '@/hooks/business/dict'; import { useForm, useFormRules } from '@/hooks/common/form'; import BusinessFormDialog from '@/components/custom/business-form-dialog.vue'; @@ -265,9 +271,29 @@ const baseFormRef = ref | null>(null) const teamStepRef = ref | null>(null); const currentStep = ref<1 | 2>(1); +// === 新增模式:角色列表(父级加载,下发给 team-step 与批量弹层) === +const roleOptions = ref([]); +const roleLoading = ref(false); +const managerRoleError = ref(''); + +const managerRole = computed(() => roleOptions.value.find(item => item.code === PROJECT_MANAGER_ROLE_CODE) ?? null); + +async function loadRoles() { + roleLoading.value = true; + managerRoleError.value = ''; + + const { data } = await fetchGetRoleSimpleList({ scopeType: 'object', objectType: 'project' }); + + roleLoading.value = false; + roleOptions.value = data ?? []; + + if (!managerRole.value) { + managerRoleError.value = '未找到项目经理角色,请联系管理员'; + } +} + const createBaseModel = ref(createBaseInfo()); const draftMembers = ref([]); -const draftWatcherUserIds = ref([]); function createBaseInfo(): ProjectCreateBaseFormModel { return { @@ -325,8 +351,7 @@ async function handleCreateSubmit() { plannedEndDate: createBaseModel.value.plannedEndDate, projectDesc: getNullableText(createBaseModel.value.projectDesc) }, - members: draftMembers.value, - watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined + members: draftMembers.value }; const { error, data } = await fetchCreateProjectWithTeam(payload); @@ -355,8 +380,8 @@ watch(visible, async value => { editModel.value = createEditModel(); createBaseModel.value = createBaseInfo(); draftMembers.value = []; - draftWatcherUserIds.value = []; await nextTick(); + await loadRoles(); editFormRef.value?.clearValidate(); return; } @@ -521,7 +546,7 @@ watch(visible, async value => { - + { :close-on-click-modal="false" destroy-on-close align-center - width="760px" + width="1080px" > -
-
- 1 - - 基础资料 - 定义项目身份和负责人 - -
-
- 2 - - 初始化团队 - 配置对象域成员角色 - -
-
+
+ + +
+
+
+ 1 + + 基础资料 + +
+
+ 2 + + 初始化团队 + +
+
+ +
+
+ +
+
+ +
+
@@ -608,6 +675,92 @@ watch(visible, async value => { padding: 0; } +.project-create-dialog__split { + display: grid; + grid-template-columns: 280px 1fr; + min-height: 0; +} + +.project-create-dialog__guide { + padding: 28px 24px; + background: linear-gradient(180deg, #f7f9fc 0%, #fafbfc 100%); + border-right: 1px solid rgb(229 233 242 / 96%); + overflow-y: auto; + max-height: min(720px, calc(100vh - 160px)); +} + +.project-create-dialog__guide-hero { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 24px; + padding-bottom: 20px; + border-bottom: 1px solid rgb(229 233 242 / 96%); +} + +.project-create-dialog__guide-hero-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 10px; + background: var(--el-color-primary-light-9); + color: var(--el-color-primary); + font-size: 22px; + flex-shrink: 0; +} + +.project-create-dialog__guide-hero-text { + min-width: 0; +} + +.project-create-dialog__guide-hero-title { + font-size: 15px; + font-weight: 650; + color: var(--el-text-color-primary); + line-height: 1.4; +} + +.project-create-dialog__guide-hero-sub { + margin-top: 2px; + font-size: 12px; + color: var(--el-text-color-secondary); + line-height: 1.4; +} + +.project-create-dialog__guide-lead { + margin: 0 0 24px; + font-size: 13.5px; + line-height: 1.8; + color: var(--el-text-color-regular); +} + +.project-create-dialog__guide-section + .project-create-dialog__guide-section { + margin-top: 20px; +} + +.project-create-dialog__guide-section h4 { + margin: 0 0 6px; + font-size: 12.5px; + font-weight: 700; + color: var(--el-text-color-primary); + letter-spacing: 0.4px; +} + +.project-create-dialog__guide-section p { + margin: 0; + font-size: 13px; + line-height: 1.75; + color: var(--el-text-color-regular); +} + +.project-create-dialog__main { + display: flex; + flex-direction: column; + min-width: 0; +} + .project-create-dialog__stepbar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -663,13 +816,15 @@ watch(visible, async value => { } .project-create-dialog__body { - min-height: 0; - max-height: min(560px, calc(100vh - 240px)); - overflow: auto; + height: min(520px, calc(100vh - 240px)); + overflow: hidden; } .project-create-dialog__panel { + height: 100%; padding: 24px; + overflow: auto; + box-sizing: border-box; } .project-create-dialog__footer { diff --git a/src/views/project/project/setting/index.vue b/src/views/project/project/setting/index.vue index d6f04f2..50667be 100644 --- a/src/views/project/project/setting/index.vue +++ b/src/views/project/project/setting/index.vue @@ -4,6 +4,8 @@ import { useMediaQuery } from '@vueuse/core'; import { LAYOUT_SCROLL_EL_ID } from '@sa/materials'; import { objectContextDomainConfigs } from '@/constants/object-context'; import { + fetchBatchCreateProjectMembers, + fetchBatchInactiveProjectMembers, fetchChangeProjectStatus, fetchCreateProjectMember, fetchDeleteProject, @@ -18,8 +20,12 @@ import { import { useObjectContextStore } from '@/store/modules/object-context'; import { useThemeStore } from '@/store/modules/theme'; import { useRouterPush } from '@/hooks/common/router'; +import ProjectTeamBatchDialog, { + type BatchMemberPayload +} from '@/views/project/shared/components/project-team-batch-dialog.vue'; import { useCurrentProject } from '../../shared/use-current-project'; import BaseInfoDialog from './modules/base-info-dialog.vue'; +import MemberBatchRemoveDialog from './modules/member-batch-remove-dialog.vue'; import MemberOperateDialog from './modules/member-operate-dialog.vue'; import MemberRemoveDialog from './modules/member-remove-dialog.vue'; import ProjectDeleteDialog from './modules/project-delete-dialog.vue'; @@ -68,7 +74,11 @@ const pageLoading = ref(false); const memberLoading = ref(false); const baseInfoVisible = ref(false); const memberOperateVisible = ref(false); +const memberBatchVisible = ref(false); const memberRemoveVisible = ref(false); +const memberBatchRemoveVisible = ref(false); +const teamPanelRef = ref | null>(null); +const selectedBatchRemoveMembers = ref([]); const statusActionVisible = ref(false); const deleteVisible = ref(false); const memberOperateMode = ref<'create' | 'edit'>('create'); @@ -211,9 +221,7 @@ function scrollToSection(key: string) { } function openCreateMember() { - memberOperateMode.value = 'create'; - selectedMember.value = null; - memberOperateVisible.value = true; + memberBatchVisible.value = true; } function openEditMember(member: Api.Project.ProjectMember) { @@ -227,6 +235,12 @@ function openRemoveMember(member: Api.Project.ProjectMember) { memberRemoveVisible.value = true; } +function openBatchRemoveMember(targetMembers: Api.Project.ProjectMember[]) { + if (!targetMembers.length) return; + selectedBatchRemoveMembers.value = targetMembers; + memberBatchRemoveVisible.value = true; +} + function openLifecycleAction(action: Api.Project.ProjectLifecycleAction) { selectedAction.value = action; statusActionVisible.value = true; @@ -299,6 +313,53 @@ async function handleSubmitRemoveMember(payload: Api.Project.InactiveProjectMemb await Promise.all([loadMembers(), loadSettings()]); } +async function handleSubmitMemberBatch(payloads: BatchMemberPayload[]) { + if (!currentObjectId.value || !payloads.length) { + return; + } + + const { error } = await fetchBatchCreateProjectMembers(currentObjectId.value, { + members: payloads.map(item => ({ + userId: item.userId, + roleId: item.roleId, + remark: item.remark.trim() || null + })) + }); + + if (error) { + return; + } + + window.$message?.success(`已新增 ${payloads.length} 名成员`); + memberBatchVisible.value = false; + + await Promise.all([loadMembers(), loadSettings()]); +} + +async function handleSubmitBatchRemoveMember(payload: { reason: string | null }) { + if (!currentObjectId.value || !selectedBatchRemoveMembers.value.length) { + return; + } + + const memberIds = selectedBatchRemoveMembers.value.map(item => item.id).filter((id): id is string => Boolean(id)); + + if (!memberIds.length) return; + + const { error } = await fetchBatchInactiveProjectMembers(currentObjectId.value, { + memberIds, + reason: payload.reason + }); + + if (error) return; + + window.$message?.success(`已移出 ${memberIds.length} 名成员`); + memberBatchRemoveVisible.value = false; + selectedBatchRemoveMembers.value = []; + teamPanelRef.value?.clearSelection(); + + await Promise.all([loadMembers(), loadSettings()]); +} + async function handleSubmitLifecycleAction(payload: Api.Project.ChangeProjectStatusParams) { if (!currentObjectId.value || !selectedAction.value) { return; @@ -387,6 +448,7 @@ watch(
@@ -432,6 +495,18 @@ watch( :member="selectedMember" @submit="handleSubmitRemoveMember" /> + + +import { computed, reactive, watch } from 'vue'; +import BusinessFormDialog from '@/components/custom/business-form-dialog.vue'; + +defineOptions({ name: 'ProjectMemberBatchRemoveDialog' }); + +interface Props { + members: Api.Project.ProjectMember[]; +} + +interface Emits { + (e: 'submit', payload: { reason: string | null }): void; +} + +const props = defineProps(); +const emit = defineEmits(); + +const visible = defineModel('visible', { + default: false +}); + +const model = reactive({ + reason: '' +}); + +const previewNames = computed(() => { + const names = props.members.map(item => item.userNickname || item.userId || '').filter(Boolean); + + if (names.length <= 5) { + return names.join('、'); + } + + return `${names.slice(0, 5).join('、')} 等 ${names.length} 人`; +}); + +function handleConfirm() { + emit('submit', { + reason: model.reason.trim() || null + }); +} + +watch( + () => visible.value, + value => { + if (!value) { + return; + } + + model.reason = ''; + } +); + + + diff --git a/src/views/project/project/setting/modules/setting-team-panel.vue b/src/views/project/project/setting/modules/setting-team-panel.vue index 7ac1c9b..5e9fd2d 100644 --- a/src/views/project/project/setting/modules/setting-team-panel.vue +++ b/src/views/project/project/setting/modules/setting-team-panel.vue @@ -1,5 +1,6 @@ + + + +