"use client"; import { useEffect, useMemo, useState } from "react"; import { Link, useTranslation } from "@/i18n/navigation"; import CityCard from "@/app/components/CityCard"; import CityModal from "@/app/components/CityModal"; import Footer from "@/app/components/Footer"; import { cities as fallbackCities, type City } from "@/app/data/cities"; import { apiFetch } from "@/app/lib/api-client"; type RecommendedCity = City & { matchScore?: number; matchReasons?: string[]; idNumber?: number; }; interface Meetup { id: string; city: string; emoji?: string; date?: string; time?: string; venue?: string; rsvpCount?: number; isUpcoming?: boolean; } interface NomadRoute { id?: string; slug: string; title: string; titleEn?: string; durationDays: number; budget: number; description: string; stops: string[]; cities?: RecommendedCity[]; } const CITY_TAGS = ["便宜", "宜居", "文化", "美食", "高速网络", "友好社区", "海滨", "都市"]; const TIMEZONES = ["UTC+8 中国", "UTC+7 东南亚", "UTC+1 欧洲", "UTC-5 美东", "UTC-8 美西", "任意"]; const VISA_OPTIONS = ["免签优先", "落地签可", "需提前办", "任意"]; const CLIMATE_OPTIONS = [ { value: "any", label: "任意气候" }, { value: "mild", label: "温和" }, { value: "warm", label: "偏暖" }, { value: "cool", label: "偏凉" }, ]; function mapCity(item: Partial, index: number): RecommendedCity { return { ...item, id: Number(item.idNumber || item.id || index + 1), name: item.name || "未知城市", country: item.country || "中国", countryEmoji: item.countryEmoji || "🇨🇳", overallScore: Number(item.overallScore || 0), costPerMonth: Number(item.costPerMonth || 0), internetSpeed: Number(item.internetSpeed || 0), safety: item.safety || "很好", liked: item.liked || "很好", temperature: Number(item.temperature || 0), humidity: Number(item.humidity || 0), nomadsNow: Number(item.nomadsNow || 0), description: item.description || "", gradientFrom: item.gradientFrom || "#ff4d4f", gradientTo: item.gradientTo || "#ff7a45", icon: item.icon || "📍", tags: Array.isArray(item.tags) ? item.tags : [], } as RecommendedCity; } function formatMeetupDate(date?: string, time?: string): string { if (!date) return time || "时间待定"; const day = date.slice(5, 10).replace("-", "月") + "日"; return time ? `${day} ${time.slice(0, 5)}` : day; } export default function DashboardPage() { const { t } = useTranslation("dashboard"); const [budget, setBudget] = useState(5000); const [internet, setInternet] = useState(50); const [timezone, setTimezone] = useState("UTC+8 中国"); const [visa, setVisa] = useState("任意"); const [climate, setClimate] = useState("mild"); const [selectedTags, setSelectedTags] = useState(["宜居"]); const [cityModal, setCityModal] = useState(null); const [baseCities, setBaseCities] = useState( fallbackCities.map((city, index) => mapCity(city, index)) ); const [recommendedCities, setRecommendedCities] = useState([]); const [meetups, setMeetups] = useState([]); const [routes, setRoutes] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { apiFetch<{ items: RecommendedCity[] }>("/api/cities") .then((data) => { if (data.items?.length) { setBaseCities(data.items.map(mapCity)); } }) .catch(() => setBaseCities(fallbackCities.map((city, index) => mapCity(city, index)))); apiFetch<{ items: Meetup[] }>("/api/meetups") .then((data) => setMeetups((data.items || []).slice(0, 8))) .catch(() => setMeetups([])); apiFetch<{ items: NomadRoute[] }>("/api/routes") .then((data) => setRoutes(data.items || [])) .catch(() => setRoutes([])); }, []); const fallbackRecommendations = useMemo(() => { return [...baseCities] .filter((city) => city.costPerMonth <= budget * 1.25 && city.internetSpeed >= internet * 0.8) .map((city) => ({ ...city, matchScore: Math.round(city.overallScore * 20 + Math.max(0, 20 - city.costPerMonth / 500)), matchReasons: [ city.costPerMonth <= budget ? "预算内" : "略超预算", city.internetSpeed >= internet ? "网络达标" : "需确认住处网络", city.nomadsNow >= 300 ? "社区活跃" : "适合安静工作", ], })) .sort((a, b) => (b.matchScore || 0) - (a.matchScore || 0)) .slice(0, 12); }, [baseCities, budget, internet]); useEffect(() => { let cancelled = false; setLoading(true); apiFetch<{ items: RecommendedCity[] }>("/api/recommendations/next-stop", { method: "POST", body: JSON.stringify({ budget, internet, climate, tags: selectedTags, limit: 12 }), }) .then((data) => { if (!cancelled) { setRecommendedCities((data.items || []).map(mapCity)); } }) .catch(() => { if (!cancelled) setRecommendedCities(fallbackRecommendations); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [budget, climate, fallbackRecommendations, internet, selectedTags]); const displayCities = recommendedCities.length ? recommendedCities : fallbackRecommendations; const bestCity = displayCities[0]; const topCost = Math.max(...displayCities.slice(0, 6).map((city) => city.costPerMonth), 1); const topNomads = Math.max(...displayCities.slice(0, 6).map((city) => city.nomadsNow), 1); const upcomingMeetups = meetups.filter((meetup) => meetup.isUpcoming !== false).slice(0, 6); const toggleTag = (tag: string) => { setSelectedTags((prev) => prev.includes(tag) ? prev.filter((item) => item !== tag) : [...prev, tag] ); }; return (

{t("title")}

{t("subtitle")}

{t("search")}

{loading && ( 正在计算 )}
{displayCities.map((city, index) => (
setCityModal(city)} />
))}
{displayCities.length === 0 && (

{t("noMatch")}

)}

{t("costCurve")}

{displayCities.slice(0, 6).map((city) => (
{city.name} ¥{city.costPerMonth.toLocaleString()}/月
))}

{t("nomadHeatmap")}

{displayCities.slice(0, 6).map((city) => (
{city.icon} {city.name} {city.nomadsNow.toLocaleString()} 人
))}
{routes.length > 0 && (

推荐路线

{routes.length} 条路线
{routes.slice(0, 3).map((route) => (

{route.title}

{route.durationDays} 天 · ¥{route.budget.toLocaleString()}

{route.stops?.join(" → ") || route.description}

))}
)}

{t("nearbyEvents")}

{(upcomingMeetups.length ? upcomingMeetups : meetups).slice(0, 6).map((meetup) => ( {meetup.emoji || "📍"} {meetup.city} {formatMeetupDate(meetup.date, meetup.time)} ))}
setCityModal(null)} />
); }