Files
eth_hedge_sim/frontend/src/components/FundsBar.tsx
T
dekun f49365b4d4 Constrain funds bar width and fold strategy rules.
Match page max-width on ultrawide and move the plan subtitle into a collapsible rules section under the funds strip.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 17:37:34 +08:00

134 lines
4.5 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { apiFetch } from "../api/client";
export type FundsSummary = {
ok: boolean;
exchange: string;
trading_day: string;
total_trades: number;
win_rate: number;
profit_loss_ratio: number | null;
total_funds: number | null;
funding_usdt: number | null;
trading_usdt: number | null;
options_funding_label: string;
options_trading_label: string;
realtime_pnl: number | null;
usdc_usdt_rate?: number;
perp_inst_id?: string;
detail?: string;
};
function fmtU(n: number | null | undefined) {
if (n == null || Number.isNaN(n)) return "—";
return `${n.toFixed(2)}U`;
}
function pnlClass(n: number | null | undefined) {
if (n == null || Number.isNaN(n) || n === 0) return "";
return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss";
}
export default function FundsBar() {
const [s, setS] = useState<FundsSummary | null>(null);
const [rulesOpen, setRulesOpen] = useState(false);
const load = useCallback(() => {
apiFetch<FundsSummary>("/api/funds/summary")
.then(setS)
.catch(() => {
/* 顶栏静默失败,避免刷屏 */
});
}, []);
useEffect(() => {
load();
const t = window.setInterval(load, 5000);
return () => window.clearInterval(t);
}, [load]);
if (!s?.ok) return null;
const rulesText = `行情 ${s.exchange || "OKX"} · ${
s.perp_inst_id || "ETH-USDT-SWAP"
} · 目标平仓(双腿/远虚只平永续) · 未达标则到期结算`;
return (
<div className="funds-bar-shell">
<div className="funds-bar-inner">
<div className="funds-bar" aria-label="资金与统计">
<div className="funds-item funds-item--primary">
<div className="funds-label"></div>
<div className="funds-value">{s.exchange || "—"}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{s.trading_day}</div>
</div>
<div className="funds-item funds-item--primary">
<div className="funds-label"></div>
<div className="funds-value mono">{s.total_trades}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">
{(s.win_rate * 100).toFixed(0)}%
</div>
</div>
<div className="funds-item" title="平均盈利 ÷ 平均亏损">
<div className="funds-label"></div>
<div className="funds-value mono">
{s.profit_loss_ratio != null
? s.profit_loss_ratio.toFixed(2)
: "—"}
</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{fmtU(s.total_funds)}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{fmtU(s.funding_usdt)}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{fmtU(s.trading_usdt)}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{s.options_funding_label}</div>
</div>
<div className="funds-item">
<div className="funds-label"></div>
<div className="funds-value mono">{s.options_trading_label}</div>
</div>
<div className="funds-item funds-item--pnl">
<div className="funds-label"></div>
<div className={`funds-value mono ${pnlClass(s.realtime_pnl)}`}>
{s.realtime_pnl == null ? "—" : fmtU(s.realtime_pnl)}
</div>
</div>
</div>
<div className="funds-rules">
<button
type="button"
className="funds-rules-toggle"
aria-expanded={rulesOpen}
onClick={() => setRulesOpen((v) => !v)}
>
<span></span>
<span className="funds-rules-chevron" aria-hidden>
{rulesOpen ? "▾" : "▸"}
</span>
</button>
{rulesOpen ? (
<p className="funds-rules-body meta">{rulesText}</p>
) : null}
</div>
</div>
</div>
);
}