stock/frontend/js/app.js

812 lines
22 KiB
JavaScript
Raw 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.

// 股票数据展示系统前端应用
// 主应用入口文件 - 更新于 $(date)
class StockDataApp {
constructor() {
this.currentPage = 1;
this.pageSize = 20;
this.currentStock = null;
this.klineChart = null;
this.init();
}
// 初始化应用
async init() {
this.setupEventListeners();
await this.loadSystemOverview();
await this.loadStockData();
this.setupNavigation();
}
// 设置事件监听器
setupEventListeners() {
// 搜索功能
const searchInput = document.getElementById('stock-search');
const searchBtn = document.getElementById('search-btn');
searchBtn.addEventListener('click', () => this.searchStocks());
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.searchStocks();
});
// 股票选择器
const stockSelector = document.getElementById('stock-selector');
stockSelector.addEventListener('change', (e) => {
this.currentStock = e.target.value;
if (this.currentStock) this.loadKlineChart();
});
// 周期选择器
const periodSelector = document.getElementById('period-selector');
periodSelector.addEventListener('change', () => {
if (this.currentStock) this.loadKlineChart();
});
// 财务数据选择器
const financialStockSelector = document.getElementById('financial-stock-selector');
financialStockSelector.addEventListener('change', (e) => {
if (e.target.value) this.loadFinancialData(e.target.value);
});
// 日志刷新
const refreshLogsBtn = document.getElementById('refresh-logs');
refreshLogsBtn.addEventListener('click', () => this.loadSystemLogs());
// 日志过滤器
const logLevelFilter = document.getElementById('log-level-filter');
logLevelFilter.addEventListener('change', () => this.loadSystemLogs());
const logDateFilter = document.getElementById('log-date-filter');
logDateFilter.addEventListener('change', () => this.loadSystemLogs());
}
// 设置导航
setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const sections = document.querySelectorAll('.content-section');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
// 移除所有激活状态
navLinks.forEach(l => l.classList.remove('active'));
sections.forEach(s => s.classList.remove('active'));
// 添加当前激活状态
link.classList.add('active');
const targetSection = document.querySelector(link.getAttribute('href'));
if (targetSection) {
targetSection.classList.add('active');
// 加载对应数据
switch(link.getAttribute('href')) {
case '#kline-chart':
this.loadKlineChart();
break;
case '#financial-data':
this.loadFinancialData();
break;
case '#system-logs':
this.loadSystemLogs();
break;
}
}
});
});
}
// 显示加载遮罩
showLoading() {
document.getElementById('loading-overlay').classList.add('show');
}
// 隐藏加载遮罩
hideLoading() {
document.getElementById('loading-overlay').classList.remove('show');
}
// 加载系统概览数据
async loadSystemOverview() {
try {
const response = await this.apiCall('/api/system/overview');
if (response.success) {
document.getElementById('stock-count').textContent = this.formatNumber(response.stock_count);
document.getElementById('kline-count').textContent = this.formatNumber(response.kline_count);
document.getElementById('financial-count').textContent = this.formatNumber(response.financial_count);
document.getElementById('log-count').textContent = this.formatNumber(response.log_count);
}
} catch (error) {
console.error('加载系统概览失败:', error);
}
}
// 加载股票数据
async loadStockData(page = 1) {
try {
this.showLoading();
const response = await this.apiCall(`/api/stocks?page=${page}&limit=${this.pageSize}`);
if (response.success) {
this.renderStockTable(response.data);
this.setupPagination(response.total, page);
this.populateStockSelectors(response.data);
}
} catch (error) {
console.error('加载股票数据失败:', error);
this.showError('加载股票数据失败');
} finally {
this.hideLoading();
}
}
// 渲染股票表格
renderStockTable(stocks) {
const tbody = document.getElementById('stock-table-body');
tbody.innerHTML = '';
stocks.forEach(stock => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${stock.code}</td>
<td>${stock.name}</td>
<td>${stock.exchange}</td>
<td>${this.formatDate(stock.listing_date)}</td>
<td>${stock.industry || '-'}</td>
<td>
<button class="btn btn-primary" onclick="app.viewStockDetails('${stock.code}')">
查看详情
</button>
</td>
`;
tbody.appendChild(row);
});
}
// 设置分页
setupPagination(total, currentPage) {
const pagination = document.getElementById('stock-pagination');
const totalPages = Math.ceil(total / this.pageSize);
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
// 上一页按钮
if (currentPage > 1) {
html += `<button onclick="app.loadStockData(${currentPage - 1})">上一页</button>`;
}
// 页码按钮 - 智能显示头尾和当前页附近的页码
const maxVisiblePages = 7; // 最多显示7个页码按钮
const halfVisible = Math.floor(maxVisiblePages / 2);
// 总是显示第一页
if (currentPage === 1) {
html += `<button class="active">1</button>`;
} else {
html += `<button onclick="app.loadStockData(1)">1</button>`;
}
// 计算起始和结束页码
let startPage = Math.max(2, currentPage - halfVisible);
let endPage = Math.min(totalPages - 1, currentPage + halfVisible);
// 如果当前页靠近开头,调整结束页码
if (currentPage <= halfVisible + 1) {
endPage = Math.min(totalPages - 1, maxVisiblePages - 1);
}
// 如果当前页靠近结尾,调整起始页码
if (currentPage >= totalPages - halfVisible) {
startPage = Math.max(2, totalPages - maxVisiblePages + 2);
}
// 添加前省略号(如果需要)
if (startPage > 2) {
html += `<button class="ellipsis" disabled>...</button>`;
}
// 添加中间页码
for (let i = startPage; i <= endPage; i++) {
if (i === currentPage) {
html += `<button class="active">${i}</button>`;
} else {
html += `<button onclick="app.loadStockData(${i})">${i}</button>`;
}
}
// 添加后省略号(如果需要)
if (endPage < totalPages - 1) {
html += `<button class="ellipsis" disabled>...</button>`;
}
// 总是显示最后一页如果总页数大于1
if (totalPages > 1) {
if (currentPage === totalPages) {
html += `<button class="active">${totalPages}</button>`;
} else {
html += `<button onclick="app.loadStockData(${totalPages})">${totalPages}</button>`;
}
}
// 下一页按钮
if (currentPage < totalPages) {
html += `<button onclick="app.loadStockData(${currentPage + 1})">下一页</button>`;
}
// 添加页面跳转输入框
html += `
<div class="page-jump">
<input type="number" id="page-jump-input" min="1" max="${totalPages}" placeholder="页码">
<button onclick="app.jumpToPage()">跳转</button>
</div>
`;
pagination.innerHTML = html;
}
// 页面跳转功能
jumpToPage() {
const input = document.getElementById('page-jump-input');
const page = parseInt(input.value);
// 从页面跳转输入框的max属性获取总页数
const maxPage = parseInt(input.getAttribute('max')) || 1;
if (page && page >= 1 && page <= maxPage) {
this.loadStockData(page);
input.value = '';
} else {
this.showError('请输入有效的页码1-' + maxPage + '');
}
}
// 填充股票选择器
populateStockSelectors(stocks) {
const selectors = [
document.getElementById('stock-selector'),
document.getElementById('financial-stock-selector')
];
selectors.forEach(selector => {
// 清空现有选项(保留第一个选项)
while (selector.children.length > 1) {
selector.removeChild(selector.lastChild);
}
// 添加股票选项
stocks.forEach(stock => {
const option = document.createElement('option');
option.value = stock.code;
option.textContent = `${stock.code} - ${stock.name}`;
selector.appendChild(option);
});
});
}
// 搜索股票
async searchStocks() {
const query = document.getElementById('stock-search').value.trim();
if (!query) {
await this.loadStockData();
return;
}
try {
this.showLoading();
const response = await this.apiCall(`/api/stocks/search?q=${encodeURIComponent(query)}`);
if (response.success) {
this.renderStockTable(response.data);
document.getElementById('stock-pagination').innerHTML = '';
}
} catch (error) {
console.error('搜索股票失败:', error);
this.showError('搜索股票失败');
} finally {
this.hideLoading();
}
}
// 加载K线图表
async loadKlineChart() {
if (!this.currentStock) return;
try {
this.showLoading();
const period = document.getElementById('period-selector').value;
const response = await this.apiCall(`/api/kline/${this.currentStock}?period=${period}`);
if (response.success) {
this.renderKlineChart(response.data);
}
} catch (error) {
console.error('加载K线数据失败:', error);
this.showError('加载K线数据失败');
} finally {
this.hideLoading();
}
}
// 渲染K线图表
renderKlineChart(klineData) {
const ctx = document.getElementById('kline-chart-canvas').getContext('2d');
// 销毁现有图表
if (this.klineChart) {
this.klineChart.destroy();
}
const dates = klineData.map(item => item.date);
const prices = klineData.map(item => item.close);
this.klineChart = new Chart(ctx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '收盘价',
data: prices,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '股票价格走势图'
}
},
scales: {
y: {
beginAtZero: false
}
}
}
});
}
// 加载财务数据
async loadFinancialData(stockCode = null) {
try {
this.showLoading();
if (!stockCode) {
stockCode = document.getElementById('financial-stock-selector').value;
}
if (!stockCode) return;
const period = document.getElementById('report-period').value;
const year = document.getElementById('report-year').value;
const response = await this.apiCall(`/api/financial/${stockCode}?year=${year}&period=${period}`);
if (response.success) {
this.renderFinancialTable(response.data);
}
} catch (error) {
console.error('加载财务数据失败:', error);
this.showError('加载财务数据失败');
} finally {
this.hideLoading();
}
}
// 渲染财务数据表格
renderFinancialTable(financialData) {
const tbody = document.getElementById('financial-table-body');
tbody.innerHTML = '';
if (!financialData || Object.keys(financialData).length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center;">暂无财务数据</td></tr>';
return;
}
const financialItems = [
{ key: 'revenue', label: '营业收入', unit: '万元' },
{ key: 'net_profit', label: '净利润', unit: '万元' },
{ key: 'total_assets', label: '总资产', unit: '万元' },
{ key: 'total_liabilities', label: '总负债', unit: '万元' },
{ key: 'eps', label: '每股收益', unit: '元' },
{ key: 'roe', label: '净资产收益率', unit: '%' }
];
financialItems.forEach(item => {
if (financialData[item.key] !== undefined) {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.label}</td>
<td>${this.formatNumber(financialData[item.key])}</td>
<td>${item.unit}</td>
<td>${this.calculateChange(financialData[item.key])}</td>
`;
tbody.appendChild(row);
}
});
}
// 加载系统日志
async loadSystemLogs() {
try {
this.showLoading();
const level = document.getElementById('log-level-filter').value;
const date = document.getElementById('log-date-filter').value;
let url = '/api/system/logs';
const params = [];
if (level) params.push(`level=${level}`);
if (date) params.push(`date=${date}`);
if (params.length > 0) {
url += '?' + params.join('&');
}
const response = await this.apiCall(url);
if (response.success) {
this.renderSystemLogs(response.data);
}
} catch (error) {
console.error('加载系统日志失败:', error);
this.showError('加载系统日志失败');
} finally {
this.hideLoading();
}
}
// 渲染系统日志
renderSystemLogs(logs) {
const logList = document.getElementById('log-list');
logList.innerHTML = '';
if (!logs || logs.length === 0) {
logList.innerHTML = '<div class="log-item">暂无系统日志</div>';
return;
}
logs.forEach(log => {
const logItem = document.createElement('div');
logItem.className = `log-item ${log.level.toLowerCase()}`;
logItem.innerHTML = `
<div class="log-header">
<span class="log-level ${log.level.toLowerCase()}">${log.level}</span>
<span class="log-time">${this.formatDateTime(log.timestamp)}</span>
</div>
<div class="log-message">${log.message}</div>
<div class="log-details">
模块: ${log.module_name} | 事件: ${log.event_type}
${log.exception_type ? ` | 异常: ${log.exception_type}` : ''}
</div>
`;
logList.appendChild(logItem);
});
}
// 查看股票详情
viewStockDetails(stockCode) {
this.showStockDetailsModal(stockCode);
}
// 显示股票详情模态框
async showStockDetailsModal(stockCode) {
try {
this.showLoading();
// 获取股票详细信息
const stockResponse = await this.apiCall(`/api/stocks/${stockCode}`);
const klineResponse = await this.apiCall(`/api/kline/${stockCode}?period=daily&limit=30`);
if (stockResponse.success && klineResponse.success) {
this.renderStockDetailsModal(stockCode, stockResponse.data, klineResponse.data);
} else {
this.showError('获取股票详情失败');
}
} catch (error) {
console.error('显示股票详情失败:', error);
this.showError('显示股票详情失败');
} finally {
this.hideLoading();
}
}
// 渲染股票详情模态框
renderStockDetailsModal(stockCode, stockData, klineData) {
// 创建模态框HTML
const modalHtml = `
<div class="modal-overlay show" id="stock-details-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">${stockData.name} (${stockData.code})</h2>
<button class="modal-close" onclick="app.closeStockDetailsModal()">&times;</button>
</div>
<div class="modal-body">
<div class="stock-basic-info">
<h3>基本信息</h3>
<div class="info-grid">
<div class="info-item">
<label>交易所:</label>
<span>${stockData.exchange}</span>
</div>
<div class="info-item">
<label>上市日期:</label>
<span>${this.formatDate(stockData.listing_date)}</span>
</div>
<div class="info-item">
<label>行业分类:</label>
<span>${stockData.industry || '-'}</span>
</div>
</div>
</div>
<div class="stock-price-info">
<h3>价格信息</h3>
${klineData && klineData.length > 0 ? `
<div class="price-grid">
<div class="price-item">
<label>最新价格:</label>
<span class="price">${klineData[klineData.length - 1].close.toFixed(2)}</span>
</div>
<div class="price-item">
<label>涨跌幅:</label>
<span class="change">${this.calculateChange(klineData[klineData.length - 1].close)}</span>
</div>
</div>
` : '<p>暂无价格数据</p>'}
</div>
<div class="stock-chart">
<h3>近期走势</h3>
<div class="mini-chart-container">
<canvas id="mini-kline-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
`;
// 添加模态框到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 渲染迷你K线图
if (klineData && klineData.length > 0) {
this.renderMiniKlineChart(klineData);
}
// 添加点击遮罩关闭功能
document.getElementById('stock-details-modal').addEventListener('click', (e) => {
if (e.target.id === 'stock-details-modal') {
this.closeStockDetailsModal();
}
});
}
// 渲染迷你K线图
renderMiniKlineChart(klineData) {
const ctx = document.getElementById('mini-kline-chart').getContext('2d');
const dates = klineData.map(item => item.date);
const prices = klineData.map(item => item.close);
new Chart(ctx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '收盘价',
data: prices,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.1,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: false }
},
scales: {
y: { display: false },
x: { display: false }
}
}
});
}
// 关闭股票详情模态框
closeStockDetailsModal() {
const modal = document.getElementById('stock-details-modal');
if (modal) {
modal.remove();
}
}
// API调用封装
async apiCall(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('API调用失败:', error);
// 如果后端API不可用回退到模拟数据
return this.getMockData(url);
}
}
// 获取模拟数据
getMockData(url) {
if (url.includes('/api/system/overview')) {
return {
success: true,
stock_count: 12595,
kline_count: 440,
financial_count: 50,
log_count: 4
};
}
if (url.includes('/api/stocks')) {
// 模拟股票数据
const mockStocks = [
{ code: '000001', name: '平安银行', exchange: 'SZ', listing_date: '1991-04-03', industry: '银行' },
{ code: '000002', name: '万科A', exchange: 'SZ', listing_date: '1991-01-29', industry: '房地产' },
{ code: '600000', name: '浦发银行', exchange: 'SH', listing_date: '1999-11-10', industry: '银行' },
{ code: '600036', name: '招商银行', exchange: 'SH', listing_date: '2002-04-09', industry: '银行' },
{ code: '601318', name: '中国平安', exchange: 'SH', listing_date: '2007-03-01', industry: '保险' }
];
return {
success: true,
data: mockStocks,
total: 12595
};
}
if (url.includes('/api/kline/')) {
// 模拟K线数据
const dates = [];
const prices = [];
const basePrice = 10;
for (let i = 30; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
dates.push(date.toISOString().split('T')[0]);
// 模拟价格波动
const price = basePrice + Math.random() * 5;
prices.push({
date: date.toISOString().split('T')[0],
open: price - 0.5,
high: price + 0.8,
low: price - 0.8,
close: price,
volume: Math.floor(Math.random() * 1000000)
});
}
return {
success: true,
data: prices
};
}
if (url.includes('/api/financial/')) {
// 模拟财务数据
return {
success: true,
data: {
revenue: 500000,
net_profit: 80000,
total_assets: 2000000,
total_liabilities: 1200000,
eps: 1.5,
roe: 15.2
}
};
}
if (url.includes('/api/system/logs')) {
// 模拟系统日志
return {
success: true,
data: [
{
id: 1,
timestamp: new Date().toISOString(),
level: 'INFO',
module_name: 'System',
event_type: 'STARTUP',
message: '系统启动成功',
exception_type: null
},
{
id: 2,
timestamp: new Date(Date.now() - 3600000).toISOString(),
level: 'INFO',
module_name: 'DataCollector',
event_type: 'DATA_COLLECTION',
message: '开始采集股票数据',
exception_type: null
},
{
id: 3,
timestamp: new Date(Date.now() - 1800000).toISOString(),
level: 'ERROR',
module_name: 'Database',
event_type: 'CONNECTION_ERROR',
message: '数据库连接失败',
exception_type: 'ConnectionError'
},
{
id: 4,
timestamp: new Date(Date.now() - 900000).toISOString(),
level: 'WARNING',
module_name: 'DataProcessor',
event_type: 'DATA_FORMAT',
message: '数据格式异常,已自动修复',
exception_type: 'FormatError'
}
]
};
}
return { success: false, message: 'API endpoint not found' };
}
// 工具函数
formatNumber(num) {
if (num === null || num === undefined) return '-';
return new Intl.NumberFormat('zh-CN').format(num);
}
formatDate(dateString) {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('zh-CN');
}
formatDateTime(dateString) {
if (!dateString) return '-';
return new Date(dateString).toLocaleString('zh-CN');
}
calculateChange(value) {
const change = (Math.random() - 0.5) * 20;
const sign = change >= 0 ? '+' : '';
const color = change >= 0 ? '#27ae60' : '#e74c3c';
return `<span style="color: ${color}">${sign}${change.toFixed(2)}%</span>`;
}
showError(message) {
alert(`错误: ${message}`);
}
}
// 全局应用实例
const app = new StockDataApp();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
console.log('股票数据展示系统已加载');
});