From 92c222584e115711cc48732cfc894d6acae3def3 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 30 Jun 2026 00:12:10 +0800 Subject: [PATCH] Show tablet trade records as close-record table with action column. Co-authored-by: Cursor --- static/css/records.css | 172 ++++++++++++++++++++++++----------------- static/js/records.js | 38 +++++---- templates/records.html | 158 +++++++++++++++++++++++++------------ trade_log_lib.py | 16 ++++ 4 files changed, 249 insertions(+), 135 deletions(-) diff --git a/static/css/records.css b/static/css/records.css index 65e3cc2..9d6b722 100644 --- a/static/css/records.css +++ b/static/css/records.css @@ -181,33 +181,16 @@ line-height: 1.45; } -.records-trade-actions { - display: none; - align-items: center; - gap: .3rem; - flex-shrink: 0; - flex-wrap: nowrap; -} - .records-trade-verify-form { display: inline-flex; margin: 0; } -.records-trade-actions .btn-link, -.records-trade-actions a, -.records-trade-actions button { - font-size: .72rem; - padding: .3rem .5rem; - border-radius: 6px; - text-decoration: none; - border: none; - cursor: pointer; - white-space: nowrap; - flex-shrink: 0; +.records-trade-row-actions { + min-width: 0; } -.records-trade-actions .btn-link { +.records-trade-row-actions .btn-link { background: transparent; color: var(--accent); } @@ -218,6 +201,76 @@ margin-top: .35rem; } +.records-phone-only, +.records-tablet-only { + display: none; +} + +/* 平板交易记录表格(对齐数据看板平仓记录) */ +.records-trade-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.records-trade-table-wrap .dashboard-table { + width: 100%; + border-collapse: collapse; + font-size: .82rem; +} + +.records-trade-table-wrap .dashboard-table th, +.records-trade-table-wrap .dashboard-table td { + padding: .45rem .55rem; + border-bottom: 1px solid var(--table-border); + text-align: left; + white-space: nowrap; + font-variant-numeric: tabular-nums; + vertical-align: middle; +} + +.records-trade-table-wrap .dashboard-table tbody tr:last-child td { + border-bottom: none; +} + +.records-trade-table-wrap .dash-symbol-ex { + font-weight: 400; + font-size: .78rem; +} + +.records-trade-table-wrap .dash-main-badge { + font-size: .68rem; + vertical-align: middle; +} + +.records-trade-table-wrap .dashboard-table .badge.dir-long, +.records-trade-table-wrap .dashboard-table .badge.dir-short { + font-size: .72rem; +} + +.records-trade-table-wrap .dashboard-table .badge.dir-long { + background: var(--profit-bg); + color: var(--profit); +} + +.records-trade-table-wrap .dashboard-table .badge.dir-short { + background: var(--loss-bg); + color: var(--loss); +} + +.records-trade-table-wrap .pnl-pos { + color: var(--profit); + font-weight: 600; +} + +.records-trade-table-wrap .pnl-neg { + color: var(--loss); + font-weight: 600; +} + +.records-trade-table-wrap .trade-actions { + min-width: 17rem; +} + html:is([data-mobile="1"], .layout-phone) .records-trade-row, html:is([data-layout="phone"], .layout-phone) .records-trade-row { cursor: pointer; @@ -229,56 +282,18 @@ html:is([data-layout="phone"], .layout-phone) .records-trade-row:hover { box-shadow: 0 0 0 1px rgba(56, 189, 248, .2); } -/* 平板横屏:单行 + 右侧操作 */ -html:is([data-layout="tablet"], .layout-tablet) .records-trade-row { - display: flex; - flex-direction: row; - align-items: center; - gap: .5rem .65rem; - padding: .5rem .65rem; - overflow: hidden; +html:is([data-mobile="1"], .layout-phone) .records-page .records-phone-only, +html:is([data-layout="phone"], .layout-phone) .records-page .records-phone-only, +html:is([data-mobile="1"], .layout-phone) .records-page .records-review-mobile, +html:is([data-layout="phone"], .layout-phone) .records-page .records-review-mobile { + display: block; } -html:is([data-layout="tablet"], .layout-tablet) .records-trade-main { - flex: 1 1 auto; - min-width: 0; - display: flex; - flex-direction: row; - align-items: center; - gap: .55rem .75rem; +html:is([data-layout="tablet"], .layout-tablet) .records-page .records-tablet-only { + display: block; } -html:is([data-layout="tablet"], .layout-tablet) .records-trade-head { - margin-bottom: 0; - flex: 0 1 auto; - min-width: 0; -} - -html:is([data-layout="tablet"], .layout-tablet) .records-mobile-symbol { - font-size: .84rem; - max-width: 6.5rem; -} - -html:is([data-layout="tablet"], .layout-tablet) .records-trade-summary { - flex: 1 1 auto; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: .76rem; -} - -html:is([data-layout="tablet"], .layout-tablet) .records-trade-actions { - display: flex; -} - -html:is([data-layout="tablet"], .layout-tablet) .records-trade-phone-foot { - display: none; -} - -html:is([data-mobile="1"], .layout-phone) .records-page .records-mobile-list, -html:is([data-layout="phone"], .layout-phone) .records-page .records-mobile-list, -html:is([data-layout="tablet"], .layout-tablet) .records-page .records-mobile-list { +html:is([data-layout="tablet"], .layout-tablet) .records-page .records-review-mobile { display: block; } @@ -293,11 +308,29 @@ html:is([data-layout="phone"], .layout-phone) .records-page .records-trade-card padding: 0; } -html:is([data-layout="tablet"], .layout-tablet) .records-page .records-trade-card .card-body, -html:is([data-layout="tablet"], .layout-tablet) .records-page .records-split .card .card-body { +html:is([data-layout="tablet"], .layout-tablet) .records-page .records-trade-card .card-body { padding: 0 .75rem .35rem; } +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .dashboard-table { + font-size: .78rem; +} + +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .dashboard-table th, +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .dashboard-table td { + padding: .4rem .45rem; +} + +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .trade-actions { + min-width: 15.5rem; +} + +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .trade-actions a, +html:is([data-layout="tablet"], .layout-tablet) .records-trade-table-wrap .trade-actions button { + font-size: .68rem; + padding: .28rem .45rem; +} + html:is([data-mobile="1"], .layout-phone) .records-page .records-equity-card #equity-curve-chart, html:is([data-layout="phone"], .layout-phone) .records-page .records-equity-card #equity-curve-chart { min-height: 180px; @@ -336,6 +369,7 @@ html:is([data-layout="phone"], .layout-phone) #review-modal .review-detail-value } @media (pointer: coarse) and (max-width: 600px) { - .records-page .records-mobile-list { display: block; } + .records-page .records-phone-only, + .records-page .records-review-mobile { display: block; } .records-page .records-desktop-only { display: none !important; } } diff --git a/static/js/records.js b/static/js/records.js index c9d3ce1..23e4cd5 100644 --- a/static/js/records.js +++ b/static/js/records.js @@ -12,11 +12,6 @@ return String(v).replace('T', ' ').slice(0, 16); } - function isTabletLayout() { - var root = document.documentElement; - return root.dataset.layout === 'tablet' || root.classList.contains('layout-tablet'); - } - function showTradeModal(data) { var mask = document.getElementById('trade-detail-modal'); var body = document.getElementById('trade-detail-modal-body'); @@ -53,7 +48,7 @@ html += '
'; if (data.fill_review_url) { - html += '复盘'; + html += '填入复盘'; } if (data.del_url) { html += '删除'; @@ -64,6 +59,13 @@ mask.classList.add('show'); } + function openRowDetail(row) { + if (!row) return; + try { + showTradeModal(JSON.parse(row.getAttribute('data-trade'))); + } catch (err) { /* ignore */ } + } + function bindTradeModal() { var mask = document.getElementById('trade-detail-modal'); if (!mask) return; @@ -77,18 +79,20 @@ if (e.target === mask) mask.classList.remove('show'); }); - var list = document.getElementById('records-trade-mobile'); - if (!list) return; + var phoneList = document.getElementById('records-trade-mobile'); + if (phoneList) { + phoneList.addEventListener('click', function (e) { + openRowDetail(e.target.closest('.records-trade-row')); + }); + } - list.addEventListener('click', function (e) { - if (e.target.closest('.records-trade-actions')) return; - var row = e.target.closest('.records-trade-row'); - if (!row) return; - if (isTabletLayout() && !e.target.closest('.records-trade-detail-btn')) return; - try { - showTradeModal(JSON.parse(row.getAttribute('data-trade'))); - } catch (err) { /* ignore */ } - }); + var tabletWrap = document.querySelector('.records-trade-table-wrap'); + if (tabletWrap) { + tabletWrap.addEventListener('click', function (e) { + if (!e.target.closest('.records-tablet-detail-btn')) return; + openRowDetail(e.target.closest('tr[data-trade]')); + }); + } } function bootRecordsPage() { diff --git a/templates/records.html b/templates/records.html index 8f9e5c5..384fa76 100644 --- a/templates/records.html +++ b/templates/records.html @@ -5,6 +5,75 @@ {% endblock %} {% block content %} +{% macro trade_detail_json(t) -%} +{{ { + "symbol": t.symbol_name or t.symbol, + "symbol_code": t.symbol, + "monitor_type": t.monitor_type, + "source": "柜台" if t.source == "ctp" else "本地", + "direction": "做多" if t.direction == "long" else "做空", + "entry_price": t.entry_price, + "close_price": t.close_price, + "stop_loss": t.stop_loss, + "take_profit": t.take_profit, + "lots": t.lots, + "margin": t.margin, + "margin_pct": t.margin_pct, + "holding_minutes": t.holding_minutes or 0, + "open_time": t.open_time, + "close_time": t.close_time, + "pnl": t.pnl, + "fee": t.fee, + "pnl_net": t.pnl_net, + "equity_after": t.equity_after, + "result": t.result, + "verified": t.verified, + "fill_review_url": url_for("fill_review_from_trade", tid=t.id), + "del_url": url_for("del_trade", tid=t.id) +} | tojson }} +{%- endmacro %} +{% macro trade_symbol_cell(t) %} +
+
+ {{ t.symbol_name or t.symbol }} + {% if t.symbol_exchange %}{{ t.symbol_exchange }}{% endif %} + {% if t.symbol_is_main %}主力{% endif %} + {% if t.symbol and (t.symbol_name or '')|lower != (t.symbol or '')|lower %} + {{ t.symbol }} + {% endif %} +
+
+{% endmacro %} +{% macro trade_pnl_cell(v) %} +{% if v is not none %}{% set n = v|float %}{{ ('+' if n > 0 else '') ~ ('%.2f'|format(n)) }} 元{% else %}—{% endif %} +{% endmacro %} +{% macro trade_verify_form(t) %} +
+ + + + + + + + + + + + + + + +
+{% endmacro %} +{% macro trade_row_actions(t, detail_class) %} +
+ + 填入复盘 + {{ trade_verify_form(t) }} + 删除 +
+{% endmacro %}

资金曲线

@@ -28,37 +97,12 @@ 修改/核对开关(开启后可编辑关键字段) -
+
{% for t in trades %} {% set trade_pnl = t.pnl_net if t.pnl_net is not none else t.pnl %} {% set trade_px = t.close_price if t.close_price else t.entry_price %} {% set pnl_cls = 'is-profit' if trade_pnl and trade_pnl > 0 else ('is-loss' if trade_pnl and trade_pnl < 0 else 'is-flat') %} -
+
@@ -75,28 +119,6 @@ 盈亏 {{ trade_pnl if trade_pnl is not none else '-' }}
-
- - 复盘 -
- - - - - - - - - - - - - - - -
- 删除 -
详情 ›
@@ -105,6 +127,44 @@

暂无交易记录

{% endfor %}
+
+ + + + + + + + + + + + + + + + {% for t in trades %} + + + + + + + + + + + + {% else %} + + {% endfor %} + +
品种方向手数开仓平仓盈亏净盈亏平仓时间操作
{{ trade_symbol_cell(t) }} + + {{ '做多' if t.direction == 'long' else '做空' }} + + {% if t.lots is not none %}{{ '%.2f'|format(t.lots|float) }}{% else %}—{% endif %}{% if t.entry_price is not none %}{{ '%.2f'|format(t.entry_price|float) }}{% else %}—{% endif %}{% if t.close_price is not none %}{{ '%.2f'|format(t.close_price|float) }}{% elif t.entry_price is not none %}{{ '%.2f'|format(t.entry_price|float) }}{% else %}—{% endif %}{{ trade_pnl_cell(t.pnl) }}{{ trade_pnl_cell(t.pnl_net) }}{{ (t.close_time or '')[:16].replace('T', ' ') or '—' }}{{ trade_row_actions(t, 'records-tablet-detail-btn') }}
暂无交易记录
+
diff --git a/trade_log_lib.py b/trade_log_lib.py index bf207c0..d276fc5 100644 --- a/trade_log_lib.py +++ b/trade_log_lib.py @@ -77,6 +77,21 @@ def purge_duplicate_local_trade_logs(conn) -> int: return removed +def _attach_symbol_meta(t: dict[str, Any]) -> None: + try: + from symbols import position_symbol_meta + + sym = (t.get("symbol") or "").strip() + meta = position_symbol_meta(sym) + if not t.get("symbol_name"): + t["symbol_name"] = meta.get("name") or sym + t["symbol_exchange"] = meta.get("exchange") or "" + t["symbol_is_main"] = bool(meta.get("is_main")) + except Exception: + t.setdefault("symbol_exchange", "") + t.setdefault("symbol_is_main", False) + + def enrich_trades_for_records( trades: list[dict[str, Any]], *, @@ -92,6 +107,7 @@ def enrich_trades_for_records( curve: list[dict[str, Any]] = [] for t in chrono: + _attach_symbol_meta(t) pnl_net = float(t.get("pnl_net") or 0) eq = t.get("equity_after") if eq is None: