Files
vps_web/static/js/main-comparison-slide.js
ddrwode 976e9afa88 哈哈
2026-02-09 17:56:23 +08:00

515 lines
18 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.

/**
* VPS Price Comparison - v3.4 Slide Mode
* 向右滑出收起的对比面板
* 实现时间: 2026-02-09
*/
(function() {
'use strict';
// ==================== 全局变量 ====================
var allPlans = [];
var comparisonPlans = [];
var isComparisonVisible = true; // 对比面板显示状态
var MAX_COMPARISON = 4;
// 排序状态
var currentSort = {
column: null,
direction: 'asc'
};
// 筛选状态
var filters = {
provider: '',
region: '',
memory: 0,
price: '0',
currency: 'CNY',
search: ''
};
// 汇率CNY 为基准)
var exchangeRates = {
CNY: 1,
USD: 0.14
};
// ==================== 初始化 ====================
function init() {
fetchData();
initEventListeners();
loadComparisonFromURL();
}
// ==================== 数据获取 ====================
function fetchData() {
fetch('/api/plans')
.then(function(response) {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(function(data) {
allPlans = data;
populateFilters();
renderTable();
updateComparison();
})
.catch(function(error) {
console.error('Error fetching data:', error);
showError('数据加载失败,请刷新页面重试');
});
}
// ==================== 事件监听 ====================
function initEventListeners() {
// 筛选器
document.getElementById('filter-provider').addEventListener('change', handleFilterChange);
document.getElementById('filter-region').addEventListener('change', handleFilterChange);
document.getElementById('filter-memory').addEventListener('change', handleFilterChange);
document.getElementById('filter-price').addEventListener('change', handleFilterChange);
document.getElementById('filter-currency').addEventListener('change', handleCurrencyChange);
document.getElementById('btn-reset').addEventListener('click', resetFilters);
// 搜索
var searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', debounce(handleSearch, 300));
// 排序
var sortableHeaders = document.querySelectorAll('.sortable');
sortableHeaders.forEach(function(header) {
header.addEventListener('click', handleSort);
});
// 对比面板控制
document.getElementById('btn-clear-comparison').addEventListener('click', clearComparison);
// 对比面板切换按钮
var toggleBtn = document.getElementById('btn-toggle-comparison');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleComparisonPanel);
}
}
// ==================== 筛选器填充 ====================
function populateFilters() {
var providers = new Set();
var regions = new Set();
allPlans.forEach(function(plan) {
providers.add(plan.provider);
regions.add(plan.region);
});
populateSelect('filter-provider', Array.from(providers).sort());
populateSelect('filter-region', Array.from(regions).sort());
}
function populateSelect(id, options) {
var select = document.getElementById(id);
var currentValue = select.value;
// 保留第一个选项("全部"
while (select.options.length > 1) {
select.remove(1);
}
options.forEach(function(option) {
var opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
select.appendChild(opt);
});
select.value = currentValue;
}
// ==================== 筛选处理 ====================
function handleFilterChange(e) {
var id = e.target.id;
var value = e.target.value;
if (id === 'filter-provider') filters.provider = value;
else if (id === 'filter-region') filters.region = value;
else if (id === 'filter-memory') filters.memory = parseFloat(value);
else if (id === 'filter-price') filters.price = value;
renderTable();
}
function handleCurrencyChange(e) {
filters.currency = e.target.value;
renderTable();
updateComparison();
}
function handleSearch(e) {
filters.search = e.target.value.toLowerCase();
renderTable();
}
function resetFilters() {
filters = {
provider: '',
region: '',
memory: 0,
price: '0',
currency: filters.currency,
search: ''
};
document.getElementById('filter-provider').value = '';
document.getElementById('filter-region').value = '';
document.getElementById('filter-memory').value = '0';
document.getElementById('filter-price').value = '0';
document.getElementById('search-input').value = '';
renderTable();
}
// ==================== 排序处理 ====================
function handleSort(e) {
var header = e.currentTarget;
var column = header.dataset.sort;
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
updateSortIcons();
renderTable();
}
function updateSortIcons() {
document.querySelectorAll('.sortable').forEach(function(header) {
var icon = header.querySelector('.sort-icon');
icon.textContent = '';
if (header.dataset.sort === currentSort.column) {
icon.textContent = currentSort.direction === 'asc' ? '↑' : '↓';
}
});
}
// ==================== 表格渲染 ====================
function renderTable() {
var filtered = filterPlans(allPlans);
var sorted = sortPlans(filtered);
var tbody = document.getElementById('table-body');
tbody.innerHTML = '';
if (sorted.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 2rem; color: var(--text-muted);">未找到匹配的方案</td></tr>';
return;
}
sorted.forEach(function(plan) {
var row = createTableRow(plan);
tbody.appendChild(row);
});
}
function filterPlans(plans) {
return plans.filter(function(plan) {
// 厂商筛选
if (filters.provider && plan.provider !== filters.provider) return false;
// 区域筛选
if (filters.region && plan.region !== filters.region) return false;
// 内存筛选
if (filters.memory > 0 && plan.memory_gb < filters.memory) return false;
// 价格筛选
if (filters.price !== '0') {
var range = filters.price.split('-');
var min = parseFloat(range[0]);
var max = parseFloat(range[1]);
if (plan.price_cny < min || plan.price_cny > max) return false;
}
// 搜索筛选
if (filters.search) {
var searchText = (plan.provider + ' ' + plan.name + ' ' + plan.region).toLowerCase();
if (searchText.indexOf(filters.search) === -1) return false;
}
return true;
});
}
function sortPlans(plans) {
if (!currentSort.column) return plans;
return plans.slice().sort(function(a, b) {
var aVal = a[currentSort.column];
var bVal = b[currentSort.column];
if (typeof aVal === 'number' && typeof bVal === 'number') {
return currentSort.direction === 'asc' ? aVal - bVal : bVal - aVal;
}
var aStr = String(aVal).toLowerCase();
var bStr = String(bVal).toLowerCase();
if (currentSort.direction === 'asc') {
return aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
} else {
return aStr > bStr ? -1 : aStr < bStr ? 1 : 0;
}
});
}
function createTableRow(plan) {
var tr = document.createElement('tr');
var price = convertPrice(plan.price_cny, filters.currency);
var priceSymbol = filters.currency === 'CNY' ? '¥' : '$';
var isInComparison = comparisonPlans.some(function(p) { return p.id === plan.id; });
var starClass = isInComparison ? 'star-active' : 'star-inactive';
var starIcon = isInComparison ? '★' : '☆';
tr.innerHTML =
'<td>' + escapeHtml(plan.provider) + '</td>' +
'<td>' + escapeHtml(plan.region) + '</td>' +
'<td>' + escapeHtml(plan.name) + '</td>' +
'<td>' + plan.vcpu + '</td>' +
'<td>' + plan.memory_gb + ' GB</td>' +
'<td>' + plan.storage_gb + ' GB</td>' +
'<td>' + plan.bandwidth + '</td>' +
'<td>' + plan.traffic + '</td>' +
'<td class="col-price">' + priceSymbol + price + '</td>' +
'<td class="col-link">' +
'<button class="btn-star ' + starClass + '" data-plan-id="' + plan.id + '" title="' + (isInComparison ? '取消对比' : '添加对比') + '">' +
starIcon +
'</button>' +
'<a href="' + escapeHtml(plan.url) + '" target="_blank" rel="noopener" class="btn-link">访问</a>' +
'</td>';
// 星标按钮事件
var starBtn = tr.querySelector('.btn-star');
starBtn.addEventListener('click', function() {
toggleComparison(plan);
});
return tr;
}
// ==================== 对比功能 ====================
function toggleComparison(plan) {
var index = comparisonPlans.findIndex(function(p) { return p.id === plan.id; });
if (index > -1) {
comparisonPlans.splice(index, 1);
} else {
if (comparisonPlans.length >= MAX_COMPARISON) {
alert('最多只能对比 ' + MAX_COMPARISON + ' 个方案');
return;
}
comparisonPlans.push(plan);
}
renderTable();
updateComparison();
updateURL();
}
function clearComparison() {
comparisonPlans = [];
renderTable();
updateComparison();
updateURL();
}
// ==================== 对比面板切换 ====================
function toggleComparisonPanel() {
isComparisonVisible = !isComparisonVisible;
updateComparison();
}
// ==================== 对比面板渲染 ====================
function updateComparison() {
var panel = document.getElementById('comparison-panel');
var toggleBtn = document.getElementById('btn-toggle-comparison');
var floatingBtn = document.getElementById('floating-toggle-btn');
// 如果没有对比方案
if (comparisonPlans.length === 0) {
panel.classList.remove('visible');
panel.classList.add('hidden');
if (floatingBtn) floatingBtn.style.display = 'none';
isComparisonVisible = false;
renderComparisonContent();
return;
}
// 有对比方案
if (isComparisonVisible) {
panel.classList.add('visible');
panel.classList.remove('hidden');
if (floatingBtn) floatingBtn.style.display = 'none';
} else {
panel.classList.remove('visible');
panel.classList.add('hidden');
if (floatingBtn) {
floatingBtn.style.display = 'flex';
var countBadge = floatingBtn.querySelector('.count-badge');
if (countBadge) {
countBadge.textContent = comparisonPlans.length;
}
}
}
renderComparisonContent();
}
function renderComparisonContent() {
var content = document.getElementById('comparison-content');
if (comparisonPlans.length === 0) {
content.innerHTML =
'<div class="comparison-empty">' +
'<svg class="empty-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
'<path d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>' +
'</svg>' +
'<p class="empty-text">点击星标收藏方案</p>' +
'<p class="empty-hint">最多对比 ' + MAX_COMPARISON + ' 个方案</p>' +
'</div>';
return;
}
// 渲染表格
content.innerHTML = renderComparisonTable();
}
function renderComparisonTable() {
var priceSymbol = filters.currency === 'CNY' ? '¥' : '$';
var html = '<div class="comparison-table-wrapper">';
html += '<table class="comparison-table">';
// 表头
html += '<thead><tr>';
html += '<th>厂商</th>';
html += '<th>配置</th>';
html += '<th>vCPU</th>';
html += '<th>内存</th>';
html += '<th>存储</th>';
html += '<th>带宽</th>';
html += '<th>流量</th>';
html += '<th>区域</th>';
html += '<th>价格</th>';
html += '<th>操作</th>';
html += '</tr></thead>';
// 表体
html += '<tbody>';
comparisonPlans.forEach(function(plan) {
var price = convertPrice(plan.price_cny, filters.currency);
html += '<tr class="comparison-row">';
html += '<td class="provider-cell"><strong>' + escapeHtml(plan.provider) + '</strong></td>';
html += '<td>' + escapeHtml(plan.name) + '</td>';
html += '<td>' + plan.vcpu + ' 核</td>';
html += '<td>' + plan.memory_gb + ' GB</td>';
html += '<td>' + plan.storage_gb + ' GB</td>';
html += '<td>' + escapeHtml(plan.bandwidth) + '</td>';
html += '<td>' + escapeHtml(plan.traffic) + '</td>';
html += '<td>' + escapeHtml(plan.region) + '</td>';
html += '<td class="price-cell">' + priceSymbol + price + '</td>';
html += '<td class="action-cell">';
html += '<a href="' + escapeHtml(plan.url) + '" target="_blank" rel="noopener" class="btn-visit">访问</a>';
html += '<button class="btn-remove" data-plan-id="' + plan.id + '" title="移除">✕</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
// 绑定移除按钮事件
setTimeout(function() {
document.querySelectorAll('.btn-remove').forEach(function(btn) {
btn.addEventListener('click', function() {
var planId = this.dataset.planId;
var plan = comparisonPlans.find(function(p) { return p.id === planId; });
if (plan) toggleComparison(plan);
});
});
}, 0);
return html;
}
// ==================== URL 同步 ====================
function updateURL() {
var ids = comparisonPlans.map(function(p) { return p.id; }).join(',');
var url = new URL(window.location);
if (ids) {
url.searchParams.set('compare', ids);
} else {
url.searchParams.delete('compare');
}
window.history.replaceState({}, '', url);
}
function loadComparisonFromURL() {
var url = new URL(window.location);
var compareIds = url.searchParams.get('compare');
if (compareIds) {
var ids = compareIds.split(',');
comparisonPlans = allPlans.filter(function(plan) {
return ids.indexOf(plan.id) > -1;
});
renderTable();
updateComparison();
}
}
// ==================== 工具函数 ====================
function convertPrice(priceCNY, currency) {
var converted = priceCNY * exchangeRates[currency];
return converted.toFixed(2);
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function debounce(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
function showError(message) {
var tbody = document.getElementById('table-body');
tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 2rem; color: #EF4444;">' + message + '</td></tr>';
}
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();