286 lines
12 KiB
HTML
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 %} |