alphax/static/sentiment.html
2026-05-13 22:32:50 +08:00

286 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}AlphaX — 舆情雷达{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/watchlist"><svg class="link-icon"><use href="#svg-star"/></svg>关注</a>
<a class="sidebar-link" href="/strategy"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link" href="/iteration"><svg class="link-icon"><use href="#svg-iterate"/></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>
<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 %}