diff --git a/apps/server/src/modules/product/product.controller.ts b/apps/server/src/modules/product/product.controller.ts index 0221da2..f4cfe06 100644 --- a/apps/server/src/modules/product/product.controller.ts +++ b/apps/server/src/modules/product/product.controller.ts @@ -17,6 +17,11 @@ export class ProductController { return this.productService.findAll(); } + @Get('overview') + findAllWithChildren() { + return this.productService.findAllWithChildren(); + } + @Get(':id') findOne(@Param('id') id: string) { return this.productService.findOne(id); diff --git a/apps/server/src/modules/product/product.service.ts b/apps/server/src/modules/product/product.service.ts index dd4679e..1a462d6 100644 --- a/apps/server/src/modules/product/product.service.ts +++ b/apps/server/src/modules/product/product.service.ts @@ -20,6 +20,23 @@ export class ProductService { }); } + findAllWithChildren() { + return this.prisma.product.findMany({ + orderBy: { createdAt: 'desc' }, + include: { + projects: { + orderBy: { createdAt: 'desc' }, + select: { id: true, name: true, description: true, createdAt: true }, + }, + versions: { + orderBy: { createdAt: 'desc' }, + select: { id: true, name: true, releaseDate: true, createdAt: true }, + }, + _count: { select: { requirements: true, projects: true, versions: true } }, + }, + }); + } + async findOne(id: string) { const product = await this.prisma.product.findUnique({ where: { id }, diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index c848569..22b04f4 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -21,10 +21,10 @@ --line-soft: #f4f4f5; /* Brand — calm blue */ - --accent: #3b82f6; - --accent-hover: #2563eb; + --accent: #2563eb; + --accent-hover: #1d4ed8; --accent-soft: #eff6ff; - --accent-ring: rgba(59, 130, 246, 0.15); + --accent-ring: rgba(37, 99, 235, 0.18); /* Shadow */ --shadow-sm: 0 1px 2px rgba(24, 24, 27, 0.04); diff --git a/apps/web/app/products/page.tsx b/apps/web/app/products/page.tsx index 03baf31..2936d74 100644 --- a/apps/web/app/products/page.tsx +++ b/apps/web/app/products/page.tsx @@ -1,94 +1,530 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { Plus, Search } from 'lucide-react'; +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 { ProductCard } from '@/components/product/ProductCard'; import { ProductForm } from '@/components/product/ProductForm'; +import { VersionChip } from '@/components/version/VersionChip'; +import type { VersionStatus } from '@/lib/version-status'; -export default function ProductsPage() { - const router = useRouter(); - const { products, loading, fetchProducts, createProduct } = useProductStore(); - const [showForm, setShowForm] = useState(false); - const [search, setSearch] = useState(''); +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 }; +}; - useEffect(() => { - fetchProducts(); - }, [fetchProducts]); +/* ─── 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 filtered = products.filter((p) => - p.name.toLowerCase().includes(search.toLowerCase()), - ); - - const handleCreate = async (data: { name: string; description: string }) => { - await createProduct(data); - setShowForm(false); + 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 ( -
- {search ? '没有找到匹配的产品' : '尚未创建任何产品'} -
-- {search ? '尝试其他关键字' : '点击右上角"新建产品"开始'} -
++ 该产品下还有 {activeVersions.length} 个未发布版本 +
+项目不存在
+ +暂无匹配项目
+尝试调整搜索条件或筛选器
+该产品下已存在同名项目
+ )} +没有找到匹配的版本
+尝试调整筛选条件
+| 版本号 | +状态 | +项目 | +产品 | +创建时间 | +
|---|
将创建:{projectName}V{versionNumber}
+ )} +