Files
Toolbox/public/tools/markets/markets_dip_eval.js
yangxiangyuan e4f84caa23 feat: 重构定投评估工具并优化Cloud工具体验
- 重构市场定投评估逻辑:优先读取本地估值快照,必要时通过探针补抓海外数据,后端接口新增force参数支持强制刷新,更新前后端注释与交互文案
- 优化Cloud工具全链路:
  - 新增运营商登录页地址字段,支持一键复制账号密码并打开对应登录页
  - 缓存密钥派生结果,避免重复阻塞事件循环,提升列表加载性能
  - 完善接口错误处理,新增面板级加载失败提示与自动重试功能
  - 修复云资源代理转发时的Cookie缺失问题
  - 并行加载所有面板数据,优化首屏渲染速度
  - 新增登录按钮样式与错误提示样式
- 新增4个Cloud工具调试脚本,用于性能测试、解密审计、回归验证与加载耗时统计
- 启动定投评估工具的定时调度任务
2026-08-17 16:33:41 +08:00

112 lines
5.5 KiB
JavaScript
Raw Permalink 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.
/**
* ===== 建行定投评估(前端逻辑) =====
* 银行场外基金定投/赎回参考:点击「开始评估」按需抓取四大指数 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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[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: '读取本地估值快照并判档,必要时通过探针补抓海外数据',
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 = `<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)
})