feat: unify key monitor UI with one-line summary and expandable details across all exchanges

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-09 16:11:31 +08:00
parent 24a86a710c
commit 260828041f
6 changed files with 257 additions and 327 deletions
+2 -108
View File
@@ -321,114 +321,7 @@
<div class="grid">
{% if page == 'key_monitor' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">关键位监控</h2>
{% if focus_key_id %}
<a href="/key_focus?key_id={{ focus_key_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
{% else %}
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
{% endif %}
</div>
<form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="upper" id="key-upper" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" id="key-lower" step="0.0001" placeholder="下沿/支撑" required>
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
<option value="standard">标准突破</option>
<option value="box_1p5">箱体1R·止盈1.5H</option>
<option value="trend_manual">趋势单·自填止盈</option>
</select>
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<button type="submit">添加</button>
</form>
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="panel-scroll pos-list">
{% for k in key %}
<div class="pos-card" id="key-row-{{ k.id }}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ k.symbol }}</strong>
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ '做多' if k.direction == 'long' else '做空' }}</span>
{% endif %}
<span class="badge direction" style="margin-left:4px">{{ k.monitor_type }}</span>
</div>
<button type="button" class="pos-close-btn" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{ k.id }})"></button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
<span class="pos-meta-item">方案: {{ '标准突破' if (k.sl_tp_mode or 'standard') == 'standard' else ('箱体1R·止盈1.5H' if k.sl_tp_mode == 'box_1p5' else '趋势单') }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
</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>
<div class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{ k.id }}" style="color:#9aa">-</span></div>
</div>
<div class="pos-meta" style="margin-top:8px"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 style="margin-bottom:8px">关键位历史</h2>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位</div>
<div class="panel-scroll pos-list">
{% for h in key_history %}
<div class="pos-card">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ '做多' if h.direction == 'long' else '做空' }}</span>
</div>
<button type="button" class="table-del" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">{{ h.monitor_type }}</span>
<span class="pos-meta-item">{{ h.close_reason }}</span>
<span class="pos-meta-item">{{ (h.closed_at or '-')[:16] }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上: {{ h.upper }} 下: {{ h.lower }}</span>
<span class="pos-meta-item">提醒: {{ h.notification_count }}</span>
</div>
{% if h.last_alert_message %}<div style="font-size:.75rem;color:#aab;margin-top:6px;white-space:pre-wrap">{{ h.last_alert_message[:180] }}{% if h.last_alert_message|length > 180 %}…{% endif %}</div>{% endif %}
</div>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
</div>
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
@@ -2235,6 +2128,7 @@ function refreshPriceSnapshotConditional(){
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
if(page === "trade"){
+2 -108
View File
@@ -322,114 +322,7 @@
<div class="grid">
{% if page == 'key_monitor' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">关键位监控</h2>
{% if focus_key_id %}
<a href="/key_focus?key_id={{ focus_key_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
{% else %}
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
{% endif %}
</div>
<form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="upper" id="key-upper" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" id="key-lower" step="0.0001" placeholder="下沿/支撑" required>
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
<option value="standard">标准突破</option>
<option value="box_1p5">箱体1R·止盈1.5H</option>
<option value="trend_manual">趋势单·自填止盈</option>
</select>
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<button type="submit">添加</button>
</form>
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="panel-scroll pos-list">
{% for k in key %}
<div class="pos-card" id="key-row-{{ k.id }}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ k.symbol }}</strong>
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ '做多' if k.direction == 'long' else '做空' }}</span>
{% endif %}
<span class="badge direction" style="margin-left:4px">{{ k.monitor_type }}</span>
</div>
<button type="button" class="pos-close-btn" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{ k.id }})"></button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
<span class="pos-meta-item">方案: {{ '标准突破' if (k.sl_tp_mode or 'standard') == 'standard' else ('箱体1R·止盈1.5H' if k.sl_tp_mode == 'box_1p5' else '趋势单') }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
</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>
<div class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{ k.id }}" style="color:#9aa">-</span></div>
</div>
<div class="pos-meta" style="margin-top:8px"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 style="margin-bottom:8px">关键位历史</h2>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位</div>
<div class="panel-scroll pos-list">
{% for h in key_history %}
<div class="pos-card">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ '做多' if h.direction == 'long' else '做空' }}</span>
</div>
<button type="button" class="table-del" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">{{ h.monitor_type }}</span>
<span class="pos-meta-item">{{ h.close_reason }}</span>
<span class="pos-meta-item">{{ (h.closed_at or '-')[:16] }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上: {{ h.upper }} 下: {{ h.lower }}</span>
<span class="pos-meta-item">提醒: {{ h.notification_count }}</span>
</div>
{% if h.last_alert_message %}<div style="font-size:.75rem;color:#aab;margin-top:6px;white-space:pre-wrap">{{ h.last_alert_message[:180] }}{% if h.last_alert_message|length > 180 %}…{% endif %}</div>{% endif %}
</div>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
</div>
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
@@ -2219,6 +2112,7 @@ function refreshPriceSnapshotConditional(){
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
if(page === "trade"){
+14 -1
View File
@@ -5431,6 +5431,12 @@ def render_main_page(page="trade"):
orphan_positions = collect_orphan_exchange_positions(order_list, conn)
except Exception as exc:
print(f"[render_main_page] orphan positions: {exc}")
key_gate_rule_text = (
f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 0.03%~0.5%"
f"确认K收于箱外|量能>前20均量×1.3|日成交前30"
f"偏离关键位 > {KEY_BREAKOUT_LIMIT_PCT}% 不提醒|"
f"【阻力/支撑】收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不自动开仓"
)
conn.close()
return render_template(
"index.html",
@@ -5502,6 +5508,7 @@ def render_main_page(page="trade"):
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
exchange_display=EXCHANGE_DISPLAY_NAME,
key_gate_rule_text=key_gate_rule_text,
**strategy_extra,
)
@@ -5512,6 +5519,12 @@ def index():
return redirect("/trade")
@app.route("/key_monitor")
@login_required
def key_monitor_page():
return render_main_page("key_monitor")
@app.route("/trade")
@login_required
def trade_page():
@@ -6284,7 +6297,7 @@ def add_key():
conn.commit()
conn.close()
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}")
return redirect("/")
return redirect("/key_monitor")
@app.route("/add_order", methods=["POST"])
@login_required
+64 -2
View File
@@ -322,6 +322,7 @@
</div>
</div>
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
@@ -354,6 +355,8 @@
<div class="export-bar">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
<a href="/export/trade_records">交易记录</a>
<a href="/export/key_monitors">关键位(当前)</a>
<a href="/export/key_monitor_history">关键位历史</a>
</div>
<div class="stat-box">
<div class="stat-item"><div class="label">交易所</div><div class="value">{{ exchange_display }}</div></div>
@@ -368,7 +371,9 @@
{% include 'gate_transfer_block.html' %}
<div class="grid">
{% if page == 'trade' %}
{% if page == 'key_monitor' %}
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
@@ -1577,6 +1582,62 @@ if(journalForm){
}
}
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(upperEl){
upperEl.style.display = showFb ? "none" : "";
upperEl.required = !showFb;
if(showFb) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = showFb ? "none" : "";
lowerEl.required = !showFb;
if(showFb) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
@@ -1874,7 +1935,7 @@ function refreshPriceSnapshotConditional(){
(data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){
pEl.innerText = Number(k.price).toFixed(6);
pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-");
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
}
const upEl = document.getElementById(`key-up-diff-${k.id}`);
@@ -1888,6 +1949,7 @@ function refreshPriceSnapshotConditional(){
}
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
if(page === "trade"){
+2 -108
View File
@@ -330,114 +330,7 @@
<div class="grid">
{% if page == 'key_monitor' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">关键位监控</h2>
{% if focus_key_id %}
<a href="/key_focus?key_id={{ focus_key_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
{% else %}
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
{% endif %}
</div>
<form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="upper" id="key-upper" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" id="key-lower" step="0.0001" placeholder="下沿/支撑" required>
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
<option value="standard">标准突破</option>
<option value="box_1p5">箱体1R·止盈1.5H</option>
<option value="trend_manual">趋势单·自填止盈</option>
</select>
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<button type="submit">添加</button>
</form>
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="panel-scroll pos-list">
{% for k in key %}
<div class="pos-card" id="key-row-{{ k.id }}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ k.symbol }}</strong>
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ '做多' if k.direction == 'long' else '做空' }}</span>
{% endif %}
<span class="badge direction" style="margin-left:4px">{{ k.monitor_type }}</span>
</div>
<button type="button" class="pos-close-btn" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{ k.id }})"></button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
<span class="pos-meta-item">方案: {{ '标准突破' if (k.sl_tp_mode or 'standard') == 'standard' else ('箱体1R·止盈1.5H' if k.sl_tp_mode == 'box_1p5' else '趋势单') }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
</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>
<div class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{ k.id }}" style="color:#9aa">-</span></div>
</div>
<div class="pos-meta" style="margin-top:8px"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 style="margin-bottom:8px">关键位历史</h2>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位</div>
<div class="panel-scroll pos-list">
{% for h in key_history %}
<div class="pos-card">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ '做多' if h.direction == 'long' else '做空' }}</span>
</div>
<button type="button" class="table-del" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div>
<div class="pos-meta">
<span class="pos-meta-item">{{ h.monitor_type }}</span>
<span class="pos-meta-item">{{ h.close_reason }}</span>
<span class="pos-meta-item">{{ (h.closed_at or '-')[:16] }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上: {{ h.upper }} 下: {{ h.lower }}</span>
<span class="pos-meta-item">提醒: {{ h.notification_count }}</span>
</div>
{% if h.last_alert_message %}<div style="font-size:.75rem;color:#aab;margin-top:6px;white-space:pre-wrap">{{ h.last_alert_message[:180] }}{% if h.last_alert_message|length > 180 %}…{% endif %}</div>{% endif %}
</div>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
</div>
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
@@ -2268,6 +2161,7 @@ function refreshPriceSnapshotConditional(){
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
if(page === "trade"){
+173
View File
@@ -0,0 +1,173 @@
<style>
.key-row-collapse{border:1px solid #2a3348;border-radius:10px;background:#141923;overflow:hidden}
.key-row-collapse+.key-row-collapse{margin-top:0}
.key-row-collapse-summary{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:10px 12px;cursor:pointer;list-style:none;font-size:.8rem;color:#c5cde0;line-height:1.45}
.key-row-collapse-summary::-webkit-details-marker{display:none}
.key-row-collapse-summary::before{content:"▸";flex:0 0 auto;color:#6d7a99;margin-top:1px;transition:transform .15s ease}
.key-row-collapse[open]>.key-row-collapse-summary::before{transform:rotate(90deg)}
.key-row-summary-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.key-row-summary-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.key-row-summary-title strong{font-size:.88rem;color:#fff}
.key-row-summary-line{color:#9aa8c4;font-size:.76rem;word-break:break-word}
.key-row-summary-live{color:#8fc8ff;font-size:.74rem}
.key-row-summary-actions{flex:0 0 auto;display:flex;gap:6px;align-items:center}
.key-row-collapse-body{padding:0 12px 12px;border-top:1px solid #232b3d}
.key-row-collapse-body .pos-meta{margin-top:10px;margin-bottom:10px}
.key-row-collapse-body .pos-grid{margin-bottom:8px}
.key-history-alert{font-size:.75rem;color:#aab;margin-top:8px;white-space:pre-wrap;line-height:1.45}
</style>
{% macro key_direction_label(k) -%}
{% if k.direction == 'watch' %}双向{% elif k.direction == 'long' %}做多{% else %}做空{% endif %}
{%- endmacro %}
{% macro key_sl_tp_mode_label(k) -%}
{% if (k.sl_tp_mode or 'standard') == 'standard' %}标准突破{% elif k.sl_tp_mode == 'box_1p5' %}箱体1R·止盈1.5H{% else %}趋势单{% endif %}
{%- endmacro %}
{% macro key_monitor_brief(k) -%}
上{{ k.upper }} / 下{{ k.lower }} · 提醒 {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
{%- if k.monitor_type in ['箱体突破','收敛突破'] %} · {{ key_sl_tp_mode_label(k) }}{% endif %}
{%- if k.breakeven_enabled %} · 保本开{% else %} · 保本关{% endif %}
{%- endmacro %}
{% macro key_history_brief(h) -%}
{{ h.close_reason or '—' }} · {{ (h.closed_at or '-')[:16] }} · 上{{ h.upper }} / 下{{ h.lower }} · 提醒 {{ h.notification_count or 0 }}
{%- endmacro %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">关键位监控</h2>
{% if focus_key_id %}
<a href="/key_focus?key_id={{ focus_key_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
{% else %}
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
{% endif %}
</div>
<form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="upper" id="key-upper" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" id="key-lower" step="0.0001" placeholder="下沿/支撑" required>
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
<option value="standard">标准突破</option>
<option value="box_1p5">箱体1R·止盈1.5H</option>
<option value="trend_manual">趋势单·自填止盈</option>
</select>
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<button type="submit">添加</button>
</form>
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="panel-scroll pos-list">
{% for k in key %}
<details class="key-row-collapse" id="key-row-{{ k.id }}">
<summary class="key-row-collapse-summary">
<span class="key-row-summary-main">
<span class="key-row-summary-title">
<strong>{{ k.symbol }}</strong>
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(k) }}</span>
{% endif %}
<span class="badge direction">{{ k.monitor_type }}</span>
</span>
<span class="key-row-summary-line">{{ key_monitor_brief(k) }}</span>
<span class="key-row-summary-live" id="key-summary-live-{{ k.id }}">现价 — · 门控 —</span>
</span>
<span class="key-row-summary-actions">
<button type="button" class="table-del" onclick="event.preventDefault(); event.stopPropagation(); deleteKeyMonitor({{ k.id }})"></button>
</span>
</summary>
<div class="key-row-collapse-body">
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
<span class="pos-meta-item">方案: {{ key_sl_tp_mode_label(k) }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
</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>
<div class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{ k.id }}" style="color:#9aa">-</span></div>
</div>
<div class="pos-meta"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
</details>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 style="margin-bottom:8px">关键位历史</h2>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位 · 点击展开详情</div>
<div class="panel-scroll pos-list">
{% for h in key_history %}
<details class="key-row-collapse">
<summary class="key-row-collapse-summary">
<span class="key-row-summary-main">
<span class="key-row-summary-title">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(h) }}</span>
<span class="badge direction">{{ h.monitor_type }}</span>
</span>
<span class="key-row-summary-line">{{ key_history_brief(h) }}</span>
</span>
<span class="key-row-summary-actions">
<button type="button" class="table-del" onclick="event.preventDefault(); event.stopPropagation(); deleteKeyHistory({{ h.id }})">删除</button>
</span>
</summary>
<div class="key-row-collapse-body">
<div class="pos-meta">
<span class="pos-meta-item">类型: {{ h.monitor_type }}</span>
<span class="pos-meta-item">结案: {{ h.close_reason or '—' }}</span>
<span class="pos-meta-item">时间: {{ h.closed_at or '—' }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ h.upper }}</span>
<span class="pos-meta-item">下沿: {{ h.lower }}</span>
<span class="pos-meta-item">提醒次数: {{ h.notification_count or 0 }}</span>
</div>
{% if h.last_alert_message %}
<div class="key-history-alert">{{ h.last_alert_message }}</div>
{% endif %}
</div>
</details>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
</div>
<script>
function paintKeyMonitorSummary(id, snap){
const el = document.getElementById(`key-summary-live-${id}`);
if(!el || !snap) return;
const px = snap.price_display || (Number.isFinite(Number(snap.price)) ? Number(snap.price).toFixed(6) : "—");
const gate = snap.gate_summary || "—";
el.innerText = `现价 ${px} · 门控 ${gate}`;
}
</script>