Files
gold_monitor/app/templates/dashboard.html
T
2026-10-17 22:11:31 +08:00

209 lines
7.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 and t.profit>0 %}var(--green){% elif t.profit and t.profit<0 %}var(--red){% else %}inherit{% endif %}">
{{ t.profit if t.profit is not none else '—' }}
</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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) + '">' + esc(t.profit != null ? t.profit : '—') + '</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 %}