修复关键位sina_code字段;复盘详情全屏两行;开单计划表单布局优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 14:22:42 +08:00
parent eaf72a13fc
commit 58020b6e9c
5 changed files with 820 additions and 724 deletions
+1
View File
@@ -174,6 +174,7 @@ def init_db():
"ALTER TABLE order_plans ADD COLUMN sina_code TEXT", "ALTER TABLE order_plans ADD COLUMN sina_code TEXT",
"ALTER TABLE order_plans ADD COLUMN market_code TEXT", "ALTER TABLE order_plans ADD COLUMN market_code TEXT",
"ALTER TABLE key_monitors ADD COLUMN market_code TEXT", "ALTER TABLE key_monitors ADD COLUMN market_code TEXT",
"ALTER TABLE key_monitors ADD COLUMN sina_code TEXT",
"ALTER TABLE trade_records ADD COLUMN market_code TEXT", "ALTER TABLE trade_records ADD COLUMN market_code TEXT",
"ALTER TABLE order_plans ADD COLUMN plan_date TEXT", "ALTER TABLE order_plans ADD COLUMN plan_date TEXT",
"ALTER TABLE order_plans ADD COLUMN decision_reason TEXT", "ALTER TABLE order_plans ADD COLUMN decision_reason TEXT",
+165 -113
View File
@@ -1,113 +1,165 @@
(function () { (function () {
function parseNum(v) { function parseNum(v) {
var n = parseFloat(v); var n = parseFloat(v);
return isNaN(n) ? null : n; return isNaN(n) ? null : n;
} }
function calcRR(direction, entry, stop, target) { function calcRR(direction, entry, stop, target) {
if (!entry || !stop || !target) return ''; if (!entry || !stop || !target) return '';
var risk, reward; var risk, reward;
if (direction === 'long') { if (direction === 'long') {
risk = entry - stop; risk = entry - stop;
reward = target - entry; reward = target - entry;
} else if (direction === 'short') { } else if (direction === 'short') {
risk = stop - entry; risk = stop - entry;
reward = entry - target; reward = entry - target;
} else { } else {
return ''; return '';
} }
if (risk <= 0) return ''; if (risk <= 0) return '';
return (reward / risk).toFixed(2); return (reward / risk).toFixed(2);
} }
function calcDuration(openVal, closeVal) { function calcDuration(openVal, closeVal) {
if (!openVal || !closeVal) return ''; if (!openVal || !closeVal) return '';
var o = new Date(openVal); var o = new Date(openVal);
var c = new Date(closeVal); var c = new Date(closeVal);
var secs = Math.floor((c - o) / 1000); var secs = Math.floor((c - o) / 1000);
if (secs < 0) return ''; if (secs < 0) return '';
var h = Math.floor(secs / 3600); var h = Math.floor(secs / 3600);
var m = Math.floor((secs % 3600) / 60); var m = Math.floor((secs % 3600) / 60);
return h ? h + '小时' + m + '分钟' : m + '分钟'; return h ? h + '小时' + m + '分钟' : m + '分钟';
} }
function recalc() { function recalc() {
var form = document.getElementById('review-form'); var form = document.getElementById('review-form');
if (!form) return; if (!form) return;
var dir = form.querySelector('[name="direction"]').value; var dir = form.querySelector('[name="direction"]').value;
var entry = parseNum(form.querySelector('[name="entry_price"]').value); var entry = parseNum(form.querySelector('[name="entry_price"]').value);
var sl = parseNum(form.querySelector('[name="stop_loss"]').value); var sl = parseNum(form.querySelector('[name="stop_loss"]').value);
var tp = parseNum(form.querySelector('[name="take_profit"]').value); var tp = parseNum(form.querySelector('[name="take_profit"]').value);
var close = parseNum(form.querySelector('[name="close_price"]').value); var close = parseNum(form.querySelector('[name="close_price"]').value);
var openT = form.querySelector('[name="open_time"]').value; var openT = form.querySelector('[name="open_time"]').value;
var closeT = form.querySelector('[name="close_time"]').value; var closeT = form.querySelector('[name="close_time"]').value;
var hold = document.getElementById('holding_duration'); var hold = document.getElementById('holding_duration');
var initR = document.getElementById('initial_rr'); var initR = document.getElementById('initial_rr');
var actR = document.getElementById('actual_rr'); var actR = document.getElementById('actual_rr');
if (hold) hold.value = calcDuration(openT, closeT); if (hold) hold.value = calcDuration(openT, closeT);
if (initR) initR.value = calcRR(dir, entry, sl, tp); if (initR) initR.value = calcRR(dir, entry, sl, tp);
if (actR) actR.value = calcRR(dir, entry, sl, close); if (actR) actR.value = calcRR(dir, entry, sl, close);
} }
function bindForm() { function bindForm() {
var form = document.getElementById('review-form'); var form = document.getElementById('review-form');
if (!form) return; if (!form) return;
form.querySelectorAll('input, select').forEach(function (el) { form.querySelectorAll('input, select').forEach(function (el) {
el.addEventListener('input', recalc); el.addEventListener('input', recalc);
el.addEventListener('change', recalc); el.addEventListener('change', recalc);
}); });
} }
function showModal(data) { function esc(v) {
var mask = document.getElementById('review-modal'); if (v === null || v === undefined || v === '') return '-';
var body = document.getElementById('review-modal-body'); return String(v);
if (!mask || !body) return; }
var html = '<div class="modal-grid">';
var fields = [ function fmtTime(v) {
['品种', data.symbol], ['方向', data.direction], if (!v) return '-';
['成交价', data.entry_price], ['止损', data.stop_loss], return String(v).replace('T', ' ').slice(0, 16);
['止盈', data.take_profit], ['平仓价', data.close_price], }
['张数', data.lots], ['开仓时间', data.open_time],
['平仓时间', data.close_time], ['持仓时长', data.holding_duration], function fmtRR(data) {
['初始盈亏比', data.initial_pnl], ['实际盈亏比', data.actual_pnl], var init = data.initial_pnl;
['盈亏金额', data.pnl], ['开仓类型', data.open_type], var act = data.actual_pnl;
['离场触发', data.exit_trigger], ['离场补充', data.exit_supplement], if (init && act) return init + ' / ' + act;
['情绪单', data.is_emotion ? '是' : ''], return act || init || '-';
['行为标签', data.behavior_tags], ['备注', data.notes] }
];
fields.forEach(function (pair) { function fmtTags(data) {
html += '<div class="item"><label>' + pair[0] + '</label><div>' + (pair[1] || '-') + '</div></div>'; var tags = data.behavior_tags || '';
}); if (data.is_emotion && tags.indexOf('情绪') === -1) {
html += '</div>'; return tags ? '情绪单 · ' + tags : '情绪单';
if (data.screenshot) { }
html += '<div style="margin-top:1rem"><img src="/uploads/' + data.screenshot + '" style="max-width:100%;border-radius:8px"></div>'; return tags || '-';
} }
body.innerHTML = html;
mask.classList.add('show'); function showModal(data) {
} var mask = document.getElementById('review-modal');
var body = document.getElementById('review-modal-body');
function bindModal() { if (!mask || !body) return;
var mask = document.getElementById('review-modal');
if (!mask) return; var labels = [
mask.querySelector('.modal-close').addEventListener('click', function () { '品种', '合约', '方向', '张数', '周期',
mask.classList.remove('show'); '成交价', '止损', '止盈', '平仓价',
}); '开仓时间', '平仓时间', '持仓时长', '盈利金额', '盈亏比',
mask.addEventListener('click', function (e) { '开仓类型', '行为标签'
if (e.target === mask) mask.classList.remove('show'); ];
}); var values = [
document.querySelectorAll('.review-view-btn').forEach(function (btn) { esc(data.symbol),
btn.addEventListener('click', function () { esc(data.symbol_code),
try { esc(data.direction),
showModal(JSON.parse(btn.getAttribute('data-review'))); esc(data.lots),
} catch (e) { /* ignore */ } esc(data.timeframe),
}); esc(data.entry_price),
}); esc(data.stop_loss),
} esc(data.take_profit),
esc(data.close_price),
document.addEventListener('DOMContentLoaded', function () { fmtTime(data.open_time),
bindForm(); fmtTime(data.close_time),
bindModal(); esc(data.holding_duration),
recalc(); esc(data.pnl),
}); fmtRR(data),
})(); esc(data.open_type),
fmtTags(data)
];
var html = '<div class="review-detail-table">';
html += '<div class="review-detail-headers">';
labels.forEach(function (lb) {
html += '<span>' + lb + '</span>';
});
html += '</div><div class="review-detail-values">';
values.forEach(function (val, i) {
var cls = '';
if (i === 15 && data.is_emotion) cls = ' class="emotion-val"';
html += '<span' + cls + '>' + val + '</span>';
});
html += '</div></div>';
html += '<div class="review-detail-image">';
if (data.screenshot) {
html += '<img src="/uploads/' + data.screenshot + '" alt="复盘截图">';
} else {
html += '<div class="no-img">暂无截图 / K线图</div>';
}
html += '</div>';
body.innerHTML = html;
mask.classList.add('show');
}
function bindModal() {
var mask = document.getElementById('review-modal');
if (!mask) return;
mask.querySelector('.modal-close').addEventListener('click', function () {
mask.classList.remove('show');
});
mask.addEventListener('click', function (e) {
if (e.target === mask) mask.classList.remove('show');
});
document.querySelectorAll('.review-view-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
try {
showModal(JSON.parse(btn.getAttribute('data-review')));
} catch (e) { /* ignore */ }
});
});
}
document.addEventListener('DOMContentLoaded', function () {
bindForm();
bindModal();
recalc();
});
})();
+378 -337
View File
@@ -1,337 +1,378 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}国内期货监控系统{% endblock %}</title> <title>{% block title %}国内期货监控系统{% endblock %}</title>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script> <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<style> <style>
:root,[data-theme="dark"]{ :root,[data-theme="dark"]{
--bg-page:#07070d; --bg-page:#07070d;
--bg-grid:rgba(76,194,255,.035); --bg-grid:rgba(76,194,255,.035);
--border-header:#1a1a28; --border-header:#1a1a28;
--text-primary:#eaeaea; --text-primary:#eaeaea;
--text-title:#ffffff; --text-title:#ffffff;
--text-muted:#8a8a9e; --text-muted:#8a8a9e;
--text-label:#b4b4e8; --text-label:#b4b4e8;
--accent:#4cc2ff; --accent:#4cc2ff;
--accent-2:#7b42ff; --accent-2:#7b42ff;
--card-bg:rgba(14,14,22,.88); --card-bg:rgba(14,14,22,.88);
--card-border:rgba(76,194,255,.18); --card-border:rgba(76,194,255,.18);
--card-glow:rgba(76,194,255,.07); --card-glow:rgba(76,194,255,.07);
--card-inner:#161625; --card-inner:#161625;
--input-bg:#1a1a29; --input-bg:#1a1a29;
--input-border:#2e2e45; --input-border:#2e2e45;
--nav-bg:#161625; --nav-bg:#161625;
--nav-border:#2a2a40; --nav-border:#2a2a40;
--nav-hover:#1e2533; --nav-hover:#1e2533;
--nav-active:#2d5aa8; --nav-active:#2d5aa8;
--nav-active-border:#3d6ec4; --nav-active-border:#3d6ec4;
--list-item-bg:rgba(22,22,37,.75); --list-item-bg:rgba(22,22,37,.75);
--table-border:#242435; --table-border:#242435;
--profit:#4cd97f; --profit:#4cd97f;
--profit-bg:#1e332f; --profit-bg:#1e332f;
--loss:#ff6666; --loss:#ff6666;
--loss-bg:#331e24; --loss-bg:#331e24;
--dir-bg:#1e2533; --dir-bg:#1e2533;
--planned-bg:#29241e; --planned-bg:#29241e;
--planned-text:#eac147; --planned-text:#eac147;
--expired-bg:#2a2a35; --expired-bg:#2a2a35;
--expired-text:#999; --expired-text:#999;
--flash-bg:#1e2533; --flash-bg:#1e2533;
--flash-text:#4cc2ff; --flash-text:#4cc2ff;
--modal-mask:rgba(0,0,0,.72); --modal-mask:rgba(0,0,0,.72);
--danger:#ff6666; --danger:#ff6666;
--shadow-card:0 8px 32px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.05); --shadow-card:0 8px 32px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.05);
--calc-bg:#151520; --calc-bg:#151520;
--toggle-bg:#161625; --toggle-bg:#161625;
--toggle-border:#2a2a40; --toggle-border:#2a2a40;
} }
[data-theme="light"]{ [data-theme="light"]{
--bg-page:#eef2f7; --bg-page:#eef2f7;
--bg-grid:rgba(37,99,235,.05); --bg-grid:rgba(37,99,235,.05);
--border-header:#d1dae6; --border-header:#d1dae6;
--text-primary:#1a2233; --text-primary:#1a2233;
--text-title:#0f172a; --text-title:#0f172a;
--text-muted:#5c6578; --text-muted:#5c6578;
--text-label:#1d4ed8; --text-label:#1d4ed8;
--accent:#2563eb; --accent:#2563eb;
--accent-2:#6366f1; --accent-2:#6366f1;
--card-bg:rgba(255,255,255,.94); --card-bg:rgba(255,255,255,.94);
--card-border:rgba(37,99,235,.22); --card-border:rgba(37,99,235,.22);
--card-glow:rgba(37,99,235,.06); --card-glow:rgba(37,99,235,.06);
--card-inner:#f1f5f9; --card-inner:#f1f5f9;
--input-bg:#ffffff; --input-bg:#ffffff;
--input-border:#b8c5d6; --input-border:#b8c5d6;
--nav-bg:#ffffff; --nav-bg:#ffffff;
--nav-border:#c5d0dc; --nav-border:#c5d0dc;
--nav-hover:#eef2f7; --nav-hover:#eef2f7;
--nav-active:#2563eb; --nav-active:#2563eb;
--nav-active-border:#2563eb; --nav-active-border:#2563eb;
--list-item-bg:#f1f5f9; --list-item-bg:#f1f5f9;
--table-border:#e2e8f0; --table-border:#e2e8f0;
--profit:#15803d; --profit:#15803d;
--profit-bg:#dcfce7; --profit-bg:#dcfce7;
--loss:#dc2626; --loss:#dc2626;
--loss-bg:#fee2e2; --loss-bg:#fee2e2;
--dir-bg:#dbeafe; --dir-bg:#dbeafe;
--planned-bg:#fef9c3; --planned-bg:#fef9c3;
--planned-text:#a16207; --planned-text:#a16207;
--expired-bg:#f1f5f9; --expired-bg:#f1f5f9;
--expired-text:#64748b; --expired-text:#64748b;
--flash-bg:#dbeafe; --flash-bg:#dbeafe;
--flash-text:#1d4ed8; --flash-text:#1d4ed8;
--modal-mask:rgba(15,23,42,.45); --modal-mask:rgba(15,23,42,.45);
--danger:#dc2626; --danger:#dc2626;
--shadow-card:0 8px 28px rgba(15,23,42,.08),inset 0 1px 0 rgba(255,255,255,.95); --shadow-card:0 8px 28px rgba(15,23,42,.08),inset 0 1px 0 rgba(255,255,255,.95);
--calc-bg:#eef2ff; --calc-bg:#eef2ff;
--toggle-bg:#ffffff; --toggle-bg:#ffffff;
--toggle-border:#c5d0dc; --toggle-border:#c5d0dc;
} }
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{ body{
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:var(--bg-page); background:var(--bg-page);
background-image: background-image:
radial-gradient(ellipse 80% 50% at 50% -20%,var(--card-glow),transparent), radial-gradient(ellipse 80% 50% at 50% -20%,var(--card-glow),transparent),
linear-gradient(var(--bg-grid) 1px,transparent 1px), linear-gradient(var(--bg-grid) 1px,transparent 1px),
linear-gradient(90deg,var(--bg-grid) 1px,transparent 1px); linear-gradient(90deg,var(--bg-grid) 1px,transparent 1px);
background-size:100% 100%,28px 28px,28px 28px; background-size:100% 100%,28px 28px,28px 28px;
color:var(--text-primary); color:var(--text-primary);
min-height:100vh; min-height:100vh;
transition:background .25s,color .25s; transition:background .25s,color .25s;
} }
.page-wrap{max-width:1800px;margin:0 auto;min-height:100vh} .page-wrap{max-width:1800px;margin:0 auto;min-height:100vh}
.site-header{text-align:center;padding:1.5rem 1rem 1.25rem;border-bottom:1px solid var(--border-header);position:relative} .site-header{text-align:center;padding:1.5rem 1rem 1.25rem;border-bottom:1px solid var(--border-header);position:relative}
.site-title{font-size:1.75rem;font-weight:700;color:var(--text-title);margin-bottom:1rem;line-height:1.3} .site-title{font-size:1.75rem;font-weight:700;color:var(--text-title);margin-bottom:1.65rem;line-height:1.3}
.header-tools{position:absolute;top:1rem;left:1.5rem;display:flex;gap:.5rem;align-items:center} .header-tools{position:absolute;top:1rem;left:1.5rem;display:flex;gap:.5rem;align-items:center}
.theme-toggle{ .theme-toggle{
display:flex;align-items:center;gap:.35rem; display:flex;align-items:center;gap:.35rem;
padding:.4rem .75rem;border-radius:8px; padding:.4rem .75rem;border-radius:8px;
border:1px solid var(--toggle-border); border:1px solid var(--toggle-border);
background:var(--toggle-bg); background:var(--toggle-bg);
color:var(--text-primary); color:var(--text-primary);
font-size:.78rem;cursor:pointer; font-size:.78rem;cursor:pointer;
transition:.2s; transition:.2s;
} }
.theme-toggle:hover{border-color:var(--accent);color:var(--accent)} .theme-toggle:hover{border-color:var(--accent);color:var(--accent)}
.theme-toggle .icon-light,.theme-toggle .icon-dark{display:none} .theme-toggle .icon-light,.theme-toggle .icon-dark{display:none}
[data-theme="dark"] .theme-toggle .icon-light{display:inline} [data-theme="dark"] .theme-toggle .icon-light{display:inline}
[data-theme="light"] .theme-toggle .icon-dark{display:inline} [data-theme="light"] .theme-toggle .icon-dark{display:inline}
.site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap} .site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap}
.site-nav a{ .site-nav a{
padding:.55rem 1.15rem;border-radius:8px; padding:.55rem 1.15rem;border-radius:8px;
border:1px solid var(--nav-border); border:1px solid var(--nav-border);
background:var(--nav-bg); background:var(--nav-bg);
color:var(--text-primary); color:var(--text-primary);
text-decoration:none;font-size:.88rem; text-decoration:none;font-size:.88rem;
transition:.2s;white-space:nowrap; 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(--accent);color:var(--text-title)}
.site-nav a.active{background:var(--nav-active);border-color:var(--nav-active-border);color:#fff} .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{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} .user-bar a{color:var(--danger);text-decoration:none;margin-left:.5rem}
.main{padding:1.5rem} .main{padding:1.5rem}
.text-muted{color:var(--text-muted)} .text-muted{color:var(--text-muted)}
.text-label{color:var(--text-label)} .text-label{color:var(--text-label)}
.text-accent{color:var(--accent)} .text-accent{color:var(--accent)}
.text-profit{color:var(--profit)} .text-profit{color:var(--profit)}
.text-loss{color:var(--loss)} .text-loss{color:var(--loss)}
.section-label{font-size:.9rem;color:var(--text-label);margin:.75rem 0 .5rem} .section-label{font-size:.9rem;color:var(--text-label);margin:.75rem 0 .5rem}
.empty-hint{color:var(--text-muted);padding:.75rem;font-size:.85rem} .empty-hint{color:var(--text-muted);padding:.75rem;font-size:.85rem}
.flash{padding:1rem;background:var(--flash-bg);color:var(--flash-text);border-radius:10px;margin-bottom:1.5rem;text-align:center;border:1px solid var(--card-border)} .flash{padding:1rem;background:var(--flash-bg);color:var(--flash-text);border-radius:10px;margin-bottom:1.5rem;text-align:center;border:1px solid var(--card-border)}
.card{ .card{
background:var(--card-bg); background:var(--card-bg);
border-radius:16px;padding:1.5rem; border-radius:16px;padding:1.5rem;
border:1px solid var(--card-border); border:1px solid var(--card-border);
margin-bottom:1.5rem; margin-bottom:1.5rem;
position:relative;overflow:hidden; position:relative;overflow:hidden;
box-shadow:var(--shadow-card); box-shadow:var(--shadow-card);
backdrop-filter:blur(10px); backdrop-filter:blur(10px);
transition:background .25s,border-color .25s,box-shadow .25s; transition:background .25s,border-color .25s,box-shadow .25s;
} }
.card::before{ .card::before{
content:"";position:absolute;inset:0; content:"";position:absolute;inset:0;
background:linear-gradient(135deg,var(--card-glow) 0%,transparent 55%); background:linear-gradient(135deg,var(--card-glow) 0%,transparent 55%);
pointer-events:none; pointer-events:none;
} }
.card::after{ .card::after{
content:"";position:absolute;top:0;left:12%;right:12%;height:1px; content:"";position:absolute;top:0;left:12%;right:12%;height:1px;
background:linear-gradient(90deg,transparent,var(--accent),var(--accent-2),transparent); background:linear-gradient(90deg,transparent,var(--accent),var(--accent-2),transparent);
opacity:.55;pointer-events:none; opacity:.55;pointer-events:none;
} }
.card-corner{ .card-corner{
position:absolute;width:18px;height:18px;pointer-events:none;z-index:0; position:absolute;width:18px;height:18px;pointer-events:none;z-index:0;
border-color:var(--accent);opacity:.35; border-color:var(--accent);opacity:.35;
} }
.card-corner.tl{top:10px;left:10px;border-top:2px solid;border-left:2px solid} .card-corner.tl{top:10px;left:10px;border-top:2px solid;border-left:2px solid}
.card-corner.br{bottom:10px;right:10px;border-bottom:2px solid;border-right:2px solid} .card-corner.br{bottom:10px;right:10px;border-bottom:2px solid;border-right:2px solid}
.card > *{position:relative;z-index:1} .card > *{position:relative;z-index:1}
.card h2{ .card h2{
font-size:1.15rem;margin-bottom:1rem;color:var(--text-label); font-size:1.15rem;margin-bottom:1rem;color:var(--text-label);
display:flex;align-items:center;gap:.5rem; display:flex;align-items:center;gap:.5rem;
} }
.card h2:before{ .card h2:before{
content:"";width:4px;height:16px; content:"";width:4px;height:16px;
background:linear-gradient(180deg,var(--accent),var(--accent-2)); background:linear-gradient(180deg,var(--accent),var(--accent-2));
border-radius:2px;box-shadow:0 0 8px var(--card-glow); border-radius:2px;box-shadow:0 0 8px var(--card-glow);
} }
.form-row{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center} .form-row{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
.form-compact{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem} .form-compact{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem}
.form-compact .form-line{display:grid;gap:.5rem;align-items:center} .form-compact .form-line{display:grid;gap:.5rem;align-items:center}
.form-compact .line-2{grid-template-columns:repeat(2,1fr)} .form-compact .line-2{grid-template-columns:repeat(2,1fr)}
.form-compact .line-3{grid-template-columns:repeat(3,1fr)} .form-compact .line-3{grid-template-columns:repeat(3,1fr)}
.form-compact .line-4{grid-template-columns:repeat(4,1fr)} .form-compact .line-4{grid-template-columns:repeat(4,1fr)}
.form-compact .line-5{grid-template-columns:repeat(5,1fr)} .form-compact .line-5{grid-template-columns:repeat(5,1fr)}
.form-compact.line-tight{gap:.35rem;margin-bottom:.5rem} .form-compact.line-tight{gap:.35rem;margin-bottom:.5rem}
.form-compact .line-btn{display:flex;gap:.5rem;align-items:center} .form-compact .line-btn{display:flex;gap:.5rem;align-items:center}
.form-compact input,.form-compact select{padding:.55rem .7rem;font-size:.85rem;border-radius:8px} .form-compact input,.form-compact select{padding:.55rem .7rem;font-size:.85rem;border-radius:8px}
.form-compact .symbol-selected{font-size:.7rem;margin-top:2px} .form-compact .symbol-selected{font-size:.7rem;margin-top:2px}
.form-compact button.btn-primary{padding:.55rem 1.5rem;font-size:.85rem;white-space:nowrap} .form-compact button.btn-primary{padding:.55rem 1.5rem;font-size:.85rem;white-space:nowrap}
.form-compact-review input.calc-readonly{color:var(--accent);background:var(--calc-bg);font-size:.82rem} .form-compact-review input.calc-readonly{color:var(--accent);background:var(--calc-bg);font-size:.82rem}
.form-compact-review textarea{min-height:44px;font-size:.85rem;padding:.45rem .65rem} .form-compact-review textarea{min-height:44px;font-size:.85rem;padding:.45rem .65rem}
.form-compact-review .section-hint{font-size:.72rem;color:var(--text-muted);margin:.25rem 0 .15rem} .form-compact-review .section-hint{font-size:.72rem;color:var(--text-muted);margin:.25rem 0 .15rem}
.form-compact-review .tag-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.15rem .5rem;font-size:.75rem} .form-compact-review .tag-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.15rem .5rem;font-size:.75rem}
.form-compact-review .tag-grid label{display:flex;align-items:center;gap:.3rem;cursor:pointer;color:var(--text-muted);white-space:nowrap} .form-compact-review .tag-grid label{display:flex;align-items:center;gap:.3rem;cursor:pointer;color:var(--text-muted);white-space:nowrap}
.form-compact-review .tag-grid input{width:auto;flex-shrink:0} .form-compact-review .tag-grid input{width:auto;flex-shrink:0}
.form-compact-review input[type="file"]{font-size:.72rem;padding:.4rem .5rem} .form-compact-review input[type="file"]{font-size:.72rem;padding:.4rem .5rem}
.form-compact-review .mini-field span{font-size:.65rem;color:var(--text-muted);display:block;line-height:1;margin-bottom:2px} .form-compact-review .mini-field span{font-size:.65rem;color:var(--text-muted);display:block;line-height:1;margin-bottom:2px}
.form-compact-review .kline-row label{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);white-space:nowrap} .form-compact-review .kline-row label{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);white-space:nowrap}
.form-compact-review .kline-row{display:grid;grid-template-columns:auto 1fr 1fr 1fr auto;gap:.5rem;align-items:end;font-size:.78rem} .form-compact-review .kline-row{display:grid;grid-template-columns:auto 1fr 1fr 1fr auto;gap:.5rem;align-items:end;font-size:.78rem}
.split-grid.records-split .card{min-height:auto} .split-grid.records-split .card{min-height:auto}
.split-grid.records-split .card h2{font-size:1rem;margin-bottom:.5rem} .split-grid.records-split .card h2{font-size:1rem;margin-bottom:.5rem}
.split-grid.records-split .card{padding:1rem 1.25rem} .split-grid.records-split .card{padding:1rem 1.25rem}
.page-title-sm{font-size:1.25rem;margin-bottom:.75rem} .page-title-sm{font-size:1.25rem;margin-bottom:.75rem}
.form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.75rem;margin-bottom:1rem} .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.75rem;margin-bottom:1rem}
.form-grid .full{grid-column:1/-1} .form-grid .full{grid-column:1/-1}
.field label{display:block;font-size:.8rem;color:var(--text-label);margin-bottom:.35rem} .field label{display:block;font-size:.8rem;color:var(--text-label);margin-bottom:.35rem}
input,select,textarea,button{ input,select,textarea,button{
padding:.7rem 1rem;border-radius:10px; padding:.7rem 1rem;border-radius:10px;
border:1px solid var(--input-border); border:1px solid var(--input-border);
background:var(--input-bg);color:var(--text-primary); background:var(--input-bg);color:var(--text-primary);
font-size:.9rem;outline:none;width:100%; font-size:.9rem;outline:none;width:100%;
transition:border-color .2s,background .2s; transition:border-color .2s,background .2s;
} }
textarea{min-height:80px;resize:vertical} textarea{min-height:80px;resize:vertical}
input:focus,select:focus,textarea:focus{border-color:var(--accent)} input:focus,select:focus,textarea:focus{border-color:var(--accent)}
button.btn-primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;cursor:pointer;color:#fff;width:auto} button.btn-primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;cursor:pointer;color:#fff;width:auto}
button.btn-primary:hover{opacity:.9} button.btn-primary:hover{opacity:.9}
.list{display:flex;flex-direction:column;gap:.75rem} .list{display:flex;flex-direction:column;gap:.75rem}
.list-item{ .list-item{
display:flex;justify-content:space-between;align-items:center; display:flex;justify-content:space-between;align-items:center;
padding:1rem;background:var(--list-item-bg); padding:1rem;background:var(--list-item-bg);
border-radius:10px;gap:1rem;flex-wrap:wrap; border-radius:10px;gap:1rem;flex-wrap:wrap;
border:1px solid var(--card-border); border:1px solid var(--card-border);
} }
.btn-del{padding:.4rem .8rem;background:var(--loss-bg);color:var(--loss);border-radius:8px;text-decoration:none;font-size:.85rem} .btn-del{padding:.4rem .8rem;background:var(--loss-bg);color:var(--loss);border-radius:8px;text-decoration:none;font-size:.85rem}
table{width:100%;border-collapse:collapse} table{width:100%;border-collapse:collapse}
th,td{padding:.85rem;text-align:left;border-bottom:1px solid var(--table-border);font-size:.9rem;color:var(--text-primary)} th,td{padding:.85rem;text-align:left;border-bottom:1px solid var(--table-border);font-size:.9rem;color:var(--text-primary)}
th{color:var(--text-label);white-space:nowrap} th{color:var(--text-label);white-space:nowrap}
.badge{padding:.25rem .5rem;border-radius:6px;font-size:.75rem} .badge{padding:.25rem .5rem;border-radius:6px;font-size:.75rem}
.badge.profit{background:var(--profit-bg);color:var(--profit)} .badge.profit{background:var(--profit-bg);color:var(--profit)}
.badge.loss{background:var(--loss-bg);color:var(--loss)} .badge.loss{background:var(--loss-bg);color:var(--loss)}
.badge.dir{background:var(--dir-bg);color:var(--accent)} .badge.dir{background:var(--dir-bg);color:var(--accent)}
.badge.planned{background:var(--planned-bg);color:var(--planned-text)} .badge.planned{background:var(--planned-bg);color:var(--planned-text)}
.badge.active{background:var(--profit-bg);color:var(--profit)} .badge.active{background:var(--profit-bg);color:var(--profit)}
.badge.expired{background:var(--expired-bg);color:var(--expired-text)} .badge.expired{background:var(--expired-bg);color:var(--expired-text)}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:1.5rem} .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:1.5rem}
.stat-item{ .stat-item{
background:var(--card-inner);padding:1rem;border-radius:12px;text-align:center; background:var(--card-inner);padding:1rem;border-radius:12px;text-align:center;
border:1px solid var(--card-border);position:relative;overflow:hidden; border:1px solid var(--card-border);position:relative;overflow:hidden;
} }
.stat-item::before{ .stat-item::before{
content:"";position:absolute;top:0;left:0;right:0;height:2px; content:"";position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--accent),var(--accent-2));opacity:.5; background:linear-gradient(90deg,var(--accent),var(--accent-2));opacity:.5;
} }
.stat-item .label{font-size:.8rem;color:var(--text-muted)} .stat-item .label{font-size:.8rem;color:var(--text-muted)}
.stat-item .value{font-size:1.4rem;font-weight:600;color:var(--text-title);margin-top:.25rem} .stat-item .value{font-size:1.4rem;font-weight:600;color:var(--text-title);margin-top:.25rem}
.symbol-wrap{position:relative} .symbol-wrap{position:relative}
.symbol-dropdown{ .symbol-dropdown{
position:absolute;top:100%;left:0;right:0; position:absolute;top:100%;left:0;right:0;
background:var(--input-bg);border:1px solid var(--input-border); background:var(--input-bg);border:1px solid var(--input-border);
border-radius:10px;margin-top:4px;z-index:100; border-radius:10px;margin-top:4px;z-index:100;
max-height:240px;overflow-y:auto;display:none; max-height:240px;overflow-y:auto;display:none;
box-shadow:var(--shadow-card); box-shadow:var(--shadow-card);
} }
.symbol-dropdown.show{display:block} .symbol-dropdown.show{display:block}
.symbol-option{padding:.65rem 1rem;cursor:pointer;font-size:.85rem;border-bottom:1px solid var(--table-border)} .symbol-option{padding:.65rem 1rem;cursor:pointer;font-size:.85rem;border-bottom:1px solid var(--table-border)}
.symbol-option:hover{background:var(--list-item-bg)} .symbol-option:hover{background:var(--list-item-bg)}
.symbol-option .sub{font-size:.75rem;color:var(--text-muted);margin-top:2px} .symbol-option .sub{font-size:.75rem;color:var(--text-muted);margin-top:2px}
.symbol-selected{font-size:.75rem;color:var(--accent);margin-top:4px} .symbol-selected{font-size:.75rem;color:var(--accent);margin-top:4px}
.check-row{display:flex;flex-wrap:wrap;gap:1rem;margin:.75rem 0} .check-row{display:flex;flex-wrap:wrap;gap:1rem;margin:.75rem 0}
.check-row label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;color:var(--text-muted);cursor:pointer} .check-row label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;color:var(--text-muted);cursor:pointer}
.check-row input{width:auto} .check-row input{width:auto}
.hint{font-size:.78rem;color:var(--text-muted);line-height:1.5;margin-top:.5rem} .hint{font-size:.78rem;color:var(--text-muted);line-height:1.5;margin-top:.5rem}
.filter-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem} .filter-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem}
.filter-row .field{width:auto;min-width:140px} .filter-row .field{width:auto;min-width:140px}
.filter-row button{width:auto} .filter-row button{width:auto}
.split-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:stretch;margin-bottom:1.5rem} .split-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:stretch;margin-bottom:1.5rem}
.split-grid .card{margin-bottom:0;height:100%;min-height:480px;display:flex;flex-direction:column} .split-grid .card{margin-bottom:0;height:100%;min-height:480px;display:flex;flex-direction:column}
.split-grid .card-body{flex:1;overflow:auto} .split-grid .card-body{flex:1;overflow:auto}
.card-scroll{max-height:420px;overflow-y:auto} .card-scroll{max-height:420px;overflow-y:auto}
.preset-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem} .preset-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}
.preset-tabs a{ .preset-tabs a{
padding:.45rem .85rem;border-radius:8px; padding:.45rem .85rem;border-radius:8px;
border:1px solid var(--input-border); border:1px solid var(--input-border);
color:var(--text-muted);text-decoration:none;font-size:.85rem; color:var(--text-muted);text-decoration:none;font-size:.85rem;
} }
.preset-tabs a.active,.preset-tabs a:hover{background:var(--dir-bg);color:var(--accent);border-color:var(--accent)} .preset-tabs a.active,.preset-tabs a:hover{background:var(--dir-bg);color:var(--accent);border-color:var(--accent)}
.btn-link{color:var(--accent);cursor:pointer;font-size:.85rem;background:none;border:none;padding:0} .btn-link{color:var(--accent);cursor:pointer;font-size:.85rem;background:none;border:none;padding:0}
.btn-link:hover{text-decoration:underline} .btn-link:hover{text-decoration:underline}
.modal-mask{position:fixed;inset:0;background:var(--modal-mask);z-index:1000;display:none;align-items:center;justify-content:center;padding:1rem} .modal-mask{position:fixed;inset:0;background:var(--modal-mask);z-index:1000;display:none;align-items:center;justify-content:center;padding:1rem}
.modal-mask.show{display:flex} .modal-mask.show{display:flex}
.modal-box{ .modal-box{
background:var(--card-bg);border:1px solid var(--card-border); background:var(--card-bg);border:1px solid var(--card-border);
border-radius:16px;max-width:900px;width:100%; border-radius:16px;max-width:900px;width:100%;
max-height:90vh;overflow:auto;padding:1.5rem; max-height:90vh;overflow:auto;padding:1.5rem;
box-shadow:var(--shadow-card); box-shadow:var(--shadow-card);
} }
.modal-box h3{margin-bottom:1rem;color:var(--text-label)} .modal-box h3{margin-bottom:1rem;color:var(--text-label)}
.modal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;font-size:.9rem} .modal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;font-size:.9rem}
.modal-grid .item label{color:var(--text-muted);font-size:.75rem;display:block} .modal-grid .item label{color:var(--text-muted);font-size:.75rem;display:block}
.modal-grid .item div{margin-top:.2rem;color:var(--text-primary)} .modal-grid .item div{margin-top:.2rem;color:var(--text-primary)}
.modal-close{float:right;color:var(--text-muted);cursor:pointer;font-size:1.2rem} .form-compact .line-plan-1{grid-template-columns:minmax(0,1fr) minmax(72px,0.45fr) minmax(0,1.6fr)}
.calc-readonly{background:var(--calc-bg);color:var(--accent)} .form-compact .line-plan-2{grid-template-columns:repeat(4,minmax(0,1fr)) auto;align-items:center}
@media(max-width:1100px){ .form-compact .field-short select{padding:.55rem .5rem}
.split-grid{grid-template-columns:1fr} .badge.emotion{background:var(--loss-bg);color:var(--loss);font-weight:600;border:1px solid var(--loss)}
.split-grid .card{min-height:auto} tr.row-emotion td{color:var(--loss)}
} tr.row-emotion td .badge.emotion{background:var(--loss);color:#fff;border-color:var(--loss)}
@media(max-width:768px){ .modal-box.review-modal-wide{max-width:960px}
.site-header{padding:1.25rem .75rem 1rem} .modal-box.review-modal-fullscreen{
.site-title{font-size:1.35rem;margin-bottom:.85rem} width:calc(100vw - 1.5rem);max-width:none;
.header-tools{position:static;justify-content:center;margin-bottom:.75rem} height:calc(100vh - 1.5rem);max-height:none;
.user-bar{position:static;text-align:center;margin-bottom:.75rem} border-radius:12px;display:flex;flex-direction:column;
.site-nav{gap:.35rem} }
.site-nav a{padding:.45rem .75rem;font-size:.82rem} .modal-box.review-modal-fullscreen h3{flex-shrink:0}
} #review-modal-body.review-modal-body{flex:1;overflow:auto;min-height:0}
</style> .review-detail-table{margin-bottom:1rem;overflow-x:auto}
{% block extra_css %}{% endblock %} .review-detail-headers,.review-detail-values{
</head> display:grid;
<body> grid-template-columns:repeat(16,minmax(52px,1fr));
<div class="page-wrap"> gap:.35rem .4rem;align-items:start;
<header class="site-header"> }
<div class="header-tools"> .review-detail-headers span{
<button type="button" id="theme-toggle" class="theme-toggle" data-active="dark" aria-label="切换主题"> font-size:.68rem;color:var(--text-muted);
<span class="icon-light">浅色</span> white-space:nowrap;line-height:1.3;
<span class="icon-dark">深色</span> }
</button> .review-detail-values span{
</div> font-size:.82rem;color:var(--text-primary);
<div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div> line-height:1.35;word-break:break-all;
<h1 class="site-title">国内期货 | 交易监控 + 复盘一体化</h1> }
<nav class="site-nav"> .review-detail-values span.emotion-val{color:var(--loss);font-weight:600}
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a> .review-detail-fields{padding:.25rem 0}
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a> .review-detail-section{margin-bottom:.85rem}
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a> .review-detail-section h4{font-size:.78rem;color:var(--text-label);margin-bottom:.45rem;font-weight:600}
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a> .review-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.45rem .65rem}
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a> .review-detail-item label{display:block;font-size:.7rem;color:var(--text-muted);margin-bottom:.15rem}
</nav> .review-detail-item div{font-size:.85rem;color:var(--text-primary);line-height:1.35}
</header> .review-detail-item.wide{grid-column:span 2}
<main class="main"> .review-detail-item.full{grid-column:1/-1}
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %} .review-detail-item.emotion div{color:var(--loss);font-weight:600}
{% block content %}{% endblock %} .review-detail-image{flex-shrink:0;padding-top:.75rem;border-top:1px solid var(--table-border)}
</main> .review-detail-image img{width:100%;border-radius:10px;border:1px solid var(--card-border)}
</div> .review-detail-image .no-img{color:var(--text-muted);font-size:.85rem;padding:2rem;text-align:center;background:var(--card-inner);border-radius:10px}
<script src="{{ url_for('static', filename='js/symbol.js') }}"></script> .modal-close{float:right;color:var(--text-muted);cursor:pointer;font-size:1.2rem}
{% block extra_js %}{% endblock %} .calc-readonly{background:var(--calc-bg);color:var(--accent)}
</body> @media(max-width:1100px){
</html> .split-grid{grid-template-columns:1fr}
.split-grid .card{min-height:auto}
}
@media(max-width:768px){
.site-header{padding:1.25rem .75rem 1rem}
.site-title{font-size:1.35rem;margin-bottom:.85rem}
.header-tools{position:static;justify-content:center;margin-bottom:.75rem}
.user-bar{position:static;text-align:center;margin-bottom:.75rem}
.site-nav{gap:.35rem}
.site-nav a{padding:.45rem .75rem;font-size:.82rem}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="page-wrap">
<header class="site-header">
<div class="header-tools">
<button type="button" id="theme-toggle" class="theme-toggle" data-active="dark" aria-label="切换主题">
<span class="icon-light">浅色</span>
<span class="icon-dark">深色</span>
</button>
</div>
<div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div>
<h1 class="site-title">国内期货 | 交易监控 + 复盘一体化</h1>
<nav class="site-nav">
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
</nav>
</header>
<main class="main">
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
{% block content %}{% endblock %}
</main>
</div>
<script src="{{ url_for('static', filename='js/symbol.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+95 -97
View File
@@ -1,97 +1,95 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}开单计划 - 国内期货监控系统{% endblock %} {% block title %}开单计划 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<div class="split-grid"> <div class="split-grid">
<div class="card"> <div class="card">
<span class="card-corner tl"></span><span class="card-corner br"></span> <span class="card-corner tl"></span><span class="card-corner br"></span>
<h2>今日计划 <span class="text-muted" style="font-size:.8rem;font-weight:normal">今日 {{ today }}</span></h2> <h2>今日计划 <span class="text-muted" style="font-size:.8rem;font-weight:normal">今日 {{ today }}</span></h2>
<div class="card-body"> <div class="card-body">
<p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;下方为进行中计划。</p> <p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;请先选择<strong>主力合约</strong>下方为进行中计划。</p>
<form action="{{ url_for('add_plan') }}" method="post" class="form-compact"> <form action="{{ url_for('add_plan') }}" method="post" class="form-compact">
<div class="form-line line-3"> <div class="form-line line-plan-1">
<div class="symbol-wrap"> <div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required> <input type="hidden" name="market_code" required>
<input type="hidden" name="sina_code"> <input type="hidden" name="sina_code">
<div class="symbol-dropdown"></div> <div class="symbol-dropdown"></div>
<div class="symbol-selected"></div> <div class="symbol-selected"></div>
</div> </div>
<select name="direction" required> <select name="direction" class="field-short" required>
<option value="">方向</option> <option value="">方向</option>
<option value="long">做多</option> <option value="long">做多</option>
<option value="short">做空</option> <option value="short">做空</option>
</select> </select>
<input name="decision_reason" type="text" placeholder="决策理由"> <input name="decision_reason" type="text" placeholder="决策理由">
</div> </div>
<div class="form-line line-4"> <div class="form-line line-plan-2">
<input name="zone_lower" type="number" step="0.0001" placeholder="决策区间下限" required> <input name="zone_lower" type="number" step="0.0001" placeholder="决策区间下限" required>
<input name="zone_upper" type="number" step="0.0001" placeholder="决策区间上限" required> <input name="zone_upper" type="number" step="0.0001" placeholder="决策区间上限" required>
<input name="stop_loss" type="number" step="0.0001" placeholder="止损" required> <input name="stop_loss" type="number" step="0.0001" placeholder="止损" required>
<input name="take_profit" type="number" step="0.0001" placeholder="止盈" required> <input name="take_profit" type="number" step="0.0001" placeholder="止盈" required>
</div> <button type="submit" class="btn-primary">添加</button>
<div class="form-line line-btn"> </div>
<button type="submit" class="btn-primary">添加</button> </form>
</div> <h3 class="section-label">进行中</h3>
</form> <div class="list card-scroll">
<h3 class="section-label">进行中</h3> {% for p in plans %}
<div class="list card-scroll"> <div class="list-item" style="padding:.75rem;font-size:.85rem">
{% for p in plans %} <div>
<div class="list-item" style="padding:.75rem;font-size:.85rem"> <strong>{{ p.symbol_name or p.symbol }}</strong>
<div> <span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span>
<strong>{{ p.symbol_name or p.symbol }}</strong> {% if p.status == 'planned' %}<span class="badge planned">待触发</span>
<span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span> {% else %}<span class="badge active">已激活</span>{% endif %}
{% if p.status == 'planned' %}<span class="badge planned">待触发</span> </div>
{% else %}<span class="badge active">已激活</span>{% endif %} <div>
</div> 区间{{ p.zone_lower }}~{{ p.zone_upper }}
<div> {% if p.decision_reason %} · {{ p.decision_reason }}{% endif %}
区间{{ p.zone_lower }}~{{ p.zone_upper }} · 损{{ p.stop_loss }}{{ p.take_profit }}
{% if p.decision_reason %} · {{ p.decision_reason }}{% endif %} </div>
· 损{{ p.stop_loss }} 盈{{ p.take_profit }} <a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')"></a>
</div> </div>
<a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')"></a> {% else %}
</div> <div class="empty-hint">今日暂无进行中的计划</div>
{% else %} {% endfor %}
<div class="empty-hint">今日暂无进行中的计划</div> </div>
{% endfor %} </div>
</div> </div>
</div>
</div> <div class="card">
<h2>历史计划</h2>
<div class="card"> <div class="card-body">
<h2>历史计划</h2> <form method="get" class="filter-row">
<div class="card-body"> <div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div>
<form method="get" class="filter-row"> <div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div>
<div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div> <button type="submit" class="btn-primary">筛选</button>
<div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div> <a href="{{ url_for('plans') }}" class="text-muted" style="font-size:.85rem;padding:.7rem">重置</a>
<button type="submit" class="btn-primary">筛选</button> </form>
<a href="{{ url_for('plans') }}" class="text-muted" style="font-size:.85rem;padding:.7rem">重置</a> <div class="card-scroll">
</form> <table>
<div class="card-scroll"> <thead><tr><th>日期</th><th>品种</th><th>方向</th><th>决策区间</th><th>决策理由</th><th>状态</th></tr></thead>
<table> <tbody>
<thead><tr><th>日期</th><th>品种</th><th>方向</th><th>决策区间</th><th>决策理由</th><th>状态</th></tr></thead> {% for p in history %}
<tbody> <tr>
{% for p in history %} <td>{{ p.plan_date or '' }}</td>
<tr> <td>{{ p.symbol_name or p.symbol }}</td>
<td>{{ p.plan_date or '' }}</td> <td><span class="badge dir">{{ '多' if p.direction == 'long' else '' }}</span></td>
<td>{{ p.symbol_name or p.symbol }}</td> <td>{{ p.zone_lower }}~{{ p.zone_upper }}</td>
<td><span class="badge dir">{{ '多' if p.direction == 'long' else '' }}</span></td> <td>{{ p.decision_reason or '' }}</td>
<td>{{ p.zone_lower }}~{{ p.zone_upper }}</td> <td>
<td>{{ p.decision_reason or '—' }}</td> {% if p.status == 'closed' %}<span class="badge profit">完成</span>
<td> {% elif p.status == 'expired' %}<span class="badge expired">失效</span>
{% if p.status == 'closed' %}<span class="badge profit">完成</span> {% else %}{{ p.status }}{% endif %}
{% elif p.status == 'expired' %}<span class="badge expired">失效</span> </td>
{% else %}{{ p.status }}{% endif %} </tr>
</td> {% else %}
</tr> <tr><td colspan="6" class="text-muted">暂无历史</td></tr>
{% else %} {% endfor %}
<tr><td colspan="6" class="text-muted">暂无历史</td></tr> </tbody>
{% endfor %} </table>
</tbody> </div>
</table> </div>
</div> </div>
</div> </div>
</div> {% endblock %}
</div>
{% endblock %}
+181 -177
View File
@@ -1,177 +1,181 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<div class="split-grid records-split"> <div class="split-grid records-split">
<div class="card"> <div class="card">
<h2>复盘上传</h2> <h2>复盘上传</h2>
<div class="card-body"> <div class="card-body">
<form id="review-form" action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data" class="form-compact form-compact-review line-tight"> <form id="review-form" action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data" class="form-compact form-compact-review line-tight">
<div class="form-line line-4"> <div class="form-line line-4">
<div class="symbol-wrap"> <div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required> <input type="hidden" name="market_code" required>
<input type="hidden" name="sina_code"> <input type="hidden" name="sina_code">
<div class="symbol-dropdown"></div> <div class="symbol-dropdown"></div>
<div class="symbol-selected"></div> <div class="symbol-selected"></div>
</div> </div>
<select name="direction" required> <select name="direction" required>
<option value="">方向</option> <option value="">方向</option>
<option value="long">做多</option> <option value="long">做多</option>
<option value="short">做空</option> <option value="short">做空</option>
</select> </select>
<input name="lots" type="number" step="1" min="1" value="1" placeholder="张数" required> <input name="lots" type="number" step="1" min="1" value="1" placeholder="张数" required>
<input name="timeframe" value="5m" placeholder="周期"> <input name="timeframe" value="5m" placeholder="周期">
</div> </div>
<div class="form-line line-4"> <div class="form-line line-4">
<input name="entry_price" type="number" step="0.0001" placeholder="成交价" required> <input name="entry_price" type="number" step="0.0001" placeholder="成交价" required>
<input name="stop_loss" type="number" step="0.0001" placeholder="止损" required> <input name="stop_loss" type="number" step="0.0001" placeholder="止损" required>
<input name="take_profit" type="number" step="0.0001" placeholder="止盈" required> <input name="take_profit" type="number" step="0.0001" placeholder="止盈" required>
<input name="close_price" type="number" step="0.0001" placeholder="平仓价" required> <input name="close_price" type="number" step="0.0001" placeholder="平仓价" required>
</div> </div>
<div class="form-line line-4"> <div class="form-line line-4">
<div class="mini-field"><span>开仓时间</span><input type="datetime-local" name="open_time" required></div> <div class="mini-field"><span>开仓时间</span><input type="datetime-local" name="open_time" required></div>
<div class="mini-field"><span>平仓时间</span><input type="datetime-local" name="close_time" required></div> <div class="mini-field"><span>平仓时间</span><input type="datetime-local" name="close_time" required></div>
<input id="holding_duration" type="text" readonly class="calc-readonly" placeholder="持仓时长(自动)"> <input id="holding_duration" type="text" readonly class="calc-readonly" placeholder="持仓时长(自动)">
<input name="pnl" type="number" step="0.01" placeholder="盈亏金额(手动)"> <input name="pnl" type="number" step="0.01" placeholder="盈亏金额(手动)">
</div> </div>
<div class="form-line line-2"> <div class="form-line line-2">
<input id="initial_rr" type="text" readonly class="calc-readonly" placeholder="初始盈亏比(自动)"> <input id="initial_rr" type="text" readonly class="calc-readonly" placeholder="初始盈亏比(自动)">
<input id="actual_rr" type="text" readonly class="calc-readonly" placeholder="实际盈亏比(自动)"> <input id="actual_rr" type="text" readonly class="calc-readonly" placeholder="实际盈亏比(自动)">
</div> </div>
<div class="form-line line-4"> <div class="form-line line-4">
<select name="open_type" required> <select name="open_type" required>
<option value="">开仓类型</option> <option value="">开仓类型</option>
{% for t in open_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %} {% for t in open_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select> </select>
<select name="exit_trigger" required> <select name="exit_trigger" required>
<option value="">离场触发</option> <option value="">离场触发</option>
{% for t in exit_triggers %}<option value="{{ t }}">{{ t }}</option>{% endfor %} {% for t in exit_triggers %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select> </select>
<input name="exit_supplement" placeholder="离场补充"> <input name="exit_supplement" placeholder="离场补充">
<select name="watch_after_breakeven" title="保本后盯盘"> <select name="watch_after_breakeven" title="保本后盯盘">
<option value="否">保本盯盘:否</option> <option value="否">保本盯盘:否</option>
<option value="是">保本盯盘:是</option> <option value="是">保本盯盘:是</option>
</select> </select>
</div> </div>
<div class="form-line line-4"> <div class="form-line line-4">
<select name="new_position_while_occupied" title="占用时新开仓"> <select name="new_position_while_occupied" title="占用时新开仓">
<option value="否">占用新开:否</option> <option value="否">占用新开:否</option>
<option value="是">占用新开:是</option> <option value="是">占用新开:是</option>
</select> </select>
<input type="file" name="screenshot" accept="image/*" title="截图上传"> <input type="file" name="screenshot" accept="image/*" title="截图上传">
<textarea name="notes" placeholder="备注" rows="1"></textarea> <textarea name="notes" placeholder="备注" rows="1"></textarea>
<button type="submit" class="btn-primary">保存</button> <button type="submit" class="btn-primary">保存</button>
</div> </div>
<div class="kline-row"> <div class="kline-row">
<label><input type="checkbox" name="auto_kline" value="1" checked> 自动K线</label> <label><input type="checkbox" name="auto_kline" value="1" checked> 自动K线</label>
<select name="kline_period1" title="周期1">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='15m' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select> <select name="kline_period1" title="周期1">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='15m' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<select name="kline_period2" title="周期2">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='1h' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select> <select name="kline_period2" title="周期2">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='1h' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<input name="kline_count" type="number" value="300" placeholder="K线数" title="K线数"> <input name="kline_count" type="number" value="300" placeholder="K线数" title="K线数">
<select name="kline_cutoff" title="K线截止">{% for c in kline_cutoffs %}<option value="{{ c }}">{{ c }}</option>{% endfor %}</select> <select name="kline_cutoff" title="K线截止">{% for c in kline_cutoffs %}<option value="{{ c }}">{{ c }}</option>{% endfor %}</select>
</div> </div>
<p class="section-hint">勾选行为标签即为情绪单</p> <p class="section-hint">勾选行为标签即为情绪单</p>
<div class="tag-grid"> <div class="tag-grid">
{% for tag in behavior_tags %} {% for tag in behavior_tags %}
<label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label> <label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label>
{% endfor %} {% endfor %}
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h2>复盘历史</h2> <h2>复盘历史</h2>
<div class="card-body"> <div class="card-body">
<div class="preset-tabs"> <div class="preset-tabs">
<a href="{{ url_for('records', preset='today') }}" class="{% if preset=='today' %}active{% endif %}">本日</a> <a href="{{ url_for('records', preset='today') }}" class="{% if preset=='today' %}active{% endif %}">本日</a>
<a href="{{ url_for('records', preset='week') }}" class="{% if preset=='week' %}active{% endif %}">本周</a> <a href="{{ url_for('records', preset='week') }}" class="{% if preset=='week' %}active{% endif %}">本周</a>
<a href="{{ url_for('records', preset='month') }}" class="{% if preset=='month' %}active{% endif %}">本月</a> <a href="{{ url_for('records', preset='month') }}" class="{% if preset=='month' %}active{% endif %}">本月</a>
<a href="{{ url_for('records', preset='custom') }}" class="{% if preset=='custom' %}active{% endif %}">自定义</a> <a href="{{ url_for('records', preset='custom') }}" class="{% if preset=='custom' %}active{% endif %}">自定义</a>
</div> </div>
{% if preset == 'custom' or start or end %} {% if preset == 'custom' or start or end %}
<form method="get" class="filter-row"> <form method="get" class="filter-row">
<input type="hidden" name="preset" value="custom"> <input type="hidden" name="preset" value="custom">
<div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div> <div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div>
<div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div> <div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div>
<button type="submit" class="btn-primary">筛选</button> <button type="submit" class="btn-primary">筛选</button>
</form> </form>
{% endif %} {% endif %}
<div class="card-scroll"> <div class="card-scroll">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>平仓</th><th>品种</th><th>方向</th><th>盈亏</th><th>情绪单</th><th>详情</th><th></th> <th>平仓</th><th>品种</th><th>方向</th><th>盈亏</th><th>情绪单</th><th>详情</th><th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in reviews %} {% for r in reviews %}
<tr> <tr class="{% if r.is_emotion %}row-emotion{% endif %}">
<td>{{ r.close_time[:16] if r.close_time else '' }}</td> <td>{{ r.close_time[:16] if r.close_time else '' }}</td>
<td>{{ r.symbol_name or r.symbol }}</td> <td>{{ r.symbol_name or r.symbol }}</td>
<td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td> <td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
<td> <td>
{% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span> {% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span>
{% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span> {% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span>
{% else %}{{ r.actual_pnl or '-' }}{% endif %} {% else %}{{ r.actual_pnl or '-' }}{% endif %}
</td> </td>
<td>{% if r.is_emotion %}<span class="badge loss">情绪</span>{% else %}-{% endif %}</td> <td>{% if r.is_emotion %}<span class="badge emotion">情绪</span>{% else %}-{% endif %}</td>
<td> <td>
<button type="button" class="btn-link review-view-btn" data-review='{{ { <button type="button" class="btn-link review-view-btn" data-review='{{ {
"symbol": r.symbol_name or r.symbol, "symbol_code": r.symbol, "direction": "做多" if r.direction=="long" else "做空", "symbol": r.symbol_name or r.symbol, "symbol_code": r.symbol,
"entry_price": r.entry_price, "stop_loss": r.stop_loss, "take_profit": r.take_profit, "direction": "做多" if r.direction=="long" else "做空",
"close_price": r.close_price, "lots": r.lots, "lots": r.lots, "timeframe": r.timeframe,
"open_time": r.open_time, "close_time": r.close_time, "entry_price": r.entry_price, "stop_loss": r.stop_loss, "take_profit": r.take_profit,
"holding_duration": r.holding_duration, "initial_pnl": r.initial_pnl, "close_price": r.close_price,
"actual_pnl": r.actual_pnl, "pnl": r.pnl, "open_time": r.open_time, "close_time": r.close_time,
"open_type": r.open_type, "holding_duration": r.holding_duration,
"exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement, "initial_pnl": r.initial_pnl, "actual_pnl": r.actual_pnl, "pnl": r.pnl,
"is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags, "open_type": r.open_type,
"notes": r.notes, "screenshot": r.screenshot "exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement,
} | tojson }}'>放大查看</button> "watch_after_breakeven": r.watch_after_breakeven,
</td> "new_position_while_occupied": r.new_position_while_occupied,
<td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td> "is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags,
</tr> "notes": r.notes, "screenshot": r.screenshot
{% else %} } | tojson }}'>放大查看</button>
<tr><td colspan="7" class="text-muted">暂无复盘记录</td></tr> </td>
{% endfor %} <td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tbody> </tr>
</table> {% else %}
</div> <tr><td colspan="7" class="text-muted">暂无复盘记录</td></tr>
</div> {% endfor %}
</div> </tbody>
</div> </table>
</div>
<div id="review-modal" class="modal-mask"> </div>
<div class="modal-box"> </div>
<span class="modal-close"></span> </div>
<h3>复盘详情</h3>
<div id="review-modal-body"></div> <div id="review-modal" class="modal-mask">
</div> <div class="modal-box review-modal-fullscreen">
</div> <span class="modal-close"></span>
<h3>复盘详情</h3>
{% if auto_records %} <div id="review-modal-body" class="review-modal-body"></div>
<div class="card" style="margin-top:1rem"> </div>
<h2>系统自动记录(止盈/止损)</h2> </div>
<table>
<thead><tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr></thead> {% if auto_records %}
<tbody> <div class="card" style="margin-top:1rem">
{% for r in auto_records %} <h2>系统自动记录(止盈/止损)</h2>
<tr> <table>
<td>{{ r.symbol_name or r.symbol }}</td> <thead><tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr></thead>
<td>{{ r.monitor_type }}</td> <tbody>
<td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td> {% for r in auto_records %}
<td>{{ r.trigger_price }}</td> <tr>
<td>{% if r.result == '止盈' %}<span class="badge profit">止盈</span>{% else %}<span class="badge loss">止损</span>{% endif %}</td> <td>{{ r.symbol_name or r.symbol }}</td>
<td>{{ r.created_at[:16] if r.created_at else '' }}</td> <td>{{ r.monitor_type }}</td>
</tr> <td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
{% endfor %} <td>{{ r.trigger_price }}</td>
</tbody> <td>{% if r.result == '止盈' %}<span class="badge profit">止盈</span>{% else %}<span class="badge loss">止损</span>{% endif %}</td>
</table> <td>{{ r.created_at[:16] if r.created_at else '' }}</td>
</div> </tr>
{% endif %} {% endfor %}
{% endblock %} </tbody>
{% block extra_js %} </table>
<script src="{{ url_for('static', filename='js/review.js') }}"></script> </div>
{% endblock %} {% endif %}
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
{% endblock %}