const API_BASE = '/api/v1/review'; const DATA_API_BASE = '/api/v1/data'; let currentPlanId = null; let currentPlanData = null; // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', function () { setupEventListeners(); loadLatestPlan(); }); function setupEventListeners() { document.getElementById('date-selector').addEventListener('change', function (e) { const selectedDate = e.target.value; if (selectedDate) { loadPlanByDate(selectedDate); } }); document.getElementById('btn-generate').addEventListener('click', function () { handleGenerate(); }); document.getElementById('btn-clear').addEventListener('click', function () { handleClear(); }); setupSortButtons(); } // ==================== 生成复盘 ==================== function handleGenerate() { const dateSelector = document.getElementById('date-selector'); let reviewDate = dateSelector.value; if (!reviewDate) { // 未选日期时默认使用当天 reviewDate = new Date().toISOString().split('T')[0]; } doGenerate(reviewDate); } function doGenerate(dateStr) { showLoading(); fetch(`${API_BASE}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ review_date: dateStr }) }) .then(res => res.json()) .then(result => { hideLoading(); if (result.success) { const s = result.data.summary; alert( `复盘与计划生成成功!\n` + `日期: ${s.review_date} ${s.week_day}\n` + `品种数: ${s.symbol_count}\n` + `交易机会: ${s.opportunity_count} 个` ); loadLatestPlan(); } else { alert('生成失败: ' + (result.message || '未知错误')); } }) .catch(err => { hideLoading(); console.error('生成复盘计划失败:', err); alert('生成失败,请稍后重试'); }); } // ==================== 清除数据 ==================== function handleClear() { if (!confirm('确定要清除所有复盘与交易计划数据吗?此操作不可恢复。')) return; fetch(`${API_BASE}/v2/clear`, { method: 'DELETE' }) .then(res => res.json()) .then(data => { if (data.success) { alert('复盘数据已清除'); document.getElementById('date-selector').value = ''; currentPlanId = null; currentPlanData = null; hideAllSections(); showSection('empty-state'); loadLatestPlan(); } else { alert(data.message || '清除失败'); } }) .catch(err => { console.error('清除数据失败:', err); alert('清除失败,请稍后重试'); }); } // ==================== 加载数据 ==================== function loadLatestPlan() { fetch(`${API_BASE}/v2/plans?limit=1`) .then(res => res.json()) .then(data => { if (data.success && data.data && data.data.length > 0) { const plan = data.data[0]; const dateSelector = document.getElementById('date-selector'); if (dateSelector && plan.review_date) { dateSelector.value = plan.review_date; } loadAndRender(plan.id); } else { hideAllSections(); showSection('empty-state'); } }) .catch(err => { console.error('加载最新计划失败:', err); }); } function loadPlanByDate(date) { showLoading(); fetch(`${API_BASE}/v2/plan/by-date/${date}`) .then(res => res.json()) .then(data => { hideLoading(); if (data.success && data.data) { currentPlanId = data.data.meta.id; currentPlanData = data.data; renderAll(data.data); } else { hideAllSections(); showSection('empty-state'); } }) .catch(err => { hideLoading(); console.error('按日期加载计划失败:', err); }); } function loadAndRender(planId) { showLoading(); fetch(`${API_BASE}/v2/plan/${planId}`) .then(res => res.json()) .then(result => { hideLoading(); if (result.success && result.data) { currentPlanId = planId; currentPlanData = result.data; renderAll(result.data); } else { alert('加载计划详情失败'); } }) .catch(err => { hideLoading(); console.error('加载计划详情失败:', err); alert('加载失败,请稍后重试'); }); } // ==================== 渲染总入口 ==================== function renderAll(data) { // 显示内容区域 const contentArea = document.getElementById('content-area'); if (contentArea) contentArea.style.display = ''; hideSection('empty-state'); const { meta, scores, plans, sectors } = data; renderHero(meta); renderStats(meta, scores); renderRisk(meta); renderGreenSection(plans, scores); renderYellowSection(scores); renderRedSection(scores); renderRankingTable(scores); renderDetails(scores); renderSectors(sectors); } // ==================== Hero 区域 ==================== function renderHero(meta) { const el = document.getElementById('hero-section'); if (!el) return; // 当数据日期与复盘日期不一致时,显示提示 let dataDateNotice = ''; if (meta.actual_data_date && !meta.data_date_matches) { dataDateNotice = `
⚠ 当前页面收盘价、涨跌幅为 ${meta.actual_data_date}(最近交易日)数据,非 ${meta.review_date} 当日数据
`; } el.innerHTML = `

