Replace control monitor cards with status table layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user