v1.6.0: 目标跟踪(概念/主题/股票) - watch_targets表+概念智能体跟踪(关键词检索+受益个股+影响度判定+邮件通知), 自动化页加持仓管理和目标管理, 报告支持股票/概念双类型
This commit is contained in:
+120
-15
@@ -29,6 +29,8 @@ async function loadAuto() {
|
||||
renderTrackingState(tr);
|
||||
loadLog();
|
||||
loadTrackReports();
|
||||
loadPositions();
|
||||
loadTargets();
|
||||
} catch (e) { toast('加载失败'); }
|
||||
}
|
||||
|
||||
@@ -102,6 +104,95 @@ async function runTrackAll() {
|
||||
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 {
|
||||
@@ -129,12 +220,14 @@ async function loadTrackReports() {
|
||||
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><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></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:320px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</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('');
|
||||
@@ -146,16 +239,31 @@ async function showTrack(id) {
|
||||
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 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>🧭 产业链跟踪:${d.stock_name}(${d.code})</h3>
|
||||
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}</h3>
|
||||
<div class="news-meta" style="margin-bottom:12px">
|
||||
<span class="cat-tag">${escapeHtml(d.industry)}</span>
|
||||
<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>
|
||||
@@ -163,14 +271,11 @@ async function showTrack(id) {
|
||||
<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>
|
||||
${newsBlock('个股直接动态', '📄', src.direct)}
|
||||
${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)}
|
||||
${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)}
|
||||
${newsBlock('同业竞争', '🏢', src.peers)}
|
||||
${sourceHtml}
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right">
|
||||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||||
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
|
||||
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
|
||||
</div>
|
||||
`);
|
||||
} catch (e) { toast('加载失败'); }
|
||||
|
||||
+22
-9
@@ -68,9 +68,11 @@ function renderRecords(items) {
|
||||
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><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></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>
|
||||
@@ -109,10 +111,25 @@ async function showDetail(id) {
|
||||
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>🧭 产业链跟踪:${d.stock_name}(${d.code})</h3>
|
||||
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}</h3>
|
||||
<div class="news-meta" style="margin-bottom:12px">
|
||||
<span class="cat-tag">${escapeHtml(d.industry)}</span>
|
||||
<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>
|
||||
@@ -120,15 +137,11 @@ async function showDetail(id) {
|
||||
<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>
|
||||
${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>
|
||||
${sourceHtml}
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right">
|
||||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||||
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
|
||||
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
|
||||
</div>
|
||||
`);
|
||||
} catch (e) { toast('加载失败'); }
|
||||
|
||||
Reference in New Issue
Block a user