423 lines
11 KiB
JavaScript
423 lines
11 KiB
JavaScript
/**
|
||
* AI 智能选股大师 主要JavaScript功能
|
||
*/
|
||
|
||
$(document).ready(function() {
|
||
// 初始化组件
|
||
initializeComponents();
|
||
|
||
// 绑定事件
|
||
bindEvents();
|
||
|
||
// 自动刷新
|
||
setupAutoRefresh();
|
||
});
|
||
|
||
/**
|
||
* 初始化组件
|
||
*/
|
||
function initializeComponents() {
|
||
// 初始化工具提示
|
||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||
|
||
// 初始化弹出框
|
||
$('[data-bs-toggle="popover"]').popover();
|
||
|
||
// 表格排序
|
||
initializeTableSorting();
|
||
|
||
// 数字格式化
|
||
formatNumbers();
|
||
}
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
function bindEvents() {
|
||
// 表格行点击事件
|
||
$('.table tbody tr').on('click', function() {
|
||
$(this).toggleClass('table-active');
|
||
});
|
||
|
||
// 筛选表单自动提交
|
||
$('.auto-submit').on('change', function() {
|
||
$(this).closest('form').submit();
|
||
});
|
||
|
||
// 复制股票代码
|
||
$('.stock-code').on('click', function() {
|
||
const stockCode = $(this).text().trim();
|
||
copyToClipboard(stockCode);
|
||
showToast('股票代码已复制: ' + stockCode);
|
||
});
|
||
|
||
// 全选/取消全选
|
||
$('#selectAll').on('change', function() {
|
||
$('.row-select').prop('checked', this.checked);
|
||
});
|
||
|
||
// 导出数据
|
||
$('.export-btn').on('click', function() {
|
||
const format = $(this).data('format');
|
||
exportData(format);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 设置自动刷新
|
||
*/
|
||
function setupAutoRefresh() {
|
||
// 每5分钟自动刷新数据
|
||
setInterval(function() {
|
||
if (document.visibilityState === 'visible') {
|
||
refreshData();
|
||
}
|
||
}, 300000); // 5分钟
|
||
|
||
// 页面可见时刷新
|
||
document.addEventListener('visibilitychange', function() {
|
||
if (document.visibilityState === 'visible') {
|
||
const lastRefresh = localStorage.getItem('lastRefresh');
|
||
const now = Date.now();
|
||
|
||
// 如果超过5分钟未刷新,则自动刷新
|
||
if (!lastRefresh || (now - parseInt(lastRefresh)) > 300000) {
|
||
refreshData();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 刷新数据
|
||
*/
|
||
function refreshData() {
|
||
// 显示加载状态
|
||
showLoading();
|
||
|
||
// 记录刷新时间
|
||
localStorage.setItem('lastRefresh', Date.now().toString());
|
||
|
||
// 刷新页面数据
|
||
if (typeof updatePageData === 'function') {
|
||
updatePageData();
|
||
} else {
|
||
// 默认重新加载页面
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示加载状态
|
||
*/
|
||
function showLoading() {
|
||
const loadingHtml = '<div class="loading-overlay"><div class="loading"></div></div>';
|
||
$('body').append(loadingHtml);
|
||
|
||
setTimeout(() => {
|
||
$('.loading-overlay').remove();
|
||
}, 2000);
|
||
}
|
||
|
||
/**
|
||
* 显示提示消息
|
||
*/
|
||
function showToast(message, type = 'success') {
|
||
const toastHtml = `
|
||
<div class="toast align-items-center text-white bg-${type} border-0" role="alert">
|
||
<div class="d-flex">
|
||
<div class="toast-body">${message}</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 创建toast容器(如果不存在)
|
||
if (!$('.toast-container').length) {
|
||
$('body').append('<div class="toast-container position-fixed top-0 end-0 p-3"></div>');
|
||
}
|
||
|
||
const $toast = $(toastHtml);
|
||
$('.toast-container').append($toast);
|
||
|
||
// 显示toast
|
||
const toast = new bootstrap.Toast($toast[0]);
|
||
toast.show();
|
||
|
||
// 自动移除
|
||
$toast.on('hidden.bs.toast', function() {
|
||
$(this).remove();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 复制到剪贴板
|
||
*/
|
||
function copyToClipboard(text) {
|
||
if (navigator.clipboard) {
|
||
navigator.clipboard.writeText(text);
|
||
} else {
|
||
// 备用方法
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化表格排序
|
||
*/
|
||
function initializeTableSorting() {
|
||
$('.sortable th').on('click', function() {
|
||
const table = $(this).closest('table');
|
||
const column = $(this).index();
|
||
const order = $(this).hasClass('asc') ? 'desc' : 'asc';
|
||
|
||
// 移除其他列的排序样式
|
||
$(this).siblings().removeClass('asc desc');
|
||
|
||
// 添加当前列的排序样式
|
||
$(this).removeClass('asc desc').addClass(order);
|
||
|
||
// 排序表格行
|
||
sortTable(table, column, order);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 表格排序
|
||
*/
|
||
function sortTable(table, column, order) {
|
||
const tbody = table.find('tbody');
|
||
const rows = tbody.find('tr').toArray();
|
||
|
||
rows.sort(function(a, b) {
|
||
const aVal = $(a).find('td').eq(column).text().trim();
|
||
const bVal = $(b).find('td').eq(column).text().trim();
|
||
|
||
// 尝试数字比较
|
||
if (!isNaN(aVal) && !isNaN(bVal)) {
|
||
return order === 'asc' ? aVal - bVal : bVal - aVal;
|
||
}
|
||
|
||
// 字符串比较
|
||
return order === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||
});
|
||
|
||
tbody.empty().append(rows);
|
||
}
|
||
|
||
/**
|
||
* 数字格式化
|
||
*/
|
||
function formatNumbers() {
|
||
$('.format-number').each(function() {
|
||
const value = parseFloat($(this).text());
|
||
if (!isNaN(value)) {
|
||
$(this).text(value.toLocaleString());
|
||
}
|
||
});
|
||
|
||
$('.format-percentage').each(function() {
|
||
const value = parseFloat($(this).text());
|
||
if (!isNaN(value)) {
|
||
$(this).text(value.toFixed(2) + '%');
|
||
}
|
||
});
|
||
|
||
$('.format-currency').each(function() {
|
||
const value = parseFloat($(this).text());
|
||
if (!isNaN(value)) {
|
||
$(this).text('¥' + value.toFixed(2));
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 时间格式化 - 转换为东八区时间
|
||
*/
|
||
function formatDatetimeToChina(datetimeStr, format = '%m-%d %H:%M') {
|
||
if (!datetimeStr) return '';
|
||
|
||
let date;
|
||
try {
|
||
// 解析时间字符串
|
||
if (datetimeStr.endsWith('Z')) {
|
||
// UTC时间
|
||
date = new Date(datetimeStr);
|
||
} else if (datetimeStr.includes('T') && !datetimeStr.includes('+')) {
|
||
// 假设是UTC时间
|
||
date = new Date(datetimeStr + 'Z');
|
||
} else {
|
||
date = new Date(datetimeStr);
|
||
}
|
||
|
||
// 转换为东八区时间
|
||
const chinaTime = new Date(date.getTime() + (8 * 60 * 60 * 1000) - (date.getTimezoneOffset() * 60 * 1000));
|
||
|
||
// 格式化显示
|
||
const year = chinaTime.getFullYear();
|
||
const month = String(chinaTime.getMonth() + 1).padStart(2, '0');
|
||
const day = String(chinaTime.getDate()).padStart(2, '0');
|
||
const hour = String(chinaTime.getHours()).padStart(2, '0');
|
||
const minute = String(chinaTime.getMinutes()).padStart(2, '0');
|
||
|
||
// 根据格式返回
|
||
if (format === '%Y-%m-%d') {
|
||
return `${year}-${month}-${day}`;
|
||
} else if (format === '%m-%d %H:%M') {
|
||
return `${month}-${day} ${hour}:${minute}`;
|
||
} else {
|
||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||
}
|
||
} catch (e) {
|
||
console.error('时间格式化错误:', e);
|
||
return datetimeStr;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导出数据
|
||
*/
|
||
function exportData(format) {
|
||
const table = $('.table').first();
|
||
if (!table.length) {
|
||
showToast('没有可导出的数据', 'warning');
|
||
return;
|
||
}
|
||
|
||
switch (format) {
|
||
case 'csv':
|
||
exportToCSV(table);
|
||
break;
|
||
case 'excel':
|
||
exportToExcel(table);
|
||
break;
|
||
case 'json':
|
||
exportToJSON(table);
|
||
break;
|
||
default:
|
||
showToast('不支持的导出格式', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导出为CSV
|
||
*/
|
||
function exportToCSV(table) {
|
||
let csv = '';
|
||
|
||
// 表头
|
||
table.find('thead tr').each(function() {
|
||
const row = [];
|
||
$(this).find('th').each(function() {
|
||
row.push('"' + $(this).text().trim() + '"');
|
||
});
|
||
csv += row.join(',') + '\n';
|
||
});
|
||
|
||
// 数据行
|
||
table.find('tbody tr').each(function() {
|
||
const row = [];
|
||
$(this).find('td').each(function() {
|
||
row.push('"' + $(this).text().trim() + '"');
|
||
});
|
||
csv += row.join(',') + '\n';
|
||
});
|
||
|
||
// 下载文件
|
||
downloadFile(csv, 'trading_signals.csv', 'text/csv');
|
||
}
|
||
|
||
/**
|
||
* 下载文件
|
||
*/
|
||
function downloadFile(content, filename, contentType) {
|
||
const blob = new Blob([content], { type: contentType });
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
|
||
/**
|
||
* 获取API数据
|
||
*/
|
||
function apiRequest(endpoint, params = {}) {
|
||
return $.ajax({
|
||
url: '/api/' + endpoint,
|
||
method: 'GET',
|
||
data: params,
|
||
dataType: 'json'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 实时更新信号数据
|
||
*/
|
||
function updateSignals() {
|
||
apiRequest('signals', { limit: 10 })
|
||
.done(function(response) {
|
||
if (response.success && response.data.length > 0) {
|
||
updateSignalTable(response.data);
|
||
showToast('信号数据已更新');
|
||
}
|
||
})
|
||
.fail(function() {
|
||
showToast('更新信号数据失败', 'error');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新信号表格
|
||
*/
|
||
function updateSignalTable(signals) {
|
||
const tbody = $('.signals-table tbody');
|
||
tbody.empty();
|
||
|
||
signals.forEach(function(signal) {
|
||
const scanTime = signal.scan_time ? formatDatetimeToChina(signal.scan_time, '%m-%d %H:%M') : '';
|
||
const signalDate = signal.signal_date ? formatDatetimeToChina(signal.signal_date, '%Y-%m-%d') : '';
|
||
|
||
const row = `
|
||
<tr>
|
||
<td><span class="badge bg-secondary">${signal.stock_code}</span></td>
|
||
<td class="fw-bold">${signal.stock_name || '未知'}</td>
|
||
<td><span class="badge bg-info">${signal.strategy_name}</span></td>
|
||
<td>${signal.timeframe}</td>
|
||
<td>${signalDate}</td>
|
||
<td class="text-success fw-bold">${signal.breakout_price.toFixed(2)}元</td>
|
||
<td>${signal.yin_high.toFixed(2)}元</td>
|
||
<td><span class="badge bg-success">${signal.breakout_pct.toFixed(2)}%</span></td>
|
||
<td>${signal.final_yang_entity_ratio.toFixed(2)}%</td>
|
||
<td>${signal.turnover_ratio.toFixed(2)}%</td>
|
||
<td class="text-muted small">${scanTime}</td>
|
||
</tr>
|
||
`;
|
||
tbody.append(row);
|
||
});
|
||
}
|
||
|
||
// 全局错误处理
|
||
window.addEventListener('error', function(e) {
|
||
console.error('JavaScript Error:', e.error);
|
||
showToast('页面发生错误,请刷新重试', 'error');
|
||
});
|
||
|
||
// 网络状态监控
|
||
window.addEventListener('online', function() {
|
||
showToast('网络连接已恢复');
|
||
});
|
||
|
||
window.addEventListener('offline', function() {
|
||
showToast('网络连接已断开', 'warning');
|
||
}); |