docs(api): 添加产品动态时间线前端API文档

- 新增产品动态时间线接口文档,明确前端调用规范
- 定义接口请求参数、响应结构和字段语义说明
- 提供请求示例和错误码说明
- 添加左侧筛选项映射规则和时间格式说明

feat(product): 实现产品首页动态时间线功能

- 重构产品首页布局结构,采用档案横幅型设计
- 新增对象基础概述横幅模块
- 实现产品动态时间线面板组件
- 集成需求池管理概览和最近变化区域
- 添加扩展信息区预留模块位

chore(docs): 更新代理工作说明和前端测试策略

- 添加前端任务测试策略说明
- 更新代理工作流程规范
- 明确git操作执行边界
- 优化组件类型声明更新
This commit is contained in:
2026-04-24 16:38:43 +08:00
parent 4122dfa50d
commit 5b9c7e781b
14 changed files with 3584 additions and 958 deletions

View 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];
}