fix(hub): align trend pullback card with instance layout

Match strategy page plan card: 3x3 metrics, DCA table, breakeven row, snapshot footer; PnL percent on plan margin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 10:51:13 +08:00
parent 3fb2023efb
commit 7037dc2334
3 changed files with 314 additions and 142 deletions
+164 -81
View File
@@ -1320,139 +1320,222 @@ body.market-chart-fs-open {
border-color: rgba(0, 255, 157, 0.38); border-color: rgba(0, 255, 157, 0.38);
} }
.hub-trend-plan-list { /* 趋势回调:与四所实例 strategy_trend_panel 同款卡片 */
.hub-trend-running-title {
margin: 0 0 10px;
font-size: 0.95rem;
color: #b8c4ff;
font-weight: 600;
}
.hub-trend-plan-list.running-plans-stack {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.hub-trend-plan-card { .hub-trend-plan-card.plan-position-card {
background: #141a2a;
border: 1px solid #2a3150;
border-radius: 12px;
padding: 12px 14px; padding: 12px 14px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid var(--border-soft);
border-radius: 10px;
} }
.hub-trend-plan-head { .hub-trend-plan-card .plan-card-head {
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px; margin-bottom: 8px;
} }
.hub-trend-plan-title { .hub-trend-plan-card .plan-card-title {
font-size: 14px;
font-weight: 600;
}
.hub-trend-plan-status {
font-size: 11px;
color: var(--muted);
}
.hub-trend-plan-meta {
display: flex; display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 14px; font-size: 1rem;
font-size: 12px; font-weight: 700;
color: var(--muted); color: #f0f2ff;
}
.hub-trend-plan-card .plan-card-meta {
font-size: 0.76rem;
color: #8892b0;
line-height: 1.55;
margin-bottom: 10px; margin-bottom: 10px;
} }
.hub-trend-plan-meta .pos-meta-accent, .hub-trend-plan-card .plan-card-meta .accent {
.hub-trend-plan-meta strong { color: #6ab8ff;
}
.hub-trend-plan-card .plan-card-meta strong {
color: var(--accent); color: var(--accent);
} }
.hub-trend-plan-foot { .hub-trend-plan-card .plan-card-grid {
margin-top: 10px; display: grid;
font-size: 11px; grid-template-columns: repeat(3, minmax(0, 1fr));
color: var(--muted); gap: 10px 14px;
margin-bottom: 10px;
} }
.pos-value.pos-tp-program { .hub-trend-plan-card .plan-cell {
color: #8fc8ff;
}
.hub-trend-plan-card--horizontal {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
gap: 14px 18px; gap: 3px;
align-items: flex-start;
background: linear-gradient(145deg, rgba(12, 18, 32, 0.92), rgba(8, 12, 22, 0.88));
border-color: rgba(0, 212, 255, 0.22);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
} }
.hub-trend-plan-body { .hub-trend-plan-card .plan-cell .lbl {
flex: 1 1 320px; font-size: 0.72rem;
min-width: 0; color: #8b95b8;
} }
.hub-trend-plan-side { .hub-trend-plan-card .plan-cell .val {
flex: 1 1 220px; color: #f0f2ff;
min-width: 200px; font-size: 0.88rem;
max-width: 100%; font-weight: 500;
} }
.hub-trend-dca-block { .hub-trend-plan-card .plan-cell .val.pnl-profit {
padding: 8px 10px; color: #4cd97f;
background: rgba(0, 0, 0, 0.22); }
border: 1px solid var(--border-soft);
.hub-trend-plan-card .plan-cell .val.pnl-loss {
color: #ff6666;
}
.hub-trend-plan-card .plan-cell .val.pnl-neutral {
color: #cfd3ef;
}
.hub-trend-plan-card .btn-close-plan {
padding: 7px 14px;
background: #5c1e2a;
color: #ffb4b4;
border: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
display: inline-block;
} }
.hub-trend-dca-title { .hub-trend-plan-card .btn-close-plan:hover {
font-size: 11px; filter: brightness(1.08);
color: var(--muted);
margin-bottom: 6px;
} }
.hub-trend-dca-table { .hub-trend-plan-card .plan-dca-block {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed #2a3558;
}
.hub-trend-plan-card .plan-dca-title {
font-size: 0.74rem;
color: #8b95b8;
margin-bottom: 8px;
}
.hub-trend-plan-card .plan-dca-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 11px; font-size: 0.76rem;
} }
.hub-trend-dca-table th, .hub-trend-plan-card .plan-dca-table th,
.hub-trend-dca-table td { .hub-trend-plan-card .plan-dca-table td {
padding: 4px 6px; padding: 6px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid #243050;
text-align: left; text-align: left;
} }
.hub-trend-dca-table th { .hub-trend-plan-card .plan-dca-table th {
color: var(--muted); color: #6a7598;
font-weight: 600; font-weight: 600;
} }
.hub-trend-dca-table .dca-done { .hub-trend-plan-card .plan-dca-table .st-done {
color: var(--green); color: #4cd97f;
} }
.hub-trend-dca-table .dca-pending { .hub-trend-plan-card .plan-dca-table .st-pending {
color: var(--muted); color: #9aa3c4;
} }
.exchange-fullscreen .hub-trend-plan-list { .hub-trend-plan-card .hub-plan-breakeven-row {
display: block; display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
margin-top: 8px;
}
.hub-trend-plan-card .hub-plan-be-label {
font-size: 0.78rem;
color: #cfd3ef;
display: flex;
align-items: center;
gap: 6px;
}
.hub-trend-plan-card .hub-plan-be-input {
width: 72px;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #304164;
background: #0f1424;
color: #cfd3ef;
opacity: 0.85;
}
.hub-trend-plan-card .hub-plan-be-btn {
padding: 6px 12px;
background: #1f4a3a;
color: #8fc8ff;
border-radius: 8px;
font-size: 0.78rem;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.hub-trend-plan-card .hub-plan-be-btn--static {
cursor: default;
}
.hub-trend-plan-card .hub-plan-be-done {
color: #6ab88a;
font-size: 0.75rem;
}
.hub-trend-plan-card .hub-plan-account-foot {
margin-bottom: 0;
}
.hub-trend-plan-card .badge.direction-long {
color: #4cd97f;
border-color: rgba(76, 217, 127, 0.45);
}
.hub-trend-plan-card .badge.direction-short {
color: #ff6666;
border-color: rgba(255, 102, 102, 0.45);
}
.exchange-fullscreen .hub-trend-plan-card.plan-position-card {
width: 100%;
max-width: 100%; max-width: 100%;
} }
.exchange-fullscreen .hub-trend-plan-card--horizontal { @media (max-width: 720px) {
width: 100%; .hub-trend-plan-card .plan-card-grid {
} grid-template-columns: 1fr;
}
.exchange-fullscreen .hub-trend-plan-metrics-row {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 10px 16px;
}
.exchange-fullscreen .hub-trend-plan-grid {
flex: 1 1 280px;
} }
/* 顺势加仓 */ /* 顺势加仓 */
+148 -59
View File
@@ -345,6 +345,76 @@
return { text: pnlText, cls: pnlCls(upnl) }; return { text: pnlText, cls: pnlCls(upnl) };
} }
/** 与实例策略页一致:浮盈亏 % = 浮盈亏 / 计划保证金 */
function formatTrendPlanFloatingPnl(upnl, planMargin) {
if (upnl == null || !Number.isFinite(Number(upnl))) {
return { text: "—", cls: "" };
}
let pnlText = fmt(upnl, 2) + "U";
const margin = Number(planMargin);
if (Number.isFinite(margin) && margin > 0) {
const pct = (Number(upnl) / margin) * 100;
pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
}
const n = Number(upnl);
let cls = "pnl-neutral";
if (n > 0) cls = "pnl-profit";
else if (n < 0) cls = "pnl-loss";
return { text: pnlText, cls };
}
function renderDirectionBadge(side) {
const s = normSide(side);
const label = sideDirLabel(side);
const cls = s === "long" ? "direction-long" : s === "short" ? "direction-short" : "";
if (!cls) return esc(String(label));
return `<span class="badge ${cls}">${esc(label)}</span>`;
}
function resolveTrendDcaLevels(t) {
if (Array.isArray(t.dca_levels) && t.dca_levels.length) return t.dca_levels;
const plan = t || {};
let grid = [];
let legAmounts = [];
try {
grid = JSON.parse(plan.grid_prices_json || "[]");
if (!Array.isArray(grid)) grid = [];
} catch (_e) {
grid = [];
}
try {
legAmounts = JSON.parse(plan.leg_amounts_json || "[]");
if (!Array.isArray(legAmounts)) legAmounts = [];
} catch (_e2) {
legAmounts = [];
}
const legsDone = Number(plan.legs_done) || 0;
const dcaLegs = Number(plan.dca_legs) || 0;
const firstDone = Number(plan.first_order_done) !== 0;
const out = [
{
label: "首仓",
price: null,
contracts: plan.first_order_amount,
status: firstDone ? "done" : "pending",
status_label: firstDone ? "已开仓" : "待开仓",
},
];
const n = Math.max(grid.length, legAmounts.length, dcaLegs);
for (let idx = 0; idx < n; idx += 1) {
const legI = idx + 1;
const done = legI <= legsDone;
out.push({
label: `补仓${legI}`,
price: idx < grid.length ? grid[idx] : null,
contracts: idx < legAmounts.length ? legAmounts[idx] : null,
status: done ? "done" : "pending",
status_label: done ? "已补仓" : "待补仓",
});
}
return out;
}
function pnlCls(v) { function pnlCls(v) {
const n = Number(v); const n = Number(v);
if (!Number.isFinite(n) || n === 0) return ""; if (!Number.isFinite(n) || n === 0) return "";
@@ -1333,6 +1403,8 @@
btn.onclick = (ev) => { btn.onclick = (ev) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const msg = (btn.dataset.confirm || "").trim();
if (msg && !confirm(msg)) return;
openInstance(btn.dataset.exId, btn.dataset.next || "/", { openInstance(btn.dataset.exId, btn.dataset.next || "/", {
newTab: ev.ctrlKey || ev.metaKey, newTab: ev.ctrlKey || ev.metaKey,
}); });
@@ -1514,7 +1586,7 @@
} }
function renderTrendDcaTable(t, tickMap) { function renderTrendDcaTable(t, tickMap) {
const levels = Array.isArray(t.dca_levels) ? t.dca_levels : []; const levels = resolveTrendDcaLevels(t);
if (!levels.length) return ""; if (!levels.length) return "";
const sym = t.exchange_symbol || t.symbol || ""; const sym = t.exchange_symbol || t.symbol || "";
const rows = levels const rows = levels
@@ -1525,7 +1597,7 @@
: "—"; : "—";
const amt = const amt =
lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—"; lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—";
const stCls = lv.status === "done" ? "dca-done" : "dca-pending"; const stCls = lv.status === "done" ? "st-done" : "st-pending";
const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓"); const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓");
return `<tr> return `<tr>
<td>${esc(lv.label || lv.leg_key || "—")}</td> <td>${esc(lv.label || lv.leg_key || "—")}</td>
@@ -1535,16 +1607,16 @@
</tr>`; </tr>`;
}) })
.join(""); .join("");
return `<div class="hub-trend-dca-block"> return `<div class="plan-dca-block">
<div class="hub-trend-dca-title">补仓计划明细</div> <div class="plan-dca-title">补仓计划明细</div>
<table class="hub-trend-dca-table"> <table class="plan-dca-table">
<thead><tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr></thead> <tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr>
<tbody>${rows}</tbody> ${rows}
</table> </table>
</div>`; </div>`;
} }
function renderTrendPlanCard(t, tickMap, pos) { function renderTrendPlanCard(t, tickMap, pos, exchangeRow) {
const sym = t.exchange_symbol || t.symbol || ""; const sym = t.exchange_symbol || t.symbol || "";
const side = (t.direction || "long").toLowerCase(); const side = (t.direction || "long").toLowerCase();
const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap); const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
@@ -1564,66 +1636,79 @@
? esc(legsDone) ? esc(legsDone)
: "—"; : "—";
const upnlTrend = resolveTrendFloatingPnl(pos, t); const upnlTrend = resolveTrendFloatingPnl(pos, t);
const notional = const pnlFmt = formatTrendPlanFloatingPnl(upnlTrend, t.plan_margin_capital);
pos && pos.notional_usdt != null const pnlVal =
? pos.notional_usdt
: t.plan_margin_capital != null
? Number(t.plan_margin_capital) * Number(t.leverage || 1)
: null;
const pnlFmt = formatFloatingPnlText(upnlTrend, notional);
const pnlInner =
pnlFmt.text === "—" pnlFmt.text === "—"
? "—" ? "—"
: `<span class="pos-value ${pnlFmt.cls}">${esc(pnlFmt.text)}</span>`; : `<span class="val ${pnlFmt.cls}">${esc(pnlFmt.text)}</span>`;
const sizing = resolveTrendSizingFooter({}, t, true);
const levTxt =
sizing.leverage != null && sizing.leverage !== "" ? `${esc(sizing.leverage)}x` : "—";
const baseTxt =
sizing.planBase != null && sizing.planBase !== "" ? `${fmt(sizing.planBase, 2)}U` : "—";
const ratioTxt =
sizing.positionRatio != null && sizing.positionRatio !== ""
? `${fmt(sizing.positionRatio, 2)}%`
: "—";
const riskTxt = const riskTxt =
t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—"; t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—";
const footHtml = `<div class="hub-trend-plan-foot pos-footer"> const snapTxt =
<span>杠杆: ${levTxt}</span> t.snapshot_available_usdt != null && t.snapshot_available_usdt !== ""
<span>计划基数: ${baseTxt}</span> ? `${fmt(t.snapshot_available_usdt, 2)}U`
<span>仓位占比: ${ratioTxt}</span> : "—";
</div>`; const marginTxt =
t.plan_margin_capital != null && t.plan_margin_capital !== ""
? `${fmt(t.plan_margin_capital, 2)}U`
: "—";
const levTxt = t.leverage != null && t.leverage !== "" ? `${esc(t.leverage)}x` : "—";
const bePct =
t.breakeven_offset_pct != null && t.breakeven_offset_pct !== ""
? esc(t.breakeven_offset_pct)
: "0.3";
const exId = exchangeRow && exchangeRow.id != null ? esc(exchangeRow.id) : "";
const canOpen = !!(exchangeRow && (exchangeRow.flask_url_browser || exchangeRow.flask_url));
const endBtn = canOpen
? `<a href="#" class="btn-close-plan btn-open-instance" data-ex-id="${exId}" data-next="/stop_trend_pullback/${esc(t.id)}" data-confirm="结束计划:市价平仓并撤掉该合约全部挂单,确定?">结束计划</a>`
: "";
const beBtn = canOpen
? `<a href="#" class="hub-plan-be-btn btn-open-instance" data-ex-id="${exId}" data-next="/strategy">保本移交下单监控</a>`
: `<span class="hub-plan-be-btn hub-plan-be-btn--static">保本移交下单监控</span>`;
const beApplied =
t.breakeven_applied
? `<span class="hub-plan-be-done">已保本 ${esc(String(t.breakeven_applied_at || "").slice(0, 16))}</span>`
: "";
const dcaHtml = renderTrendDcaTable(t, tickMap); const dcaHtml = renderTrendDcaTable(t, tickMap);
return `<div class="hub-trend-plan-card hub-pos-card hub-trend-plan-card--horizontal"> return `<div class="plan-position-card hub-trend-plan-card">
<div class="hub-trend-plan-body"> <div class="plan-card-head">
<div class="hub-trend-plan-head"> <div class="plan-card-title">
<span class="hub-trend-plan-title">#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)}</span> <span>#${esc(t.id)} ${esc(sym)}</span>
<span class="hub-trend-plan-status">${esc(t.status || "active")}</span> ${renderDirectionBadge(t.direction)}
</div>
<div class="hub-trend-plan-meta">
<span>来源: 趋势回调计划</span>
<span>风险: ${riskTxt}</span>
<span>${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
<span>已补仓 <strong>${legsTxt}</strong></span>
</div>
<div class="hub-trend-plan-metrics-row">
<div class="pos-grid hub-trend-plan-grid">
<div class="pos-cell"><span class="pos-label">均价</span><span class="pos-value">${esc(avg)}</span></div>
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${esc(sl)}</span></div>
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value pos-tp-program">程序监控 · ${esc(tp)}</span></div>
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${esc(rrTxt)}</span></div>
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${esc(mark)}</span></div>
<div class="pos-cell"><span class="pos-label">浮盈亏</span>${pnlInner}</div>
</div>
${footHtml}
</div> </div>
${endBtn}
</div>
<div class="plan-card-meta">
来源: 趋势回调计划 | 风险: ${riskTxt}
<span class="accent">${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
已补仓 <strong>${legsTxt}</strong>
</div>
<div class="plan-card-grid">
<div class="plan-cell"><span class="lbl">均价</span><span class="val">${esc(avg)}</span></div>
<div class="plan-cell"><span class="lbl">止损</span><span class="val">${esc(sl)}</span></div>
<div class="plan-cell"><span class="lbl">止盈</span><span class="val">${esc(tp)}</span></div>
<div class="plan-cell"><span class="lbl">盈亏比</span><span class="val">${esc(rrTxt)}</span></div>
<div class="plan-cell"><span class="lbl">标记价</span><span class="val">${esc(mark)}</span></div>
<div class="plan-cell"><span class="lbl">浮盈亏</span>${pnlVal}</div>
</div>
${dcaHtml}
<div class="plan-card-meta hub-plan-breakeven-row">
<label class="hub-plan-be-label">
保本移交 偏移%
<input type="number" disabled value="${bePct}" class="hub-plan-be-input" />
</label>
${beBtn}
${beApplied}
</div>
<div class="plan-card-meta hub-plan-account-foot">
快照可用: ${esc(snapTxt)} 计划保证金${esc(marginTxt)} 杠杆: ${levTxt}
</div> </div>
${dcaHtml ? `<div class="hub-trend-plan-side">${dcaHtml}</div>` : ""}
</div>`; </div>`;
} }
function renderTrendSection(trends, tickMap, positions) { function renderTrendSection(trends, tickMap, positions, exchangeRow) {
if (!trends || !trends.length) return ""; if (!trends || !trends.length) return "";
const posList = Array.isArray(positions) ? positions : []; const posList = Array.isArray(positions) ? positions : [];
return `<div class="hub-trend-plan-list">${trends const cards = trends
.map((t) => { .map((t) => {
const sym = t.exchange_symbol || t.symbol || ""; const sym = t.exchange_symbol || t.symbol || "";
const side = (t.direction || "long").toLowerCase(); const side = (t.direction || "long").toLowerCase();
@@ -1636,9 +1721,13 @@
break; break;
} }
} }
return renderTrendPlanCard(t, tickMap, matched); return renderTrendPlanCard(t, tickMap, matched, exchangeRow);
}) })
.join("")}</div>`; .join("");
return `<div class="hub-trend-running">
<div class="hub-trend-running-title">运行中的计划</div>
<div class="running-plans-stack hub-trend-plan-list">${cards}</div>
</div>`;
} }
function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) { function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) {
@@ -2052,7 +2141,7 @@
if ((row.capabilities || []).includes("trend")) { if ((row.capabilities || []).includes("trend")) {
html += renderHubSectionCard( html += renderHubSectionCard(
"趋势回调", "趋势回调",
renderTrendSection(trends, tickMap, pos), renderTrendSection(trends, tickMap, pos, row),
"暂无运行中的趋势回调计划" "暂无运行中的趋势回调计划"
); );
} }
+2 -2
View File
@@ -14,7 +14,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-dca" /> <link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-instance" />
</head> </head>
<body> <body>
<div class="app-bg" aria-hidden="true"></div> <div class="app-bg" aria-hidden="true"></div>
@@ -235,6 +235,6 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-hub-trend-plan"></script> <script src="/assets/chart.js?v=20260604-hub-trend-plan"></script>
<script src="/assets/app.js?v=20260604-hub-trend-dca"></script> <script src="/assets/app.js?v=20260604-hub-trend-instance"></script>
</body> </body>
</html> </html>