mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
feat(performance): 优化性能并添加离线支持
- 新增 .babelrc 配置文件,使用 Babel 进行代码转换和压缩 - 添加 PERFORMANCE.md 文件,详细说明性能优化措施 - 在 README.md 中增加性能优化相关说明 - 更新 index.html,添加 Service Worker 注册和性能优化脚本
This commit is contained in:
21
.babelrc
Normal file
21
.babelrc
Normal 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
233
PERFORMANCE.md
Normal 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文件
|
||||
44
README.md
44
README.md
@@ -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欧元话费赠送**
|
||||
|
||||
### 🎁 Giffgaff邀请奖励
|
||||
新用户开卡可享受**额外5英镑话费赠送**
|
||||
|
||||
## 🚀 本地部署
|
||||
|
||||
### 快速开始
|
||||
@@ -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调用文档
|
||||
- [ ] **用户指南**: 更新用户使用指南,包含新的自动化流程
|
||||
- [ ] **开发文档**: 添加自动化脚本的开发说明
|
||||
|
||||
89
index.html
89
index.html
@@ -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
27350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -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
24
postcss.config.js
Normal 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
159
scripts/compress.js
Normal 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
122
scripts/optimize-images.js
Normal 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 };
|
||||
@@ -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
8
src/js/giffgaff.js
Normal 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
8
src/js/main.js
Normal 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
310
src/js/performance.js
Normal 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
8
src/js/simyo.js
Normal 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
252
src/styles/animations.css
Normal 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
144
src/sw.js
Normal 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
114
webpack.config.js
Normal 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'
|
||||
};
|
||||
Reference in New Issue
Block a user