Replace control monitor cards with status table layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 12:01:49 +08:00
parent bc3041dcf2
commit 46a861d311
2 changed files with 271 additions and 124 deletions
+150 -124
View File
@@ -102,9 +102,12 @@ function closeReasonZh(v: string): string {
type RiskLines = {
riskBased: boolean;
sizing: string;
sizingShort: string;
lossPct: string | null;
exit: string;
openRatio: string | null;
/** 表格「风险/开仓」列:以损显示风险%;手动显示名义比例 */
riskOrOpen: string;
leverage: string;
};
@@ -131,7 +134,6 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
? `固定 ${fmt(t, 2)}U`
: "固定 —";
}
// 以损定仓必显风险比例(risk_loss_pct);手动隐藏
let lossPct: string | null = null;
if (riskBased) {
const lossN = Number(strat.risk_loss_pct);
@@ -141,17 +143,44 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
}
const openRatio = riskBased
? `${unitLabel(strat.risk_perp_unit)}:${unitLabel(strat.risk_option_unit)}`
: null;
: `${unitLabel(strat.perp_qty_eth)}:${unitLabel(strat.option_qty_eth)}`;
const riskOrOpen = riskBased ? lossPct || "—" : openRatio;
return {
riskBased,
sizing: riskBased ? "以损定仓" : "手动仓位",
sizingShort: riskBased ? "以损" : "手动",
lossPct,
exit,
openRatio,
riskOrOpen,
leverage: leveragePair(strat),
};
}
function modeLabel(mode: string, strat: Record<string, unknown>): string {
const m = String(mode || "-").toUpperCase();
const short = m.includes("LIVE") ? "LIVE" : m.includes("SIM") ? "SIM" : m || "-";
return `${short}/${riskLines(strat).sizingShort}`;
}
function tokenDotClass(n: {
token_configured?: boolean;
fleet_ok?: boolean | null;
}): string {
if (!n.token_configured) return "dot muted";
if (n.fleet_ok === false) return "dot warn";
return "dot ok";
}
function tokenDotTitle(n: {
token_configured?: boolean;
fleet_ok?: boolean | null;
}): string {
if (!n.token_configured) return "Token 未配对";
if (n.fleet_ok === false) return "Token 配对失败";
return "Token 已配对";
}
function RiskParamsBox({ strat }: { strat: Record<string, unknown> }) {
const r = riskLines(strat);
return (
@@ -479,129 +508,126 @@ export default function MonitorPage() {
</div>
</div>
{err ? <div className="err">{err}</div> : null}
<div className="card-grid">
{nodes.map((n) => {
const s = pickStrategy(n);
const running = s.running === true || s.running === 1;
const inPos = hasOpenPosition(s.position);
return (
<article
key={n.id}
className={`node-card ${n.online ? "online" : "offline"} ${
running ? "running" : ""
}`}
onClick={() => setDetailId(n.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setDetailId(n.id);
}
}}
>
<header className="node-card-head">
<label
className="check"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<input
type="checkbox"
checked={selected.has(n.id)}
onChange={() => toggle(n.id)}
/>
<strong>{n.name}</strong>
</label>
<span className={`pill ${n.online ? "ok" : "bad"}`}>
{n.online ? "在线" : "离线"}
</span>
</header>
<div className="node-meta mono">{n.base_url}</div>
<dl className="kv">
<div>
<dt></dt>
<dd>{s.mode}</dd>
</div>
<div>
<dt></dt>
<dd>{s.exchange}</dd>
</div>
<div>
<dt></dt>
<dd>
{running ? "运行中" : "已停"} · {s.phase}
</dd>
</div>
<div>
<dt></dt>
<dd className={inPos ? "pnl-pos" : "pnl-neg"}>
{inPos ? "持仓中" : "无持仓"}
</dd>
</div>
<div>
<dt></dt>
<dd>{s.rounds ?? "-"}</dd>
</div>
<div>
<dt></dt>
<dd>{s.market ? "已连接" : "断开"}</dd>
</div>
<div>
<dt>Token</dt>
<dd>
{n.token_configured
? n.fleet_ok === false
? "配对失败"
: "已配对"
: "未配对"}
</dd>
</div>
</dl>
<RiskParamsBox strat={s.strat} />
{n.fleet_error ? <div className="err soft">{n.fleet_error}</div> : null}
{n.error ? <div className="err soft">{n.error}</div> : null}
<div className="node-actions" onClick={(e) => e.stopPropagation()}>
<button
type="button"
className={`btn ${running ? "btn-running" : ""}`}
disabled={!!busy[n.id] || !n.token_configured || running}
onClick={() => void act(n.id, "start")}
>
{running ? "运行中" : "启动"}
</button>
<button
type="button"
className="btn ghost"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "pause")}
>
</button>
<button
type="button"
className="btn"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "login")}
>
</button>
<button
type="button"
className="btn ghost"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "update")}
>
</button>
</div>
</article>
);
})}
</div>
{!nodes.length ? (
{nodes.length ? (
<div className="monitor-table-wrap">
<table className="monitor-table">
<thead>
<tr>
<th className="col-check">
<span className="sr-only"></span>
</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>/</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{nodes.map((n) => {
const s = pickStrategy(n);
const running = s.running === true || s.running === 1;
const inPos = hasOpenPosition(s.position);
const risk = riskLines(s.strat);
return (
<tr
key={n.id}
className={`monitor-row ${running ? "is-running" : "is-stopped"}`}
onClick={() => setDetailId(n.id)}
>
<td
className="col-check"
onClick={(e) => e.stopPropagation()}
>
<input
type="checkbox"
checked={selected.has(n.id)}
onChange={() => toggle(n.id)}
aria-label={`选择 ${n.name}`}
/>
</td>
<td>
<div className="machine-cell">
<span
className={`dot ${n.online ? "ok" : "bad"}`}
title={n.online ? "在线" : "离线"}
/>
<span
className={tokenDotClass(n)}
title={tokenDotTitle(n)}
/>
<span
className={`run-dot ${running ? "ok" : "bad"}`}
title={running ? "运行中" : "已停"}
/>
<strong className="machine-name">{n.name}</strong>
</div>
</td>
<td className="mono">{modeLabel(s.mode, s.strat)}</td>
<td className="mono">{s.exchange}</td>
<td className={inPos ? "pnl-pos" : "pnl-neg"}>
{inPos ? "持仓中" : "无持仓"}
</td>
<td className="mono">{s.rounds ?? "—"}</td>
<td className="mono" title={risk.riskBased ? "风险比例" : "开仓比例"}>
{risk.riskOrOpen}
</td>
<td className="mono">{risk.exit}</td>
<td className="mono">{risk.leverage}</td>
<td
className="col-actions"
onClick={(e) => e.stopPropagation()}
>
<div className="row-actions">
<button
type="button"
className={`btn btn-sm ${running ? "btn-running" : ""}`}
disabled={
!!busy[n.id] || !n.token_configured || running
}
onClick={() => void act(n.id, "start")}
>
{running ? "运行中" : "启动"}
</button>
<button
type="button"
className="btn btn-sm ghost"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "pause")}
>
</button>
<button
type="button"
className="btn btn-sm"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "login")}
>
</button>
<button
type="button"
className="btn btn-sm ghost"
disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "update")}
>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<p className="meta"> Token</p>
) : null}
)}
{detailNode && detail ? (
<div