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 = { type RiskLines = {
riskBased: boolean; riskBased: boolean;
sizing: string; sizing: string;
sizingShort: string;
lossPct: string | null; lossPct: string | null;
exit: string; exit: string;
openRatio: string | null; openRatio: string | null;
/** 表格「风险/开仓」列:以损显示风险%;手动显示名义比例 */
riskOrOpen: string;
leverage: string; leverage: string;
}; };
@@ -131,7 +134,6 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
? `固定 ${fmt(t, 2)}U` ? `固定 ${fmt(t, 2)}U`
: "固定 —"; : "固定 —";
} }
// 以损定仓必显风险比例(risk_loss_pct);手动隐藏
let lossPct: string | null = null; let lossPct: string | null = null;
if (riskBased) { if (riskBased) {
const lossN = Number(strat.risk_loss_pct); const lossN = Number(strat.risk_loss_pct);
@@ -141,17 +143,44 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
} }
const openRatio = riskBased const openRatio = riskBased
? `${unitLabel(strat.risk_perp_unit)}:${unitLabel(strat.risk_option_unit)}` ? `${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 { return {
riskBased, riskBased,
sizing: riskBased ? "以损定仓" : "手动仓位", sizing: riskBased ? "以损定仓" : "手动仓位",
sizingShort: riskBased ? "以损" : "手动",
lossPct, lossPct,
exit, exit,
openRatio, openRatio,
riskOrOpen,
leverage: leveragePair(strat), 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> }) { function RiskParamsBox({ strat }: { strat: Record<string, unknown> }) {
const r = riskLines(strat); const r = riskLines(strat);
return ( return (
@@ -479,129 +508,126 @@ export default function MonitorPage() {
</div> </div>
</div> </div>
{err ? <div className="err">{err}</div> : null} {err ? <div className="err">{err}</div> : null}
<div className="card-grid"> {nodes.length ? (
{nodes.map((n) => { <div className="monitor-table-wrap">
const s = pickStrategy(n); <table className="monitor-table">
const running = s.running === true || s.running === 1; <thead>
const inPos = hasOpenPosition(s.position); <tr>
return ( <th className="col-check">
<article <span className="sr-only"></span>
key={n.id} </th>
className={`node-card ${n.online ? "online" : "offline"} ${ <th></th>
running ? "running" : "" <th></th>
}`} <th></th>
onClick={() => setDetailId(n.id)} <th></th>
role="button" <th></th>
tabIndex={0} <th>/</th>
onKeyDown={(e) => { <th></th>
if (e.key === "Enter" || e.key === " ") { <th></th>
e.preventDefault(); <th></th>
setDetailId(n.id); </tr>
} </thead>
}} <tbody>
> {nodes.map((n) => {
<header className="node-card-head"> const s = pickStrategy(n);
<label const running = s.running === true || s.running === 1;
className="check" const inPos = hasOpenPosition(s.position);
onClick={(e) => e.stopPropagation()} const risk = riskLines(s.strat);
onKeyDown={(e) => e.stopPropagation()} return (
> <tr
<input key={n.id}
type="checkbox" className={`monitor-row ${running ? "is-running" : "is-stopped"}`}
checked={selected.has(n.id)} onClick={() => setDetailId(n.id)}
onChange={() => toggle(n.id)} >
/> <td
<strong>{n.name}</strong> className="col-check"
</label> onClick={(e) => e.stopPropagation()}
<span className={`pill ${n.online ? "ok" : "bad"}`}> >
{n.online ? "在线" : "离线"} <input
</span> type="checkbox"
</header> checked={selected.has(n.id)}
<div className="node-meta mono">{n.base_url}</div> onChange={() => toggle(n.id)}
<dl className="kv"> aria-label={`选择 ${n.name}`}
<div> />
<dt></dt> </td>
<dd>{s.mode}</dd> <td>
</div> <div className="machine-cell">
<div> <span
<dt></dt> className={`dot ${n.online ? "ok" : "bad"}`}
<dd>{s.exchange}</dd> title={n.online ? "在线" : "离线"}
</div> />
<div> <span
<dt></dt> className={tokenDotClass(n)}
<dd> title={tokenDotTitle(n)}
{running ? "运行中" : "已停"} · {s.phase} />
</dd> <span
</div> className={`run-dot ${running ? "ok" : "bad"}`}
<div> title={running ? "运行中" : "已停"}
<dt></dt> />
<dd className={inPos ? "pnl-pos" : "pnl-neg"}> <strong className="machine-name">{n.name}</strong>
{inPos ? "持仓中" : "无持仓"} </div>
</dd> </td>
</div> <td className="mono">{modeLabel(s.mode, s.strat)}</td>
<div> <td className="mono">{s.exchange}</td>
<dt></dt> <td className={inPos ? "pnl-pos" : "pnl-neg"}>
<dd>{s.rounds ?? "-"}</dd> {inPos ? "持仓中" : "无持仓"}
</div> </td>
<div> <td className="mono">{s.rounds ?? "—"}</td>
<dt></dt> <td className="mono" title={risk.riskBased ? "风险比例" : "开仓比例"}>
<dd>{s.market ? "已连接" : "断开"}</dd> {risk.riskOrOpen}
</div> </td>
<div> <td className="mono">{risk.exit}</td>
<dt>Token</dt> <td className="mono">{risk.leverage}</td>
<dd> <td
{n.token_configured className="col-actions"
? n.fleet_ok === false onClick={(e) => e.stopPropagation()}
? "配对失败" >
: "已配对" <div className="row-actions">
: "未配对"} <button
</dd> type="button"
</div> className={`btn btn-sm ${running ? "btn-running" : ""}`}
</dl> disabled={
<RiskParamsBox strat={s.strat} /> !!busy[n.id] || !n.token_configured || running
{n.fleet_error ? <div className="err soft">{n.fleet_error}</div> : null} }
{n.error ? <div className="err soft">{n.error}</div> : null} onClick={() => void act(n.id, "start")}
<div className="node-actions" onClick={(e) => e.stopPropagation()}> >
<button {running ? "运行中" : "启动"}
type="button" </button>
className={`btn ${running ? "btn-running" : ""}`} <button
disabled={!!busy[n.id] || !n.token_configured || running} type="button"
onClick={() => void act(n.id, "start")} className="btn btn-sm ghost"
> disabled={!!busy[n.id] || !n.token_configured}
{running ? "运行中" : "启动"} onClick={() => void act(n.id, "pause")}
</button> >
<button
type="button" </button>
className="btn ghost" <button
disabled={!!busy[n.id] || !n.token_configured} type="button"
onClick={() => void act(n.id, "pause")} className="btn btn-sm"
> disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "login")}
</button> >
<button
type="button" </button>
className="btn" <button
disabled={!!busy[n.id] || !n.token_configured} type="button"
onClick={() => void act(n.id, "login")} className="btn btn-sm ghost"
> disabled={!!busy[n.id] || !n.token_configured}
onClick={() => void act(n.id, "update")}
</button> >
<button
type="button" </button>
className="btn ghost" </div>
disabled={!!busy[n.id] || !n.token_configured} </td>
onClick={() => void act(n.id, "update")} </tr>
> );
})}
</button> </tbody>
</div> </table>
</article> </div>
); ) : (
})}
</div>
{!nodes.length ? (
<p className="meta"> Token</p> <p className="meta"> Token</p>
) : null} )}
{detailNode && detail ? ( {detailNode && detail ? (
<div <div
+121
View File
@@ -206,6 +206,127 @@ input {
font-weight: 600; font-weight: 600;
} }
.monitor-table-wrap {
overflow: auto;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
}
.monitor-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.monitor-table th,
.monitor-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
white-space: nowrap;
vertical-align: middle;
}
.monitor-table th {
color: var(--muted);
font-weight: 600;
background: rgba(255, 255, 255, 0.03);
position: sticky;
top: 0;
z-index: 1;
}
.monitor-table tbody tr {
cursor: pointer;
}
.monitor-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
.monitor-table tbody tr.is-running {
box-shadow: inset 3px 0 0 rgba(60, 179, 113, 0.85);
}
.monitor-table tbody tr.is-stopped {
box-shadow: inset 3px 0 0 rgba(227, 93, 93, 0.75);
}
.monitor-table .col-check {
width: 36px;
text-align: center;
}
.monitor-table .col-actions {
white-space: normal;
}
.machine-cell {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.machine-name {
font-weight: 650;
}
.dot,
.run-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: 0 0 auto;
background: var(--muted);
}
.dot.ok,
.run-dot.ok {
background: var(--ok, #3cb371);
box-shadow: 0 0 0 2px rgba(60, 179, 113, 0.2);
}
.dot.bad,
.run-dot.bad {
background: var(--bad, #e35d5d);
box-shadow: 0 0 0 2px rgba(227, 93, 93, 0.2);
}
.dot.warn {
background: #e6a23c;
box-shadow: 0 0 0 2px rgba(230, 162, 60, 0.25);
}
.dot.muted {
background: #5a6570;
}
.row-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.btn-sm {
padding: 4px 8px;
font-size: 0.78rem;
min-height: 28px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));