feat: show time-close countdown after symbol in monitor area

Move timed-close badge next to contract name on hub board and instance position cards; refresh countdown on board render.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 20:20:13 +08:00
parent 035060b68a
commit 0d82cd2ad3
9 changed files with 96 additions and 41 deletions
+7 -6
View File
@@ -413,6 +413,13 @@
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
@@ -427,12 +434,6 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
{% if not o.time_close_enabled %}style="display:none"{% endif %}
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· 倒计时 <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
+7 -6
View File
@@ -393,6 +393,13 @@
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
@@ -407,12 +414,6 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
{% if not o.time_close_enabled %}style="display:none"{% endif %}
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· 倒计时 <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
+7 -6
View File
@@ -393,6 +393,13 @@
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
@@ -407,12 +414,6 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
{% if not o.time_close_enabled %}style="display:none"{% endif %}
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· 倒计时 <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
+7 -6
View File
@@ -422,6 +422,13 @@
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
@@ -436,12 +443,6 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
{% if not o.time_close_enabled %}style="display:none"{% endif %}
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· 倒计时 <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
+19
View File
@@ -1023,6 +1023,25 @@ body.market-chart-fs-open {
min-width: 0;
}
.hub-pos-card .pos-symbol-time-close,
.hub-mini-title .pos-symbol-time-close {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.72rem;
font-weight: 500;
color: #8fc8ff;
padding: 1px 6px;
border-radius: 4px;
background: rgba(143, 200, 255, 0.1);
white-space: nowrap;
vertical-align: middle;
}
.hub-pos-card .pos-symbol-time-close .pos-time-close-cd,
.hub-mini-title .pos-symbol-time-close .pos-time-close-cd {
font-variant-numeric: tabular-nums;
letter-spacing: 0.03em;
}
.hub-pos-card .pos-card-symbol strong {
font-size: 14px;
color: var(--text);
+28 -12
View File
@@ -1495,6 +1495,9 @@
}
syncMonitorGridColumns(box, displayRows.length);
bindMonitorInteractions(box);
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
TimeCloseUI.tickLocalCountdowns();
}
if (expandedExchangeId && fs && fsInner) {
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
@@ -1505,6 +1508,9 @@
fs.setAttribute("aria-hidden", "false");
document.body.classList.add("hub-fullscreen-open");
bindMonitorInteractions(fsInner);
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
TimeCloseUI.tickLocalCountdowns();
}
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
btn.onclick = (ev) => {
ev.stopPropagation();
@@ -2073,6 +2079,17 @@
return html;
}
function timeCloseSymbolBadgeHtml(item) {
if (!item || !item.time_close_enabled) return "";
const tcLabel = item.time_close_label || `时间平仓 ${item.time_close_hours || ""}h`;
const tcCd = item.time_close_countdown || "--:--:--";
const tcAt = item.time_close_at_ms != null ? String(item.time_close_at_ms) : "";
return (
`<span class="pos-symbol-time-close pos-time-close-meta pos-meta-on" data-close-at-ms="${esc(tcAt)}">` +
`${esc(tcLabel)} · <span class="pos-time-close-cd">${esc(tcCd)}</span></span>`
);
}
function renderTrendDcaTable(t, tickMap) {
const levels = resolveTrendDcaLevels(t);
if (!levels.length) return "";
@@ -2304,26 +2321,18 @@
meta.push(
`<span class="${beOn ? "pos-meta-on" : "pos-meta-off"}">移动保本:${beOn ? "开" : "关"}</span>`
);
if (mo.time_close_enabled) {
const tcLabel = mo.time_close_label || `时间平仓 ${mo.time_close_hours || ""}h`;
const tcCd = mo.time_close_countdown || "--:--:--";
const tcAt = mo.time_close_at_ms != null ? String(mo.time_close_at_ms) : "";
meta.push(
`<span class="pos-meta-item pos-meta-on pos-time-close-meta" data-close-at-ms="${esc(tcAt)}">` +
`${esc(tcLabel)} · 倒计时 <span class="pos-time-close-cd">${esc(tcCd)}</span></span>`
);
}
} else {
meta.push("来源: 交易所持仓");
meta.push("风格: —");
meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
}
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
const tcSymBadge = !isTrend && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
return `<div class="pos-card hub-pos-card">
<div class="pos-card-head">
<div class="pos-card-symbol">
<button type="button" class="btn-open-market sym-link pos-symbol-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)"><strong>${esc(symbol)}</strong></button>${symBeBadge}
<button type="button" class="btn-open-market sym-link pos-symbol-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)"><strong>${esc(symbol)}</strong></button>${tcSymBadge}${symBeBadge}
<span class="pos-side-badge ${sideCls}">${sideCn}</span>
</div>
<div class="pos-head-actions">
@@ -2382,8 +2391,14 @@
const amtLine = amtTxt
? `<div class="hub-mini-line">挂单数量 ${esc(amtTxt)}</div>`
: "";
const keyTc =
k.time_close_enabled && k.time_close_at_ms
? timeCloseSymbolBadgeHtml(k)
: k.time_close_enabled && k.time_close_hours
? `<span class="pos-symbol-time-close pos-meta-on">时间平仓 ${esc(k.time_close_hours)}h</span>`
: "";
return `<div class="${cardCls}">
<div class="hub-mini-title">${esc(k.symbol)} · ${esc(mt)}${dir} ${pendingTag}</div>
<div class="hub-mini-title">${esc(k.symbol)} ${keyTc} · ${esc(mt)}${dir} ${pendingTag}</div>
<div class="hub-mini-line">上沿 ${esc(k.upper)} / 下沿 ${esc(k.lower)}</div>
${amtLine}
<div class="hub-mini-line hub-key-status-line">${esc(kp.gate_summary || kp.price_display || kp.price || "—")}${kp.gate_metrics ? ` · ${esc(kp.gate_metrics)}` : ""}</div>
@@ -2398,8 +2413,9 @@
return orders
.map((o) => {
const sym = o.exchange_symbol || o.symbol || "";
const tcBadge = o.time_close_enabled ? timeCloseSymbolBadgeHtml(o) : "";
return `<div class="hub-mini-card">
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)}</div>
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} ${tcBadge} · ${renderDirectionHtml(o.direction)}</div>
<div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
</div>`;
})
+2 -2
View File
@@ -15,7 +15,7 @@
<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'" />
<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=20260612-trade-row-click" />
<link rel="stylesheet" href="/assets/app.css?v=20260612-time-close-symbol" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head>
<body>
@@ -589,6 +589,6 @@
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=2"></script>
<script src="/assets/app.js?v=20260612-time-close"></script>
<script src="/assets/app.js?v=20260612-time-close-symbol"></script>
</body>
</html>
+16
View File
@@ -348,6 +348,22 @@ html[data-theme="light"] .pos-meta-item::after {
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.pos-symbol-time-close {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.72rem;
font-weight: 500;
color: #8fc8ff;
padding: 1px 6px;
border-radius: 4px;
background: rgba(143, 200, 255, 0.1);
white-space: nowrap;
}
.pos-symbol-time-close .pos-time-close-cd {
font-variant-numeric: tabular-nums;
letter-spacing: 0.03em;
}
.key-time-close-wrap.is-disabled > label,
.order-time-close-wrap.is-disabled > label {
opacity: 0.72;
+3 -3
View File
@@ -182,6 +182,9 @@
<span class="key-row-summary-main">
<span class="key-row-summary-title">
<strong>{{ k.symbol }}</strong>
{% if k.time_close_enabled and k.time_close_hours %}
<span class="pos-symbol-time-close pos-meta-on">时间平仓 {{ k.time_close_hours }}h</span>
{% endif %}
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
@@ -207,9 +210,6 @@
<span class="pos-meta-item">方案: {{ key_sl_tp_mode_label(k) }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
{% if k.time_close_enabled and k.time_close_hours %}
<span class="pos-meta-item pos-meta-on">时间平仓: {{ k.time_close_hours }}h</span>
{% endif %}
</div>
<div class="pos-grid">
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>