(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 filterPrice = document.getElementById('filter-price'); const filterCurrency = document.getElementById('filter-currency'); const searchInput = document.getElementById('search-input'); const btnReset = document.getElementById('btn-reset'); let allPlans = []; let filteredPlans = []; let isLoading = false; let currentSort = { column: null, order: 'asc' }; let favorites = JSON.parse(localStorage.getItem('vps_favorites') || '[]'); // ========== 工具函数 ========== 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 escapeAttr(s) { const div = document.createElement('div'); div.textContent = s; return div.innerHTML.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 showVal(val, suffix) { if (val == null || val === '' || (typeof val === 'number' && isNaN(val))) return "—"; return suffix ? val + suffix : val; } // ========== 价格处理 ========== function getPriceValue(plan) { const isCNY = filterCurrency.value === 'CNY'; let price = isCNY ? plan.price_cny : plan.price_usd; if (price == null || price === '' || isNaN(price)) { price = isCNY ? plan.price_usd : plan.price_cny; } return price != null && !isNaN(price) ? Number(price) : null; } 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 isFavorite(planId) { return favorites.indexOf(planId) !== -1; } function toggleFavorite(planId) { const index = favorites.indexOf(planId); if (index === -1) { favorites.push(planId); } else { favorites.splice(index, 1); } localStorage.setItem('vps_favorites', JSON.stringify(favorites)); refresh(); } // ========== 筛选功能 ========== function applyFilters() { const provider = filterProvider.value; const region = filterRegion.value; const memoryMin = parseInt(filterMemory.value, 10) || 0; const priceRange = filterPrice.value; const searchTerm = searchInput.value.toLowerCase().trim(); 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; // 价格区间筛选 if (priceRange && priceRange !== '0') { const price = getPriceValue(plan); if (price == null) return false; const parts = priceRange.split('-'); const min = parseFloat(parts[0]); const max = parseFloat(parts[1]); if (price < min || price > max) return false; } // 搜索筛选 if (searchTerm) { const searchableText = [ plan.provider, plan.name, getDisplayRegion(plan), plan.vcpu ? plan.vcpu + '核' : '', plan.memory_gb ? plan.memory_gb + 'G' : '' ].join(' ').toLowerCase(); if (searchableText.indexOf(searchTerm) === -1) return false; } return true; }); } // ========== 排序功能 ========== function sortPlans(plans, column, order) { if (!column) return plans; return plans.slice().sort(function (a, b) { let valA, valB; if (column === 'price') { valA = getPriceValue(a); valB = getPriceValue(b); } else { valA = a[column]; valB = b[column]; } // 处理 null 值 if (valA == null && valB == null) return 0; if (valA == null) return 1; if (valB == null) return -1; if (order === 'asc') { return valA > valB ? 1 : valA < valB ? -1 : 0; } else { return valA < valB ? 1 : valA > valB ? -1 : 0; } }); } function updateSortIcons() { document.querySelectorAll('.sortable').forEach(function (th) { const icon = th.querySelector('.sort-icon'); const column = th.getAttribute('data-sort'); if (column === currentSort.column) { icon.textContent = currentSort.order === 'asc' ? ' ↑' : ' ↓'; th.classList.add('sorted'); } else { icon.textContent = ''; th.classList.remove('sorted'); } }); } // ========== 渲染功能 ========== function fillFilterOptions() { const providers = unique(allPlans.map(function (p) { return p.provider; })); const regions = getAllRegionOptions(allPlans); filterProvider.innerHTML = ''; providers.forEach(function (p) { const opt = document.createElement('option'); opt.value = p; opt.textContent = p; filterProvider.appendChild(opt); }); filterRegion.innerHTML = ''; regions.forEach(function (r) { const opt = document.createElement('option'); opt.value = r; opt.textContent = r; filterRegion.appendChild(opt); }); } function showLoadingSkeleton() { const colCount = 10; const skeletonRows = Array(5).fill(0).map(function() { return ''; }).join(''); tableBody.innerHTML = skeletonRows; } function renderTable(plans) { const colCount = 10; if (plans.length === 0) { tableBody.innerHTML = '

🔍 没有符合条件的方案

'; return; } tableBody.style.opacity = '0'; tableBody.innerHTML = plans.map(function (plan, index) { const url = (plan.official_url || "").trim(); const isFav = isFavorite(plan.id); const favIcon = isFav ? '★' : '☆'; const favClass = isFav ? 'favorited' : ''; return ( '' + '' + escapeHtml(plan.provider) + '' + '' + escapeHtml(getDisplayRegion(plan)) + '' + '' + escapeHtml(plan.name) + '' + '' + showVal(plan.vcpu) + '' + '' + showVal(plan.memory_gb, ' GB') + '' + '' + showVal(plan.storage_gb, ' GB') + '' + '' + showVal(plan.bandwidth_mbps, ' Mbps') + '' + '' + (plan.traffic ? escapeHtml(plan.traffic) : '—') + '' + '' + formatPrice(plan) + '' + '' + '' + (url ? '官网' : '') + '' + '' ); }).join(''); setTimeout(function() { tableBody.style.opacity = '1'; attachFavoriteListeners(); }, 10); } function attachFavoriteListeners() { document.querySelectorAll('.btn-favorite').forEach(function(btn) { btn.addEventListener('click', function(e) { e.preventDefault(); const planId = parseInt(this.getAttribute('data-id')); toggleFavorite(planId); }); }); } function updateResultCount(count) { const existingCount = document.querySelector('.result-count'); if (existingCount) { existingCount.textContent = '共 ' + count + ' 条结果'; } } function refresh() { if (isLoading) return; filteredPlans = applyFilters(); const sortedPlans = sortPlans(filteredPlans, currentSort.column, currentSort.order); renderTable(sortedPlans); updateResultCount(filteredPlans.length); updateURL(); } // ========== URL 同步 ========== function updateURL() { const params = new URLSearchParams(); if (filterProvider.value) params.set('provider', filterProvider.value); if (filterRegion.value) params.set('region', filterRegion.value); if (filterMemory.value !== '0') params.set('memory', filterMemory.value); if (filterPrice.value !== '0') params.set('price', filterPrice.value); if (filterCurrency.value !== 'CNY') params.set('currency', filterCurrency.value); if (searchInput.value) params.set('search', searchInput.value); if (currentSort.column) { params.set('sort', currentSort.column); params.set('order', currentSort.order); } const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : ''); window.history.replaceState({}, '', newURL); } function loadFromURL() { const params = new URLSearchParams(window.location.search); if (params.get('provider')) filterProvider.value = params.get('provider'); if (params.get('region')) filterRegion.value = params.get('region'); if (params.get('memory')) filterMemory.value = params.get('memory'); if (params.get('price')) filterPrice.value = params.get('price'); if (params.get('currency')) filterCurrency.value = params.get('currency'); if (params.get('search')) searchInput.value = params.get('search'); if (params.get('sort')) { currentSort.column = params.get('sort'); currentSort.order = params.get('order') || 'asc'; } } // ========== 事件监听 ========== // 筛选器变化 filterProvider.addEventListener('change', refresh); filterRegion.addEventListener('change', refresh); filterMemory.addEventListener('change', refresh); filterPrice.addEventListener('change', refresh); filterCurrency.addEventListener('change', refresh); // 搜索输入(防抖) let searchTimeout; searchInput.addEventListener('input', function() { clearTimeout(searchTimeout); searchTimeout = setTimeout(refresh, 300); }); // 重置按钮 btnReset.addEventListener('click', function () { filterProvider.value = ''; filterRegion.value = ''; filterMemory.value = '0'; filterPrice.value = '0'; filterCurrency.value = 'CNY'; searchInput.value = ''; currentSort = { column: null, order: 'asc' }; updateSortIcons(); refresh(); }); // 表头排序点击 document.querySelectorAll('.sortable').forEach(function(th) { th.style.cursor = 'pointer'; th.addEventListener('click', function() { const column = this.getAttribute('data-sort'); if (currentSort.column === column) { currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc'; } else { currentSort.column = column; currentSort.order = 'asc'; } updateSortIcons(); 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(); loadFromURL(); isLoading = false; updateSortIcons(); 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; font-weight: 500;'; countEl.textContent = '共 ' + plans.length + ' 条结果'; filters.appendChild(countEl); } }) .catch(function (error) { isLoading = false; console.error('加载失败:', error); tableBody.innerHTML = '

❌ 加载失败,请刷新页面重试

'; }); // ========== 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; } .sortable { user-select: none; transition: var(--transition); } .sortable:hover { background: var(--bg-elevated) !important; color: var(--accent); } .sortable.sorted { color: var(--accent); font-weight: 600; } .sort-icon { font-size: 0.8em; margin-left: 0.25rem; } .search-bar { margin-bottom: 1rem; } .search-bar input { width: 100%; max-width: 500px; padding: 0.65rem 1rem; font-size: 0.95rem; border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--bg-card); color: var(--text); transition: var(--transition); } .search-bar input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } .search-bar input::placeholder { color: var(--text-muted); } .btn-favorite { background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 0.25rem 0.5rem; color: var(--text-muted); transition: var(--transition); margin-right: 0.5rem; } .btn-favorite:hover { color: var(--orange); transform: scale(1.2); } .favorited { background: rgba(234, 88, 12, 0.05) !important; } .favorited .btn-favorite { color: var(--orange); } `; document.head.appendChild(style); })();