From ce25e6b0f81907e13ea12c90caf3742087f90280 Mon Sep 17 00:00:00 2001 From: ddrwode <34234@3来 34> Date: Tue, 10 Feb 2026 16:54:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=93=88=E5=93=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 122 ++++++++--- static/css/forum.css | 148 +++++++++++++ static/css/style.css | 460 +++++++++++++++++++++++++++++++++++++++ static/js/main-simple.js | 58 +++++ templates/index.html | 160 +++++++++----- 5 files changed, 865 insertions(+), 83 deletions(-) diff --git a/app.py b/app.py index 127ca5c..1d67924 100644 --- a/app.py +++ b/app.py @@ -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/") +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() diff --git a/static/css/forum.css b/static/css/forum.css index a78421e..564cc40 100644 --- a/static/css/forum.css +++ b/static/css/forum.css @@ -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; diff --git a/static/css/style.css b/static/css/style.css index ecbb76c..8bef15d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; + } +} diff --git a/static/js/main-simple.js b/static/js/main-simple.js index bb98e66..31c16cb 100644 --- a/static/js/main-simple.js +++ b/static/js/main-simple.js @@ -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 = '' + message + ''; } + 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); diff --git a/templates/index.html b/templates/index.html index 74b31fe..6da92a5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,24 +3,24 @@ - {{ site_name }} - Global VPS Price Comparison + {{ t.meta_title }} - - + + - - - + + + - + - +
@@ -76,53 +76,95 @@
-
+
+
+
+

{{ t.hero_kicker }}

+

{{ t.hero_title }}

+

{{ t.hero_lede }}

+
+ {{ t.hero_trust_1 }} + {{ t.hero_trust_2 }} + {{ t.hero_trust_3 }} +
+
+
+
+

{{ t.metric_total_plans }}

+

--

+
+
+

{{ t.metric_providers }}

+

--

+
+
+

{{ t.metric_regions }}

+

--

+
+
+

{{ t.metric_lowest }}

+

--

+
+
+
+
-
- - +
+
+

{{ t.filters_title }}

+

{{ t.filters_subtitle }}

+
+

--

-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
-
- - -
-
- - -
-
- - -
- -
@@ -132,6 +174,9 @@
+
+

{{ t.table_caption }}

+
@@ -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 }};