Files
eSIM-Tools/index.html

400 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" data-page="home">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="eSIM 工具集,支持 Giffgaff 与 Simyo 的 eSIM 申请与管理、二维码生成及验证码处理。">
<!-- Sentry 错误监控配置(必须在其他脚本之前) -->
<script>
// Sentry 配置 - 在 Netlify 部署时会通过环境变量注入
window.SENTRY_DSN = ''; // 在 Netlify 环境变量中配置
window.SENTRY_ENVIRONMENT = 'production';
window.SENTRY_RELEASE = 'esim-tools@unknown';
</script>
<script src="/src/js/sentry-loader.js"></script>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://browser.sentry-cdn.com https://www.googletagmanager.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data:; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com https://*.sentry.io; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com https://www.google.com;">
<title>eSIM Tools - Giffgaff & Simyo</title>
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#6366f1">
<!-- 性能优化:预连接与预加载关键资源 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://browser.sentry-cdn.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
<link rel="preconnect" href="https://qrcode.show" crossorigin>
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://appapi.simyo.nl" crossorigin>
<!-- Favicon -->
<link rel="icon" href="/src/assets/favicon.ico" type="image/x-icon">
<!-- 关键CSS优先preload 后备 rel=stylesheet -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" as="style" onload="this.rel='stylesheet'">
<noscript><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"></noscript>
<!-- Font Awesome直接引入避免 onload 触发器被 CSP 阻断导致样式未生效 -->
<link rel="stylesheet" href="/src/assets/fontawesome/all.min.css">
<!-- Font Awesome 资源路径woff2在 /src/assets/fontawesome/,确保已发布且 CSP 允许 font-src 'self' -->
<!-- 本地样式文件 -->
<link rel="preload" href="/src/styles/design-system.css" as="style" onload="this.rel='stylesheet'">
<link rel="preload" href="/src/styles/animations.css" as="style" onload="this.rel='stylesheet'">
<link rel="stylesheet" href="/src/styles/fa-local.css">
<link rel="stylesheet" href="/src/styles/notification.css">
<meta name="build-rev" content="v1">
<style>
/* Design System: 语义化令牌与统一规范 */
:root{
/* Brand */
--brand-start:#6366f1; /* indigo-500 */
--brand-end:#8b5cf6; /* violet-500 */
--accent-ok:#10b981; /* emerald-500 */
--accent-warn:#f59e0b; /* amber-500 */
--accent-danger:#ef4444; /* red-500 */
/* Surface & Text */
/* 背景改为淡色 #f2f5c4 */
--bg-grad-start:#f2f5c4;
--bg-grad-end:#f1f5bb;
--surface:#ffffff;
--surface-alpha:92%;
--text-primary:#1f2937; /* slate-800 */
--text-secondary:#374151; /* gray-700 */
--text-muted:#6b7280; /* gray-500 */
/* Radius & Elevation & Spacing */
--radius-lg:18px;
--shadow-sm:0 4px 10px rgba(0,0,0,.08);
--shadow-md:0 10px 30px rgba(0,0,0,.10);
--shadow-lg:0 18px 40px rgba(0,0,0,.15);
--ring:0 0 0 3px rgba(99,102,241,.35);
--s1:8px; --s2:12px; --s3:16px; --s4:20px; --s5:24px; --s6:32px;
/* Motion */
--ease-standard:cubic-bezier(.2,.8,.2,1);
--duration-fast:.18s;
--duration-normal:.28s;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, var(--bg-grad-start) 0%, var(--bg-grad-end) 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--s5);
color: var(--text-primary);
}
.main-container{
background: color-mix(in oklab, white var(--surface-alpha), transparent);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-radius: var(--radius-lg);
padding: clamp(20px, 4vw, 40px);
box-shadow: var(--shadow-lg);
max-width: 960px;
width: 100%;
text-align: center;
border: 1px solid color-mix(in oklab, white 70%, transparent);
}
.language-switcher{
display:flex;
justify-content:flex-end;
width:100%;
margin-bottom: var(--s3);
}
.lang-switch{
display:inline-flex;
align-items:center;
gap:6px;
padding:6px 16px;
border-radius:999px;
border:1px solid rgba(99,102,241,.25);
background: rgba(255,255,255,.7);
color: var(--text-primary);
font-weight:600;
cursor:pointer;
transition: transform var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard);
}
.lang-switch:hover{
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.lang-switch:focus-visible{
outline:none;
box-shadow: var(--ring);
}
.header{ margin-bottom: var(--s6); }
.header h1{
font-size: clamp(1.9rem, 3vw, 2.6rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: var(--s2);
letter-spacing: .2px;
display:flex;align-items:center;justify-content:center;gap:10px;
}
.header .subtitle{
font-size: clamp(.95rem, 1.9vw, 1.12rem);
color: var(--text-muted);
margin: 0;
}
.header-actions{
display: flex;
gap: var(--s2);
justify-content: center;
flex-wrap: wrap;
margin-top: var(--s3);
}
.github-btn, .kofi-btn{
font-size: 16px;
font-weight: 600;
padding: 10px 18px;
display:inline-flex;
align-items:center;
justify-content:center;
border-radius: 999px;
transition: transform var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard);
gap: 8px;
box-shadow: var(--shadow-sm);
text-decoration: none !important;
outline: none;
will-change: transform;
border: 1px solid rgba(255,255,255,.18);
}
.github-btn{ background: #24292e; color:#fff !important; }
.github-btn:hover{ transform: translateY(-1px); box-shadow: var(--shadow-md); background:#111; }
.kofi-btn{ background: #FF5E5B; color:#fff !important; }
.kofi-btn:hover{ transform: translateY(-1px); box-shadow: var(--shadow-md); background:#e64a47; }
.github-btn:focus-visible, .kofi-btn:focus-visible{ outline: none; box-shadow: var(--ring); }
.tools-grid{
display:grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(16px, 3vw, 28px);
margin-top: var(--s6);
}
@media (max-width: 720px){ .tools-grid{ grid-template-columns: 1fr; } }
.tool-card{
background: var(--surface);
border-radius: var(--radius-lg);
padding: clamp(20px, 3.2vw, 28px);
box-shadow: var(--shadow-md);
transition: transform var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
border: 2px solid transparent;
position: relative;
overflow: hidden;
will-change: transform, box-shadow;
}
.tool-card::before{
content: '';
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
}
.tool-card:hover{
transform: translateY(-6px);
box-shadow: var(--shadow-lg);
border-color: color-mix(in oklab, var(--primary) 40%, transparent);
}
.tool-card:focus-within{ box-shadow: var(--ring); }
/* 品牌色注入 */
.giffgaff-card{ --primary:#ff6b00; --secondary:#ff8c42; }
.simyo-card{ --primary:#e74c3c; --secondary:#c0392b; }
.tool-icon{
font-size: clamp(2.2rem, 3.2vw, 2.8rem);
color: var(--primary);
margin-bottom: var(--s3);
}
.tool-title{
font-size: clamp(1.2rem, 2.2vw, 1.4rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: var(--s2);
}
.tool-description{
color: var(--text-muted);
margin-bottom: var(--s4);
line-height: 1.65;
}
.tool-features{
list-style: none;
padding: 0;
margin-bottom: var(--s4);
text-align: left;
}
.tool-features li{
display: flex;
align-items: baseline;
gap: 10px;
padding: 6px 0;
color: var(--text-secondary);
}
.tool-features li i{ color: var(--primary); flex: 0 0 auto; }
.tool-features li span{ flex: 1 1 auto; }
.tool-btn{
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: #fff;
border: none;
padding: 12px 22px;
border-radius: 999px;
font-size: 1rem;
font-weight: 800;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 10px;
transition: transform var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), filter var(--duration-fast) var(--ease-standard);
box-shadow: 0 6px 16px color-mix(in oklab, var(--primary) 35%, transparent);
outline: none;
will-change: transform, filter;
}
.tool-btn:hover{ transform: translateY(-2px); text-decoration: none; color: #fff; filter: brightness(1.03); }
.tool-btn:focus-visible{ box-shadow: var(--ring); }
.footer{
margin-top: var(--s6);
padding-top: var(--s5);
border-top: 1px solid color-mix(in oklab, #ecf0f1 85%, transparent);
color: #6b7280;
font-size: .94rem;
line-height: 1.6;
text-align: center;
}
/* 入场动画IntersectionObserver 触发,避免 setTimeout 抖动 */
.reveal{ opacity: 0; transform: translateY(12px); transition: opacity .5s var(--ease-standard), transform .5s var(--ease-standard); will-change: opacity, transform; }
.reveal.show{ opacity: 1; transform: translateY(0); }
/* 降低动效:尊重用户的减少动态偏好 */
@media (prefers-reduced-motion: reduce){
.reveal{ transition: none; transform: none; opacity: 1; }
.tool-card, .github-btn, .tool-btn{ transition: none!important; }
}
/* 小屏进一步优化 */
@media (max-width: 360px){
.github-btn{ padding: 9px 14px; font-size: 14px; }
.tool-btn{ padding: 10px 18px; font-size: .95rem; }
}
</style>
<!-- Google tag (gtag.js) 延迟加载 -->
<script type="module" src="/src/js/captcha-client.js"></script>
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-HGX38HMNL1"></script>
</head>
<body aria-label="eSIM 工具选择">
<div class="main-container" role="region" aria-label="eSIM 工具选择">
<div class="language-switcher">
<button id="languageSwitcher" class="lang-switch" type="button" aria-label="切换语言到英文">🌐 English</button>
</div>
<div class="header">
<h1><i class="fas fa-sim-card" aria-hidden="true"></i> <span class="header-title-text">eSIM 工具集</span></h1>
<p class="subtitle">轻松管理您的 Giffgaff 与 Simyo eSIM 配置</p>
<div class="header-actions">
<a href="https://github.com/Silentely/eSIM-Tools" target="_blank" rel="noopener noreferrer" class="github-btn" aria-label="在新窗口打开 GitHub 仓库">
<i class="fab fa-github" aria-hidden="true"></i> GitHub
</a>
<a href="https://ko-fi.com/debug402" target="_blank" rel="noopener noreferrer" class="kofi-btn" aria-label="Buy me a coffee on Ko-fi">
<i class="fas fa-coffee" aria-hidden="true"></i> <span>Buy Me a Coffee</span>
</a>
</div>
</div>
<div class="tools-grid" aria-live="polite">
<!-- Giffgaff工具卡片 -->
<section class="tool-card giffgaff-card reveal" tabindex="-1" aria-labelledby="giffgaff-title" aria-describedby="giffgaff-desc">
<div class="tool-icon" aria-hidden="true">
<i class="fas fa-mobile-alt"></i>
</div>
<h2 id="giffgaff-title" class="tool-title"><span class="tool-title-text">Giffgaff eSIM 工具</span></h2>
<p id="giffgaff-desc" class="tool-description">
<span class="tool-description-text">完整的 Giffgaff eSIM 申请与管理,支持 OAuth 认证、MFA 验证与 GraphQL API 调用</span>
</p>
<ul class="tool-features">
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>OAuth 2.0 PKCE 认证</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>邮件验证码与 MFA 支持</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>GraphQL API 集成</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>自动生成 eSIM 二维码</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>完善的错误处理</span></li>
</ul>
<a href="/giffgaff" class="tool-btn" aria-describedby="giffgaff-desc">
<i class="fas fa-arrow-right" aria-hidden="true"></i><span>使用 Giffgaff 工具</span>
</a>
</section>
<!-- Simyo工具卡片 -->
<section class="tool-card simyo-card reveal" tabindex="-1" aria-labelledby="simyo-title" aria-describedby="simyo-desc">
<div class="tool-icon" aria-hidden="true">
<i class="fas fa-sim-card"></i>
</div>
<h2 id="simyo-title" class="tool-title"><span class="tool-title-text">Simyo 工具</span></h2>
<p id="simyo-desc" class="tool-description">
<span class="tool-description-text">专为 Simyo NL 用户的 eSIM 管理,支持设备更换与短信验证码处理</span>
</p>
<ul class="tool-features">
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>简单快捷的登录验证</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>设备更换完整流程</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>短信验证码支持</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>一键生成二维码</span></li>
<li><i class="fa-solid fa-check" aria-hidden="true"></i><span>安装确认功能</span></li>
</ul>
<a href="/simyo" class="tool-btn" aria-describedby="simyo-desc">
<i class="fas fa-arrow-right" aria-hidden="true"></i><span>使用 Simyo 工具</span>
</a>
</section>
</div>
<footer class="footer" aria-label="免责声明与隐私说明">
<p>
<i class="fas fa-info-circle me-2" aria-hidden="true"></i>
本工具仅供个人使用,请遵守相关服务条款。<br>
<i class="fas fa-shield-alt me-2" aria-hidden="true"></i>
所有数据处理均在本地进行,保护您的隐私安全。
</p>
</footer>
</div>
<!-- 将 bootstrap 脚本 defer减少阻塞 -->
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<!-- 首页逻辑脚本(无内联) -->
<script defer src="/src/js/home.js"></script>
<!-- 统一版权页脚(自动注入年份/品牌) -->
<script type="module" src="/src/js/bootstrap-footer.js"></script>
<!-- 国际化初始化 -->
<script type="module" src="/src/js/init-home-i18n.js"></script>
<!-- 通知系统初始化 -->
<script type="module">
import NotificationService from '/src/js/modules/notification-service.js';
document.addEventListener('DOMContentLoaded', () => {
NotificationService.init();
});
</script>
<!-- 性能优化脚本 -->
<script src="/src/js/performance.js"></script>
</body>
</html>