| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |
| |
| |
| 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> |
| ); |
| } |
|
|
| |
| 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> |
| ); |
| } |
|
|
| |
| 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> |
| ); |
| } |
|
|
| |
| 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> |
| ); |
| } |
|
|
| |
| 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> |
| ); |
| } |
|
|