feat: 实现产品/项目/版本三大模块完整功能

- 产品列表:拖拽排序持久化、编辑弹窗、删除校验(活跃版本需迁移)
- 项目列表:新建项目表单(产品选择+重名校验)、产品筛选、版本归属修复
- 版本列表:新建版本表单(迭代类型自动生成版本号)、状态/项目筛选、倒序排列
- 项目详情页:概览统计、人员墙(参与次数)、版本时间线(阶段流水线+进度条)
- 全局优化:筛选栏统一为header下方固定行、按钮颜色改为主题蓝、API探测300ms、store缓存
- 数据持久化到localStorage,支持离线开发

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-08 17:11:23 +08:00
parent e0120c470c
commit eca65f1bce
18 changed files with 2171 additions and 94 deletions

98
apps/web/lib/derive.ts Normal file
View File

@@ -0,0 +1,98 @@
import { VersionStatus } from './version-status';
import type { Stage, Role } from './stage';
interface VersionMember {
role: Role;
name: string;
}
interface RoleProgress {
role: Role;
percent: number;
daysSpent: number;
}
interface ProductOverviewLike {
id: string;
name: string;
projects: { id: string; name: string; description: string; createdAt: string }[];
versions: {
id: string; name: string; status?: string; releaseDate: string | null; createdAt: string;
currentStage?: Stage; startDate?: string | null; expectedReleaseDate?: string | null;
members?: VersionMember[]; progress?: RoleProgress[];
}[];
}
export interface ProjectWithContext {
id: string;
name: string;
description: string;
createdAt: string;
productId: string;
productName: string;
versions: VersionWithContext[];
}
export interface VersionWithContext {
id: string;
name: string;
status: VersionStatus;
releaseDate: string | null;
createdAt: string;
productId: string;
productName: string;
projectName: string;
currentStage?: Stage;
startDate?: string | null;
expectedReleaseDate?: string | null;
members?: VersionMember[];
progress?: RoleProgress[];
}
export function flattenProjects(overview: ProductOverviewLike[]): ProjectWithContext[] {
const result: ProjectWithContext[] = [];
for (const product of overview) {
for (const project of product.projects) {
const versions = product.versions
.filter((v) => v.name.toLowerCase().startsWith(project.name.toLowerCase()))
.map((v) => ({
...v,
status: (v.status || 'released') as VersionStatus,
productId: product.id,
productName: product.name,
projectName: project.name,
}));
result.push({
...project,
productId: product.id,
productName: product.name,
versions,
});
}
}
return result;
}
export function flattenVersions(overview: ProductOverviewLike[]): VersionWithContext[] {
const result: VersionWithContext[] = [];
for (const product of overview) {
for (const version of product.versions) {
const project = product.projects.find((p) =>
version.name.toLowerCase().startsWith(p.name.toLowerCase()),
);
result.push({
...version,
status: (version.status || 'released') as VersionStatus,
productId: product.id,
productName: product.name,
projectName: project?.name || '未关联',
});
}
}
return result;
}
export function getProjectDetail(overview: ProductOverviewLike[], projectId: string): ProjectWithContext | null {
const projects = flattenProjects(overview);
return projects.find((p) => p.id === projectId) || null;
}