feat: 实现产品/项目/版本三大模块完整功能
- 产品列表:拖拽排序持久化、编辑弹窗、删除校验(活跃版本需迁移) - 项目列表:新建项目表单(产品选择+重名校验)、产品筛选、版本归属修复 - 版本列表:新建版本表单(迭代类型自动生成版本号)、状态/项目筛选、倒序排列 - 项目详情页:概览统计、人员墙(参与次数)、版本时间线(阶段流水线+进度条) - 全局优化:筛选栏统一为header下方固定行、按钮颜色改为主题蓝、API探测300ms、store缓存 - 数据持久化到localStorage,支持离线开发 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user