docs(api): 添加产品动态时间线前端API文档
- 新增产品动态时间线接口文档,明确前端调用规范 - 定义接口请求参数、响应结构和字段语义说明 - 提供请求示例和错误码说明 - 添加左侧筛选项映射规则和时间格式说明 feat(product): 实现产品首页动态时间线功能 - 重构产品首页布局结构,采用档案横幅型设计 - 新增对象基础概述横幅模块 - 实现产品动态时间线面板组件 - 集成需求池管理概览和最近变化区域 - 添加扩展信息区预留模块位 chore(docs): 更新代理工作说明和前端测试策略 - 添加前端任务测试策略说明 - 更新代理工作流程规范 - 明确git操作执行边界 - 优化组件类型声明更新
This commit is contained in:
390
src/views/product/dashboard/homepage.ts
Normal file
390
src/views/product/dashboard/homepage.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
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 ProductRequirementPoolSummarySource {
|
||||
total: number;
|
||||
todo: number;
|
||||
analyzing: number;
|
||||
planned: number;
|
||||
done: number;
|
||||
highPriorityTodo: number;
|
||||
}
|
||||
|
||||
export interface ProductRequirementPoolSummary {
|
||||
metrics: ProductHomepageMetric[];
|
||||
distribution: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
total: number;
|
||||
todo: number;
|
||||
highPriorityTodo: number;
|
||||
}
|
||||
|
||||
export interface ProductRequirementPoolRecentChangeSource {
|
||||
id: string;
|
||||
title: string;
|
||||
actionLabel: string;
|
||||
time: string;
|
||||
statusLabel: string;
|
||||
}
|
||||
|
||||
export interface ProductRequirementPoolRecentChange {
|
||||
id: string;
|
||||
title: string;
|
||||
actionLabel: string;
|
||||
time: string;
|
||||
statusLabel: 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: ProductRequirementPoolSummarySource | null | undefined
|
||||
): ProductRequirementPoolSummary {
|
||||
const total = normalizeCount(source?.total);
|
||||
const todo = normalizeCount(source?.todo);
|
||||
const analyzing = normalizeCount(source?.analyzing);
|
||||
const planned = normalizeCount(source?.planned);
|
||||
const done = normalizeCount(source?.done);
|
||||
const highPriorityTodo = normalizeCount(source?.highPriorityTodo);
|
||||
const distribution = [
|
||||
{ label: '待处理', value: String(todo) },
|
||||
{ label: '分析中', value: String(analyzing) },
|
||||
{ label: '已规划', value: String(planned) },
|
||||
{ label: '已完成', value: String(done) }
|
||||
];
|
||||
|
||||
return {
|
||||
metrics: [
|
||||
{
|
||||
label: '需求总量',
|
||||
value: String(total),
|
||||
hint: '当前需求池累计收录的需求数量'
|
||||
},
|
||||
{
|
||||
label: '状态类型',
|
||||
value: String(distribution.length),
|
||||
hint: '首页当前重点展示的需求状态分层'
|
||||
},
|
||||
{
|
||||
label: '待处理',
|
||||
value: String(todo),
|
||||
hint: '等待进入分析或分派的需求数量'
|
||||
},
|
||||
{
|
||||
label: '高优先级待处理',
|
||||
value: String(highPriorityTodo),
|
||||
hint: '需要优先推进的待处理需求数量'
|
||||
}
|
||||
],
|
||||
distribution,
|
||||
total,
|
||||
todo,
|
||||
highPriorityTodo
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRequirementPoolRecentChanges(
|
||||
source: readonly ProductRequirementPoolRecentChangeSource[] | null | undefined
|
||||
) {
|
||||
return [...(source || [])]
|
||||
.filter(item => getTimeValue(item.time) > 0)
|
||||
.sort((left, right) => getTimeValue(right.time) - getTimeValue(left.time))
|
||||
.map(item => ({
|
||||
...item,
|
||||
time: formatDateTime(item.time)
|
||||
})) 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];
|
||||
}
|
||||
Reference in New Issue
Block a user