From 25bcc350639ed28dcf1d6fcb33d892cb953fc7c9 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 20:58:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=A9=E5=BA=A6=E9=93=BE=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20settlement=5Fcurrent=20=E5=85=9C=E5=BA=95=EF=BC=9ACWA/HKO=20?= =?UTF-8?q?=E7=AD=89=E5=AE=98=E6=96=B9=E7=BB=93=E7=AE=97=E7=AB=99=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E4=BC=98=E5=85=88=E4=BA=8E=20METAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scan-terminal/CitySelectorDropdown.tsx | 209 +++++++++++++----- web/analysis_service.py | 13 ++ 2 files changed, 172 insertions(+), 50 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx b/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx index 069e0476..a7193d9b 100644 --- a/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx +++ b/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import type { ScanOpportunityRow } from "@/lib/dashboard-types"; import { REGIONS, getCityRegion } from "./continent-grouping"; -import { rowName, temp } from "./utils"; +import { rowName } from "./utils"; interface CitySelectorDropdownProps { isEn: boolean; @@ -14,6 +14,105 @@ interface CitySelectorDropdownProps { className?: string; } +// Map each city to its airport code (IATA) to match the financial symbol style of Koyfin +const CITY_IATA_MAP: Record = { + beijing: "PEK", + shanghai: "SHA", + shenzhen: "SZX", + guangzhou: "CAN", + chengdu: "CTU", + chongqing: "CKG", + wuhan: "WUH", + taipei: "TPE", + "hong kong": "HKG", + tokyo: "HND", + seoul: "ICN", + singapore: "SIN", + "kuala lumpur": "KUL", + manila: "MNL", + jakarta: "CGK", + karachi: "KHI", + lucknow: "LKO", + london: "LHR", + paris: "CDG", + munich: "MUC", + milan: "MXP", + madrid: "MAD", + amsterdam: "AMS", + warsaw: "WAW", + helsinki: "HEL", + "cape town": "CPT", + jeddah: "JED", + toronto: "YYZ", + "new york": "LGA", + "los angeles": "LAX", + "san francisco": "SFO", + denver: "DEN", + austin: "AUS", + houston: "HOU", + dallas: "DAL", + miami: "MIA", + atlanta: "ATL", + seattle: "SEA", + "mexico city": "MEX", + "panama city": "PAC", + "buenos aires": "EZE", + "sao paulo": "GRU", + wellington: "WLG", +}; + +// Map each city to its ICAO code to prevent referencing non-existent field row.icao in TypeScript +const CITY_ICAO_MAP: Record = { + beijing: "ZBAA", + shanghai: "ZSPD", + shenzhen: "LFS", + guangzhou: "ZGGG", + chengdu: "ZUUU", + chongqing: "ZUCK", + wuhan: "ZHHH", + taipei: "RCSS", + "hong kong": "VHHH", + tokyo: "RJTT", + seoul: "RKSI", + singapore: "WSSS", + "kuala lumpur": "WMKK", + manila: "RPLL", + jakarta: "WIHH", + karachi: "OPKC", + lucknow: "VILK", + london: "EGLC", + paris: "LFPB", + munich: "EDDM", + milan: "LIMC", + madrid: "LEMD", + amsterdam: "EHAM", + warsaw: "EPWA", + helsinki: "EFHK", + "cape town": "FACT", + jeddah: "OEJN", + toronto: "CYYZ", + "new york": "KLGA", + "los angeles": "KLAX", + "san francisco": "KSFO", + denver: "KBKF", + austin: "KAUS", + houston: "KHOU", + dallas: "KDAL", + miami: "KMIA", + atlanta: "KATL", + seattle: "KSEA", + "mexico city": "MMMX", + "panama city": "MPMG", + "buenos aires": "SAEZ", + "sao paulo": "SBGR", + wellington: "NZWN", +}; + +const getCityCode = (city: string): string => { + const normalized = String(city || "").toLowerCase().trim(); + return CITY_IATA_MAP[normalized] || normalized.substring(0, 3).toUpperCase(); +}; + export function CitySelectorDropdown({ isEn, rows, @@ -53,13 +152,13 @@ export function CitySelectorDropdown({ }; }, [onClose]); - // Tab definitions + // Tab definitions matching the Koyfin pill row style const tabs = useMemo(() => { return [ { key: "all", labelEn: "ALL", labelZh: "全部" }, ...REGIONS.map((r) => ({ key: r.key, - labelEn: r.labelEn.replace("Asia", "").replace("America", "").trim() || r.labelEn, + labelEn: r.labelEn.replace("Asia", "").replace("America", "").trim().toUpperCase() || r.labelEn, labelZh: r.labelZh, })), ]; @@ -77,11 +176,16 @@ export function CitySelectorDropdown({ // 2. Query filter if (!q) return true; + const key = String(row.city || "").toLowerCase().trim(); + const code = getCityCode(row.city || ""); + const icao = CITY_ICAO_MAP[key] || ""; const haystack = [ row.city, row.city_display_name, row.display_name, row.airport, + icao, + code, ] .filter(Boolean) .map((s) => s!.toLowerCase()); @@ -89,36 +193,35 @@ export function CitySelectorDropdown({ }); }, [rows, searchQuery, activeTab]); + const getRegionLabel = (regionKey: string): string => { + const match = REGIONS.find((r) => r.key === regionKey); + if (!match) return regionKey; + return isEn ? match.labelEn : match.labelZh; + }; + return (
e.stopPropagation()} // Prevent card activation + onClick={(e) => e.stopPropagation()} // Prevent triggering slot clicks > {/* Search Input Area */} -
+
setSearchQuery(e.target.value)} - placeholder={isEn ? "Search city or airport..." : "搜索城市、机场..."} - className="w-full px-2.5 py-1.5 border border-slate-300 rounded bg-white text-xs outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20" + placeholder="Search..." + className="w-full px-2.5 py-1.5 border border-[#3b82f6] rounded bg-white text-xs outline-none ring-2 ring-blue-500/10 focus:border-blue-500" /> -
- {/* Region filter tabs */} -
+ {/* Koyfin-style Category Pills Bar */} +
{tabs.map((tab) => { const isActive = activeTab === tab.key; return ( @@ -127,10 +230,10 @@ export function CitySelectorDropdown({ type="button" onClick={() => setActiveTab(tab.key)} className={clsx( - "px-2 py-0.5 text-[10px] font-bold rounded whitespace-nowrap transition-all", + "px-2 py-0.5 text-[9px] font-bold rounded transition-all uppercase tracking-wider", isActive - ? "bg-blue-600 text-white shadow-sm" - : "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700" + ? "bg-[#0070f3] text-white shadow-sm" + : "text-slate-500 hover:text-slate-800 hover:bg-slate-200/50" )} > {isEn ? tab.labelEn : tab.labelZh} @@ -139,8 +242,8 @@ export function CitySelectorDropdown({ })}
- {/* Scrollable list */} -
+ {/* Results List */} +
{filteredRows.length === 0 ? (
{isEn ? "No matching cities" : "无匹配城市"} @@ -151,45 +254,51 @@ export function CitySelectorDropdown({ const obsTemp = row.current_temp ?? row.current_max_so_far; const debPrediction = row.deb_prediction; const symbol = row.temp_symbol || "°C"; + const regionKey = getCityRegion(row) || "unknown"; + const regionLabel = getRegionLabel(regionKey); + const cityKey = String(row.city || "").toLowerCase().trim(); return ( ); diff --git a/web/analysis_service.py b/web/analysis_service.py index 8e5416cf..9712f328 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -825,6 +825,14 @@ def _analyze( cur_temp = _sf(live_mc.get("temp")) if cur_temp is not None and not _is_plausible_city_temp(city, cur_temp, sym): cur_temp = None + # Official settlement station: e.g. CWA for Taipei, HKO for Hong Kong + if cur_temp is None: + cur_temp = _sf((settlement_current.get("current") or {}).get("temp")) + if cur_temp is not None: + current_source = settlement_source or "settlement" + current_source_label = settlement_source_label or "Settlement Station" + current_station_code = settlement_current.get("station_code") or current_station_code + current_station_name = settlement_current.get("station_name") or current_station_name if cur_temp is None: cur_temp = _sf(mg_cur.get("temp")) if cur_temp is not None and not _is_plausible_city_temp(city, cur_temp, sym): @@ -1837,6 +1845,11 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]: cur_temp = _sf(live_mc.get("temp")) if cur_temp is not None and not _is_plausible_city_temp(city, cur_temp, sym): cur_temp = None + if cur_temp is None: + cur_temp = _sf((settlement_current.get("current") or {}).get("temp")) + if cur_temp is not None: + current_source = settlement_source or "settlement" + current_source_label = settlement_source_label or "Settlement Station" if cur_temp is None: cur_temp = _sf(mg_cur.get("temp")) if cur_temp is not None and not _is_plausible_city_temp(city, cur_temp, sym):