// API配置 const API_URL = 'api.php?action=fund_data'; const STATS_URL = 'api.php?action=get_stats'; const FUND_CHART_API = (code) => `api.php?action=get_fund_chart&fund_code=${code}`; // 全局数据存储 let fundData = null; let visitStats = null; // 自动刷新相关变量 let autoRefreshInterval = null; const AUTO_REFRESH_INTERVAL = 10000; // 10秒自动刷新一次 // 取消控制器,避免并发请求 let fundController = null; const cardCharts = new Map(); // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { // 应用已保存的主题(优先使用用户选择,其次匹配系统偏好) applySavedTheme(); // 添加动画样式 addAnimationStyles(); loadFundData(); loadVisitStats(); // 启动自动刷新 startAutoRefresh(); document.getElementById('refreshBtn').addEventListener('click', function() { loadFundData(); loadVisitStats(); }); document.getElementById('themeBtn').addEventListener('click', toggleTheme); document.getElementById('statsBtn').addEventListener('click', function() { toggleStats(); loadVisitStats(); // 刷新统计数据 }); }); /** * 启动自动刷新基金数据 */ function startAutoRefresh() { // 清除可能存在的旧定时器 if (autoRefreshInterval) { clearInterval(autoRefreshInterval); } // 添加防抖变量 let isLoading = false; // 设置新的定时器 autoRefreshInterval = setInterval(function() { // 如果上一个请求还在处理中,跳过本次请求 if (isLoading) { console.log('上一个请求未完成,跳过本次刷新...'); return; } console.log('自动刷新基金数据...'); isLoading = true; loadFundData(true).finally(() => { isLoading = false; }); }, AUTO_REFRESH_INTERVAL); } /** * 停止自动刷新 */ function stopAutoRefresh() { if (autoRefreshInterval) { clearInterval(autoRefreshInterval); autoRefreshInterval = null; } } /** * 更新页面元素的数值(不刷新整个UI) * @param {Object} newData - 新的基金数据 * @param {Object} oldData - 旧的基金数据 */ function updateElements(newData, oldData) { if (!oldData) return; const { summary: newSummary, channelStats: newChannelStats } = newData; const { summary: oldSummary, channelStats: oldChannelStats } = oldData; // 更新仪表盘统计数据 if (newSummary.total !== oldSummary.total) { const el = document.querySelector('[data-stat="total-funds"]'); if (el) { el.textContent = newSummary.total; el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); } } if (newSummary.upCount !== oldSummary.upCount) { const el = document.querySelector('[data-stat="up-funds"]'); if (el) { el.textContent = newSummary.upCount; el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); } } if (newSummary.downCount !== oldSummary.downCount) { const el = document.querySelector('[data-stat="down-funds"]'); if (el) { el.textContent = newSummary.downCount; el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); } } if (formatNumber(newSummary.avgChange) !== formatNumber(oldSummary.avgChange)) { const el = document.querySelector('[data-stat="avg-change"]'); if (el) { el.textContent = formatNumber(newSummary.avgChange) + '%'; el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); } } // 更新总盈亏 if (newSummary.formattedTotalProfitLoss !== oldSummary.formattedTotalProfitLoss) { const el = document.querySelector('[data-stat="total-profit"]'); if (el) { const profitClass = newSummary.profitLossClass; el.innerHTML = ` ${newSummary.profitLossIcon} ${newSummary.formattedTotalProfitLoss} ${newSummary.formattedProfitLossRate}% `; el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); } } // 更新渠道统计数据 for (const [channelName, newStats] of Object.entries(newChannelStats)) { const oldStats = oldChannelStats[channelName]; if (!oldStats) continue; // 更新平均涨跌 if (formatNumber(newStats.avgChange) !== formatNumber(oldStats.avgChange)) { const elements = document.querySelectorAll(`.channel-avg-change[data-channel="${channelName}"]`); elements.forEach(el => { el.textContent = formatNumber(newStats.avgChange) + '%'; // 高亮变化的元素 el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); }); } // 更新渠道盈亏 if (newStats.totalProfitLoss !== oldStats.totalProfitLoss) { const profitClass = newStats.totalProfitLoss > 0 ? 'profit-positive' : 'profit-negative'; const profitIcon = newStats.totalProfitLoss > 0 ? '📈' : '📉'; const elements = document.querySelectorAll(`.channel-profit-section[data-channel="${channelName}"]`); elements.forEach(el => { el.className = 'channel-profit-section ' + profitClass; el.textContent = `${profitIcon} 渠道盈亏: ${formatCurrency(newStats.totalProfitLoss)} (${formatNumber(newStats.profitLossRate)}%)`; // 高亮变化的元素 el.classList.add('highlight'); setTimeout(() => el.classList.remove('highlight'), 300); }); } } // 更新基金卡片数据 for (const [channelName, newStats] of Object.entries(newChannelStats)) { const oldStats = oldChannelStats[channelName]; if (!oldStats) continue; for (const [fundCode, newFund] of Object.entries(newStats.funds)) { const oldFund = oldStats.funds[fundCode]; if (!oldFund) continue; // 解析基本数据 const parsedUnitNetValue = parseFloat(newFund.unit_net_value || newFund.dwjz) || 1; const parsedCurrentNetValue = parseFloat(newFund.current_net_value || newFund.gsz) || 1; const parsedShares = parseFloat(newFund.shares || (newFund.investment / parsedUnitNetValue)) || 0; const todayProfitLoss = parsedShares * (parsedCurrentNetValue - parsedUnitNetValue); const oldParsedUnitNetValue = parseFloat(oldFund.unit_net_value || oldFund.dwjz) || 1; const oldParsedCurrentNetValue = parseFloat(oldFund.current_net_value || oldFund.gsz) || 1; const oldParsedShares = parseFloat(oldFund.shares || (oldFund.investment / oldParsedUnitNetValue)) || 0; const oldTodayProfitLoss = oldParsedShares * (oldParsedCurrentNetValue - oldParsedUnitNetValue); // 更新估算净值 if (newFund.gsz !== oldFund.gsz) { const gszEl = document.querySelector(`.est-net-value[data-fund="${fundCode}"]`); if (gszEl) { gszEl.textContent = newFund.gsz; gszEl.classList.add('highlight'); setTimeout(() => gszEl.classList.remove('highlight'), 300); } } // 更新涨跌幅 if (newFund.gszzl !== oldFund.gszzl) { const changeEl = document.querySelector(`.change-display[data-fund="${fundCode}"]`); if (changeEl) { const change = parseFloat(newFund.gszzl); const changeDisplayClass = change > 0 ? 'change-positive' : 'change-negative'; const changeIcon = change > 0 ? '📈' : (change < 0 ? '📉' : '➡️'); const changeDisplay = change > 0 ? '+' + newFund.gszzl + '%' : newFund.gszzl + '%'; changeEl.className = 'change-display ' + changeDisplayClass + ' highlight'; changeEl.innerHTML = `${changeDisplay}`; setTimeout(() => changeEl.classList.remove('highlight'), 300); } } // 更新今日盈亏 if (Math.abs(todayProfitLoss - oldTodayProfitLoss) > 0.01) { const todayProfitEl = document.querySelector(`.today-profit[data-fund="${fundCode}"]`); if (todayProfitEl) { const todayProfitClass = todayProfitLoss >= 0 ? 'profit-positive' : 'profit-negative'; const todayProfitIcon = todayProfitLoss >= 0 ? '📈' : '📉'; todayProfitEl.className = 'profit-value today-profit ' + todayProfitClass + ' highlight'; todayProfitEl.textContent = `${todayProfitIcon} ${formatCurrency(todayProfitLoss)}`; setTimeout(() => todayProfitEl.classList.remove('highlight'), 300); } } // 更新更新时间 if (newFund.gztime !== oldFund.gztime) { const timeEl = document.querySelector(`.update-time[data-fund="${fundCode}"]`); if (timeEl) { timeEl.innerHTML = ` ${newFund.gztime}`; } } } } } /** * 加载基金数据 * @param {boolean} isAutoRefresh - 是否为自动刷新模式 * @returns {Promise} - 返回Promise以便使用finally方法 */ async function loadFundData(isAutoRefresh = false) { try { // 只有手动刷新时才显示加载动画 if (!isAutoRefresh) { showLoading(); } // 若存在未完成的请求,则取消 if (fundController) { try { fundController.abort(); } catch (e) {} } fundController = new AbortController(); const response = await fetch(API_URL, { signal: fundController.signal }); const result = await response.json(); if (result.success) { // 保存旧数据用于比较 const oldData = { ...fundData }; // 更新数据 fundData = result.data; // 更新最后更新时间 document.getElementById('lastUpdate').innerHTML = ` 最后更新: ${fundData.timestamp} `; if (isAutoRefresh && oldData) { // 自动刷新时只更新变化的元素,不重新渲染整个页面 updateElements(fundData, oldData); // 检查是否有数据变化 const hasDataChanged = JSON.stringify(fundData) !== JSON.stringify(oldData); if (hasDataChanged) { showUpdateNotification(); } } else { // 首次加载或手动刷新时重新渲染整个页面 renderPage(); } } else { throw new Error('API返回错误'); } } catch (error) { // 忽略主动取消造成的错误 if (error && (error.name === 'AbortError' || error.message === 'The operation was aborted.')) { return; } console.error('加载基金数据失败:', error); // 只有手动刷新时才显示错误信息 if (!isAutoRefresh) { showError('数据加载失败,请稍后重试'); } // 重新抛出错误以便外部捕获 throw error; } } /** * 显示数据更新通知 */ function showUpdateNotification() { // 检查是否已存在通知元素 let notification = document.getElementById('updateNotification'); // 如果不存在,创建通知元素 if (!notification) { notification = document.createElement('div'); notification.id = 'updateNotification'; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background-color: #4CAF50; color: white; padding: 12px 20px; border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 1000; opacity: 0; transition: opacity 0.3s ease; font-size: 14px; `; document.body.appendChild(notification); } // 设置通知内容 notification.textContent = '基金数据已更新'; // 显示通知 notification.style.opacity = '1'; // 3秒后隐藏通知 setTimeout(() => { notification.style.opacity = '0'; }, 3000); // 添加数据变化的视觉反馈 highlightChangedElements(); } /** * 高亮显示变化的元素 */ function highlightChangedElements() { // 为关键数据元素添加高亮效果 const keyElements = document.querySelectorAll('.fund-item .fund-value, .fund-item .fund-change, .fund-item .fund-profit, .stat-card .stat-number'); keyElements.forEach(element => { // 保存原始样式 const originalBackground = element.style.backgroundColor; const originalTransition = element.style.transition; // 添加高亮样式 element.style.backgroundColor = '#fff3cd2d'; element.style.transition = 'background-color 0.5s ease'; // 1.5秒后恢复原始样式 setTimeout(() => { element.style.backgroundColor = originalBackground; element.style.transition = originalTransition; }, 1500); }); // 为上涨/下跌状态变化的元素添加特殊效果 const statusElements = document.querySelectorAll('.fund-item .fund-status'); statusElements.forEach(element => { // 添加闪烁动画 element.style.animation = 'pulse 1s ease-in-out 2'; // 动画结束后移除动画样式 setTimeout(() => { element.style.animation = ''; }, 2000); }); } // 添加动画样式 function addAnimationStyles() { // 检查是否已存在动画样式 if (document.getElementById('animationStyles')) return; const styleElement = document.createElement('style'); styleElement.id = 'animationStyles'; styleElement.textContent = ` @keyframes pulse { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.05); } 100% { opacity: 1; transform: scale(1); } } .highlight { animation: highlight 0.3s ease-in-out; } @keyframes highlight { 0% { background-color: transparent; } 50% { background-color: rgba(255, 255, 0, 0.03); } 100% { background-color: transparent; } } `; document.head.appendChild(styleElement); } // 加载访问统计 async function loadVisitStats() { try { const response = await fetch(STATS_URL); const result = await response.json(); if (result.success) { visitStats = result.data; updateStatsDisplay(); } } catch (error) { console.error('加载访问统计失败:', error); } } // 更新统计显示 function updateStatsDisplay() { if (!visitStats) return; // 更新页脚统计 document.getElementById('todayVisits').textContent = visitStats.today_visits; document.getElementById('totalVisits').textContent = visitStats.total_visits; document.getElementById('uniqueVisitors').textContent = visitStats.unique_visitors; // 更新统计面板 const statsGrid = document.getElementById('statsGrid'); const recentVisits = document.getElementById('recentVisits'); if (statsGrid) { statsGrid.innerHTML = `
暂无访问记录
'; } else { visitStats.recent_visits.forEach(visit => { visitsHTML += `正在加载基金数据...
${message}
${getChannelDescription(channelName)}