320 lines
17 KiB
HTML
320 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}AlphaX Agent — 舆情雷达{% endblock %}
|
||
{% block extra_head_css %}
|
||
<style>
|
||
|
||
/* SHELL */
|
||
.shell { position: relative; z-index: 1; width: min(100% - 40px, 1180px); margin: 0 auto; padding: 24px 0 44px; }
|
||
|
||
/* 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: 16px; }
|
||
|
||
/* === SECTION: DASHBOARD === */
|
||
.market-context { display: grid; grid-template-columns: minmax(200px, 250px) 1fr; gap: 10px; margin-bottom: 14px; }
|
||
@media(max-width:760px) { .market-context { grid-template-columns: 1fr; } }
|
||
|
||
/* Fear & Greed */
|
||
.fg-card {
|
||
background: var(--canvas); border: 1px solid var(--hairline-soft);
|
||
border-radius: var(--radius-lg); padding: 10px 12px;
|
||
display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 8px 10px;
|
||
}
|
||
.fg-card .fg-label { grid-column: 1 / -1; font-size: 11px; color: var(--stone); font-weight: 800; }
|
||
.fg-card .fg-value { font-size: 28px; font-weight: 900; line-height: 1; transition: color .3s; }
|
||
.fg-card .fg-class { justify-self: start; font-size: 12px; font-weight: 800; padding: 3px 9px; border-radius: var(--radius-full); }
|
||
.fg-gauge { grid-column: 1 / -1; width: 100%; height: 5px; border-radius: 999px; background: linear-gradient(to right, #e53e3e, #f59e0b, #84cc16); position: relative; }
|
||
.fg-gauge::after {
|
||
content: ""; position: absolute; top: -3px;
|
||
width: 11px; height: 11px; border-radius: 50%; background: var(--canvas);
|
||
border: 2px solid var(--ink); transition: left .5s;
|
||
left: calc(var(--pos, 0%) - 5px);
|
||
}
|
||
|
||
/* Trending card */
|
||
.trend-card {
|
||
background: var(--canvas); border: 1px solid var(--hairline-soft);
|
||
border-radius: var(--radius-lg); padding: 10px 12px; min-width: 0;
|
||
}
|
||
.trend-card .section-label { font-size: 11px; color: var(--stone); font-weight: 800; margin-bottom: 8px; }
|
||
.trend-list { display: flex; flex-wrap: wrap; gap: 6px; min-height: 29px; align-items: center; }
|
||
.trend-pill { display: inline-flex; align-items: center; gap: 6px; max-width: 160px; padding: 4px 8px; border: 1px solid var(--hairline-soft); border-radius: 999px; background: var(--surface); color: var(--slate); font-size: 12px; font-weight: 800; }
|
||
.trend-pill img { width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0; }
|
||
.trend-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
|
||
.trend-symbol { font-size: 11px; color: var(--stone); margin-left: 4px; }
|
||
.trend-rank { font-size: 10px; color: var(--muted); }
|
||
|
||
.sentiment-grid { display:grid; grid-template-columns:minmax(0, 1.05fr) minmax(360px, .95fr); gap:14px; align-items:start; }
|
||
.panel { background: var(--canvas); border: 1px solid var(--hairline-soft); border-radius: var(--radius-lg); overflow:hidden; min-width:0; }
|
||
.panel-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:14px 16px; border-bottom:1px solid var(--hairline-soft); }
|
||
.panel-title { font-size: 16px; font-weight: 900; color: var(--ink); }
|
||
.panel-sub { margin-top:3px; color:var(--stone); font-size:12px; line-height:1.45; }
|
||
.feed-count { flex-shrink:0; font-size: 12px; color: var(--muted); background: var(--surface); padding: 3px 10px; border-radius: var(--radius-full); font-weight:800; }
|
||
.source-chips { display:flex; flex-wrap:wrap; gap:6px; padding:10px 16px 0; }
|
||
.source-chip { display:inline-flex; align-items:center; gap:5px; border:1px solid var(--hairline-soft); border-radius:999px; background:var(--surface); padding:4px 8px; color:var(--slate); font-size:11px; font-weight:850; }
|
||
.source-chip b { color:var(--ink); }
|
||
|
||
.news-feed { display: flex; flex-direction: column; gap: 8px; }
|
||
|
||
.news-card { background: var(--canvas); border-top: 1px solid var(--hairline-soft); padding: 14px 16px; transition: .15s; cursor: pointer; display: flex; gap: 12px; 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: 750; 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); }
|
||
.news-card.important .news-source { color:var(--blue); background:rgba(66,98,255,.06); }
|
||
.news-card.risk .news-source { color:var(--red); background:var(--red-light); }
|
||
.analysis-card { padding: 16px; }
|
||
.analysis-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:12px; }
|
||
.analysis-title { font-size: 16px; font-weight: 900; color: var(--ink); }
|
||
.analysis-meta { color: var(--stone); font-size: 11px; font-weight: 800; text-align:right; line-height:1.5; }
|
||
.mood { display:inline-flex; border-radius:999px; padding:4px 9px; font-size:11px; font-weight:900; background:var(--surface); color:var(--slate); border:1px solid var(--hairline-soft); }
|
||
.mood.risk_on { color: var(--green); background: var(--green-light); border-color: rgba(0,180,115,.18); }
|
||
.mood.risk_off { color: var(--red); background: var(--red-light); border-color: rgba(229,62,62,.18); }
|
||
.analysis-summary { color: var(--slate); font-size: 14px; line-height: 1.75; margin-bottom: 14px; }
|
||
.analysis-grid { display:grid; grid-template-columns: 1fr; gap: 10px; }
|
||
.analysis-section { border:1px solid var(--hairline-soft); border-radius: var(--radius-lg); background: var(--surface); padding: 12px; min-width:0; }
|
||
.analysis-section h3 { font-size: 12px; font-weight: 900; color: var(--ink); margin-bottom: 9px; }
|
||
.analysis-item { border-top:1px solid var(--hairline-soft); padding:8px 0; color:var(--slate); font-size:12px; line-height:1.55; }
|
||
.analysis-item:first-of-type { border-top:0; padding-top:0; }
|
||
.analysis-item b { color:var(--ink); font-size:13px; }
|
||
.analysis-item .sub { display:block; margin-top:3px; color:var(--stone); }
|
||
.symbol-tags { display:flex; flex-wrap:wrap; gap:4px; margin-top:5px; }
|
||
.symbol-tag { display:inline-flex; padding:3px 7px; border-radius:999px; background:var(--canvas); border:1px solid var(--hairline-soft); color:var(--blue); font-size:10px; font-weight:900; }
|
||
@media(max-width:1040px){ .sentiment-grid{grid-template-columns:1fr;} .analysis-grid{grid-template-columns:1fr 1fr;} }
|
||
@media(max-width:760px){ .analysis-grid{grid-template-columns:1fr;} .analysis-head{display:block;} .analysis-meta{text-align:left;margin-top:8px;} }
|
||
|
||
/* 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) } }
|
||
|
||
@media(max-width:640px) {
|
||
.shell { width: min(100% - 24px, 1180px); }
|
||
.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">聚合快讯展示系统正在监控的原始新闻,AI 分析展示基于最新新闻源的结构化研判。</p>
|
||
|
||
<!-- Dashboard -->
|
||
<div class="market-context">
|
||
<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">
|
||
<span class="trend-pill">加载中...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sentiment-grid">
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="panel-title">新闻源快讯</div>
|
||
<div class="panel-sub">来自 Binance、吴说、PANews 等入库新闻源的原始信息。</div>
|
||
</div>
|
||
<span class="feed-count" id="feedCount">--</span>
|
||
</div>
|
||
<div class="source-chips" id="sourceChips"></div>
|
||
<div class="news-feed" id="newsFeed">
|
||
<div class="empty-state"><p>加载中...</p></div>
|
||
</div>
|
||
</section>
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="panel-title">新闻舆情 AI 分析</div>
|
||
<div class="panel-sub">基于最近新闻源批次生成,只读解释,不直接改变推荐状态。</div>
|
||
</div>
|
||
</div>
|
||
<div id="aiAnalysis" class="analysis-card">
|
||
<div class="empty-state"><p>等待 AI 舆情分析结果...</p></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
{% block extra_script %}
|
||
<script>
|
||
// ====== 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 esc(v){ return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];}); }
|
||
|
||
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)';
|
||
}
|
||
|
||
function fmtTime(t) {
|
||
if (!t) return '--';
|
||
var d = new Date(t);
|
||
if (isNaN(d.getTime())) return t;
|
||
return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
|
||
}
|
||
|
||
function renderAnalysis(resp) {
|
||
var box = document.getElementById('aiAnalysis');
|
||
var a = resp && resp.analysis ? resp.analysis : null;
|
||
if (!a) {
|
||
var msg = resp && resp.error ? ('AI 舆情分析未完成:' + resp.error) : '暂无 AI 舆情分析。调度器会定时生成,或运行 llm-insights --scope sentiment。';
|
||
box.innerHTML = '<div class="empty-state"><p>'+esc(msg)+'</p></div>';
|
||
return;
|
||
}
|
||
var mood = a.market_mood || 'neutral';
|
||
function symbolsHtml(items) {
|
||
return (items || []).slice(0, 8).map(function(s){ return '<span class="symbol-tag">'+esc(s)+'</span>'; }).join('');
|
||
}
|
||
var themes = (a.hot_themes || []).slice(0, 6).map(function(x){
|
||
return '<div class="analysis-item"><b>'+esc(x.theme || '主题')+'</b><span class="sub">'+esc(x.impact || x.reason || '--')+'</span><div class="symbol-tags">'+symbolsHtml(x.symbols || [])+'</div></div>';
|
||
}).join('') || '<div class="analysis-item">暂无明确主题</div>';
|
||
var impacts = (a.coin_impacts || []).slice(0, 8).map(function(x){
|
||
var check = x.need_technical_check ? ' · 需要技术检查' : '';
|
||
return '<div class="analysis-item"><b>'+esc(x.symbol || '--')+'</b><span class="sub">'+esc((x.direction || 'neutral') + check)+'</span><span class="sub">'+esc(x.reason || '--')+'</span></div>';
|
||
}).join('') || '<div class="analysis-item">暂无币种影响</div>';
|
||
var risks = (a.risk_events || []).slice(0, 6).map(function(x){
|
||
return '<div class="analysis-item"><b>'+esc(x.risk_type || x.title || '风险事件')+'</b><span class="sub">'+esc(x.title || x.reason || '--')+'</span><div class="symbol-tags">'+symbolsHtml(x.symbols || [])+'</div></div>';
|
||
}).join('') || '<div class="analysis-item">暂无集中风险</div>';
|
||
var watch = (a.watchlist || []).slice(0, 8).map(function(x){
|
||
return '<div class="analysis-item"><b>'+esc(x.symbol || '--')+'</b><span class="sub">'+esc(x.why || '--')+'</span><span class="sub">触发条件:'+esc(x.trigger || '--')+'</span></div>';
|
||
}).join('') || '<div class="analysis-item">暂无重点观察</div>';
|
||
box.innerHTML =
|
||
'<div class="analysis-head"><div><div class="analysis-title">AI 舆情研判 <span class="mood '+esc(mood)+'">'+esc(mood)+'</span></div></div><div class="analysis-meta">模型 '+esc(resp.model || '--')+'<br>更新 '+fmtTime(resp.updated_at)+' · 来源 '+esc(resp.event_count || 0)+' 条</div></div>'+
|
||
'<div class="analysis-summary">'+esc(a.summary || '暂无摘要')+'</div>'+
|
||
'<div class="analysis-grid">'+
|
||
'<div class="analysis-section"><h3>主线主题</h3>'+themes+'</div>'+
|
||
'<div class="analysis-section"><h3>币种影响</h3>'+impacts+'</div>'+
|
||
'<div class="analysis-section"><h3>风险事件</h3>'+risks+'</div>'+
|
||
'<div class="analysis-section"><h3>重点观察</h3>'+watch+'</div>'+
|
||
'</div>';
|
||
}
|
||
|
||
function renderSourceChips(items) {
|
||
var box = document.getElementById('sourceChips');
|
||
var counts = {};
|
||
(items || []).forEach(function(n){
|
||
var source = n.source_label || n.source || '新闻源';
|
||
counts[source] = (counts[source] || 0) + 1;
|
||
});
|
||
var chips = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; }).slice(0, 8).map(function(k){
|
||
return '<span class="source-chip">'+esc(k)+' <b>'+counts[k]+'</b></span>';
|
||
}).join('');
|
||
box.innerHTML = chips || '<span class="source-chip">暂无源数据</span>';
|
||
}
|
||
|
||
function renderNewsFeed(items) {
|
||
var news = (items || []).slice(0, 80);
|
||
document.getElementById('feedCount').textContent = news.length + ' 条';
|
||
renderSourceChips(news);
|
||
if (!news.length) {
|
||
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>暂无新闻源快讯</p></div>';
|
||
return;
|
||
}
|
||
document.getElementById('newsFeed').innerHTML = news.map(function(n) {
|
||
var importance = String(n.importance || 'B').toUpperCase();
|
||
var cls = importance === 'RISK' ? 'risk' : (importance === 'S' || importance === 'A') ? 'important' : '';
|
||
var meta = [
|
||
n.related_symbol || n.related_base || '',
|
||
importance ? ('重要性 ' + importance) : '',
|
||
n.decision ? ('技术检查 ' + n.decision) : '',
|
||
ageStr(n.age_hours)
|
||
].filter(Boolean).join(' · ');
|
||
return '<a class="news-card '+cls+'" href="' + esc(n.url || '#') + '" target="_blank" rel="noopener">' +
|
||
'<span class="news-source">' + esc(n.source_label || n.source || '新闻源') + '</span>' +
|
||
'<div class="news-body">' +
|
||
'<div class="news-title">' + esc(n.title) + '</div>' +
|
||
'<div class="news-meta"><span>' + esc(meta || '刚刚更新') + '</span></div>' +
|
||
'</div>' +
|
||
'</a>';
|
||
}).join('');
|
||
}
|
||
|
||
async function loadFeed() {
|
||
try {
|
||
var resp = await fetch(API + '/api/newsfeed');
|
||
var data = await resp.json();
|
||
var analysisResp = {};
|
||
try {
|
||
var ar = await fetch(API + '/api/sentiment/analysis');
|
||
if (ar.ok) analysisResp = await ar.json();
|
||
} catch(e) {}
|
||
renderAnalysis(analysisResp);
|
||
|
||
// 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 + '%');
|
||
}
|
||
|
||
// Trending
|
||
var trends = data.trending || [];
|
||
document.getElementById('trendList').classList.remove('loading-pulse');
|
||
if (trends.length) {
|
||
document.getElementById('trendList').innerHTML = trends.slice(0, 8).map(function(t) {
|
||
var symbol = String(t.symbol || '').toUpperCase();
|
||
var icon = t.thumb ? '<img src="' + esc(t.thumb) + '" alt="' + esc(symbol) + '" onerror="this.remove()">' : '';
|
||
return '<span class="trend-pill">' +
|
||
icon +
|
||
'<span class="trend-name">' + esc(t.name || symbol || '--') + '</span>' +
|
||
'<span class="trend-symbol">' + esc(symbol) + '</span>' +
|
||
'<span class="trend-rank">#' + esc(t.market_cap_rank || '--') + '</span>' +
|
||
'</span>';
|
||
}).join('');
|
||
} else {
|
||
document.getElementById('trendList').innerHTML = '<span style="color:var(--muted);font-size:12px">暂无数据</span>';
|
||
}
|
||
|
||
renderNewsFeed(data.news || []);
|
||
} catch(e) {
|
||
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>加载失败,请稍后重试</p></div>';
|
||
}
|
||
}
|
||
|
||
loadFeed();
|
||
// Auto-refresh every 5 minutes
|
||
setInterval(loadFeed, 300000);
|
||
</script>
|
||
{% endblock %}
|