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 `
方向${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 `
收盘价${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.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 = `
交易计划
方向${dirText}
入场区间${formatPrice(plan.entry_low)} - ${formatPrice(plan.entry_high)}
止损${formatPrice(plan.stop_loss)}
目标1${formatPrice(plan.target1)}
目标2${formatPrice(plan.target2)}
触发条件${plan.trigger || '-'}
`;
}
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 = `
基本信息
收盘价${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.composite_score}
排名#${s.rank || '-'}
${planHtml}
评分明细
振幅评分${s.amplitude_score}
量能评分${s.volume_score}
趋势评分${s.trend_score}
活跃度${s.activity_score}
多周期趋势
60分钟${s.trend_60m != null ? s.trend_60m : '-'}
15分钟${s.trend_15m != null ? s.trend_15m : '-'}
5分钟${s.trend_5m != null ? s.trend_5m : '-'}
方向${directionTag(s.direction_tag)}
枢轴点位
${pivotTags.join('')}
`;
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 ``;
}
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';
}