Files
Chan/data_provider/homepage.html

426 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LVVXH3TL04"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-LVVXH3TL04');
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chan 数据提供商</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--purple: #bc8cff;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--mono: "SF Mono", "Fira Code", "Consolas", monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
/* Header */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 28px;
}
header .brand {
display: flex;
align-items: center;
gap: 14px;
}
header .logo {
display: flex;
align-items: center;
justify-content: center;
width: 44px; height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, #1c3d5a 0%, #2d1b5e 100%);
border: 1px solid var(--border);
font-size: 20px; font-weight: 700;
color: var(--accent);
}
header .brand h1 { font-size: 20px; font-weight: 600; }
header .brand h1 span { color: var(--accent); }
header .brand .sub { font-size: 12px; color: var(--text-secondary); }
.header-time {
font-family: var(--mono);
font-size: 13px;
color: var(--text-secondary);
}
/* Overview cards */
.overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 24px;
}
.ov-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
text-align: center;
}
.ov-card .ov-label { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
.ov-card .ov-value {
font-family: var(--mono);
font-size: 18px;
font-weight: 600;
}
.ov-card .ov-value.green { color: var(--green); }
.ov-card .ov-value.orange { color: var(--orange); }
.ov-card .ov-value.accent { color: var(--accent); }
.ov-card .ov-value.purple { color: var(--purple); }
/* Section */
.section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.section-header:hover { background: rgba(255,255,255,0.02); }
.section-header h2 { font-size: 14px; font-weight: 600; }
.section-header .count-badge {
font-size: 12px;
font-family: var(--mono);
color: var(--text-secondary);
background: var(--bg);
padding: 2px 8px;
border-radius: 10px;
}
.section-body { padding: 8px 16px 12px; }
/* Status dot */
.dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.green { background: var(--green); box-shadow: 0 0 6px #3fb95060; }
.dot.orange { background: var(--orange); box-shadow: 0 0 6px #d2992260; }
.dot.red { background: var(--red); box-shadow: 0 0 6px #f8514960; }
.dot.gray { background: var(--text-secondary); }
/* Symbol row */
.symbol-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(48,54,61,0.5);
font-size: 14px;
}
.symbol-row:last-child { border-bottom: none; }
.symbol-row .name { font-family: var(--mono); font-size: 13px; min-width: 140px; }
.symbol-row .tag {
font-size: 11px;
font-family: var(--mono);
padding: 1px 6px;
border-radius: 4px;
background: rgba(88,166,255,0.1);
color: var(--accent);
}
/* TF chips */
.tf-list { display: flex; flex-wrap: wrap; gap: 4px; }
.tf-chip {
font-family: var(--mono);
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-secondary);
}
.tf-chip.base { border-color: #58a6ff40; color: var(--accent); }
.tf-chip.derived { border-color: #bc8cff40; color: var(--purple); }
/* Links bar */
.links-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 16px 0 24px;
}
.links-bar a {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
text-decoration: none;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
transition: border-color 0.15s;
}
.links-bar a:hover { border-color: var(--accent); }
/* Footer */
footer {
border-top: 1px solid var(--border);
padding: 16px 0;
margin-top: 32px;
text-align: center;
color: var(--text-secondary);
font-size: 13px;
}
.fade-in { animation: fadeIn 0.3s ease-in; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<div class="container">
<header>
<div class="brand">
<div class="logo">C</div>
<div>
<h1><span>Chan</span> 数据提供商</h1>
<div class="sub">加密货币 K 线数据服务</div>
</div>
</div>
<div class="header-time" id="header-time"></div>
</header>
<!-- Overview -->
<div class="overview" id="overview">
<div class="ov-card"><div class="ov-label">服务状态</div><div class="ov-value" id="ov-status">加载中...</div></div>
<div class="ov-card"><div class="ov-label">交易所</div><div class="ov-value accent" id="ov-exchange"></div></div>
<div class="ov-card"><div class="ov-label">交易对</div><div class="ov-value" id="ov-symbols"></div></div>
<div class="ov-card"><div class="ov-label">基础周期</div><div class="ov-value accent" id="ov-base-tf"></div></div>
<div class="ov-card"><div class="ov-label">衍生周期</div><div class="ov-value purple" id="ov-derived-tf"></div></div>
<div class="ov-card"><div class="ov-label">数据就绪</div><div class="ov-value" id="ov-ready"></div></div>
</div>
<!-- Links -->
<div class="links-bar">
<a href="/api/docs">📖 API 文档</a>
<a href="/docs">📋 Swagger UI</a>
<a href="/redoc">📄 ReDoc</a>
<a href="/api/candles?symbol=BTC/USDT:USDT&tf=1m&limit=5" target="_blank">📊 BTC 1m 示例</a>
<a href="/api/candles?symbol=ETH/USDT:USDT&tf=4h&limit=10" target="_blank">📊 ETH 4h 示例</a>
</div>
<!-- Symbols -->
<div class="section" id="section-symbols">
<div class="section-header" onclick="this.parentElement.classList.toggle('collapsed')">
<h2>📈 交易对</h2>
<span class="count-badge" id="sym-count">0</span>
</div>
<div class="section-body" id="symbol-list">
<div style="color:var(--text-secondary);font-size:13px;">加载中...</div>
</div>
</div>
<!-- Timeframes -->
<div class="section">
<div class="section-header" onclick="this.parentElement.classList.toggle('collapsed')">
<h2>⏱ 时间周期</h2>
<span class="count-badge" id="tf-total">0</span>
</div>
<div class="section-body">
<div style="margin-bottom:8px;font-size:13px;color:var(--text-secondary);">基础周期(交易所直拉)</div>
<div class="tf-list" id="base-tf-list"></div>
<div style="margin:10px 0 8px;font-size:13px;color:var(--text-secondary);">衍生周期(内存合成)</div>
<div class="tf-list" id="derived-tf-list"></div>
</div>
</div>
<!-- Quick query -->
<div class="section">
<div class="section-header">
<h2>⚡ 快速查询</h2>
</div>
<div class="section-body">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;">
<div>
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">交易对</div>
<select id="q-symbol" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;"></select>
</div>
<div>
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">周期</div>
<select id="q-tf" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;"></select>
</div>
<div>
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">数量</div>
<select id="q-limit" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;">
<option>5</option><option selected>10</option><option>20</option><option>50</option>
</select>
</div>
<button onclick="quickQuery()" style="background:var(--accent);color:#fff;border:none;padding:6px 18px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;">查询</button>
</div>
<pre id="q-result" style="margin-top:10px;display:none;"></pre>
</div>
</div>
</div>
<footer>
Chan Data Provider &nbsp;·&nbsp; Built with FastAPI + ccxt + pandas &nbsp;·&nbsp;
更新于 <span id="footer-time"></span>
</footer>
<script>
let healthData = null;
function fmtTime(ts) {
return new Date(ts).toLocaleString('zh-CN', { timeZone: 'UTC', hour12: false }) + ' UTC';
}
async function loadHealth() {
try {
const res = await fetch('/health');
healthData = await res.json();
renderHealth(healthData);
} catch {
document.getElementById('ov-status').textContent = '无法连接';
document.getElementById('ov-status').style.color = 'var(--red)';
document.getElementById('ov-ready').textContent = '断开';
document.getElementById('ov-ready').style.color = 'var(--red)';
}
}
function renderHealth(d) {
const now = Date.now();
document.getElementById('header-time').textContent = fmtTime(now);
document.getElementById('footer-time').textContent = fmtTime(now);
// Overview
const statusEl = document.getElementById('ov-status');
statusEl.textContent = '运行中';
statusEl.style.color = 'var(--green)';
document.getElementById('ov-exchange').textContent = d.exchange || '—';
const symCount = (d.symbols || []).length;
const symEl = document.getElementById('ov-symbols');
symEl.textContent = symCount + ' 个';
symEl.style.color = 'var(--accent)';
document.getElementById('ov-base-tf').textContent = (d.base_timeframes || []).length + ' 个';
document.getElementById('ov-derived-tf').textContent = (d.derived_timeframes || []).length + ' 个';
const readyEl = document.getElementById('ov-ready');
if (d.ready) {
readyEl.textContent = '已就绪';
readyEl.style.color = 'var(--green)';
} else {
readyEl.textContent = '同步中...';
readyEl.style.color = 'var(--orange)';
setTimeout(loadHealth, 2000);
}
// Symbols
const symList = document.getElementById('symbol-list');
const symCountEl = document.getElementById('sym-count');
symCountEl.textContent = symCount;
if (d.symbols && d.symbols.length > 0) {
symList.innerHTML = d.symbols.map(s => `
<div class="symbol-row">
<span class="dot green"></span>
<span class="name">${s}</span>
<span class="tag">${d.exchange || '—'}</span>
<a href="/api/candles?symbol=${encodeURIComponent(s)}&tf=1m&limit=5" target="_blank" style="margin-left:auto;font-size:12px;color:var(--accent);text-decoration:none;">1m →</a>
</div>
`).join('');
} else {
symList.innerHTML = '<div style="color:var(--text-secondary);font-size:13px;">暂无交易对</div>';
}
// Timeframes
document.getElementById('tf-total').textContent = (d.timeframes || []).length;
const baseList = document.getElementById('base-tf-list');
if (d.base_timeframes) {
baseList.innerHTML = d.base_timeframes.map(t => `<span class="tf-chip base">${t}</span>`).join('');
}
const derivedList = document.getElementById('derived-tf-list');
if (d.derived_timeframes) {
derivedList.innerHTML = d.derived_timeframes.map(t => `<span class="tf-chip derived">${t}</span>`).join('');
}
// Populate query selects
const symSelect = document.getElementById('q-symbol');
if (d.symbols && symSelect.options.length === 0) {
d.symbols.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
symSelect.appendChild(opt);
});
}
const tfSelect = document.getElementById('q-tf');
if (d.timeframes && tfSelect.options.length === 0) {
d.timeframes.forEach(t => {
const opt = document.createElement('option');
opt.value = t;
opt.textContent = t;
tfSelect.appendChild(opt);
});
}
}
async function quickQuery() {
const symbol = document.getElementById('q-symbol').value;
const tf = document.getElementById('q-tf').value;
const limit = document.getElementById('q-limit').value;
const url = `/api/candles?symbol=${encodeURIComponent(symbol)}&tf=${tf}&limit=${limit}`;
const pre = document.getElementById('q-result');
pre.style.display = 'block';
pre.textContent = '查询中...';
try {
const res = await fetch(url);
const data = await res.json();
pre.textContent = JSON.stringify(data, null, 2);
} catch {
pre.textContent = '查询失败';
}
}
loadHealth();
setInterval(loadHealth, 10000);
</script>
</body>
</html>