统一 METAR 获取:非 HK/深圳城市走同一条 NOAA 路径,前端移除所有流浮山字样
This commit is contained in:
@@ -47,6 +47,7 @@ import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-termi
|
|||||||
import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
|
import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
|
||||||
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
|
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
|
||||||
import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown";
|
import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown";
|
||||||
|
import { GridLayoutSelector } from "@/components/dashboard/scan-terminal/GridLayoutSelector";
|
||||||
|
|
||||||
function createEmptyAccess(loading = true): ProAccessState {
|
function createEmptyAccess(loading = true): ProAccessState {
|
||||||
return {
|
return {
|
||||||
@@ -357,20 +358,120 @@ function PolyWeatherTerminal({
|
|||||||
const [navExpanded, setNavExpanded] = useState(false);
|
const [navExpanded, setNavExpanded] = useState(false);
|
||||||
const [activeNavKey, setActiveNavKey] = useState<string>("contracts");
|
const [activeNavKey, setActiveNavKey] = useState<string>("contracts");
|
||||||
|
|
||||||
|
const [gridCols, setGridCols] = useState<number>(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
const val = localStorage.getItem("polyweather_terminal_grid_cols");
|
||||||
|
if (val) {
|
||||||
|
const num = parseInt(val, 10);
|
||||||
|
if (num >= 1 && num <= 3) return num;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [gridRows, setGridRows] = useState<number>(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
const val = localStorage.getItem("polyweather_terminal_grid_rows");
|
||||||
|
if (val) {
|
||||||
|
const num = parseInt(val, 10);
|
||||||
|
if (num >= 1 && num <= 3) return num;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalSlots = gridCols * gridRows;
|
||||||
|
|
||||||
const [slots, setSlots] = useState<Array<string | null>>(() => {
|
const [slots, setSlots] = useState<Array<string | null>>(() => {
|
||||||
try {
|
if (typeof window !== "undefined") {
|
||||||
const stored = localStorage.getItem("polyweather_terminal_slots");
|
try {
|
||||||
if (stored) {
|
const stored = localStorage.getItem("polyweather_terminal_slots");
|
||||||
const parsed = JSON.parse(stored);
|
if (stored) {
|
||||||
if (Array.isArray(parsed) && parsed.length === 4) return parsed;
|
const parsed = JSON.parse(stored);
|
||||||
}
|
if (Array.isArray(parsed)) {
|
||||||
} catch {}
|
let targetLen = 4;
|
||||||
return [null, null, null, null];
|
try {
|
||||||
|
const cVal = localStorage.getItem("polyweather_terminal_grid_cols");
|
||||||
|
const rVal = localStorage.getItem("polyweather_terminal_grid_rows");
|
||||||
|
if (cVal && rVal) {
|
||||||
|
const c = parseInt(cVal, 10);
|
||||||
|
const r = parseInt(rVal, 10);
|
||||||
|
if (c >= 1 && c <= 3 && r >= 1 && r <= 3) {
|
||||||
|
targetLen = c * r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (parsed.length === targetLen) {
|
||||||
|
return parsed;
|
||||||
|
} else if (parsed.length < targetLen) {
|
||||||
|
return [...parsed, ...Array(targetLen - parsed.length).fill(null)];
|
||||||
|
} else {
|
||||||
|
return parsed.slice(0, targetLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
let defaultLen = 4;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
const cVal = localStorage.getItem("polyweather_terminal_grid_cols");
|
||||||
|
const rVal = localStorage.getItem("polyweather_terminal_grid_rows");
|
||||||
|
if (cVal && rVal) {
|
||||||
|
const c = parseInt(cVal, 10);
|
||||||
|
const r = parseInt(rVal, 10);
|
||||||
|
if (c >= 1 && c <= 3 && r >= 1 && r <= 3) {
|
||||||
|
defaultLen = c * r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return Array(defaultLen).fill(null);
|
||||||
});
|
});
|
||||||
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
|
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
|
||||||
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
|
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
|
||||||
const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(null);
|
const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleSetGridSize = (cols: number, rows: number) => {
|
||||||
|
const nextTotalSlots = cols * rows;
|
||||||
|
|
||||||
|
setGridCols(cols);
|
||||||
|
setGridRows(rows);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem("polyweather_terminal_grid_cols", String(cols));
|
||||||
|
localStorage.setItem("polyweather_terminal_grid_rows", String(rows));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
let nextSlots = [...slots];
|
||||||
|
if (nextSlots.length < nextTotalSlots) {
|
||||||
|
const diff = nextTotalSlots - nextSlots.length;
|
||||||
|
nextSlots = [...nextSlots, ...Array(diff).fill(null)];
|
||||||
|
} else if (nextSlots.length > nextTotalSlots) {
|
||||||
|
nextSlots = nextSlots.slice(0, nextTotalSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSlots(nextSlots);
|
||||||
|
try {
|
||||||
|
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(nextSlots));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (activeSlotIndex >= nextTotalSlots) {
|
||||||
|
setActiveSlotIndex(0);
|
||||||
|
}
|
||||||
|
if (maximizedSlotIndex !== null && maximizedSlotIndex >= nextTotalSlots) {
|
||||||
|
setMaximizedSlotIndex(null);
|
||||||
|
}
|
||||||
|
if (activeSearchSlotIndex !== null && activeSearchSlotIndex >= nextTotalSlots) {
|
||||||
|
setActiveSearchSlotIndex(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" },
|
{ key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" },
|
||||||
{ key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" },
|
{ key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" },
|
||||||
@@ -390,18 +491,15 @@ function PolyWeatherTerminal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredRegionRows.length && slots.every((s) => s === null)) {
|
if (filteredRegionRows.length && slots.every((s) => s === null)) {
|
||||||
const next = [
|
const next = Array(totalSlots)
|
||||||
filteredRegionRows[0]?.city || null,
|
.fill(null)
|
||||||
filteredRegionRows[1]?.city || null,
|
.map((_, idx) => filteredRegionRows[idx]?.city || null);
|
||||||
filteredRegionRows[2]?.city || null,
|
|
||||||
filteredRegionRows[3]?.city || null,
|
|
||||||
];
|
|
||||||
setSlots(next);
|
setSlots(next);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
|
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}, [filteredRegionRows, slots]);
|
}, [filteredRegionRows, slots, totalSlots]);
|
||||||
|
|
||||||
const handleSelectCityForSlot = (index: number, city: string | null) => {
|
const handleSelectCityForSlot = (index: number, city: string | null) => {
|
||||||
const next = [...slots];
|
const next = [...slots];
|
||||||
@@ -612,6 +710,14 @@ function PolyWeatherTerminal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
<span className="hidden font-mono md:inline text-slate-500">{userLocalTime}</span>
|
<span className="hidden font-mono md:inline text-slate-500">{userLocalTime}</span>
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<GridLayoutSelector
|
||||||
|
isEn={isEn}
|
||||||
|
cols={gridCols}
|
||||||
|
rows={gridRows}
|
||||||
|
onSelectGrid={handleSetGridSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleLocale}
|
onClick={toggleLocale}
|
||||||
@@ -743,9 +849,15 @@ function PolyWeatherTerminal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 2x2 grid layout
|
// Custom grid layout
|
||||||
<div className="grid grid-cols-2 grid-rows-2 gap-2 h-full">
|
<div
|
||||||
{[0, 1, 2, 3].map((slotIndex) => {
|
className="grid gap-2 h-full"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
||||||
|
gridTemplateRows: `repeat(${gridRows}, minmax(0, 1fr))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: totalSlots }).map((_, slotIndex) => {
|
||||||
const isSlotActive = activeSlotIndex === slotIndex;
|
const isSlotActive = activeSlotIndex === slotIndex;
|
||||||
const cityInSlot = slots[slotIndex];
|
const cityInSlot = slots[slotIndex];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { LayoutGrid } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
interface GridLayoutSelectorProps {
|
||||||
|
isEn: boolean;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
onSelectGrid: (cols: number, rows: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GridLayoutSelector({
|
||||||
|
isEn,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
onSelectGrid,
|
||||||
|
}: GridLayoutSelectorProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [hoveredCols, setHoveredCols] = useState(0);
|
||||||
|
const [hoveredRows, setHoveredRows] = useState(0);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Handle click outside to close dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCellClick = (c: number, r: number) => {
|
||||||
|
onSelectGrid(c, r);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewCols = hoveredCols > 0 ? hoveredCols : cols;
|
||||||
|
const previewRows = hoveredRows > 0 ? hoveredRows : rows;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative z-30">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
className="flex items-center gap-1.5 h-7 rounded border border-slate-300 bg-white px-2.5 text-[11px] font-bold text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors shadow-sm outline-none"
|
||||||
|
title={isEn ? "Grid Layout" : "图表网格布局"}
|
||||||
|
>
|
||||||
|
<LayoutGrid size={13} className="text-slate-500" />
|
||||||
|
<span>
|
||||||
|
{isEn ? `Layout: ${cols}x${rows}` : `布局: ${cols}x${rows}`}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute right-0 top-8 w-[150px] bg-white border border-slate-200 rounded shadow-2xl p-3 animate-in fade-in-50 zoom-in-95 duration-100">
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-3 gap-1 mb-2.5 mx-auto w-[90px]"
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredCols(0);
|
||||||
|
setHoveredRows(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[1, 2, 3].map((r) =>
|
||||||
|
[1, 2, 3].map((c) => {
|
||||||
|
const isHighlighted = c <= previewCols && r <= previewRows;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${r}-${c}`}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHoveredCols(c);
|
||||||
|
setHoveredRows(r);
|
||||||
|
}}
|
||||||
|
onClick={() => handleCellClick(c, r)}
|
||||||
|
className={clsx(
|
||||||
|
"h-6 w-6 rounded-sm border cursor-pointer transition-colors",
|
||||||
|
isHighlighted
|
||||||
|
? "bg-blue-500 border-blue-600"
|
||||||
|
: "bg-slate-100 border-slate-200 hover:bg-slate-200"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-[10px] font-bold text-center text-slate-500 font-sans tracking-wide uppercase border-t border-slate-100 pt-1.5">
|
||||||
|
{previewCols} × {previewRows}{" "}
|
||||||
|
{isEn
|
||||||
|
? previewCols * previewRows === 1
|
||||||
|
? "Chart"
|
||||||
|
: "Charts"
|
||||||
|
: "图表"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -842,12 +842,9 @@ export function LiveTemperatureThresholdChart({
|
|||||||
}) {
|
}) {
|
||||||
const [hourly, setHourly] = useState<HourlyForecast>(null);
|
const [hourly, setHourly] = useState<HourlyForecast>(null);
|
||||||
const city = String(row?.city || "").toLowerCase().trim();
|
const city = String(row?.city || "").toLowerCase().trim();
|
||||||
const [timeframe, setTimeframe] = useState<"1D" | "3D" | "5D" | "7D">("1D");
|
const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D");
|
||||||
const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState<Set<string>>(new Set());
|
const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState<Set<string>>(new Set());
|
||||||
|
const [prevCityAndTf, setPrevCityAndTf] = useState({ city: "", timeframe: "" });
|
||||||
useEffect(() => {
|
|
||||||
setHiddenSeriesKeys(new Set());
|
|
||||||
}, [timeframe]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city) return;
|
if (!city) return;
|
||||||
@@ -892,12 +889,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
if (timeframe === "3D") {
|
if (timeframe === "3D") {
|
||||||
return build3DayChartData(row, hourly);
|
return build3DayChartData(row, hourly);
|
||||||
}
|
}
|
||||||
if (timeframe === "5D") {
|
|
||||||
return buildDailyChartData(row, hourly, 5);
|
|
||||||
}
|
|
||||||
if (timeframe === "7D") {
|
|
||||||
return buildDailyChartData(row, hourly, 7);
|
|
||||||
}
|
|
||||||
return buildFullDayChartData(row, hourly);
|
return buildFullDayChartData(row, hourly);
|
||||||
}, [row, hourly, timeframe]);
|
}, [row, hourly, timeframe]);
|
||||||
|
|
||||||
@@ -911,11 +902,36 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const settlementPlate = useMemo(() => runwayPlates.find((p) => p.isSettlement), [runwayPlates]);
|
const settlementPlate = useMemo(() => runwayPlates.find((p) => p.isSettlement), [runwayPlates]);
|
||||||
|
|
||||||
const chartSeries = useMemo(() => {
|
const chartSeries = useMemo(() => {
|
||||||
if (timeframe !== "1D") {
|
return series;
|
||||||
return series;
|
}, [series]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentKey = `${city}-${timeframe}`;
|
||||||
|
const prevKey = `${prevCityAndTf.city}-${prevCityAndTf.timeframe}`;
|
||||||
|
|
||||||
|
if (currentKey !== prevKey) {
|
||||||
|
const defaults = new Set<string>();
|
||||||
|
// Always default all model curves to hidden across all cities and timeframes
|
||||||
|
series.forEach((s) => {
|
||||||
|
if (s.key.startsWith("model_curve_")) {
|
||||||
|
defaults.add(s.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setHiddenSeriesKeys(defaults);
|
||||||
|
setPrevCityAndTf({ city, timeframe });
|
||||||
|
} else {
|
||||||
|
setHiddenSeriesKeys((prev) => {
|
||||||
|
const next = new Set<string>();
|
||||||
|
const seriesKeys = new Set(series.map((s) => s.key));
|
||||||
|
prev.forEach((k) => {
|
||||||
|
if (seriesKeys.has(k)) {
|
||||||
|
next.add(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return series.filter((item) => !hasRunwayData || !item.key.startsWith("model_curve_"));
|
}, [city, timeframe, series, prevCityAndTf]);
|
||||||
}, [series, hasRunwayData, timeframe]);
|
|
||||||
|
|
||||||
const activeSeries = useMemo(() => {
|
const activeSeries = useMemo(() => {
|
||||||
return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key));
|
return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key));
|
||||||
@@ -1045,7 +1061,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
const timeframeActions = (
|
const timeframeActions = (
|
||||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||||
{(["1D", "3D", "5D", "7D"] as const).map((tf) => (
|
{(["1D", "3D"] as const).map((tf) => (
|
||||||
<button
|
<button
|
||||||
key={tf}
|
key={tf}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1358,10 +1374,10 @@ export function LiveTemperatureThresholdChart({
|
|||||||
dataKey={item.key}
|
dataKey={item.key}
|
||||||
name={item.label}
|
name={item.label}
|
||||||
stroke={item.color}
|
stroke={item.color}
|
||||||
strokeWidth={item.featured ? 2 : 1.2}
|
strokeWidth={item.featured ? 2.8 : 1.2}
|
||||||
strokeDasharray={item.dashed ? "4 3" : undefined}
|
strokeDasharray={item.dashed ? "4 3" : undefined}
|
||||||
dot={timeframe === "5D" || timeframe === "7D"}
|
dot={false}
|
||||||
activeDot={{ r: item.featured ? 5 : 4 }}
|
activeDot={{ r: item.featured ? 6 : 4 }}
|
||||||
connectNulls={true}
|
connectNulls={true}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const CITY_NAME_ZH: Record<string, string> = {
|
|||||||
Jakarta: "雅加达",
|
Jakarta: "雅加达",
|
||||||
Jeddah: "吉达",
|
Jeddah: "吉达",
|
||||||
Karachi: "卡拉奇",
|
Karachi: "卡拉奇",
|
||||||
"shenzhen": "深圳流浮山",
|
"shenzhen": "深圳",
|
||||||
London: "伦敦",
|
London: "伦敦",
|
||||||
Lucknow: "勒克瑙",
|
Lucknow: "勒克瑙",
|
||||||
Madrid: "马德里",
|
Madrid: "马德里",
|
||||||
@@ -77,7 +77,7 @@ const AIRPORT_NAME_ZH: Record<string, string> = {
|
|||||||
Jakarta: "苏加诺-哈达国际机场",
|
Jakarta: "苏加诺-哈达国际机场",
|
||||||
Jeddah: "阿卜杜勒-阿齐兹国王国际机场",
|
Jeddah: "阿卜杜勒-阿齐兹国王国际机场",
|
||||||
Karachi: "真纳国际机场",
|
Karachi: "真纳国际机场",
|
||||||
"shenzhen": "深圳流浮山监测站",
|
"shenzhen": "深圳监测站",
|
||||||
London: "希思罗机场",
|
London: "希思罗机场",
|
||||||
Lucknow: "乔杜里·查兰·辛格国际机场",
|
Lucknow: "乔杜里·查兰·辛格国际机场",
|
||||||
Madrid: "马德里-巴拉哈斯机场",
|
Madrid: "马德里-巴拉哈斯机场",
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
|
|||||||
kind: "metar",
|
kind: "metar",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "深圳流浮山站(HKO)",
|
label: "深圳站(HKO)",
|
||||||
href: "https://www.hko.gov.hk/sc/wxinfo/ts/index.htm",
|
href: "https://www.hko.gov.hk/sc/wxinfo/ts/index.htm",
|
||||||
kind: "agency",
|
kind: "agency",
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
|
|||||||
kind: "agency",
|
kind: "agency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "深圳流浮山站(HKO)",
|
label: "深圳站(HKO)",
|
||||||
href: "https://www.hko.gov.hk/sc/wxinfo/ts/index.htm",
|
href: "https://www.hko.gov.hk/sc/wxinfo/ts/index.htm",
|
||||||
kind: "airport",
|
kind: "airport",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ CITY_REGISTRY = {
|
|||||||
"icao": "LFS",
|
"icao": "LFS",
|
||||||
"settlement_source": "hko",
|
"settlement_source": "hko",
|
||||||
"settlement_station_code": "LFS",
|
"settlement_station_code": "LFS",
|
||||||
"settlement_station_label": "Lau Fau Shan",
|
"settlement_station_label": "Shenzhen (HKO)",
|
||||||
"settlement_station_candidates": ["Lau Fau Shan"],
|
"settlement_station_candidates": ["Lau Fau Shan"],
|
||||||
"disable_aviationweather": True,
|
"disable_aviationweather": True,
|
||||||
"tz_offset": 28800,
|
"tz_offset": 28800,
|
||||||
@@ -149,9 +149,9 @@ CITY_REGISTRY = {
|
|||||||
"is_major": True,
|
"is_major": True,
|
||||||
"risk_level": "medium",
|
"risk_level": "medium",
|
||||||
"risk_emoji": "🟡",
|
"risk_emoji": "🟡",
|
||||||
"airport_name": "流浮山天文台站",
|
"airport_name": "深圳(HKO 天文台站)",
|
||||||
"distance_km": 0.0,
|
"distance_km": 0.0,
|
||||||
"warning": "香港天文台流浮山站分钟级观测,深圳温度市场结算源;非机场 METAR,不可与 ZGSZ/VHHH 报文混用。",
|
"warning": "深圳温度市场结算按香港天文台(HKO)站口径,通过流浮山分钟级观测。非机场 METAR,不可与宝安机场 ZGSZ/VHHH 报文混用。",
|
||||||
},
|
},
|
||||||
"taipei": {
|
"taipei": {
|
||||||
"name": "Taipei",
|
"name": "Taipei",
|
||||||
|
|||||||
Reference in New Issue
Block a user