Show tablet trade records as close-record table with action column.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 00:12:10 +08:00
parent a6b3c4a657
commit 92c222584e
4 changed files with 249 additions and 135 deletions
+103 -69
View File
@@ -181,33 +181,16 @@
line-height: 1.45; line-height: 1.45;
} }
.records-trade-actions {
display: none;
align-items: center;
gap: .3rem;
flex-shrink: 0;
flex-wrap: nowrap;
}
.records-trade-verify-form { .records-trade-verify-form {
display: inline-flex; display: inline-flex;
margin: 0; margin: 0;
} }
.records-trade-actions .btn-link, .records-trade-row-actions {
.records-trade-actions a, min-width: 0;
.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-actions .btn-link { .records-trade-row-actions .btn-link {
background: transparent; background: transparent;
color: var(--accent); color: var(--accent);
} }
@@ -218,6 +201,76 @@
margin-top: .35rem; 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-mobile="1"], .layout-phone) .records-trade-row,
html:is([data-layout="phone"], .layout-phone) .records-trade-row { html:is([data-layout="phone"], .layout-phone) .records-trade-row {
cursor: pointer; 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); box-shadow: 0 0 0 1px rgba(56, 189, 248, .2);
} }
/* 平板横屏:单行 + 右侧操作 */ html:is([data-mobile="1"], .layout-phone) .records-page .records-phone-only,
html:is([data-layout="tablet"], .layout-tablet) .records-trade-row { html:is([data-layout="phone"], .layout-phone) .records-page .records-phone-only,
display: flex; html:is([data-mobile="1"], .layout-phone) .records-page .records-review-mobile,
flex-direction: row; html:is([data-layout="phone"], .layout-phone) .records-page .records-review-mobile {
align-items: center; display: block;
gap: .5rem .65rem;
padding: .5rem .65rem;
overflow: hidden;
} }
html:is([data-layout="tablet"], .layout-tablet) .records-trade-main { html:is([data-layout="tablet"], .layout-tablet) .records-page .records-tablet-only {
flex: 1 1 auto; display: block;
min-width: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: .55rem .75rem;
} }
html:is([data-layout="tablet"], .layout-tablet) .records-trade-head { html:is([data-layout="tablet"], .layout-tablet) .records-page .records-review-mobile {
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 {
display: block; display: block;
} }
@@ -293,11 +308,29 @@ html:is([data-layout="phone"], .layout-phone) .records-page .records-trade-card
padding: 0; 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-trade-card .card-body {
html:is([data-layout="tablet"], .layout-tablet) .records-page .records-split .card .card-body {
padding: 0 .75rem .35rem; 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-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 { html:is([data-layout="phone"], .layout-phone) .records-page .records-equity-card #equity-curve-chart {
min-height: 180px; 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) { @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; } .records-page .records-desktop-only { display: none !important; }
} }
+21 -17
View File
@@ -12,11 +12,6 @@
return String(v).replace('T', ' ').slice(0, 16); 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) { function showTradeModal(data) {
var mask = document.getElementById('trade-detail-modal'); var mask = document.getElementById('trade-detail-modal');
var body = document.getElementById('trade-detail-modal-body'); var body = document.getElementById('trade-detail-modal-body');
@@ -53,7 +48,7 @@
html += '<div class="records-detail-actions">'; html += '<div class="records-detail-actions">';
if (data.fill_review_url) { if (data.fill_review_url) {
html += '<a href="' + data.fill_review_url + '" class="btn-fill">复盘</a>'; html += '<a href="' + data.fill_review_url + '" class="btn-fill">填入复盘</a>';
} }
if (data.del_url) { if (data.del_url) {
html += '<a href="' + data.del_url + '" class="btn-del" onclick="return confirm(\'删除?\')">删除</a>'; html += '<a href="' + data.del_url + '" class="btn-del" onclick="return confirm(\'删除?\')">删除</a>';
@@ -64,6 +59,13 @@
mask.classList.add('show'); mask.classList.add('show');
} }
function openRowDetail(row) {
if (!row) return;
try {
showTradeModal(JSON.parse(row.getAttribute('data-trade')));
} catch (err) { /* ignore */ }
}
function bindTradeModal() { function bindTradeModal() {
var mask = document.getElementById('trade-detail-modal'); var mask = document.getElementById('trade-detail-modal');
if (!mask) return; if (!mask) return;
@@ -77,20 +79,22 @@
if (e.target === mask) mask.classList.remove('show'); if (e.target === mask) mask.classList.remove('show');
}); });
var list = document.getElementById('records-trade-mobile'); var phoneList = document.getElementById('records-trade-mobile');
if (!list) return; if (phoneList) {
phoneList.addEventListener('click', function (e) {
list.addEventListener('click', function (e) { openRowDetail(e.target.closest('.records-trade-row'));
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() { function bootRecordsPage() {
if (!document.querySelector('.records-page')) return; if (!document.querySelector('.records-page')) return;
bindTradeModal(); bindTradeModal();
+109 -49
View File
@@ -5,6 +5,75 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/records.css') }}?v={{ asset_v }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/records.css') }}?v={{ asset_v }}">
{% endblock %} {% endblock %}
{% block content %} {% 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) %}
<div class="dash-symbol-cell">
<div class="dash-symbol-title">
{{ t.symbol_name or t.symbol }}
{% if t.symbol_exchange %}<span class="dash-symbol-ex text-muted">{{ t.symbol_exchange }}</span>{% endif %}
{% if t.symbol_is_main %}<span class="badge planned dash-main-badge">主力</span>{% endif %}
{% if t.symbol and (t.symbol_name or '')|lower != (t.symbol or '')|lower %}
<span class="text-accent">{{ t.symbol }}</span>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro trade_pnl_cell(v) %}
{% if v is not none %}{% set n = v|float %}<span class="{% if n > 0 %}pnl-pos{% elif n < 0 %}pnl-neg{% endif %}">{{ ('+' if n > 0 else '') ~ ('%.2f'|format(n)) }} 元</span>{% else %}—{% endif %}
{% endmacro %}
{% macro trade_verify_form(t) %}
<form method="post" action="{{ url_for('update_trade', tid=t.id) }}" class="records-trade-verify-form">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
<input type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
<input type="hidden" name="direction" value="{{ t.direction }}">
<input type="hidden" name="entry_price" value="{{ t.entry_price }}">
<input type="hidden" name="stop_loss" value="{{ t.stop_loss }}">
<input type="hidden" name="take_profit" value="{{ t.take_profit }}">
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
<input type="hidden" name="lots" value="{{ t.lots }}">
<input type="hidden" name="margin" value="{{ t.margin or '' }}">
<input type="hidden" name="holding_minutes" value="{{ t.holding_minutes or 0 }}">
<input type="hidden" name="open_time" value="{{ t.open_time or '' }}">
<input type="hidden" name="close_time" value="{{ t.close_time or '' }}">
<input type="hidden" name="pnl" value="{{ t.pnl or '' }}">
<input type="hidden" name="result" value="{{ t.result }}">
<button type="submit" class="btn-verify" {% if t.verified %}disabled{% endif %}>核对修改</button>
</form>
{% endmacro %}
{% macro trade_row_actions(t, detail_class) %}
<div class="trade-actions records-trade-row-actions">
<button type="button" class="btn-link {{ detail_class }}">详情</button>
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-fill">填入复盘</a>
{{ trade_verify_form(t) }}
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
</div>
{% endmacro %}
<div class="records-page"> <div class="records-page">
<div class="card records-equity-card" style="margin-bottom:1.25rem"> <div class="card records-equity-card" style="margin-bottom:1.25rem">
<h2>资金曲线</h2> <h2>资金曲线</h2>
@@ -28,37 +97,12 @@
<input type="checkbox" id="trade-edit-switch"> <input type="checkbox" id="trade-edit-switch">
<span>修改/核对开关(开启后可编辑关键字段)</span> <span>修改/核对开关(开启后可编辑关键字段)</span>
</label> </label>
<div class="records-mobile-list" id="records-trade-mobile"> <div class="records-mobile-list records-phone-only" id="records-trade-mobile">
{% for t in trades %} {% for t in trades %}
{% set trade_pnl = t.pnl_net if t.pnl_net is not none else t.pnl %} {% 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 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') %} {% 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') %}
<div class="records-trade-row" data-trade-id="{{ t.id }}" data-trade='{{ { <div class="records-trade-row" data-trade-id="{{ t.id }}" data-trade='{{ trade_detail_json(t)|trim }}'>
"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),
"verify_url": url_for("update_trade", tid=t.id)
} | tojson }}'>
<div class="records-trade-main"> <div class="records-trade-main">
<div class="records-trade-head"> <div class="records-trade-head">
<span class="records-trade-title"> <span class="records-trade-title">
@@ -75,28 +119,6 @@
盈亏 <span class="records-mobile-pnl {{ pnl_cls }}">{{ trade_pnl if trade_pnl is not none else '-' }}</span> 盈亏 <span class="records-mobile-pnl {{ pnl_cls }}">{{ trade_pnl if trade_pnl is not none else '-' }}</span>
</div> </div>
</div> </div>
<div class="records-trade-actions">
<button type="button" class="btn-link records-trade-detail-btn">详情</button>
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-fill">复盘</a>
<form method="post" action="{{ url_for('update_trade', tid=t.id) }}" class="records-trade-verify-form">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
<input type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
<input type="hidden" name="direction" value="{{ t.direction }}">
<input type="hidden" name="entry_price" value="{{ t.entry_price }}">
<input type="hidden" name="stop_loss" value="{{ t.stop_loss }}">
<input type="hidden" name="take_profit" value="{{ t.take_profit }}">
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
<input type="hidden" name="lots" value="{{ t.lots }}">
<input type="hidden" name="margin" value="{{ t.margin or '' }}">
<input type="hidden" name="holding_minutes" value="{{ t.holding_minutes or 0 }}">
<input type="hidden" name="open_time" value="{{ t.open_time or '' }}">
<input type="hidden" name="close_time" value="{{ t.close_time or '' }}">
<input type="hidden" name="pnl" value="{{ t.pnl or '' }}">
<input type="hidden" name="result" value="{{ t.result }}">
<button type="submit" class="btn-verify" {% if t.verified %}disabled{% endif %}>{% if t.verified %}已核对{% else %}核对{% endif %}</button>
</form>
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
</div>
<div class="records-trade-phone-foot"> <div class="records-trade-phone-foot">
<span class="records-mobile-chevron">详情 </span> <span class="records-mobile-chevron">详情 </span>
</div> </div>
@@ -105,6 +127,44 @@
<p class="records-mobile-empty">暂无交易记录</p> <p class="records-mobile-empty">暂无交易记录</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="card-scroll records-trade-table-wrap records-tablet-only">
<table class="dashboard-table records-trade-table">
<thead>
<tr>
<th>品种</th>
<th>方向</th>
<th>手数</th>
<th>开仓</th>
<th>平仓</th>
<th>盈亏</th>
<th>净盈亏</th>
<th>平仓时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for t in trades %}
<tr data-trade-id="{{ t.id }}" data-trade='{{ trade_detail_json(t)|trim }}'>
<td>{{ trade_symbol_cell(t) }}</td>
<td>
<span class="badge dir {% if t.direction == 'long' %}dir-long{% else %}dir-short{% endif %}">
{{ '做多' if t.direction == 'long' else '做空' }}
</span>
</td>
<td>{% if t.lots is not none %}{{ '%.2f'|format(t.lots|float) }}{% else %}—{% endif %}</td>
<td>{% if t.entry_price is not none %}{{ '%.2f'|format(t.entry_price|float) }}{% else %}—{% endif %}</td>
<td>{% 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 %}</td>
<td>{{ trade_pnl_cell(t.pnl) }}</td>
<td>{{ trade_pnl_cell(t.pnl_net) }}</td>
<td>{{ (t.close_time or '')[:16].replace('T', ' ') or '—' }}</td>
<td>{{ trade_row_actions(t, 'records-tablet-detail-btn') }}</td>
</tr>
{% else %}
<tr><td colspan="9" class="text-muted">暂无交易记录</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="trade-table-wrap records-desktop-only"> <div class="trade-table-wrap records-desktop-only">
<table class="trade-table"> <table class="trade-table">
<thead> <thead>
+16
View File
@@ -77,6 +77,21 @@ def purge_duplicate_local_trade_logs(conn) -> int:
return removed 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( def enrich_trades_for_records(
trades: list[dict[str, Any]], trades: list[dict[str, Any]],
*, *,
@@ -92,6 +107,7 @@ def enrich_trades_for_records(
curve: list[dict[str, Any]] = [] curve: list[dict[str, Any]] = []
for t in chrono: for t in chrono:
_attach_symbol_meta(t)
pnl_net = float(t.get("pnl_net") or 0) pnl_net = float(t.get("pnl_net") or 0)
eq = t.get("equity_after") eq = t.get("equity_after")
if eq is None: if eq is None: