Files
vps_web/static/js/main-simple.js

582 lines
20 KiB
JavaScript
Raw Normal View History

2026-02-09 17:56:23 +08:00
/**
* VPS Price - Simple Mode (无对比功能)
* 实现时间: 2026-02-09
*/
(function() {
'use strict';
// ==================== 全局变量 ====================
var allPlans = [];
2026-02-10 16:54:06 +08:00
var isEnglish = window.LANG === 'en';
2026-02-09 17:56:23 +08:00
// 排序状态
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
};
2026-02-09 22:36:32 +08:00
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;
}
2026-02-11 17:38:06 +08:00
function getDisplayRegion(plan) {
return (plan.countries && String(plan.countries).trim()) ? String(plan.countries).trim() : (plan.region || '');
}
function getAllRegionOptions(plans) {
var regionSet = new Set();
plans.forEach(function(plan) {
var countriesText = (plan.countries || '').trim();
if (countriesText) {
countriesText.split(',').forEach(function(part) {
var regionName = part.trim();
if (regionName) regionSet.add(regionName);
});
}
var regionRaw = (plan.region || '').trim();
if (regionRaw) regionSet.add(regionRaw);
});
return Array.from(regionSet).sort();
}
function matchesRegion(plan, targetRegion) {
if (!targetRegion) return true;
var region = String(targetRegion).trim();
if (!region) return true;
var countriesText = (plan.countries || '').trim();
if (countriesText === region) return true;
if (countriesText) {
var segments = countriesText.split(',');
for (var i = 0; i < segments.length; i++) {
if (segments[i].trim() === region) return true;
}
}
var regionRaw = (plan.region || '').trim();
return regionRaw === region;
}
2026-02-09 17:56:23 +08:00
// ==================== 初始化 ====================
function init() {
fetchData();
initEventListeners();
}
// ==================== 数据获取 ====================
function fetchData() {
2026-02-10 13:57:46 +08:00
// 优先使用服务端直出的数据,首屏无需再请求 /api/plans
if (window.__INITIAL_PLANS__ && Array.isArray(window.__INITIAL_PLANS__) && window.__INITIAL_PLANS__.length >= 0) {
allPlans = window.__INITIAL_PLANS__;
2026-02-10 16:54:06 +08:00
updateSummaryMetrics(allPlans);
2026-02-10 13:57:46 +08:00
populateFilters();
2026-02-11 17:38:06 +08:00
applyUrlPrefillFromQuery();
2026-02-10 13:57:46 +08:00
renderTable();
return;
}
2026-02-09 17:56:23 +08:00
fetch('/api/plans')
.then(function(response) {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(function(data) {
allPlans = data;
2026-02-10 16:54:06 +08:00
updateSummaryMetrics(allPlans);
2026-02-09 17:56:23 +08:00
populateFilters();
2026-02-11 17:38:06 +08:00
applyUrlPrefillFromQuery();
2026-02-09 17:56:23 +08:00
renderTable();
})
.catch(function(error) {
console.error('Error fetching data:', error);
2026-02-10 11:49:01 +08:00
showError((window.I18N_JS && window.I18N_JS.load_error) || '数据加载失败,请刷新页面重试');
2026-02-09 17:56:23 +08:00
});
}
// ==================== 事件监听 ====================
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();
allPlans.forEach(function(plan) {
providers.add(plan.provider);
});
populateSelect('filter-provider', Array.from(providers).sort());
2026-02-11 17:38:06 +08:00
populateSelect('filter-region', getAllRegionOptions(allPlans));
}
function getQueryParams() {
try {
return new URLSearchParams(window.location.search || '');
} catch (err) {
return new URLSearchParams('');
}
}
function hasSelectOption(select, value) {
if (!select || !value) return false;
for (var i = 0; i < select.options.length; i++) {
if (String(select.options[i].value) === String(value)) {
return true;
}
}
return false;
}
function normalizeMemoryValue(raw) {
var value = parseInt(raw || '0', 10);
if (isNaN(value) || value <= 0) return '0';
if (value >= 8) return '8';
if (value >= 4) return '4';
if (value >= 2) return '2';
return '1';
}
function syncFiltersFromControls() {
var providerEl = document.getElementById('filter-provider');
var regionEl = document.getElementById('filter-region');
var memoryEl = document.getElementById('filter-memory');
var priceEl = document.getElementById('filter-price');
var currencyEl = document.getElementById('filter-currency');
var searchEl = document.getElementById('search-input');
filters.provider = (providerEl && providerEl.value) || '';
filters.region = (regionEl && regionEl.value) || '';
filters.memory = parseFloat((memoryEl && memoryEl.value) || '0') || 0;
filters.price = (priceEl && priceEl.value) || '0';
filters.currency = (currencyEl && currencyEl.value) || 'CNY';
filters.search = ((searchEl && searchEl.value) || '').toLowerCase().trim();
}
function renderSourceHint(params) {
var hintEl = document.getElementById('filter-source-hint');
if (!hintEl) return;
var sourcePostRaw = (params.get('source_post') || '').trim();
var sourcePost = parseInt(sourcePostRaw, 10);
if (isNaN(sourcePost) || sourcePost <= 0) {
hintEl.hidden = true;
hintEl.textContent = '';
return;
}
var sourceTitle = (params.get('source_title') || '').trim();
if (sourceTitle.length > 56) {
sourceTitle = sourceTitle.slice(0, 56);
}
var sourceHref = '/forum/post/' + sourcePost + (isEnglish ? '?lang=en' : '');
var summaryParts = [];
if (params.get('provider')) summaryParts.push((isEnglish ? 'provider ' : '厂商 ') + params.get('provider'));
if (params.get('region')) summaryParts.push((isEnglish ? 'region ' : '地区 ') + params.get('region'));
if (params.get('memory')) summaryParts.push((isEnglish ? 'memory ≥' : '内存≥') + normalizeMemoryValue(params.get('memory')) + 'GB');
if (params.get('price')) summaryParts.push(isEnglish ? 'budget range' : '预算区间');
hintEl.textContent = '';
hintEl.appendChild(document.createTextNode(isEnglish ? 'From ' : '来源:'));
var link = document.createElement('a');
link.href = sourceHref;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = '#' + sourcePost + ' ' + (sourceTitle || (isEnglish ? 'source topic' : '原帖'));
hintEl.appendChild(link);
if (summaryParts.length) {
hintEl.appendChild(document.createTextNode(isEnglish ? ' | Prefill: ' : ' | 预填:'));
hintEl.appendChild(document.createTextNode(summaryParts.slice(0, 3).join(' / ')));
}
hintEl.hidden = false;
}
function applyUrlPrefillFromQuery() {
var params = getQueryParams();
var providerEl = document.getElementById('filter-provider');
var regionEl = document.getElementById('filter-region');
var memoryEl = document.getElementById('filter-memory');
var priceEl = document.getElementById('filter-price');
var currencyEl = document.getElementById('filter-currency');
var searchEl = document.getElementById('search-input');
var provider = (params.get('provider') || '').trim();
var region = (params.get('region') || '').trim();
var memory = normalizeMemoryValue(params.get('memory'));
var price = (params.get('price') || '0').trim();
var currency = (params.get('currency') || '').trim().toUpperCase();
var search = (params.get('search') || '').trim();
if (provider && hasSelectOption(providerEl, provider)) {
providerEl.value = provider;
}
if (region && hasSelectOption(regionEl, region)) {
regionEl.value = region;
}
if (memoryEl && hasSelectOption(memoryEl, memory)) {
memoryEl.value = memory;
}
if (priceEl && hasSelectOption(priceEl, price)) {
priceEl.value = price;
}
if (currencyEl && (currency === 'CNY' || currency === 'USD')) {
currencyEl.value = currency;
}
if (searchEl && search) {
searchEl.value = search;
}
renderSourceHint(params);
syncFiltersFromControls();
2026-02-09 17:56:23 +08:00
}
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);
2026-02-10 16:54:06 +08:00
updateResultCount(sorted.length, allPlans.length);
updateLowestMetric(sorted);
2026-02-09 17:56:23 +08:00
var tbody = document.getElementById('table-body');
tbody.innerHTML = '';
if (sorted.length === 0) {
2026-02-10 11:49:01 +08:00
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>';
2026-02-09 17:56:23 +08:00
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;
// 区域筛选
2026-02-11 17:38:06 +08:00
if (filters.region && !matchesRegion(plan, filters.region)) return false;
2026-02-09 17:56:23 +08:00
// 内存筛选
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]);
2026-02-09 22:36:32 +08:00
var cnyPrice = getCnyPrice(plan);
if (cnyPrice == null) return false;
if (cnyPrice < min || cnyPrice > max) return false;
2026-02-09 17:56:23 +08:00
}
// 搜索筛选
if (filters.search) {
2026-02-09 18:07:42 +08:00
var searchText = (plan.provider + ' ' + plan.name + ' ' + plan.countries).toLowerCase();
2026-02-09 17:56:23 +08:00
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) {
2026-02-09 22:36:32 +08:00
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];
}
2026-02-09 17:56:23 +08:00
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');
2026-02-09 22:36:32 +08:00
var currentPrice = getPriceValue(plan, filters.currency);
var displayPrice = currentPrice ? currentPrice.symbol + currentPrice.value.toFixed(2) : '—';
2026-02-11 17:38:06 +08:00
var officialUrl = (plan.official_url || '').trim();
2026-02-09 17:56:23 +08:00
2026-02-10 13:48:58 +08:00
var btnText = (window.I18N_JS && window.I18N_JS.btn_visit) || '访问';
2026-02-11 17:38:06 +08:00
var linkCell = officialUrl
? '<a href="' + escapeAttr(officialUrl) + '" target="_blank" rel="noopener noreferrer nofollow" class="btn-link">' + btnText + '</a>'
: '-';
2026-02-10 13:48:58 +08:00
2026-02-09 18:07:42 +08:00
tr.innerHTML =
2026-02-09 17:56:23 +08:00
'<td>' + escapeHtml(plan.provider) + '</td>' +
2026-02-09 18:07:42 +08:00
'<td>' + escapeHtml(plan.countries) + '</td>' +
2026-02-09 17:56:23 +08:00
'<td>' + escapeHtml(plan.name) + '</td>' +
'<td>' + plan.vcpu + '</td>' +
'<td>' + plan.memory_gb + ' GB</td>' +
'<td>' + plan.storage_gb + ' GB</td>' +
2026-02-09 18:07:42 +08:00
'<td>' + (plan.bandwidth_mbps ? plan.bandwidth_mbps + ' Mbps' : '-') + '</td>' +
2026-02-09 17:56:23 +08:00
'<td>' + plan.traffic + '</td>' +
2026-02-09 22:36:32 +08:00
'<td class="col-price">' + displayPrice + '</td>' +
2026-02-11 17:38:06 +08:00
'<td class="col-link">' + linkCell + '</td>';
2026-02-09 17:56:23 +08:00
return tr;
}
2026-02-11 17:38:06 +08:00
function escapeAttr(text) {
if (text == null || text === '') return '';
var div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML.replace(/"/g, '&quot;');
}
2026-02-09 17:56:23 +08:00
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>';
}
2026-02-10 16:54:06 +08:00
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);
2026-02-11 17:38:06 +08:00
var regionText = getDisplayRegion(plan);
if (regionText) {
regionText.split(',').forEach(function(part) {
var token = part.trim();
if (token) regionSet.add(token);
});
}
2026-02-10 16:54:06 +08:00
});
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);
}
2026-02-09 17:56:23 +08:00
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();