Files
jj2/recommend_fund.js
2025-12-12 11:54:44 +08:00

658 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 推荐基金页面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 = `
<div class="loading-spinner">
<i class="fas fa-circle-notch fa-spin"></i>
<span>加载基金数据中...</span>
</div>
`;
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<Object>} 基金数据对象
*/
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<string>} fundCodes - 基金代码数组
* @returns {Promise<Array>} 基金数据结果数组
*/
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');
}
}