天气合约市场改为按城市分组折叠,展开显示所有温度档位
This commit is contained in:
@@ -4,7 +4,9 @@ import clsx from "clsx";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
LineChart,
|
LineChart,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -12,7 +14,7 @@ import {
|
|||||||
Table2,
|
Table2,
|
||||||
UserRound,
|
UserRound,
|
||||||
} from "lucide-react";
|
} 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 type { ProAccessState, ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { getInitialLocaleFromNavigator } from "@/lib/i18n";
|
import { getInitialLocaleFromNavigator } from "@/lib/i18n";
|
||||||
import { isBrowserLocalFullAccess } from "@/lib/local-dev-access";
|
import { isBrowserLocalFullAccess } from "@/lib/local-dev-access";
|
||||||
@@ -267,7 +269,7 @@ function KoyfinMarketPanel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Panel title={title}>
|
<Panel title={title}>
|
||||||
<KoyfinRowsTable
|
<CityGroupedTable
|
||||||
compact={compact}
|
compact={compact}
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
onSelect={onSelect}
|
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({
|
function PolyWeatherTerminal({
|
||||||
generatedText,
|
generatedText,
|
||||||
isEn,
|
isEn,
|
||||||
|
|||||||
Reference in New Issue
Block a user