alphax/static/sentiment.html
2026-05-14 11:21:21 +08:00

288 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.

{% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 舆情雷达{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link active" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %}
<style>
/* SHELL */
.shell { position: relative; z-index: 1; width: min(100% - 40px, 960px); margin: 0 auto; padding: 24px 0; }
/* Page title */
.page-title { font-size: 24px; font-weight: 800; color: var(--ink); margin-bottom: 4px; }
.page-sub { font-size: 13px; color: var(--stone); margin-bottom: 24px; }
/* === SECTION: DASHBOARD === */
.dashboard-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 14px; margin-bottom: 28px;
}
@media(max-width:640px) { .dashboard-grid { grid-template-columns: 1fr; } }
/* Fear & Greed */
.fg-card {
background: var(--canvas); border: 1px solid var(--hairline-soft);
border-radius: var(--radius-xl); padding: 24px;
display: flex; flex-direction: column; align-items: center; gap: 12px;
}
.fg-card .fg-label { font-size: 12px; color: var(--stone); font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
.fg-card .fg-value { font-size: 56px; font-weight: 900; line-height: 1; transition: color .3s; }
.fg-card .fg-class { font-size: 15px; font-weight: 700; padding: 4px 16px; border-radius: var(--radius-full); }
.fg-gauge { width: 100%; height: 8px; border-radius: 4px; background: linear-gradient(to right, #e53e3e, #f59e0b, #84cc16); position: relative; }
.fg-gauge::after {
content: ""; position: absolute; top: -4px;
width: 16px; height: 16px; border-radius: 50%; background: var(--canvas);
border: 3px solid var(--ink); transition: left .5s;
left: 0%;
}
/* Trending card */
.trend-card {
background: var(--canvas); border: 1px solid var(--hairline-soft);
border-radius: var(--radius-xl); padding: 18px 20px;
}
.trend-card .section-label { font-size: 12px; color: var(--stone); font-weight: 600; margin-bottom: 14px; }
.trend-list { display: flex; flex-direction: column; gap: 10px; }
.trend-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--hairline-soft); }
.trend-row:last-child { border-bottom: 0; }
.trend-icon { width: 28px; height: 28px; border-radius: 50%; background: var(--surface); display: grid; place-items: center; font-weight: 800; font-size: 10px; color: var(--steel); flex-shrink: 0; }
.trend-icon img { width: 28px; height: 28px; border-radius: 50%; }
.trend-name { font-weight: 700; font-size: 14px; color: var(--ink); }
.trend-symbol { font-size: 11px; color: var(--stone); margin-left: 4px; }
.trend-rank { margin-left: auto; font-size: 11px; color: var(--muted); }
/* === SECTION: NEWS FEED === */
.feed-header { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.feed-header h2 { font-size: 18px; font-weight: 700; }
.feed-header .feed-count { font-size: 12px; color: var(--muted); background: var(--surface); padding: 2px 10px; border-radius: var(--radius-full); }
.news-feed { display: flex; flex-direction: column; gap: 8px; }
.news-card {
background: var(--canvas); border: 1px solid var(--hairline-soft);
border-radius: var(--radius-xl); padding: 16px 18px;
transition: .15s; cursor: pointer; display: flex; gap: 14px; align-items: flex-start;
}
.news-card:hover { border-color: var(--hairline); box-shadow: 0 2px 8px rgba(5,0,56,.04); }
.news-card:active { transform: scale(.995); }
.news-source {
flex-shrink: 0; min-width: 56px; font-size: 10px; font-weight: 700;
color: var(--stone); padding: 4px 8px; background: var(--surface);
border-radius: var(--radius-md); text-align: center; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.news-source.cn { color: var(--blue); background: rgba(66,98,255,.06); }
.news-body { flex: 1; min-width: 0; }
.news-title { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; }
.news-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--muted); }
.news-meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--hairline); }
/* Empty */
.empty-state { text-align:center; padding:48px 20px; color:var(--stone); }
.empty-state p { font-size:14px; }
/* Loading */
.loading-pulse { animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%,100%{ opacity:1 } 50%{ opacity:.3 } }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to{ transform:rotate(360deg) } }
.shell { width: min(100% - 24px, 960px); }
.fg-card .fg-value { font-size: 42px; }
.news-card { padding: 14px 14px; gap: 10px; }
.news-source { min-width: 48px; font-size: 9px; padding: 3px 6px; }
}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<h1 class="page-title">实时舆情</h1>
<p class="page-sub">市场情绪 + 热门币种 + 最新加密新闻</p>
<!-- Dashboard -->
<div class="dashboard-grid">
<div class="fg-card" id="fgCard">
<div class="fg-label">恐惧 & 贪婪指数</div>
<div class="fg-value loading-pulse" id="fgValue">--</div>
<div class="fg-class" id="fgClass">加载中</div>
<div class="fg-gauge" id="fgGauge"></div>
</div>
<div class="trend-card">
<div class="section-label">CoinGecko 热门币种</div>
<div class="trend-list loading-pulse" id="trendList">
<div class="trend-row"><span class="trend-icon">?</span><span class="trend-name">加载中...</span></div>
</div>
</div>
</div>
<!-- News Feed -->
<div class="feed-header">
<h2>新闻信息流</h2>
<span class="feed-count" id="feedCount">--</span>
</div>
<div class="news-feed" id="newsFeed">
<div class="empty-state"><p>加载中...</p></div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var API = '';
// ====== USER ======
var currentUser = null;
var $ = function(id){ return document.getElementById(id); };
async function loadUser() {
try {
var resp = await fetch(API + '/api/auth/me');
if (!resp.ok) return;
var data = await resp.json();
currentUser = data.user;
var email = currentUser.email || '--';
$('userInitial').textContent = email.charAt(0).toUpperCase();
$('userEmailShort').textContent = email.length > 14 ? email.slice(0,12) + '…' : email;
$('ddEmail').textContent = email;
} catch(e) {}
}
function toggleUserMenu() {
$('userDropdown').classList.toggle('show');
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.sidebar-user') && !e.target.closest('.user-dropdown')) $('userDropdown').classList.remove('show');
});
function showChangePwd() {
$('userDropdown').classList.remove('show');
$('pwdModal').classList.add('show');
$('oldPwd').value = ''; $('newPwd').value = ''; $('cfmPwd').value = ''; $('pwdMsg').textContent = '';
}
function closePwdModal() { $('pwdModal').classList.remove('show'); }
async function changePwd() {
var old = $('oldPwd').value, nw = $('newPwd').value, cf = $('cfmPwd').value;
if (!old || !nw) { $('pwdMsg').className = 'modal-msg err'; $('pwdMsg').textContent = '请填写所有字段'; return; }
if (nw.length < 8) { $('pwdMsg').className = 'modal-msg err'; $('pwdMsg').textContent = '新密码至少 8 位'; return; }
if (nw !== cf) { $('pwdMsg').className = 'modal-msg err'; $('pwdMsg').textContent = '两次密码不一致'; return; }
try {
var r = await fetch(API + '/api/auth/change-password', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({old_password: old, new_password: nw})
});
var d = await r.json();
if (!r.ok) throw new Error(d.detail || '修改失败');
$('pwdMsg').className = 'modal-msg ok'; $('pwdMsg').textContent = d.message || '修改成功';
setTimeout(closePwdModal, 1200);
} catch(e) { $('pwdMsg').className = 'modal-msg err'; $('pwdMsg').textContent = e.message; }
}
async function doLogout() {
await fetch(API + '/api/auth/logout', { method: 'POST' });
window.location.href = '/auth';
}
// ====== FEED ======
function ageStr(h) {
if (h == null) return '';
if (h < 1) return Math.round(h * 60) + '分钟前';
if (h < 24) return Math.floor(h) + '小时前';
return Math.floor(h / 24) + '天前';
}
function fgColor(v) {
if (v <= 25) return 'var(--red)';
if (v <= 45) return 'var(--orange)';
if (v <= 55) return 'var(--yellow)';
if (v <= 75) return 'var(--green)';
return 'var(--green)';
}
async function loadFeed() {
try {
var resp = await fetch(API + '/api/newsfeed');
var data = await resp.json();
// Fear & Greed
var fg = data.fear_greed;
if (fg) {
var v = fg.value, cls = fg.classification, clr = fgColor(v);
document.getElementById('fgValue').textContent = v;
document.getElementById('fgValue').style.color = clr;
document.getElementById('fgValue').classList.remove('loading-pulse');
document.getElementById('fgClass').textContent = cls;
document.getElementById('fgClass').style.color = clr;
document.getElementById('fgClass').style.background = clr + '15';
document.getElementById('fgGauge').style.setProperty('--pos', v + '%');
// Update gauge pointer position
var g = document.getElementById('fgGauge');
g.style.setProperty('--pos', v + '%');
// Re-apply ::after style with JS since CSS custom properties in pseudo-elements can be tricky
var sheet = document.createElement('style');
sheet.textContent = '#fgGauge::after { left: calc(' + v + '% - 8px); }';
document.head.appendChild(sheet);
}
// Trending
var trends = data.trending || [];
document.getElementById('trendList').classList.remove('loading-pulse');
if (trends.length) {
document.getElementById('trendList').innerHTML = trends.map(function(t, i) {
var icon = t.thumb
? '<img src="' + t.thumb + '" alt="' + t.symbol + '" onerror="this.style.display=\'none\';this.nextSibling.style.display=\'block\'"><span style="display:none">' + t.symbol.charAt(0) + '</span>'
: t.symbol.slice(0, 2).toUpperCase();
return '<div class="trend-row">' +
'<div class="trend-icon">' + icon + '</div>' +
'<div><span class="trend-name">' + t.name + '</span><span class="trend-symbol">' + t.symbol + '</span></div>' +
'<span class="trend-rank">#' + (t.market_cap_rank || '--') + '</span>' +
'</div>';
}).join('');
} else {
document.getElementById('trendList').innerHTML = '<div style="color:var(--muted);font-size:13px;text-align:center;padding:20px">暂无数据</div>';
}
// News feed
var news = data.news || [];
document.getElementById('feedCount').textContent = news.length + ' 条';
if (news.length) {
document.getElementById('newsFeed').innerHTML = news.map(function(n) {
var isCn = n.lang === 'cn';
return '<a class="news-card" href="' + n.url + '" target="_blank" rel="noopener">' +
'<span class="news-source' + (isCn ? ' cn' : '') + '">' + n.source + '</span>' +
'<div class="news-body">' +
'<div class="news-title">' + n.title + '</div>' +
'<div class="news-meta">' +
'<span>' + (isCn ? '中文' : 'EN') + '</span>' +
'<span class="dot"></span>' +
'<span>' + ageStr(n.age_hours) + '</span>' +
'</div>' +
'</div>' +
'</a>';
}).join('');
} else {
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>暂无新闻数据</p></div>';
}
} catch(e) {
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>加载失败,请稍后重试</p></div>';
}
}
loadUser();
loadFeed();
// Auto-refresh every 5 minutes
setInterval(loadFeed, 300000);
</script>
{% endblock %}