Align trade record verify/edit UX with Binance instance single-row prompts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 22:02:48 +08:00
parent b6b7bfb248
commit 32838daae0
5 changed files with 259 additions and 139 deletions
+12 -1
View File
@@ -1328,6 +1328,17 @@ def update_trade(tid):
fee = calc["fee"]
pnl_net = calc["pnl_net"]
form_pnl_raw = d.get("pnl")
if form_pnl_raw not in (None, ""):
pnl = float(form_pnl_raw)
pnl_net = round(pnl - fee, 2)
try:
from app import holding_to_minutes
minutes = int(holding_to_minutes(open_time, close_time) or 0)
except Exception:
minutes = int(d.get("holding_minutes") or row.get("holding_minutes") or 0)
conn.execute(
"""UPDATE trade_logs SET
symbol_name=?, monitor_type=?, direction=?,
@@ -1345,7 +1356,7 @@ def update_trade(tid):
close_px,
lots,
float(d.get("margin") or 0),
int(d.get("holding_minutes") or 0),
minutes,
open_time,
close_time,
pnl,
+65
View File
@@ -93,6 +93,66 @@
display: flex;
}
.records-page .records-verify-toggle {
display: flex;
align-items: center;
gap: .45rem;
margin-bottom: .75rem;
font-size: .82rem;
color: var(--text-muted);
cursor: pointer;
}
.records-page .records-verify-toggle input {
flex-shrink: 0;
}
.records-trade-card .records-src-badge {
margin-left: .25rem;
font-size: .65rem;
}
.records-trade-card .records-verified-inline {
margin-left: .25rem;
}
.records-trade-card .btn-records-action,
.records-trade-table-wrap .btn-records-action {
background: #1f3a5a;
color: #8fc8ff;
border: none;
border-radius: 6px;
padding: .3rem .55rem;
font-size: .72rem;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.records-trade-card .btn-records-action:disabled,
.records-trade-table-wrap .btn-records-action:disabled {
opacity: .45;
cursor: not-allowed;
}
.records-trade-card .btn-records-del,
.records-trade-table-wrap .btn-records-del {
background: rgba(239, 68, 68, .15);
color: var(--loss);
border: none;
border-radius: 6px;
padding: .3rem .55rem;
font-size: .72rem;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.records-trade-card .records-trade-actions,
.records-trade-table-wrap .records-trade-actions {
min-width: 15.5rem;
}
#trade-detail-modal .records-detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -303,6 +363,11 @@ html:is([data-layout="tablet"], .layout-tablet) .records-page .records-desktop-o
display: none !important;
}
html:is([data-mobile="1"], .layout-phone) .records-page .records-verify-toggle,
html:is([data-layout="phone"], .layout-phone) .records-page .records-verify-toggle {
display: flex;
}
html:is([data-mobile="1"], .layout-phone) .records-page .records-trade-card .card-body,
html:is([data-layout="phone"], .layout-phone) .records-page .records-trade-card .card-body {
padding: 0;
+26
View File
@@ -47,6 +47,9 @@
html += '</div>';
html += '<div class="records-detail-actions">';
if (data.id) {
html += '<button type="button" class="btn-records-action review-edit-btn" disabled>核对修改</button>';
}
if (data.fill_review_url) {
html += '<a href="' + data.fill_review_url + '" class="btn-fill">填入复盘</a>';
}
@@ -56,6 +59,29 @@
html += '</div>';
body.innerHTML = html;
var reviewBtn = body.querySelector('.review-edit-btn');
if (reviewBtn && data.id) {
reviewBtn.setAttribute('data-trade-edit', JSON.stringify({
id: data.id,
symbol_name: data.symbol_name || data.symbol,
monitor_type: data.monitor_type,
direction: data.direction_code || (data.direction === '做多' ? 'long' : 'short'),
entry_price: data.entry_price,
close_price: data.close_price,
stop_loss: data.stop_loss,
take_profit: data.take_profit,
lots: data.lots,
margin: data.margin,
holding_minutes: data.holding_minutes,
open_time: data.open_time,
close_time: data.close_time,
pnl: data.pnl,
result: data.result
}));
}
if (typeof window.toggleTradeReviewMode === 'function') {
window.toggleTradeReviewMode();
}
mask.classList.add('show');
}
+85 -32
View File
@@ -1,46 +1,99 @@
/* Copyright (c) 2025-2026 . All rights reserved.
* 专有软件 未经授权禁止复制传播转售
* 详见 LICENSE.zh-CN.txt
* 交易记录核对修改 与币安实例一致开关启用后逐条弹窗核对
*/
(function () {
var switchEl = document.getElementById('trade-edit-switch');
if (!switchEl) return;
function setEditMode(on) {
document.querySelectorAll('.cell-edit-hide').forEach(function (el) {
el.style.display = on ? 'none' : '';
});
document.querySelectorAll('.cell-edit-show').forEach(function (el) {
if (el.type === 'hidden') return;
el.style.display = on ? '' : 'none';
});
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
function syncReviewEditButtons() {
var on = !!switchEl.checked;
document.querySelectorAll('.review-edit-btn').forEach(function (btn) {
btn.disabled = !on;
});
}
switchEl.addEventListener('change', function () {
setEditMode(switchEl.checked);
});
function normalizeDatetime(v) {
var raw = String(v || '').trim().replace('T', ' ');
var m = raw.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2}:\d{2})(:\d{2})?/);
if (!m) return raw.slice(0, 19);
return m[1] + ' ' + m[2] + ':' + (m[3] ? m[3].slice(1) : '00');
}
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = btn.closest('tr[data-trade-id]');
if (!row) return;
var id = row.getAttribute('data-trade-id');
var form = document.createElement('form');
form.method = 'POST';
form.action = '/update_trade/' + id;
row.querySelectorAll('.cell-edit-show').forEach(function (el) {
if (!el.name) return;
var input = document.createElement('input');
input.type = 'hidden';
input.name = el.name;
input.value = el.value;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
function fmtPrice(v) {
if (v === null || v === undefined || v === '') return '';
return String(v);
}
function editTradeRecordReview(t) {
if (!t || !t.id) return;
var opened = prompt('开仓时间(YYYY-MM-DD HH:MM:SS)', normalizeDatetime(t.open_time));
if (opened === null) return;
var closed = prompt('平仓时间(YYYY-MM-DD HH:MM:SS)', normalizeDatetime(t.close_time));
if (closed === null) return;
var entry = prompt('开仓价(核对后用于统计)', fmtPrice(t.entry_price));
if (entry === null) return;
var closePx = prompt('平仓价(跨日持仓请以柜台成交为准)', fmtPrice(t.close_price));
if (closePx === null) return;
var stopLoss = prompt('止损价格(核对后用于统计)', fmtPrice(t.stop_loss));
if (stopLoss === null) return;
var takeProfit = prompt('止盈价格(核对后用于统计)', fmtPrice(t.take_profit));
if (takeProfit === null) return;
var pnl = prompt('盈亏(元,可手工核对后填写)', String(t.pnl != null ? t.pnl : ''));
if (pnl === null) return;
var result = prompt(
'结果(止盈/止损/保本止盈/移动止盈/手动平仓/CTP同步)',
String(t.result || '手动平仓')
);
if (result === null) return;
var form = document.createElement('form');
form.method = 'POST';
form.action = '/update_trade/' + t.id;
var fields = {
symbol_name: t.symbol_name || t.symbol || '',
monitor_type: t.monitor_type || '',
direction: t.direction || 'long',
entry_price: entry,
close_price: closePx,
stop_loss: stopLoss,
take_profit: takeProfit,
lots: t.lots != null ? t.lots : '',
margin: t.margin != null ? t.margin : '',
holding_minutes: t.holding_minutes != null ? t.holding_minutes : 0,
open_time: normalizeDatetime(opened),
close_time: normalizeDatetime(closed),
pnl: pnl,
result: String(result || '').trim() || '手动平仓',
};
Object.keys(fields).forEach(function (name) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = fields[name];
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
window.editTradeRecordReview = editTradeRecordReview;
window.toggleTradeReviewMode = syncReviewEditButtons;
switchEl.addEventListener('change', syncReviewEditButtons);
switchEl.addEventListener('input', syncReviewEditButtons);
syncReviewEditButtons();
window.addEventListener('pageshow', syncReviewEditButtons);
document.addEventListener('click', function (e) {
var btn = e.target.closest('.review-edit-btn');
if (!btn || btn.disabled) return;
e.preventDefault();
e.stopPropagation();
var raw = btn.getAttribute('data-trade-edit');
if (!raw) return;
try {
editTradeRecordReview(JSON.parse(raw));
} catch (err) { /* ignore */ }
});
})();
+71 -106
View File
@@ -7,11 +7,14 @@
{% block content %}
{% macro trade_detail_json(t) -%}
{{ {
"id": t.id,
"symbol": t.symbol_name or t.symbol,
"symbol_code": t.symbol,
"symbol_name": t.symbol_name or t.symbol,
"monitor_type": t.monitor_type,
"source": "柜台" if t.source == "ctp" else "本地",
"direction": "做多" if t.direction == "long" else "做空",
"direction_code": t.direction,
"entry_price": t.entry_price,
"close_price": t.close_price,
"stop_loss": t.stop_loss,
@@ -47,31 +50,42 @@
{% 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_edit_json(t) -%}
{{ {
"id": t.id,
"symbol_name": t.symbol_name or t.symbol,
"monitor_type": t.monitor_type,
"direction": t.direction,
"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,
"holding_minutes": t.holding_minutes or 0,
"open_time": t.open_time,
"close_time": t.close_time,
"pnl": t.pnl,
"result": t.result,
}|tojson }}
{%- endmacro %}
{% macro trade_review_edit_btn(t) %}
<button
type="button"
class="btn-records-action review-edit-btn"
data-trade-edit='{{ trade_edit_json(t)|trim }}'
disabled
>核对修改</button>
{% 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>
{{ trade_review_edit_btn(t) }}
{% 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>
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-records-action">填入复盘</a>
{{ trade_review_edit_btn(t) }}
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-records-del" onclick="return confirm('删除?')">删除</a>
</div>
{% endmacro %}
<div class="records-page">
@@ -96,9 +110,9 @@
<p class="hint" style="margin-top:0;margin-bottom:.75rem">
跨日持仓的盈亏以<strong>平仓价</strong>与柜台结算为准;表格中「开仓价」为程序记录,「平仓价」为成交回报,二者不一致时请以平仓价核对净盈亏。
</p>
<label class="trade-switch-label records-desktop-only">
<label class="trade-switch-label records-verify-toggle">
<input type="checkbox" id="trade-edit-switch">
<span>修改/核对开关(开启后可编辑关键字段</span>
<span>修改/核对开关(开启后可点击各行「核对修改」</span>
</label>
<div class="records-mobile-list records-phone-only" id="records-trade-mobile">
{% for t in trades %}
@@ -181,106 +195,57 @@
</thead>
<tbody>
{% for t in trades %}
<tr data-trade-id="{{ t.id }}">
<tr id="trade-row-{{ t.id }}" data-trade-id="{{ t.id }}">
<td><span class="cell-readonly">{{ t.symbol_name or t.symbol }}</span></td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.monitor_type }}</span>
<span class="cell-readonly">{{ t.monitor_type }}</span>
{% if t.source == 'ctp' %}
<span class="badge" style="margin-left:.25rem;font-size:.65rem">柜台</span>
<span class="badge records-src-badge">柜台</span>
{% else %}
<span class="text-muted" style="margin-left:.25rem;font-size:.65rem">本地</span>
<span class="text-muted records-src-badge">本地</span>
{% endif %}
<input class="cell-edit-show" type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
</td>
<td>
<span class="cell-readonly cell-edit-hide">
<span class="badge dir">{{ '做多' if t.direction == 'long' else '做空' }}</span>
</span>
<select class="cell-edit-show" name="direction" style="display:none">
<option value="long" {% if t.direction=='long' %}selected{% endif %}>做多</option>
<option value="short" {% if t.direction=='short' %}selected{% endif %}>做空</option>
</select>
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.entry_price if t.entry_price is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="entry_price" value="{{ t.entry_price }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.close_price if t.close_price is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="close_price" value="{{ t.close_price or '' }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.stop_loss }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="stop_loss" value="{{ t.stop_loss }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.take_profit }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="take_profit" value="{{ t.take_profit }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.lots }}</span>
<input class="cell-edit-show" type="number" step="0.01" name="lots" value="{{ t.lots }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.margin if t.margin is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.01" name="margin" value="{{ t.margin or '' }}" placeholder="保证金" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">
{% if t.margin_pct is not none %}{{ t.margin_pct }}%{% else %}-{% endif %}
<span class="badge dir {% if t.direction == 'long' %}dir-long{% else %}dir-short{% endif %}">
{{ '做多' if t.direction == 'long' else '做空' }}
</span>
</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) }}{% else %}—{% endif %}</td>
<td>{% if t.stop_loss is not none %}{{ '%.2f'|format(t.stop_loss|float) }}{% else %}—{% endif %}</td>
<td>{% if t.take_profit is not none %}{{ '%.2f'|format(t.take_profit|float) }}{% else %}—{% endif %}</td>
<td>{% if t.lots is not none %}{{ '%.2f'|format(t.lots|float) }}{% else %}—{% endif %}</td>
<td>{% if t.margin is not none %}{{ '%.2f'|format(t.margin|float) }}{% else %}—{% endif %}</td>
<td>{% if t.margin_pct is not none %}{{ t.margin_pct }}%{% else %}—{% endif %}</td>
<td>{{ t.holding_minutes or 0 }}</td>
<td>{{ (t.open_time or '')[:16].replace('T',' ') or '—' }}</td>
<td>{{ (t.close_time or '')[:16].replace('T',' ') or '—' }}</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.holding_minutes or 0 }}</span>
<input class="cell-edit-show" type="number" name="holding_minutes" value="{{ t.holding_minutes or 0 }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ (t.open_time or '')[:16].replace('T',' ') }}</span>
<input class="cell-edit-show" type="text" name="open_time" value="{{ t.open_time or '' }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ (t.close_time or '')[:16].replace('T',' ') }}</span>
<input class="cell-edit-show" type="text" name="close_time" value="{{ t.close_time or '' }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide {% if t.pnl and t.pnl > 0 %}text-profit{% elif t.pnl and t.pnl < 0 %}text-loss{% endif %}">
{{ t.pnl if t.pnl is not none else '-' }}
</span>
<input class="cell-edit-show" type="number" step="0.01" name="pnl" value="{{ t.pnl or '' }}" style="display:none">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
</td>
<td><span class="cell-readonly text-muted">{{ t.fee if t.fee is not none else '-' }}</span></td>
<td>
<span class="cell-readonly {% if t.pnl_net and t.pnl_net > 0 %}text-profit{% elif t.pnl_net and t.pnl_net < 0 %}text-loss{% endif %}">
{{ t.pnl_net if t.pnl_net is not none else '-' }}
<span class="{% if t.pnl and t.pnl > 0 %}text-profit{% elif t.pnl and t.pnl < 0 %}text-loss{% endif %}">
{{ t.pnl if t.pnl is not none else '—' }}
</span>
</td>
<td><span class="text-muted">{{ t.fee if t.fee is not none else '—' }}</span></td>
<td>
<span class="cell-readonly">{{ t.equity_after if t.equity_after is not none else '-' }}</span>
</td>
<td>
<span class="cell-readonly cell-edit-hide">
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
{% elif t.result == '移动止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '保本止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
{% if t.verified %}<span class="badge active" style="margin-left:.25rem">已核对</span>{% endif %}
<span class="{% if t.pnl_net and t.pnl_net > 0 %}text-profit{% elif t.pnl_net and t.pnl_net < 0 %}text-loss{% endif %}">
{{ t.pnl_net if t.pnl_net is not none else '—' }}
</span>
<select class="cell-edit-show" name="result" style="display:none">
<option value="手动平仓" {% if t.result=='手动平仓' %}selected{% endif %}>手动平仓</option>
<option value="止盈" {% if t.result=='止盈' %}selected{% endif %}>止盈</option>
<option value="止损" {% if t.result=='止损' %}selected{% endif %}>止损</option>
<option value="移动止盈" {% if t.result=='移动止盈' %}selected{% endif %}>移动止盈</option>
<option value="保本止盈" {% if t.result=='保本止盈' %}selected{% endif %}>保本止盈</option>
</select>
</td>
<td>{{ t.equity_after if t.equity_after is not none else '—' }}</td>
<td>
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
{% elif t.result == '移动止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '保本止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
{% if t.verified %}<span class="badge active records-verified-inline">已核对</span>{% endif %}
</td>
<td>
<div class="trade-actions">
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-fill">填入复盘</a>
<button type="button" class="btn-verify trade-save-btn" disabled>核对修改</button>
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
<div class="trade-actions records-trade-actions">
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-records-action">填入复盘</a>
{{ trade_review_edit_btn(t) }}
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-records-del" onclick="return confirm('删除?')">删除</a>
</div>
</td>
</tr>