Files
cn-rdms-web/src/views/product/dashboard/homepage.ts

382 lines
11 KiB
TypeScript
Raw Normal View History

import dayjs from 'dayjs';
const productStatusLabelMap = {
active: '启用',
paused: '暂停',
archived: '归档',
abandoned: '废弃'
} as const satisfies Record<Api.Product.ProductStatusCode, string>;
export interface ProductHomepageMetric {
label: string;
value: string;
hint: string;
}
export interface ProductHomepageFact {
label: string;
value: string;
}
export interface ProductHomepageBanner {
identity: {
name: string;
code: string;
directionCode: string;
statusCode: Api.Product.ProductStatusCode | null;
statusLabel: string;
managerLabel: string;
description: string;
facts: ProductHomepageFact[];
};
metrics: ProductHomepageMetric[];
}
export interface ProductHomepageBannerSource {
product: Api.Product.Product | null;
settings: Api.Product.ProductSettings | null;
members: readonly Api.Product.ProductMember[];
requirementSummary: ProductRequirementPoolSummary;
latestActivityTime?: string | null;
}
export interface ProductHomepageTimelineItem {
key: string;
tag: '对象' | '状态' | '团队';
title: string;
content: string;
time: string;
tone: 'sky' | 'emerald' | 'amber' | 'rose' | 'slate';
}
export interface ProductRequirementPoolSummary {
metrics: ProductHomepageMetric[];
distribution: Array<{
label: string;
value: string;
}>;
total: number;
todo: number;
highPriorityTodo: number;
completionRate: number;
}
export interface ProductRequirementPoolRecentChange {
id: string;
title: string;
actionType: Api.Product.ProductRequirementDashboardRecentChangeActionType;
actionLabel: string;
time: string;
content: string;
}
export interface ProductHomepageExtensionModule {
key: 'milestone' | 'risk' | 'document';
title: string;
description: string;
items: string[];
}
function normalizeCount(value: number | null | undefined) {
if (!Number.isFinite(value)) {
return 0;
}
return Math.max(0, Number(value));
}
function getTimeValue(value: string | null | undefined) {
const parsed = dayjs(value);
return parsed.isValid() ? parsed.valueOf() : 0;
}
function formatDateTime(value: string | null | undefined) {
const parsed = dayjs(value);
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : '--';
}
function getStatusLabel(status: Api.Product.ProductStatusCode | null | undefined) {
if (!status) {
return '--';
}
return productStatusLabelMap[status] || '--';
}
function getActiveMembers(members: readonly Api.Product.ProductMember[]) {
return members.filter(item => item.status === 0);
}
function getManagerLabel(settings: Api.Product.ProductSettings | null, members: readonly Api.Product.ProductMember[]) {
return (
settings?.baseInfo.managerUserNickname ||
getActiveMembers(members).find(item => item.managerFlag)?.userNickname ||
'--'
);
}
function getRoleSummary(members: readonly Api.Product.ProductMember[]) {
const activeMembers = getActiveMembers(members);
if (!activeMembers.length) {
return '--';
}
const roleCounter = new Map<string, number>();
activeMembers.forEach(member => {
const roleName = member.roleName || '未命名角色';
roleCounter.set(roleName, (roleCounter.get(roleName) || 0) + 1);
});
return Array.from(roleCounter.entries())
.sort((left, right) => {
const leftWeight = left[0].includes('经理') ? 0 : 1;
const rightWeight = right[0].includes('经理') ? 0 : 1;
if (leftWeight !== rightWeight) {
return leftWeight - rightWeight;
}
return left[0].localeCompare(right[0], 'zh-CN');
})
.map(([roleName, count]) => `${roleName} ${count}`)
.join(' / ');
}
function resolveLatestTimelineTime(
product: Api.Product.Product | null,
settings: Api.Product.ProductSettings | null,
members: readonly Api.Product.ProductMember[]
) {
const timeValues = [
product?.createTime,
product?.updateTime,
settings?.lifecycle.lastStatusReason ? product?.updateTime : null,
...members.flatMap(member => [member.joinedTime, member.leftTime || null])
];
const latestValue = timeValues.reduce((latest, current) => {
return Math.max(latest, getTimeValue(current));
}, 0);
return latestValue ? dayjs(latestValue).format('YYYY-MM-DD HH:mm') : '--';
}
export function buildRequirementPoolSummary(
source: Api.Product.ProductRequirementDashboardSummary | null | undefined
): ProductRequirementPoolSummary {
const total = normalizeCount(source?.total);
const todo = normalizeCount(source?.todo);
const pendingClaim = normalizeCount(source?.pendingClaim);
const pendingReview = normalizeCount(source?.pendingReview);
const pendingDispatch = normalizeCount(source?.pendingDispatch);
const completionRate = Math.min(100, normalizeCount(source?.completionRate));
const highPriorityTodo = normalizeCount(source?.highPriorityTodo);
const distribution = [
{ label: '等待处理', value: String(todo) },
{ label: '等待认领', value: String(pendingClaim) },
{ label: '等待评审', value: String(pendingReview) },
{ label: '等待指派', value: String(pendingDispatch) }
];
return {
metrics: [
{
label: '需求总量',
value: String(total),
hint: '当前需求池累计收录的需求数量'
},
{
label: '状态类型',
value: String(distribution.length),
hint: '首页当前重点展示的需求状态分层'
},
{
label: '待处理',
value: String(todo),
hint: '等待认领、评审、指派的需求,这些需求应该着重关注'
},
{
label: '高优先级待处理',
value: String(highPriorityTodo),
hint: '需要优先推进的待处理需求数量P0、P1类型的需求'
}
],
distribution,
total,
todo,
highPriorityTodo,
completionRate
};
}
export function buildRequirementPoolRecentChanges(
source: readonly Api.Product.ProductRequirementDashboardRecentChange[] | null | undefined
) {
return [...(source || [])]
.filter(item => getTimeValue(item.occurredAt) > 0)
.sort((left, right) => getTimeValue(right.occurredAt) - getTimeValue(left.occurredAt))
.map(item => ({
id: item.id,
title: item.title,
actionType: item.actionType,
actionLabel: item.actionLabel,
content: item.content,
time: formatDateTime(item.occurredAt)
})) satisfies ProductRequirementPoolRecentChange[];
}
export function buildProductHomepageTimeline(
product: Api.Product.Product | null,
settings: Api.Product.ProductSettings | null,
members: readonly Api.Product.ProductMember[]
) {
const items: Array<Omit<ProductHomepageTimelineItem, 'time'> & { time: string | null | undefined }> = [];
if (product?.createTime) {
items.push({
key: `product-create-${product.id}`,
tag: '对象',
title: '创建产品',
content: `产品 ${product.name || product.code} 已创建并进入产品管理域。`,
time: product.createTime,
tone: 'sky'
});
}
const statusReason =
settings?.lifecycle.lastStatusReason || settings?.baseInfo.lastStatusReason || product?.lastStatusReason;
if (product?.updateTime && settings?.lifecycle.statusCode && statusReason) {
const statusCode = settings.lifecycle.statusCode;
const toneMap: Record<Api.Product.ProductStatusCode, ProductHomepageTimelineItem['tone']> = {
active: 'emerald',
paused: 'amber',
archived: 'slate',
abandoned: 'rose'
};
items.push({
key: `product-status-${product.id}-${product.updateTime}`,
tag: '状态',
title: `状态调整为${getStatusLabel(statusCode)}`,
content: statusReason,
time: product.updateTime,
tone: toneMap[statusCode]
});
}
members.forEach(member => {
if (member.joinedTime) {
items.push({
key: `member-join-${member.id}`,
tag: '团队',
title: '成员加入',
content: `${member.userNickname}${member.roleName}身份加入当前产品。`,
time: member.joinedTime,
tone: member.managerFlag ? 'emerald' : 'sky'
});
}
if (member.status === 1 && member.leftTime) {
items.push({
key: `member-leave-${member.id}`,
tag: '团队',
title: '成员移出',
content: `${member.userNickname} 已退出当前产品团队。`,
time: member.leftTime,
tone: 'rose'
});
}
});
return items
.filter(item => getTimeValue(item.time) > 0)
.sort((left, right) => getTimeValue(right.time) - getTimeValue(left.time))
.slice(0, 8)
.map(item => ({
...item,
time: formatDateTime(item.time)
})) satisfies ProductHomepageTimelineItem[];
}
function buildProductHomepageBannerIdentity(source: ProductHomepageBannerSource) {
const { product, settings, members } = source;
const managerLabel = getManagerLabel(settings, members);
const baseInfo = settings?.baseInfo;
const statusCode = resolveProductHomepageStatusCode(product, settings);
return {
name: product?.name || baseInfo?.name || '--',
code: product?.code || baseInfo?.code || '--',
directionCode: product?.directionCode || baseInfo?.directionCode || '',
statusCode,
statusLabel: getStatusLabel(statusCode),
managerLabel,
description: resolveProductHomepageDescription(product, settings),
facts: [
{ label: '产品经理', value: managerLabel },
{ label: '角色摘要', value: getRoleSummary(members) }
]
} satisfies ProductHomepageBanner['identity'];
}
function resolveProductHomepageStatusCode(
product: Api.Product.Product | null,
settings: Api.Product.ProductSettings | null
) {
return settings?.lifecycle.statusCode || product?.statusCode || null;
}
function resolveProductHomepageDescription(
product: Api.Product.Product | null,
settings: Api.Product.ProductSettings | null
) {
return product?.description?.trim() || settings?.baseInfo.description?.trim() || '';
}
function buildProductHomepageBannerMetrics(source: ProductHomepageBannerSource) {
const activeMembers = getActiveMembers(source.members);
const fallbackLatestTimelineTime = resolveLatestTimelineTime(source.product, source.settings, source.members);
const latestTimelineTime = source.latestActivityTime?.trim() || fallbackLatestTimelineTime || '--';
const { requirementSummary } = source;
return [
{
label: '团队人数',
value: String(activeMembers.length),
hint: '当前处于有效状态的团队成员数'
},
{
label: '需求总量',
value: String(requirementSummary.total),
hint: '需求池累计收录的需求数量'
},
{
label: '待处理需求',
value: String(requirementSummary.todo),
hint: '需要进行认领、评审、指派的需求,这些需求应该着重关注'
},
{
label: '最近动态时间',
value: latestTimelineTime,
hint: '对象或团队最近一次可确认的变动时间'
}
] satisfies ProductHomepageMetric[];
}
export function buildProductHomepageBanner(source: ProductHomepageBannerSource): ProductHomepageBanner {
return {
identity: buildProductHomepageBannerIdentity(source),
metrics: buildProductHomepageBannerMetrics(source)
};
}
export function getProductHomepageExtensionModules(modules: readonly ProductHomepageExtensionModule[]) {
return [...modules];
}