Dashboard: 信号tab + API(signals + xiaoguo-scan),前端展示

This commit is contained in:
知微
2026-06-21 02:44:05 +08:00
parent aa1f621b03
commit 70514bf542
2 changed files with 117 additions and 1 deletions
+53 -1
View File
@@ -198,7 +198,59 @@ def api_market():
return jsonify(_load_json(DATA_DIR / "market.json", {})) return jsonify(_load_json(DATA_DIR / "market.json", {}))
# ── 数据写入API(供 cron/update_data.py 调用) ────────── # ── 信号API(新增) ─────────────────────────────────────
@app.route("/api/signals")
def api_signals():
"""最近信号 + 小果分析"""
try:
from mofin_db import get_conn
conn = get_conn()
signals = conn.execute("""
SELECT sn.id, sn.sector, sn.overall_sentiment,
sn.summary, sn.source, sn.created_at,
ss.signal_type, ss.severity
FROM signal_news sn
LEFT JOIN sector_signals ss ON sn.signal_id = ss.id
ORDER BY sn.id DESC LIMIT 20
""").fetchall()
conn.close()
return jsonify([dict(r) for r in signals])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/xiaoguo-scan")
def api_xiaoguo_scan():
"""小果扫描统计"""
try:
from mofin_db import get_conn
conn = get_conn()
total = conn.execute("SELECT COUNT(*) FROM xiaoguo_scan_tracker").fetchone()[0]
found = conn.execute("SELECT COUNT(*) FROM xiaoguo_scan_tracker WHERE found_count>0").fetchone()[0]
recent = conn.execute("""
SELECT code, name, last_scanned_at, found_count
FROM xiaoguo_scan_tracker
ORDER BY last_scanned_at DESC LIMIT 20
""").fetchall()
source_count = conn.execute("""
SELECT source, COUNT(*) as cnt FROM signal_news
WHERE datetime(created_at) > datetime('now', '-1 day')
GROUP BY source
""").fetchall()
conn.close()
return jsonify({
"total_scanned": total,
"found_signals": found,
"recent": [dict(r) for r in recent],
"source_today": {r["source"]: r["cnt"] for r in source_count}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ── 数据写入API ──
@app.route("/api/update/portfolio", methods=["POST"]) @app.route("/api/update/portfolio", methods=["POST"])
def update_portfolio(): def update_portfolio():
+64
View File
@@ -50,6 +50,7 @@ body { background: #0f0f13; color: #e2e8f0; }
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="evaluation">📊 评估</button> <button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="evaluation">📊 评估</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="prompts">📝 提示词</button> <button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="prompts">📝 提示词</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button> <button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
<a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a> <a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a>
</div> </div>
@@ -62,6 +63,7 @@ body { background: #0f0f13; color: #e2e8f0; }
<div id="tab-evaluation" class="tab-content hidden"></div> <div id="tab-evaluation" class="tab-content hidden"></div>
<div id="tab-prompts" class="tab-content hidden"></div> <div id="tab-prompts" class="tab-content hidden"></div>
<div id="tab-market" class="tab-content hidden"></div> <div id="tab-market" class="tab-content hidden"></div>
<div id="tab-signals" class="tab-content hidden"></div>
</div> </div>
<!-- Stock Detail Modal --> <!-- Stock Detail Modal -->
@@ -144,6 +146,7 @@ function renderTab(name) {
else if (name === 'evaluation') renderEvaluation(); else if (name === 'evaluation') renderEvaluation();
else if (name === 'prompts') renderPrompts(); else if (name === 'prompts') renderPrompts();
else if (name === 'market') renderMarket(); else if (name === 'market') renderMarket();
else if (name === 'signals') renderSignals();
} }
// ── Data Fetching ── // ── Data Fetching ──
@@ -1101,6 +1104,67 @@ function closePromptModal() {
document.getElementById('promptModal').classList.add('hidden'); document.getElementById('promptModal').classList.add('hidden');
document.getElementById('promptModal').classList.remove('flex'); document.getElementById('promptModal').classList.remove('flex');
} }
// ── 信号页 ──
async function renderSignals() {
const el = document.getElementById('tab-signals');
el.innerHTML = '<div class="text-slate-400 text-sm">加载中…</div>';
try {
const [signals, scan] = await Promise.all([
fetchJSON('/api/signals'),
fetchJSON('/api/xiaoguo-scan')
]);
const sourceToday = scan.source_today || {};
let html = `
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<div class="card p-4">
<div class="text-xs text-slate-500 mb-1">今日信号来源</div>
<div class="flex gap-4 text-sm">
<span class="text-blue-400">📡 趋势: ${sourceToday.trend || 0}</span>
<span class="text-green-400">🔍 小果扫描: ${sourceToday.xiaoguo || 0}</span>
</div>
</div>
<div class="card p-4">
<div class="text-xs text-slate-500 mb-1">小果扫描统计</div>
<div class="flex gap-4 text-sm">
<span class="text-slate-300">📋 累计扫描: ${scan.total_scanned || 0} 只</span>
<span class="text-yellow-400">🏆 发现信号: ${scan.found_signals || 0} 只</span>
</div>
</div>
</div>`;
if (!signals || signals.length === 0) {
html += '<div class="card p-6 text-center text-slate-500 text-sm">暂无信号</div>';
el.innerHTML = html;
return;
}
html += '<div class="space-y-3">';
for (const s of signals) {
const sev = s.severity || '-';
const sevColor = sev === 'high' ? 'text-red-400' : sev === 'medium' ? 'text-yellow-400' : 'text-green-400';
const sent = s.overall_sentiment || '-';
const sentColor = sent === '利好' ? 'text-green-400' : sent === '利空' ? 'text-red-400' : 'text-slate-400';
const source = s.source || 'trend';
const sourceIcon = source === 'xiaoguo' ? '🔍' : '📡';
html += `
<div class="card p-4">
<div class="flex items-center gap-2 mb-1">
<span class="${sevColor} text-xs font-semibold">${sev.toUpperCase()}</span>
<span class="${sentColor} text-sm font-semibold">${sent}</span>
<span class="text-slate-500 text-xs">${sourceIcon} ${source}</span>
<span class="text-slate-600 text-xs ml-auto">${s.created_at ? s.created_at.slice(5,16) : ''}</span>
</div>
<div class="text-sm text-slate-300">${s.sector || '-'}</div>
<div class="text-xs text-slate-500 mt-1">${(s.summary || '').slice(0,120)}</div>
</div>`;
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
}
}
</script> </script>
</body> </body>
</html> </html>