${meta.review_date} ${meta.week_day || ''}

${meta.data_basis || ''}
${dataDateNotice}
${meta.core_conclusion || ''}
`; showSection('hero-section'); } // ==================== 统计卡片 ==================== function renderStats(meta, scores) { const el = document.getElementById('stats-grid'); if (!el) return; const top3 = (scores || []).slice(0, 3); const top3Html = top3.map((s, i) => `${i + 1}. ${s.symbol} ${s.name || ''} ${s.composite_score}` ).join(''); const bullCount = meta.bull_count || 0; const bearCount = meta.bear_count || 0; const neutralCount = meta.neutral_count || 0; const oppCount = meta.opportunity_count || 0; el.innerHTML = `
核心结论
${meta.core_conclusion || '-'}
综合评分 Top 3
${top3Html || '暂无'}
市场概览
多头 ${bullCount} 空头 ${bearCount} 中性 ${neutralCount} 机会 ${oppCount}
`; showSection('stats-grid'); } // ==================== 风险提示 ==================== function renderRisk(meta) { const el = document.getElementById('risk-banner'); if (!el) return; const warnings = meta.risk_warnings || []; if (warnings.length === 0) { el.innerHTML = `
⚠️ 风险提示
暂无特别风险提示
`; } else { const items = warnings.map(w => `
  • ${w}
  • `).join(''); el.innerHTML = `
    ⚠️ 风险提示
    `; } showSection('risk-banner'); } // ==================== 绿色机会区 ==================== function renderGreenSection(plans, scores) { const el = document.getElementById('green-section'); if (!el) return; const grid = document.getElementById('green-grid'); if (!grid) return; if (!plans || plans.length === 0) { grid.innerHTML = `
    暂无交易机会
    `; showSection('green-section'); return; } const scoreMap = {}; (scores || []).forEach(s => { scoreMap[s.symbol] = s; }); const cards = plans.map(plan => { const s = scoreMap[plan.symbol] || {}; const dirClass = plan.direction === 'long' ? 'dir-long' : 'dir-short'; const dirText = plan.direction === 'long' ? '做多' : '做空'; const dataDateTag = s.data_date ? `${s.data_date}` : ''; return `
    ${plan.symbol} ${plan.name || ''} ${plan.composite_score}
    方向${dirText}
    收盘价${formatPrice(s.close_price)} ${dataDateTag}
    涨跌幅${formatChange(s.change_pct)}
    量比${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}
    振幅${scoreBar(plan.amplitude_score)}
    量能${scoreBar(plan.volume_score)}
    趋势${scoreBar(plan.trend_score)}
    活跃度${scoreBar(plan.activity_score)}
    入场区间${formatPrice(plan.entry_low)} - ${formatPrice(plan.entry_high)}
    止损${formatPrice(plan.stop_loss)}
    目标1${formatPrice(plan.target1)}
    目标2${formatPrice(plan.target2)}
    触发条件${plan.trigger || '-'}
    `; }).join(''); grid.innerHTML = cards; showSection('green-section'); } // ==================== 黄色关注区 ==================== function renderYellowSection(scores) { const tbody = document.getElementById('yellow-list-body'); if (!tbody) return; const yellowItems = (scores || []).filter(s => s.category === 'yellow'); if (yellowItems.length === 0) { tbody.innerHTML = `暂无关注品种`; showSection('yellow-section'); return; } tbody.innerHTML = yellowItems.map((s, i) => ` ${i + 1} ${s.symbol} ${s.name || ''} ${formatPrice(s.close_price)} ${formatChange(s.change_pct)}
    ${scoreBar(s.composite_score)}${s.composite_score}
    ${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'} ${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'} ${directionTag(s.direction_tag)} ${s.data_date || '-'} `).join(''); showSection('yellow-section'); } // ==================== 红色风险区 ==================== function renderRedSection(scores) { const tbody = document.getElementById('red-list-body'); if (!tbody) return; const redItems = (scores || []).filter(s => s.category === 'red'); if (redItems.length === 0) { tbody.innerHTML = `暂无规避品种`; showSection('red-section'); return; } tbody.innerHTML = redItems.map((s, i) => ` ${i + 1} ${s.symbol} ${s.name || ''} ${formatPrice(s.close_price)} ${formatChange(s.change_pct)}
    ${scoreBar(s.composite_score)}${s.composite_score}
    ${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'} ${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'} ${directionTag(s.direction_tag)} ${s.data_date || '-'} `).join(''); showSection('red-section'); } // ==================== 排名表格(支持多列排序) ==================== let currentSortField = 'composite_score'; let currentSortOrder = 'desc'; function renderRankingTable(scores) { const table = document.getElementById('ranking-table'); if (!table) return; const tbody = table.querySelector('tbody'); if (!tbody) return; if (!scores || scores.length === 0) { tbody.innerHTML = `暂无排名数据`; showSection('table-section'); return; } // 排序 const sorted = [...scores].sort((a, b) => { let va = a[currentSortField]; let vb = b[currentSortField]; // 方向排序特殊处理 if (currentSortField === 'direction_tag') { const dirOrder = { '强多': 1, '偏多': 2, '多头共振': 1, '震荡': 3, '偏空': 4, '强空': 5, '空头共振': 5 }; va = dirOrder[va] || 3; vb = dirOrder[vb] || 3; } if (va == null) va = -Infinity; if (vb == null) vb = -Infinity; if (currentSortOrder === 'asc') return va > vb ? 1 : va < vb ? -1 : 0; return va < vb ? 1 : va > vb ? -1 : 0; }); tbody.innerHTML = sorted.map((s, i) => { const rank = i + 1; const rankClass = rank <= 3 ? 'rank-top' : ''; return ` ${rank} ${s.symbol} ${s.name || ''} ${formatPrice(s.close_price)} ${formatChange(s.change_pct)}
    ${scoreBar(s.composite_score)}${s.composite_score}
    ${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'} ${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'} ${s.trend_score != null ? s.trend_score : '-'} ${directionTag(s.direction_tag)} ${s.activity_score != null ? s.activity_score : '-'} ${s.data_date || '-'} `; }).join(''); showSection('table-section'); } function setupSortButtons() { const controls = document.getElementById('sort-controls'); if (!controls) return; controls.addEventListener('click', function (e) { const btn = e.target.closest('.sort-btn'); if (!btn) return; const field = btn.dataset.sort; const defaultOrder = btn.dataset.order || 'desc'; // 切换排序方向 if (currentSortField === field) { currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc'; } else { currentSortField = field; currentSortOrder = defaultOrder; } // 更新按钮状态 controls.querySelectorAll('.sort-btn').forEach(b => { b.classList.remove('active'); b.textContent = b.textContent.replace(/ [▲▼]$/, ''); }); btn.classList.add('active'); const arrow = currentSortOrder === 'desc' ? ' ▼' : ' ▲'; btn.textContent = btn.textContent + arrow; // 重新渲染 if (currentPlanData) { renderRankingTable(currentPlanData.scores); } }); } // ==================== 品种详情卡片 ==================== function renderDetails(scores) { const el = document.getElementById('details-section'); if (!el) return; if (!scores || scores.length === 0) { el.innerHTML = `
    📋 品种详情
    暂无详情数据
    `; showSection('details-section'); return; } const cards = scores.map(s => { const pivotTags = []; if (s.pivot != null) pivotTags.push(`Pivot ${formatPrice(s.pivot)}`); if (s.r1 != null) pivotTags.push(`R1 ${formatPrice(s.r1)}`); if (s.r2 != null) pivotTags.push(`R2 ${formatPrice(s.r2)}`); if (s.s1 != null) pivotTags.push(`S1 ${formatPrice(s.s1)}`); if (s.s2 != null) pivotTags.push(`S2 ${formatPrice(s.s2)}`); return `
    ${s.symbol} ${s.name || ''} ${s.composite_score}
    收盘价${formatPrice(s.close_price)}${s.data_date ? ' ' + s.data_date + '' : ''}
    振幅${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}
    涨跌幅${formatChange(s.change_pct)}
    量比${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}
    振幅评分${s.amplitude_score != null ? s.amplitude_score : '-'}
    量能评分${s.volume_score != null ? s.volume_score : '-'}
    趋势评分${s.trend_score != null ? s.trend_score : '-'}
    活跃度${s.activity_score != null ? s.activity_score : '-'}
    60m趋势${s.trend_60m != null ? s.trend_60m : '-'}
    15m趋势${s.trend_15m != null ? s.trend_15m : '-'}
    5m趋势${s.trend_5m != null ? s.trend_5m : '-'}
    方向${directionTag(s.direction_tag)}
    ${pivotTags.join('')}
    `; }).join(''); el.innerHTML = `
    📋 品种详情
    ${cards}
    `; showSection('details-section'); } // ==================== 板块热力 ==================== function renderSectors(sectors) { const el = document.getElementById('sectors-section'); if (!el) return; if (!sectors || sectors.length === 0) { el.innerHTML = `
    🔥 板块热力
    暂无板块数据
    `; showSection('sectors-section'); return; } const cards = sectors.map(sec => { const heatLevel = sec.heat_level || 0; const fireIcons = '🔥'.repeat(Math.min(heatLevel, 5)); const iceIcons = heatLevel <= 1 ? '🧊' : ''; const heatIndicator = heatLevel >= 3 ? fireIcons : (heatLevel <= 1 ? iceIcons : fireIcons); const members = (sec.members || []).map(m => `${m.symbol} ${m.name || ''} ${m.score}` ).join(''); return `
    ${sec.sector_name} ${heatIndicator}
    均分${sec.avg_score != null ? sec.avg_score.toFixed(1) : '-'}
    方向${sec.direction || '-'}
    领涨${sec.leader_symbol || '-'} ${sec.leader_score != null ? sec.leader_score.toFixed(1) : ''}
    ${members || '暂无成员'}
    `; }).join(''); el.innerHTML = `
    🔥 板块热力
    ${cards}
    `; showSection('sectors-section'); } // ==================== 品种详情弹窗 ==================== function showDetail(symbol, name) { const scores = currentPlanData ? currentPlanData.scores : []; const plans = currentPlanData ? currentPlanData.plans : []; const s = scores.find(x => x.symbol === symbol); if (!s) return; // 查找对应的交易计划 const plan = plans.find(p => p.symbol === symbol); const modal = document.getElementById('detail-modal'); if (!modal) return; document.getElementById('modal-title').textContent = `${symbol} ${name || s.name || ''}`; const body = document.getElementById('modal-body'); // 交易计划部分(仅绿色品种有) let planHtml = ''; if (plan) { const dirClass = plan.direction === 'long' ? 'dir-long' : 'dir-short'; const dirText = plan.direction === 'long' ? '做多' : '做空'; planHtml = ` `; } const pivotTags = []; if (s.pivot != null) pivotTags.push(`Pivot ${formatPrice(s.pivot)}`); if (s.r1 != null) pivotTags.push(`R1 ${formatPrice(s.r1)}`); if (s.r2 != null) pivotTags.push(`R2 ${formatPrice(s.r2)}`); if (s.s1 != null) pivotTags.push(`S1 ${formatPrice(s.s1)}`); if (s.s2 != null) pivotTags.push(`S2 ${formatPrice(s.s2)}`); body.innerHTML = ` ${planHtml} `; modal.classList.add('show'); } function closeModal() { const modal = document.getElementById('detail-modal'); if (modal) modal.classList.remove('show'); } // ==================== 工具函数 ==================== function scoreBarColor(score) { if (score > 70) return 'var(--gn)'; if (score > 40) return 'var(--ac)'; if (score > 20) return 'var(--yw)'; return 'var(--rd)'; } function scoreBar(score) { const color = scoreBarColor(score); const pct = Math.min(Math.max(score, 0), 100); return `
    ${score}
    `; } function directionTag(tag) { if (!tag) return '-'; let cls = 'dir-neutral'; if (tag.includes('强多') || tag.includes('多头共振')) cls = 'dir-strong-long'; else if (tag.includes('多')) cls = 'dir-long'; else if (tag.includes('强空') || tag.includes('空头共振')) cls = 'dir-strong-short'; else if (tag.includes('空')) cls = 'dir-short'; return `${tag}`; } function formatPrice(price) { if (price == null || isNaN(price)) return '-'; return Number(price).toFixed(2); } function formatChange(pct) { if (pct == null || isNaN(pct)) return '-'; const val = Number(pct); const sign = val > 0 ? '+' : ''; const cls = val > 0 ? 'change-up' : (val < 0 ? 'change-down' : 'change-neutral'); return `${sign}${val.toFixed(2)}%`; } function showSection(id) { const el = document.getElementById(id); if (el) el.style.display = ''; } function hideSection(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; } function hideAllSections() { const contentArea = document.getElementById('content-area'); if (contentArea) contentArea.style.display = 'none'; const ids = [ 'hero-section', 'stats-grid', 'risk-banner', 'green-section', 'yellow-section', 'red-section', 'table-section', 'details-section', 'sectors-section', 'summary-content' ]; ids.forEach(id => hideSection(id)); } function showLoading() { let overlay = document.getElementById('loading-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'loading-overlay'; overlay.innerHTML = '
    ⏳ 加载中...
    '; document.body.appendChild(overlay); } overlay.style.display = 'flex'; } function hideLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.style.display = 'none'; }