/** * ===== 建行定投评估(前端逻辑) ===== * 银行场外基金定投/赎回参考:点击「开始评估」按需抓取四大指数 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) })