138 lines
5.5 KiB
TypeScript
138 lines
5.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Link from "next/link";
|
||
import { api } from "@/lib/api";
|
||
import { useI18n } from "@/lib/i18n";
|
||
import { useToast } from "@/lib/toast";
|
||
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
|
||
import { meetupCityHref } from "@/lib/meetupLinks";
|
||
import type { CostResult, Destination } from "@/lib/types";
|
||
|
||
interface Props { destinations: Destination[] }
|
||
|
||
export default function Calculator({ destinations }: Props) {
|
||
const { t } = useI18n();
|
||
const { toast } = useToast();
|
||
const [slug, setSlug] = useState(destinations[0]?.slug || "");
|
||
const [housing, setHousing] = useState("mid");
|
||
const [months, setMonths] = useState(3);
|
||
const [result, setResult] = useState<CostResult | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const selected = destinations.find((d) => d.slug === slug);
|
||
|
||
const calculate = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await api.calculateCost({ destination_slug: slug, housing, months });
|
||
setResult(data);
|
||
} catch {
|
||
setResult(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const addToPlan = () => {
|
||
if (!selected) return;
|
||
const { added } = mergeDestinationsIntoTrip([selected], months);
|
||
toast(
|
||
added
|
||
? t.common.addedToPlanMonths
|
||
.replace("{emoji}", selected.emoji)
|
||
.replace("{city}", selected.name)
|
||
.replace("{months}", String(months))
|
||
: t.common.alreadyInPlan,
|
||
added ? "success" : "info",
|
||
{ href: "/plan", label: t.strip.openPlan }
|
||
);
|
||
};
|
||
|
||
return (
|
||
<section className="section calculator-section" id="calculator">
|
||
<div className="container">
|
||
<div className="section-header reveal">
|
||
<span className="section-tag">🧮 CALCULATOR</span>
|
||
<h2>旅居费用计算器</h2>
|
||
<p>根据目的地和生活方式,精准估算你的旅居预算</p>
|
||
</div>
|
||
<div className="calculator-wrapper reveal">
|
||
<div className="calculator-form">
|
||
<div className="calc-field">
|
||
<label>🏙️ 目的地</label>
|
||
<select value={slug} onChange={(e) => setSlug(e.target.value)}>
|
||
{destinations.map((d) => (
|
||
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}, {d.country}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="calc-field">
|
||
<label>🏠 住宿档次</label>
|
||
<div className="calc-options">
|
||
{[{ v: "budget", l: "💰 经济" }, { v: "mid", l: "🏠 舒适" }, { v: "premium", l: "✨ 豪华" }].map((o) => (
|
||
<button key={o.v} className={`calc-option${housing === o.v ? " active" : ""}`}
|
||
onClick={() => setHousing(o.v)}>{o.l}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="calc-field">
|
||
<label>📅 旅居月数: {months} 个月</label>
|
||
<input type="range" min={1} max={12} value={months} onChange={(e) => setMonths(+e.target.value)} />
|
||
</div>
|
||
<button className="btn btn-primary" onClick={calculate} disabled={loading}>
|
||
{loading ? "计算中..." : "🧮 开始计算"}
|
||
</button>
|
||
</div>
|
||
<div className="calculator-result">
|
||
{result ? (
|
||
<div className="result-card">
|
||
<div className="result-header">
|
||
<span style={{ fontSize: "3rem" }}>{result.emoji}</span>
|
||
<div>
|
||
<h3>{result.destination}</h3>
|
||
<p>{result.months} 个月旅居预算</p>
|
||
</div>
|
||
</div>
|
||
<div className="result-breakdown">
|
||
{Object.entries(result.breakdown).map(([k, v]) => (
|
||
<div key={k} className="breakdown-item">
|
||
<span>{k}</span>
|
||
<div className="breakdown-bar">
|
||
<div className="breakdown-fill" style={{ width: `${(v / result.per_month) * 100}%` }} />
|
||
</div>
|
||
<strong>¥{v.toLocaleString()}</strong>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="result-total">
|
||
<div><span>月均</span><strong>¥{result.per_month.toLocaleString()}</strong></div>
|
||
<div><span>总计</span><strong className="gradient-text">¥{result.total.toLocaleString()}</strong></div>
|
||
</div>
|
||
{selected && (
|
||
<div className="dating-empty-actions" style={{ marginTop: "1rem" }}>
|
||
<button type="button" className="btn btn-primary btn-sm" onClick={addToPlan}>
|
||
🗓️ {t.nav.plan}
|
||
</button>
|
||
<Link href={`/destinations/${selected.slug}`} className="btn btn-sm">
|
||
{t.common.detail}
|
||
</Link>
|
||
<Link href={meetupCityHref(selected.name)} className="btn btn-sm btn-ghost">
|
||
{t.common.cityMeetups}
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="result-placeholder">
|
||
<span style={{ fontSize: "4rem" }}>🧮</span>
|
||
<p>选择目的地和参数,点击计算查看预算明细</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|