feat: implement PolyWeather dashboard with map UI, data collection, analysis, and comprehensive documentation.

This commit is contained in:
2569718930@qq.com
2026-03-10 09:02:56 +08:00
parent aab4477ab3
commit 396c373cba
18 changed files with 2398 additions and 634 deletions
+67 -10
View File
@@ -3,6 +3,7 @@
import {
CityDetail,
CityListItem,
MarketScan,
CitySummary,
HistoryPoint,
} from "@/lib/dashboard-types";
@@ -12,6 +13,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
const pendingCityDetailRequests = new Map<string, Promise<CityDetail>>();
const pendingHistoryRequests = new Map<string, Promise<HistoryPoint[]>>();
const pendingCitySummaryRequests = new Map<string, Promise<CitySummary>>();
const pendingMarketScanRequests = new Map<string, Promise<MarketScan | null>>();
type CityCacheMeta = {
cachedAt: number;
@@ -134,20 +136,75 @@ export const dashboardClient = {
async getCityDetail(cityName: string, options?: { force?: boolean }) {
const force = options?.force ?? false;
const requestKey = `${cityName}::${force ? "force" : "cached"}`;
const existing = pendingCityDetailRequests.get(requestKey);
if (existing) {
return existing;
if (!force) {
const requestKey = `${cityName}::cached`;
const existing = pendingCityDetailRequests.get(requestKey);
if (existing) {
return existing;
}
const request = fetchJson<CityDetail>(
`/api/city/${normalizeCityName(cityName)}?force_refresh=false`,
).finally(() => {
pendingCityDetailRequests.delete(requestKey);
});
pendingCityDetailRequests.set(requestKey, request);
return request;
}
const request = fetchJson<CityDetail>(
`/api/city/${normalizeCityName(cityName)}?force_refresh=${force}`,
).finally(() => {
pendingCityDetailRequests.delete(requestKey);
const params = new URLSearchParams({
force_refresh: "true",
_ts: String(Date.now()),
});
return fetchJson<CityDetail>(
`/api/city/${normalizeCityName(cityName)}?${params.toString()}`,
);
},
pendingCityDetailRequests.set(requestKey, request);
return request;
async getCityMarketScan(
cityName: string,
options?: { force?: boolean; marketSlug?: string | null },
) {
const force = options?.force ?? false;
const marketSlug = options?.marketSlug || null;
if (!force) {
const requestKey = `${cityName}::cached::${marketSlug || "-"}`;
const existing = pendingMarketScanRequests.get(requestKey);
if (existing) {
return existing;
}
const params = new URLSearchParams({
force_refresh: "false",
});
if (marketSlug) {
params.set("market_slug", marketSlug);
}
const request = fetchJson<{ market_scan?: MarketScan }>(
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
)
.then((data) => data.market_scan || null)
.finally(() => {
pendingMarketScanRequests.delete(requestKey);
});
pendingMarketScanRequests.set(requestKey, request);
return request;
}
const params = new URLSearchParams({
force_refresh: "true",
_ts: String(Date.now()),
});
if (marketSlug) {
params.set("market_slug", marketSlug);
}
return fetchJson<{ market_scan?: MarketScan }>(
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
).then((data) => data.market_scan || null);
},
async getHistory(cityName: string) {
+67
View File
@@ -181,6 +181,71 @@ export interface DailyModelForecast {
probabilities?: ProbabilityBucket[];
}
export interface MarketToken {
outcome?: string | null;
token_id?: string | null;
implied_probability?: number | null;
buy_price?: number | null;
sell_price?: number | null;
midpoint?: number | null;
last_trade_price?: number | null;
}
export interface MarketPrimary {
id?: string | null;
question?: string | null;
slug?: string | null;
condition_id?: string | null;
end_date?: string | null;
active?: boolean;
closed?: boolean;
liquidity?: number | null;
volume?: number | null;
}
export interface MarketTopBucket {
label?: string | null;
value?: number | null;
temp?: number | null;
probability?: number | null;
market_price?: number | null;
yes_buy?: number | null;
yes_sell?: number | null;
no_buy?: number | null;
no_sell?: number | null;
slug?: string | null;
question?: string | null;
is_primary?: boolean;
}
export interface MarketScan {
available?: boolean;
reason?: string | null;
primary_market?: MarketPrimary | null;
selected_date?: string | null;
selected_condition_id?: string | null;
selected_slug?: string | null;
temperature_bucket?: ProbabilityBucket | null;
model_probability?: number | null;
market_price?: number | null;
edge_percent?: number | null;
signal_label?: string | null;
confidence?: string | null;
yes_token?: MarketToken | null;
no_token?: MarketToken | null;
yes_buy?: number | null;
yes_sell?: number | null;
no_buy?: number | null;
no_sell?: number | null;
last_trade_price?: number | null;
liquidity?: number | null;
volume?: number | null;
sparkline?: number[];
top_buckets?: MarketTopBucket[] | null;
recent_trades?: unknown[];
websocket?: Record<string, unknown>;
}
export interface AiAnalysisStructured {
summary?: string | null;
text?: string | null;
@@ -227,6 +292,7 @@ export interface CityDetail {
updated_at?: string;
multi_model_daily?: Record<string, DailyModelForecast>;
source_forecasts?: SourceForecasts;
market_scan?: MarketScan;
}
export interface HistoryPoint {
@@ -241,6 +307,7 @@ export interface LoadingState {
cityDetail: boolean;
refresh: boolean;
history: boolean;
marketScan?: boolean;
}
export interface HistoryState {
+12 -8
View File
@@ -71,7 +71,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.score": "趋势评分",
"future.todayTempTrend": "今日温度走势",
"future.targetTempTrend": "目标日小时走势",
"future.probability": "结算概率分布",
"future.probability": "模型结算概率分布",
"future.models": "多模型预报",
"future.structureToday": "今日日内结构信号",
"future.structureDate": "未来 6-48 小时趋势",
@@ -91,7 +91,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.todayTempTrend": "今日温度走势",
"section.chartEmpty": "暂无小时级数据",
"section.probability": "结算概率分布",
"section.probability": "模型结算概率分布",
"section.mu": "动态分布中心 μ = {value}{unit}",
"section.noProb": "暂无概率数据",
"section.models": "多模型预报",
@@ -133,7 +133,8 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"No scenery image matched. You can still review station and observation profile below.",
"detail.profile": "City Profile",
"detail.todayMiniTrend": "Today's Intraday Trend (Compact)",
"detail.chartLegendEmpty": "No hourly observations or forecast curve available.",
"detail.chartLegendEmpty":
"No hourly observations or forecast curve available.",
"forecast.title": "Multi-day Forecast",
"forecast.empty": "No multi-day forecast available",
@@ -171,7 +172,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.score": "Trend Score",
"future.todayTempTrend": "Today's Temperature Trend",
"future.targetTempTrend": "Target-day Hourly Trend",
"future.probability": "Settlement Probability Distribution",
"future.probability": "Model Settlement Probabilities",
"future.models": "Multi-model Forecast",
"future.structureToday": "Intraday Structural Signal",
"future.structureDate": "6-48h Structural Trend",
@@ -179,11 +180,13 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.confidence": "Confidence",
"future.maxPrecip": "Max Precip Probability",
"future.ai": "AI Deep Analysis",
"future.noAi": "No AI analysis available. Structured meteorological and model data are used as baseline.",
"future.noAi":
"No AI analysis available. Structured meteorological and model data are used as baseline.",
"future.weatherGov": "weather.gov text",
"future.risk": "Settlement & Deviation Risk",
"future.climate": "What Mainly Drives Local Climate",
"future.chartLegendEmpty": "No METAR bulletin or hourly observations available",
"future.chartLegendEmpty":
"No METAR bulletin or hourly observations available",
"confidence.high": "High",
"confidence.medium": "Medium",
@@ -191,13 +194,14 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.todayTempTrend": "Today's Temperature Trend",
"section.chartEmpty": "No hourly data available",
"section.probability": "Settlement Probability Distribution",
"section.probability": "Model Settlement Probabilities",
"section.mu": "Dynamic center μ = {value}{unit}",
"section.noProb": "No probability data available",
"section.models": "Multi-model Forecast",
"section.noModels": "No multi-model forecast available",
"section.ai": "AI Deep Analysis",
"section.aiEmpty": "No AI analysis available. Structured meteorological and model data are currently used.",
"section.aiEmpty":
"No AI analysis available. Structured meteorological and model data are currently used.",
"section.risk": "Data Deviation Risk",
"section.noRiskProfile": "No risk profile available",
"section.airport": "Airport",
+14
View File
@@ -292,6 +292,20 @@ export interface MarketScan {
liquidity: number | null;
volume: number | null;
sparkline: number[];
top_buckets?: Array<{
label?: string | null;
value?: number | null;
temp?: number | null;
probability?: number | null;
market_price?: number | null;
yes_buy?: number | null;
yes_sell?: number | null;
no_buy?: number | null;
no_sell?: number | null;
slug?: string | null;
question?: string | null;
is_primary?: boolean;
}>;
recent_trades: Trade[];
websocket: any;
}