Download client/src/components/ProjectCard.tsx from zuocong12/README: direct link, hf CLI and curl.
- Browser
- Download file 9.23 kB
-
https://huggingface.co/spaces/zuocong12/README/resolve/main/client/src/components/ProjectCard.tsx
- Command line
-
hf download hf://spaces/zuocong12/README/client/src/components/ProjectCard.tsx
-
curl -L -o ProjectCard.tsx https://huggingface.co/spaces/zuocong12/README/resolve/main/client/src/components/ProjectCard.tsx
9.23 kB
| /** | |
| * ProjectCard - Enterprise SaaS Style (Linear/Notion inspired) | |
| * | |
| * Visual Design: | |
| * - Modern typography with Microsoft YaHei optimization | |
| * - Soft shadows and generous spacing | |
| * - Clean section headers with decorative accents | |
| * - Traffic light system for FAI values | |
| */ | |
| import { ProjectData } from '@/types/project'; | |
| import { | |
| getRiskBadgeClass, | |
| formatDate, | |
| getDisplayValue, | |
| parseFAIValue, | |
| getFAIColorClass, | |
| getCardVisualState, | |
| formatExcelDate | |
| } from '@/lib/projectUtils'; | |
| import { Target, FlaskConical, CheckCircle2 } from 'lucide-react'; | |
| interface ProjectCardProps { | |
| project: ProjectData; | |
| } | |
| export default function ProjectCard({ project }: ProjectCardProps) { | |
| const { no, identity, milestones, details } = project; | |
| // Get visual state based on currentNode | |
| const visualState = getCardVisualState(milestones.currentNode); | |
| return ( | |
| <div | |
| className={` | |
| ${visualState.bgClass} rounded-xl overflow-hidden | |
| transition-all duration-300 ease-out | |
| hover:shadow-xl hover:-translate-y-0.5 | |
| ${visualState.borderClass} | |
| `} | |
| > | |
| {/* Card Header */} | |
| <div className="px-8 py-5 border-b border-slate-100 bg-gradient-to-r from-slate-50/50 to-white"> | |
| <div className="flex items-start justify-between gap-4"> | |
| <div className="flex-1 min-w-0"> | |
| <h3 className="text-2xl font-bold text-slate-900 tracking-tight leading-tight"> | |
| {getDisplayValue(identity.projectName, 'N/A')} | |
| </h3> | |
| <p className="text-sm text-slate-500 mt-1.5 font-medium"> | |
| {getDisplayValue(identity.productName, 'N/A')} <span className="text-slate-300 mx-2">•</span> NO. {getDisplayValue(no, 'N/A')} | |
| </p> | |
| </div> | |
| {/* Status Badge - Only show if valid currentNode */} | |
| {visualState.showBadge && ( | |
| <span className={`px-4 py-1.5 rounded-lg text-sm font-semibold border-2 whitespace-nowrap ${visualState.badgeClass}`}> | |
| {visualState.badgeLabel} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Section 1: 项目基本信息 */} | |
| <div className="px-8 py-6 bg-slate-50/50"> | |
| <div className="flex items-center gap-2 mb-4"> | |
| <div className="w-1 h-5 bg-blue-500 rounded-full" /> | |
| <h4 className="text-lg font-bold text-slate-800"> | |
| 项目基本信息 | |
| </h4> | |
| </div> | |
| <div className="grid grid-cols-4 gap-x-6 gap-y-4"> | |
| <InfoField label="客户名称" value={identity.customerName} /> | |
| <InfoField label="客户基地" value={identity.customerBase} /> | |
| <InfoField label="落地工厂" value={identity.factory} /> | |
| <InfoField label="模具套数" value={identity.moldSets} /> | |
| <InfoField label="料号" value={identity.partNumber} /> | |
| <InfoField label="穴号" value={identity.cavityNumber} /> | |
| <InfoField label="模具编号" value={identity.moldNumber} /> | |
| <InfoField label="项目经理" value={identity.projectManager} /> | |
| <InfoField label="项目工程师" value={identity.projectEngineer} /> | |
| <InfoField label="模具项目" value={identity.moldProject} /> | |
| <InfoField label="项目QE" value={identity.qe} /> | |
| <InfoField label="风险等级" value={identity.riskLevel} highlight /> | |
| </div> | |
| </div> | |
| {/* Section 2: 内部节点 */} | |
| <div className="px-8 py-6 bg-white"> | |
| <div className="flex items-center gap-2 mb-5"> | |
| <div className="w-1 h-5 bg-indigo-500 rounded-full" /> | |
| <h4 className="text-lg font-bold text-slate-800"> | |
| 内部节点 | |
| </h4> | |
| </div> | |
| {/* Timeline Visual - CSS Grid for Perfect Alignment */} | |
| <div className="mb-6"> | |
| <div className="relative w-full"> | |
| {/* The Grey Line - Perfectly centered start-to-end */} | |
| <div className="absolute top-4 sm:top-5 left-[10%] right-[10%] h-0.5 bg-slate-200 -z-10" /> | |
| {/* The Nodes - Grid System (5 equal columns) */} | |
| <div className="grid grid-cols-5 w-full"> | |
| <TimelineNode label="KICK OFF" date={formatDate(milestones.projectStart)} /> | |
| <TimelineNode label="T1" date={formatDate(milestones.t1)} /> | |
| <TimelineNode label="G/L" date={formatDate(milestones.glTime)} /> | |
| <TimelineNode label="VMP" date={formatDate(milestones.vmp)} /> | |
| <TimelineNode label="MP" date={formatDate(milestones.mp)} /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Status Indicators Grid */} | |
| <div className="grid grid-cols-3 gap-4"> | |
| <StatusField | |
| icon={<Target className="w-4 h-4" />} | |
| label="当前阶段" | |
| value={milestones.currentStage} | |
| /> | |
| <StatusField | |
| icon={<FlaskConical className="w-4 h-4" />} | |
| label="试模次数" | |
| value={milestones.trialCount} | |
| /> | |
| <StatusField | |
| icon={<CheckCircle2 className="w-4 h-4" />} | |
| label="T1尺寸达标" | |
| value={milestones.t1SizeQualified} | |
| /> | |
| {/* FAI Fields with Traffic Light Logic */} | |
| <FAIField | |
| label="Tooling FAI" | |
| value={milestones.toolingFAI} | |
| /> | |
| <FAIField | |
| label="Part FAI" | |
| value={milestones.partFAI} | |
| /> | |
| {/* Status is now shown in header badge */} | |
| </div> | |
| </div> | |
| {/* Section 3: 项目推进细节 - Executive View */} | |
| <div className="px-8 py-6 bg-slate-50 border-t-2 border-slate-100"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-1 h-5 bg-slate-700 rounded-full" /> | |
| <h4 className="text-lg font-bold text-slate-700"> | |
| 项目推进细节 | |
| </h4> | |
| </div> | |
| {/* Date Display with formatExcelDate */} | |
| <span className="text-sm font-medium text-slate-500"> | |
| 更新时间: {formatExcelDate(details.detailDate)} | |
| </span> | |
| </div> | |
| {/* The Content Block - Executive View Typography (Premium Bold) */} | |
| <p | |
| className="whitespace-pre-wrap text-lg font-bold leading-loose text-slate-800" | |
| style={{ | |
| fontFamily: "'Microsoft YaHei', 'PingFang SC', sans-serif", | |
| letterSpacing: "0.02em" // Adds design feel to Chinese text | |
| }} | |
| > | |
| {getDisplayValue(details.detailProgress, '无详细内容')} | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Helper component for info fields in Section 1 | |
| function InfoField({ | |
| label, | |
| value, | |
| highlight = false | |
| }: { | |
| label: string; | |
| value: string; | |
| highlight?: boolean; | |
| }) { | |
| return ( | |
| <div className="space-y-1"> | |
| <div className="text-xs font-medium text-slate-500 uppercase tracking-wider">{label}</div> | |
| <div className={`text-sm font-semibold leading-snug ${highlight ? 'text-blue-600' : 'text-slate-900'}`}> | |
| {getDisplayValue(value, '-')} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Helper component for timeline nodes in Section 2 | |
| function TimelineNode({ label, date }: { label: string; date: string }) { | |
| return ( | |
| <div className="relative flex flex-col items-center"> | |
| <div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-white border-2 border-indigo-500 shadow-sm flex items-center justify-center mb-1.5 sm:mb-2"> | |
| <div className="w-3 h-3 sm:w-4 sm:h-4 rounded-full bg-indigo-500" /> | |
| </div> | |
| <div className="text-[10px] sm:text-xs font-bold text-slate-600 uppercase tracking-wider mb-0.5 whitespace-nowrap text-center">{label}</div> | |
| <div className="text-[9px] sm:text-[10px] text-slate-500 font-medium whitespace-nowrap text-center scale-90">{date}</div> | |
| </div> | |
| ); | |
| } | |
| // Helper component for status fields in Section 2 | |
| function StatusField({ | |
| icon, | |
| label, | |
| value | |
| }: { | |
| icon?: React.ReactNode; | |
| label: string; | |
| value: string; | |
| }) { | |
| return ( | |
| <div className="flex items-start gap-3 p-3 rounded-lg bg-slate-50/50"> | |
| {icon && <span className="text-slate-400 mt-0.5">{icon}</span>} | |
| <div className="flex-1 min-w-0"> | |
| <div className="text-xs font-medium text-slate-500 uppercase tracking-wider mb-1">{label}</div> | |
| <div className="text-sm font-semibold text-slate-900 truncate"> | |
| {getDisplayValue(value, '-')} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Helper component for FAI fields with traffic light logic | |
| function FAIField({ | |
| label, | |
| value | |
| }: { | |
| label: string; | |
| value: string; | |
| }) { | |
| const displayValue = parseFAIValue(value); | |
| const colorClass = getFAIColorClass(value); | |
| return ( | |
| <div className="flex items-start gap-3 p-3 rounded-lg bg-slate-50/50"> | |
| <div className="flex-1 min-w-0"> | |
| <div className="text-xs font-medium text-slate-500 uppercase tracking-wider mb-1">{label}</div> | |
| <div className={`text-sm font-semibold truncate ${colorClass}`}> | |
| {displayValue} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |