'use client'; import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, Search, GripVertical, ChevronRight, Pencil, Trash2, X, AlertTriangle, Package, } from 'lucide-react'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useProductStore } from '@/stores/useProductStore'; import { ProductForm } from '@/components/product/ProductForm'; import { VersionChip } from '@/components/version/VersionChip'; import type { VersionStatus } from '@/lib/version-status'; type ProductOverview = { id: string; name: string; description: string; projects: { id: string; name: string; description: string; createdAt: string }[]; versions: { id: string; name: string; status?: string; releaseDate: string | null; createdAt: string; }[]; _count?: { requirements: number; projects: number; versions?: number }; }; /* ─── Sortable Product Row ─── */ function SortableProductRow({ product, expanded, onToggle, onEdit, onDelete, router, }: { product: ProductOverview; expanded: boolean; onToggle: () => void; onEdit: () => void; onDelete: () => void; router: ReturnType; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: product.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; const activeVersions = product.versions.filter( (v) => v.status === 'developing' ); const releasedCount = product.versions.filter((v) => v.status === 'released').length; return (
{product.name} {product.description && ( {product.description} )}
{product._count?.projects ?? product.projects.length} 项目 {product._count?.versions ?? product.versions.length} 版本 {product._count?.requirements != null && ( {product._count.requirements} 需求 )}
{expanded && (
{product.projects.length === 0 ? (
暂无项目
) : (
{product.projects.map((proj) => { const projVersions = product.versions.filter((v) => v.name.toLowerCase().startsWith(proj.name.toLowerCase()) ); const projActive = projVersions.filter( (v) => v.status === 'developing' ); const projReleased = projVersions.filter((v) => v.status === 'released').length; return (
{proj.description && ( {proj.description} )}
{projActive.map((v) => ( ))} {projReleased > 0 && ( 已发布 {projReleased} )}
); })}
)}
)}
); } /* ─── Edit Product Modal ─── */ function EditProductModal({ product, onClose, onSubmit, }: { product: ProductOverview; onClose: () => void; onSubmit: (data: { name: string; description: string }) => void | Promise; }) { return (
e.stopPropagation()} >

编辑产品

); } /* ─── Migrate Dialog ─── */ function MigrateDialog({ source, others, onClose, onConfirm, }: { source: ProductOverview; others: ProductOverview[]; onClose: () => void; onConfirm: (targetId: string) => void | Promise; }) { const [targetId, setTargetId] = useState(''); const activeVersions = source.versions.filter( (v) => v.status === 'developing' || v.status === 'planned' ); return (
e.stopPropagation()} >

删除产品

该产品下还有 {activeVersions.length} 个未发布版本

    {activeVersions.map((v) => (
  • · {v.name}
  • ))}
); } /* ─── Main Page ─── */ export default function ProductsPage() { const router = useRouter(); const { overview, loading, fetchOverview, createProduct, updateProduct, deleteProduct, reorderProducts, migrateAndDeleteProduct, } = useProductStore(); const [search, setSearch] = useState(''); const [showCreate, setShowCreate] = useState(false); const [expanded, setExpanded] = useState>(new Set()); const [editing, setEditing] = useState(null); const [migrateTarget, setMigrateTarget] = useState(null); const [hasInitialized, setHasInitialized] = useState(false); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { if (!hasInitialized && overview.length > 0) { setExpanded(new Set([overview[0].id])); setHasInitialized(true); } }, [overview, hasInitialized]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const filtered = useMemo(() => { if (!search.trim()) return overview; const q = search.toLowerCase(); return overview.filter( (p) => p.name.toLowerCase().includes(q) || (p.description ?? '').toLowerCase().includes(q) ); }, [overview, search]); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = overview.findIndex((p) => p.id === active.id); const newIndex = overview.findIndex((p) => p.id === over.id); if (oldIndex < 0 || newIndex < 0) return; const next = [...overview]; const [moved] = next.splice(oldIndex, 1); next.splice(newIndex, 0, moved); reorderProducts(next.map((p) => p.id)); }; const toggleExpand = (id: string) => { setExpanded((prev) => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); }; const handleDelete = (product: ProductOverview) => { const active = product.versions.filter( (v) => v.status === 'developing' || v.status === 'planned' ); if (active.length === 0) { if (confirm(`确认删除产品「${product.name}」?此操作不可撤销。`)) { deleteProduct(product.id); } } else { setMigrateTarget(product); } }; return (
{/* Header */}

产品

{overview.length}
{/* Filters */}
setSearch(e.target.value)} placeholder="搜索产品..." className="w-full h-8 pl-9 pr-3 rounded-lg border border-[var(--line)] bg-[var(--bg)] text-[13px] placeholder:text-[var(--ink-muted)] focus:outline-none focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent-ring)] transition-colors" />
开发中 已发布
{/* Content */}
{/* Inline Create Form */} {showCreate && (

新建产品

{ await createProduct(data); setShowCreate(false); }} onCancel={() => setShowCreate(false)} />
)} {/* Product List */}
{loading && overview.length === 0 ? (
加载中...
) : filtered.length === 0 ? (
暂无产品
) : ( p.id)} strategy={verticalListSortingStrategy} > {filtered.map((product) => ( toggleExpand(product.id)} onEdit={() => setEditing(product)} onDelete={() => handleDelete(product)} router={router} /> ))} )}
{/* Edit Modal */} {editing && ( setEditing(null)} onSubmit={async (data) => { await updateProduct(editing.id, data); setEditing(null); }} /> )} {/* Migrate Dialog */} {migrateTarget && ( p.id !== migrateTarget.id)} onClose={() => setMigrateTarget(null)} onConfirm={async (targetId) => { await migrateAndDeleteProduct(migrateTarget.id, targetId); setMigrateTarget(null); }} /> )}
); }