ui: 系统设置卡片对齐,策略页说明与布局完善

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 13:10:21 +08:00
parent 59420e0550
commit 09f4649d79
3 changed files with 237 additions and 115 deletions
+64 -4
View File
@@ -16,6 +16,42 @@
return o; return o;
} }
function showPreview(el, text, ok) {
if (!el) return;
if (!text) {
el.hidden = true;
el.textContent = '';
return;
}
el.hidden = false;
el.textContent = text;
el.style.color = ok === false ? 'var(--loss)' : '';
}
function formatPlan(plan) {
if (!plan) return '';
var lines = [];
if (plan.symbol) lines.push('品种:' + plan.symbol);
if (plan.target_lots != null) lines.push('目标手数:' + plan.target_lots);
if (plan.first_lots != null) lines.push('首仓:' + plan.first_lots + ' 手');
if (plan.grid && plan.grid.length) {
lines.push('补仓档位:' + plan.grid.map(function (g) { return g.price; }).join(' → '));
}
if (plan.message) lines.push(plan.message);
return lines.length ? lines.join('\n') : JSON.stringify(plan, null, 2);
}
function formatRoll(preview) {
if (!preview) return '';
var lines = [];
if (preview.add_lots != null) lines.push('加仓手数:' + preview.add_lots);
if (preview.new_stop_loss != null) lines.push('新止损:' + preview.new_stop_loss);
if (preview.total_lots != null) lines.push('合计手数:' + preview.total_lots);
if (preview.worst_loss != null) lines.push('最坏亏损:' + preview.worst_loss + ' 元');
if (preview.message) lines.push(preview.message);
return lines.length ? lines.join('\n') : JSON.stringify(preview, null, 2);
}
var trendForm = document.getElementById('trend-form'); var trendForm = document.getElementById('trend-form');
var btnPreview = document.getElementById('btn-trend-preview'); var btnPreview = document.getElementById('btn-trend-preview');
var btnExec = document.getElementById('btn-trend-exec'); var btnExec = document.getElementById('btn-trend-exec');
@@ -23,20 +59,32 @@
if (btnPreview && trendForm) { if (btnPreview && trendForm) {
btnPreview.addEventListener('click', function () { btnPreview.addEventListener('click', function () {
btnPreview.disabled = true;
jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) { jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) {
if (!d.ok) { previewEl.textContent = d.error || '预览失败'; btnExec.hidden = true; return; } if (!d.ok) {
showPreview(previewEl, d.error || '预览失败', false);
btnExec.hidden = true;
return;
}
trendPayload = formData(trendForm); trendPayload = formData(trendForm);
previewEl.textContent = JSON.stringify(d.plan, null, 2); showPreview(previewEl, formatPlan(d.plan), true);
btnExec.hidden = false; btnExec.hidden = false;
}).finally(function () {
btnPreview.disabled = false;
}); });
}); });
} }
if (btnExec) { if (btnExec) {
btnExec.addEventListener('click', function () { btnExec.addEventListener('click', function () {
if (!trendPayload) return; if (!trendPayload) return;
btnExec.disabled = true;
btnExec.textContent = '执行中…';
jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) { jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) {
if (!d.ok) { alert(d.error); return; } if (!d.ok) { alert(d.error); return; }
location.reload(); location.reload();
}).finally(function () {
btnExec.disabled = false;
btnExec.textContent = '确认执行首仓';
}); });
}); });
} }
@@ -47,18 +95,30 @@
var rollPrev = document.getElementById('roll-preview'); var rollPrev = document.getElementById('roll-preview');
if (btnRollP && rollForm) { if (btnRollP && rollForm) {
btnRollP.addEventListener('click', function () { btnRollP.addEventListener('click', function () {
btnRollP.disabled = true;
jsonPost('/api/strategy/roll/preview', formData(rollForm)).then(function (d) { jsonPost('/api/strategy/roll/preview', formData(rollForm)).then(function (d) {
if (!d.ok) { rollPrev.textContent = d.error; btnRollE.hidden = true; return; } if (!d.ok) {
rollPrev.textContent = JSON.stringify(d.preview, null, 2); showPreview(rollPrev, d.error, false);
btnRollE.hidden = true;
return;
}
showPreview(rollPrev, formatRoll(d.preview), true);
btnRollE.hidden = false; btnRollE.hidden = false;
}).finally(function () {
btnRollP.disabled = false;
}); });
}); });
} }
if (btnRollE && rollForm) { if (btnRollE && rollForm) {
btnRollE.addEventListener('click', function () { btnRollE.addEventListener('click', function () {
btnRollE.disabled = true;
btnRollE.textContent = '执行中…';
jsonPost('/api/strategy/roll/execute', formData(rollForm)).then(function (d) { jsonPost('/api/strategy/roll/execute', formData(rollForm)).then(function (d) {
if (!d.ok) { alert(d.error); return; } if (!d.ok) { alert(d.error); return; }
location.reload(); location.reload();
}).finally(function () {
btnRollE.disabled = false;
btnRollE.textContent = '执行滚仓';
}); });
}); });
} }
+115 -90
View File
@@ -2,113 +2,138 @@
{% block title %}系统设置 - 国内期货监控系统{% endblock %} {% block title %}系统设置 - 国内期货监控系统{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;align-items:start} .settings-page{display:flex;flex-direction:column;gap:1.25rem}
.settings-grid .card{margin-bottom:0} .settings-page .split-grid{margin-bottom:0}
.settings-password-form{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem;max-width:520px} .settings-page .split-grid .card{margin-bottom:0;min-height:100%;height:100%;display:flex;flex-direction:column}
.settings-page .split-grid .card > form,
.settings-page .split-grid .card > .card-inner{flex:1;display:flex;flex-direction:column}
.settings-password-form{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem}
.settings-password-form .field-full{grid-column:1/-1} .settings-password-form .field-full{grid-column:1/-1}
.settings-password-form .field label{font-size:.78rem} .settings-password-form .field label{font-size:.78rem}
.settings-password-form input{padding:.55rem .7rem;font-size:.85rem} .settings-password-form input{padding:.55rem .7rem;font-size:.85rem}
.settings-tips{flex:1;display:flex;flex-direction:column;justify-content:center;gap:.5rem;margin:0;padding:0;list-style:none;font-size:.85rem;color:var(--text-muted);line-height:1.55}
.settings-tips li{padding-left:1rem;position:relative}
.settings-tips li::before{content:"";position:absolute;left:0;top:.55em;width:5px;height:5px;border-radius:50%;background:var(--accent)}
@media(max-width:900px){ @media(max-width:900px){
.settings-grid{grid-template-columns:1fr} .settings-password-form{grid-template-columns:1fr}
.settings-password-form{grid-template-columns:1fr;max-width:none}
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="settings-grid"> <div class="settings-page">
<div class="card"> <div class="split-grid">
<h2>导航显示</h2> <div class="card">
<form action="{{ url_for('settings') }}" method="post"> <h2>导航显示</h2>
<input type="hidden" name="action" value="nav"> <form action="{{ url_for('settings') }}" method="post">
<p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。</p> <input type="hidden" name="action" value="nav">
<div class="check-row"> <p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。</p>
{% for key, label in nav_toggles.items() %} <div class="check-row">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;white-space:nowrap"> {% for key, label in nav_toggles.items() %}
<input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}> <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;white-space:nowrap">
<span>{{ label }}</span> <input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}>
</label> <span>{{ label }}</span>
{% endfor %} </label>
</div> {% endfor %}
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存导航</button> </div>
</form> <button type="submit" class="btn-primary" style="margin-top:.75rem">保存导航</button>
</form>
</div>
<div class="card">
<h2>交易模式</h2>
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="trading">
<div class="form-grid">
<div class="field">
<label>交易通道</label>
<select name="trading_mode">
<option value="simulation" {% if trading_mode == 'simulation' %}selected{% endif %}>SimNowvnpy CTP</option>
<option value="live" {% if trading_mode == 'live' %}selected{% endif %}>期货公司 CTP(后期接入)</option>
</select>
</div>
<div class="field">
<label>计仓模式</label>
<select name="position_sizing_mode">
<option value="risk" {% if position_sizing_mode == 'risk' %}selected{% endif %}>以损定仓</option>
<option value="fixed" {% if position_sizing_mode == 'fixed' %}selected{% endif %}>固定张数</option>
</select>
</div>
<div class="field">
<label>单笔风险比例(以损定仓,%</label>
<input name="risk_percent" type="number" step="0.1" min="0.1" max="100" value="{{ risk_percent }}">
</div>
</div>
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
<p class="hint" style="margin-top:.75rem;margin-bottom:0">
<code>.env</code> 配置 <code>SIMNOW_USER</code>,于「持仓监控」连接 CTP;权益与行情优先来自柜台。
</p>
</form>
</div>
</div> </div>
<div class="card"> <div class="split-grid">
<h2>交易模式</h2> <div class="card">
<form action="{{ url_for('settings') }}" method="post"> <h2>行情说明</h2>
<input type="hidden" name="action" value="trading"> <div class="card-inner">
<div class="form-grid"> <p class="hint" style="font-size:.88rem;line-height:1.6;margin:0">
<div class="field"> 当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br>
<label>交易通道</label> CTP 已连接时使用<strong>柜台行情</strong>;未连接时回退新浪接口。<br>
<select name="trading_mode"> 合约代码按同花顺格式(如 ag2608、IF2606)。
<option value="simulation" {% if trading_mode == 'simulation' %}selected{% endif %}>模拟盘 · SimNowvnpy CTP</option> </p>
<option value="live" {% if trading_mode == 'live' %}selected{% endif %}>实盘 · 期货公司 CTP(后期接入)</option> </div>
</select> </div>
<div class="card">
<h2>企业微信推送</h2>
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="wechat">
<div class="field" style="margin-bottom:.75rem">
<label>Webhook 地址</label>
<input name="wechat_webhook" type="url" placeholder="https://qyapi.weixin.qq.com/..." value="{{ webhook }}">
</div>
<button type="submit" class="btn-primary">保存</button>
<p class="hint" style="margin-top:.75rem;margin-bottom:0">在企业微信群添加机器人后,粘贴 Webhook 地址保存。</p>
</form>
</div>
</div>
<div class="split-grid">
<div class="card">
<h2>修改密码</h2>
<form action="{{ url_for('settings') }}" method="post" class="settings-password-form">
<input type="hidden" name="action" value="password">
<div class="field field-full">
<label>当前账号</label>
<input type="text" value="{{ username }}" disabled>
</div> </div>
<div class="field"> <div class="field">
<label>计仓模式</label> <label>原密码</label>
<select name="position_sizing_mode"> <input name="old_password" type="password" required>
<option value="risk" {% if position_sizing_mode == 'risk' %}selected{% endif %}>以损定仓</option>
<option value="fixed" {% if position_sizing_mode == 'fixed' %}selected{% endif %}>固定张数</option>
</select>
</div> </div>
<div class="field"> <div class="field">
<label>单笔风险比例(以损定仓,%</label> <label>新密码</label>
<input name="risk_percent" type="number" step="0.1" min="0.1" max="100" value="{{ risk_percent }}"> <input name="new_password" type="password" required minlength="6" placeholder="至少 6 位">
</div> </div>
</div> <div class="field field-full">
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button> <label>确认新密码</label>
</form> <input name="new_password2" type="password" required minlength="6">
<p class="hint" style="margin-top:.75rem"> </div>
<strong>模拟盘</strong><code>.env</code> 配置 <code>SIMNOW_USER</code> 等,于「持仓监控」连接 CTP。 <div class="field-full">
权益与行情优先来自<strong> CTP 柜台</strong> <button type="submit" class="btn-primary">修改密码</button>
</p> </div>
</div> </form>
</div>
<div class="card"> <div class="card">
<h2>行情说明</h2> <h2>使用提示</h2>
<p class="hint" style="font-size:.88rem;line-height:1.6;margin:0"> <ul class="settings-tips">
当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br> <li>持仓监控:连接 CTP 后下单、看持仓与品种推荐</li>
CTP 已连接时使用<strong>柜台行情</strong>(与下单、持仓一致);未连接时回退新浪免费接口。<br> <li>策略交易:趋势回调自动补仓;顺势加仓需先开仓</li>
合约代码按<strong>同花顺格式</strong>显示(如 ag2608、IF2606)。 <li>手续费:默认 CTP 柜台费率,连接后点同步</li>
</p> <li>手机端:浏览器菜单可「添加到主屏幕」安装 App</li>
</div> </ul>
</div>
<div class="card">
<h2>企业微信推送</h2>
<form action="{{ url_for('settings') }}" method="post" class="form-row">
<input type="hidden" name="action" value="wechat">
<input name="wechat_webhook" type="url" placeholder="企业微信 Webhook 地址" value="{{ webhook }}" style="flex:1;min-width:0">
<button type="submit" class="btn-primary">保存</button>
</form>
<p class="hint" style="margin-top:.75rem">在企业微信群中添加机器人后,将 Webhook 地址粘贴到上方保存即可。</p>
</div>
<div class="card">
<h2>修改密码</h2>
<form action="{{ url_for('settings') }}" method="post" class="settings-password-form">
<input type="hidden" name="action" value="password">
<div class="field field-full">
<label>当前账号</label>
<input type="text" value="{{ username }}" disabled>
</div>
<div class="field">
<label>原密码</label>
<input name="old_password" type="password" required>
</div>
<div class="field">
<label>新密码(至少 6 位)</label>
<input name="new_password" type="password" required minlength="6">
</div>
<div class="field field-full">
<label>确认新密码</label>
<input name="new_password2" type="password" required minlength="6">
</div>
<div class="field-full">
<button type="submit" class="btn-primary">修改密码</button>
</div>
</form>
</div> </div>
</div> </div>
+58 -21
View File
@@ -1,20 +1,37 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}策略交易 - 国内期货监控系统{% endblock %} {% block title %}策略交易 - 国内期货监控系统{% endblock %}
{% block extra_css %}
<style>
.strategy-page .split-grid .card{min-height:420px;display:flex;flex-direction:column}
.strategy-page .split-grid .card-body{flex:1}
.strategy-preview{background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;margin-top:.75rem;white-space:pre-wrap;max-height:200px;overflow:auto}
.strategy-steps{margin:.75rem 0 0;padding-left:1.1rem;font-size:.82rem;color:var(--text-muted);line-height:1.6}
.strategy-steps a{color:var(--accent)}
.strategy-active-roll{margin-top:.65rem;padding:.55rem .75rem;background:var(--card-inner);border-radius:8px;font-size:.8rem;border:1px solid var(--card-border)}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="strategy-page">
<div class="split-grid"> <div class="split-grid">
<div class="card"> <div class="card">
<h2>趋势回调</h2> <h2>趋势回调</h2>
<div class="card-body">
{% if active_trend %} {% if active_trend %}
<p class="hint">运行中 #{{ active_trend.id }} {{ active_trend.symbol }} {{ active_trend.direction }} <p class="hint">运行中 #{{ active_trend.id }} · {{ active_trend.symbol }} · {{ '做多' if active_trend.direction == 'long' else '做空' }}</p>
已开 {{ active_trend.lots_open or 0 }}/{{ active_trend.target_lots }} 手</p> <p class="hint">已开 <strong>{{ active_trend.lots_open or 0 }}</strong> / {{ active_trend.target_lots }} 手 · 止损 {{ active_trend.stop_loss }} · 止盈 {{ active_trend.take_profit }}</p>
<form id="trend-stop-form" class="form-row"> <form id="trend-stop-form" class="form-row" style="margin-top:.75rem">
<input type="hidden" name="plan_id" value="{{ active_trend.id }}"> <input type="hidden" name="plan_id" value="{{ active_trend.id }}">
<button type="button" class="btn-primary" id="btn-trend-stop">结束计划</button> <button type="button" class="btn-primary" id="btn-trend-stop">结束计划</button>
</form> </form>
<p class="hint" style="margin-top:.75rem;font-size:.75rem">后台按档位自动补仓,触及止盈或手动结束。</p>
{% else %} {% else %}
<p class="hint" style="margin-bottom:.65rem">设置止损/补仓边界/止盈 → 预览 → 确认执行首仓;后续自动分档加仓。</p>
<form id="trend-form" class="form-compact"> <form id="trend-form" class="form-compact">
<div class="form-line line-2"> <div class="form-line line-2">
<div class="symbol-wrap"><input class="symbol-input" name="symbol" placeholder="合约" required><div class="symbol-dropdown"></div></div> <div class="symbol-wrap symbol-mains">
<input class="symbol-input" name="symbol" placeholder="品种,输入中文或代码" autocomplete="off" required>
<div class="symbol-dropdown"></div>
</div>
<select name="direction"><option value="long">做多</option><option value="short">做空</option></select> <select name="direction"><option value="long">做多</option><option value="short">做空</option></select>
</div> </div>
<div class="form-line line-3"> <div class="form-line line-3">
@@ -23,41 +40,61 @@
<input name="take_profit" type="number" step="any" placeholder="止盈" required> <input name="take_profit" type="number" step="any" placeholder="止盈" required>
</div> </div>
<div class="form-line line-2"> <div class="form-line line-2">
<input name="risk_percent" type="number" step="0.1" value="{{ risk_percent }}" placeholder="风险%"> <input name="risk_percent" type="number" step="0.1" value="{{ risk_percent }}" placeholder="单笔风险 %" title="单笔风险%">
<button type="button" class="btn-primary" id="btn-trend-preview">预览</button> <button type="button" class="btn-primary" id="btn-trend-preview">预览计划</button>
</div> </div>
</form> </form>
<pre id="trend-preview" class="hint" style="white-space:pre-wrap;margin-top:.75rem"></pre> <div id="trend-preview" class="strategy-preview" hidden></div>
<button type="button" class="btn-primary" id="btn-trend-exec" hidden>确认执行首仓</button> <button type="button" class="btn-primary" id="btn-trend-exec" hidden style="margin-top:.65rem;width:100%">确认执行首仓</button>
{% endif %} {% endif %}
</div>
</div> </div>
<div class="card"> <div class="card">
<h2>顺势加仓(滚仓)</h2> <h2>顺势加仓(滚仓)</h2>
<p class="hint">须先有「下单监控」持仓;最多 3 腿;止盈锁首仓。</p> <div class="card-body">
<p class="hint" style="margin-bottom:.65rem">在已有持仓上扩大仓位,统一抬高止损;最多 3 腿,止盈锁定首仓。</p>
{% if roll_groups %}
{% for g in roll_groups %}
<div class="strategy-active-roll">
运行中 · 监控 #{{ g.order_monitor_id }} · {{ g.leg_count or 1 }} 腿 · 止损 {{ g.current_stop_loss }}
</div>
{% endfor %}
{% endif %}
{% if monitors %} {% if monitors %}
<form id="roll-form" class="form-compact"> <form id="roll-form" class="form-compact">
<select name="monitor_id" required> <div class="field" style="margin-bottom:.5rem">
{% for m in monitors %} <label class="text-label" style="font-size:.72rem">选择下单监控(开仓后生成)</label>
<option value="{{ m.id }}">{{ m.symbol }} {{ m.direction }} {{ m.lots }}手</option> <select name="monitor_id" required>
{% endfor %} {% for m in monitors %}
</select> <option value="{{ m.id }}">{{ m.symbol_name or m.symbol }} {{ m.symbol }} · {{ '多' if m.direction == 'long' else '空' }} {{ m.lots }}手 · SL {{ m.stop_loss or '—' }}</option>
{% endfor %}
</select>
</div>
<div class="form-line line-2"> <div class="form-line line-2">
<input name="new_stop_loss" type="number" step="any" placeholder="新统一止损" required> <input name="new_stop_loss" type="number" step="any" placeholder="新统一止损" required>
<input name="risk_percent" type="number" step="0.1" value="2" placeholder="总风险%"> <input name="risk_percent" type="number" step="0.1" value="2" placeholder="总风险 %" title="总风险%">
</div> </div>
<div class="form-line line-2"> <div class="form-line line-2">
<input name="add_price" type="number" step="any" placeholder="加仓参考价"> <input name="add_price" type="number" step="any" placeholder="加仓参考价(可选)">
<button type="button" class="btn-primary" id="btn-roll-preview">预览</button> <button type="button" class="btn-primary" id="btn-roll-preview">预览滚仓</button>
</div> </div>
<pre id="roll-preview" class="hint" style="white-space:pre-wrap"></pre> <div id="roll-preview" class="strategy-preview" hidden></div>
<button type="button" class="btn-primary" id="btn-roll-exec" hidden>执行滚仓</button> <button type="button" class="btn-primary" id="btn-roll-exec" hidden style="margin-top:.65rem;width:100%">执行滚仓</button>
</form> </form>
{% else %} {% else %}
<p class="empty-hint">请先在「持仓监控 → 期货下单」进入策略交易开仓。</p> <p class="empty-hint">暂无可用持仓监控</p>
<ol class="strategy-steps">
<li>打开 <a href="{{ url_for('positions') }}">持仓监控</a>,连接 CTP</li>
<li>在「期货下单」填写品种、止损/止盈并<strong>开仓</strong></li>
<li>开仓成功后会生成本页可选的监控记录,即可滚仓</li>
</ol>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
<p class="hint"><a href="{{ url_for('strategy_records_page') }}">策略交易记录 →</a></p> <p class="hint" style="margin-top:1rem"><a href="{{ url_for('strategy_records_page') }}">策略交易记录 →</a></p>
</div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ url_for('static', filename='js/strategy.js') }}"></script> <script src="{{ url_for('static', filename='js/strategy.js') }}"></script>