refactor(projects): 优化产品项目新增逻辑
This commit is contained in:
@@ -33,6 +33,8 @@ interface ProductMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -74,6 +76,7 @@ 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,
|
||||
|
||||
@@ -147,6 +147,8 @@ export interface ProjectMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -225,6 +227,7 @@ 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,
|
||||
|
||||
11
src/typings/api/product.d.ts
vendored
11
src/typings/api/product.d.ts
vendored
@@ -99,10 +99,15 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称 */
|
||||
/** 角色名称(主角色) */
|
||||
roleName: string;
|
||||
/** 角色编码 */
|
||||
/** 角色编码(主角色) */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 经理重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否当前产品经理 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -218,6 +223,8 @@ declare namespace Api {
|
||||
interface CreateProductWithTeamParams {
|
||||
product: SaveProductParams;
|
||||
members: CreateProductMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
interface UpdateProductMemberParams {
|
||||
|
||||
11
src/typings/api/project.d.ts
vendored
11
src/typings/api/project.d.ts
vendored
@@ -519,10 +519,15 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称 */
|
||||
/** 角色名称(主角色) */
|
||||
roleName: string;
|
||||
/** 角色编码 */
|
||||
/** 角色编码(主角色) */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 负责人重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否项目负责人 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -628,6 +633,8 @@ declare namespace Api {
|
||||
interface CreateProjectWithTeamParams {
|
||||
project: SaveProjectParams;
|
||||
members: CreateProjectMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
// ========== 项目需求相关类型定义 ==========
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -104,6 +104,7 @@ declare module 'vue' {
|
||||
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-circle-fill')['default']
|
||||
'IconFe:eye': typeof import('~icons/fe/eye')['default']
|
||||
'IconFe:question': typeof import('~icons/fe/question')['default']
|
||||
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
||||
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const teamTableHeight = getProductTeamTableHeight(5);
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含产品经理本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProductTeamTableHeight(4);
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
@@ -205,6 +214,24 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此产品的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<ProductCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
@@ -295,4 +343,27 @@ 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,6 +114,7 @@ const currentStep = ref<1 | 2>(1);
|
||||
|
||||
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProductCreateBaseFormModel {
|
||||
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
|
||||
@@ -157,7 +158,8 @@ async function handleCreateSubmit() {
|
||||
managerUserId: createBaseModel.value.managerUserId as string,
|
||||
description: getNullableText(createBaseModel.value.description)
|
||||
},
|
||||
members: draftMembers.value
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProductWithTeam(payload);
|
||||
@@ -186,6 +188,7 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
@@ -330,6 +333,7 @@ watch(visible, async value => {
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,23 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn prop="roleName" label="当前角色" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||
@@ -180,6 +196,17 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.setting-team-panel__header {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Project.CreateProjectMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const teamTableHeight = getProjectTeamTableHeight(5);
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含项目负责人本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProjectTeamTableHeight(4);
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
@@ -205,6 +214,24 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此项目的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<ProjectCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
@@ -295,4 +343,27 @@ 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -267,6 +267,7 @@ const currentStep = ref<1 | 2>(1);
|
||||
|
||||
const createBaseModel = ref<ProjectCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Project.CreateProjectMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProjectCreateBaseFormModel {
|
||||
return {
|
||||
@@ -324,7 +325,8 @@ async function handleCreateSubmit() {
|
||||
plannedEndDate: createBaseModel.value.plannedEndDate,
|
||||
projectDesc: getNullableText(createBaseModel.value.projectDesc)
|
||||
},
|
||||
members: draftMembers.value
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProjectWithTeam(payload);
|
||||
@@ -353,6 +355,7 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
@@ -556,6 +559,7 @@ watch(visible, async value => {
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,23 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn prop="roleName" label="当前角色" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||
@@ -170,6 +186,17 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.setting-team-panel__header {
|
||||
align-items: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user