diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index d0cb6719..e7609ce7 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -70,6 +70,12 @@ export function getWindowPhaseMeta( locale: string, ): PhaseMeta { const mode = String(row.window_phase || "").toLowerCase(); + if (mode === "city_snapshot") { + return { + label: locale === "en-US" ? "City Snapshot" : "城市概况", + tone: "blue", + }; + } if (mode === "active_peak") { return { label: locale === "en-US" ? "Peak Window" : "峰值窗口", diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index de1d0de7..c145c302 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -193,6 +193,27 @@ function sortRowsByUserTime(rows: ScanOpportunityRow[]) { }); } +function normalizeCityKey(value?: string | null) { + return String(value || "") + .trim() + .toLowerCase() + .replace(/[\s_-]+/g, ""); +} + +function rowMatchesCity(row: ScanOpportunityRow, cityName: string) { + const cityKey = normalizeCityKey(cityName); + if (!cityKey) return false; + return [row.city, row.city_display_name, row.display_name].some( + (value) => normalizeCityKey(value) === cityKey, + ); +} + +function findRowForCity(rows: ScanOpportunityRow[], cityName?: string | null) { + const normalized = normalizeCityKey(cityName); + if (!normalized) return null; + return rows.find((row) => rowMatchesCity(row, cityName || "")) || null; +} + function getSideRow( marketScan: MarketScan | null | undefined, selectedRow: ScanOpportunityRow, @@ -493,6 +514,9 @@ function DetailPanel({ ); const scoreClass = scoreTone(displayRow.final_score); const phaseMeta = getWindowPhaseMeta(displayRow, locale); + const isCitySnapshot = + String(row.window_phase || "").toLowerCase() === "city_snapshot" || + !row.market_slug; const cityDetail = store.selectedDetail?.name?.toLowerCase() === row.city.toLowerCase() ? store.selectedDetail @@ -664,28 +688,40 @@ function DetailPanel({
{isEn ? "Best Side" : "主方向"} - {displayRow.side === "no" ? "NO" : "YES"} + + {isCitySnapshot + ? isEn + ? "Watch" + : "观察" + : displayRow.side === "no" + ? "NO" + : "YES"} +
{isEn ? "Best Buy" : "最优买价"} - {displayRow.side === "no" - ? formatPrice(getDetailSideAsk(noRow, marketScan, "no")) - : formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))} + {isCitySnapshot + ? "--" + : displayRow.side === "no" + ? formatPrice(getDetailSideAsk(noRow, marketScan, "no")) + : formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))}
{isEn ? "EMOS" : "EMOS 概率"} - {displayRow.side === "no" - ? formatProbability(noRow?.model_probability) - : formatProbability(yesRow?.model_probability)} + {isCitySnapshot + ? "--" + : displayRow.side === "no" + ? formatProbability(noRow?.model_probability) + : formatProbability(yesRow?.model_probability)}
{isEn ? "Edge" : "边际优势"} = 0 ? "positive" : "negative"}> - {formatPercent(displayRow.edge_percent, true)} + {isCitySnapshot ? "--" : formatPercent(displayRow.edge_percent, true)}
@@ -702,8 +738,8 @@ function DetailPanel({
- {Number(displayRow.final_score || 0).toFixed(0)} - /100 + {isCitySnapshot ? "--" : Number(displayRow.final_score || 0).toFixed(0)} + {!isCitySnapshot ? /100 : null}
@@ -1204,17 +1240,100 @@ function ScanTerminalScreen() { }, [timeSortedRows, selectedRowId]); const mapFocusedRow = useMemo(() => { - const cityName = String(mapSelectedCityName || store.selectedCity || "").toLowerCase(); - if (!cityName) return null; - return ( - timeSortedRows.find( - (row) => - row.city.toLowerCase() === cityName || - String(row.city_display_name || row.display_name || "").toLowerCase() === cityName, - ) || null + return findRowForCity( + timeSortedRows, + mapSelectedCityName || store.selectedCity, ); }, [mapSelectedCityName, store.selectedCity, timeSortedRows]); + const mapFallbackRow = useMemo(() => { + const rawCityName = mapSelectedCityName || store.selectedCity; + const cityKey = normalizeCityKey(rawCityName); + if (!cityKey || mapFocusedRow) return null; + const selectedDetail = + store.selectedDetail && normalizeCityKey(store.selectedDetail.name) === cityKey + ? store.selectedDetail + : Object.values(store.cityDetailsByName).find( + (detail) => normalizeCityKey(detail?.name) === cityKey, + ) || null; + const selectedSummary = + Object.values(store.citySummariesByName).find( + (summary) => normalizeCityKey(summary?.name) === cityKey, + ) || null; + const selectedCityItem = + store.cities.find( + (city) => + normalizeCityKey(city.name) === cityKey || + normalizeCityKey(city.display_name) === cityKey, + ) || null; + const canonicalCity = + selectedDetail?.name || + selectedSummary?.name || + selectedCityItem?.name || + String(rawCityName || "").trim(); + if (!canonicalCity) return null; + + const tempSymbol = + selectedDetail?.temp_symbol || + selectedSummary?.temp_symbol || + (selectedCityItem?.temp_unit === "fahrenheit" ? "°F" : "°C"); + const displayName = + selectedDetail?.display_name || + selectedSummary?.display_name || + selectedCityItem?.display_name || + canonicalCity; + const currentTemp = + selectedDetail?.current?.temp ?? selectedSummary?.current?.temp ?? null; + + return { + id: `map-city:${canonicalCity}`, + city: canonicalCity, + city_display_name: displayName, + display_name: displayName, + selected_date: selectedDetail?.local_date || null, + local_date: selectedDetail?.local_date || null, + local_time: selectedDetail?.local_time || selectedSummary?.local_time || null, + temp_symbol: tempSymbol, + current_temp: currentTemp, + current_max_so_far: + selectedDetail?.current?.max_so_far ?? currentTemp ?? null, + deb_prediction: + selectedDetail?.deb?.prediction ?? + selectedSummary?.deb?.prediction ?? + null, + airport: + selectedDetail?.risk?.airport || + selectedCityItem?.airport || + selectedCityItem?.settlement_station_label || + null, + risk_level: + selectedDetail?.risk?.level || + selectedSummary?.risk?.level || + selectedCityItem?.risk_level || + "low", + market_slug: null, + market_question: isEn ? "Current city snapshot" : "当前城市概况", + target_label: isEn ? "City snapshot" : "城市概况", + side: null, + edge_percent: null, + final_score: null, + window_phase: "city_snapshot", + tradable: false, + active: false, + closed: false, + accepting_orders: false, + } satisfies ScanOpportunityRow; + }, [ + isEn, + mapFocusedRow, + mapSelectedCityName, + store.cityDetailsByName, + store.citySummariesByName, + store.cities, + store.selectedCity, + store.selectedDetail, + ]); + const fetchTerminal = async (filters: FilterState, force = false) => { setLoading(true); try { @@ -1293,7 +1412,10 @@ function ScanTerminalScreen() { const resolvedView: ContentView = activeView; const mapFocusedCity = mapSelectedCityName || store.selectedCity; - const activeDetailRow = resolvedView === "map" && mapFocusedCity ? mapFocusedRow : selectedRow; + const activeDetailRow = + resolvedView === "map" && mapFocusedCity + ? mapFocusedRow || mapFallbackRow + : selectedRow; const selectedDetail = activeDetailRow ? detailByRowId[activeDetailRow.id] : null; const scanStatus = terminalData?.status || (loading ? "loading" : error ? "failed" : "ready"); const staleReason = @@ -1309,12 +1431,7 @@ function ScanTerminalScreen() { const handleMapCitySelect = useCallback((cityName: string) => { setMapSelectedCityName(cityName); - const matchedRow = - timeSortedRows.find( - (row) => - row.city.toLowerCase() === cityName.toLowerCase() || - String(row.city_display_name || row.display_name || "").toLowerCase() === cityName.toLowerCase(), - ) || null; + const matchedRow = findRowForCity(timeSortedRows, cityName); setSelectedRowId(matchedRow?.id || null); }, [timeSortedRows]);