Download client/src/components/ProjectHierarchyTable.tsx from zuocong12/README: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/spaces/zuocong12/README/resolve/main/client/src/components/ProjectHierarchyTable.tsx
- Command line
-
hf download hf://spaces/zuocong12/README/client/src/components/ProjectHierarchyTable.tsx
-
curl -L -o ProjectHierarchyTable.tsx https://huggingface.co/spaces/zuocong12/README/resolve/main/client/src/components/ProjectHierarchyTable.tsx
10.1 kB
| /** | |
| * ProjectHierarchyTable - Leadership Overview Module | |
| * | |
| * Three-level hierarchical table: | |
| * Level 1: Project Name (Dark header) | |
| * Level 2: Product Name (Light header, indented) | |
| * Level 3: Mold data rows (Table format) | |
| * | |
| * Design: Dense, scannable, desktop-optimized with mobile scroll | |
| */ | |
| import { ProjectData } from '@/types/project'; | |
| import { | |
| groupProjectsByHierarchy, | |
| isDateStale, | |
| formatDateShort, | |
| ProjectGroup, | |
| ProductGroup, | |
| MoldItem | |
| } from '@/lib/hierarchyUtils'; | |
| import { ChevronDown, ChevronRight, Clock, AlertCircle, CheckCircle2 } from 'lucide-react'; | |
| import { useState } from 'react'; | |
| interface ProjectHierarchyTableProps { | |
| projects: ProjectData[]; | |
| } | |
| export default function ProjectHierarchyTable({ projects }: ProjectHierarchyTableProps) { | |
| const hierarchicalData = groupProjectsByHierarchy(projects); | |
| // Collapse state for each project | |
| const [collapsedProjects, setCollapsedProjects] = useState<Set<string>>(new Set()); | |
| const toggleProject = (projectName: string) => { | |
| setCollapsedProjects(prev => { | |
| const newSet = new Set(prev); | |
| if (newSet.has(projectName)) { | |
| newSet.delete(projectName); | |
| } else { | |
| newSet.add(projectName); | |
| } | |
| return newSet; | |
| }); | |
| }; | |
| return ( | |
| <div | |
| className="relative z-0 w-full bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mb-8" | |
| style={{ overflowAnchor: 'none' }} | |
| > | |
| {/* Header */} | |
| <div className="bg-gradient-to-r from-slate-700 to-slate-600 px-6 py-4 border-b border-slate-500"> | |
| <h2 className="text-xl font-bold text-white tracking-tight"> | |
| 项目整体概览 / Project Overview | |
| </h2> | |
| <p className="text-sm text-slate-300 mt-1"> | |
| 按项目和产品分组的宏观视图 · {projects.length} 个模具 | |
| </p> | |
| </div> | |
| {/* Table Container - No horizontal scroll, responsive reflow */} | |
| <div> | |
| {hierarchicalData.map(projectGroup => ( | |
| <ProjectGroupRow | |
| key={projectGroup.projectName} | |
| projectGroup={projectGroup} | |
| isCollapsed={collapsedProjects.has(projectGroup.projectName)} | |
| onToggle={() => toggleProject(projectGroup.projectName)} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Level 1: Project Name Row | |
| function ProjectGroupRow({ | |
| projectGroup, | |
| isCollapsed, | |
| onToggle | |
| }: { | |
| projectGroup: ProjectGroup; | |
| isCollapsed: boolean; | |
| onToggle: () => void; | |
| }) { | |
| const totalMolds = projectGroup.products.reduce((sum, p) => sum + p.molds.length, 0); | |
| return ( | |
| <div className="border-b border-slate-200 last:border-b-0"> | |
| {/* Project Header - Clickable to collapse */} | |
| <button | |
| onClick={onToggle} | |
| className="w-full bg-slate-700 hover:bg-slate-600 transition-colors px-6 py-3 flex items-center justify-between group" | |
| > | |
| <div className="flex items-center gap-3"> | |
| {isCollapsed ? ( | |
| <ChevronRight className="w-5 h-5 text-slate-300" /> | |
| ) : ( | |
| <ChevronDown className="w-5 h-5 text-slate-300" /> | |
| )} | |
| <span className="text-base font-bold text-white"> | |
| {projectGroup.projectName} | |
| </span> | |
| <span className="text-xs text-slate-400 font-medium"> | |
| {projectGroup.products.length} 产品 · {totalMolds} 模具 | |
| </span> | |
| </div> | |
| </button> | |
| {/* Products (Level 2) - Only show if not collapsed */} | |
| {!isCollapsed && projectGroup.products.map(productGroup => ( | |
| <ProductGroupRow | |
| key={productGroup.productName} | |
| productGroup={productGroup} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| // Level 2: Product Name + Level 3: Mold Table | |
| function ProductGroupRow({ productGroup }: { productGroup: ProductGroup }) { | |
| return ( | |
| <div className="bg-slate-50"> | |
| {/* Product Header */} | |
| <div className="bg-slate-100 px-6 py-2.5 border-b border-slate-200"> | |
| <div className="flex items-center gap-2 pl-8"> | |
| <div className="w-1 h-4 bg-indigo-500 rounded-full" /> | |
| <span className="text-sm font-bold text-slate-700"> | |
| {productGroup.productName} | |
| </span> | |
| <span className="text-xs text-slate-500"> | |
| ({productGroup.molds.length} 模具) | |
| </span> | |
| </div> | |
| </div> | |
| {/* Mold Data Table - Responsive: Table on desktop, Cards on mobile */} | |
| <div className="bg-white"> | |
| {/* Table Header - Hidden on mobile, shown on desktop */} | |
| {/* CRITICAL: Must use grid-cols-12 to match data row layout exactly */} | |
| <div className="hidden md:grid md:grid-cols-12 md:gap-4 md:items-center md:px-4 md:py-3 md:bg-slate-50 md:border-b md:border-slate-200"> | |
| {/* Col 1-2: Mold Number Header (2 cols) */} | |
| <div className="md:col-span-2 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider"> | |
| 模具编号 | |
| </div> | |
| {/* Col 3-4: Update Date Header (2 cols) */} | |
| <div className="md:col-span-2 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider"> | |
| 更新日期 | |
| </div> | |
| {/* Col 5-10: Progress Details Header (6 cols) - Add pl-4 for spacing */} | |
| <div className="md:col-span-6 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider pl-4"> | |
| 项目推进细节 | |
| </div> | |
| {/* Col 11-12: Current Node Header (2 cols) - Right-aligned to match data */} | |
| <div className="md:col-span-2 text-right text-xs font-semibold text-slate-600 uppercase tracking-wider"> | |
| 当前节点 | |
| </div> | |
| </div> | |
| {/* Data Rows - Stacked cards on mobile, table rows on desktop */} | |
| <div> | |
| {productGroup.molds.map((mold, index) => ( | |
| <MoldDataRow key={`${mold.moldNumber}-${index}`} mold={mold} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Level 3: Individual Mold Row | |
| function MoldDataRow({ mold }: { mold: MoldItem }) { | |
| const isStale = isDateStale(mold.updateDate); | |
| const formattedDate = formatDateShort(mold.updateDate); | |
| // Truncate long text | |
| const truncateText = (text: string, maxLength: number = 80) => { | |
| if (!text || text === '-') return '-'; | |
| return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; | |
| }; | |
| // Render current node: Show all, highlight critical | |
| const renderCurrentNode = (nodeValue: string) => { | |
| // Data cleaning: trim to remove invisible spaces | |
| const status = (nodeValue || '').toString().trim(); | |
| switch (status) { | |
| case '进行中': | |
| return ( | |
| <div className="flex items-center gap-1.5 text-yellow-600"> | |
| <Clock className="w-4 h-4" /> | |
| <span className="text-sm font-medium">进行中</span> | |
| </div> | |
| ); | |
| case '已超时': | |
| return ( | |
| <div className="flex items-center gap-1.5 text-red-600"> | |
| <AlertCircle className="w-4 h-4" /> | |
| <span className="text-sm font-bold">已超时</span> | |
| </div> | |
| ); | |
| case '已完成': | |
| return ( | |
| <div className="flex items-center gap-1.5 text-green-600"> | |
| <CheckCircle2 className="w-4 h-4" /> | |
| <span className="text-sm font-medium">已完成</span> | |
| </div> | |
| ); | |
| default: | |
| // Show all other status text (T1, PQR, PB2, etc.) in light gray | |
| return ( | |
| <span className="text-gray-400 text-sm"> | |
| {status || '-'} | |
| </span> | |
| ); | |
| } | |
| }; | |
| return ( | |
| <div className="relative border-b border-slate-100 hover:bg-slate-50 transition-colors"> | |
| {/* Mobile: Vertical Card Layout */} | |
| <div className="flex flex-col p-4 md:hidden"> | |
| {/* Top Row: Mold No (Left) + Current Node (Right) */} | |
| <div className="flex items-start justify-between mb-2"> | |
| <div className="font-bold text-gray-800"> | |
| {mold.moldNumber} | |
| </div> | |
| <div className="ml-4"> | |
| {renderCurrentNode(mold.currentNode)} | |
| </div> | |
| </div> | |
| {/* Middle Row: Update Date */} | |
| <div className="text-sm text-gray-400 mb-2"> | |
| <span className="font-medium">更新于: </span> | |
| <span className={isStale ? 'text-red-600 font-bold' : ''}> | |
| {formattedDate} | |
| </span> | |
| {isStale && ( | |
| <span className="ml-1.5 text-xs text-red-500">⚠</span> | |
| )} | |
| </div> | |
| {/* Bottom Row: Progress Details (Full width, wrapping allowed) */} | |
| <div className="text-sm text-gray-600 break-words whitespace-normal"> | |
| {mold.progressDetails} | |
| </div> | |
| </div> | |
| {/* Desktop: CSS Grid 12-Column Layout */} | |
| <div className="hidden md:grid md:grid-cols-12 md:gap-4 md:items-start md:py-3 md:px-4"> | |
| {/* Col 1-2: Mold Number (2 cols) */} | |
| <div className="md:col-span-2"> | |
| <div className="font-medium text-gray-900 text-sm"> | |
| {mold.moldNumber} | |
| </div> | |
| </div> | |
| {/* Col 3-4: Update Date (2 cols) */} | |
| <div className="md:col-span-2"> | |
| <span className={`text-sm ${isStale ? 'text-red-600 font-bold' : 'text-gray-500'}`}> | |
| {formattedDate} | |
| </span> | |
| {isStale && ( | |
| <span className="ml-1.5 text-xs text-red-500">⚠</span> | |
| )} | |
| </div> | |
| {/* Col 5-10: Progress Details (6 cols = 50% width) - Add pl-4 to match header */} | |
| <div className="md:col-span-6 pl-4"> | |
| <div className="text-sm text-gray-700 whitespace-normal break-words leading-relaxed"> | |
| {mold.progressDetails} | |
| </div> | |
| </div> | |
| {/* Col 11-12: Current Node (2 cols, right-aligned) */} | |
| <div className="md:col-span-2 flex justify-end"> | |
| {renderCurrentNode(mold.currentNode)} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |