310 lines
15 KiB
JavaScript
310 lines
15 KiB
JavaScript
/* 自动化监控页:舆情驱动 + 持仓跟踪 双选项卡 */
|
||
let autoSettings = null;
|
||
|
||
/* ===================== 选项卡 ===================== */
|
||
$$('.tab-item').forEach(t => t.onclick = () => {
|
||
$$('.tab-item').forEach(x => x.classList.toggle('active', x === t));
|
||
$$('.tab-panel').forEach(p => p.style.display = (p.id === 'panel-' + t.dataset.tab) ? '' : 'none');
|
||
});
|
||
|
||
/* ===================== 加载 ===================== */
|
||
async function loadAuto() {
|
||
try {
|
||
autoSettings = await api('/api/settings');
|
||
const mono = autoSettings.monitor, tr = autoSettings.tracking;
|
||
// 舆情
|
||
$('#monitorEnabled').checked = !!mono.enabled;
|
||
$('#monitorInterval').value = mono.interval_min;
|
||
$('#monitorImportance').value = mono.importance_threshold;
|
||
$('#monitorSentiment').value = mono.sentiment_weight;
|
||
$('#monitorKeywords').value = (mono.keywords || []).join(',');
|
||
const cats = mono.categories || [];
|
||
$$('#monitorCats input').forEach(c => c.checked = cats.includes(c.value));
|
||
renderMonitorState(autoSettings.state);
|
||
// 静默期(舆情)
|
||
const mq = (autoSettings.quiet && autoSettings.quiet.monitor) || {};
|
||
$('#monitorQuietEnabled').checked = !!mq.enabled;
|
||
$('#monitorQuietRanges').value = mq.ranges || '';
|
||
// 持仓跟踪
|
||
$('#trackingEnabled').checked = !!tr.enabled;
|
||
$('#trackingInterval').value = tr.interval_min;
|
||
$('#trackingImpact').value = tr.impact_threshold;
|
||
$('#trackingNotify').checked = !!tr.notify;
|
||
renderTrackingState(tr);
|
||
// 静默期(跟踪)
|
||
const tq = (autoSettings.quiet && autoSettings.quiet.tracking) || {};
|
||
$('#trackingQuietEnabled').checked = !!tq.enabled;
|
||
$('#trackingQuietRanges').value = tq.ranges || '';
|
||
loadLog();
|
||
loadTrackReports();
|
||
loadPositions();
|
||
loadTargets();
|
||
} catch (e) { toast('加载失败'); }
|
||
}
|
||
|
||
function renderMonitorState(state) {
|
||
$('#monitorState').innerHTML = `
|
||
<div class="mini-stat"><div class="ms-label">已处理新闻水位</div><div class="ms-value num">#${state.last_news_id || 0}</div></div>
|
||
<div class="mini-stat"><div class="ms-label">上次扫描</div><div class="ms-value">${state.last_scan || '—'}</div></div>
|
||
<div class="mini-stat"><div class="ms-label">上次通知</div><div class="ms-value num">${state.last_sent_count || 0} 条</div></div>`;
|
||
}
|
||
|
||
function renderTrackingState(tr) {
|
||
$('#trackingState').innerHTML = `
|
||
<div class="mini-stat"><div class="ms-label">自动跟踪</div><div class="ms-value">${tr.enabled ? '✅ 开启' : '⏸ 关闭'}(每 ${tr.interval_min} 分钟)</div></div>
|
||
<div class="mini-stat"><div class="ms-label">上次运行</div><div class="ms-value">${tr.last_run || '—'}</div></div>
|
||
<div class="mini-stat"><div class="ms-label">最近跟踪</div><div class="ms-value">${tr.last_stock || '—'}</div></div>
|
||
<div class="mini-stat"><div class="ms-label">最近告警影响度</div><div class="ms-value num">${tr.last_alert ? tr.last_alert + '/100' : '—'}</div></div>`;
|
||
}
|
||
|
||
function collectMonitor() {
|
||
const cats = $$('#monitorCats input:checked').map(c => c.value);
|
||
return {
|
||
monitor_enabled: $('#monitorEnabled').checked,
|
||
monitor_interval: $('#monitorInterval').value,
|
||
monitor_importance: $('#monitorImportance').value,
|
||
monitor_sentiment: $('#monitorSentiment').value,
|
||
monitor_categories: cats.join(','),
|
||
monitor_keywords: $('#monitorKeywords').value
|
||
};
|
||
}
|
||
|
||
function collectMonitorQuiet() {
|
||
return {
|
||
enabled: $('#monitorQuietEnabled').checked,
|
||
ranges: $('#monitorQuietRanges').value.trim()
|
||
};
|
||
}
|
||
|
||
function collectTrackingQuiet() {
|
||
return {
|
||
enabled: $('#trackingQuietEnabled').checked,
|
||
ranges: $('#trackingQuietRanges').value.trim()
|
||
};
|
||
}
|
||
|
||
/* ===================== 保存 ===================== */
|
||
async function saveMonitor() {
|
||
try {
|
||
const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
||
monitor: collectMonitor(),
|
||
quiet: { monitor: collectMonitorQuiet() }
|
||
}) });
|
||
toast(r.msg || '已保存');
|
||
} catch (e) { toast('保存失败:' + e.message); }
|
||
}
|
||
|
||
async function saveTracking() {
|
||
try {
|
||
const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tracking: {
|
||
tracking_enabled: $('#trackingEnabled').checked,
|
||
tracking_interval: $('#trackingInterval').value,
|
||
tracking_impact_threshold: $('#trackingImpact').value,
|
||
tracking_notify: $('#trackingNotify').checked
|
||
}, quiet: { tracking: collectTrackingQuiet() } }) });
|
||
toast(r.msg || '已保存');
|
||
} catch (e) { toast('保存失败:' + e.message); }
|
||
}
|
||
|
||
/* ===================== 动作 ===================== */
|
||
async function scanNow() {
|
||
const btn = event.target; btn.disabled = true;
|
||
try {
|
||
const r = await api('/api/monitor/scan', { method: 'POST' });
|
||
toast(`扫描完成:检查 ${r.checked} 条,重要 ${r.important} 条,发送 ${r.sent} 条`);
|
||
loadAuto();
|
||
} catch (e) { toast('扫描失败:' + e.message); }
|
||
btn.disabled = false;
|
||
}
|
||
|
||
async function runTrackAll() {
|
||
const btn = event.target; btn.disabled = true;
|
||
try {
|
||
const r = await api('/api/tracking/run', { method: 'POST' });
|
||
toast(r.msg || '已启动');
|
||
setTimeout(loadTrackReports, 5000);
|
||
setTimeout(loadTrackReports, 30000);
|
||
setTimeout(loadTrackReports, 70000);
|
||
} catch (e) { toast('启动失败:' + e.message); }
|
||
btn.disabled = false;
|
||
}
|
||
|
||
/* ===================== 持仓(自选股)管理 ===================== */
|
||
async function loadPositions() {
|
||
try {
|
||
const d = await api('/api/watchlist');
|
||
const items = d.items || [];
|
||
if (!items.length) {
|
||
$('#posManage').innerHTML = '<div class="empty">暂无持仓。在下方搜索添加,或到 <a href="/stocks">股票池</a> / 个股详情页 ⭐ 添加</div>';
|
||
return;
|
||
}
|
||
$('#posManage').innerHTML = `<table>
|
||
<tr><th>股票</th><th>现价</th><th>今日</th><th>评分</th><th>操作</th></tr>
|
||
${items.map(p => `<tr>
|
||
<td><a href="/stock/${p.code}" target="_blank"><b>${p.name}</b></a><div style="color:var(--muted);font-size:12px">${p.code} · ${p.industry}</div></td>
|
||
<td class="num">${p.close}</td>
|
||
<td class="num ${pctClass(p.change_pct)}">${fmtPct(p.change_pct)}</td>
|
||
<td class="num">${p.score}</td>
|
||
<td><button class="btn btn-danger" onclick="removePos('${p.code}')">移除</button></td>
|
||
</tr>`).join('')}
|
||
</table>`;
|
||
} catch (e) { $('#posManage').innerHTML = '<div class="empty">加载失败</div>'; }
|
||
}
|
||
|
||
async function addPos() {
|
||
const kw = $('#posSearch').value.trim();
|
||
if (!kw) return;
|
||
try {
|
||
const d = await api('/api/stocks?keyword=' + encodeURIComponent(kw) + '&per=5');
|
||
const items = d.items || [];
|
||
if (!items.length) { toast('未找到该股票'); return; }
|
||
// 精确匹配或取第一个
|
||
const hit = items.find(x => x.code === kw || x.name === kw) || items[0];
|
||
await api('/api/watchlist/' + hit.code, { method: 'POST' });
|
||
toast(`已添加 ${hit.name} 为持仓`);
|
||
$('#posSearch').value = '';
|
||
loadPositions();
|
||
} catch (e) { toast('添加失败'); }
|
||
}
|
||
|
||
async function removePos(code) {
|
||
try {
|
||
await api('/api/watchlist/' + code, { method: 'DELETE' });
|
||
toast('已移除持仓');
|
||
loadPositions();
|
||
} catch (e) { toast('操作失败'); }
|
||
}
|
||
|
||
/* ===================== 概念/主题/股票目标管理 ===================== */
|
||
async function loadTargets() {
|
||
try {
|
||
const d = await api('/api/targets');
|
||
const items = d.items || [];
|
||
if (!items.length) {
|
||
$('#tgtManage').innerHTML = '<div class="empty">暂无跟踪目标,添加概念/主题(如 AI算力、低空经济)或个股目标</div>';
|
||
return;
|
||
}
|
||
$('#tgtManage').innerHTML = `<table>
|
||
<tr><th>类型</th><th>目标</th><th>检索关键词</th><th>状态</th><th>操作</th></tr>
|
||
${items.map(t => `<tr>
|
||
<td><span class="cat-tag ${t.type === 'concept' ? '行业' : '公司'}">${t.type === 'concept' ? '概念/主题' : '股票'}</span></td>
|
||
<td><b>${escapeHtml(t.name)}</b>${t.code ? `<div style="color:var(--muted);font-size:12px">${t.code}</div>` : ''}</td>
|
||
<td style="color:var(--text2)">${escapeHtml(t.keywords || '—')}</td>
|
||
<td>${t.enabled ? '<span class="tag tag-推荐">跟踪中</span>' : '<span class="tag tag-观望">停用</span>'}</td>
|
||
<td><button class="btn btn-danger" onclick="removeTarget(${t.id})">移除</button></td>
|
||
</tr>`).join('')}
|
||
</table>`;
|
||
} catch (e) { $('#tgtManage').innerHTML = '<div class="empty">加载失败</div>'; }
|
||
}
|
||
|
||
async function addTarget() {
|
||
const type = $('#tgtType').value;
|
||
const name = $('#tgtName').value.trim();
|
||
const keywords = $('#tgtKeywords').value.trim();
|
||
if (!name) { toast('请输入目标名称'); return; }
|
||
try {
|
||
await api('/api/targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, name, keywords }) });
|
||
toast('已添加跟踪目标');
|
||
$('#tgtName').value = ''; $('#tgtKeywords').value = '';
|
||
loadTargets();
|
||
} catch (e) { toast('添加失败:' + e.message); }
|
||
}
|
||
|
||
async function removeTarget(id) {
|
||
try {
|
||
await api('/api/targets/' + id, { method: 'DELETE' });
|
||
toast('已移除目标');
|
||
loadTargets();
|
||
} catch (e) { toast('操作失败'); }
|
||
}
|
||
|
||
/* ===================== 日志与报告 ===================== */
|
||
async function loadLog() {
|
||
try {
|
||
const d = await api('/api/monitor/log');
|
||
const items = d.items || [];
|
||
if (!items.length) return;
|
||
$('#logTb').innerHTML = items.map(n => `
|
||
<tr>
|
||
<td style="color:var(--muted)">${n.sent_at}</td>
|
||
<td style="max-width:300px;white-space:normal">${escapeHtml(n.title)}</td>
|
||
<td><span class="cat-tag ${n.category}">${n.category}</span></td>
|
||
<td class="num ${pctClass(n.sentiment)}">${n.sentiment > 0 ? '利好' : n.sentiment < 0 ? '利空' : '中性'} (${Number(n.sentiment).toFixed(2)})</td>
|
||
<td class="num"><b>${n.importance}</b></td>
|
||
<td><span class="tag ${n.status === 'sent' ? 'tag-推荐' : n.status === 'skipped' ? 'tag-关注' : n.status === 'quiet' ? 'tag-关注' : 'tag-负'}">${n.status === 'sent' ? '已发送' : n.status === 'skipped' ? '已跳过' : n.status === 'quiet' ? '🌙 静默' : '失败'}</span></td>
|
||
</tr>`).join('');
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function loadTrackReports() {
|
||
try {
|
||
const d = await api('/api/tracking');
|
||
const items = d.items || [];
|
||
if (!items.length) return;
|
||
$('#trackTb').innerHTML = items.slice(0, 10).map(r => {
|
||
let meta = {};
|
||
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||
const isConcept = r.target_type === 'concept';
|
||
return `<tr class="row-link" onclick="showTrack(${r.id})">
|
||
<td style="color:var(--muted)">${r.created_at}</td>
|
||
<td><span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念' : '股票'}</span></td>
|
||
<td><b>${r.stock_name}</b>${isConcept ? '' : `<div style="color:var(--muted);font-size:12px">${r.code}</div>`}</td>
|
||
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
||
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
||
<td style="max-width:300px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
|
||
<td><button class="btn" onclick="event.stopPropagation();showTrack(${r.id})">查看 ↗</button></td>
|
||
</tr>`;
|
||
}).join('');
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function showTrack(id) {
|
||
try {
|
||
const d = await api('/api/tracking/' + id);
|
||
const meta = d.meta || {}, src = d.sources || {};
|
||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||
const isConcept = d.target_type === 'concept';
|
||
// 概念报告:展示关键词/资讯/受益个股;股票报告:四环节数据源
|
||
let sourceHtml = '';
|
||
if (isConcept) {
|
||
const dc = (meta.news_counts || {}).direct || 0, rc = (meta.news_counts || {}).rag || 0, nc = (meta.news_counts || {}).related || 0;
|
||
sourceHtml = `
|
||
<details open><summary>🔎 相关资讯(${dc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.db_news || '')}</pre></div></details>
|
||
<details><summary>🧠 语义检索命中(${rc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.rag_news || '')}</pre></div></details>
|
||
<details><summary>🏢 受益个股(${nc} 只)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.related || '')}</pre></div></details>`;
|
||
} else {
|
||
const newsBlock = (label, icon, seg) => {
|
||
if (!seg) return '';
|
||
const txt = ((seg.db || '') + (seg.rag || '')).trim();
|
||
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
||
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
||
};
|
||
sourceHtml = newsBlock('个股直接动态', '📄', src.direct) +
|
||
newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) +
|
||
newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) +
|
||
newsBlock('同业竞争', '🏢', src.peers);
|
||
}
|
||
openModal(`
|
||
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}</h3>
|
||
<div class="news-meta" style="margin-bottom:12px">
|
||
<span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念/主题' : d.industry}</span>
|
||
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
||
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
||
<span style="color:var(--muted)">${d.created_at}</span>
|
||
</div>
|
||
<div class="markdown-body">${mdRender(d.report)}</div>
|
||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
||
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
||
${sourceHtml}
|
||
</div>
|
||
<div style="margin-top:14px;text-align:right">
|
||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
|
||
</div>
|
||
`);
|
||
} catch (e) { toast('加载失败'); }
|
||
}
|
||
|
||
loadAuto();
|