天气合约市场改为按城市分组折叠,展开显示所有温度档位

This commit is contained in:
2569718930@qq.com
2026-05-25 05:19:44 +08:00
parent a2da66ca5e
commit 68e3744650
@@ -4,7 +4,9 @@ import clsx from "clsx";
import Link from "next/link";
import {
Activity,
ChevronDown,
ChevronLeft,
ChevronRight,
GraduationCap,
LineChart,
Menu,
@@ -12,7 +14,7 @@ import {
Table2,
UserRound,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import type { ProAccessState, ScanOpportunityRow } from "@/lib/dashboard-types";
import { getInitialLocaleFromNavigator } from "@/lib/i18n";
import { isBrowserLocalFullAccess } from "@/lib/local-dev-access";
@@ -267,7 +269,7 @@ function KoyfinMarketPanel({
}) {
return (
<Panel title={title}>
<KoyfinRowsTable
<CityGroupedTable
compact={compact}
isEn={isEn}
onSelect={onSelect}
@@ -278,6 +280,146 @@ function KoyfinMarketPanel({
);
}
function CityGroupedTable({
compact,
isEn,
onSelect,
rows,
selectedId,
}: {
compact?: boolean;
isEn: boolean;
onSelect: (row: ScanOpportunityRow) => void;
rows: ScanOpportunityRow[];
selectedId?: string | null;
}) {
const [expandedCities, setExpandedCities] = useState<Set<string>>(new Set());
const cityGroups = useMemo(() => {
const map = new Map<string, ScanOpportunityRow[]>();
rows.forEach((row) => {
const key = String(row.city || "").toLowerCase();
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(row);
});
return Array.from(map.entries())
.map(([city, cityRows]) => {
const best = cityRows.reduce((a, b) =>
Math.abs(Number(b.edge_percent || 0)) > Math.abs(Number(a.edge_percent || 0)) ? b : a
);
const buckets = [...cityRows].sort((a, b) =>
Number(b.edge_percent || 0) - Number(a.edge_percent || 0)
);
return { city, displayName: rowName(best), best, buckets };
})
.sort((a, b) =>
Math.abs(Number(b.best.edge_percent || 0)) - Math.abs(Number(a.best.edge_percent || 0))
);
}, [rows]);
const toggle = (city: string) => {
setExpandedCities((prev) => {
const next = new Set(prev);
if (next.has(city)) next.delete(city);
else next.add(city);
return next;
});
};
const labelExpand = isEn ? "Expand" : "展开";
const labelBuckets = isEn ? "buckets" : "档位";
return (
<div className="overflow-auto h-full">
<table className="w-full min-w-[680px] border-collapse text-[11px]">
<thead>
<tr className="border-b border-slate-200 bg-[#f3f5f7] text-[9px] uppercase tracking-wide text-slate-500">
<th className="w-5 px-2 py-1.5 text-left font-black">
<span className="block h-3 w-3 rounded-[2px] border border-slate-300 bg-white" />
</th>
<th className="px-1.5 py-1.5 text-left font-black">
{isEn ? "City / Best Signal" : "城市 / 最佳信号"}
</th>
<th className="px-1.5 py-1.5 text-right font-black">{isEn ? "Edge" : "优势"}</th>
<th className="px-1.5 py-1.5 text-right font-black">Live</th>
<th className="px-1.5 py-1.5 text-right font-black">DEB</th>
<th className="px-1.5 py-1.5 text-right font-black">{isEn ? "Mkt" : "市场"}</th>
<th className="px-1.5 py-1.5 text-right font-black">{isEn ? "Liq" : "流动性"}</th>
</tr>
</thead>
<tbody>
{cityGroups.map(({ city, displayName, best, buckets }) => {
const isExpanded = expandedCities.has(city);
const hasMultiple = buckets.length > 1;
return (
<Fragment key={city}>
<tr
className={clsx(
"cursor-pointer border-b border-slate-100 hover:bg-slate-50/80",
selectedId === best.id && "bg-blue-50/50"
)}
onClick={() => onSelect(best)}
>
<td className="px-2 py-1.5">
<button
type="button"
className="grid h-4 w-4 place-items-center text-slate-400 hover:text-slate-700"
onClick={(e) => { e.stopPropagation(); toggle(city); }}
title={`${labelExpand} ${hasMultiple ? `${buckets.length} ${labelBuckets}` : ""}`}
>
{hasMultiple ? (
isExpanded ? <ChevronDown size={11} /> : <ChevronRight size={11} />
) : (
<span className="text-[8px] text-slate-300"></span>
)}
</button>
</td>
<td className="px-1.5 py-1.5">
<div className="font-bold text-slate-800">{displayName}</div>
<div className="text-[9px] text-slate-400">
{best.target_label || ""}
{hasMultiple && !isExpanded ? ` +${buckets.length - 1}` : ""}
</div>
</td>
<td className={clsx("px-1.5 py-1.5 text-right font-mono font-bold", edgeClass(best.edge_percent))}>
{pct(best.edge_percent)}
</td>
<td className="px-1.5 py-1.5 text-right font-mono">{temp(best.current_temp || best.current_max_so_far, best.temp_symbol)}</td>
<td className="px-1.5 py-1.5 text-right font-mono">{temp(best.deb_prediction, best.temp_symbol)}</td>
<td className="px-1.5 py-1.5 text-right font-mono">{pct(best.market_probability ?? best.market_event_probability)}</td>
<td className="px-1.5 py-1.5 text-right font-mono">{money(best.book_liquidity ?? best.market_liquidity ?? best.volume)}</td>
</tr>
{isExpanded && buckets.map((row) => (
<tr
key={row.id}
className={clsx(
"cursor-pointer border-b border-slate-50 bg-slate-50/50 hover:bg-blue-50/50",
selectedId === row.id && "bg-blue-50"
)}
onClick={() => onSelect(row)}
>
<td className="px-2 py-1" />
<td className="px-1.5 py-1 pl-5">
<span className="text-[10px] text-slate-500">{row.target_label || "--"}</span>
</td>
<td className={clsx("px-1.5 py-1 text-right font-mono text-[10px] font-bold", edgeClass(row.edge_percent))}>
{pct(row.edge_percent)}
</td>
<td className="px-1.5 py-1 text-right font-mono text-[10px]">{temp(row.current_temp || row.current_max_so_far, row.temp_symbol)}</td>
<td className="px-1.5 py-1 text-right font-mono text-[10px]">{temp(row.deb_prediction, row.temp_symbol)}</td>
<td className="px-1.5 py-1 text-right font-mono text-[10px]">{pct(row.market_probability ?? row.market_event_probability)}</td>
<td className="px-1.5 py-1 text-right font-mono text-[10px]">{money(row.book_liquidity ?? row.market_liquidity ?? row.volume)}</td>
</tr>
))}
</Fragment>
);
})}
</tbody>
</table>
</div>
);
}
function PolyWeatherTerminal({
generatedText,
isEn,