Files
vps_web/static/js/main.js
ddrwode 540db103a8 哈哈
2026-02-09 16:52:28 +08:00

219 lines
8.0 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 = [];
let isLoading = false;
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 showLoadingSkeleton() {
const colCount = 10;
const skeletonRows = Array(5).fill(0).map(function() {
return '<tr class="loading-row"><td colspan="' + colCount + '"></td></tr>';
}).join('');
tableBody.innerHTML = skeletonRows;
}
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.style.opacity = '0';
tableBody.innerHTML = plans.map(function (plan, index) {
const url = (plan.official_url || "").trim();
const linkCell = url
? '<a href="' + escapeAttr(url) + '" target="_blank" rel="noopener noreferrer">🔗 官网</a>'
: "—";
return (
'<tr style="animation: fadeIn 0.3s ease-in-out ' + (index * 0.02) + 's forwards; opacity: 0;">' +
'<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('');
// 触发淡入
setTimeout(function() {
tableBody.style.opacity = '1';
}, 10);
}
function escapeAttr(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function escapeHtml(s) {
if (s == null || s === '') return '';
const div = document.createElement('div');
div.textContent = String(s);
return div.innerHTML;
}
function refresh() {
if (isLoading) return;
const filtered = applyFilters();
renderTable(filtered);
// 更新结果计数
updateResultCount(filtered.length);
}
function updateResultCount(count) {
const existingCount = document.querySelector('.result-count');
if (existingCount) {
existingCount.textContent = '共 ' + count + ' 条结果';
}
}
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();
});
// 显示加载状态
isLoading = true;
showLoadingSkeleton();
fetch('/api/plans')
.then(function (res) {
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
})
.then(function (plans) {
allPlans = plans;
fillFilterOptions();
isLoading = false;
refresh();
// 添加结果计数显示
const filters = document.querySelector('.filters');
if (filters && !document.querySelector('.result-count')) {
const countEl = document.createElement('div');
countEl.className = 'result-count';
countEl.style.cssText = 'margin-left: auto; color: var(--text-muted); font-size: 0.9rem;';
countEl.textContent = '共 ' + plans.length + ' 条结果';
filters.appendChild(countEl);
}
})
.catch(function (error) {
isLoading = false;
console.error('加载失败:', error);
tableBody.innerHTML = '<tr><td colspan="10" class="empty-state"><p>❌ 加载失败,请刷新页面重试</p></td></tr>';
});
// 添加 CSS 动画
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.table-wrap { transition: opacity 0.3s ease; }
`;
document.head.appendChild(style);
})();