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 = {
|
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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user