'use client';
import { useState, useEffect, useMemo } from 'react';
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,
}: {
product: ProductOverview;
expanded: boolean;
onToggle: () => void;
onEdit: () => void;
onDelete: () => void;
}) {
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' || v.status === 'planned'
);
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' || v.status === 'planned'
);
const projReleased = projVersions.filter((v) => v.status === 'released').length;
return (
{proj.name}
{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 {
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 */}
{/* 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)}
/>
))}
)}
{/* 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);
}}
/>
)}
);
}