优化分页组件:实现智能页码显示和页面跳转功能
This commit is contained in:
parent
34be13df68
commit
99d5d53cc5
@ -455,4 +455,350 @@ body {
|
||||
|
||||
.control-panel button:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
/* 股票详情模态框样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* 错误提示样式 */
|
||||
.error-message {
|
||||
background: #fdf2f2;
|
||||
border: 1px solid #e74c3c;
|
||||
color: #e74c3c;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #f0fff4;
|
||||
border: 1px solid #38a169;
|
||||
color: #38a169;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.log-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 股票详情模态框样式 */
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.stock-basic-info,
|
||||
.stock-price-info,
|
||||
.stock-chart {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stock-basic-info h3,
|
||||
.stock-price-info h3,
|
||||
.stock-chart h3 {
|
||||
font-size: 1.2rem;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-weight: 600;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.price-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.price-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.price-item label {
|
||||
font-size: 0.9rem;
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.price-item .price {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 智能分页样式 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin: 2rem 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(.active):not(.ellipsis) {
|
||||
background: #f5f5f5;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.pagination button.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.pagination button.ellipsis {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: default;
|
||||
color: #999;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.pagination button.ellipsis:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 页面跳转样式 */
|
||||
.page-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.page-jump input {
|
||||
width: 80px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-jump input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.page-jump button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.page-jump button:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
/* 响应式分页 */
|
||||
@media (max-width: 768px) {
|
||||
.pagination {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-width: 35px;
|
||||
height: 35px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.page-jump {
|
||||
margin-left: 0.5rem;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-jump input {
|
||||
width: 60px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.page-jump button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-jump {
|
||||
margin-left: 0;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.price-item .change {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-chart-container {
|
||||
height: 200px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计增强 */
|
||||
@media (max-width: 480px) {
|
||||
.modal-content {
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.price-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mini-chart-container {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
@ -178,8 +178,38 @@ class StockDataApp {
|
||||
html += `<button onclick="app.loadStockData(${currentPage - 1})">上一页</button>`;
|
||||
}
|
||||
|
||||
// 页码按钮
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
// 页码按钮 - 智能显示头尾和当前页附近的页码
|
||||
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 {
|
||||
@ -187,13 +217,51 @@ class StockDataApp {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加后省略号(如果需要)
|
||||
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) {
|
||||
@ -428,20 +496,160 @@ class StockDataApp {
|
||||
|
||||
// 查看股票详情
|
||||
viewStockDetails(stockCode) {
|
||||
alert(`查看股票 ${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()">×</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) {
|
||||
// 模拟API调用,实际项目中需要连接到后端API
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// 模拟数据
|
||||
const mockData = this.getMockData(url);
|
||||
resolve(mockData);
|
||||
}, 500);
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模拟数据
|
||||
|
||||
Loading…
Reference in New Issue
Block a user