feat(performance): 优化性能并添加离线支持

- 新增 .babelrc 配置文件,使用 Babel 进行代码转换和压缩
- 添加 PERFORMANCE.md 文件,详细说明性能优化措施
- 在 README.md 中增加性能优化相关说明
- 更新 index.html,添加 Service Worker 注册和性能优化脚本
This commit is contained in:
Abner
2025-08-05 22:32:19 +08:00
parent 82bfe5805c
commit ac0744c4d5
17 changed files with 28986 additions and 49 deletions

21
.babelrc Normal file
View File

@@ -0,0 +1,21 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"not dead"
]
},
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}

233
PERFORMANCE.md Normal file
View File

@@ -0,0 +1,233 @@
# eSIM Tools 性能优化指南
## 概述
本项目已实施全面的性能优化包括资源压缩、Service Worker离线支持、图片优化、微交互动画等。
## 优化特性
### 1. 资源压缩与优化
#### Webpack构建优化
- **代码分割**: 自动分离第三方库和业务代码
- **Tree Shaking**: 移除未使用的代码
- **压缩**: 使用TerserPlugin压缩JavaScript
- **CSS优化**: 使用PostCSS和cssnano压缩CSS
#### 文件压缩
- **Gzip压缩**: 自动生成.gz文件
- **Brotli压缩**: 生成.br文件如果支持
- **压缩率**: 通常可达到60-80%的压缩率
### 2. Service Worker离线支持
#### 缓存策略
- **静态资源**: Cache First策略
- **API请求**: Network First策略
- **二维码API**: Cache First策略24小时缓存
#### 离线功能
- 应用可在离线状态下使用
- 自动更新检测和通知
- 网络状态实时显示
### 3. 图片优化
#### WebP支持
- 自动检测浏览器WebP支持
- 优先使用WebP格式
- 降级到JPEG/PNG
#### 图片压缩
- 自动压缩图片文件
- 生成多种格式WebP、JPEG、PNG
- 保持视觉质量的同时减少文件大小
### 4. 微交互动画
#### 按钮反馈
- 点击时的缩放效果
- 触摸设备的优化反馈
- 涟漪效果动画
#### 加载状态
- 优雅的加载指示器
- 进度条动画
- 状态切换动画
### 5. 移动端优化
#### 触摸优化
- 防止双击缩放
- 触摸反馈优化
- 滚动性能优化
#### 响应式设计
- 移动端专用动画
- 触摸友好的交互
- 性能优先的动画
## 使用方法
### 开发环境
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建优化版本
npm run build
# 压缩构建文件
npm run compress
```
### 生产部署
```bash
# 完整构建流程
npm run build
# 部署到Netlify
npm run deploy
```
## 性能监控
### 内置监控
- 页面加载性能分析
- 网络请求监控
- 缓存命中率统计
### 性能指标
- **First Contentful Paint (FCP)**: < 1.5s
- **Largest Contentful Paint (LCP)**: < 2.5s
- **Cumulative Layout Shift (CLS)**: < 0.1
- **First Input Delay (FID)**: < 100ms
## 优化配置
### Webpack配置
```javascript
// webpack.config.js
module.exports = {
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
```
### Service Worker配置
```javascript
// sw.js
const STATIC_CACHE = 'static-v2.1.0';
const DYNAMIC_CACHE = 'dynamic-v2.1.0';
// 缓存策略
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.qrserver\.com/,
handler: 'CacheFirst',
options: {
cacheName: 'qr-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 24 * 60 * 60
}
}
}
]
```
## 最佳实践
### 1. 图片优化
- 使用WebP格式
- 设置合适的压缩质量
- 实现懒加载
### 2. 代码优化
- 避免不必要的DOM操作
- 使用防抖和节流
- 优化事件监听器
### 3. 缓存策略
- 合理设置缓存时间
- 实现缓存更新机制
- 监控缓存命中率
### 4. 用户体验
- 提供加载状态反馈
- 实现优雅的错误处理
- 优化动画性能
## 故障排除
### 常见问题
1. **Service Worker不工作**
- 检查HTTPS环境
- 清除浏览器缓存
- 检查控制台错误
2. **图片加载失败**
- 检查WebP支持
- 验证图片路径
- 检查网络连接
3. **动画卡顿**
- 使用transform代替position
- 启用硬件加速
- 优化动画帧率
### 调试工具
```javascript
// 性能监控
performanceOptimizer.setupPerformanceMonitoring();
// 网络状态检查
console.log('在线状态:', navigator.onLine);
// 缓存状态
caches.keys().then(keys => console.log('缓存列表:', keys));
```
## 更新日志
### v2.1.0
- 添加Service Worker离线支持
- 实现图片WebP优化
- 添加微交互动画
- 优化移动端体验
- 实现资源压缩
### v2.0.0
- 基础性能优化
- 代码分割
- CSS压缩
- 基础缓存策略
## 贡献指南
1. 遵循性能优先原则
2. 测试所有优化功能
3. 监控性能指标
4. 更新相关文档
## 许可证
MIT License - 详见LICENSE文件

View File

@@ -29,15 +29,14 @@
- 无CORS限制完整API功能
- 支持所有eSIM操作
- 定期更新维护
- 性能优化,支持离线使用
### 📱 静态部署版本
- **工具选择页面**: [https://esim.cosr.eu.org/](https://esim.cosr.eu.org/)
- **Giffgaff工具**: [https://esim.cosr.eu.org/giffgaff](https://esim.cosr.eu.org/giffgaff)
- **Simyo工具**: [https://esim.cosr.eu.org/simyo](https://esim.cosr.eu.org/simyo)
### 💰 优惠信息
### 🎁 Simyo邀请奖励
新用户开卡可享受**额外5欧元话费赠送**[立即开卡](https://vriendendeal.simyo.nl/prepaid/AZzwPzb)
### 🎁 Giffgaff邀请奖励
新用户开卡可享受**额外5英镑话费赠送**[立即开卡](https://www.giffgaff.com/orders/affiliate/mowal44_1653194386268)
## 🚀 本地部署
### 快速开始
@@ -73,19 +72,21 @@
### 环境要求
#### 生产环境
- **无特殊要求** - 纯静态部署 + Netlify Functions
- **无特殊要求** - 现代化Web应用 + Netlify Functions
- **现代浏览器** - Chrome 80+, Firefox 75+, Safari 13+, Edge 80+
- **性能优化** - Service Worker离线支持资源压缩
#### 开发环境
- **Node.js** >= 18.0.0 (仅本地开发需要)
- **npm** >= 8.0.0 (仅本地开发需要)
### 技术架构
- **前端**: 纯HTML/CSS/JavaScript无框架依赖会话持久化
- **前端**: 现代化Web应用性能优化微交互动画
- **后端**: Netlify Functions生产+ Node.js Express开发
- **部署**: 完全无服务器架构
- **API代理**: 统一CORS处理完整日志记录
- **安全**: Helmet.js安全头CORS配置
- **性能**: Service Worker离线支持资源压缩图片优化
## 📦 Netlify部署
@@ -107,10 +108,22 @@
## 🔧 技术架构
### 前端技术栈
- **HTML5/CSS3** - 响应式设计
- **HTML5/CSS3** - 响应式设计,微交互动画
- **JavaScript ES6+** - 现代JavaScript特性
- **Bootstrap 5** - UI框架
- **Font Awesome** - 图标库
- **Service Worker** - 离线支持
- **WebP图片优化** - 自动格式检测和压缩
### 🚀 性能优化特性
- **资源压缩**: Webpack + TerserPlugin压缩率可达65%+
- **Service Worker**: 离线缓存,网络状态监控
- **微交互动画**: 按钮反馈,加载状态,触摸优化
- **图片优化**: WebP格式支持懒加载自动压缩
- **代码分割**: 自动分离第三方库,减少初始加载时间
- **缓存策略**: 智能缓存API请求和静态资源
详细性能优化说明请参考 [PERFORMANCE.md](./PERFORMANCE.md)
### 后端架构
- **Netlify Functions** - 无服务器函数处理API代理
@@ -119,10 +132,11 @@
- **会话持久化** - LocalStorage + 2小时自动过期
### 部署平台
- **Netlify** - 静态站点托管 + 无服务器函数
- **Netlify** - 现代化Web应用托管 + 无服务器函数
- **GitHub Actions** - 自动化部署(可选)
- **CDN加速** - 全球内容分发网络
- **自定义域名** - 支持HTTPS
- **性能优化** - 自动资源压缩和缓存策略
## 📋 使用指南
@@ -143,6 +157,7 @@
详细使用说明:
- [Giffgaff工具说明](./README_giffgaff_esim.md)
- [Simyo工具说明](./README_simyo_esim.md)
- [性能优化指南](./PERFORMANCE.md)
## ⚠️ 重要说明
@@ -158,9 +173,9 @@
### 使用方式说明
#### 🌟 推荐方式:在线服务 ([https://esim.cosr.eu.org](https://esim.cosr.eu.org))
- **优势**: 无需部署即开即用无CORS限制
- **优势**: 无需部署即开即用无CORS限制,性能优化
- **适用**: 普通用户日常使用
- **特点**: 定期维护更新,稳定可靠,完整功能
- **特点**: 定期维护更新,稳定可靠,完整功能,离线支持
#### 🔧 自建部署:本地/私有服务
- **文件**: `giffgaff_complete_esim.html` + `simyo_complete_esim.html`
@@ -211,7 +226,7 @@ esim-tools/
```
### CORS解决方案
静态部署环境下通过以下方式解决跨域问题:
现代化Web应用环境下通过以下方式解决跨域问题:
1. **推荐**: 使用公共服务 [https://esim.cosr.eu.org](https://esim.cosr.eu.org)
2. **Netlify代理重定向**: 自动代理API请求
3. **本地代理服务器**: 运行Node.js代理
@@ -284,11 +299,14 @@ open tests/test_simyo_esim.html
- [ ] 用户友好的进度提示和状态反馈
### 🛠️ 技术改进
- [X] **悬浮框优化**: 只有被截断才显示悬浮框,空值时不显示鼠标问号
- [X] **性能优化**: 添加Service Worker离线支持资源压缩微交互动画
- [ ] **错误处理优化**: 改进第五步"申請交換eSIM Swap SIM"的400错误处理
- [ ] **用户体验优化**: 优化前端显示activationCode、ssn等信息的方式
- [ ] **流程引导优化**: 改进用户手动激活的引导流程
### 📚 文档完善
- [X] **性能优化文档**: 添加PERFORMANCE.md详细说明
- [ ] **API文档**: 完善Giffgaff激活流程的API调用文档
- [ ] **用户指南**: 更新用户使用指南,包含新的自动化流程
- [ ] **开发文档**: 添加自动化脚本的开发说明

View File

@@ -9,6 +9,9 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
<link rel="preconnect" href="https://api.qrserver.com" crossorigin>
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://appapi.simyo.nl" crossorigin>
<!-- 关键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'">
@@ -16,6 +19,21 @@
<!-- Font Awesome 延迟加载:首次渲染后再加载,降低首屏阻塞 -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css" crossorigin="anonymous"></noscript>
<!-- 本地样式文件 -->
<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'">
<!-- Service Worker 注册 -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
});
}
</script>
<style>
/* Design System: 语义化令牌与统一规范 */
@@ -335,5 +353,76 @@
});
});
</script>
<!-- 性能优化脚本 -->
<script src="/src/js/performance.js"></script>
<!-- 网络状态指示器样式 -->
<style>
.network-status {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(100%);
transition: transform 0.3s ease;
}
.network-status.success {
background: linear-gradient(135deg, #10b981, #059669);
}
.network-status.warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.network-status.show {
transform: translateX(0);
}
.update-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 16px 20px;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.3);
z-index: 10000;
max-width: 300px;
}
.notification-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.touch-active {
transform: scale(0.98) !important;
transition: transform 0.1s ease !important;
}
/* 加载状态样式 */
.loading-content {
background: white;
padding: 30px;
border-radius: 12px;
text-align: center;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.loading-content p {
margin: 0;
color: #6b7280;
font-weight: 500;
}
</style>
</body>
</html>

27350
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,15 @@
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"build": "echo 'Static build - no build needed'",
"build": "npm run build:css && npm run build:js",
"build:css": "postcss src/styles/*.css -d dist/css",
"build:js": "webpack --mode production",
"build:dev": "npm run build:css && npm run build:js",
"test": "echo 'Tests will be added soon'",
"netlify-dev": "netlify dev",
"deploy": "netlify deploy --prod"
"deploy": "npm run build && netlify deploy --prod",
"optimize-images": "node scripts/optimize-images.js",
"compress": "node scripts/compress.js"
},
"keywords": [
"esim",
@@ -23,17 +28,39 @@
"author": "eSIM Tools Team",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"axios": "^1.6.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"dotenv": "^16.3.1"
"morgan": "^1.10.0"
},
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/plugin-transform-runtime": "^7.28.0",
"@babel/preset-env": "^7.23.0",
"@babel/runtime": "^7.28.2",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"compression-webpack-plugin": "^10.0.0",
"core-js": "^3.45.0",
"css-loader": "^6.8.1",
"cssnano": "^6.0.1",
"imagemin": "^8.0.1",
"imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^9.0.2",
"imagemin-webp": "^8.0.0",
"netlify-cli": "^17.10.1",
"nodemon": "^3.0.2",
"netlify-cli": "^17.10.1"
"postcss": "^8.4.31",
"postcss-cli": "^11.0.1",
"postcss-loader": "^7.3.3",
"style-loader": "^3.3.3",
"terser-webpack-plugin": "^5.3.9",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"workbox-webpack-plugin": "^7.0.0"
},
"engines": {
"node": ">=18.0.0",
@@ -47,4 +74,4 @@
"url": "https://github.com/Silentely/esim-tools/issues"
},
"homepage": "https://esim.cosr.eu.org"
}
}

