Files
gold_monitor/app/templates/dashboard.html
T
2026-10-21 20:32:24 +08:00

212 lines
7.8 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 %}">{% if t.side=='buy' %}买入{% elif t.side=='sell' %}卖出{% else %}{{ t.side }}{% endif %}</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, '&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';
var sideCn = t.side === 'buy' ? '买入' : (t.side === 'sell' ? '卖出' : (t.side || '—'));
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(sideCn) + '</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 %}