142 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|