'use client'; import { RouteGuard } from '@/components/auth/Guard'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Search, FolderKanban, Package, ChevronDown, Plus, X, MoreHorizontal, Trash2, Pencil } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { flattenProjects, flattenVersions, ProjectWithContext } from '@/lib/derive'; import { VersionChip } from '@/components/version/VersionChip'; import { Pagination, usePagination } from '@/components/Pagination'; import { buildVersionProgressMap } from '@/lib/version-progress'; import { useHasPermission } from '@/components/auth/Guard'; export default function ProjectsPage() { return ( ); } function ProjectsPageContent() { const router = useRouter(); const { overview, fetchOverview, createProject, updateProject, deleteProject } = useProductStore(); const { requirements, fetchRequirements } = useRequirementStore(); const { plans, fetchPlans } = useVersionPlanStore(); const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore(); const { testCases, fetchTestCases } = useTestCaseStore(); const [search, setSearch] = useState(''); const [productFilter, setProductFilter] = useState('all'); const [showForm, setShowForm] = useState(false); const [editingProject, setEditingProject] = useState(null); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchRequirements(); }, [fetchRequirements]); useEffect(() => { fetchPlans(); }, [fetchPlans]); useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); const allProjects = useMemo(() => flattenProjects(overview), [overview]); const allVersions = useMemo(() => flattenVersions(overview), [overview]); const versionProgressMap = useMemo( () => buildVersionProgressMap(allVersions, plans, requirements, devTasks, testCases), [allVersions, plans, requirements, devTasks, testCases], ); const filtered = useMemo(() => { return allProjects.filter((p) => { if (productFilter !== 'all' && p.productId !== productFilter) return false; if (search && !p.name.toLowerCase().includes(search.toLowerCase())) return false; return true; }); }, [allProjects, search, productFilter]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filtered, 20); const productOptions = useMemo( () => overview.map((p) => ({ id: p.id, name: p.name })), [overview] ); const activeProductName = productFilter === 'all' ? '全部产品' : productOptions.find((p) => p.id === productFilter)?.name || '全部产品'; return (
{/* Header */}

项目

{allProjects.length}
{/* Filters */}
setSearch(e.target.value)} placeholder="搜索项目名称…" className="h-8 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-8 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]" />
{/* List */}
{filtered.length === 0 ? ( ) : (
{paged.map((proj) => ( router.push(`/projects/${proj.id}`)} onEdit={() => setEditingProject(proj)} onDelete={() => { if (!confirm(`确认删除项目「${proj.name}」?此操作不可恢复。`)) return; deleteProject(proj.productId, proj.id); }} /> ))}
)}
{showForm && ( setShowForm(false)} onSubmit={async (productId, name, description) => { await createProject(productId, { name, description }); setShowForm(false); }} /> )} {editingProject && ( setEditingProject(null)} onSubmit={async (name) => { updateProject(editingProject.productId, editingProject.id, { name }); setEditingProject(null); }} /> )}
); } /* ─── ProjectRow ─────────────────────────────────────────────────── */ function ProjectRow({ proj, versionProgressMap, onClick, onEdit, onDelete, }: { proj: ProjectWithContext; versionProgressMap: Record; onClick: () => void; onEdit: () => void; onDelete: () => void; }) { const canEdit = useHasPermission('project:edit'); const canDelete = useHasPermission('project:delete'); const [menuOpen, setMenuOpen] = useState(false); const activeVersions = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) < 100); const completedCount = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) >= 100).length; const versionCount = proj.versions.length; const canActuallyDelete = versionCount === 0; return (
{proj.name} {proj.productName} {proj.description}
{activeVersions.map((v) => ( ))} {completedCount > 0 && ( +{completedCount} 已完成 )}
{(canEdit || canDelete) && (
e.stopPropagation()}> {menuOpen && ( <>
setMenuOpen(false)} />
{canEdit && ( )} {canDelete && ( )}
)}
)}
); } /* ─── ProductDropdown ────────────────────────────────────────────── */ function ProductDropdown({ value, options, onChange, activeName, }: { value: string; options: { id: string; name: string }[]; onChange: (v: string) => void; activeName: string; }) { const [open, setOpen] = useState(false); return (
{open && ( <>
setOpen(false)} />
{ onChange('all'); setOpen(false); }} label="全部产品" /> {options.map((opt) => ( { onChange(opt.id); setOpen(false); }} label={opt.name} /> ))}
)}
); } function DropdownItem({ active, onClick, label, }: { active: boolean; onClick: () => void; label: string; }) { return ( ); } /* ─── LegendDot ──────────────────────────────────────────────────── */ function LegendDot({ color, label }: { color: string; label: string }) { return ( {label} ); } /* ─── EmptyState ─────────────────────────────────────────────────── */ function EmptyState() { return (

暂无匹配项目

尝试调整搜索条件或筛选器

); } /* ─── EditProjectModal ───────────────────────────────────────────── */ function EditProjectModal({ project, overview, onClose, onSubmit, }: { project: ProjectWithContext; overview: any[]; onClose: () => void; onSubmit: (name: string) => Promise; }) { const [name, setName] = useState(project.name); const [submitting, setSubmitting] = useState(false); const [duplicateError, setDuplicateError] = useState(false); const handleSubmit = async () => { const nextName = name.trim(); if (!nextName) return; const product = overview.find((p: any) => p.id === project.productId); const exists = product?.projects?.some( (proj: any) => proj.id !== project.id && proj.name === nextName, ); if (exists) { setDuplicateError(true); return; } setSubmitting(true); try { await onSubmit(nextName); } finally { setSubmitting(false); } }; return (

编辑项目

{ setName(e.target.value); setDuplicateError(false); }} className={`h-9 w-full rounded-lg border bg-[var(--bg)] px-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:outline-none focus:ring-2 ${ duplicateError ? 'border-red-500 focus:border-red-500 focus:ring-red-100' : 'border-[var(--line)] focus:border-[var(--accent)] focus:ring-[var(--accent-ring)]' }`} /> {duplicateError && (

该产品下已存在同名项目

)}
); } /* ─── CreateProjectModal ─────────────────────────────────────────── */ function CreateProjectModal({ products, overview, onClose, onSubmit, }: { products: { id: string; name: string }[]; overview: any[]; onClose: () => void; onSubmit: (productId: string, name: string, description: string) => Promise; }) { const [productId, setProductId] = useState(''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [submitting, setSubmitting] = useState(false); const [duplicateError, setDuplicateError] = useState(false); const handleSubmit = async () => { if (!productId || !name.trim()) return; const product = overview.find((p: any) => p.id === productId); if (product) { const exists = product.projects?.some( (proj: any) => proj.name === name.trim() ); if (exists) { setDuplicateError(true); return; } } setSubmitting(true); try { await onSubmit(productId, name.trim(), description.trim()); } finally { setSubmitting(false); } }; return (

新建项目

{/* Product select */}
{/* Name */}
{ setName(e.target.value); setDuplicateError(false); }} placeholder="请输入项目名称" className={`h-9 w-full rounded-lg border bg-[var(--bg)] px-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:outline-none focus:ring-2 ${ duplicateError ? 'border-red-500 focus:border-red-500 focus:ring-red-100' : 'border-[var(--line)] focus:border-[var(--accent)] focus:ring-[var(--accent-ring)]' }`} /> {duplicateError && (

该产品下已存在同名项目

)}
{/* Description */}