fix: 修复AI分析卡片样式问题

master^2
Lxy 1 week ago
parent 41a2bec6e3
commit 7f9d55f288

@ -546,6 +546,35 @@ body {
overflow: hidden;
}
.futures-card.no-ai-data {
opacity: 0.85;
border-style: dashed;
}
.futures-card.no-ai-data:hover {
opacity: 1;
border-color: var(--purple, #8b5cf6);
}
.ai-hint {
text-align: center;
padding: 8px;
margin: 8px 0;
background: rgba(139, 92, 246, 0.08);
border: 1px dashed rgba(139, 92, 246, 0.3);
border-radius: 8px;
color: var(--purple, #8b5cf6);
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.ai-hint i {
font-size: 14px;
}
.futures-card::before {
content: '';
position: absolute;
@ -2521,6 +2550,63 @@ body.theme-minimal .watch-btn.active {
background: rgba(6, 182, 212, 0.1);
}
.card-ai-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: 1px solid var(--purple-soft, rgba(139, 92, 246, 0.3));
border-radius: 8px;
color: var(--purple, #8b5cf6);
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
}
.card-ai-btn:hover {
border-color: var(--purple, #8b5cf6);
background: rgba(139, 92, 246, 0.1);
}
.card-ai-btn.analyzing {
color: var(--amber);
border-color: var(--amber);
animation: pulse 1s infinite;
}
.ai-analyze-all-btn {
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--purple, #8b5cf6), #6d28d9);
border: none;
border-radius: 8px;
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.ai-analyze-all-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.ai-analyze-all-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
body.theme-minimal .card-refresh-btn {
border-radius: 9999px;
background: var(--bg-card);

@ -122,6 +122,10 @@
<i class="fas fa-sync-alt"></i>
<span>刷新全部</span>
</button>
<button class="ai-analyze-all-btn" id="ai-analyze-all-btn" title="AI分析全部品种">
<i class="fas fa-brain"></i>
<span>全部分析</span>
</button>
<div class="sort-select">
<select id="sort-select">
<option value="trend_score">趋势评分</option>

@ -73,6 +73,9 @@ function initEventListeners() {
// 刷新全部按钮
document.getElementById('refresh-all-btn').addEventListener('click', refreshAllSymbols);
// 全部AI分析按钮
document.getElementById('ai-analyze-all-btn').addEventListener('click', analyzeAllSymbols);
// 详情页刷新按钮
document.getElementById('refresh-symbol-btn').addEventListener('click', function() {
if (currentSymbol) {
@ -224,7 +227,10 @@ async function loadFuturesList() {
const response = await fetch(`${API_BASE}/list`);
const data = await response.json();
if (data.success) {
allFuturesData = data.data;
allFuturesData = data.data.map(item => ({
...item,
hasAIAnalysis: false // 默认没有AI分析数据
}));
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
}
@ -289,8 +295,10 @@ function renderFuturesGrid(data) {
grid.innerHTML = data.map(item => {
const isWatched = watchedSymbols.includes(item.symbol);
const hasAI = item.hasAIAnalysis;
return `
<div class="futures-card" onclick="showDetailView('${item.symbol}')">
<div class="futures-card ${!hasAI ? 'no-ai-data' : ''}" onclick="showDetailView('${item.symbol}')">
<div class="card-top">
<div class="card-symbol">
<div class="symbol-tag">${item.symbol.replace(/[0-9]/g, '').substring(0, 2)}</div>
@ -307,7 +315,7 @@ function renderFuturesGrid(data) {
</div>
</div>
</div>
<span class="suggestion-badge ${item.suggestionType}">${item.suggestion}</span>
<span class="suggestion-badge ${hasAI ? item.suggestionType : 'neutral'}" id="suggestion-${item.symbol}">${hasAI ? item.suggestion : '--'}</span>
<div class="card-metrics">
<div class="metric-item">
<span class="metric-label">成功率</span>
@ -320,21 +328,25 @@ function renderFuturesGrid(data) {
<span class="metric-value ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? '' : 'down'}">${item.trendScore}</span>
</div>
</div>
<div class="period-trends">
<span class="period-tag ${item.periods['5']}">5M</span>
<span class="period-tag ${item.periods['15']}">15M</span>
<span class="period-tag ${item.periods['30']}">30M</span>
<span class="period-tag ${item.periods['60']}">1H</span>
<div class="period-trends" id="period-trends-${item.symbol}">
<span class="period-tag ${hasAI ? (item.periods['5'] || 'neutral') : 'neutral'}" id="period-5-${item.symbol}">5M</span>
<span class="period-tag ${hasAI ? (item.periods['15'] || 'neutral') : 'neutral'}" id="period-15-${item.symbol}">15M</span>
<span class="period-tag ${hasAI ? (item.periods['30'] || 'neutral') : 'neutral'}" id="period-30-${item.symbol}">30M</span>
<span class="period-tag ${hasAI ? (item.periods['60'] || 'neutral') : 'neutral'}" id="period-60-${item.symbol}">1H</span>
</div>
${!hasAI ? `<div class="ai-hint"><i class="fas fa-info-circle"></i> 请先进行AI分析</div>` : ''}
<div class="card-footer">
<div class="key-levels">
<span><span class="label">压力</span> <span class="down">${formatNumber(item.resistance)}</span></span>
<span><span class="label">支撑</span> <span class="up">${formatNumber(item.support)}</span></span>
<span><span class="label">压力</span> <span class="down" id="resistance-${item.symbol}">${hasAI ? formatNumber(item.resistance) : '--'}</span></span>
<span><span class="label">支撑</span> <span class="up" id="support-${item.symbol}">${hasAI ? formatNumber(item.support) : '--'}</span></span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<button class="card-refresh-btn" onclick="event.stopPropagation(); refreshSingleSymbol('${item.symbol}', this)" title="刷新数据">
<i class="fas fa-sync-alt"></i>
</button>
<button class="card-ai-btn" onclick="event.stopPropagation(); analyzeSingleSymbol('${item.symbol}', '${item.name}', this)" title="AI分析">
<i class="fas fa-brain"></i>
</button>
<button class="watch-btn ${isWatched ? 'active' : ''}" onclick="toggleWatch('${item.symbol}', '${item.name}', event)" title="${isWatched ? '取消自选' : '加入自选'}">
<i class="fas fa-star"></i>
</button>
@ -1279,35 +1291,178 @@ async function refreshSingleSymbol(symbol, btnElement = null) {
showToast('info', '检查数据', `正在检查 ${symbol} 数据新鲜度...`);
try {
const response = await fetch(`${API_BASE}/refresh/${symbol}`, { method: 'POST' });
const data = await response.json();
// 刷新K线数据
await loadKlineData(currentSymbol, currentPeriod);
showToast('success', '刷新成功', `${symbol} 数据已更新`);
} catch (error) {
showToast('error', '刷新失败', error.message || '网络错误');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
if (data.success) {
if (data.refreshed) {
btn.innerHTML = '<i class="fas fa-check"></i>';
async function analyzeSingleSymbol(symbol, name, btnElement = null) {
let btn = btnElement;
if (!btn) {
try {
const evt = event;
if (evt && evt.target) {
btn = evt.target.closest('.card-ai-btn');
}
} catch (e) {}
}
await loadFuturesDetail(symbol);
await loadKlineData(symbol, currentPeriod);
await loadFuturesList();
if (!btn) {
showToast('error', '分析失败', '无法找到分析按钮');
return;
}
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.classList.add('analyzing');
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
showToast('info', 'AI分析中', `正在分析 ${symbol}...`);
showToast('success', '数据已更新', `${symbol} 最新数据已同步`);
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
const data = await response.json();
if (data.success && data.data) {
const result = data.data.result;
syncAIToSymbolCard(symbol, result);
showToast('success', '分析完成', `${symbol} AI分析已更新`);
} else {
btn.innerHTML = '<i class="fas fa-check"></i>';
showToast('success', '数据新鲜', `${symbol} 数据仍在有效期内,无需刷新`);
showToast('warning', '分析失败', data.error || 'AI分析失败');
}
setTimeout(() => {
} catch (error) {
console.error('AI分析失败:', error);
showToast('error', '分析失败', '网络错误,请稍后重试');
} finally {
btn.disabled = false;
btn.classList.remove('analyzing');
btn.innerHTML = originalContent;
}, 1000);
}
}
async function analyzeAllSymbols() {
const allBtn = document.getElementById('ai-analyze-all-btn');
if (!allBtn) return;
const originalContent = allBtn.innerHTML;
allBtn.disabled = true;
allBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 分析中...';
showToast('info', '批量分析', '开始对所有合约进行AI分析...');
const symbols = allFuturesData.map(item => item.symbol);
const batchSize = 3; // 每次并发分析3个合约
for (let i = 0; i < symbols.length; i += batchSize) {
const batch = symbols.slice(i, i + batchSize);
const promises = batch.map(async (symbol) => {
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
const data = await response.json();
if (data.success && data.data) {
syncAIToSymbolCard(symbol, data.data.result);
return { symbol, success: true };
} else {
showToast('error', '刷新失败', data.message || '请稍后重试');
btn.disabled = false;
btn.innerHTML = originalContent;
return { symbol, success: false, error: data.error };
}
} catch (error) {
console.error(`${symbol} 分析失败:`, error);
return { symbol, success: false, error: error.message };
}
});
const results = await Promise.all(promises);
const successCount = results.filter(r => r.success).length;
showToast('info', '批量分析进度', `已完成 ${Math.min(i + batchSize, symbols.length)}/${symbols.length} 个合约,本批成功 ${successCount}`);
// 每批之间等待2秒避免API限流
if (i + batchSize < symbols.length) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
showToast('success', '批量分析完成', `所有 ${symbols.length} 个合约AI分析已完成`);
allBtn.disabled = false;
allBtn.innerHTML = originalContent;
}
function syncAIToSymbolCard(symbol, result) {
// 标记该合约已有AI分析数据
const item = allFuturesData.find(d => d.symbol === symbol);
if (item) {
item.hasAIAnalysis = true;
}
// 移除无AI数据的样式
const card = document.querySelector(`.futures-card[onclick="showDetailView('${symbol}')"]`);
if (card) {
card.classList.remove('no-ai-data');
const hint = card.querySelector('.ai-hint');
if (hint) hint.remove();
}
const suggestion = result.trading_suggestion || {};
const fourDim = result.four_dimensional || {};
const pivotPoints = result.pivot_points || {};
// 1. 更新操作建议
const suggestionEl = document.getElementById(`suggestion-${symbol}`);
if (suggestionEl && suggestion.direction) {
suggestionEl.textContent = suggestion.direction;
suggestionEl.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`;
}
// 2. 更新压力支撑位
const resistanceEl = document.getElementById(`resistance-${symbol}`);
const supportEl = document.getElementById(`support-${symbol}`);
if (resistanceEl && pivotPoints.r1) resistanceEl.textContent = pivotPoints.r1;
if (supportEl && pivotPoints.s1) supportEl.textContent = pivotPoints.s1;
// 3. 更新多周期趋势
const periodNames = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
Object.entries(fourDim).forEach(([period, data]) => {
const periodNum = periodNames[period];
if (!periodNum) return;
const trendEl = document.getElementById(`period-${periodNum}-${symbol}`);
if (trendEl) {
const trend = data.conclusion || data.macd?.trend || 'neutral';
const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
trendEl.className = `period-tag ${trendClass}`;
}
});
}
async function refreshAllSymbols() {
const btn = document.getElementById('refresh-all-btn');
if (!btn) return;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 刷新中...';
showToast('info', '刷新中', '正在刷新所有合约数据...');
try {
const response = await fetch(`${API_BASE}/refresh-all`);
const data = await response.json();
if (data.success) {
showToast('success', '刷新成功', `已刷新 ${data.count} 个合约`);
loadFuturesData();
} else {
showToast('error', '刷新失败', data.error || '未知错误');
}
} catch (error) {
console.error('刷新失败:', error);
showToast('error', '刷新失败', '网络错误,请稍后重试');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}

Loading…
Cancel
Save