From 67683f55629df39d4ee6014bb5e4984c47682e8a Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 12:46:23 +0800 Subject: [PATCH] =?UTF-8?q?ui:=20=E9=A1=B6=E6=A0=8F=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E3=80=81=E8=AE=BE=E7=BD=AE=E4=B8=A4=E5=88=97=E3=80=81=E4=B8=8B?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E6=8C=81=E4=BB=93=E7=9B=91=E6=8E=A7=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 导航栏与页面背景一致;系统设置双列布局;下单三行表单与开仓状态反馈;持仓卡片增加平仓与止盈止损挂单展示。 Co-authored-by: Cursor --- install_trading.py | 83 ++++++++++++++++++++++++++++++ static/css/tech.css | 4 +- static/css/trade.css | 26 +++++++--- static/js/trade.js | 111 +++++++++++++++++++++++++++++++++------- templates/base.html | 12 ++--- templates/settings.html | 24 +++++++-- templates/trade.html | 74 ++++++++++++++------------- vnpy_bridge.py | 45 ++++++++++++++++ 8 files changed, 304 insertions(+), 75 deletions(-) diff --git a/install_trading.py b/install_trading.py index 8ff57a1..cb12bb4 100644 --- a/install_trading.py +++ b/install_trading.py @@ -48,6 +48,7 @@ from ctp_symbol import ths_to_vnpy_symbol from vnpy_bridge import ( ctp_connect, ctp_get_account, + ctp_list_active_orders, ctp_list_positions, ctp_status, execute_order, @@ -110,6 +111,68 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se except Exception: return "" + def _build_pending_orders(conn, mode: str) -> list[dict]: + pending: list[dict] = [] + for r in conn.execute( + "SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC" + ).fetchall(): + mon = dict(r) + sym = mon.get("symbol") or "" + direction = mon.get("direction") or "long" + lots = int(mon.get("lots") or 0) + base = { + "symbol_code": sym, + "symbol": mon.get("symbol_name") or sym, + "direction": direction, + "direction_label": "做多" if direction == "long" else "做空", + "lots": lots, + "source": "monitor", + } + sl = mon.get("stop_loss") + tp = mon.get("take_profit") + if sl is not None: + pending.append({ + **base, + "order_kind": "stop_loss", + "label": "止损挂单", + "price": float(sl), + }) + if tp is not None: + pending.append({ + **base, + "order_kind": "take_profit", + "label": "止盈挂单", + "price": float(tp), + }) + ctp_st = ctp_status(mode) + if ctp_st.get("connected"): + for o in _ctp_active_orders(mode): + sym = o.get("symbol") or "" + offset_s = (o.get("offset") or "").upper() + kind = "limit" + label = "委托挂单" + if "CLOSE" in offset_s: + label = "平仓委托" + pending.append({ + "symbol_code": sym, + "symbol": sym, + "direction": o.get("direction") or "long", + "direction_label": "做多" if o.get("direction") == "long" else "做空", + "lots": int(o.get("lots") or 0), + "price": float(o.get("price") or 0), + "order_kind": kind, + "label": label, + "source": "ctp", + "order_id": o.get("order_id"), + }) + return pending + + def _ctp_active_orders(mode: str) -> list: + try: + return ctp_list_active_orders(mode) + except Exception: + return [] + def _build_trading_live_rows(conn) -> list[dict]: from zoneinfo import ZoneInfo tz = ZoneInfo("Asia/Shanghai") @@ -150,6 +213,23 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se break sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None + pending_for_row: list[dict] = [] + if sl is not None: + pending_for_row.append({ + "order_kind": "stop_loss", + "label": "止损挂单", + "price": sl, + "lots": lots, + "source": "monitor", + }) + if tp is not None: + pending_for_row.append({ + "order_kind": "take_profit", + "label": "止盈挂单", + "price": tp, + "lots": lots, + "source": "monitor", + }) rows.append({ "key": f"ctp:{sym.lower()}:{direction}", "source": "ctp", @@ -169,6 +249,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se "price_precision": tick.get("price_precision"), "tick_size": tick.get("tick_size"), "can_close": True, + "pending_orders": pending_for_row, }) return rows @@ -234,11 +315,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se mode = get_trading_mode(get_setting) ctp_st = ctp_status(mode) rows = _build_trading_live_rows(conn) + pending_orders = _build_pending_orders(conn, mode) capital = _capital(conn) risk = get_risk_status(conn) conn.commit() return jsonify({ "rows": rows, + "pending_orders": pending_orders, "capital": capital, "ctp_status": ctp_st, "trading_mode_label": trading_mode_label(get_setting), diff --git a/static/css/tech.css b/static/css/tech.css index d32c145..c439bec 100644 --- a/static/css/tech.css +++ b/static/css/tech.css @@ -56,8 +56,8 @@ .site-header{ border-bottom:1px solid var(--border-header); - background:var(--header-bg); - backdrop-filter:blur(12px); + background:transparent; + backdrop-filter:none; } .site-header::after{ content:"";display:block;height:1px;margin-top:-1px; diff --git a/static/css/trade.css b/static/css/trade.css index dc3141c..66105f2 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -10,26 +10,38 @@ .trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem} .trade-order-status-compact{margin-top:0} .trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem} -.trade-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem .65rem;margin-bottom:.85rem} -.trade-form-grid .span-2{grid-column:span 2} +.trade-form-rows{display:flex;flex-direction:column;gap:.75rem;margin-bottom:.85rem} +.trade-form-line{display:grid;gap:.65rem;align-items:end} +.trade-form-line.line-3{grid-template-columns:1.4fr 0.8fr 0.8fr} .trade-field label{display:block;font-size:.72rem;margin-bottom:.28rem;color:var(--text-label)} .trade-field select,.trade-field input{width:100%;box-sizing:border-box} .trade-field .lots-auto{color:var(--accent);font-weight:600;background:var(--card-inner);cursor:default} .price-type-tabs{display:flex;gap:.35rem;margin-bottom:.35rem} -.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center} +.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center;width:auto} .price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600;background:rgba(56,189,248,.08)} .market-hint{font-size:.7rem;margin-top:.25rem} -.trade-action-row{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin:.85rem 0 .55rem} -.trade-action-row .btn-open,.trade-action-row .btn-secondary{padding:.6rem .75rem;font-size:.88rem;width:100%} +.trade-action-row{display:flex;flex-direction:column;gap:.45rem;margin:.85rem 0 .55rem} +.trade-action-row .btn-open{padding:.65rem .75rem;font-size:.9rem;width:100%} +.trade-action-row .btn-open:disabled{opacity:.65;cursor:wait} +.trade-order-msg{font-size:.82rem;text-align:center;margin:0;padding:.35rem} +.trade-order-msg.ok{color:var(--profit)} +.trade-order-msg.err{color:var(--loss)} .trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem} .trade-footer strong{color:var(--accent)} .rec-blocked td{opacity:.55} .rec-ok td:first-child{font-weight:600} #positions .card-body{max-height:460px;overflow-y:auto} +.pos-pending-orders{margin-top:.55rem;padding-top:.55rem;border-top:1px dashed var(--table-border)} +.pos-pending-orders .pending-title{font-size:.68rem;color:var(--text-muted);margin-bottom:.35rem} +.pos-pending-item{display:flex;justify-content:space-between;align-items:center;gap:.5rem;font-size:.75rem;padding:.35rem .5rem;border-radius:6px;margin-bottom:.25rem;background:var(--list-item-bg)} +.pos-pending-item.sl{border-left:3px solid var(--loss)} +.pos-pending-item.tp{border-left:3px solid var(--profit)} +.pos-pending-item.ctp{border-left:3px solid var(--accent)} +.pos-close-btn{padding:.4rem .85rem;font-size:.78rem;border-radius:8px;border:1px solid var(--loss);background:var(--loss-bg);color:var(--loss);cursor:pointer;white-space:nowrap;width:auto;flex-shrink:0} +.pos-close-btn:disabled{opacity:.55;cursor:wait} @media (max-width:900px){ .trade-row-split{grid-template-columns:1fr} #positions .card-body{max-height:360px} - .trade-form-grid{grid-template-columns:1fr} - .trade-form-grid .span-2{grid-column:span 1} + .trade-form-line.line-3{grid-template-columns:1fr} } diff --git a/static/js/trade.js b/static/js/trade.js index ff261f8..6a63b22 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -177,28 +177,46 @@ }); } + function showOrderMsg(text, ok) { + var el = document.getElementById('order-msg'); + if (!el) return; + if (!text) { + el.hidden = true; + el.textContent = ''; + el.className = 'trade-order-msg'; + return; + } + el.hidden = false; + el.textContent = text; + el.className = 'trade-order-msg ' + (ok ? 'ok' : 'err'); + } + function postOrder(offset) { var sym = selectedSymbol(); - if (!sym) { alert('请选择品种'); return; } + if (!sym) { showOrderMsg('请选择品种', false); return; } var direction = dirSelect ? dirSelect.value : 'long'; var price = entryPrice(); if (!price || price <= 0) { - alert('无法获取有效价格,请先填写或刷新行情'); + showOrderMsg('无法获取有效价格,请先填写或刷新行情', false); return; } var lots = effectiveLots(); if (offset === 'open') { if (isRiskMode() && lots <= 0) { - alert('请填写止损,系统将自动计算手数'); + showOrderMsg('请填写止损,系统将自动计算手数', false); return; } if (!isRiskMode() && lots <= 0) { - alert('请填写手数'); + showOrderMsg('请填写手数', false); return; } - } else { - lots = parseInt(lotsInput && lotsInput.value, 10) || 1; } + var btnOpen = document.getElementById('btn-open'); + if (btnOpen) { + btnOpen.disabled = true; + btnOpen.textContent = '开仓中…'; + } + showOrderMsg('开仓中…', true); var body = { symbol: sym, offset: offset, @@ -214,22 +232,48 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) { return r.json(); }).then(function (data) { - if (!data.ok) { alert(data.error || '下单失败'); return; } - alert((offset === 'open' ? '开仓' : '平仓') + '已提交 ' + (data.lots || lots) + ' 手'); + if (!data.ok) { + showOrderMsg(data.error || '下单失败', false); + return; + } + showOrderMsg('开仓成功 · ' + (data.lots || lots) + ' 手', true); pollPositions(); refreshQuote(); + setTimeout(function () { showOrderMsg(''); }, 4000); + }).catch(function () { + showOrderMsg('网络错误,请重试', false); + }).finally(function () { + if (btnOpen) { + btnOpen.disabled = false; + btnOpen.textContent = '开仓'; + } }); } + function buildPendingHtml(items) { + if (!items || !items.length) return ''; + var rows = items.map(function (p) { + var cls = p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp'); + return ( + '
' + + '' + (p.label || '挂单') + '' + + '' + fmtNum(p.price) + ' · ' + (p.lots || 1) + ' 手' + + '
' + ); + }).join(''); + return '
止盈止损挂单
' + rows + '
'; + } + function buildPosCard(row) { var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : ''); var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--'; var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); + var closePayload = encodeURIComponent(JSON.stringify({ + source: row.source, symbol_code: row.symbol_code, direction: row.direction, + lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null + })); var closeBtn = row.can_close ? - '' : ''; + '' : ''; return ( '
' + '
' + row.symbol + ' ' + dirBadge + '
' + @@ -240,21 +284,39 @@ '
' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '
' + '
' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '
' + '
' + pnlText + '
' + - '
' + '
' + buildPendingHtml(row.pending_orders) + + '' ); } - function closePosition(payload) { + function closePosition(payload, btn) { function doClose(price) { if (!price || price <= 0) { alert('无法获取现价'); return; } if (!confirm('确认平仓 ' + payload.lots + ' 手?')) return; + if (btn) { + btn.disabled = true; + btn.textContent = '平仓中…'; + } fetch('/api/trading/close', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({}, payload, { price: price })) }).then(function (r) { return r.json(); }).then(function (d) { - if (!d.ok) { alert(d.error || '平仓失败'); return; } + if (!d.ok) { + alert(d.error || '平仓失败'); + if (btn) { + btn.disabled = false; + btn.textContent = '平仓'; + } + return; + } + if (btn) btn.textContent = '已平仓'; pollPositions(); + }).catch(function () { + if (btn) { + btn.disabled = false; + btn.textContent = '平仓'; + } }); } if (payload.mark_price > 0) { @@ -285,13 +347,26 @@ return; } if (!rows.length) { - list.innerHTML = '
柜台暂无持仓。
'; + var pendingOnly = data.pending_orders || []; + if (pendingOnly.length) { + list.innerHTML = '
柜台暂无持仓
' + + pendingOnly.map(function (p) { + return ( + '
' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '' + + '' + fmtNum(p.price) + ' · ' + (p.lots || 1) + ' 手
' + ); + }).join(''); + } else { + list.innerHTML = '
柜台暂无持仓。
'; + } return; } list.innerHTML = rows.map(buildPosCard).join(''); list.querySelectorAll('[data-close]').forEach(function (btn) { btn.addEventListener('click', function () { - closePosition(JSON.parse(btn.getAttribute('data-close'))); + closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn); }); }); }) @@ -357,9 +432,7 @@ } var btnOpen = document.getElementById('btn-open'); - var btnClose = document.getElementById('btn-close-pos'); if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); }); - if (btnClose) btnClose.addEventListener('click', function () { postOrder('close'); }); var btnConnect = document.getElementById('btn-ctp-connect'); if (btnConnect) { diff --git a/templates/base.html b/templates/base.html index ffe0326..8a1f38f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,7 +25,7 @@ --bg-page:#050508; --bg-grid:rgba(76,194,255,.045); --border-header:rgba(76,194,255,.12); - --header-bg:rgba(8,10,18,.75); + --header-bg:transparent; --text-primary:#e8eaf6; --text-title:#ffffff; --text-muted:#7a82a0; @@ -79,8 +79,8 @@ [data-theme="light"]{ --bg-page:#e8eef8; --bg-grid:rgba(37,99,235,.07); - --border-header:rgba(37,99,235,.15); - --header-bg:rgba(255,255,255,.82); + --border-header:rgba(37,99,235,.12); + --header-bg:transparent; --text-primary:#1a2233; --text-title:#0a1628; --text-muted:#5c6578; @@ -162,13 +162,13 @@ .site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap} .site-nav a{ padding:.55rem 1.15rem;border-radius:8px; - border:1px solid var(--nav-border); - background:var(--nav-bg); + border:1px solid transparent; + background:transparent; color:var(--text-primary); text-decoration:none;font-size:.88rem; transition:.2s;white-space:nowrap; } - .site-nav a:hover{background:var(--nav-hover);border-color:var(--accent);color:var(--text-title)} + .site-nav a:hover{background:var(--nav-hover);border-color:var(--nav-border);color:var(--text-title)} .site-nav a.active{background:var(--nav-active);border-color:var(--nav-active-border);color:#fff} .user-bar{position:absolute;top:1rem;right:1.5rem;font-size:.8rem;color:var(--text-muted);white-space:nowrap} .user-bar a{color:var(--danger);text-decoration:none;margin-left:.5rem} diff --git a/templates/settings.html b/templates/settings.html index 69717c3..26ba153 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1,15 +1,26 @@ {% extends "base.html" %} {% block title %}系统设置 - 国内期货监控系统{% endblock %} +{% block extra_css %} + +{% endblock %} {% block content %} +
+

导航显示

关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。

-
+
{% for key, label in nav_toggles.items() %} -
-
-
- - -
-
-
-
- - -
- -
- - - -
- -
- -
- - +
+
+
+ + +
+
+
+
+ + +
+
+ + +
- -
-
- - -
-
- - +
+
+ +
+ + +
+ + +
+
+ + +
+
+ + +
- +