Files
2026-06-06 04:29:46 -05:00

142 lines
5.1 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { useTheme } from "@/app/context/ThemeContext";
import { useLocale, useTranslation } from "@/i18n/navigation";
import { Link } from "@/i18n/navigation";
import { MembersMap, BarChart, PieChart, LineChart } from "@/app/lib/charts";
import { MAP_CITIES } from "@/app/data/map-cities";
import { apiFetch } from "@/app/lib/api-client";
function regionOf(city: { lat: number; lng: number }): string {
if (city.lng >= 118 && city.lat >= 28) return "华东";
if (city.lng >= 110 && city.lat < 28) return "华南";
if (city.lng < 110 && city.lat < 32) return "西南";
if (city.lat >= 34) return "华北";
return "其他";
}
export default function DataReportPage() {
const { t } = useTranslation("dataReport");
const locale = useLocale();
const { theme } = useTheme();
const dark = theme === "dark";
const [mapCities, setMapCities] = useState(MAP_CITIES);
useEffect(() => {
apiFetch<{ items: typeof MAP_CITIES }>("/api/stats/member-map")
.then((data) => {
if (data.items?.length) setMapCities(data.items);
})
.catch(() => setMapCities(MAP_CITIES));
}, []);
const cityBarData = useMemo(
() =>
[...mapCities]
.sort((a, b) => b.nomadsNow - a.nomadsNow)
.slice(0, 10)
.map((c) => ({ name: locale === "zh" ? c.name : c.nameEn, value: c.nomadsNow })),
[locale, mapCities]
);
const regionPieData = useMemo(() => {
const totals = new Map<string, number>();
mapCities.forEach((city) => {
const region = regionOf(city);
totals.set(region, (totals.get(region) || 0) + city.nomadsNow);
});
return Array.from(totals.entries()).map(([name, value]) => ({ name, value }));
}, [mapCities]);
const growthLineData = useMemo(() => {
const totalNomads = mapCities.reduce((sum, city) => sum + city.nomadsNow, 0);
const totalCities = mapCities.length;
return {
xAxis: ["1月", "2月", "3月", "4月", "5月", "6月"],
series: [
{
name: "活跃成员",
data: [0.62, 0.68, 0.74, 0.82, 0.91, 1].map((ratio) => Math.round(totalNomads * ratio)),
},
{
name: "活跃城市",
data: [0.66, 0.72, 0.78, 0.86, 0.94, 1].map((ratio) => Math.max(1, Math.round(totalCities * ratio))),
},
],
};
}, [mapCities]);
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 py-8 sm:py-12">
<header className="mb-8 flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div>
<Link
href="/report"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-2"
>
← {t("backToReport")}
</Link>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<span className="w-1 h-8 bg-[#ff4d4f] rounded-full" />
{t("title")}
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm sm:text-base">
{t("subtitle")}
</p>
</div>
</header>
<div className="space-y-8">
{/* 成员地图 */}
<section className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 overflow-hidden p-4 sm:p-6">
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">
{t("membersMap")}
</h2>
<div className="rounded-xl overflow-hidden bg-gray-50 dark:bg-gray-800/50">
<MembersMap
data={mapCities}
locale={locale}
dark={dark}
className="min-h-[400px]"
/>
</div>
</section>
{/* 图表网格 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<section className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-4 sm:p-6">
<BarChart
data={cityBarData}
title={t("cityRanking")}
dark={dark}
/>
</section>
<section className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-4 sm:p-6">
<PieChart
data={regionPieData}
title={t("regionDist")}
dark={dark}
/>
</section>
</div>
<section className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-4 sm:p-6">
<LineChart
data={growthLineData}
title={t("growthTrend")}
dark={dark}
/>
</section>
</div>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-8 text-center">
城市分布来自本机 FastAPI 与 PocketBase 的 cities 数据表。
</p>
</main>
</div>
);
}