179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import { clsx } from 'clsx';
|
||
import { CheckCircle, XCircle, AlertTriangle, Info, Award, TrendingUp } from 'lucide-react';
|
||
|
||
interface CategoryAssessment {
|
||
category: string;
|
||
categoryName: string;
|
||
eligible: boolean;
|
||
score: number;
|
||
confidence: number;
|
||
highlights: string[];
|
||
concerns: string[];
|
||
missingInfo?: string[];
|
||
subClass?: string;
|
||
}
|
||
|
||
interface AssessmentData {
|
||
assessments: CategoryAssessment[];
|
||
overallRecommendation: string;
|
||
topRecommended: string[];
|
||
suitabilityScore: number;
|
||
summary: string;
|
||
}
|
||
|
||
function ScoreBar({ score, label }: { score: number; label?: string }) {
|
||
const getColor = (s: number) => {
|
||
if (s >= 90) return 'bg-green-500';
|
||
if (s >= 70) return 'bg-blue-500';
|
||
if (s >= 50) return 'bg-yellow-500';
|
||
if (s >= 30) return 'bg-orange-500';
|
||
return 'bg-red-500';
|
||
};
|
||
|
||
const getLabel = (s: number) => {
|
||
if (s >= 90) return '高度适合';
|
||
if (s >= 70) return '比较适合';
|
||
if (s >= 50) return '有条件适合';
|
||
if (s >= 30) return '适合度低';
|
||
return '不适合';
|
||
};
|
||
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex-1 h-2 bg-secondary-100 rounded-full overflow-hidden">
|
||
<div
|
||
className={clsx('h-full rounded-full transition-all', getColor(score))}
|
||
style={{ width: `${score}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-xs font-medium text-secondary-600 whitespace-nowrap w-16 text-right">
|
||
{label || getLabel(score)} {score}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CategoryCard({ assessment, isRecommended }: { assessment: CategoryAssessment; isRecommended: boolean }) {
|
||
return (
|
||
<div className={clsx(
|
||
'p-3 rounded-lg border transition-all',
|
||
isRecommended ? 'border-primary-300 bg-primary-50/50' : 'border-secondary-200 bg-white',
|
||
)}>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
{isRecommended && <Award className="w-4 h-4 text-primary-500" />}
|
||
<span className="font-medium text-sm">{assessment.categoryName}</span>
|
||
<span className="text-xs text-secondary-400">{assessment.category}</span>
|
||
{assessment.subClass && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-secondary-100 rounded">{assessment.subClass}</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
{assessment.eligible ? (
|
||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||
) : (
|
||
<XCircle className="w-4 h-4 text-red-400" />
|
||
)}
|
||
<span className={clsx('text-xs', assessment.eligible ? 'text-green-600' : 'text-red-500')}>
|
||
{assessment.eligible ? '符合条件' : '不符合'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<ScoreBar score={assessment.score} />
|
||
|
||
{assessment.highlights.length > 0 && (
|
||
<div className="mt-2">
|
||
{assessment.highlights.map((h, i) => (
|
||
<div key={i} className="flex items-start gap-1.5 text-xs text-green-700 mt-1">
|
||
<CheckCircle className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||
<span>{h}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{assessment.concerns.length > 0 && (
|
||
<div className="mt-1">
|
||
{assessment.concerns.map((c, i) => (
|
||
<div key={i} className="flex items-start gap-1.5 text-xs text-orange-600 mt-1">
|
||
<AlertTriangle className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||
<span>{c}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{assessment.missingInfo && assessment.missingInfo.length > 0 && (
|
||
<div className="mt-1">
|
||
{assessment.missingInfo.map((m, i) => (
|
||
<div key={i} className="flex items-start gap-1.5 text-xs text-secondary-500 mt-1">
|
||
<Info className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||
<span>{m}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function AssessmentResultCard({ data }: { data: AssessmentData }) {
|
||
const topSet = new Set(data.topRecommended);
|
||
|
||
// Sort: recommended first, then by score
|
||
const sorted = [...data.assessments].sort((a, b) => {
|
||
const aRec = topSet.has(a.category) ? 1 : 0;
|
||
const bRec = topSet.has(b.category) ? 1 : 0;
|
||
if (aRec !== bRec) return bRec - aRec;
|
||
return b.score - a.score;
|
||
});
|
||
|
||
return (
|
||
<div className="mt-3 p-4 bg-white rounded-lg border border-secondary-200">
|
||
{/* Header with overall score */}
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-primary-500" />
|
||
<span className="font-semibold text-sm">移民资格评估报告</span>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="text-xs text-secondary-500">综合适合度</span>
|
||
<span className={clsx(
|
||
'text-lg font-bold',
|
||
data.suitabilityScore >= 70 ? 'text-green-600' :
|
||
data.suitabilityScore >= 50 ? 'text-yellow-600' : 'text-red-500',
|
||
)}>
|
||
{data.suitabilityScore}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
{data.summary && (
|
||
<p className="text-xs text-secondary-600 mb-3 leading-relaxed">{data.summary}</p>
|
||
)}
|
||
|
||
{/* Category assessments */}
|
||
<div className="space-y-2">
|
||
{sorted.map((assessment) => (
|
||
<CategoryCard
|
||
key={assessment.category}
|
||
assessment={assessment}
|
||
isRecommended={topSet.has(assessment.category)}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* Overall recommendation */}
|
||
{data.overallRecommendation && (
|
||
<div className="mt-3 p-2 bg-primary-50 rounded-lg">
|
||
<p className="text-xs text-primary-800 leading-relaxed">
|
||
<strong>建议:</strong>{data.overallRecommendation}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|