/**
* ===== 建行定投评估(前端逻辑) =====
* 银行场外基金定投/赎回参考:点击「开始评估」按需抓取四大指数 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 = '
| 未获取到有效数据,请查看下方异常提示 |
'
} 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 ? `⚠ ${dipEscapeHtml(r.note)}
` : ''
return `
| ${dipEscapeHtml(r.name)} |
${dipEscapeHtml(r.code)} |
${dipEscapeHtml(r.market || '—')} |
${pe} |
${pct} |
${dipEscapeHtml(r.pe_date || '—')} |
${dipEscapeHtml(r.source || '—')} |
${st.label}:${dipEscapeHtml(r.advice || '—')}
${note}
|
`
}).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 => `⚠ ${dipEscapeHtml(e)}
`).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: '读取本地估值快照并判档,必要时通过探针补抓海外数据',
summary: 'A股走中证指数官网;标普走 multpl / DQYDJ 探针;纳指走 Yahoo+QQQ 探针自算,蛋卷兜底。',
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 = `| 评估失败:${dipEscapeHtml(String((e && e.message) || e))}(网络波动或数据源不可用,可稍后重试) |
`
}
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)
})