+
- {city.temp_unit === "fahrenheit" ? "Fahrenheit" : "Celsius"}
+
+ {city.temp_unit === "fahrenheit"
+ ? text.fahrenheit
+ : text.celsius}
+
))}
diff --git a/frontend/components/map-canvas.tsx b/frontend/components/map-canvas.tsx
index 84dadbff..d55ef4f2 100644
--- a/frontend/components/map-canvas.tsx
+++ b/frontend/components/map-canvas.tsx
@@ -11,20 +11,19 @@ type MapCanvasProps = {
onSelectCity: (cityName: string) => void;
};
-const tempIcon = (risk: CitySummary["risk_level"]) =>
+function shortName(name: string) {
+ return name.length > 8 ? `${name.slice(0, 7)}.` : name;
+}
+
+const tempIcon = (
+ city: CitySummary,
+ selected: boolean,
+) =>
L.divIcon({
className: "",
- html: `
${risk.toUpperCase()}
`,
- iconSize: [56, 28],
- iconAnchor: [28, 14],
+ html: `
${shortName(city.display_name)}
`,
+ iconSize: [78, 30],
+ iconAnchor: [39, 15],
});
export function MapCanvas({ cities, selectedCity, onSelectCity }: MapCanvasProps) {
@@ -38,7 +37,7 @@ export function MapCanvas({ cities, selectedCity, onSelectCity }: MapCanvasProps
onSelectCity(city.name) }}
>
diff --git a/frontend/components/polyweather-dashboard.tsx b/frontend/components/polyweather-dashboard.tsx
index 279eeb65..0a54e44d 100644
--- a/frontend/components/polyweather-dashboard.tsx
+++ b/frontend/components/polyweather-dashboard.tsx
@@ -1,21 +1,32 @@
"use client";
import { useEffect, useMemo, useState } from "react";
-import { AlertTriangle, Bot, Globe2 } from "lucide-react";
+import {
+ AlertTriangle,
+ Bot,
+ Globe2,
+ Languages,
+ Radar,
+ Signal,
+ Waves,
+} from "lucide-react";
import { Button } from "@/components/ui/button";
import { CityDetailPanel } from "@/components/city-detail-panel";
import { CityList } from "@/components/city-list";
import { MapView } from "@/components/map-view";
import { getCities, getCityDetail } from "@/lib/api";
+import { copy, type Locale } from "@/lib/i18n";
import type { CityDetail, CitySummary } from "@/lib/types";
export function PolyWeatherDashboard() {
+ const [locale, setLocale] = useState("zh");
const [cities, setCities] = useState([]);
const [selectedCity, setSelectedCity] = useState(null);
const [selectedDetail, setSelectedDetail] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+ const t = copy[locale];
useEffect(() => {
let mounted = true;
@@ -64,6 +75,14 @@ export function PolyWeatherDashboard() {
);
}, [cities]);
+ const riskStats = useMemo(() => {
+ return {
+ high: cities.filter((c) => c.risk_level === "high").length,
+ medium: cities.filter((c) => c.risk_level === "medium").length,
+ low: cities.filter((c) => c.risk_level === "low").length,
+ };
+ }, [cities]);
+
async function refreshCurrentCity() {
if (!selectedCity) return;
setLoading(true);
@@ -77,35 +96,94 @@ export function PolyWeatherDashboard() {
}
}
+ function localizeError(raw: string) {
+ if (raw.includes("POLYWEATHER_API_BASE_URL")) {
+ return t.backendConfigMissing;
+ }
+ if (raw.toLowerCase().includes("failed to load cities")) {
+ return `${t.loadCitiesFailed}: ${raw}`;
+ }
+ if (raw.toLowerCase().includes("failed to load city detail")) {
+ return `${t.loadCityDetailFailed}: ${raw}`;
+ }
+ return raw;
+ }
+
return (
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
PolyWeather
-
- Global Weather Risk Intelligence
+
+ {t.brandSubtitle}
-
+
+
+
+ {t.live}
+
+
+
+
+
+
+
+
+ H {riskStats.high}
+
+
+ M {riskStats.medium}
+
+
+ L {riskStats.low}
+
+
+
+ {cities.length} {t.cities}
+
+
+
+ AI + DEB
+
+
+
+
-
@@ -126,7 +238,7 @@ export function PolyWeatherDashboard() {
-
{error}
+
{localizeError(error)}
) : null}
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
index bdfa89c2..184a478a 100644
--- a/frontend/lib/api.ts
+++ b/frontend/lib/api.ts
@@ -1,9 +1,20 @@
import type { CityDetail, CitySummary } from "@/lib/types";
+async function readError(res: Response, fallback: string) {
+ try {
+ const data = await res.json();
+ const msg = data?.error ? String(data.error) : fallback;
+ const detail = data?.detail ? ` ${String(data.detail)}` : "";
+ return `${msg}${detail}`.trim();
+ } catch {
+ return `${fallback}: ${res.status}`;
+ }
+}
+
export async function getCities(): Promise
{
const res = await fetch("/api/cities", { cache: "no-store" });
if (!res.ok) {
- throw new Error(`Failed to load cities: ${res.status}`);
+ throw new Error(await readError(res, `Failed to load cities (${res.status})`));
}
const data = await res.json();
return data.cities ?? [];
@@ -21,7 +32,9 @@ export async function getCityDetail(
},
);
if (!res.ok) {
- throw new Error(`Failed to load city detail: ${res.status}`);
+ throw new Error(
+ await readError(res, `Failed to load city detail (${res.status})`),
+ );
}
return await res.json();
}
diff --git a/frontend/lib/i18n.ts b/frontend/lib/i18n.ts
new file mode 100644
index 00000000..2706ea08
--- /dev/null
+++ b/frontend/lib/i18n.ts
@@ -0,0 +1,55 @@
+export type Locale = "zh" | "en";
+
+export const copy = {
+ en: {
+ brandSubtitle: "Global Weather Risk Intelligence",
+ technicalGuide: "Technical Guide",
+ monitoredCities: "Monitored Cities",
+ cityDetail: "City Detail",
+ selectCityHint: "Select a city from the left list or map marker.",
+ live: "Live",
+ cities: "Cities",
+ currentMax: "Current / Max",
+ cloud: "Cloud",
+ wind: "Wind",
+ topProb: "Top Settlement Probabilities",
+ noProb: "No probability data yet.",
+ aiSummary: "AI Summary",
+ noAnalysis: "No analysis yet.",
+ refresh: "Refresh",
+ fahrenheit: "Fahrenheit",
+ celsius: "Celsius",
+ high: "High",
+ medium: "Med",
+ low: "Low",
+ loadCitiesFailed: "Failed to load cities",
+ loadCityDetailFailed: "Failed to load city detail",
+ backendConfigMissing:
+ "Server config missing: POLYWEATHER_API_BASE_URL is not set.",
+ },
+ zh: {
+ brandSubtitle: "全球天气风险智能分析",
+ technicalGuide: "技术说明",
+ monitoredCities: "监控城市",
+ cityDetail: "城市详情",
+ selectCityHint: "请从左侧列表或地图标记选择城市。",
+ live: "实时",
+ cities: "城市",
+ currentMax: "当前 / 最高",
+ cloud: "云量",
+ wind: "风速",
+ topProb: "结算概率Top3",
+ noProb: "暂无概率数据",
+ aiSummary: "AI 分析",
+ noAnalysis: "暂无分析",
+ refresh: "刷新",
+ fahrenheit: "华氏",
+ celsius: "摄氏",
+ high: "高",
+ medium: "中",
+ low: "低",
+ loadCitiesFailed: "城市列表加载失败",
+ loadCityDetailFailed: "城市详情加载失败",
+ backendConfigMissing: "服务端未配置 POLYWEATHER_API_BASE_URL。",
+ },
+} as const;