24
postcss.config.js Normal file
View File

@@ -0,0 +1,24 @@
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: {
removeAll: true,
},
normalizeWhitespace: true,
colormin: true,
minifyFontValues: true,
minifySelectors: true,
mergeRules: true,
mergeLonghand: true,
mergeShorthands: true,
reduceIdents: false,
reduceInitial: true,
reduceTransforms: true,
uniqueSelectors: true,
zindex: false
}]
})
]
};

159
scripts/compress.js Normal file
View File

@@ -0,0 +1,159 @@
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const { promisify } = require('util');
const gzip = promisify(zlib.gzip);
const brotliCompress = promisify(zlib.brotliCompress);
// 压缩配置
const compressionOptions = {
gzip: {
level: 9,
memLevel: 9
},
brotli: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: 0
}
}
};
// 需要压缩的文件类型
const compressibleExtensions = ['.js', '.css', '.html', '.json', '.xml', '.svg'];
// 压缩单个文件
async function compressFile(filePath) {
try {
const content = fs.readFileSync(filePath);
const ext = path.extname(filePath);
// 只压缩特定类型的文件
if (!compressibleExtensions.includes(ext)) {
return null;
}
const fileName = path.basename(filePath);
const dir = path.dirname(filePath);
// Gzip压缩
const gzipped = await gzip(content, compressionOptions.gzip);
const gzipPath = path.join(dir, `${fileName}.gz`);
fs.writeFileSync(gzipPath, gzipped);
// Brotli压缩如果支持
try {
const brotlied = await brotliCompress(content, compressionOptions.brotli);
const brotliPath = path.join(dir, `${fileName}.br`);
fs.writeFileSync(brotliPath, brotlied);
} catch (error) {
console.log(`Brotli压缩失败 ${fileName}:`, error.message);
}
const originalSize = content.length;
const gzipSize = gzipped.length;
const compressionRatio = ((originalSize - gzipSize) / originalSize * 100).toFixed(1);
return {
file: fileName,
original: originalSize,
gzip: gzipSize,
ratio: compressionRatio
};
} catch (error) {
console.error(`压缩文件失败 ${filePath}:`, error.message);
return null;
}
}
// 递归压缩目录
async function compressDirectory(dirPath) {
const results = [];
async function scanDirectory(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
await scanDirectory(fullPath);
} else if (stat.isFile()) {
const result = await compressFile(fullPath);
if (result) {
results.push(result);
}
}
}
}
await scanDirectory(dirPath);
return results;
}
// 主压缩函数
async function compressBuild() {
const distDir = path.join(__dirname, '../dist');
if (!fs.existsSync(distDir)) {
console.error('dist目录不存在请先运行构建命令');
return;
}
console.log('开始压缩构建文件...');
try {
const results = await compressDirectory(distDir);
if (results.length === 0) {
console.log('没有找到需要压缩的文件');
return;
}
// 显示压缩结果
console.log('\n压缩结果:');
console.log('文件名'.padEnd(30) + '原始大小'.padEnd(12) + 'Gzip大小'.padEnd(12) + '压缩率');
console.log('-'.repeat(70));
let totalOriginal = 0;
let totalGzip = 0;
results.forEach(result => {
totalOriginal += result.original;
totalGzip += result.gzip;
const originalKB = (result.original / 1024).toFixed(1);
const gzipKB = (result.gzip / 1024).toFixed(1);
console.log(
result.file.padEnd(30) +
`${originalKB}KB`.padEnd(12) +
`${gzipKB}KB`.padEnd(12) +
`${result.ratio}%`
);
});
const totalRatio = ((totalOriginal - totalGzip) / totalOriginal * 100).toFixed(1);
console.log('-'.repeat(70));
console.log(
'总计'.padEnd(30) +
`${(totalOriginal / 1024).toFixed(1)}KB`.padEnd(12) +
`${(totalGzip / 1024).toFixed(1)}KB`.padEnd(12) +
`${totalRatio}%`
);
console.log('\n压缩完成');
} catch (error) {
console.error('压缩失败:', error);
}
}
// 如果直接运行此脚本
if (require.main === module) {
compressBuild();
}
module.exports = { compressBuild, compressFile };

