/** * 推荐基金页面JavaScript功能 */ // 配置常量 const API_TIMEOUT = 5000; // API 请求超时时间(毫秒) const MAX_RETRY_COUNT = 3; // 最大重试次数 const RETRY_DELAY = 1000; // 重试延迟(毫秒) const BATCH_SIZE = 10; // 批量请求数量 const UPDATE_INTERVAL = 60000; // 自动更新间隔(毫秒) const HIGHLIGHT_DURATION = 300; // 高亮效果持续时间 // 全局状态 let updateIntervalId = null; let isLoading = false; let fundDataCache = {}; // 缓存基金数据 let lastData = {}; // 上一次的数据,用于比较变化 let errorCount = 0; const MAX_ERROR_COUNT = 5; // 连续错误次数上限 /** * 格式化数字(保留两位小数) * @param {number} num - 要格式化的数字 * @returns {string} 格式化后的字符串 */ function formatNumber(num) { if (typeof num !== 'number' || isNaN(num)) { return '--'; } return num.toFixed(2); } /** * 显示加载状态 * @param {boolean} show - 是否显示 */ function showLoading(show) { const loadingElement = document.getElementById('loading-indicator'); if (!loadingElement && show) { // 创建加载指示器 const div = document.createElement('div'); div.id = 'loading-indicator'; div.className = 'loading-overlay'; div.innerHTML = `
加载基金数据中...
`; document.body.appendChild(div); } else if (loadingElement) { loadingElement.style.display = show ? 'flex' : 'none'; } } /** * 清理JSONP请求的资源 * @param {string} scriptId - script标签ID * @param {string} callbackName - 回调函数名 */ function cleanupResources(scriptId, callbackName) { // 清理script标签 const script = document.getElementById(scriptId); if (script && script.parentNode) { script.parentNode.removeChild(script); } // 清理回调函数 if (window[callbackName]) { delete window[callbackName]; } } /** * 处理API错误 * @param {string} errorMessage - 错误消息 */ function handleApiError(errorMessage) { errorCount++; console.error(errorMessage); // 如果连续错误次数过多,显示错误提示 if (errorCount >= MAX_ERROR_COUNT) { updateErrorDisplay('获取基金数据时遇到问题,请稍后再试', true); } } /** * 获取单个基金数据(使用 JSONP 方式,支持预加载) * @param {string} fundCode - 基金代码 * @param {number} retryCount - 当前重试次数 * @returns {Promise} 基金数据对象 */ function fetchFundData(fundCode, retryCount = 0) { // 清理基金代码,确保只包含数字和字母 const cleanFundCode = String(fundCode).replace(/[^\dA-Za-z]/g, '').trim(); if (!cleanFundCode) { console.error('无效的基金代码:', fundCode); return Promise.resolve({ success: false, error: '无效的基金代码', fundCode: fundCode }); } // 1. 检查是否有预加载数据 if (window.preloadedFundData && window.preloadedFundData[cleanFundCode]) { console.log(`使用预加载数据: ${cleanFundCode}`); return Promise.resolve({ success: true, data: window.preloadedFundData[cleanFundCode], fromCache: true }); } // 2. 检查缓存 const cachedData = fundDataCache[cleanFundCode]; if (cachedData && (Date.now() - cachedData.timestamp) < 30000) { // 30秒缓存 return Promise.resolve({ success: true, data: cachedData.data, fromCache: true }); } return new Promise((resolve, reject) => { // 设置超时 const timeoutId = setTimeout(() => { reject(new Error('请求超时')); }, API_TIMEOUT); // 创建唯一的回调函数名 const callbackName = `jsonp_callback_${cleanFundCode}_${Date.now()}`; const scriptId = `jsonp_script_${cleanFundCode}`; // 定义全局回调函数 window[callbackName] = function(data) { clearTimeout(timeoutId); // 检查数据有效性 if (!data || !data.fundcode || data.fundcode !== cleanFundCode) { cleanupResources(scriptId, callbackName); handleApiError(`基金${cleanFundCode}数据格式错误`); resolve({ success: false, error: '数据格式错误', fundCode: cleanFundCode }); return; } // 缓存数据 fundDataCache[cleanFundCode] = { data: data, timestamp: Date.now() }; // 重置错误计数 errorCount = 0; // 清理资源并返回结果 cleanupResources(scriptId, callbackName); resolve({ success: true, data: data }); }; // 创建脚本元素 const script = document.createElement('script'); script.id = scriptId; script.src = `http://fundgz.1234567.com.cn/js/${cleanFundCode}.js?callback=${callbackName}`; script.onerror = function() { clearTimeout(timeoutId); cleanupResources(scriptId, callbackName); reject(new Error('脚本加载失败')); }; // 添加到文档中 document.body.appendChild(script); }).catch(error => { console.error(`获取基金 ${cleanFundCode} 数据失败:`, error.message); // 重试逻辑 if (retryCount < MAX_RETRY_COUNT) { console.log(`尝试重试 ${cleanFundCode}, 第 ${retryCount + 1} 次`); return new Promise(resolve => { setTimeout(() => { resolve(fetchFundData(cleanFundCode, retryCount + 1)); }, RETRY_DELAY * (retryCount + 1)); }); } handleApiError(`获取基金 ${cleanFundCode} 数据失败`); return { success: false, error: error.message, fundCode: cleanFundCode }; }); } /** * 批量获取基金数据 * @param {Array} fundCodes - 基金代码数组 * @returns {Promise} 基金数据结果数组 */ async function fetchFundsDataInBatch(fundCodes) { // 分批处理,避免一次请求过多 const batches = []; for (let i = 0; i < fundCodes.length; i += BATCH_SIZE) { batches.push(fundCodes.slice(i, i + BATCH_SIZE)); } const allResults = []; // 逐批处理 for (const batch of batches) { // 使用Promise.allSettled确保所有请求都完成 const batchPromises = batch.map(code => fetchFundData(code)); const batchResults = await Promise.allSettled(batchPromises); batchResults.forEach((result, index) => { if (result.status === 'fulfilled') { allResults.push(result.value); } else { console.error(`处理批处理中基金 ${batch[index]} 时出错:`, result.reason); allResults.push({ success: false, error: result.reason?.message || '未知错误', fundCode: batch[index] }); } }); // 批处理之间添加短暂延迟,避免请求过于密集 if (batches.indexOf(batch) < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } return allResults; } /** * 更新基金错误状态显示 * @param {string} fundCode - 基金代码 */ function updateFundError(fundCode) { // 更新基金名称显示 document.querySelectorAll(`.fund-name[data-fund-code="${fundCode}"]`).forEach(el => { if (el.textContent === '加载中...') { el.textContent = `${fundCode} (数据获取失败)`; el.classList.add('error-text'); } }); // 更新涨幅显示 document.querySelectorAll(`.fund-change[data-fund-code="${fundCode}"]`).forEach(el => { el.textContent = '--'; el.classList.add('error'); }); } /** * 加载所有基金信息 */ async function loadAllFundInfo() { if (isLoading) return; isLoading = true; showLoading(true); try { // 保存旧数据用于比较 lastData = {}; for (const fundCode in fundDataCache) { lastData[fundCode] = fundDataCache[fundCode].data; } // 获取所有需要的基金代码 const fundElements = document.querySelectorAll('[data-fund-code]'); const fundCodes = Array.from(new Set(Array.from(fundElements).map(el => { const code = el.getAttribute('data-fund-code'); // 清理基金代码 return String(code || '').replace(/[^\dA-Za-z]/g, '').trim(); }))).filter(code => code.length > 0); // 过滤空代码 if (fundCodes.length === 0) { console.log('没有找到需要加载的基金'); return; } console.log(`开始加载 ${fundCodes.length} 支基金数据`, fundCodes); // 批量获取基金数据 const results = await fetchFundsDataInBatch(fundCodes); // 更新UI let successCount = 0; results.forEach(result => { if (result.success && result.data) { updateFundDisplay(result.data); successCount++; } else { // 对于失败的请求,尝试更新为错误状态 if (result.fundCode) { updateFundError(result.fundCode); } } }); // 更新IP组的总涨幅 updateIpTotalChanges(); console.log(`基金数据加载完成: 成功${successCount}/${fundCodes.length}`); } catch (error) { console.error('加载基金信息时发生错误:', error); updateErrorDisplay('加载基金数据失败,请稍后重试', true); } finally { isLoading = false; showLoading(false); } } /** * 更新单个基金的显示信息 * @param {Object} fundData - 基金数据 */ function updateFundDisplay(fundData) { if (!fundData || !fundData.fundcode) return; const fundCode = fundData.fundcode; const name = fundData.name || '未知名称'; const gszzl = parseFloat(fundData.gszzl) || 0; // 检查是否有数据变化 const oldData = lastData[fundCode]; const hasChanged = !oldData || oldData.gszzl !== fundData.gszzl; // 更新基金名称 document.querySelectorAll(`.fund-name[data-fund-code="${fundCode}"]`).forEach(el => { el.textContent = name; }); // 更新基金涨幅 document.querySelectorAll(`.fund-change[data-fund-code="${fundCode}"]`).forEach(el => { const oldValue = el.textContent; el.textContent = `${formatNumber(gszzl)}%`; // 根据涨跌设置颜色 el.classList.remove('positive', 'negative', 'neutral', 'highlight'); if (gszzl > 0) { el.classList.add('positive'); } else if (gszzl < 0) { el.classList.add('negative'); } else { el.classList.add('neutral'); } // 如果有变化,添加高亮效果 if (hasChanged) { el.classList.add('highlight'); setTimeout(() => { el.classList.remove('highlight'); }, HIGHLIGHT_DURATION); } }); } /** * 更新IP组的总涨幅 */ function updateIpTotalChanges() { // 获取所有IP分组 const ipGroups = document.querySelectorAll('[id^="ip-"]'); ipGroups.forEach(group => { const ipId = group.id; const changeElements = group.querySelectorAll('.fund-change'); let totalChange = 0; let validCount = 0; changeElements.forEach(el => { const text = el.textContent.trim(); if (text !== '--' && text !== '') { const change = parseFloat(text.replace('%', '')); if (!isNaN(change)) { totalChange += change; validCount++; } } }); // 计算平均涨幅 const avgChange = validCount > 0 ? totalChange / validCount : 0; // 更新显示 const totalChangeElement = document.querySelector(`#total-change-${ipId.split('-')[1]}`); if (totalChangeElement) { const oldValue = totalChangeElement.textContent; const newValue = `${formatNumber(avgChange)}%`; totalChangeElement.textContent = newValue; // 如果有变化,添加高亮效果 if (oldValue !== newValue) { totalChangeElement.classList.add('highlight'); setTimeout(() => { totalChangeElement.classList.remove('highlight'); }, HIGHLIGHT_DURATION); } // 根据涨跌设置颜色 totalChangeElement.classList.remove('positive', 'negative', 'neutral'); if (avgChange > 0) { totalChangeElement.classList.add('positive'); } else if (avgChange < 0) { totalChangeElement.classList.add('negative'); } else { totalChangeElement.classList.add('neutral'); } // 将平均涨幅写入分组容器的data属性,便于排序 const dropdownItem = document.getElementById(ipId)?.closest('.dropdown-item'); if (dropdownItem) { dropdownItem.dataset.avgChange = String(avgChange); } } }); } /** * 更新错误显示 * @param {string} message - 错误信息 * @param {boolean} show - 是否显示错误 */ function updateErrorDisplay(message, show = true) { let errorElement = document.getElementById('api-error'); if (!errorElement) { errorElement = document.createElement('div'); errorElement.id = 'api-error'; errorElement.className = 'error-message'; document.body.prepend(errorElement); } if (show) { errorElement.textContent = message; errorElement.style.display = 'block'; // 5秒后自动隐藏错误信息 setTimeout(() => { errorElement.style.display = 'none'; }, 5000); } else { errorElement.style.display = 'none'; } } /** * 切换下拉菜单显示/隐藏 * @param {string} id - 要切换的元素ID */ function toggleDropdown(id) { const element = document.getElementById(id); if (element) { element.classList.toggle('show'); // 切换箭头方向 const header = element.previousElementSibling; if (header) { const arrow = header.querySelector('.dropdown-arrow i'); if (arrow) { arrow.classList.toggle('fa-chevron-down'); arrow.classList.toggle('fa-chevron-up'); } } } } /** * 初始化函数 */ function init() { // 初始加载基金数据 loadAllFundInfo(); // 设置自动更新 updateIntervalId = setInterval(loadAllFundInfo, UPDATE_INTERVAL); // 清理函数 window.addEventListener('beforeunload', cleanup); // 为所有下拉菜单添加点击事件委托 document.addEventListener('click', function(e) { // 检查是否点击了下拉菜单标题 const dropdownHeader = e.target.closest('.dropdown-header'); if (dropdownHeader) { // 获取父级dropdown-item元素 const dropdownItem = dropdownHeader.closest('.dropdown-item'); if (dropdownItem) { // 关闭所有其他下拉菜单 document.querySelectorAll('.dropdown-item').forEach(item => { if (item !== dropdownItem) { item.classList.remove('open'); const arrow = item.querySelector('.dropdown-arrow i'); if (arrow) { arrow.classList.remove('fa-chevron-up'); arrow.classList.add('fa-chevron-down'); } } }); // 切换当前下拉菜单 dropdownItem.classList.toggle('open'); // 切换箭头方向 const arrow = dropdownHeader.querySelector('.dropdown-arrow i'); if (arrow) { arrow.classList.toggle('fa-chevron-down'); arrow.classList.toggle('fa-chevron-up'); } } } else { // 点击其他地方,关闭所有下拉菜单 document.querySelectorAll('.dropdown-item').forEach(item => { item.classList.remove('open'); const arrow = item.querySelector('.dropdown-arrow i'); if (arrow) { arrow.classList.remove('fa-chevron-up'); arrow.classList.add('fa-chevron-down'); } }); } }); // 初始化排序工具条 initSortToolbar(); } /** * 清理函数 */ function cleanup() { if (updateIntervalId) { clearInterval(updateIntervalId); updateIntervalId = null; } // 清理所有可能的JSONP回调函数 Object.keys(window).forEach(key => { if (key.startsWith('jsonp_callback_')) { delete window[key]; } }); // 清理所有JSONP脚本标签 document.querySelectorAll('script[id^="jsonp_script_"]').forEach(script => { if (script.parentNode) { script.parentNode.removeChild(script); } }); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } /** * 初始化排序工具条 */ function initSortToolbar() { const container = document.querySelector('.dropdown-container'); const buttons = document.querySelectorAll('.sort-btn'); const resetBtn = document.querySelector('.sort-reset'); if (!container || buttons.length === 0) return; const applyActive = (activeBtn) => { buttons.forEach(btn => btn.classList.toggle('active', btn === activeBtn)); // 更新箭头指示符 buttons.forEach(btn => { const indicator = btn.querySelector('.order-indicator'); if (indicator) { indicator.textContent = btn.dataset.order === 'asc' ? '↑' : '↓'; } }); }; const sortIpGroups = (key, order) => { const items = Array.from(container.querySelectorAll('.dropdown-item')); const getVal = (el) => { switch (key) { case 'time': { const t = el.dataset.latestTime || ''; // 兼容 YYYY-MM-DD HH:mm:ss const timeNum = Date.parse(t.replace(/-/g, '/')) || 0; return timeNum; } case 'count': return parseInt(el.dataset.fundCount || '0', 10); case 'avg': return parseFloat(el.dataset.avgChange || '0'); default: return 0; } }; items.sort((a, b) => { const av = getVal(a); const bv = getVal(b); if (av === bv) return 0; return order === 'asc' ? (av - bv) : (bv - av); }); // 重新插入排序后的元素 items.forEach(el => container.appendChild(el)); }; // 绑定按钮事件 buttons.forEach(btn => { btn.addEventListener('click', () => { const sortKey = btn.dataset.sort; const currentOrder = btn.dataset.order || 'desc'; // 如果重复点击同一按钮,切换升降序 const newOrder = (btn.classList.contains('active') && currentOrder === 'desc') ? 'asc' : 'desc'; btn.dataset.order = newOrder; applyActive(btn); sortIpGroups(sortKey, newOrder); }); }); // 重置排序:恢复时间倒序 if (resetBtn) { resetBtn.addEventListener('click', () => { // 所有按钮恢复箭头为倒序 buttons.forEach(b => { b.dataset.order = 'desc'; }); const defaultBtn = document.querySelector('.sort-btn[data-sort="time"]'); if (defaultBtn) { defaultBtn.dataset.order = 'desc'; applyActive(defaultBtn); sortIpGroups('time', 'desc'); } }); } // 默认执行一次“按时间倒序”排序,保持与服务端一致 const defaultBtn = document.querySelector('.sort-btn[data-sort="time"]'); if (defaultBtn) { applyActive(defaultBtn); sortIpGroups('time', defaultBtn.dataset.order || 'desc'); } }