Files
finance-talk/ft-app/app/templates/admin.html
T
2026-07-26 14:54:52 +08:00

349 lines
16 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 %}系统管理{% endblock %}
{% block heading %}系统管理{% endblock %}
{% block breadcrumb %}品种与合约{% endblock %}
{% block content %}
<style>
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
.tab-btn {
padding: 10px 20px; border: none; background: none;
font-size: 0.88rem; font-weight: 500; color: var(--sub);
cursor: pointer; border-bottom: 2px solid transparent;
margin-bottom: -2px; transition: color .12s, border-color .12s;
}
.tab-btn:hover { color: var(--fg); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
</style>
{% set tab = request.query_params.get('tab', 'sync') %}
<div class="tabs">
<button class="tab-btn{% if tab == 'sync' %} active{% endif %}" onclick="switchTab('sync')">📡 数据同步</button>
<button class="tab-btn{% if tab == 'product' %} active{% endif %}" onclick="switchTab('product')">🏷️ 品种管理</button>
<button class="tab-btn{% if tab == 'contract' %} active{% endif %}" onclick="switchTab('contract')">📋 合约管理</button>
</div>
{# ═══════════════════ Tab: 数据同步 ═══════════════════ #}
<div class="tab-panel{% if tab == 'sync' %} active{% endif %}" id="tab-sync">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_contracts }} 个合约</span>
</div>
<div id="sync-progress" style="display:none;margin-bottom:18px;padding:12px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">
<span id="sync-progress-label" style="font-size:0.82rem;color:var(--fg);">正在同步...</span>
<span id="sync-progress-pct" style="font-size:0.8rem;color:var(--sub);margin-left:auto;"></span>
</div>
<div style="width:100%;height:5px;background:var(--border);border-radius:3px;overflow:hidden;">
<div id="sync-progress-bar" style="width:0%;height:100%;background:var(--accent);border-radius:3px;transition:width .3s;"></div>
</div>
</div>
{% if products %}
{% for p in products %}
<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
<div class="product-header"
style="display:flex;align-items:center;gap:12px;padding:12px 18px;background:var(--th-bg);cursor:pointer;user-select:none;"
onclick="toggleProduct(this)">
<span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;transform:rotate(-90deg);"></span>
<strong style="font-size:0.92rem;">{{ p.code }} · {{ p.name }}</strong>
<span style="font-size:0.78rem;color:var(--sub);">{{ p.exchange }}</span>
<span style="font-size:0.78rem;color:var(--sub);margin-left:8px;">{{ p.contracts|length }} 合约</span>
<form method="post" action="/admin/sync/product/{{ p.id }}" style="display:inline;margin-left:auto;" onclick="event.stopPropagation()">
<button type="submit" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--accent);color:#fff;border-radius:5px;">同步行情</button>
</form>
</div>
<div class="product-body" style="display:none;">
<table style="font-size:0.84rem;">
<tr><th style="text-align:left;padding:8px 14px;">合约</th><th style="text-align:center;padding:8px 14px;">行情</th><th style="text-align:center;padding:8px 14px;">持仓</th><th style="text-align:center;padding:8px 14px;">状态</th><th style="text-align:center;padding:8px 14px;">操作</th></tr>
{% for c in p.contracts %}
<tr>
<td style="text-align:left;padding:6px 14px;"><strong>{{ c.code }}</strong></td>
<td style="text-align:center;padding:6px 14px;">{{ c.bar_count }}</td>
<td style="text-align:center;padding:6px 14px;">{{ c.pos_days }}</td>
<td style="text-align:center;padding:6px 14px;">
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
</td>
<td style="text-align:center;padding:6px 14px;">
<a href="/contracts/{{ c.code }}" style="color:var(--accent);text-decoration:none;font-size:0.82rem;">查看</a>
<button style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:0.82rem;margin-left:12px;" onclick="syncBars('{{ c.code }}')">↻ 行情</button>
<button style="background:none;border:none;color:var(--success);cursor:pointer;font-size:0.82rem;margin-left:10px;" onclick="syncPositions('{{ c.code }}')">↻ 持仓</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endfor %}
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
{% endif %}
</div>
{# ═══════════════════ Tab: 品种管理 ═══════════════════ #}
<div class="tab-panel{% if tab == 'product' %} active{% endif %}" id="tab-product">
<div class="form-card" style="margin-bottom:24px;">
<div class="section-title">新建品种</div>
<form method="post" action="/admin/product" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
<label>品种代码</label>
<input type="text" name="code" required placeholder="FG" maxlength="6" style="text-transform:uppercase;">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
<label>品种名称</label>
<input type="text" name="name" required placeholder="玻璃">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
<label>交易所</label>
<select name="exchange" required>
{% for ex in exchanges %}
<option value="{{ ex }}">{{ ex }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">新建</button>
</form>
</div>
{% if products %}
<div class="table-wrap">
<table>
<tr><th>代码</th><th>名称</th><th>交易所</th><th>合约数</th><th>操作</th></tr>
{% for p in products %}
<tr>
<td><strong>{{ p.code }}</strong></td>
<td>{{ p.name }}</td>
<td>{{ p.exchange }}</td>
<td>{{ p.contracts|length }}</td>
<td>
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;">
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先新建。</div>
{% endif %}
</div>
{# ═══════════════════ Tab: 合约管理 ═══════════════════ #}
<div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract">
{% if products %}
{# ── Add contract form ── #}
<div class="form-card" style="margin-bottom:24px;">
<div class="section-title">添加合约</div>
<form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>品种</label>
<select name="product_id" required>
<option value="">-- 选择品种 --</option>
{% for p in products %}
<option value="{{ p.id }}">{{ p.code }} · {{ p.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>月份 <span style="font-weight:400;color:var(--sub);font-size:0.75rem;">如 2609</span></label>
<input type="text" name="month" required placeholder="2609" maxlength="4" style="text-transform:uppercase;">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</div>
{# ── Product tabs for contract list ── #}
{% set ctab = request.query_params.get('ctab', products[0].code if products else '') %}
<div class="tabs" style="margin-top:24px;">
{% for p in products %}
<a href="/admin/?tab=contract&ctab={{ p.code }}" class="tab-btn{% if ctab == p.code %} active{% endif %}">{{ p.code }} · {{ p.name }}</a>
{% endfor %}
</div>
{% for p in products %}
{% if p.code == ctab and p.contracts %}
<div class="table-wrap" style="margin-bottom:20px;">
<table>
<tr><th>合约代码</th><th>月份</th><th>状态</th><th>操作</th></tr>
{% for c in p.contracts %}
<tr>
<td><strong>{{ c.code }}</strong></td>
<td>{{ c.name }}</td>
<td>
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
</td>
<td>
<form method="post" action="/admin/contract/{{ c.id }}/toggle" style="display:inline;">
<button style="background:none;border:none;color:var(--sub);cursor:pointer;font-size:0.82rem;">
{% if c.is_active %}停用{% else %}启用{% endif %}
</button>
</form>
<form method="post" action="/admin/contract/{{ c.id }}/delete" style="display:inline;" onsubmit="return confirm('删除 {{ c.code }}')">
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endfor %}
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
{% endif %}
</div>
<script>
function escapeHtml(s) {
var d = document.createElement('div');
d.appendChild(document.createTextNode(s));
return d.innerHTML;
}
var syncTimer = null;
var progressShownAt = 0;
function showProgress() {
progressShownAt = Date.now();
document.getElementById('sync-progress').style.display = 'block';
document.getElementById('sync-progress-bar').style.width = '0%';
document.getElementById('sync-progress-pct').textContent = '';
document.getElementById('sync-progress-label').textContent = '正在同步...';
}
function hideProgress() {
clearInterval(syncTimer);
syncTimer = null;
document.getElementById('sync-progress').style.display = 'none';
}
function pollProgress() {
fetch('/admin/sync-status')
.then(function(r) { return r.json(); })
.then(function(data) {
var pct = data.total > 0 ? Math.round(data.done / data.total * 100) : 0;
document.getElementById('sync-progress-bar').style.width = pct + '%';
document.getElementById('sync-progress-pct').textContent = pct + '%';
document.getElementById('sync-progress-label').textContent = data.label;
if (data.error) {
clearInterval(syncTimer);
syncTimer = null;
document.getElementById('sync-progress-bar').style.background = 'var(--danger)';
document.getElementById('sync-progress-label').innerHTML =
'<span style="color:var(--danger-fg);">✕ ' + escapeHtml(data.label) + '</span>' +
'<span style="display:block;font-size:0.78rem;color:var(--danger-fg);margin-top:4px;">' + escapeHtml(data.error) + '</span>';
document.getElementById('sync-progress-pct').textContent = '';
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
return;
}
if (data.finished) {
var elapsed = Date.now() - progressShownAt;
var delay = elapsed < 1000 ? 1000 - elapsed : 0;
setTimeout(function() {
clearInterval(syncTimer);
syncTimer = null;
document.getElementById('sync-progress-bar').style.width = '100%';
if (data.result > 0) {
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ · +' + data.result;
} else if (data.total > 0) {
document.getElementById('sync-progress-label').textContent = data.label + ' ⚠ 无新数据,历史日期无可用持仓';
} else {
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ 已是最新';
}
document.getElementById('sync-progress-pct').textContent = '';
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
if (data.result > 0) {
var expanded = [];
document.querySelectorAll('.product-body').forEach(function(b, i) {
if (b.style.display === 'block') expanded.push(i);
});
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
setTimeout(function() { location.reload(); }, 1200);
}
}, delay);
}
});
}
function startSync(url) {
if (syncTimer) { clearInterval(syncTimer); syncTimer = null; }
showProgress();
fetch(url, { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
// Another sync is already running — just show its progress
syncTimer = setInterval(pollProgress, 500);
pollProgress();
return;
}
syncTimer = setInterval(pollProgress, 500);
});
}
function syncBars(code) { startSync('/admin/sync/' + code + '/bg'); }
function syncPositions(code) { startSync('/admin/sync-positions/' + code + '/bg'); }
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
document.querySelector('.tab-btn[onclick="switchTab(\'' + name + '\')"]').classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
var url = new URL(window.location);
url.searchParams.set('tab', name);
history.replaceState(null, '', url);
}
function toggleProduct(header) {
var body = header.nextElementSibling;
var arrow = header.querySelector('.collapse-arrow');
if (body.style.display === 'none') {
body.style.display = 'block';
arrow.style.transform = 'rotate(0deg)';
} else {
body.style.display = 'none';
arrow.style.transform = 'rotate(-90deg)';
}
}
// Restore expanded accordion state after reload
(function() {
try {
var expanded = JSON.parse(sessionStorage.getItem('ft-expanded') || '[]');
var bodies = document.querySelectorAll('.product-body');
expanded.forEach(function(i) {
if (bodies[i]) {
bodies[i].style.display = 'block';
var arrow = bodies[i].previousElementSibling.querySelector('.collapse-arrow');
if (arrow) arrow.style.transform = 'rotate(0deg)';
}
});
sessionStorage.removeItem('ft-expanded');
} catch(e) {}
})();
// Auto-show progress bar if a sync is already running
(function() {
fetch('/admin/sync-status')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.running) {
showProgress();
syncTimer = setInterval(pollProgress, 500);
pollProgress();
}
});
})();
</script>
{% endblock %}