feat: Implement initial PolyWeather application with interactive map UI, backend API, and Polymarket data client.

This commit is contained in:
2569718930@qq.com
2026-03-06 09:38:28 +08:00
parent b308709edb
commit d9876256b3
18 changed files with 755 additions and 1097 deletions
-40
View File
@@ -1,40 +0,0 @@
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<CitySummary[]> {
const res = await fetch("/api/cities", { cache: "no-store" });
if (!res.ok) {
throw new Error(await readError(res, `Failed to load cities (${res.status})`));
}
const data = await res.json();
return data.cities ?? [];
}
export async function getCityDetail(
cityName: string,
forceRefresh = false,
): Promise<CityDetail> {
const slug = cityName.replace(/\s/g, "-");
const res = await fetch(
`/api/city/${encodeURIComponent(slug)}?force_refresh=${forceRefresh}`,
{
cache: "no-store",
},
);
if (!res.ok) {
throw new Error(
await readError(res, `Failed to load city detail (${res.status})`),
);
}
return await res.json();
}
-55
View File
@@ -1,55 +0,0 @@
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;
-35
View File
@@ -1,35 +0,0 @@
export type CitySummary = {
name: string;
display_name: string;
lat: number;
lon: number;
risk_level: "low" | "medium" | "high";
risk_emoji?: string;
temp_unit: "fahrenheit" | "celsius";
is_major?: boolean;
};
export type CityDetail = {
name: string;
display_name: string;
lat: number;
lon: number;
temp_symbol: string;
local_time: string;
current?: {
temp?: number | null;
max_so_far?: number | null;
wu_settlement?: number | null;
cloud_desc?: string | null;
wind_speed_kt?: number | null;
obs_time?: string | null;
};
deb?: {
prediction?: number | null;
};
probabilities?: {
mu?: number | null;
distribution?: Array<{ value: number; probability: number }>;
};
ai_analysis?: string;
};
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}