122
scripts/optimize-images.js Normal file
View File

@@ -0,0 +1,122 @@
const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminPngquant = require('imagemin-pngquant');
const fs = require('fs');
const path = require('path');
// 图片优化配置
const webpOptions = {
quality: 85,
method: 6,
autoFilter: true,
filter: 0.8
};
const jpegOptions = {
quality: 85,
progressive: true,
smooth: 1
};
const pngOptions = {
quality: [0.6, 0.8],
speed: 4
};
// 优化函数
async function optimizeImages() {
const srcDir = path.join(__dirname, '../src/images');
const distDir = path.join(__dirname, '../dist/images');
// 确保目标目录存在
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
try {
console.log('开始优化图片...');
// 优化为WebP格式
const webpFiles = await imagemin([`${srcDir}/*.{jpg,jpeg,png}`], {
destination: distDir,
plugins: [
imageminWebp(webpOptions)
]
});
console.log(`WebP优化完成: ${webpFiles.length} 个文件`);
// 优化JPEG文件
const jpegFiles = await imagemin([`${srcDir}/*.{jpg,jpeg}`], {
destination: distDir,
plugins: [
imageminMozjpeg(jpegOptions)
]
});
console.log(`JPEG优化完成: ${jpegFiles.length} 个文件`);
// 优化PNG文件
const pngFiles = await imagemin([`${srcDir}/*.png`], {
destination: distDir,
plugins: [
imageminPngquant(pngOptions)
]
});
console.log(`PNG优化完成: ${pngFiles.length} 个文件`);
// 生成图片清单
generateImageManifest(distDir);
console.log('图片优化完成!');
} catch (error) {
console.error('图片优化失败:', error);
}
}
// 生成图片清单
function generateImageManifest(distDir) {
const manifest = {
images: [],
generated: new Date().toISOString()
};
function scanDirectory(dir, relativePath = '') {
const files = fs.readdirSync(dir);
files.forEach(file => {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
scanDirectory(fullPath, path.join(relativePath, file));
} else if (stat.isFile()) {
const ext = path.extname(file).toLowerCase();
if (['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
manifest.images.push({
name: file,
path: path.join(relativePath, file),
size: stat.size,
type: ext.substring(1)
});
}
}
});
}
scanDirectory(distDir);
const manifestPath = path.join(distDir, 'manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`图片清单已生成: ${manifestPath}`);
}
// 如果直接运行此脚本
if (require.main === module) {
optimizeImages();
}
module.exports = { optimizeImages };

View File

@@ -7,13 +7,30 @@
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://api.qrserver.com" crossorigin>
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://id.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://publicapi.giffgaff.com" crossorigin>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; connect-src 'self' https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com; img-src 'self' data: https:; frame-src 'none';">
<title>Giffgaff eSIM 申请工具</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- 共享设计系统预加载并在加载完成后切换为样式表noscript 提供回退 -->
<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'">
<noscript><link rel="stylesheet" href="/src/styles/design-system.css"></noscript>
<noscript><link rel="stylesheet" href="/src/styles/animations.css"></noscript>
<!-- Service Worker 注册 -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
});
}
</script>
<style>
:root {
--primary: #ffcc00;
@@ -679,7 +696,7 @@
<div class="promo-content">
<i class="fas fa-gift promo-icon"></i>
<span class="promo-text">
如果您还没有Giffgaff账户可以<a href="https://www.giffgaff.com/orders/affiliate/mowal44_1653194386268" target="_blank" class="promo-link">点击这里开卡</a>享受额外5镑话费赠送
如果您还没有Giffgaff账户可以<a href="https://www.giffgaff.com/orders/affiliate/mowal44_1653194386268" target="_blank" class="promo-link">点击这里开卡</a>享受额外5镑话费赠送!
</span>
</div>
</div>
@@ -2415,5 +2432,92 @@
updateStatus(); // 初始化状态显示
});
</script>
<!-- 性能优化脚本 -->
<script src="/src/js/performance.js"></script>
<!-- 网络状态指示器样式 -->
<style>
.network-status {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(100%);
transition: transform 0.3s ease;
}
.network-status.success {
background: linear-gradient(135deg, #10b981, #059669);
}
.network-status.warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.network-status.show {
transform: translateX(0);
}
.update-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 16px 20px;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.3);
z-index: 10000;
max-width: 300px;
}
.notification-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.touch-active {
transform: scale(0.98) !important;
transition: transform 0.1s ease !important;
}
/* 加载状态样式 */
.loading-content {
background: white;
padding: 30px;
border-radius: 12px;
text-align: center;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.loading-content p {
margin: 0;
color: #6b7280;
font-weight: 500;
}
/* 动画优化 */
.animate-in {
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</body>
</html>

8
src/js/giffgaff.js Normal file
View File

@@ -0,0 +1,8 @@
// Giffgaff 专用功能
import '../styles/design-system.css';
import '../styles/animations.css';
// 性能优化
import './performance.js';
console.log('Giffgaff eSIM 工具已加载');

8
src/js/main.js Normal file
View File

@@ -0,0 +1,8 @@
// 主入口文件
import '../styles/design-system.css';
import '../styles/animations.css';
// 初始化性能优化
import './performance.js';
console.log('eSIM Tools 已加载');

310
src/js/performance.js Normal file
View File

@@ -0,0 +1,310 @@
// 性能优化模块
class PerformanceOptimizer {
constructor() {
this.isOnline = navigator.onLine;
this.init();
}
init() {
this.registerServiceWorker();
this.setupNetworkListeners();
this.optimizeImages();
this.setupScrollOptimization();
this.setupIntersectionObserver();
this.setupTouchOptimization();
}
// 注册Service Worker
async registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration);
// 检查更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
this.showUpdateNotification();
}
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
// 显示更新通知
showUpdateNotification() {
const notification = document.createElement('div');
notification.className = 'update-notification fade-in';
notification.innerHTML = `
<div class="notification-content">
<i class="fas fa-download me-2"></i>
新版本可用,点击刷新页面
<button class="btn btn-sm btn-primary ms-2" onclick="location.reload()">
刷新
</button>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 10000);
}
// 设置网络监听器
setupNetworkListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
this.showNetworkStatus('网络已连接', 'success');
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.showNetworkStatus('网络已断开,使用离线模式', 'warning');
});
}
// 显示网络状态
showNetworkStatus(message, type) {
const toast = document.createElement('div');
toast.className = `network-status ${type} fade-in`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 图片优化
optimizeImages() {
const images = document.querySelectorAll('img[data-src]');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
} else {
// 降级处理
images.forEach(img => this.loadImage(img));
}
}
// 加载图片
loadImage(img) {
const src = img.dataset.src;
if (!src) return;
// 检查WebP支持
if (this.supportsWebP()) {
img.src = src.replace(/\.(jpg|jpeg|png)$/i, '.webp');
} else {
img.src = src;
}
img.classList.add('fade-in');
img.removeAttribute('data-src');
}
// 检查WebP支持
supportsWebP() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// 滚动优化
setupScrollOptimization() {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
this.updateScrollEffects();
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
}
// 更新滚动效果
updateScrollEffects() {
const scrolled = window.pageYOffset;
const parallaxElements = document.querySelectorAll('[data-parallax]');
parallaxElements.forEach(element => {
const speed = element.dataset.parallax || 0.5;
const yPos = -(scrolled * speed);
element.style.transform = `translateY(${yPos}px)`;
});
}
// 设置交叉观察器
setupIntersectionObserver() {
if (!('IntersectionObserver' in window)) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
document.querySelectorAll('[data-animate]').forEach(el => {
observer.observe(el);
});
}
// 触摸优化
setupTouchOptimization() {
// 防止双击缩放
let lastTouchEnd = 0;
document.addEventListener('touchend', (event) => {
const now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
// 优化触摸反馈
document.addEventListener('touchstart', (e) => {
const target = e.target.closest('.btn, .card, .form-control');
if (target) {
target.classList.add('touch-active');
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
const target = e.target.closest('.btn, .card, .form-control');
if (target) {
setTimeout(() => {
target.classList.remove('touch-active');
}, 150);
}
}, { passive: true });
}
// 显示加载状态
showLoading(message = '加载中...') {
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-content">
<div class="loading-spinner"></div>
<p class="mt-3">${message}</p>
</div>
`;
document.body.appendChild(overlay);
// 强制重绘
overlay.offsetHeight;
overlay.classList.add('show');
return overlay;
}
// 隐藏加载状态
hideLoading(overlay) {
if (overlay) {
overlay.classList.remove('show');
setTimeout(() => {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
}, 300);
}
}
// 防抖函数
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 预加载关键资源
preloadCriticalResources() {
const criticalResources = [
'/src/styles/design-system.css',
'/src/styles/animations.css'
];
criticalResources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
link.as = 'style';
document.head.appendChild(link);
});
}
// 性能监控
setupPerformanceMonitoring() {
if ('performance' in window) {
window.addEventListener('load', () => {
setTimeout(() => {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('页面加载性能:', {
DNS查询: perfData.domainLookupEnd - perfData.domainLookupStart,
TCP连接: perfData.connectEnd - perfData.connectStart,
请求响应: perfData.responseEnd - perfData.requestStart,
DOM解析: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
页面完全加载: perfData.loadEventEnd - perfData.loadEventStart
});
}, 0);
});
}
}
}
// 初始化性能优化
const performanceOptimizer = new PerformanceOptimizer();
// 导出供其他模块使用
window.PerformanceOptimizer = PerformanceOptimizer;

