6a7a2b089a
1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置 2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议 3. 新增推送告警白名单过滤功能,限制推送指定指数告警 4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求 5. 新增多套调试探针脚本与单元测试用例
112 lines
5.4 KiB
JavaScript
112 lines
5.4 KiB
JavaScript
/**
|
||
* ===== 建行定投评估(前端逻辑) =====
|
||
* 银行场外基金定投/赎回参考:点击「开始评估」按需抓取四大指数 PE-TTM 与近10年分位,无定时任务。
|
||
*
|
||
* 档位阈值与后端 src/server/markets_dip_eval.js 保持一致:
|
||
* 沪深300:PE<12 定投200元/日 | 12~14.5 定投150元/日 | 14.5~16 定投100元/日·可止盈50% | PE>16 暂停定投+止盈清仓
|
||
* 中证500:PE<25 定投200元/日 | 25~32 定投150元/日 | 32~36 定投100元/日·可止盈50% | PE>36 暂停定投+止盈清仓
|
||
* 纳指100:PE<24 定投200元/日 | 24~30 定投150元/日 | 30~36 定投100元/日·可止盈50% | PE>36 暂停定投+止盈清仓
|
||
* 标普500:PE<16 定投200元/日 | 16~21 定投150元/日 | 21~25 定投100元/日·可止盈50% | PE>25 暂停定投+止盈清仓
|
||
*
|
||
* 依赖 markets.js 提供的全局函数:el / runTrackedInteraction / setButtonBusy
|
||
*/
|
||
|
||
// 档位 → 徽章样式(绿=定投加仓、蓝=轻定投、黄=可止盈一半、红=暂停+清仓)
|
||
const DIP_LEVEL_STYLE = {
|
||
buy_heavy: { bg: '#dcfce7', color: '#15803d', label: '低估档' },
|
||
buy: { bg: '#dcfce7', color: '#16a34a', label: '定投档' },
|
||
buy_light: { bg: '#e0f2fe', color: '#0369a1', label: '轻定投档' },
|
||
take_half: { bg: '#fef3c7', color: '#b45309', label: '止盈档' },
|
||
exit: { bg: '#fee2e2', color: '#b91c1c', label: '清仓档' },
|
||
unknown: { bg: '#f1f5f9', color: '#64748b', label: '未知' }
|
||
}
|
||
|
||
// HTML 转义,防止数据源返回内容注入
|
||
const dipEscapeHtml = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
}[c]))
|
||
|
||
// 渲染评估结果表格
|
||
const renderDipReport = data => {
|
||
const list = el('dip-list')
|
||
const status = el('dip-status')
|
||
const errorsBox = el('dip-errors')
|
||
if (!list) return
|
||
const rows = Array.isArray(data.rows) ? data.rows : []
|
||
|
||
if (!rows.length) {
|
||
list.innerHTML = '<tr><td colspan="8" class="muted" style="text-align:center;padding:30px">未获取到有效数据,请查看下方异常提示</td></tr>'
|
||
} else {
|
||
list.innerHTML = rows.map(r => {
|
||
const st = DIP_LEVEL_STYLE[r.level] || DIP_LEVEL_STYLE.unknown
|
||
const pe = Number.isFinite(r.pe_ttm) ? r.pe_ttm.toFixed(2) : '—'
|
||
const pct = Number.isFinite(r.percentile) ? `${r.percentile.toFixed(1)}%` : '—'
|
||
const note = r.note ? `<div style="margin-top:6px;font-size:12px;color:#b45309;line-height:1.6">⚠ ${dipEscapeHtml(r.note)}</div>` : ''
|
||
return `<tr>
|
||
<td><b>${dipEscapeHtml(r.name)}</b></td>
|
||
<td>${dipEscapeHtml(r.code)}</td>
|
||
<td>${dipEscapeHtml(r.market || '—')}</td>
|
||
<td style="font-weight:700">${pe}</td>
|
||
<td title="${dipEscapeHtml(r.percentile_note || '')}">${pct}</td>
|
||
<td>${dipEscapeHtml(r.pe_date || '—')}</td>
|
||
<td>${dipEscapeHtml(r.source || '—')}</td>
|
||
<td>
|
||
<span style="display:inline-block;padding:3px 10px;border-radius:999px;background:${st.bg};color:${st.color};font-weight:700;font-size:13px">${st.label}:${dipEscapeHtml(r.advice || '—')}</span>
|
||
${note}
|
||
</td>
|
||
</tr>`
|
||
}).join('')
|
||
}
|
||
|
||
if (status) {
|
||
status.style.display = ''
|
||
status.textContent = `评估时间 ${data.captured_at || '—'} | 指数 ${rows.length} 个 | 异常 ${(Array.isArray(data.errors) ? data.errors.length : 0)} 条`
|
||
}
|
||
|
||
// 异常提示区:单指数失败不影响其他指数展示
|
||
if (errorsBox) {
|
||
const errs = Array.isArray(data.errors) ? data.errors : []
|
||
if (errs.length) {
|
||
errorsBox.style.display = ''
|
||
errorsBox.innerHTML = errs.map(e => `<div style="font-size:13px;color:#b91c1c;line-height:1.9">⚠ ${dipEscapeHtml(e)}</div>`).join('')
|
||
} else {
|
||
errorsBox.style.display = 'none'
|
||
errorsBox.innerHTML = ''
|
||
}
|
||
}
|
||
}
|
||
|
||
// 点击「开始评估」:走 markets 统一交互详情弹窗(双模式由 runTrackedInteraction 内部处理)
|
||
const runDipEval = async () => {
|
||
const btn = el('dip-btn-run')
|
||
try {
|
||
const data = await runTrackedInteraction({
|
||
title: '建行定投评估',
|
||
action: '抓取四大指数 PE-TTM 与近10年分位并判档',
|
||
summary: '按需抓取:中证指数官网 / 蛋卷基金(备源 multpl.com)...',
|
||
button: btn,
|
||
loadingText: '评估中…',
|
||
request: { url: '/api/markets/dip_eval', method: 'GET' },
|
||
successText: d => `评估完成:${(d && Array.isArray(d.rows)) ? d.rows.length : 0} 个指数,${((d && d.errors) || []).length} 条异常`
|
||
}, async ctx => {
|
||
return await ctx.fetchJson('/api/markets/dip_eval', {}, { 说明: '按需抓取,无定时任务' })
|
||
})
|
||
renderDipReport(data || {})
|
||
} catch (e) {
|
||
// 交互弹窗已展示错误详情(异常时窗口停留),这里给表格区一个兜底提示
|
||
const list = el('dip-list')
|
||
if (list) {
|
||
list.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:30px;color:#b91c1c">评估失败:${dipEscapeHtml(String((e && e.message) || e))}(网络波动或数据源不可用,可稍后重试)</td></tr>`
|
||
}
|
||
const status = el('dip-status')
|
||
if (status) status.style.display = 'none'
|
||
const errorsBox = el('dip-errors')
|
||
if (errorsBox) { errorsBox.style.display = 'none'; errorsBox.innerHTML = '' }
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const btn = el('dip-btn-run')
|
||
if (btn) btn.addEventListener('click', runDipEval)
|
||
})
|