import dayjs from 'dayjs'; const productStatusLabelMap = { active: '启用', paused: '暂停', archived: '归档', abandoned: '废弃' } as const satisfies Record; 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(); 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 & { 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 = { 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]; }