You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
681 lines
28 KiB
681 lines
28 KiB
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();
|
|
});
|
|
}
|
|
|
|
// ==================== 生成复盘 ====================
|
|
|
|
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;
|
|
|
|
el.innerHTML = `
|
|
<div class="hero-title">
|
|
<h1>${meta.review_date} <span class="hero-weekday">${meta.week_day || ''}</span></h1>
|
|
</div>
|
|
<div class="hero-subtitle">${meta.data_basis || ''}</div>
|
|
<div class="hero-conclusion">${meta.core_conclusion || ''}</div>
|
|
`;
|
|
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) =>
|
|
`<span class="stat-top-item">${i + 1}. ${s.symbol} ${s.name || ''} <b>${s.composite_score}</b></span>`
|
|
).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 = `
|
|
<div class="stat-card">
|
|
<div class="stat-card-label">核心结论</div>
|
|
<div class="stat-card-value">${meta.core_conclusion || '-'}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-label">综合评分 Top 3</div>
|
|
<div class="stat-card-value stat-top-list">${top3Html || '暂无'}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-label">市场概览</div>
|
|
<div class="stat-card-value stat-overview">
|
|
<span class="stat-bull">多头 <b>${bullCount}</b></span>
|
|
<span class="stat-bear">空头 <b>${bearCount}</b></span>
|
|
<span class="stat-neutral">中性 <b>${neutralCount}</b></span>
|
|
<span class="stat-opp">机会 <b>${oppCount}</b></span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `<div class="risk-title">⚠️ 风险提示</div><div class="risk-list">暂无特别风险提示</div>`;
|
|
} else {
|
|
const items = warnings.map(w => `<li class="risk-item">${w}</li>`).join('');
|
|
el.innerHTML = `
|
|
<div class="risk-title">⚠️ 风险提示</div>
|
|
<ul class="risk-list">${items}</ul>
|
|
`;
|
|
}
|
|
showSection('risk-banner');
|
|
}
|
|
|
|
// ==================== 绿色机会区 ====================
|
|
|
|
function renderGreenSection(plans, scores) {
|
|
const el = document.getElementById('green-section');
|
|
if (!el) return;
|
|
|
|
if (!plans || plans.length === 0) {
|
|
el.innerHTML = `<div class="section-title green-title">🟢 交易机会(绿色区)</div><div class="empty-hint">暂无交易机会</div>`;
|
|
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' ? '做多' : '做空';
|
|
|
|
return `
|
|
<div class="opp-card green-card">
|
|
<div class="opp-card-header">
|
|
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${plan.symbol}','${plan.name || ''}')">${plan.symbol} ${plan.name || ''}</a>
|
|
<span class="score-badge">${plan.composite_score}</span>
|
|
</div>
|
|
<div class="opp-card-rows">
|
|
<div class="opp-row"><span class="opp-label">方向</span><span class="${dirClass}">${dirText}</span></div>
|
|
<div class="opp-row"><span class="opp-label">收盘价</span><span>${formatPrice(s.close_price)}</span></div>
|
|
<div class="opp-row"><span class="opp-label">涨跌幅</span>${formatChange(s.change_pct)}</div>
|
|
<div class="opp-row"><span class="opp-label">量比</span><span>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
|
|
</div>
|
|
<div class="score-bars">
|
|
<div class="score-bar-row"><span class="score-bar-label">振幅</span>${scoreBar(plan.amplitude_score)}</div>
|
|
<div class="score-bar-row"><span class="score-bar-label">量能</span>${scoreBar(plan.volume_score)}</div>
|
|
<div class="score-bar-row"><span class="score-bar-label">趋势</span>${scoreBar(plan.trend_score)}</div>
|
|
<div class="score-bar-row"><span class="score-bar-label">活跃度</span>${scoreBar(plan.activity_score)}</div>
|
|
</div>
|
|
<div class="plan-box">
|
|
<div class="plan-box-row"><span class="plan-box-label">入场区间</span><span class="plan-box-val">${formatPrice(plan.entry_low)} - ${formatPrice(plan.entry_high)}</span></div>
|
|
<div class="plan-box-row"><span class="plan-box-label">止损</span><span class="plan-box-val stop">${formatPrice(plan.stop_loss)}</span></div>
|
|
<div class="plan-box-row"><span class="plan-box-label">目标1</span><span class="plan-box-val target">${formatPrice(plan.target1)}</span></div>
|
|
<div class="plan-box-row"><span class="plan-box-label">目标2</span><span class="plan-box-val target">${formatPrice(plan.target2)}</span></div>
|
|
<div class="plan-box-row"><span class="plan-box-label">触发条件</span><span class="plan-box-val trigger">${plan.trigger || '-'}</span></div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title green-title">🟢 交易机会(绿色区)</div>
|
|
<div class="opp-grid">${cards}</div>
|
|
`;
|
|
showSection('green-section');
|
|
}
|
|
|
|
// ==================== 黄色关注区 ====================
|
|
|
|
function renderYellowSection(scores) {
|
|
const el = document.getElementById('yellow-section');
|
|
if (!el) return;
|
|
|
|
const yellowItems = (scores || []).filter(s => s.category === 'yellow');
|
|
|
|
if (yellowItems.length === 0) {
|
|
el.innerHTML = `<div class="section-title yellow-title">🟡 关注列表(黄色区)</div><div class="empty-hint">暂无关注品种</div>`;
|
|
showSection('yellow-section');
|
|
return;
|
|
}
|
|
|
|
const items = yellowItems.map(s => `
|
|
<div class="watch-item yellow-item">
|
|
<div class="watch-left">
|
|
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol} ${s.name || ''}</a>
|
|
${directionTag(s.direction_tag)}
|
|
</div>
|
|
<div class="watch-mid">
|
|
${scoreBar(s.composite_score)}
|
|
</div>
|
|
<div class="watch-right">
|
|
<span class="watch-score">${s.composite_score}</span>
|
|
${formatChange(s.change_pct)}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title yellow-title">🟡 关注列表(黄色区)</div>
|
|
<div class="watch-list">${items}</div>
|
|
`;
|
|
showSection('yellow-section');
|
|
}
|
|
|
|
// ==================== 红色风险区 ====================
|
|
|
|
function renderRedSection(scores) {
|
|
const el = document.getElementById('red-section');
|
|
if (!el) return;
|
|
|
|
const redItems = (scores || []).filter(s => s.category === 'red');
|
|
|
|
if (redItems.length === 0) {
|
|
el.innerHTML = `<div class="section-title red-title">🔴 规避列表(红色区)</div><div class="empty-hint">暂无规避品种</div>`;
|
|
showSection('red-section');
|
|
return;
|
|
}
|
|
|
|
const items = redItems.map(s => `
|
|
<div class="watch-item red-item">
|
|
<div class="watch-left">
|
|
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol} ${s.name || ''}</a>
|
|
${directionTag(s.direction_tag)}
|
|
</div>
|
|
<div class="watch-mid">
|
|
${scoreBar(s.composite_score)}
|
|
</div>
|
|
<div class="watch-right">
|
|
<span class="watch-score">${s.composite_score}</span>
|
|
${formatChange(s.change_pct)}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title red-title">🔴 规避列表(红色区)</div>
|
|
<div class="watch-list">${items}</div>
|
|
`;
|
|
showSection('red-section');
|
|
}
|
|
|
|
// ==================== 排名表格 ====================
|
|
|
|
function renderRankingTable(scores) {
|
|
const el = document.getElementById('ranking-table');
|
|
if (!el) return;
|
|
|
|
if (!scores || scores.length === 0) {
|
|
el.innerHTML = `<div class="section-title">📊 综合排名</div><div class="empty-hint">暂无排名数据</div>`;
|
|
showSection('ranking-table');
|
|
return;
|
|
}
|
|
|
|
const rows = scores.map((s, i) => {
|
|
const rank = s.rank || (i + 1);
|
|
const rankClass = rank <= 3 ? 'rank-top' : '';
|
|
return `
|
|
<tr>
|
|
<td class="${rankClass}">${rank}</td>
|
|
<td><a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol}</a></td>
|
|
<td>${s.name || ''}</td>
|
|
<td>${formatPrice(s.close_price)}</td>
|
|
<td><div class="table-score-bar-wrap">${scoreBar(s.composite_score)}<span class="table-score-num">${s.composite_score}</span></div></td>
|
|
<td>${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</td>
|
|
<td>${formatChange(s.change_pct)}</td>
|
|
<td>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</td>
|
|
<td>${s.trend_score != null ? s.trend_score : '-'}</td>
|
|
<td>${directionTag(s.direction_tag)}</td>
|
|
<td>${s.activity_score != null ? s.activity_score : '-'}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title">📊 综合排名</div>
|
|
<div class="table-wrap">
|
|
<table class="ranking-tbl">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>代码</th>
|
|
<th>名称</th>
|
|
<th>收盘价</th>
|
|
<th>综合评分</th>
|
|
<th>振幅%</th>
|
|
<th>涨跌幅</th>
|
|
<th>量比</th>
|
|
<th>趋势</th>
|
|
<th>方向</th>
|
|
<th>活跃度</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
showSection('ranking-table');
|
|
}
|
|
|
|
// ==================== 品种详情卡片 ====================
|
|
|
|
function renderDetails(scores) {
|
|
const el = document.getElementById('details-section');
|
|
if (!el) return;
|
|
|
|
if (!scores || scores.length === 0) {
|
|
el.innerHTML = `<div class="section-title">📋 品种详情</div><div class="empty-hint">暂无详情数据</div>`;
|
|
showSection('details-section');
|
|
return;
|
|
}
|
|
|
|
const cards = scores.map(s => {
|
|
const pivotTags = [];
|
|
if (s.pivot != null) pivotTags.push(`<span class="pivot-tag pivot">Pivot ${formatPrice(s.pivot)}</span>`);
|
|
if (s.r1 != null) pivotTags.push(`<span class="pivot-tag r1">R1 ${formatPrice(s.r1)}</span>`);
|
|
if (s.r2 != null) pivotTags.push(`<span class="pivot-tag r2">R2 ${formatPrice(s.r2)}</span>`);
|
|
if (s.s1 != null) pivotTags.push(`<span class="pivot-tag s1">S1 ${formatPrice(s.s1)}</span>`);
|
|
if (s.s2 != null) pivotTags.push(`<span class="pivot-tag s2">S2 ${formatPrice(s.s2)}</span>`);
|
|
|
|
return `
|
|
<div class="detail-card">
|
|
<div class="detail-card-header">
|
|
<span class="detail-symbol">${s.symbol} ${s.name || ''}</span>
|
|
<span class="detail-score">${s.composite_score}</span>
|
|
</div>
|
|
<div class="detail-metrics">
|
|
<div class="detail-metric"><span class="dm-label">收盘价</span><span class="dm-val">${formatPrice(s.close_price)}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">振幅</span><span class="dm-val">${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">涨跌幅</span><span class="dm-val">${formatChange(s.change_pct)}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">量比</span><span class="dm-val">${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">振幅评分</span><span class="dm-val">${s.amplitude_score != null ? s.amplitude_score : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">量能评分</span><span class="dm-val">${s.volume_score != null ? s.volume_score : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">趋势评分</span><span class="dm-val">${s.trend_score != null ? s.trend_score : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">活跃度</span><span class="dm-val">${s.activity_score != null ? s.activity_score : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">60m趋势</span><span class="dm-val">${s.trend_60m != null ? s.trend_60m : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">15m趋势</span><span class="dm-val">${s.trend_15m != null ? s.trend_15m : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">5m趋势</span><span class="dm-val">${s.trend_5m != null ? s.trend_5m : '-'}</span></div>
|
|
<div class="detail-metric"><span class="dm-label">方向</span><span class="dm-val">${directionTag(s.direction_tag)}</span></div>
|
|
</div>
|
|
<div class="detail-pivots">${pivotTags.join('')}</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title">📋 品种详情</div>
|
|
<div class="detail-grid">${cards}</div>
|
|
`;
|
|
showSection('details-section');
|
|
}
|
|
|
|
// ==================== 板块热力 ====================
|
|
|
|
function renderSectors(sectors) {
|
|
const el = document.getElementById('sectors-section');
|
|
if (!el) return;
|
|
|
|
if (!sectors || sectors.length === 0) {
|
|
el.innerHTML = `<div class="section-title">🔥 板块热力</div><div class="empty-hint">暂无板块数据</div>`;
|
|
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 =>
|
|
`<span class="sector-member">${m.symbol} ${m.name || ''} <b>${m.score}</b></span>`
|
|
).join('');
|
|
|
|
return `
|
|
<div class="sector-card heat-${heatLevel}">
|
|
<div class="sector-header">
|
|
<span class="sector-name">${sec.sector_name}</span>
|
|
<span class="sector-heat">${heatIndicator}</span>
|
|
</div>
|
|
<div class="sector-stats">
|
|
<div class="sector-stat"><span class="ss-label">均分</span><span class="ss-val">${sec.avg_score != null ? sec.avg_score.toFixed(1) : '-'}</span></div>
|
|
<div class="sector-stat"><span class="ss-label">方向</span><span class="ss-val">${sec.direction || '-'}</span></div>
|
|
<div class="sector-stat"><span class="ss-label">领涨</span><span class="ss-val">${sec.leader_symbol || '-'} ${sec.leader_score != null ? sec.leader_score.toFixed(1) : ''}</span></div>
|
|
</div>
|
|
<div class="sector-members">${members || '暂无成员'}</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="section-title">🔥 板块热力</div>
|
|
<div class="sector-grid">${cards}</div>
|
|
`;
|
|
showSection('sectors-section');
|
|
}
|
|
|
|
// ==================== 品种详情弹窗 ====================
|
|
|
|
function showDetail(symbol, name) {
|
|
const scores = currentPlanData ? currentPlanData.scores : [];
|
|
const s = scores.find(x => x.symbol === symbol);
|
|
if (!s) return;
|
|
|
|
const modal = document.getElementById('detail-modal');
|
|
if (!modal) return;
|
|
|
|
document.getElementById('modal-title').textContent = `${symbol} ${name || s.name || ''}`;
|
|
|
|
const body = document.getElementById('modal-body');
|
|
body.innerHTML = `
|
|
<div class="modal-section">
|
|
<div class="modal-section-title">基本信息</div>
|
|
<div class="modal-metrics-grid">
|
|
<div class="modal-metric"><span class="mm-label">收盘价</span><span class="mm-val">${formatPrice(s.close_price)}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">振幅</span><span class="mm-val">${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">涨跌幅</span><span class="mm-val">${formatChange(s.change_pct)}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">量比</span><span class="mm-val">${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">综合评分</span><span class="mm-val"><b>${s.composite_score}</b></span></div>
|
|
<div class="modal-metric"><span class="mm-label">排名</span><span class="mm-val">#${s.rank || '-'}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-section">
|
|
<div class="modal-section-title">评分明细</div>
|
|
<div class="modal-metrics-grid">
|
|
<div class="modal-metric"><span class="mm-label">振幅评分</span><span class="mm-val">${s.amplitude_score}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">量能评分</span><span class="mm-val">${s.volume_score}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">趋势评分</span><span class="mm-val">${s.trend_score}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">活跃度</span><span class="mm-val">${s.activity_score}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-section">
|
|
<div class="modal-section-title">多周期趋势</div>
|
|
<div class="modal-metrics-grid">
|
|
<div class="modal-metric"><span class="mm-label">60分钟</span><span class="mm-val">${s.trend_60m != null ? s.trend_60m : '-'}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">15分钟</span><span class="mm-val">${s.trend_15m != null ? s.trend_15m : '-'}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">5分钟</span><span class="mm-val">${s.trend_5m != null ? s.trend_5m : '-'}</span></div>
|
|
<div class="modal-metric"><span class="mm-label">方向</span><span class="mm-val">${directionTag(s.direction_tag)}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-section">
|
|
<div class="modal-section-title">枢轴点位</div>
|
|
<div class="modal-pivots">
|
|
${s.r2 != null ? `<span class="pivot-tag r2">R2 ${formatPrice(s.r2)}</span>` : ''}
|
|
${s.r1 != null ? `<span class="pivot-tag r1">R1 ${formatPrice(s.r1)}</span>` : ''}
|
|
${s.pivot != null ? `<span class="pivot-tag pivot">Pivot ${formatPrice(s.pivot)}</span>` : ''}
|
|
${s.s1 != null ? `<span class="pivot-tag s1">S1 ${formatPrice(s.s1)}</span>` : ''}
|
|
${s.s2 != null ? `<span class="pivot-tag s2">S2 ${formatPrice(s.s2)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 `<div class="score-bar"><div class="score-bar-fill" style="width:${pct}%;background:${color}"></div><span class="score-bar-text">${score}</span></div>`;
|
|
}
|
|
|
|
function directionTag(tag) {
|
|
if (!tag) return '<span class="dir-badge">-</span>';
|
|
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 `<span class="dir-badge ${cls}">${tag}</span>`;
|
|
}
|
|
|
|
function formatPrice(price) {
|
|
if (price == null || isNaN(price)) return '-';
|
|
return Number(price).toFixed(2);
|
|
}
|
|
|
|
function formatChange(pct) {
|
|
if (pct == null || isNaN(pct)) return '<span class="change-neutral">-</span>';
|
|
const val = Number(pct);
|
|
const sign = val > 0 ? '+' : '';
|
|
const cls = val > 0 ? 'change-up' : (val < 0 ? 'change-down' : 'change-neutral');
|
|
return `<span class="${cls}">${sign}${val.toFixed(2)}%</span>`;
|
|
}
|
|
|
|
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',
|
|
'ranking-table', '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 = '<div class="loading-spinner">⏳ 加载中...</div>';
|
|
document.body.appendChild(overlay);
|
|
}
|
|
overlay.style.display = 'flex';
|
|
}
|
|
|
|
function hideLoading() {
|
|
const overlay = document.getElementById('loading-overlay');
|
|
if (overlay) overlay.style.display = 'none';
|
|
}
|