Dashboard: 信号tab + API(signals + xiaoguo-scan),前端展示
This commit is contained in:
@@ -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():
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user