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