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
+121
View File
@@ -206,6 +206,127 @@ input {
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 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));