Files
stock-advisor/static/js/tracking.js
T

157 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 持仓跟踪智能体 */
async function load() {
try {
const d = await api('/api/tracking');
const items = d.items || [];
renderRecords(items);
loadPositions(items);
} catch (e) {
$('#recTb').innerHTML = '<tr><td colspan="7" class="empty">加载失败</td></tr>';
}
loadState();
}
function loadState() {
api('/api/settings').then(d => {
const t = d.tracking || {};
$('#trackState').innerHTML = `
<div class="kpi"><div class="kpi-label">自动跟踪</div><div class="kpi-value" style="font-size:15px">${t.enabled ? '✅ 开启' : '⏸ 关闭'}<span style="color:var(--muted);font-size:12px">(每 ${t.interval_min} 分钟)</span></div></div>
<div class="kpi"><div class="kpi-label">上次运行</div><div class="kpi-value" style="font-size:15px">${t.last_run || '—'}</div></div>
<div class="kpi"><div class="kpi-label">最近跟踪标的</div><div class="kpi-value" style="font-size:15px">${t.last_stock || '—'}</div></div>
<div class="kpi"><div class="kpi-label">最近告警影响度</div><div class="kpi-value" style="font-size:15px">${t.last_alert ? t.last_alert + '/100' : '—'}</div></div>
<div class="kpi"><div class="kpi-label">重大判定阈值</div><div class="kpi-value" style="font-size:15px">≥ ${t.impact_threshold}</div></div>`;
}).catch(() => {});
}
async function loadPositions(all) {
try {
const w = await api('/api/watchlist');
const pos = w.items || [];
if (!pos.length) {
$('#posBox').innerHTML = '<div class="empty">自选股为空。请先在 <a href="/stocks">股票池</a> 或个股详情页 ⭐ 添加持仓/关注标的</div>';
return;
}
// 每只自选股取最近一条跟踪
const rows = [];
for (const p of pos) {
const latest = all.find(r => r.code === p.code);
rows.push({ stock: p, latest });
}
$('#posBox').innerHTML = `<table>
<tr><th>股票</th><th>现价</th><th>最近跟踪</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr>
${rows.map(x => {
const p = x.stock, r = x.latest;
const meta = r ? (JSON.parse(r.meta || '{}')) : null;
const sigCls = meta ? (meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注') : 'tag-观望';
return `<tr>
<td><b>${p.name}</b><div style="color:var(--muted);font-size:12px">${p.code} · ${p.industry}</div></td>
<td class="num">${p.close}</td>
<td style="color:var(--muted)">${r ? r.created_at : '—'}</td>
<td>${r && meta ? `<span class="tag ${sigCls}">${meta.impact_score}</span>` : '<span style="color:var(--muted)">未跟踪</span>'}</td>
<td>${meta ? `<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)">${meta ? escapeHtml(meta.summary || '') : '—'}</td>
<td><div class="flex">
<button class="btn btn-primary" onclick="runOne('${p.code}')">跟踪</button>
${r ? `<button class="btn" onclick="showDetail(${r.id})">详情</button>` : ''}
</div></td>
</tr>`;
}).join('')}
</table>`;
} catch (e) {
$('#posBox').innerHTML = '<div class="empty">加载失败</div>';
}
}
function renderRecords(items) {
if (!items.length) return;
$('#recTb').innerHTML = items.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="showDetail(${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 style="color:var(--text2)">${escapeHtml(r.industry)}</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:320px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
<td><button class="btn" onclick="event.stopPropagation();showDetail(${r.id})">查看报告 ↗</button></td>
</tr>`;
}).join('');
}
async function runAll() {
if (!confirm('将对全部自选股执行一次产业链深度跟踪(每只约30-60秒,逐只进行),确定?')) return;
try {
const r = await api('/api/tracking/run', { method: 'POST' });
toast(r.msg || '已启动');
pollRefresh();
} catch (e) { toast('启动失败:' + e.message); }
}
async function runOne(code) {
try {
await api('/api/tracking/run/' + code, { method: 'POST' });
toast('跟踪已启动,完成后自动刷新');
setTimeout(() => { load(); toast('跟踪完成'); }, 80000);
} catch (e) { toast('启动失败:' + e.message); }
}
/* 详情弹窗:报告 + 数据源 */
async function showDetail(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 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>`;
};
const isConcept = d.target_type === 'concept';
let sourceHtml;
if (isConcept) {
const nc = meta.news_counts || {};
sourceHtml = `
<details open><summary>🔎 相关资讯(${nc.direct || 0} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.db_news || '')}</pre></div></details>
<details><summary>🧠 语义检索命中(${nc.rag || 0} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.rag_news || '')}</pre></div></details>
<details><summary>🏢 受益个股(${nc.related || 0} 只)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.related || '')}</pre></div></details>`;
} else {
sourceHtml = newsBlock('个股直接动态', '📄', src.direct) +
newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) +
newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) +
newsBlock('同业竞争', '🏢', src.peers) +
`<details><summary>📈 技术面 / 机构动向</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.indicators || '')}</pre></div></details>`;
}
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('加载失败'); }
}
let pollTimer = null;
function pollRefresh() {
clearTimeout(pollTimer);
pollTimer = setTimeout(() => load(), 30000);
}
load();