README / client /src /components /ProjectCard.tsx
firoruheqo4's picture
Upload 139 files
926b211 verified
Raw
History Blame Contribute Delete
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>
);
}