e68393fe5b
Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
7.6 KiB
HTML
211 lines
7.6 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}总览 · {{ app_name }}{% endblock %}
|
||
{% block content %}
|
||
<h2 style="margin:0 0 14px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
总览
|
||
<span id="live-hint" class="tag" style="font-size:12px;font-weight:500">连接中…</span>
|
||
</h2>
|
||
<div class="grid cols-3">
|
||
<div class="card"><div class="muted">账户</div><div class="stat">{{ accounts|length }}</div></div>
|
||
<div class="card">
|
||
<div class="muted">在线机器</div>
|
||
<div class="stat"><span id="stat-online">{{ online_count }}</span><span class="muted" style="font-size:16px"> / {{ machine_count }}</span></div>
|
||
</div>
|
||
<div class="card"><div class="muted">当前持仓(笔)</div><div class="stat" id="stat-trades">{{ trades|length }}</div></div>
|
||
</div>
|
||
|
||
<div class="grid cols-2" style="margin-top:14px">
|
||
<div class="card">
|
||
<h3>在线机器</h3>
|
||
<table>
|
||
<thead><tr><th>机器</th><th>状态</th><th>最近心跳</th></tr></thead>
|
||
<tbody id="online-machines-body">
|
||
{% for m in online_machines %}
|
||
<tr>
|
||
<td>{{ m.name }} <span class="muted">#{{ m.id }}</span></td>
|
||
<td><span class="tag green">在线</span></td>
|
||
<td class="muted">{{ m.last_seen_fmt }}</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="3" class="muted">当前无在线机器(客户端约每 2 秒心跳)</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card">
|
||
<h3>快速入口</h3>
|
||
<p class="muted">下载通用辅助包并用激活码绑定;本页显示当前持仓,历史成交见交易复盘。</p>
|
||
<p style="margin-top:12px">
|
||
<a class="btn" href="{{ url_for('download_page') }}">下载中心</a>
|
||
<a class="btn secondary" href="{{ url_for('records_page') }}">交易复盘</a>
|
||
</p>
|
||
<p class="muted" style="margin-top:14px">对外地址:{{ public_base }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:14px">
|
||
<h3>当前持仓</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>开仓时间</th><th>机器</th><th>品种</th><th>方向</th><th>手数</th>
|
||
<th>浮盈</th><th>账户</th><th>单号</th><th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="live-trades-body">
|
||
{% for t in trades %}
|
||
<tr>
|
||
<td class="muted">{{ t.time_fmt }}</td>
|
||
<td>{{ t.machine_name or '—' }}</td>
|
||
<td>{{ t.symbol }}</td>
|
||
<td><span class="tag {% if t.side=='buy' %}green{% else %}red{% endif %}">{{ t.side }}</span></td>
|
||
<td>{{ t.volume if t.volume is not none else '—' }}</td>
|
||
<td style="color:{% if t.profit is not none and t.profit>0 %}var(--green){% elif t.profit is not none and t.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
||
{% if t.profit is none %}—{% else %}{{ '%.2f'|format(t.profit) }}{% endif %}
|
||
</td>
|
||
<td>{{ t.account_name or '—' }}</td>
|
||
<td class="muted">{{ t.ticket or '—' }}</td>
|
||
<td><a href="{{ url_for('record_detail', tid=t.id) }}">复盘</a></td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="9" class="muted">当前无持仓。开仓后由客户端上报显示。</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% endblock %}
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
var streamUrl = {{ url_for('dashboard_stream')|tojson }};
|
||
var liveUrl = {{ url_for('dashboard_live')|tojson }};
|
||
var detailBase = {{ url_for('record_detail', tid=0)|tojson }}.replace(/0$/, '');
|
||
var hint = document.getElementById('live-hint');
|
||
var mode = 'connecting';
|
||
var pollStarted = false;
|
||
var msgCount = 0;
|
||
|
||
function pad(n) { return n < 10 ? '0' + n : String(n); }
|
||
function nowClock() {
|
||
var d = new Date();
|
||
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
||
}
|
||
|
||
function setHint(text, kind) {
|
||
if (!hint) return;
|
||
hint.textContent = text;
|
||
hint.className = 'tag' + (kind ? ' ' + kind : '');
|
||
}
|
||
|
||
function markUpdated() {
|
||
if (mode === 'sse') {
|
||
setHint('SSE · 更新于 ' + nowClock(), 'green');
|
||
} else if (mode === 'poll') {
|
||
setHint('轮询 1s · 更新于 ' + nowClock(), '');
|
||
}
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function renderMachines(list) {
|
||
var body = document.getElementById('online-machines-body');
|
||
if (!body) return;
|
||
if (!list.length) {
|
||
body.innerHTML = '<tr><td colspan="3" class="muted">当前无在线机器(客户端约每 2 秒心跳)</td></tr>';
|
||
return;
|
||
}
|
||
body.innerHTML = list.map(function (m) {
|
||
return '<tr><td>' + esc(m.name) + ' <span class="muted">#' + esc(m.id) + '</span></td>' +
|
||
'<td><span class="tag green">在线</span></td>' +
|
||
'<td class="muted">' + esc(m.last_seen_fmt || '—') + '</td></tr>';
|
||
}).join('');
|
||
}
|
||
|
||
function profitColor(p) {
|
||
if (p == null || p === '') return 'inherit';
|
||
var n = Number(p);
|
||
if (n > 0) return 'var(--green)';
|
||
if (n < 0) return 'var(--red)';
|
||
return 'inherit';
|
||
}
|
||
|
||
function renderTrades(list) {
|
||
var body = document.getElementById('live-trades-body');
|
||
if (!body) return;
|
||
document.getElementById('stat-trades').textContent = String(list.length);
|
||
if (!list.length) {
|
||
body.innerHTML = '<tr><td colspan="9" class="muted">当前无持仓。开仓后由客户端上报显示。</td></tr>';
|
||
return;
|
||
}
|
||
body.innerHTML = list.map(function (t) {
|
||
var sideCls = t.side === 'buy' ? 'green' : 'red';
|
||
return '<tr>' +
|
||
'<td class="muted">' + esc(t.time_fmt) + '</td>' +
|
||
'<td>' + esc(t.machine_name || '—') + '</td>' +
|
||
'<td>' + esc(t.symbol) + '</td>' +
|
||
'<td><span class="tag ' + sideCls + '">' + esc(t.side) + '</span></td>' +
|
||
'<td>' + esc(t.volume != null ? t.volume : '—') + '</td>' +
|
||
'<td style="color:' + profitColor(t.profit) + '">' +
|
||
(t.profit == null || t.profit === '' ? '—' : Number(t.profit).toFixed(2)) +
|
||
'</td>' +
|
||
'<td>' + esc(t.account_name || '—') + '</td>' +
|
||
'<td class="muted">' + esc(t.ticket || '—') + '</td>' +
|
||
'<td><a href="' + detailBase + t.id + '">复盘</a></td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
}
|
||
|
||
function apply(data) {
|
||
if (!data || !data.ok) return;
|
||
document.getElementById('stat-online').textContent = String(data.online_count);
|
||
renderMachines(data.online_machines || []);
|
||
renderTrades(data.trades || []);
|
||
markUpdated();
|
||
}
|
||
|
||
function startPollFallback(reason) {
|
||
if (pollStarted) return;
|
||
pollStarted = true;
|
||
mode = 'poll';
|
||
setHint(reason || '轮询 1s(SSE 不可用)', '');
|
||
setInterval(function () {
|
||
fetch(liveUrl, { credentials: 'same-origin' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(apply)
|
||
.catch(function () { setHint('轮询失败 · 重试中', 'red'); });
|
||
}, 1000);
|
||
}
|
||
|
||
if (typeof EventSource === 'undefined') {
|
||
startPollFallback('轮询 1s(浏览器不支持 SSE)');
|
||
return;
|
||
}
|
||
|
||
setHint('SSE 连接中…', '');
|
||
var es = new EventSource(streamUrl);
|
||
es.onopen = function () {
|
||
mode = 'sse';
|
||
setHint('SSE 已连接', 'green');
|
||
};
|
||
es.onmessage = function (ev) {
|
||
msgCount += 1;
|
||
mode = 'sse';
|
||
try { apply(JSON.parse(ev.data)); } catch (e) {}
|
||
};
|
||
es.onerror = function () {
|
||
// 已收到过数据则让浏览器自行重连;长时间无数据再降级轮询
|
||
if (msgCount === 0 || es.readyState === EventSource.CLOSED) {
|
||
try { es.close(); } catch (e) {}
|
||
startPollFallback('轮询 1s(SSE 断开)');
|
||
} else {
|
||
setHint('SSE 重连中…', '');
|
||
}
|
||
};
|
||
})();
|
||
</script>
|
||
{% endblock %}
|