160 lines
5.9 KiB
JavaScript
160 lines
5.9 KiB
JavaScript
(function () {
|
|
const tableBody = document.getElementById('table-body');
|
|
const filterProvider = document.getElementById('filter-provider');
|
|
const filterRegion = document.getElementById('filter-region');
|
|
const filterMemory = document.getElementById('filter-memory');
|
|
const filterCurrency = document.getElementById('filter-currency');
|
|
const btnReset = document.getElementById('btn-reset');
|
|
|
|
let allPlans = [];
|
|
|
|
function unique(values) {
|
|
return [...new Set(values)].filter(Boolean).sort();
|
|
}
|
|
|
|
function getDisplayRegion(plan) {
|
|
return (plan.countries && plan.countries.trim()) ? plan.countries.trim() : (plan.region || "—");
|
|
}
|
|
|
|
function getAllRegionOptions(plans) {
|
|
const set = new Set();
|
|
plans.forEach(function (p) {
|
|
if (p.countries) {
|
|
p.countries.split(',').forEach(function (s) {
|
|
const t = s.trim();
|
|
if (t) set.add(t);
|
|
});
|
|
}
|
|
if (p.region && p.region.trim()) set.add(p.region.trim());
|
|
});
|
|
return unique([...set]);
|
|
}
|
|
|
|
function fillFilterOptions() {
|
|
const providers = unique(allPlans.map(function (p) { return p.provider; }));
|
|
const regions = getAllRegionOptions(allPlans);
|
|
|
|
filterProvider.innerHTML = '<option value="">全部</option>';
|
|
providers.forEach(function (p) {
|
|
const opt = document.createElement('option');
|
|
opt.value = p;
|
|
opt.textContent = p;
|
|
filterProvider.appendChild(opt);
|
|
});
|
|
|
|
filterRegion.innerHTML = '<option value="">全部</option>';
|
|
regions.forEach(function (r) {
|
|
const opt = document.createElement('option');
|
|
opt.value = r;
|
|
opt.textContent = r;
|
|
filterRegion.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
function formatPrice(plan) {
|
|
const isCNY = filterCurrency.value === 'CNY';
|
|
let price = isCNY ? plan.price_cny : plan.price_usd;
|
|
let symbol = isCNY ? '¥' : '$';
|
|
if (price == null || price === '' || isNaN(price)) {
|
|
price = isCNY ? plan.price_usd : plan.price_cny;
|
|
symbol = isCNY ? '$' : '¥';
|
|
}
|
|
if (price == null || price === '' || isNaN(price)) return "—";
|
|
return symbol + Number(price).toLocaleString();
|
|
}
|
|
|
|
function showVal(val, suffix) {
|
|
if (val == null || val === '' || (typeof val === 'number' && isNaN(val))) return "—";
|
|
return suffix ? val + suffix : val;
|
|
}
|
|
|
|
function applyFilters() {
|
|
const provider = filterProvider.value;
|
|
const region = filterRegion.value;
|
|
const memoryMin = parseInt(filterMemory.value, 10) || 0;
|
|
|
|
return allPlans.filter(function (plan) {
|
|
if (provider && plan.provider !== provider) return false;
|
|
if (region) {
|
|
const matchRegion = getDisplayRegion(plan) === region ||
|
|
(plan.countries && plan.countries.split(',').map(function (s) { return s.trim(); }).indexOf(region) !== -1) ||
|
|
plan.region === region;
|
|
if (!matchRegion) return false;
|
|
}
|
|
if (memoryMin && (plan.memory_gb == null || plan.memory_gb < memoryMin)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function renderTable(plans) {
|
|
const colCount = 10;
|
|
if (plans.length === 0) {
|
|
tableBody.innerHTML = '<tr><td colspan="' + colCount + '" class="empty-state"><p>没有符合条件的方案</p></td></tr>';
|
|
return;
|
|
}
|
|
|
|
tableBody.innerHTML = plans.map(function (plan) {
|
|
const url = (plan.official_url || "").trim();
|
|
const linkCell = url
|
|
? '<a href="' + escapeAttr(url) + '" target="_blank" rel="noopener noreferrer">官网</a>'
|
|
: "—";
|
|
return (
|
|
'<tr>' +
|
|
'<td class="provider">' + escapeHtml(plan.provider) + '</td>' +
|
|
'<td class="region">' + escapeHtml(getDisplayRegion(plan)) + '</td>' +
|
|
'<td>' + escapeHtml(plan.name) + '</td>' +
|
|
'<td>' + showVal(plan.vcpu) + '</td>' +
|
|
'<td>' + showVal(plan.memory_gb, ' GB') + '</td>' +
|
|
'<td>' + showVal(plan.storage_gb, ' GB') + '</td>' +
|
|
'<td>' + showVal(plan.bandwidth_mbps, ' Mbps') + '</td>' +
|
|
'<td>' + (plan.traffic ? escapeHtml(plan.traffic) : '—') + '</td>' +
|
|
'<td class="col-price">' + formatPrice(plan) + '</td>' +
|
|
'<td class="col-link">' + linkCell + '</td>' +
|
|
'</tr>'
|
|
);
|
|
}).join('');
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
const div = document.createElement('div');
|
|
div.textContent = s;
|
|
return div.innerHTML.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
if (s == null || s === '') return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = String(s);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function refresh() {
|
|
const filtered = applyFilters();
|
|
renderTable(filtered);
|
|
}
|
|
|
|
filterProvider.addEventListener('change', refresh);
|
|
filterRegion.addEventListener('change', refresh);
|
|
filterMemory.addEventListener('change', refresh);
|
|
filterCurrency.addEventListener('change', refresh);
|
|
|
|
btnReset.addEventListener('click', function () {
|
|
filterProvider.value = '';
|
|
filterRegion.value = '';
|
|
filterMemory.value = '0';
|
|
filterCurrency.value = 'CNY';
|
|
refresh();
|
|
});
|
|
|
|
fetch('/api/plans')
|
|
.then(function (res) { return res.json(); })
|
|
.then(function (plans) {
|
|
allPlans = plans;
|
|
fillFilterOptions();
|
|
refresh();
|
|
})
|
|
.catch(function () {
|
|
tableBody.innerHTML = '<tr><td colspan="10" class="empty-state"><p>加载失败,请刷新页面</p></td></tr>';
|
|
});
|
|
})();
|