alphax/legacy/web/index.html
2026-05-13 22:49:47 +08:00

330 lines
13 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>山寨币爆发监控</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { background:#0a0a0f; color:#e0e0e0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; min-height:100vh; }
/* 顶部导航 */
.nav { background:#141420; padding:12px 16px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #2a2a3a; position:sticky; top:0; z-index:100; }
.nav h1 { font-size:18px; color:#fff; }
.nav .refresh-btn { background:none; border:1px solid #4a4a5a; color:#aaa; padding:6px 12px; border-radius:6px; font-size:13px; cursor:pointer; }
.nav .refresh-btn:active { background:#2a2a3a; }
/* 统计卡片 */
.stats-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; padding:12px 16px; }
.stat-card { background:#141420; border-radius:10px; padding:12px; text-align:center; }
.stat-card .label { font-size:11px; color:#888; margin-bottom:4px; }
.stat-card .value { font-size:20px; font-weight:bold; }
.stat-card .value.green { color:#00d4aa; }
.stat-card .value.red { color:#ff4444; }
.stat-card .value.yellow { color:#ffaa00; }
.stat-card .value.blue { color:#4488ff; }
/* Tab切换 */
.tabs { display:flex; padding:0 16px; background:#141420; border-bottom:1px solid #2a2a3a; }
.tab { padding:10px 16px; font-size:14px; color:#888; cursor:pointer; border-bottom:2px solid transparent; flex:1; text-align:center; }
.tab.active { color:#fff; border-bottom:2px solid #4488ff; }
/* 内容区 */
.content { padding:12px 16px; }
.section-title { font-size:14px; color:#888; margin:12px 0 8px; padding-left:4px; }
/* 推荐卡片 */
.rec-card { background:#1a1a28; border-radius:12px; padding:14px; margin-bottom:10px; border-left:4px solid; }
.rec-card.burst { border-left-color:#ff4444; }
.rec-card.accel { border-left-color:#ffaa00; }
.rec-card.gather { border-left-color:#4488ff; }
.rec-card.closed-profit { border-left-color:#00d4aa; }
.rec-card.closed-loss { border-left-color:#ff4444; }
.rec-card.closed-expired { border-left-color:#666; }
.rec-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
.rec-symbol { font-size:16px; font-weight:bold; color:#fff; }
.rec-state { font-size:12px; padding:3px 8px; border-radius:4px; font-weight:bold; }
.rec-state.burst { background:#ff4444; color:#fff; }
.rec-state.accel { background:#ffaa00; color:#000; }
.rec-state.gather { background:#4488ff; color:#fff; }
.rec-state.closed-profit { background:#00d4aa; color:#000; }
.rec-state.closed-loss { background:#ff4444; color:#fff; }
.rec-state.closed-expired { background:#666; color:#fff; }
.rec-state.tp1_hit { background:#00d4aa; color:#000; }
.rec-price { font-size:14px; margin-bottom:6px; }
.rec-price .current { color:#fff; font-weight:bold; }
.rec-price .change { font-size:13px; }
.rec-price .change.pos { color:#00d4aa; }
.rec-price .change.neg { color:#ff4444; }
.rec-details { font-size:12px; color:#888; line-height:1.6; }
.rec-details span { color:#aaa; }
.rec-signals { margin-top:6px; }
.rec-signal { display:inline-block; background:#2a2a3a; padding:2px 8px; border-radius:4px; font-size:11px; color:#ccc; margin:2px 2px; }
/* 入场方案卡片 */
.entry-card { background:#1e1e2e; border-radius:8px; padding:10px; margin-top:8px; }
.entry-title { font-size:12px; color:#4488ff; margin-bottom:4px; }
.entry-row { display:flex; justify-content:space-between; font-size:12px; padding:2px 0; }
.entry-row .key { color:#888; }
.entry-row .val { color:#fff; }
.entry-row .val.green { color:#00d4aa; }
.entry-row .val.red { color:#ff4444; }
/* PnL条 */
.pnl-bar { height:6px; border-radius:3px; background:#2a2a3a; margin-top:8px; position:relative; overflow:hidden; }
.pnl-bar .fill { height:100%; border-radius:3px; }
.pnl-bar .fill.green { background:#00d4aa; }
.pnl-bar .fill.red { background:#ff4444; }
/* 空状态 */
.empty { text-align:center; padding:40px; color:#666; font-size:14px; }
/* 最后更新时间 */
.last-update { text-align:center; font-size:11px; color:#666; padding:8px 0 20px; }
/* 动画 */
@keyframes fadeIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
.rec-card { animation: fadeIn 0.3s ease; }
</style>
</head>
<body>
<div class="nav">
<h1>🚀 山寨币监控</h1>
<button class="refresh-btn" onclick="loadData()">刷新</button>
</div>
<div class="stats-grid" id="stats-grid"></div>
<div class="tabs">
<div class="tab active" data-tab="recommendations" onclick="switchTab('recommendations')">🔥 推荐</div>
<div class="tab" data-tab="candidates" onclick="switchTab('candidates')">📋 候选</div>
<div class="tab" data-tab="history" onclick="switchTab('history')">📜 历史</div>
</div>
<div class="content" id="tab-content"></div>
<div class="last-update" id="last-update"></div>
<script>
let currentTab = 'recommendations';
let dashboardData = null;
// 自动刷新
setInterval(loadData, 60000); // 1分钟刷新
async function loadData() {
try {
const resp = await fetch('/api/dashboard');
dashboardData = await resp.json();
renderStats();
renderTab();
const t = dashboardData.latest_scan_time || '--';
document.getElementById('last-update').textContent = '最后更新: ' + formatTime(t);
} catch(e) {
console.error('加载失败', e);
}
}
function renderStats() {
const s = dashboardData.stats || {};
const total = s.total_recs || 0;
const wins = s.wins || 0;
const losses = s.losses || 0;
const active = s.active || 0;
const avgPnl = s.avg_pnl || 0;
const winRate = total > 0 ? Math.round(wins/(wins+losses)*100) : 0;
document.getElementById('stats-grid').innerHTML = `
<div class="stat-card"><div class="label">活跃推荐</div><div class="value blue">${active}</div></div>
<div class="stat-card"><div class="label">胜率</div><div class="value ${winRate>=50?'green':'red'}">${wins+losses>0?winRate+'%':'--'}</div></div>
<div class="stat-card"><div class="label">平均盈亏</div><div class="value ${avgPnl>=0?'green':'red'}">${avgPnl>0?'+'+avgPnl:avgPnl}%</div></div>
<div class="stat-card"><div class="label">总推荐数</div><div class="value yellow">${total}</div></div>
`;
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
renderTab();
}
function renderTab() {
const container = document.getElementById('tab-content');
if (!dashboardData) { container.innerHTML = '<div class="empty">加载中...</div>'; return; }
if (currentTab === 'recommendations') {
renderRecommendations(container);
} else if (currentTab === 'candidates') {
renderCandidates(container);
} else if (currentTab === 'history') {
renderHistory(container);
}
}
function renderRecommendations(container) {
const active = dashboardData.active_recommendations || [];
const closed = dashboardData.closed_recommendations || [];
if (active.length === 0 && closed.length === 0) {
container.innerHTML = '<div class="empty">暂无推荐 — 等待爆发信号确认</div>';
return;
}
let html = '';
if (active.length > 0) {
html += '<div class="section-title">🔥 活跃推荐</div>';
active.forEach(r => { html += renderRecCard(r); });
}
if (closed.length > 0) {
html += '<div class="section-title">📜 已关闭</div>';
closed.forEach(r => { html += renderRecCard(r); });
}
container.innerHTML = html;
}
function renderCandidates(container) {
const cands = dashboardData.latest_candidates || [];
if (cands.length === 0) {
container.innerHTML = '<div class="empty">暂无候选 — 等待下次筛选</div>';
return;
}
let html = '<div class="section-title">最近筛选结果 (' + formatTime(dashboardData.latest_scan_time) + ')</div>';
cands.forEach(c => {
const stateClass = c.state === '加速' ? 'accel' : 'gather';
const stateTag = c.state === '加速' ? '🔥🔥加速' : '🔥蓄力';
const tagClass = c.state === '加速' ? 'accel' : 'gather';
const signals = (c.signals||'').split(',').filter(Boolean);
const isMeme = c.is_meme ? ' 🎮MEME' : '';
const change24h = c.change_24h || 0;
const changeClass = change24h >= 0 ? 'pos' : 'neg';
const changeSign = change24h >= 0 ? '+' : '';
html += `<div class="rec-card ${stateClass}">
<div class="rec-header">
<div class="rec-symbol">${c.symbol.replace('/USDT','')}${isMeme}</div>
<div class="rec-state ${tagClass}">${stateTag} 评分${c.score}</div>
</div>
<div class="rec-price">
<span class="current">$${(c.price_at_scan||0).toFixed(4)}</span>
<span class="change ${changeClass}">${changeSign}${change24h.toFixed(1)}%/24h</span>
</div>
<div class="rec-details">
${c.sector?'<span>板块:'+c.sector+'</span>':''}
${c.leader_status?'<span>'+c.leader_status+'</span>':''}
</div>
<div class="rec-signals">${signals.map(s=>'<span class="rec-signal">'+s+'</span>').join('')}</div>
</div>`;
});
container.innerHTML = html;
}
function renderHistory(container) {
const scans = dashboardData.scan_stats || [];
if (scans.length === 0) {
container.innerHTML = '<div class="empty">暂无筛选历史</div>';
return;
}
let html = '<div class="section-title">筛选历史 (最近24h)</div>';
scans.forEach(s => {
html += `<div class="rec-card gather" style="border-left-color:#4488ff">
<div class="rec-header">
<div class="rec-symbol" style="font-size:14px">${formatTime(s.scan_time)}</div>
<div class="rec-state gather">${s.count}个候选</div>
</div>
<div class="rec-details">
<span>加速:${s.accelerating||0}</span> | <span>蓄力:${s.gathering||0}</span>
</div>
</div>`;
});
container.innerHTML = html;
}
function renderRecCard(r) {
const status = r.status || 'active';
let cardClass = 'gather';
let stateTag = '🔥蓄力';
let tagClass = 'gather';
if (status === 'active') { cardClass = 'accel'; stateTag = '🔥🔥加速'; tagClass = 'accel'; }
else if (status === 'tp1_hit') { cardClass = 'burst'; stateTag = '✅TP1触发'; tagClass = 'tp1_hit'; }
else if (status === 'closed_profit') { cardClass = 'closed-profit'; stateTag = '✅盈利'; tagClass = 'closed-profit'; }
else if (status === 'closed_loss') { cardClass = 'closed-loss'; stateTag = '❌止损'; tagClass = 'closed-loss'; }
else if (status === 'closed_expired') { cardClass = 'closed-expired'; stateTag = '⏰过期'; tagClass = 'closed-expired'; }
const pnl = r.pnl_pct || 0;
const pnlClass = pnl >= 0 ? 'green' : 'red';
const pnlSign = pnl >= 0 ? '+' : '';
const currentPrice = r.current_price || r.recommended_price || 0;
const maxProfit = r.max_profit_pct || 0;
const maxLoss = r.max_loss_pct || 0;
const signals = (r.signals||'').split(',').filter(Boolean);
let entryHtml = '';
if (r.entry_price > 0 && (status === 'active' || status === 'tp1_hit')) {
const rr1 = r.rr1 || 0;
const rr2 = r.rr2 || 0;
entryHtml = `<div class="entry-card">
<div class="entry-title">📊 入场方案</div>
<div class="entry-row"><span class="key">入场</span><span class="val">$${r.entry_price.toFixed(4)}</span></div>
<div class="entry-row"><span class="key">止损</span><span class="val red">$${r.stop_loss.toFixed(4)} (${r.stop_pct}%)</span></div>
<div class="entry-row"><span class="key">TP1</span><span class="val green">$${r.tp1.toFixed(4)} (RR=${rr1})</span></div>
<div class="entry-row"><span class="key">TP2</span><span class="val green">$${r.tp2.toFixed(4)} (RR=${rr2})</span></div>
</div>`;
}
// PnL条
let pnlBarHtml = '';
if (r.recommended_price > 0) {
const pnlAbs = Math.abs(pnl);
const barWidth = Math.min(pnlAbs * 5, 100); // 1% = 5px宽max 100%
pnlBarHtml = `<div class="pnl-bar"><div class="fill ${pnlClass}" style="width:${barWidth}%"></div></div>`;
}
const closeInfo = r.close_reason ? `<div class="rec-details" style="margin-top:4px"><span>${r.close_reason}</span></div>` : '';
return `<div class="rec-card ${cardClass}">
<div class="rec-header">
<div class="rec-symbol">${r.symbol.replace('/USDT','')}</div>
<div class="rec-state ${tagClass}">${stateTag}</div>
</div>
<div class="rec-price">
<span class="current">$${currentPrice.toFixed(4)}</span>
<span class="change ${pnlClass}">${pnlSign}${pnl.toFixed(2)}%</span>
</div>
<div class="rec-details">
推荐价: $${(r.recommended_price||0).toFixed(4)} | 最高: +${maxProfit.toFixed(2)}% | 最低: ${maxLoss.toFixed(2)}%
${r.sector?' | 板块:'+r.sector:''}
</div>
${closeInfo}
<div class="rec-signals">${signals.map(s=>'<span class="rec-signal">'+s+'</span>').join('')}</div>
${entryHtml}
${pnlBarHtml}
</div>`;
}
function formatTime(t) {
if (!t || t === '--') return '--';
try {
const d = new Date(t);
const month = (d.getMonth()+1).toString().padStart(2,'0');
const day = d.getDate().toString().padStart(2,'0');
const hour = d.getHours().toString().padStart(2,'0');
const min = d.getMinutes().toString().padStart(2,'0');
return `${month}-${day} ${hour}:${min}`;
} catch(e) { return t; }
}
// 首次加载
loadData();
</script>
</body>
</html>