refactor(projects): 优化产品项目新增逻辑
This commit is contained in:
@@ -33,6 +33,8 @@ interface ProductMemberResponse {
|
|||||||
roleId: string | number;
|
roleId: string | number;
|
||||||
roleName: string;
|
roleName: string;
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
|
/** 多角色合并展示的非主角色名列表 */
|
||||||
|
additionalRoleNames?: string[] | null;
|
||||||
managerFlag: boolean;
|
managerFlag: boolean;
|
||||||
status: 0 | 1;
|
status: 0 | 1;
|
||||||
joinedTime: string;
|
joinedTime: string;
|
||||||
@@ -74,6 +76,7 @@ export function normalizeProductMember(response: ProductMemberResponse): Api.Pro
|
|||||||
roleId: normalizeStringId(response.roleId),
|
roleId: normalizeStringId(response.roleId),
|
||||||
roleName: response.roleName || '',
|
roleName: response.roleName || '',
|
||||||
roleCode: response.roleCode || '',
|
roleCode: response.roleCode || '',
|
||||||
|
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||||
managerFlag: Boolean(response.managerFlag),
|
managerFlag: Boolean(response.managerFlag),
|
||||||
status: response.status,
|
status: response.status,
|
||||||
joinedTime: response.joinedTime,
|
joinedTime: response.joinedTime,
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export interface ProjectMemberResponse {
|
|||||||
roleId: string | number;
|
roleId: string | number;
|
||||||
roleName: string;
|
roleName: string;
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
|
/** 多角色合并展示的非主角色名列表 */
|
||||||
|
additionalRoleNames?: string[] | null;
|
||||||
managerFlag: boolean;
|
managerFlag: boolean;
|
||||||
status: 0 | 1;
|
status: 0 | 1;
|
||||||
joinedTime: string;
|
joinedTime: string;
|
||||||
@@ -225,6 +227,7 @@ export function normalizeProjectMember(response: ProjectMemberResponse): Api.Pro
|
|||||||
roleId: normalizeStringId(response.roleId),
|
roleId: normalizeStringId(response.roleId),
|
||||||
roleName: response.roleName || '',
|
roleName: response.roleName || '',
|
||||||
roleCode: response.roleCode || '',
|
roleCode: response.roleCode || '',
|
||||||
|
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||||
managerFlag: Boolean(response.managerFlag),
|
managerFlag: Boolean(response.managerFlag),
|
||||||
status: response.status,
|
status: response.status,
|
||||||
joinedTime: response.joinedTime,
|
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;
|
userNickname: string;
|
||||||
/** 角色 ID */
|
/** 角色 ID */
|
||||||
roleId: string;
|
roleId: string;
|
||||||
/** 角色名称 */
|
/** 角色名称(主角色) */
|
||||||
roleName: string;
|
roleName: string;
|
||||||
/** 角色编码 */
|
/** 角色编码(主角色) */
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
|
/**
|
||||||
|
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||||
|
* 单角色时为空数组 [];典型场景:创建者 + 经理重合时,主行 manager,creator 名进此列表
|
||||||
|
*/
|
||||||
|
additionalRoleNames: string[];
|
||||||
/** 是否当前产品经理 */
|
/** 是否当前产品经理 */
|
||||||
managerFlag: boolean;
|
managerFlag: boolean;
|
||||||
/** 成员状态 */
|
/** 成员状态 */
|
||||||
@@ -218,6 +223,8 @@ declare namespace Api {
|
|||||||
interface CreateProductWithTeamParams {
|
interface CreateProductWithTeamParams {
|
||||||
product: SaveProductParams;
|
product: SaveProductParams;
|
||||||
members: CreateProductMemberParams[];
|
members: CreateProductMemberParams[];
|
||||||
|
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||||
|
watcherUserIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProductMemberParams {
|
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;
|
userNickname: string;
|
||||||
/** 角色 ID */
|
/** 角色 ID */
|
||||||
roleId: string;
|
roleId: string;
|
||||||
/** 角色名称 */
|
/** 角色名称(主角色) */
|
||||||
roleName: string;
|
roleName: string;
|
||||||
/** 角色编码 */
|
/** 角色编码(主角色) */
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
|
/**
|
||||||
|
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||||
|
* 单角色时为空数组 [];典型场景:创建者 + 负责人重合时,主行 manager,creator 名进此列表
|
||||||
|
*/
|
||||||
|
additionalRoleNames: string[];
|
||||||
/** 是否项目负责人 */
|
/** 是否项目负责人 */
|
||||||
managerFlag: boolean;
|
managerFlag: boolean;
|
||||||
/** 成员状态 */
|
/** 成员状态 */
|
||||||
@@ -628,6 +633,8 @@ declare namespace Api {
|
|||||||
interface CreateProjectWithTeamParams {
|
interface CreateProjectWithTeamParams {
|
||||||
project: SaveProjectParams;
|
project: SaveProjectParams;
|
||||||
members: CreateProjectMemberParams[];
|
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']
|
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
||||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||||
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-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']
|
'IconFe:question': typeof import('~icons/fe/question')['default']
|
||||||
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
||||||
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
|
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): void;
|
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): void;
|
||||||
|
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
|||||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||||
const editingKey = ref<string | null>(null);
|
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])));
|
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;
|
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);
|
onMounted(loadRoles);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
|||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</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
|
<ProductCreateTeamMemberDialog
|
||||||
v-model:visible="memberDialogVisible"
|
v-model:visible="memberDialogVisible"
|
||||||
:mode="memberDialogMode"
|
:mode="memberDialogMode"
|
||||||
@@ -295,4 +343,27 @@ defineExpose({ validate: runValidate });
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ const currentStep = ref<1 | 2>(1);
|
|||||||
|
|
||||||
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
|
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
|
||||||
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
|
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
|
||||||
|
const draftWatcherUserIds = ref<string[]>([]);
|
||||||
|
|
||||||
function createBaseInfo(): ProductCreateBaseFormModel {
|
function createBaseInfo(): ProductCreateBaseFormModel {
|
||||||
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
|
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
|
||||||
@@ -157,7 +158,8 @@ async function handleCreateSubmit() {
|
|||||||
managerUserId: createBaseModel.value.managerUserId as string,
|
managerUserId: createBaseModel.value.managerUserId as string,
|
||||||
description: getNullableText(createBaseModel.value.description)
|
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);
|
const { error, data } = await fetchCreateProductWithTeam(payload);
|
||||||
@@ -186,6 +188,7 @@ watch(visible, async value => {
|
|||||||
editModel.value = createEditModel();
|
editModel.value = createEditModel();
|
||||||
createBaseModel.value = createBaseInfo();
|
createBaseModel.value = createBaseInfo();
|
||||||
draftMembers.value = [];
|
draftMembers.value = [];
|
||||||
|
draftWatcherUserIds.value = [];
|
||||||
await nextTick();
|
await nextTick();
|
||||||
editFormRef.value?.clearValidate();
|
editFormRef.value?.clearValidate();
|
||||||
return;
|
return;
|
||||||
@@ -330,6 +333,7 @@ watch(visible, async value => {
|
|||||||
:base-info="createBaseModel"
|
:base-info="createBaseModel"
|
||||||
:user-options="managerUserOptions"
|
:user-options="managerUserOptions"
|
||||||
@update:members="draftMembers = $event"
|
@update:members="draftMembers = $event"
|
||||||
|
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,23 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
|||||||
>
|
>
|
||||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
<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">
|
<ElTableColumn label="成员状态" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||||
@@ -180,6 +196,17 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
|||||||
gap: 12px;
|
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) {
|
@media (width <= 768px) {
|
||||||
.setting-team-panel__header {
|
.setting-team-panel__header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:members', members: Api.Project.CreateProjectMemberParams[]): void;
|
(e: 'update:members', members: Api.Project.CreateProjectMemberParams[]): void;
|
||||||
|
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
|||||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||||
const editingKey = ref<string | null>(null);
|
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])));
|
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;
|
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);
|
onMounted(loadRoles);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
|||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</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
|
<ProjectCreateTeamMemberDialog
|
||||||
v-model:visible="memberDialogVisible"
|
v-model:visible="memberDialogVisible"
|
||||||
:mode="memberDialogMode"
|
:mode="memberDialogMode"
|
||||||
@@ -295,4 +343,27 @@ defineExpose({ validate: runValidate });
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ const currentStep = ref<1 | 2>(1);
|
|||||||
|
|
||||||
const createBaseModel = ref<ProjectCreateBaseFormModel>(createBaseInfo());
|
const createBaseModel = ref<ProjectCreateBaseFormModel>(createBaseInfo());
|
||||||
const draftMembers = ref<Api.Project.CreateProjectMemberParams[]>([]);
|
const draftMembers = ref<Api.Project.CreateProjectMemberParams[]>([]);
|
||||||
|
const draftWatcherUserIds = ref<string[]>([]);
|
||||||
|
|
||||||
function createBaseInfo(): ProjectCreateBaseFormModel {
|
function createBaseInfo(): ProjectCreateBaseFormModel {
|
||||||
return {
|
return {
|
||||||
@@ -324,7 +325,8 @@ async function handleCreateSubmit() {
|
|||||||
plannedEndDate: createBaseModel.value.plannedEndDate,
|
plannedEndDate: createBaseModel.value.plannedEndDate,
|
||||||
projectDesc: getNullableText(createBaseModel.value.projectDesc)
|
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);
|
const { error, data } = await fetchCreateProjectWithTeam(payload);
|
||||||
@@ -353,6 +355,7 @@ watch(visible, async value => {
|
|||||||
editModel.value = createEditModel();
|
editModel.value = createEditModel();
|
||||||
createBaseModel.value = createBaseInfo();
|
createBaseModel.value = createBaseInfo();
|
||||||
draftMembers.value = [];
|
draftMembers.value = [];
|
||||||
|
draftWatcherUserIds.value = [];
|
||||||
await nextTick();
|
await nextTick();
|
||||||
editFormRef.value?.clearValidate();
|
editFormRef.value?.clearValidate();
|
||||||
return;
|
return;
|
||||||
@@ -556,6 +559,7 @@ watch(visible, async value => {
|
|||||||
:base-info="createBaseModel"
|
:base-info="createBaseModel"
|
||||||
:user-options="managerUserOptions"
|
:user-options="managerUserOptions"
|
||||||
@update:members="draftMembers = $event"
|
@update:members="draftMembers = $event"
|
||||||
|
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,7 +95,23 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
|||||||
>
|
>
|
||||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
<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">
|
<ElTableColumn label="成员状态" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||||
@@ -170,6 +186,17 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
|||||||
gap: 12px;
|
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) {
|
@media (width <= 768px) {
|
||||||
.setting-team-panel__header {
|
.setting-team-panel__header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
1144
关心人功能-API接口文档.html
Normal file
1144
关心人功能-API接口文档.html
Normal file
File diff suppressed because it is too large
Load Diff
754
成员列表接口变更-前端对接说明.html
Normal file
754
成员列表接口变更-前端对接说明.html
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>成员列表接口变更 — 前端对接说明</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--fg: #1f2328;
|
||||||
|
--fg-muted: #57606a;
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-soft: #f6f8fa;
|
||||||
|
--border: #d0d7de;
|
||||||
|
--border-soft: #e7ecf2;
|
||||||
|
--accent: #0969da;
|
||||||
|
--accent-soft: #ddf4ff;
|
||||||
|
--warn: #9a6700;
|
||||||
|
--warn-soft: #fff8c5;
|
||||||
|
--danger: #cf222e;
|
||||||
|
--danger-soft: #ffebe9;
|
||||||
|
--ok: #1a7f37;
|
||||||
|
--ok-soft: #dafbe1;
|
||||||
|
--code-bg: #f6f8fa;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB',
|
||||||
|
'Source Han Sans CN', 'Noto Sans CJK SC', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 32px 96px;
|
||||||
|
}
|
||||||
|
header.doc-header {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
header.doc-header h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
header.doc-header .meta {
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 40px 0 12px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 28px 0 8px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 20px 0 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
code,
|
||||||
|
pre {
|
||||||
|
font-family: 'JetBrains Mono', Menlo, Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
tr:nth-child(2n) td {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.callout.warn {
|
||||||
|
border-left-color: var(--warn);
|
||||||
|
background: var(--warn-soft);
|
||||||
|
}
|
||||||
|
.callout.danger {
|
||||||
|
border-left-color: var(--danger);
|
||||||
|
background: var(--danger-soft);
|
||||||
|
}
|
||||||
|
.callout.ok {
|
||||||
|
border-left-color: var(--ok);
|
||||||
|
background: var(--ok-soft);
|
||||||
|
}
|
||||||
|
.callout .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin: 14px 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.method {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.method.get {
|
||||||
|
background: #0969da;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.badge.new {
|
||||||
|
background: var(--ok-soft);
|
||||||
|
color: var(--ok);
|
||||||
|
border-color: #aceeb6;
|
||||||
|
}
|
||||||
|
.badge.keep {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.compare > div {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.compare .before {
|
||||||
|
border-top: 3px solid var(--warn);
|
||||||
|
}
|
||||||
|
.compare .after {
|
||||||
|
border-top: 3px solid var(--ok);
|
||||||
|
}
|
||||||
|
.compare h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin: 16px 0 24px;
|
||||||
|
}
|
||||||
|
.toc ol {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.toc a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.toc a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="doc-header">
|
||||||
|
<h1>成员列表接口变更 — 前端对接说明</h1>
|
||||||
|
<p class="meta">变更日期 2026-05-14 · 后端已就绪 · 前端需要配合调整渲染逻辑</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="callout">
|
||||||
|
<div class="title">一句话总结</div>
|
||||||
|
<strong>产品 / 项目成员列表接口</strong>
|
||||||
|
现在按"
|
||||||
|
<strong>一人一行</strong>
|
||||||
|
"返回,同一用户的多个角色合并到主角色行,其他角色名通过新增的
|
||||||
|
<code>additionalRoleNames: string[]</code>
|
||||||
|
字段返回。前端只需要把这个数组拼到角色名旁边显示即可。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toc">
|
||||||
|
<strong>目录</strong>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#sec-1">背景:为什么改</a></li>
|
||||||
|
<li><a href="#sec-2">受影响的接口(仅 2 个)</a></li>
|
||||||
|
<li><a href="#sec-3">改动前后对比</a></li>
|
||||||
|
<li><a href="#sec-4">新增字段 additionalRoleNames</a></li>
|
||||||
|
<li><a href="#sec-5">前端渲染建议</a></li>
|
||||||
|
<li><a href="#sec-6">边界场景</a></li>
|
||||||
|
<li><a href="#sec-7">不受影响的接口</a></li>
|
||||||
|
<li><a href="#sec-8">前端落地 checklist</a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== 1 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-1">1. 背景:为什么改</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
多角色改造后,
|
||||||
|
<strong>
|
||||||
|
产品/项目的"创建者"角色会自动落一条
|
||||||
|
<code>rdms_user_object_role</code>
|
||||||
|
行
|
||||||
|
</strong>
|
||||||
|
。当
|
||||||
|
<strong>创建者本人就是负责人</strong>
|
||||||
|
时,同一用户在同一对象内会出现两条 ACTIVE 记录:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
一条
|
||||||
|
<code>product_manager</code>
|
||||||
|
/
|
||||||
|
<code>project_manager</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
一条
|
||||||
|
<code>product_creator</code>
|
||||||
|
/
|
||||||
|
<code>project_creator</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
如果后端原样返回,前端列表会出现"同一个人重复两行"。讨论后约定:后端在
|
||||||
|
<strong>列表接口</strong>
|
||||||
|
层做合并展示,
|
||||||
|
<strong>不影响</strong>
|
||||||
|
底层数据(每个角色行仍独立存在,便于后续可能的"按角色单独操作")。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="callout warn">
|
||||||
|
<div class="title">业务边界(重要)</div>
|
||||||
|
当前业务上
|
||||||
|
<strong>
|
||||||
|
只有
|
||||||
|
<code>creator + manager</code>
|
||||||
|
这一种组合
|
||||||
|
</strong>
|
||||||
|
会让同人多角色出现。其他角色(产品专员、开发等)仍是一人一角色。所以前端可以放心按"主角色 + 附加角色名"
|
||||||
|
的方式渲染,附加列表通常是 0 或 1 个元素。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== 2 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-2">2. 受影响的接口(仅 2 个)</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<code>/admin-api/project/product/{productId}/members</code>
|
||||||
|
<p style="color: var(--fg-muted); font-size: 13px; margin: 6px 0 0">产品团队成员列表</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<code>/admin-api/project/project/{projectId}/members</code>
|
||||||
|
<p style="color: var(--fg-muted); font-size: 13px; margin: 6px 0 0">项目团队成员列表</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
响应结构两个接口对称,本文档下文统一用
|
||||||
|
<code>RespVO</code>
|
||||||
|
表示,字段路径一致。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ====== 3 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-3">3. 改动前后对比</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
假设:产品 P 里
|
||||||
|
<code>用户A</code>
|
||||||
|
是产品经理(同时创建了该产品,所以也有
|
||||||
|
<code>product_creator</code>
|
||||||
|
角色),
|
||||||
|
<code>用户B</code>
|
||||||
|
是产品专员,
|
||||||
|
<code>用户C</code>
|
||||||
|
是已退场的历史成员。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="compare">
|
||||||
|
<div class="before">
|
||||||
|
<h4>改之前(数据原样返回,4 行)</h4>
|
||||||
|
<pre><code>[
|
||||||
|
{ id: "100", userId: "A", roleCode: "product_manager",
|
||||||
|
roleName: "产品经理", status: 0, ... },
|
||||||
|
{ id: "101", userId: "A", roleCode: "product_creator",
|
||||||
|
roleName: "产品创建者", status: 0, ... },
|
||||||
|
{ id: "102", userId: "B", roleCode: "product_specialist",
|
||||||
|
roleName: "产品专员", status: 0, ... },
|
||||||
|
{ id: "103", userId: "C", roleCode: "product_specialist",
|
||||||
|
roleName: "产品专员", status: 1, leftTime: "..." }
|
||||||
|
]</code></pre>
|
||||||
|
<p style="color: var(--fg-muted); font-size: 12px">用户 A 出现 2 次 — 前端要么重复显示,要么自己去重。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="after">
|
||||||
|
<h4>改之后(合并后,3 行)</h4>
|
||||||
|
<pre><code>[
|
||||||
|
{ id: "100", userId: "A", roleCode: "product_manager",
|
||||||
|
roleName: "产品经理",
|
||||||
|
additionalRoleNames: ["产品创建者"], // ← 新增字段
|
||||||
|
status: 0, ... },
|
||||||
|
{ id: "102", userId: "B", roleCode: "product_specialist",
|
||||||
|
roleName: "产品专员",
|
||||||
|
additionalRoleNames: [],
|
||||||
|
status: 0, ... },
|
||||||
|
{ id: "103", userId: "C", roleCode: "product_specialist",
|
||||||
|
roleName: "产品专员",
|
||||||
|
additionalRoleNames: [],
|
||||||
|
status: 1, leftTime: "..." }
|
||||||
|
]</code></pre>
|
||||||
|
<p style="color: var(--fg-muted); font-size: 12px">
|
||||||
|
用户 A 1 行 — 主行 manager,creator 名字进
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>合并规则</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>规则</th>
|
||||||
|
<th>说明</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>仅合并 ACTIVE 行</td>
|
||||||
|
<td>
|
||||||
|
<code>status = 0</code>
|
||||||
|
的多角色行才聚合;
|
||||||
|
<code>status = 1</code>
|
||||||
|
的历史 INACTIVE 行
|
||||||
|
<strong>保持每条独立成行</strong>
|
||||||
|
(历史角色留痕,便于审计)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>主角色选择</td>
|
||||||
|
<td>
|
||||||
|
同人多角色时,
|
||||||
|
<code>product_manager</code>
|
||||||
|
/
|
||||||
|
<code>project_manager</code>
|
||||||
|
角色行优先做主;不在则按
|
||||||
|
<code>roleId</code>
|
||||||
|
升序选第一条(理论不该走到这条兜底)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>主行字段</td>
|
||||||
|
<td>
|
||||||
|
<code>id</code>
|
||||||
|
/
|
||||||
|
<code>roleId</code>
|
||||||
|
/
|
||||||
|
<code>roleCode</code>
|
||||||
|
/
|
||||||
|
<code>roleName</code>
|
||||||
|
/
|
||||||
|
<code>joinedTime</code>
|
||||||
|
/
|
||||||
|
<code>leftTime</code>
|
||||||
|
/
|
||||||
|
<code>remark</code>
|
||||||
|
等都按主角色行的值返回
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>非主角色名顺序</td>
|
||||||
|
<td>
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
按角色中文名字典序升序,前端可以直接顺序渲染
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ====== 4 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-4">
|
||||||
|
4. 新增字段
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>字段名</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>说明</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>additionalRoleNames</code></td>
|
||||||
|
<td><code>string[]</code></td>
|
||||||
|
<td><span class="badge new">本次新增</span></td>
|
||||||
|
<td>
|
||||||
|
非主角色的中文名列表,多角色场景使用;
|
||||||
|
<strong>
|
||||||
|
单角色时为空数组
|
||||||
|
<code>[]</code>
|
||||||
|
</strong>
|
||||||
|
,前端可以放心
|
||||||
|
<code>length</code>
|
||||||
|
判空
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
所有
|
||||||
|
<strong>原有字段保持不变</strong>
|
||||||
|
(
|
||||||
|
<code>id</code>
|
||||||
|
、
|
||||||
|
<code>userId</code>
|
||||||
|
、
|
||||||
|
<code>userNickname</code>
|
||||||
|
、
|
||||||
|
<code>roleId</code>
|
||||||
|
、
|
||||||
|
<code>roleName</code>
|
||||||
|
、
|
||||||
|
<code>roleCode</code>
|
||||||
|
、
|
||||||
|
<code>managerFlag</code>
|
||||||
|
、
|
||||||
|
<code>status</code>
|
||||||
|
、
|
||||||
|
<code>joinedTime</code>
|
||||||
|
、
|
||||||
|
<code>leftTime</code>
|
||||||
|
、
|
||||||
|
<code>remark</code>
|
||||||
|
),前端原有代码不会因为字段消失而报错。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ====== 5 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-5">5. 前端渲染建议</h2>
|
||||||
|
|
||||||
|
<h3>5.1 最简单的展示方式 — 拼成一个字符串</h3>
|
||||||
|
|
||||||
|
<pre><code>const displayRoleName = additionalRoleNames?.length
|
||||||
|
? `${roleName} + ${additionalRoleNames.join(', ')}`
|
||||||
|
: roleName;
|
||||||
|
//
|
||||||
|
// 例:roleName="产品经理", additionalRoleNames=["产品创建者"]
|
||||||
|
// → "产品经理 + 产品创建者"
|
||||||
|
//
|
||||||
|
// 例:roleName="产品专员", additionalRoleNames=[]
|
||||||
|
// → "产品专员"</code></pre>
|
||||||
|
|
||||||
|
<h3>5.2 更友好的展示 — 主角色 + 浅色 chip 标签</h3>
|
||||||
|
|
||||||
|
<pre><code><span class="role-main">{{ roleName }}</span>
|
||||||
|
<span v-for="extra in additionalRoleNames" :key="extra" class="role-tag">
|
||||||
|
{{ extra }}
|
||||||
|
</span>
|
||||||
|
//
|
||||||
|
// CSS:role-tag 设计成浅色 background + 小圆角,跟主角色拉开视觉层级</code></pre>
|
||||||
|
|
||||||
|
<h3>5.3 表格单元格示意</h3>
|
||||||
|
|
||||||
|
<pre><code>| 用户 | 角色 | 状态 |
|
||||||
|
|-----------|---------------------------|------|
|
||||||
|
| 灿能管理 | 产品经理 [产品创建者] | 有效 |
|
||||||
|
| 洪圣文 | 产品专员 | 有效 |
|
||||||
|
| 李凡 | 游客 | 历史 |</code></pre>
|
||||||
|
|
||||||
|
<!-- ====== 6 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-6">6. 边界场景</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>场景</th>
|
||||||
|
<th>预期返回</th>
|
||||||
|
<th>前端展示</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>用户 A 仅是产品经理(不是创建者)</td>
|
||||||
|
<td>
|
||||||
|
1 行,
|
||||||
|
<code>additionalRoleNames=[]</code>
|
||||||
|
</td>
|
||||||
|
<td>"产品经理"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>用户 A 同时是产品经理 + 创建者</td>
|
||||||
|
<td>
|
||||||
|
1 行(主行 manager),
|
||||||
|
<code>additionalRoleNames=["产品创建者"]</code>
|
||||||
|
</td>
|
||||||
|
<td>"产品经理 + 产品创建者"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>用户 A 仅是创建者(罕见,理论上创建后立即把 manager 转给别人才会出现)</td>
|
||||||
|
<td>
|
||||||
|
1 行,主行就是 creator,
|
||||||
|
<code>additionalRoleNames=[]</code>
|
||||||
|
</td>
|
||||||
|
<td>"产品创建者"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>用户 A 是产品专员(单角色非 manager)</td>
|
||||||
|
<td>
|
||||||
|
1 行,
|
||||||
|
<code>additionalRoleNames=[]</code>
|
||||||
|
</td>
|
||||||
|
<td>"产品专员"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>用户 A 退场(status=1 INACTIVE 历史行)</td>
|
||||||
|
<td>
|
||||||
|
每条 INACTIVE 行独立 1 行,
|
||||||
|
<code>additionalRoleNames=[]</code>
|
||||||
|
</td>
|
||||||
|
<td>"产品专员(已退场)"等灰显</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
用户 A 同时有
|
||||||
|
<strong>历史失效</strong>
|
||||||
|
的角色行 +
|
||||||
|
<strong>当前生效</strong>
|
||||||
|
的角色行
|
||||||
|
</td>
|
||||||
|
<td>ACTIVE 行合并 1 行 + 每条 INACTIVE 行各占 1 行;同用户在列表里会出现多次(不同 status)</td>
|
||||||
|
<td>分别用"有效 / 历史"区分;不要再二次合并</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ====== 7 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-7">7. 不受影响的接口</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
以下接口
|
||||||
|
<strong>不变</strong>
|
||||||
|
,前端原有调用方式保持:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>接口</th>
|
||||||
|
<th>说明</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>POST /admin-api/project/product/{productId}/members</code>
|
||||||
|
<br />
|
||||||
|
<code>POST /admin-api/project/project/{projectId}/members</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
新增成员 — 仍按"一个角色一条记录"操作,传
|
||||||
|
<code>userId + roleId</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>PUT /admin-api/project/.../members/{memberId}</code></td>
|
||||||
|
<td>
|
||||||
|
更新成员 — 按
|
||||||
|
<code>memberId</code>
|
||||||
|
(即
|
||||||
|
<code>rdms_user_object_role.id</code>
|
||||||
|
)定位具体角色行操作
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>PUT /admin-api/project/.../members/{memberId}/inactive</code></td>
|
||||||
|
<td>
|
||||||
|
失效成员 — 同上,按
|
||||||
|
<code>memberId</code>
|
||||||
|
操作
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>GET /admin-api/project/product/{productId}/context</code>
|
||||||
|
<br />
|
||||||
|
<code>GET /admin-api/project/project/{projectId}/context</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
对象上下文 —
|
||||||
|
<code>currentRole.additionalRoleNames</code>
|
||||||
|
字段早已存在,
|
||||||
|
<strong>不在本次变更范围</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="callout warn">
|
||||||
|
<div class="title">特别提醒 — 编辑 / 失效操作</div>
|
||||||
|
当用户 A 同时是 manager + creator(合并展示成 1 行)时,前端如果允许"编辑这一行的角色"或"踢出",传的
|
||||||
|
<code>memberId</code>
|
||||||
|
是
|
||||||
|
<strong>
|
||||||
|
主角色行的
|
||||||
|
<code>id</code>
|
||||||
|
</strong>
|
||||||
|
(也就是 manager 那条)。
|
||||||
|
<strong>不会影响</strong>
|
||||||
|
同人的 creator 角色行 — creator 角色仍保留。
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
这是符合设计的行为:creator 是"留痕"角色,原则上不应该被踢出。如果业务上要踢出整个人,需要前端先调一次 list
|
||||||
|
接口拿到该用户的所有 active 角色行(注意 — 合并后的 list 里看不到 creator 那条的
|
||||||
|
<code>id</code>
|
||||||
|
),后续
|
||||||
|
<strong>是否要单独提供"列出某 user 所有角色行"接口</strong>
|
||||||
|
取决于业务实际诉求,目前没有这个接口。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== 8 ====== -->
|
||||||
|
|
||||||
|
<h2 id="sec-8">8. 前端落地 checklist</h2>
|
||||||
|
|
||||||
|
<div class="callout ok">
|
||||||
|
<ul style="margin: 6px 0">
|
||||||
|
<li>
|
||||||
|
✅ 把
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
数组加到 TypeScript 类型定义(ProductMemberRespVO / ProjectMemberRespVO 两处)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ 列表渲染逻辑:把
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
拼到
|
||||||
|
<code>roleName</code>
|
||||||
|
旁边显示(字符串或 chip 标签均可)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ 验证:找一个"创建者 = 经理"的产品/项目,确认列表里这个人只出现
|
||||||
|
<strong>1 行</strong>
|
||||||
|
,且能看到"产品经理 + 产品创建者"字样
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ 验证:找一个普通成员(产品专员等),
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
应该是
|
||||||
|
<code>[]</code>
|
||||||
|
,不影响展示
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ 验证:历史退场成员(
|
||||||
|
<code>status=1</code>
|
||||||
|
),仍按原方式各占一行
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
⏸ 编辑/失效操作 — 当前不动,仍按
|
||||||
|
<code>memberId</code>
|
||||||
|
操作主角色行;若业务需要"踢人整体",后端再补接口
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>变更影响最小范围说明</h3>
|
||||||
|
|
||||||
|
<p style="color: var(--fg-muted); font-size: 13px">
|
||||||
|
本次后端改动仅在
|
||||||
|
<code>ProductMemberServiceImpl.getProductMemberList</code>
|
||||||
|
和
|
||||||
|
<code>ProjectMemberServiceImpl.getProjectMemberList</code>
|
||||||
|
两个方法中实现,
|
||||||
|
<strong>不修改底层数据</strong>
|
||||||
|
(
|
||||||
|
<code>rdms_user_object_role</code>
|
||||||
|
仍按一行一角色存储)。即使前端暂时不读
|
||||||
|
<code>additionalRoleNames</code>
|
||||||
|
字段,也只是看不到"+ 产品创建者"字样,不会出现数据错误或重复行问题。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user