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

View File

@@ -3,25 +3,116 @@
import { create } from 'zustand';
import { Product } from '@ftb/shared';
import { api } from '@/lib/api';
import type { Stage, Role } from '@/lib/stage';
interface ProductWithCount extends Product {
_count?: { requirements: number; projects: number };
interface VersionMember {
role: Role;
name: string;
}
interface RoleProgress {
role: Role;
percent: number;
daysSpent: number;
}
interface VersionItem {
id: string;
name: string;
status?: string;
releaseDate: string | null;
createdAt: string;
currentStage?: Stage;
startDate?: string | null;
expectedReleaseDate?: string | null;
members?: VersionMember[];
progress?: RoleProgress[];
}
const MOCK_OVERVIEW: ProductOverview[] = [
{
id: 'mock-1',
name: '翻台宝',
description: '餐饮 SaaS 主产品,覆盖门店运营管理全流程',
createdAt: '2024-01-01',
updatedAt: '2024-06-01',
projects: [
{ id: 'p1', name: '考勤', description: '员工打卡、排班管理', createdAt: '2024-01-10' },
{ id: 'p2', name: '电子合同', description: '入职、续签、离职合同电子化', createdAt: '2024-02-15' },
{ id: 'p3', name: '值班', description: '门店值班排班与交接', createdAt: '2024-03-01' },
],
versions: [
{ id: 'v1', name: '考勤V1.0', status: 'released', releaseDate: '2024-02-01', createdAt: '2024-01-20', currentStage: 'released', startDate: '2024-01-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 5 }, { role: 'backend', percent: 100, daysSpent: 4 }, { role: 'testing', percent: 100, daysSpent: 2 }] },
{ id: 'v2', name: '考勤V1.2', status: 'developing', releaseDate: null, createdAt: '2024-04-15', currentStage: 'dev', startDate: '2024-04-15', expectedReleaseDate: '2024-06-10', members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 5 }, { role: 'ui', percent: 100, daysSpent: 7 }, { role: 'frontend', percent: 60, daysSpent: 10 }, { role: 'backend', percent: 40, daysSpent: 12 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
{ id: 'v3', name: '考勤V1.3', status: 'planned', releaseDate: null, createdAt: '2024-06-01' },
{ id: 'v4', name: '电子合同V1.0', status: 'released', releaseDate: '2024-03-20', createdAt: '2024-03-01', currentStage: 'released', startDate: '2024-03-01', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'backend', percent: 100, daysSpent: 6 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
{ id: 'v5', name: '电子合同V2.0', status: 'released', releaseDate: '2024-06-01', createdAt: '2024-05-20', currentStage: 'released', startDate: '2024-05-01', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 5 }, { role: 'ui', percent: 100, daysSpent: 6 }, { role: 'frontend', percent: 100, daysSpent: 12 }, { role: 'backend', percent: 100, daysSpent: 10 }, { role: 'testing', percent: 100, daysSpent: 4 }] },
{ id: 'v6', name: '电子合同V2.1', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'ui_design', startDate: '2024-06-05', expectedReleaseDate: '2024-07-15', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 50, daysSpent: 4 }, { role: 'frontend', percent: 0, daysSpent: 0 }, { role: 'backend', percent: 0, daysSpent: 0 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
{ id: 'v7', name: '值班V1.0', status: 'released', releaseDate: '2024-04-01', createdAt: '2024-03-15', currentStage: 'released', startDate: '2024-03-15', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
{ id: 'v8', name: '值班V1.1', status: 'released', releaseDate: '2024-05-15', createdAt: '2024-05-01', currentStage: 'released', startDate: '2024-05-01', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 2 }, { role: 'frontend', percent: 100, daysSpent: 5 }, { role: 'backend', percent: 100, daysSpent: 4 }, { role: 'testing', percent: 100, daysSpent: 2 }] },
{ id: 'v9', name: '值班V1.2', status: 'developing', releaseDate: null, createdAt: '2024-06-02', currentStage: 'testing', startDate: '2024-06-02', expectedReleaseDate: '2024-06-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 2 }, { role: 'frontend', percent: 100, daysSpent: 6 }, { role: 'backend', percent: 100, daysSpent: 5 }, { role: 'testing', percent: 30, daysSpent: 2 }] },
{ id: 'v10', name: '值班V2.0', status: 'planned', releaseDate: null, createdAt: '2024-06-08' },
],
_count: { requirements: 12, projects: 3, versions: 10 },
},
{
id: 'mock-2',
name: '智慧收银',
description: '收银台 POS + 会员支付一体化',
createdAt: '2024-03-01',
updatedAt: '2024-06-01',
projects: [
{ id: 'p4', name: '支付', description: '微信/支付宝/现金多渠道收银', createdAt: '2024-03-10' },
{ id: 'p5', name: '会员', description: '储值卡、积分、优惠券', createdAt: '2024-04-01' },
],
versions: [
{ id: 'v11', name: '支付V1.0', status: 'released', releaseDate: '2024-04-20', createdAt: '2024-04-01', currentStage: 'released', startDate: '2024-04-01', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'backend', percent: 100, daysSpent: 7 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
{ id: 'v12', name: '支付V1.1', status: 'released', releaseDate: '2024-06-01', createdAt: '2024-05-25', currentStage: 'released', startDate: '2024-05-10', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 7 }, { role: 'backend', percent: 100, daysSpent: 6 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
{ id: 'v13', name: '支付V2.0', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'dev', startDate: '2024-06-05', expectedReleaseDate: '2024-07-20', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'ui', percent: 100, daysSpent: 5 }, { role: 'frontend', percent: 30, daysSpent: 5 }, { role: 'backend', percent: 20, daysSpent: 4 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
{ id: 'v14', name: '会员V1.0', status: 'released', releaseDate: '2024-05-10', createdAt: '2024-04-20', currentStage: 'released', startDate: '2024-04-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 9 }, { role: 'backend', percent: 100, daysSpent: 7 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
{ id: 'v15', name: '会员V1.1', status: 'developing', releaseDate: null, createdAt: '2024-06-03', currentStage: 'integration', startDate: '2024-06-03', expectedReleaseDate: '2024-06-25', members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 90, daysSpent: 8 }, { role: 'backend', percent: 85, daysSpent: 7 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
{ id: 'v16', name: '会员V2.0', status: 'planned', releaseDate: null, createdAt: '2024-06-08' },
],
_count: { requirements: 8, projects: 2, versions: 6 },
},
];
interface ProductWithCount {
id: string;
name: string;
description: string;
createdAt: string;
updatedAt: string;
_count?: { requirements: number; projects: number; versions?: number };
}
interface ProductOverview extends ProductWithCount {
projects: { id: string; name: string; description: string; createdAt: string }[];
versions: VersionItem[];
}
interface ProductState {
products: ProductWithCount[];
overview: ProductOverview[];
currentProduct: (Product & { requirements?: any[]; versions?: any[] }) | null;
loading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
fetchOverview: () => Promise<void>;
fetchProduct: (id: string) => Promise<void>;
createProduct: (data: { name: string; description?: string }) => Promise<void>;
updateProduct: (id: string, data: { name?: string; description?: string }) => Promise<void>;
deleteProduct: (id: string) => Promise<void>;
reorderProducts: (ids: string[]) => void;
migrateAndDeleteProduct: (sourceId: string, targetId: string) => void;
createProject: (productId: string, data: { name: string; description?: string }) => void;
createVersion: (productId: string, data: { name: string; status: string }) => void;
updateVersion: (productId: string, versionId: string, data: Partial<VersionItem>) => void;
}
export const useProductStore = create<ProductState>((set, get) => ({
products: [],
overview: [],
currentProduct: null,
loading: false,
error: null,
@@ -31,8 +122,26 @@ export const useProductStore = create<ProductState>((set, get) => ({
try {
const products = await api.get<ProductWithCount[]>('/products');
set({ products, loading: false });
} catch (e: any) {
set({ error: e.message, loading: false });
} catch {
set({ products: [], error: null, loading: false });
}
},
fetchOverview: async () => {
if (get().overview.length > 0) return;
const cached = loadLocal();
if (cached.length > 0) set({ overview: cached, loading: false });
else set({ loading: true, error: null });
try {
const overview = await api.get<ProductOverview[]>('/products/overview');
set({ overview, loading: false });
saveLocal(overview);
} catch {
if (cached.length === 0) {
set({ overview: MOCK_OVERVIEW, error: null, loading: false });
} else {
set({ loading: false });
}
}
},
@@ -41,23 +150,156 @@ export const useProductStore = create<ProductState>((set, get) => ({
try {
const product = await api.get<Product>(`/products/${id}`);
set({ currentProduct: product, loading: false });
} catch (e: any) {
set({ error: e.message, loading: false });
} catch {
const found = get().overview.find((p) => p.id === id);
if (found) {
set({ currentProduct: found as any, loading: false });
} else {
set({ error: '产品不存在', loading: false });
}
}
},
createProduct: async (data) => {
await api.post('/products', data);
await get().fetchProducts();
try {
await api.post('/products', data);
await get().fetchOverview();
} catch {
const newProduct: ProductOverview = {
id: `local-${Date.now()}`,
name: data.name || '',
description: data.description || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
projects: [],
versions: [],
_count: { requirements: 0, projects: 0, versions: 0 },
};
const updated = [newProduct, ...get().overview];
set({ overview: updated });
saveLocal(updated);
}
},
updateProduct: async (id, data) => {
await api.patch(`/products/${id}`, data);
await get().fetchProducts();
try {
await api.patch(`/products/${id}`, data);
await get().fetchOverview();
} catch {
const updated = get().overview.map((p) =>
p.id === id ? { ...p, ...data } : p,
);
set({ overview: updated });
saveLocal(updated);
}
},
deleteProduct: async (id) => {
await api.delete(`/products/${id}`);
set({ products: get().products.filter((p) => p.id !== id) });
try {
await api.delete(`/products/${id}`);
} catch {}
const updated = get().overview.filter((p) => p.id !== id);
set({
products: get().products.filter((p) => p.id !== id),
overview: updated,
});
saveLocal(updated);
},
reorderProducts: (ids) => {
const map = new Map(get().overview.map((p) => [p.id, p]));
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
set({ overview: reordered });
saveLocal(reordered);
},
migrateAndDeleteProduct: (sourceId, targetId) => {
const source = get().overview.find((p) => p.id === sourceId);
const target = get().overview.find((p) => p.id === targetId);
if (!source || !target) return;
const migratedTarget: ProductOverview = {
...target,
projects: [...target.projects, ...source.projects],
versions: [...target.versions, ...source.versions],
_count: {
requirements: (target._count?.requirements ?? 0) + (source._count?.requirements ?? 0),
projects: (target._count?.projects ?? 0) + (source._count?.projects ?? 0),
versions: (target._count?.versions ?? 0) + (source._count?.versions ?? 0),
},
};
const updated = get().overview
.filter((p) => p.id !== sourceId)
.map((p) => (p.id === targetId ? migratedTarget : p));
set({ overview: updated });
saveLocal(updated);
},
createProject: (productId, data) => {
const newProject = {
id: `proj-${Date.now()}`,
name: data.name || '',
description: data.description || '',
createdAt: new Date().toISOString(),
};
const updated = get().overview.map((p) => {
if (p.id !== productId) return p;
return {
...p,
projects: [...p.projects, newProject],
_count: { ...p._count, projects: (p._count?.projects ?? 0) + 1 } as any,
};
});
set({ overview: updated });
saveLocal(updated);
},
createVersion: (productId, data) => {
const newVersion = {
id: `ver-${Date.now()}`,
name: data.name,
status: data.status,
releaseDate: null,
createdAt: new Date().toISOString(),
};
const updated = get().overview.map((p) => {
if (p.id !== productId) return p;
return {
...p,
versions: [...p.versions, newVersion],
_count: { ...p._count, versions: (p._count?.versions ?? 0) + 1 } as any,
};
});
set({ overview: updated });
saveLocal(updated);
},
updateVersion: (productId, versionId, data) => {
const updated = get().overview.map((p) => {
if (p.id !== productId) return p;
return {
...p,
versions: p.versions.map((v) =>
v.id === versionId ? { ...v, ...data } : v,
),
};
});
set({ overview: updated });
saveLocal(updated);
},
}));
const STORAGE_KEY = 'ftb_products_overview';
function saveLocal(data: ProductOverview[]) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
}
function loadLocal(): ProductOverview[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch {}
return MOCK_OVERVIEW;
}