180 lines
6.2 KiB
TypeScript
180 lines
6.2 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import type { Visa } from "@/lib/types";
|
||
|
||
interface Props {
|
||
visas: Visa[];
|
||
}
|
||
|
||
const INCOME_OPTIONS = [
|
||
{ key: "low", label: "💰 低于 ¥8,000/月", max: 8000 },
|
||
{ key: "mid", label: "💼 ¥8,000 – ¥20,000/月", max: 20000 },
|
||
{ key: "high", label: "🚀 ¥20,000 – ¥50,000/月", max: 50000 },
|
||
{ key: "premium", label: "👑 高于 ¥50,000/月", max: Infinity },
|
||
];
|
||
|
||
const DURATION_OPTIONS = [
|
||
{ key: "short", label: "⏱️ 1-3 个月", months: 3 },
|
||
{ key: "medium", label: "📅 3-12 个月", months: 12 },
|
||
{ key: "long", label: "🏠 1 年以上", months: 24 },
|
||
];
|
||
|
||
const REGION_OPTIONS = [
|
||
{ key: "any", label: "🌏 不限" },
|
||
{ key: "europe", label: "🏰 欧洲" },
|
||
{ key: "sea", label: "🌴 东南亚" },
|
||
{ key: "latam", label: "🌮 拉美" },
|
||
];
|
||
|
||
const VISA_INCOME: Record<string, number> = {
|
||
"1": 6000, "2": 17000, "3": 2000, "4": 47000, "5": 18000, "6": 28000,
|
||
};
|
||
|
||
const VISA_REGION: Record<string, string> = {
|
||
"1": "europe", "2": "europe", "3": "sea", "4": "sea", "5": "latam", "6": "europe",
|
||
};
|
||
|
||
export default function VisaWizard({ visas }: Props) {
|
||
const [step, setStep] = useState(0);
|
||
const [income, setIncome] = useState("");
|
||
const [duration, setDuration] = useState("");
|
||
const [region, setRegion] = useState("any");
|
||
const [showResults, setShowResults] = useState(false);
|
||
|
||
const results = useMemo(() => {
|
||
if (!income || !duration) return [];
|
||
const incomeOpt = INCOME_OPTIONS.find((o) => o.key === income)!;
|
||
const durOpt = DURATION_OPTIONS.find((o) => o.key === duration)!;
|
||
|
||
return visas
|
||
.map((v) => {
|
||
let score = 0;
|
||
const reqIncome = VISA_INCOME[v.id] ?? 15000;
|
||
const vRegion = VISA_REGION[v.id] ?? "any";
|
||
|
||
if (reqIncome <= incomeOpt.max) score += 40;
|
||
else if (reqIncome <= incomeOpt.max * 1.5) score += 20;
|
||
|
||
if (v.difficulty <= 35) score += 25;
|
||
else if (v.difficulty <= 55) score += 15;
|
||
else score += 5;
|
||
|
||
if (durOpt.months >= 12 && v.duration.includes("年")) score += 20;
|
||
else if (durOpt.months <= 3) score += v.duration.includes("天") ? 20 : 10;
|
||
else score += 15;
|
||
|
||
if (region === "any" || vRegion === region) score += 15;
|
||
|
||
return { visa: v, score: Math.min(score, 100) };
|
||
})
|
||
.filter((r) => r.score >= 30)
|
||
.sort((a, b) => b.score - a.score)
|
||
.slice(0, 3);
|
||
}, [visas, income, duration, region]);
|
||
|
||
const canNext = step === 0 ? !!income : step === 1 ? !!duration : true;
|
||
|
||
const handleFinish = () => {
|
||
setShowResults(true);
|
||
setStep(3);
|
||
};
|
||
|
||
const reset = () => {
|
||
setStep(0);
|
||
setIncome("");
|
||
setDuration("");
|
||
setRegion("any");
|
||
setShowResults(false);
|
||
};
|
||
|
||
return (
|
||
<div className="visa-wizard reveal">
|
||
<div className="visa-wizard-header">
|
||
<span className="section-tag">🧙 VISA WIZARD</span>
|
||
<h3>签证智能匹配</h3>
|
||
<p>回答 3 个问题,找到最适合你的远程工作签证</p>
|
||
</div>
|
||
|
||
{!showResults ? (
|
||
<div className="visa-wizard-body">
|
||
<div className="visa-wizard-steps">
|
||
{["月收入", "停留时长", "目的地"].map((label, i) => (
|
||
<div key={label} className={`wizard-step-dot${i <= step ? " active" : ""}${i < step ? " done" : ""}`}>
|
||
<span>{i < step ? "✓" : i + 1}</span>
|
||
<small>{label}</small>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{step === 0 && (
|
||
<div className="wizard-options">
|
||
{INCOME_OPTIONS.map((o) => (
|
||
<button key={o.key} className={`wizard-option${income === o.key ? " selected" : ""}`} onClick={() => setIncome(o.key)}>
|
||
{o.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{step === 1 && (
|
||
<div className="wizard-options">
|
||
{DURATION_OPTIONS.map((o) => (
|
||
<button key={o.key} className={`wizard-option${duration === o.key ? " selected" : ""}`} onClick={() => setDuration(o.key)}>
|
||
{o.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{step === 2 && (
|
||
<div className="wizard-options">
|
||
{REGION_OPTIONS.map((o) => (
|
||
<button key={o.key} className={`wizard-option${region === o.key ? " selected" : ""}`} onClick={() => setRegion(o.key)}>
|
||
{o.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="wizard-nav">
|
||
{step > 0 && <button className="btn btn-ghost" onClick={() => setStep(step - 1)}>← 上一步</button>}
|
||
{step < 2 ? (
|
||
<button className="btn btn-primary" disabled={!canNext} onClick={() => setStep(step + 1)}>下一步 →</button>
|
||
) : (
|
||
<button className="btn btn-primary" onClick={handleFinish}>查看推荐 ✨</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="visa-wizard-results">
|
||
{results.length === 0 ? (
|
||
<p className="wizard-empty">暂无完全匹配的签证,建议咨询专业移民顾问或放宽条件重试</p>
|
||
) : (
|
||
<div className="wizard-result-cards">
|
||
{results.map(({ visa, score }, i) => (
|
||
<div key={visa.id} className={`wizard-result-card${i === 0 ? " winner" : ""}`}>
|
||
{i === 0 && <span className="wizard-winner-badge">🏆 最佳匹配</span>}
|
||
<div className="wizard-result-top">
|
||
<span className="wizard-result-flag">{visa.flag}</span>
|
||
<div>
|
||
<h4>{visa.name}</h4>
|
||
<span className="wizard-match-score">{score}% 匹配</span>
|
||
</div>
|
||
</div>
|
||
<ul>
|
||
<li>💰 {visa.income_req}</li>
|
||
<li>📅 {visa.duration}</li>
|
||
<li>⏱️ {visa.approval_time}</li>
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<button className="btn btn-ghost" onClick={reset}>🔄 重新匹配</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|