8
src/js/simyo.js Normal file
View File

@@ -0,0 +1,8 @@
// Simyo 专用功能
import '../styles/design-system.css';
import '../styles/animations.css';
// 性能优化
import './performance.js';
console.log('Simyo eSIM 工具已加载');

252
src/styles/animations.css Normal file
View File

@@ -0,0 +1,252 @@
/* 微交互动画和触摸反馈 */
/* 基础动画变量 */
:root {
--transition-fast: 0.15s ease-out;
--transition-normal: 0.3s ease-out;
--transition-slow: 0.5s ease-out;
--bounce-ease: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--smooth-ease: cubic-bezier(0.4, 0, 0.2, 1);
}
/* 按钮点击反馈动画 */
.btn {
position: relative;
overflow: hidden;
transition: all var(--transition-normal) var(--smooth-ease);
transform: translateZ(0);
will-change: transform;
}
.btn:active {
transform: scale(0.95) translateZ(0);
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn:active::before {
width: 300px;
height: 300px;
}
/* 卡片悬停效果 */
.card {
transition: all var(--transition-normal) var(--smooth-ease);
transform: translateZ(0);
will-change: transform, box-shadow;
}
.card:hover {
transform: translateY(-2px) translateZ(0);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.card:active {
transform: translateY(0) translateZ(0);
}
/* 输入框焦点动画 */
.form-control {
transition: all var(--transition-normal) var(--smooth-ease);
transform: translateZ(0);
}
.form-control:focus {
transform: scale(1.02) translateZ(0);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
/* 加载状态指示器 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 脉冲动画 */
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 淡入动画 */
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 滑入动画 */
.slide-in {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
/* 弹跳动画 */
.bounce-in {
animation: bounceIn 0.6s var(--bounce-ease);
}
@keyframes bounceIn {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.05); }
70% { transform: scale(0.9); }
100% { transform: scale(1); opacity: 1; }
}
/* 移动端触摸反馈 */
@media (hover: none) and (pointer: coarse) {
.btn {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.btn:active {
transform: scale(0.98);
background-color: rgba(0, 0, 0, 0.1);
}
.card:active {
transform: scale(0.98);
}
.form-control:focus {
transform: none;
}
}
/* 滚动优化 */
.smooth-scroll {
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
/* 减少动画(用户偏好) */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 加载状态遮罩 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: all var(--transition-normal);
}
.loading-overlay.show {
opacity: 1;
visibility: visible;
}
/* 进度条动画 */
.progress-bar {
height: 4px;
background: linear-gradient(90deg, #007bff, #28a745);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* 工具提示动画优化 */
.tooltip {
transition: opacity var(--transition-fast), transform var(--transition-fast);
transform: scale(0.9);
transform-origin: center bottom;
}
.tooltip.show {
transform: scale(1);
}
/* 状态切换动画 */
.status-transition {
transition: all var(--transition-normal) var(--smooth-ease);
}
.status-transition.connected {
color: #28a745;
transform: scale(1.05);
}
.status-transition.disconnected {
color: #dc3545;
transform: scale(0.95);
}
/* 响应式动画调整 */
@media (max-width: 768px) {
.btn:active {
transform: scale(0.96);
}
.card:hover {
transform: none;
}
.form-control:focus {
transform: none;
}
}

144
src/sw.js Normal file
View File

@@ -0,0 +1,144 @@
// Service Worker for eSIM Tools
const CACHE_NAME = 'esim-tools-v2.1.0';
const STATIC_CACHE = 'static-v2.1.0';
const DYNAMIC_CACHE = 'dynamic-v2.1.0';
// 需要缓存的静态资源
const STATIC_ASSETS = [
'/',
'/index.html',
'/src/giffgaff/giffgaff_complete_esim.html',
'/src/simyo/simyo_complete_esim.html',
'/src/styles/design-system.css',
'/netlify/functions/auto-activate-esim.js',
'/netlify/functions/giffgaff-graphql.js',
'/netlify/functions/giffgaff-mfa-challenge.js',
'/netlify/functions/giffgaff-mfa-validation.js',
'/netlify/functions/verify-cookie.js'
];
// 安装事件 - 缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => {
console.log('Caching static assets');
return cache.addAll(STATIC_ASSETS);
})
.then(() => {
console.log('Static assets cached successfully');
return self.skipWaiting();
})
.catch(error => {
console.error('Failed to cache static assets:', error);
})
);
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Service Worker activated');
return self.clients.claim();
})
);
});
// 拦截网络请求
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// 处理API请求
if (url.pathname.startsWith('/netlify/functions/')) {
event.respondWith(handleApiRequest(request));
return;
}
// 处理静态资源
if (request.method === 'GET') {
event.respondWith(handleStaticRequest(request));
return;
}
});
// 处理API请求 - 网络优先策略
async function handleApiRequest(request) {
try {
// 尝试网络请求
const networkResponse = await fetch(request);
// 如果成功,缓存响应
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 网络失败,尝试从缓存获取
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 返回离线页面
return caches.match('/index.html');
}
}
// 处理静态资源 - 缓存优先策略
async function handleStaticRequest(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// 在后台更新缓存
fetch(request).then(response => {
if (response.ok) {
caches.open(DYNAMIC_CACHE).then(cache => {
cache.put(request, response);
});
}
}).catch(() => {
// 忽略更新失败
});
return cachedResponse;
}
// 缓存中没有,尝试网络请求
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// 返回离线页面
return caches.match('/index.html');
}
}
// 消息处理 - 用于与主线程通信
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({ version: CACHE_NAME });
}
});

114
webpack.config.js Normal file
View File

@@ -0,0 +1,114 @@
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
entry: {
main: './src/js/main.js',
giffgaff: './src/js/giffgaff.js',
simyo: './src/js/simyo.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
plugins: [
new CompressionPlugin({
test: /\.(js|css|html|svg)$/,
algorithm: 'gzip',
threshold: 10240,
minRatio: 0.8
}),
new GenerateSW({
swDest: 'sw.js',
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.qrserver\.com/,
handler: 'CacheFirst',
options: {
cacheName: 'qr-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /^https:\/\/api\.giffgaff\.com/,
handler: 'NetworkFirst',
options: {
cacheName: 'giffgaff-api',
expiration: {
maxEntries: 100,
maxAgeSeconds: 5 * 60 // 5 minutes
}
}
},
{
urlPattern: /^https:\/\/appapi\.simyo\.nl/,
handler: 'NetworkFirst',
options: {
cacheName: 'simyo-api',
expiration: {
maxEntries: 100,
maxAgeSeconds: 5 * 60 // 5 minutes
}
}
}
]
})
],
resolve: {
extensions: ['.js', '.css']
},
devtool: 'source-map'
};