Files
vps_web/static/js/main-simple.js
ddrwode 4210e0d70a 哈哈
2026-02-10 17:54:22 +08:00

410 lines
14 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 - Simple Mode (无对比功能)
* 实现时间: 2026-02-09
*/
(function() {
'use strict';
// ==================== 全局变量 ====================
var allPlans = [];
var isEnglish = window.LANG === 'en';
// 排序状态
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 toNumber(value) {
if (value == null || value === '') return null;
var n = Number(value);
return isNaN(n) ? null : n;
}
function getPriceValue(plan, currency) {
var cny = toNumber(plan.price_cny);
var usd = toNumber(plan.price_usd);
if (currency === 'USD') {
if (usd != null) return { value: usd, symbol: '$' };
if (cny != null) return { value: cny * exchangeRates.USD, symbol: '$' };
return null;
}
if (cny != null) return { value: cny, symbol: '¥' };
if (usd != null) return { value: usd / exchangeRates.USD, symbol: '¥' };
return null;
}
function getCnyPrice(plan) {
var cny = toNumber(plan.price_cny);
if (cny != null) return cny;
var usd = toNumber(plan.price_usd);
if (usd != null) return usd / exchangeRates.USD;
return null;
}
// ==================== 初始化 ====================
function init() {
fetchData();
initEventListeners();
}
// ==================== 数据获取 ====================
function fetchData() {
// 优先使用服务端直出的数据,首屏无需再请求 /api/plans
if (window.__INITIAL_PLANS__ && Array.isArray(window.__INITIAL_PLANS__) && window.__INITIAL_PLANS__.length >= 0) {
allPlans = window.__INITIAL_PLANS__;
updateSummaryMetrics(allPlans);
populateFilters();
renderTable();
return;
}
fetch('/api/plans')
.then(function(response) {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(function(data) {
allPlans = data;
updateSummaryMetrics(allPlans);
populateFilters();
renderTable();
})
.catch(function(error) {
console.error('Error fetching data:', error);
showError((window.I18N_JS && window.I18N_JS.load_error) || '数据加载失败,请刷新页面重试');
});
}
// ==================== 事件监听 ====================
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);
});
}
// ==================== 筛选器填充 ====================
function populateFilters() {
var providers = new Set();
var regions = new Set();
allPlans.forEach(function(plan) {
providers.add(plan.provider);
regions.add(plan.countries);
});
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();
}
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);
updateResultCount(sorted.length, allPlans.length);
updateLowestMetric(sorted);
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);">' + ((window.I18N_JS && window.I18N_JS.empty_state) || '未找到匹配的方案') + '</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.countries !== 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]);
var cnyPrice = getCnyPrice(plan);
if (cnyPrice == null) return false;
if (cnyPrice < min || cnyPrice > max) return false;
}
// 搜索筛选
if (filters.search) {
var searchText = (plan.provider + ' ' + plan.name + ' ' + plan.countries).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;
var bVal;
if (currentSort.column === 'price') {
var aPrice = getPriceValue(a, filters.currency);
var bPrice = getPriceValue(b, filters.currency);
aVal = aPrice ? aPrice.value : Number.POSITIVE_INFINITY;
bVal = bPrice ? bPrice.value : Number.POSITIVE_INFINITY;
} else {
aVal = a[currentSort.column];
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 currentPrice = getPriceValue(plan, filters.currency);
var displayPrice = currentPrice ? currentPrice.symbol + currentPrice.value.toFixed(2) : '—';
var btnText = (window.I18N_JS && window.I18N_JS.btn_visit) || '访问';
tr.innerHTML =
'<td>' + escapeHtml(plan.provider) + '</td>' +
'<td>' + escapeHtml(plan.countries) + '</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_mbps ? plan.bandwidth_mbps + ' Mbps' : '-') + '</td>' +
'<td>' + plan.traffic + '</td>' +
'<td class="col-price">' + displayPrice + '</td>' +
'<td class="col-link">' +
'<a href="' + escapeHtml(plan.official_url) + '" target="_blank" rel="noopener noreferrer nofollow" class="btn-link">' + btnText + '</a>' +
'</td>';
return tr;
}
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>';
}
function setText(id, text) {
var el = document.getElementById(id);
if (el) el.textContent = text;
}
function formatCount(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return value.toLocaleString();
}
function updateSummaryMetrics(plans) {
var providerSet = new Set();
var regionSet = new Set();
plans.forEach(function(plan) {
if (plan.provider) providerSet.add(plan.provider);
if (plan.countries) regionSet.add(plan.countries);
});
setText('metric-total-plans', formatCount(plans.length));
setText('metric-providers', formatCount(providerSet.size));
setText('metric-regions', formatCount(regionSet.size));
}
function updateLowestMetric(plans) {
var lowest = null;
var symbol = filters.currency === 'USD' ? '$' : '¥';
plans.forEach(function(plan) {
var currentPrice = getPriceValue(plan, filters.currency);
if (!currentPrice) return;
symbol = currentPrice.symbol;
if (lowest == null || currentPrice.value < lowest) {
lowest = currentPrice.value;
}
});
if (lowest == null) {
setText('metric-lowest', '--');
return;
}
setText('metric-lowest', symbol + lowest.toFixed(2));
}
function updateResultCount(visibleCount, totalCount) {
var pattern = (window.I18N_JS && window.I18N_JS.result_count_pattern)
|| (isEnglish ? 'Showing {visible} of {total} plans' : '筛选结果 {visible} / {total}');
var label = pattern
.replace('{visible}', formatCount(visibleCount))
.replace('{total}', formatCount(totalCount));
setText('result-count', label);
}
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();