216 lines
6.5 KiB
TypeScript
216 lines
6.5 KiB
TypeScript
|
|
import dayjs from 'dayjs';
|
||
|
|
|
||
|
|
export interface ProductManagerMemberLike {
|
||
|
|
roleId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ProductTeamManageContext {
|
||
|
|
buttonCodes: readonly string[];
|
||
|
|
loginUserId: string | null | undefined;
|
||
|
|
currentManagerUserId: string | null | undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ProductLifecycleStatusSummary {
|
||
|
|
tone: 'emerald' | 'amber' | 'slate' | 'rose';
|
||
|
|
caption: string;
|
||
|
|
description: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ProductLifecycleActionCardMeta {
|
||
|
|
tone: 'emerald' | 'amber' | 'slate' | 'rose';
|
||
|
|
description: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const productSettingSectionKeys = ['base-info', 'team', 'lifecycle', 'danger'] as const;
|
||
|
|
|
||
|
|
export type ProductSettingSectionKey = (typeof productSettingSectionKeys)[number];
|
||
|
|
|
||
|
|
const productSettingSectionAuthCodeMap: Partial<Record<ProductSettingSectionKey, string>> = {
|
||
|
|
lifecycle: 'project:product:status',
|
||
|
|
danger: 'project:product:delete'
|
||
|
|
};
|
||
|
|
|
||
|
|
const productBaseInfoReadonlyMessageMap: Partial<Record<Api.Product.ProductStatusCode, string>> = {
|
||
|
|
paused: '当前产品已暂停,基础信息仅支持查看,不可编辑。',
|
||
|
|
archived: '当前产品已归档,基础信息仅支持查看,不可编辑。',
|
||
|
|
abandoned: '当前产品已废弃,基础信息仅支持查看,不可编辑。'
|
||
|
|
};
|
||
|
|
|
||
|
|
const productLifecycleStatusSummaryMap: Record<Api.Product.ProductStatusCode, ProductLifecycleStatusSummary> = {
|
||
|
|
active: {
|
||
|
|
tone: 'emerald',
|
||
|
|
caption: '产品正常服务中',
|
||
|
|
description: '当前可以执行暂停、归档或废弃。'
|
||
|
|
},
|
||
|
|
paused: {
|
||
|
|
tone: 'amber',
|
||
|
|
caption: '产品已暂停推进',
|
||
|
|
description: '条件恢复后可重新启用,也可继续归档或废弃。'
|
||
|
|
},
|
||
|
|
archived: {
|
||
|
|
tone: 'slate',
|
||
|
|
caption: '产品已收口归档',
|
||
|
|
description: '保留历史信息,当前不再开放新的生命周期动作。'
|
||
|
|
},
|
||
|
|
abandoned: {
|
||
|
|
tone: 'rose',
|
||
|
|
caption: '产品已停止建设',
|
||
|
|
description: '产品已结束推进,当前不再开放新的生命周期动作。'
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const productLifecycleActionCardMetaMap: Record<Api.Product.ProductStatusActionCode, ProductLifecycleActionCardMeta> = {
|
||
|
|
pause: {
|
||
|
|
tone: 'amber',
|
||
|
|
description: '暂停当前产品,后续仍可恢复或归档。'
|
||
|
|
},
|
||
|
|
resume: {
|
||
|
|
tone: 'emerald',
|
||
|
|
description: '恢复启用后,继续推进产品协作。'
|
||
|
|
},
|
||
|
|
archive: {
|
||
|
|
tone: 'slate',
|
||
|
|
description: '收口当前产品,保留历史记录并结束维护。'
|
||
|
|
},
|
||
|
|
abandon: {
|
||
|
|
tone: 'rose',
|
||
|
|
description: '终止当前产品建设,请谨慎确认。'
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const productTeamTableHeaderHeight = 40;
|
||
|
|
const productTeamTableRowHeight = 40;
|
||
|
|
|
||
|
|
export function shouldRequireManagerHandover(
|
||
|
|
targetRoleId: string,
|
||
|
|
currentManager: ProductManagerMemberLike | null | undefined
|
||
|
|
) {
|
||
|
|
if (!currentManager?.roleId) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return targetRoleId === currentManager.roleId;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getPreviousManagerRoleOptions(roleOptions: Api.SystemManage.RoleSimple[], managerRoleId: string) {
|
||
|
|
return roleOptions.filter(role => role.id !== managerRoleId);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getProductSettingSectionKeys() {
|
||
|
|
return [...productSettingSectionKeys];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isProductBaseInfoEditable(status: Api.Product.ProductStatusCode | null | undefined) {
|
||
|
|
return status === 'active';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveVisibleProductSettingSections(
|
||
|
|
sectionKeys: readonly ProductSettingSectionKey[],
|
||
|
|
buttonCodes: readonly string[]
|
||
|
|
) {
|
||
|
|
return sectionKeys.filter(sectionKey => {
|
||
|
|
const authCode = productSettingSectionAuthCodeMap[sectionKey];
|
||
|
|
|
||
|
|
if (!authCode) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return buttonCodes.includes(authCode);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveVisibleProductSettingSectionKey(
|
||
|
|
currentKey: ProductSettingSectionKey | string | null | undefined,
|
||
|
|
visibleSectionKeys: readonly ProductSettingSectionKey[]
|
||
|
|
) {
|
||
|
|
if (!visibleSectionKeys.length) {
|
||
|
|
return 'base-info' satisfies ProductSettingSectionKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentKey && visibleSectionKeys.includes(currentKey as ProductSettingSectionKey)) {
|
||
|
|
return currentKey as ProductSettingSectionKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
return visibleSectionKeys[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getProductBaseInfoReadonlyMessage(status: Api.Product.ProductStatusCode | null | undefined) {
|
||
|
|
if (!status || isProductBaseInfoEditable(status)) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
return productBaseInfoReadonlyMessageMap[status] || '当前产品状态不允许编辑基础信息。';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getProductLifecycleStatusSummary(status: Api.Product.ProductStatusCode) {
|
||
|
|
return productLifecycleStatusSummaryMap[status];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatProductMemberDate(value: string | number | null | undefined) {
|
||
|
|
if (value === null || value === undefined || value === '') {
|
||
|
|
return '--';
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizedValue = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value;
|
||
|
|
const parsedDate = dayjs(normalizedValue);
|
||
|
|
|
||
|
|
if (!parsedDate.isValid()) {
|
||
|
|
return String(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsedDate.format('YYYY-MM-DD');
|
||
|
|
}
|
||
|
|
|
||
|
|
export function filterProductMembersByKeyword(
|
||
|
|
members: readonly Api.Product.ProductMember[],
|
||
|
|
keyword: string | null | undefined
|
||
|
|
) {
|
||
|
|
return filterProductMembers(members, { keyword });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function filterProductMembers(
|
||
|
|
members: readonly Api.Product.ProductMember[],
|
||
|
|
filters: {
|
||
|
|
keyword?: string | null | undefined;
|
||
|
|
roleId?: string | null | undefined;
|
||
|
|
}
|
||
|
|
) {
|
||
|
|
const normalizedKeyword = String(filters.keyword || '')
|
||
|
|
.trim()
|
||
|
|
.toLocaleLowerCase();
|
||
|
|
const normalizedRoleId = String(filters.roleId || '').trim();
|
||
|
|
|
||
|
|
if (!normalizedKeyword && !normalizedRoleId) {
|
||
|
|
return [...members];
|
||
|
|
}
|
||
|
|
|
||
|
|
return members.filter(member => {
|
||
|
|
const matchesKeyword = !normalizedKeyword || member.userNickname.toLocaleLowerCase().includes(normalizedKeyword);
|
||
|
|
const matchesRole = !normalizedRoleId || member.roleId === normalizedRoleId;
|
||
|
|
|
||
|
|
return matchesKeyword && matchesRole;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getProductTeamTableHeight(visibleRows: number) {
|
||
|
|
const normalizedRows = Math.max(0, visibleRows);
|
||
|
|
|
||
|
|
return productTeamTableHeaderHeight + normalizedRows * productTeamTableRowHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getProductLifecycleActionCardMeta(actionCode: Api.Product.ProductStatusActionCode) {
|
||
|
|
return productLifecycleActionCardMetaMap[actionCode];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function canManageProductTeam(context: ProductTeamManageContext) {
|
||
|
|
const hasUpdateAuth = context.buttonCodes.includes('project:product:update');
|
||
|
|
const loginUserId = String(context.loginUserId || '');
|
||
|
|
const currentManagerUserId = String(context.currentManagerUserId || '');
|
||
|
|
|
||
|
|
if (!hasUpdateAuth || !loginUserId || !currentManagerUserId) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return loginUserId === currentManagerUserId;
|
||
|
|
}
|