This commit is contained in:
ddrwode
2026-02-10 16:54:06 +08:00
parent 6b309fb03f
commit ce25e6b0f8
5 changed files with 865 additions and 83 deletions

122
app.py
View File

@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
"""云服务器价格对比 - Flask 应用"""
import io
import os
from time import monotonic
from datetime import datetime, timezone
from urllib.parse import urlencode
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, send_file
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, send_file, send_from_directory
from werkzeug.middleware.proxy_fix import ProxyFix
from sqlalchemy import text, func, or_
from sqlalchemy.orm import joinedload
@@ -496,7 +497,9 @@ def _humanize_time(dt, lang=None):
return ""
active_lang = lang or session.get("lang", "zh")
if dt.tzinfo is None:
now = datetime.utcnow()
# 兼容历史“无时区”时间:按 UTC 解释后与当前 UTC 进行比较,避免 utcnow 弃用告警
dt = dt.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
else:
now = datetime.now(dt.tzinfo)
delta = now - dt
@@ -830,32 +833,57 @@ def _forum_redirect_with_msg(post_id, text_msg):
# 首页多语言文案(中文 / English
I18N = {
"zh": {
"tagline": "云服务器价格一目了然",
"filter_provider": "厂商",
"filter_region": "区域",
"meta_title": "全球 VPS 价格与配置对比 | 云价眼",
"meta_description": "面向技术与采购团队的云服务器价格情报平台:统一对比主流厂商 VPS 月付价格、配置与区域,支持快速筛选并直达官方购买页。",
"meta_keywords": "VPS价格对比,云服务器采购,云主机报价,云厂商比价,企业云成本,阿里云腾讯云DigitalOceanVultr",
"og_title": "云价眼 | 全球 VPS 价格与配置决策台",
"og_description": "为团队采购与技术选型提供可比价的云服务器数据视图,快速定位成本与性能平衡点。",
"og_locale": "zh_CN",
"schema_webapp_description": "面向团队采购与技术选型的 VPS 价格与配置对比平台。",
"schema_table_about": "云价眼 - 全球 VPS 价格与配置决策台",
"schema_table_name": "VPS 价格与配置对比表",
"schema_table_description": "主流云厂商 VPS 方案的配置、区域与月付价格数据",
"tagline": "面向团队采购的云服务器价格情报",
"hero_kicker": "企业云资源采购情报",
"hero_title": "全球 VPS 价格与配置决策台",
"hero_lede": "聚合主流云厂商公开报价,统一月付口径与配置维度,帮助技术与采购团队更快完成方案筛选与预算评估。",
"hero_trust_1": "主流云厂商持续收录",
"hero_trust_2": "统一月付与配置口径",
"hero_trust_3": "直达官方购买与文档",
"metric_total_plans": "可比较方案",
"metric_providers": "覆盖厂商",
"metric_regions": "覆盖区域",
"metric_lowest": "筛选后最低月价",
"filters_title": "采购筛选控制台",
"filters_subtitle": "按厂商、区域、资源规格与预算快速收敛候选方案。",
"table_caption": "价格与配置根据筛选条件实时刷新,用于初步比选与预算评估。",
"filter_provider": "供应商",
"filter_region": "区域市场",
"filter_memory": "内存 ≥",
"filter_price": "价格区间",
"filter_currency": "货币",
"search_placeholder": "搜索厂商、配置...",
"filter_currency": "计价货币",
"search_placeholder": "搜索供应商、方案或区域...",
"all": "全部",
"unlimited": "不限",
"btn_reset": "重置筛选",
"th_provider": "厂商",
"th_country": "国家",
"th_config": "配置",
"btn_reset": "清空筛选",
"btn_visit": "查看官网",
"th_provider": "供应商",
"th_country": "区域",
"th_config": "实例规格",
"th_vcpu": "vCPU",
"th_memory": "内存",
"th_storage": "存储",
"th_bandwidth": "带宽",
"th_traffic": "流量",
"th_price": "月付价",
"th_action": "操作",
"disclaimer": "* 价格仅供参考,以各厂商官网为准。部分为按量/包年折算月价",
"footer_note": "数据仅供参考 · 请以云厂商官网实时价为准",
"th_price": "月付参考",
"th_action": "官方链接",
"disclaimer": "* 数据来自公开页面与规则换算,可能存在时差或促销偏差;下单前请以厂商官网实时价格与条款为准",
"footer_note": "仅作采购调研参考 · 请以云厂商官网实时价为准",
"contact_label": "联系我们",
"empty_state": "未找到匹配的方案",
"load_error": "数据加载失败,请刷新页面重试",
"search_label": "",
"search_label": "关键词检",
"result_count_pattern": "当前筛选:{visible} / {total} 个方案",
"price_under50": "< ¥50",
"price_50_100": "¥50-100",
"price_100_300": "¥100-300",
@@ -865,32 +893,57 @@ I18N = {
"usd": "美元 ($)",
},
"en": {
"tagline": "VPS & cloud server prices at a glance",
"meta_title": "Global VPS Pricing & Configuration Comparison | VPS Price",
"meta_description": "Pricing intelligence for engineering and procurement teams: compare VPS monthly costs, specs, and regions across major providers with normalized criteria.",
"meta_keywords": "VPS pricing comparison,cloud server procurement,provider pricing benchmark,cloud cost planning,infrastructure buying",
"og_title": "VPS Price | Global VPS Pricing Decision Console",
"og_description": "A procurement-ready view of VPS pricing and specs across major providers for faster, more confident infrastructure decisions.",
"og_locale": "en_US",
"schema_webapp_description": "A pricing and configuration comparison platform for VPS procurement and technical planning.",
"schema_table_about": "VPS Price - Global VPS Pricing Decision Console",
"schema_table_name": "VPS Pricing and Configuration Table",
"schema_table_description": "Comparable monthly pricing, specs, and region data across mainstream VPS providers",
"tagline": "Cloud pricing intelligence for engineering and procurement teams",
"hero_kicker": "Enterprise Infrastructure Intelligence",
"hero_title": "Global VPS Pricing Decision Console",
"hero_lede": "Aggregate public VPS offers, normalize monthly pricing and specs, and help engineering and procurement teams shortlist options faster.",
"hero_trust_1": "Major providers continuously tracked",
"hero_trust_2": "Normalized monthly pricing and specs",
"hero_trust_3": "Direct links to official purchase pages",
"metric_total_plans": "Comparable Plans",
"metric_providers": "Providers Covered",
"metric_regions": "Regions Covered",
"metric_lowest": "Lowest Monthly Price",
"filters_title": "Procurement Filter Console",
"filters_subtitle": "Narrow candidates by provider, region, resource profile, and budget range.",
"table_caption": "Pricing and specs refresh in real time based on active filters for quicker shortlist decisions.",
"filter_provider": "Provider",
"filter_region": "Region",
"filter_memory": "Memory ≥",
"filter_price": "Price range",
"filter_currency": "Currency",
"search_placeholder": "Search provider, config...",
"search_placeholder": "Search provider, plan, or region...",
"all": "All",
"unlimited": "Any",
"btn_reset": "Reset",
"btn_reset": "Clear filters",
"btn_visit": "Visit Site",
"th_provider": "Provider",
"th_country": "Country",
"th_config": "Config",
"th_country": "Region",
"th_config": "Plan Spec",
"th_vcpu": "vCPU",
"th_memory": "Memory",
"th_storage": "Storage",
"th_bandwidth": "Bandwidth",
"th_traffic": "Traffic",
"th_price": "Monthly",
"th_action": "Action",
"disclaimer": "* Prices are indicative. See provider sites for current rates.",
"footer_note": "Data for reference only. Check provider sites for latest pricing.",
"th_price": "Monthly Price",
"th_action": "Official Link",
"disclaimer": "* Data is compiled from public sources and normalization rules. Final billing terms and live pricing are determined by each provider.",
"footer_note": "For research and shortlisting only. Always verify latest pricing on official provider websites.",
"contact_label": "Contact",
"empty_state": "No matching plans found",
"load_error": "Failed to load data. Please refresh.",
"search_label": "Search",
"search_label": "Keyword Search",
"result_count_pattern": "Showing {visible} of {total} plans",
"price_under50": "< 50",
"price_50_100": "50-100",
"price_100_300": "100-300",
@@ -939,6 +992,23 @@ def index():
)
@app.route("/assets/<path:filename>")
def legacy_assets(filename):
"""
兼容历史内容中的 /assets/* 链接:
- 若 static/assets 下存在目标文件则直接返回
- 否则回退到站点标识图,避免前端出现 404 噪音
"""
assets_dir = os.path.join(app.static_folder or "", "assets")
candidate = os.path.normpath(os.path.join(assets_dir, filename))
assets_dir_abs = os.path.abspath(assets_dir)
candidate_abs = os.path.abspath(candidate)
if candidate_abs.startswith(assets_dir_abs + os.sep) and os.path.isfile(candidate_abs):
rel_path = os.path.relpath(candidate_abs, assets_dir_abs)
return send_from_directory(assets_dir_abs, rel_path)
return redirect(url_for("static", filename="img/site-logo-mark.svg"), code=302)
@app.route("/api/plans")
def api_plans():
now_ts = monotonic()

View File

@@ -1217,6 +1217,154 @@
}
}
/* ==========================================================================
Commercial Refresh (2026-02)
========================================================================== */
.forum-page {
background:
radial-gradient(880px 330px at 6% -12%, rgba(3, 105, 161, 0.07), transparent 64%),
radial-gradient(760px 300px at 92% -20%, rgba(15, 23, 42, 0.08), transparent 68%),
var(--bg);
}
.forum-header {
backdrop-filter: blur(14px);
background: rgba(248, 250, 252, 0.88);
border-bottom: 1px solid rgba(148, 163, 184, 0.26);
}
.forum-header::before {
height: 1px;
opacity: 1;
background: linear-gradient(90deg, rgba(3, 105, 161, 0.2), rgba(15, 23, 42, 0.1), rgba(3, 105, 161, 0.2));
}
.forum-primary-nav a,
.forum-link {
font-weight: 600;
}
.forum-user-chip {
border-color: rgba(148, 163, 184, 0.38);
background: rgba(255, 255, 255, 0.92);
color: #475569;
}
.forum-shell {
padding-top: 1.2rem;
}
.topic-stream,
.side-card,
.topic-post-card,
.comment-form-card {
border: 1px solid rgba(148, 163, 184, 0.27);
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.08),
0 16px 32px rgba(15, 23, 42, 0.06);
}
.topic-head {
background: linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(248, 250, 252, 0.58));
color: #475569;
}
.topic-row:hover {
background: rgba(241, 245, 249, 0.82);
}
.topic-avatar {
background: linear-gradient(145deg, rgba(3, 105, 161, 0.13), rgba(15, 23, 42, 0.08));
color: #0f172a;
}
.topic-title {
color: #020617;
}
.topic-title:hover {
color: #0369a1;
}
.topic-meta,
.topic-stat,
.topic-result,
.side-empty {
color: #64748b;
}
.side-card h3 {
color: #0f172a;
letter-spacing: -0.01em;
}
.forum-btn-primary {
background: #0f172a;
border: 1px solid #0f172a;
}
.forum-btn-primary:hover {
background: #020617;
border-color: #020617;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
}
.forum-btn-muted {
border-color: rgba(148, 163, 184, 0.45);
}
.forum-btn-muted:hover,
.forum-btn-muted.active {
border-color: rgba(3, 105, 161, 0.45);
background: rgba(3, 105, 161, 0.08);
}
.page-link {
border-color: rgba(148, 163, 184, 0.38);
background: rgba(255, 255, 255, 0.9);
}
.page-link.active {
border-color: #0f172a;
background: #0f172a;
}
.post-form input[type="text"],
.post-form input[type="password"],
.post-form textarea,
.post-form select,
.form-group input,
.form-group textarea,
.form-group select {
border-color: rgba(148, 163, 184, 0.48);
border-radius: 10px;
}
.post-form input[type="text"]:focus,
.post-form input[type="password"]:focus,
.post-form textarea:focus,
.post-form select:focus,
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
border-color: rgba(3, 105, 161, 0.6);
box-shadow: 0 0 0 3px rgba(3, 105, 161, 0.12);
}
@media (max-width: 768px) {
.forum-shell {
padding-top: 1rem;
}
.topic-stream,
.side-card,
.topic-post-card,
.comment-form-card {
border-radius: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;

View File

@@ -2505,3 +2505,463 @@ html {
transform: none;
}
}
/* ==========================================================================
Commercial Refresh (2026-02)
========================================================================== */
:root {
--font-sans: "Plus Jakarta Sans", "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.homepage-body {
background:
radial-gradient(1100px 460px at 12% -12%, rgba(3, 105, 161, 0.08), transparent 62%),
radial-gradient(1000px 420px at 88% -18%, rgba(15, 23, 42, 0.08), transparent 64%),
var(--bg);
}
.homepage-body .header {
position: sticky;
top: 0;
z-index: 120;
backdrop-filter: blur(14px);
background: rgba(248, 250, 252, 0.88);
border-bottom: 1px solid rgba(148, 163, 184, 0.26);
}
.homepage-body .header::before {
height: 1px;
opacity: 1;
background: linear-gradient(90deg, rgba(3, 105, 161, 0.22), rgba(15, 23, 42, 0.1), rgba(3, 105, 161, 0.22));
}
.homepage-body .header-brand {
gap: 0.9rem;
}
.homepage-body .site-logo {
width: clamp(172px, 22vw, 232px);
}
.homepage-body .tagline {
font-size: 0.82rem;
letter-spacing: 0.01em;
}
.homepage-body .header-nav a {
font-weight: 600;
color: #334155;
}
.homepage-body .header-nav a:hover {
color: #0f172a;
background: rgba(3, 105, 161, 0.1);
}
.homepage-main {
padding-top: 1.65rem;
padding-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 1.15rem;
}
.hero-panel {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
gap: 1rem;
padding: 1.2rem;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.28);
background:
linear-gradient(135deg, rgba(248, 250, 252, 0.95), rgba(241, 245, 249, 0.95));
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.08),
0 18px 36px rgba(15, 23, 42, 0.07);
}
.hero-copy {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.hero-kicker {
margin: 0;
display: inline-flex;
align-items: center;
width: fit-content;
font-size: 0.76rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #0f172a;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(3, 105, 161, 0.1);
border: 1px solid rgba(3, 105, 161, 0.2);
}
.hero-title {
margin: 0;
font-size: clamp(1.6rem, 3.1vw, 2.2rem);
line-height: 1.15;
letter-spacing: -0.02em;
color: #020617;
}
.hero-lede {
margin: 0;
color: #334155;
font-size: 0.98rem;
max-width: 66ch;
}
.hero-trust-row {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.hero-trust-row span {
display: inline-flex;
align-items: center;
font-size: 0.76rem;
color: #334155;
padding: 0.35rem 0.6rem;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.36);
background: rgba(255, 255, 255, 0.75);
}
.hero-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
.metric-card {
border-radius: 12px;
padding: 0.82rem 0.85rem;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(148, 163, 184, 0.3);
}
.metric-label {
margin: 0;
font-size: 0.75rem;
color: #64748b;
letter-spacing: 0.02em;
}
.metric-value {
margin: 0.28rem 0 0;
font-size: 1.32rem;
line-height: 1;
color: #020617;
font-weight: 700;
font-family: var(--font-mono);
}
.homepage-body .filters {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.95rem;
padding: 1rem;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(255, 255, 255, 0.92);
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.08),
0 14px 28px rgba(15, 23, 42, 0.06);
}
.homepage-body .filters:hover {
border-color: rgba(51, 65, 85, 0.28);
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.08),
0 20px 34px rgba(15, 23, 42, 0.08);
}
.filters-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.filters-title {
margin: 0;
font-size: 1.02rem;
letter-spacing: -0.01em;
color: #0f172a;
}
.filters-subtitle {
margin: 0.28rem 0 0;
color: #64748b;
font-size: 0.84rem;
}
.homepage-body .result-count {
margin: 0;
color: #0f172a;
font-size: 0.82rem;
font-weight: 600;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.3);
background: rgba(241, 245, 249, 0.9);
padding: 0.38rem 0.68rem;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.66rem;
align-items: end;
}
.homepage-body .filter-group {
grid-column: span 2;
gap: 0.32rem;
}
.homepage-body .filter-group-search {
grid-column: span 3;
min-width: 0;
}
.filter-actions {
grid-column: span 1;
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
.homepage-body .filter-group label {
text-transform: none;
letter-spacing: 0.01em;
font-size: 0.74rem;
color: #475569;
font-weight: 600;
}
.homepage-body .filter-group select,
.homepage-body .filter-group-search input {
min-height: 40px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.45);
box-shadow: inset 0 1px 1px rgba(15, 23, 42, 0.02);
}
.homepage-body .filter-group select:hover,
.homepage-body .filter-group-search input:hover {
border-color: rgba(3, 105, 161, 0.45);
}
.homepage-body .btn-reset {
margin-left: 0;
min-height: 40px;
border-radius: 10px;
border: 1px solid #0f172a;
background: #0f172a;
color: #f8fafc;
font-weight: 600;
white-space: nowrap;
padding: 0.5rem 0.95rem;
}
.homepage-body .btn-reset:hover {
border-color: #020617;
background: #020617;
color: #f8fafc;
transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
}
.homepage-body .table-wrap {
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(255, 255, 255, 0.96);
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.08),
0 16px 28px rgba(15, 23, 42, 0.07);
}
.homepage-body .table-wrap::before {
display: none;
}
.table-wrap-head {
padding: 0.7rem 0.9rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.24);
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9), rgba(248, 250, 252, 0.55));
}
.table-caption {
margin: 0;
font-size: 0.78rem;
color: #64748b;
}
.homepage-body .price-table {
font-size: 0.88rem;
}
.homepage-body .price-table th {
position: sticky;
top: 0;
z-index: 3;
background: #f8fafc;
color: #475569;
}
.homepage-body .price-table tbody tr {
transition: background-color 0.2s ease;
}
.homepage-body .price-table tbody tr:hover {
background: rgba(241, 245, 249, 0.86);
transform: none;
box-shadow: none;
}
.homepage-body .price-table td.col-price {
color: #047857;
}
.homepage-body .price-table th.col-link,
.homepage-body .price-table td.col-link {
display: table-cell;
}
.homepage-body .price-table td.col-link {
white-space: nowrap;
}
.homepage-body .price-table .col-link a {
border-radius: 9px;
font-weight: 600;
}
.homepage-body .disclaimer {
margin-top: 0;
color: #64748b;
font-size: 0.78rem;
}
.homepage-body .footer {
border-top: 1px solid rgba(148, 163, 184, 0.3);
background: rgba(248, 250, 252, 0.88);
}
.floating-contact-btn {
right: 1.3rem;
bottom: 1.3rem;
width: 50px;
height: 50px;
border-radius: 14px;
border: 1px solid rgba(15, 23, 42, 0.14);
background: linear-gradient(145deg, #0f172a, #1e293b);
box-shadow: 0 12px 22px rgba(15, 23, 42, 0.24);
animation: none;
}
.floating-contact-btn:hover {
background: linear-gradient(145deg, #020617, #0f172a);
transform: translateY(-2px);
box-shadow: 0 16px 28px rgba(15, 23, 42, 0.26);
}
.floating-contact-btn:active {
transform: translateY(0);
}
.floating-contact-icon {
width: 24px;
height: 24px;
}
@media (max-width: 1024px) {
.hero-panel {
grid-template-columns: 1fr;
}
.hero-metrics {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.filter-grid {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.homepage-body .filter-group {
grid-column: span 2;
}
.homepage-body .filter-group-search {
grid-column: span 4;
}
.filter-actions {
grid-column: span 2;
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.homepage-main {
padding-top: 1.05rem;
gap: 0.9rem;
}
.hero-panel {
padding: 0.9rem;
border-radius: 14px;
}
.hero-title {
font-size: 1.52rem;
}
.hero-lede {
font-size: 0.9rem;
}
.hero-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filter-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.homepage-body .filter-group,
.homepage-body .filter-group-search,
.filter-actions {
grid-column: span 2;
}
.filter-actions {
justify-content: stretch;
}
.homepage-body .btn-reset {
width: 100%;
}
.homepage-body .price-table th,
.homepage-body .price-table td {
padding: 0.54rem 0.62rem;
}
.floating-contact-btn {
right: 1rem;
bottom: 1rem;
width: 46px;
height: 46px;
border-radius: 12px;
}
}

View File

@@ -8,6 +8,7 @@
// ==================== 全局变量 ====================
var allPlans = [];
var isEnglish = window.LANG === 'en';
// 排序状态
var currentSort = {
@@ -71,6 +72,7 @@
// 优先使用服务端直出的数据,首屏无需再请求 /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;
@@ -82,6 +84,7 @@
})
.then(function(data) {
allPlans = data;
updateSummaryMetrics(allPlans);
populateFilters();
renderTable();
})
@@ -218,6 +221,8 @@
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 = '';
@@ -342,6 +347,59 @@
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);

View File

@@ -3,24 +3,24 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - Global VPS Price Comparison</title>
<title>{{ t.meta_title }}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/site-logo-mark.svg') }}">
<meta name="description" content="云服务器 VPS 价格对比表阿里云、腾讯云、华为云、DigitalOcean、Vultr、Linode、AWS Lightsail 等厂商月付价格与配置对比,支持按区域、内存筛选,一键跳转官网。">
<meta name="keywords" content="云服务器价格,VPS价格对比,阿里云价格,腾讯云价格,DigitalOcean,Vultr,Linode,云主机月付">
<meta name="description" content="{{ t.meta_description }}">
<meta name="keywords" content="{{ t.meta_keywords }}">
<link rel="canonical" href="{{ site_url }}/">
<!-- Open Graph / 社交分享 -->
<meta property="og:type" content="website">
<meta property="og:url" content="{{ site_url }}/">
<meta property="og:title" content="{{ site_name }} - 云服务器价格对比">
<meta property="og:description" content="云服务器 VPS 月付价格对比阿里云、腾讯云、DigitalOcean、Vultr 等,支持筛选与官网跳转。">
<meta property="og:locale" content="zh_CN">
<meta property="og:title" content="{{ t.og_title }}">
<meta property="og:description" content="{{ t.og_description }}">
<meta property="og:locale" content="{{ t.og_locale }}">
<!-- JSON-LD 结构化数据,便于搜索引擎理解 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "{{ site_name }}",
"description": "云服务器 VPS 月付价格对比表,支持多厂商筛选与官网跳转。",
"description": "{{ t.schema_webapp_description }}",
"url": "{{ site_url }}",
"applicationCategory": "UtilitiesApplication"
}
@@ -29,17 +29,17 @@
{
"@context": "https://schema.org",
"@type": "Table",
"about": "云价眼 - 云服务器 VPS 价格对比",
"name": "VPS 价格对比表",
"description": "各云厂商 VPS 方案配置与月付价格"
"about": "{{ t.schema_table_about }}",
"name": "{{ t.schema_table_name }}",
"description": "{{ t.schema_table_description }}"
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+SC:wght@400;500;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body data-lang="{{ lang }}">
<body data-lang="{{ lang }}" class="homepage-body">
<header class="header">
<div class="header-inner">
<div class="header-brand">
@@ -76,53 +76,95 @@
</div>
</header>
<main class="main">
<main class="main homepage-main">
<section class="hero-panel">
<div class="hero-copy">
<p class="hero-kicker">{{ t.hero_kicker }}</p>
<h1 class="hero-title">{{ t.hero_title }}</h1>
<p class="hero-lede">{{ t.hero_lede }}</p>
<div class="hero-trust-row">
<span>{{ t.hero_trust_1 }}</span>
<span>{{ t.hero_trust_2 }}</span>
<span>{{ t.hero_trust_3 }}</span>
</div>
</div>
<div class="hero-metrics">
<article class="metric-card">
<p class="metric-label">{{ t.metric_total_plans }}</p>
<p class="metric-value" id="metric-total-plans">--</p>
</article>
<article class="metric-card">
<p class="metric-label">{{ t.metric_providers }}</p>
<p class="metric-value" id="metric-providers">--</p>
</article>
<article class="metric-card">
<p class="metric-label">{{ t.metric_regions }}</p>
<p class="metric-value" id="metric-regions">--</p>
</article>
<article class="metric-card">
<p class="metric-label">{{ t.metric_lowest }}</p>
<p class="metric-value" id="metric-lowest">--</p>
</article>
</div>
</section>
<section class="filters">
<div class="filter-group">
<label for="filter-provider">{{ t.filter_provider }}</label>
<select id="filter-provider">
<option value="">{{ t.all }}</option>
</select>
<div class="filters-head">
<div>
<h2 class="filters-title">{{ t.filters_title }}</h2>
<p class="filters-subtitle">{{ t.filters_subtitle }}</p>
</div>
<p class="result-count" id="result-count">--</p>
</div>
<div class="filter-group">
<label for="filter-region">{{ t.filter_region }}</label>
<select id="filter-region">
<option value="">{{ t.all }}</option>
</select>
<div class="filter-grid">
<div class="filter-group">
<label for="filter-provider">{{ t.filter_provider }}</label>
<select id="filter-provider">
<option value="">{{ t.all }}</option>
</select>
</div>
<div class="filter-group">
<label for="filter-region">{{ t.filter_region }}</label>
<select id="filter-region">
<option value="">{{ t.all }}</option>
</select>
</div>
<div class="filter-group">
<label for="filter-memory">{{ t.filter_memory }}</label>
<select id="filter-memory">
<option value="0">{{ t.unlimited }}</option>
<option value="1">1 GB</option>
<option value="2">2 GB</option>
<option value="4">4 GB</option>
<option value="8">8 GB</option>
</select>
</div>
<div class="filter-group">
<label for="filter-price">{{ t.filter_price }}</label>
<select id="filter-price">
<option value="0">{{ t.unlimited }}</option>
<option value="0-50">{{ t.price_under50 }}</option>
<option value="50-100">{{ t.price_50_100 }}</option>
<option value="100-300">{{ t.price_100_300 }}</option>
<option value="300-500">{{ t.price_300_500 }}</option>
<option value="500-99999">{{ t.price_over500 }}</option>
</select>
</div>
<div class="filter-group">
<label for="filter-currency">{{ t.filter_currency }}</label>
<select id="filter-currency">
<option value="CNY">{{ t.cny }}</option>
<option value="USD">{{ t.usd }}</option>
</select>
</div>
<div class="filter-group filter-group-search">
<label for="search-input">{{ t.search_label }}</label>
<input type="text" id="search-input" placeholder="{{ t.search_placeholder }}" />
</div>
<div class="filter-actions">
<button type="button" class="btn-reset" id="btn-reset">{{ t.btn_reset }}</button>
</div>
</div>
<div class="filter-group">
<label for="filter-memory">{{ t.filter_memory }}</label>
<select id="filter-memory">
<option value="0">{{ t.unlimited }}</option>
<option value="1">1 GB</option>
<option value="2">2 GB</option>
<option value="4">4 GB</option>
<option value="8">8 GB</option>
</select>
</div>
<div class="filter-group">
<label for="filter-price">{{ t.filter_price }}</label>
<select id="filter-price">
<option value="0">{{ t.unlimited }}</option>
<option value="0-50">{{ t.price_under50 }}</option>
<option value="50-100">{{ t.price_50_100 }}</option>
<option value="100-300">{{ t.price_100_300 }}</option>
<option value="300-500">{{ t.price_300_500 }}</option>
<option value="500-99999">{{ t.price_over500 }}</option>
</select>
</div>
<div class="filter-group">
<label for="filter-currency">{{ t.filter_currency }}</label>
<select id="filter-currency">
<option value="CNY">{{ t.cny }}</option>
<option value="USD">{{ t.usd }}</option>
</select>
</div>
<div class="filter-group filter-group-search">
<label for="search-input">{{ t.search_label }}</label>
<input type="text" id="search-input" placeholder="{{ t.search_placeholder }}" />
</div>
<button type="button" class="btn-reset" id="btn-reset">{{ t.btn_reset }}</button>
</section>
<!-- 广告位 2表格上方。可放置矩形或横条广告 -->
@@ -132,6 +174,9 @@
<!-- 服务器列表 -->
<section class="table-wrap">
<div class="table-wrap-head">
<p class="table-caption">{{ t.table_caption }}</p>
</div>
<table class="price-table">
<thead>
<tr>
@@ -185,7 +230,8 @@
window.I18N_JS = {
empty_state: {{ t.empty_state|tojson }},
load_error: {{ t.load_error|tojson }},
btn_visit: {{ ('访问' if lang == 'zh' else 'Visit')|tojson }}
btn_visit: {{ t.btn_visit|tojson }},
result_count_pattern: {{ t.result_count_pattern|tojson }}
};
// 首屏直出数据,避免等待 /api/plans 再渲染表格,加快首屏
window.__INITIAL_PLANS__ = {{ initial_plans_json|tojson }};