mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
docs: 添加通知系统使用指南和完整实现文档
- 新增详细的通知系统使用指南,涵盖架构设计、快速开始、后端配置和故障排查 - 添加通知管理器组件,支持 success、warning、error、info 四种通知类型 - 实现通知服务,支持自动轮询和已读状态管理 - 创建 Netlify Functions 通知 API,提供通知消息查询接口 - 集成通知系统到主页面,在DOM加载完成后自动初始化 - 提供完整的样式系统,包含响应式设计和无障碍访问支持
This commit is contained in:
427
docs/guides/notification-system.md
Normal file
427
docs/guides/notification-system.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# 通知系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
eSIM-Tools 通知系统是一个轻量级的消息通知解决方案,支持在部署时自动显示重要更新和修复信息。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 组件构成
|
||||
|
||||
1. **前端通知组件** (`notification-manager.js`)
|
||||
- 轻量级Toast通知UI
|
||||
- 支持4种类型:success、warning、error、info
|
||||
- 自动显示/隐藏机制
|
||||
- 响应式设计
|
||||
|
||||
2. **通知服务** (`notification-service.js`)
|
||||
- 定期从后端获取通知
|
||||
- 防止重复显示(LocalStorage记录)
|
||||
- 自动初始化和轮询
|
||||
|
||||
3. **后端API** (`netlify/functions/notifications.js`)
|
||||
- Netlify Serverless Function
|
||||
- 提供通知消息查询接口
|
||||
- 支持多种查询模式
|
||||
|
||||
4. **样式系统** (`notification.css`)
|
||||
- 与Design System完美融合
|
||||
- 优雅的渐变背景
|
||||
- 流畅的动画效果
|
||||
- 支持减少动效(prefers-reduced-motion)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 前端使用
|
||||
|
||||
#### 基础用法
|
||||
|
||||
```javascript
|
||||
import NotificationManager from './modules/notification-manager.js';
|
||||
|
||||
// 显示成功通知
|
||||
NotificationManager.success('操作成功!');
|
||||
|
||||
// 显示警告通知
|
||||
NotificationManager.warning('请注意检查输入');
|
||||
|
||||
// 显示错误通知
|
||||
NotificationManager.error('操作失败,请重试');
|
||||
|
||||
// 显示信息通知
|
||||
NotificationManager.info('这是一条提示信息');
|
||||
```
|
||||
|
||||
#### 高级用法
|
||||
|
||||
```javascript
|
||||
// 自定义配置
|
||||
NotificationManager.show({
|
||||
message: '自定义消息',
|
||||
type: 'success', // success | warning | error | info
|
||||
duration: 8000, // 持续时间(ms),0表示不自动关闭
|
||||
closable: true // 是否显示关闭按钮
|
||||
});
|
||||
|
||||
// 手动关闭通知
|
||||
const id = NotificationManager.info('这条消息可以手动关闭');
|
||||
setTimeout(() => {
|
||||
NotificationManager.hide(id);
|
||||
}, 3000);
|
||||
|
||||
// 清除所有通知
|
||||
NotificationManager.clearAll();
|
||||
```
|
||||
|
||||
### 2. 后端配置
|
||||
|
||||
#### 添加新通知
|
||||
|
||||
编辑 `netlify/functions/notifications.js`:
|
||||
|
||||
```javascript
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: 'fix-400-error', // 唯一标识
|
||||
message: '已修复报错400问题', // 通知消息
|
||||
type: 'success', // 类型
|
||||
timestamp: '2025-01-23T10:00:00Z',
|
||||
active: true, // 是否激活
|
||||
priority: 1 // 优先级(数字越小优先级越高)
|
||||
},
|
||||
{
|
||||
id: 'new-feature',
|
||||
message: '新功能:支持批量导入',
|
||||
type: 'info',
|
||||
timestamp: '2025-01-22T15:00:00Z',
|
||||
active: true,
|
||||
priority: 2
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
#### API端点
|
||||
|
||||
**获取所有通知**
|
||||
```
|
||||
GET /.netlify/functions/notifications?mode=all
|
||||
```
|
||||
|
||||
**获取最新通知**
|
||||
```
|
||||
GET /.netlify/functions/notifications?mode=latest
|
||||
```
|
||||
|
||||
**响应格式**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "fix-400-error",
|
||||
"message": "已修复报错400问题",
|
||||
"type": "success",
|
||||
"timestamp": "2025-01-23T10:00:00Z",
|
||||
"active": true,
|
||||
"priority": 1
|
||||
},
|
||||
"timestamp": "2025-01-23T12:34:56.789Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 自动化集成
|
||||
|
||||
通知系统已自动集成到主页面 (`index.html`),会在页面加载时:
|
||||
|
||||
1. 初始化通知服务
|
||||
2. 检查是否有新通知
|
||||
3. 自动显示未读通知
|
||||
4. 每5分钟轮询一次
|
||||
|
||||
---
|
||||
|
||||
## 通知类型与样式
|
||||
|
||||
### Success (成功)
|
||||
- **颜色**: 绿色渐变
|
||||
- **图标**: check-circle
|
||||
- **用途**: 操作成功、功能修复
|
||||
|
||||
```javascript
|
||||
NotificationManager.success('已修复报错400问题');
|
||||
```
|
||||
|
||||
### Warning (警告)
|
||||
- **颜色**: 橙色渐变
|
||||
- **图标**: exclamation-triangle
|
||||
- **用途**: 重要提示、注意事项
|
||||
|
||||
```javascript
|
||||
NotificationManager.warning('请及时更新到最新版本');
|
||||
```
|
||||
|
||||
### Error (错误)
|
||||
- **颜色**: 红色渐变
|
||||
- **图标**: times-circle
|
||||
- **用途**: 错误提示、失败信息
|
||||
|
||||
```javascript
|
||||
NotificationManager.error('操作失败,请稍后重试');
|
||||
```
|
||||
|
||||
### Info (信息)
|
||||
- **颜色**: 紫色渐变
|
||||
- **图标**: info-circle
|
||||
- **用途**: 一般信息、新功能通知
|
||||
|
||||
```javascript
|
||||
NotificationManager.info('新功能:支持OAuth认证');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署场景
|
||||
|
||||
### 场景1: 修复Bug后通知用户
|
||||
|
||||
1. 修复代码中的Bug
|
||||
2. 在 `notifications.js` 中添加通知:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'fix-mfa-bug-20250123',
|
||||
message: '已修复MFA验证失败问题,请刷新页面',
|
||||
type: 'success',
|
||||
timestamp: new Date().toISOString(),
|
||||
active: true,
|
||||
priority: 1
|
||||
}
|
||||
```
|
||||
|
||||
3. 部署到Netlify
|
||||
4. 用户访问时自动显示通知
|
||||
|
||||
### 场景2: 新功能上线
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'feature-batch-import',
|
||||
message: '新功能:现在支持批量导入eSIM配置!',
|
||||
type: 'info',
|
||||
timestamp: new Date().toISOString(),
|
||||
active: true,
|
||||
priority: 2
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 重要维护通知
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'maintenance-notice',
|
||||
message: '系统将于今晚22:00-23:00进行维护',
|
||||
type: 'warning',
|
||||
timestamp: new Date().toISOString(),
|
||||
active: true,
|
||||
priority: 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 通知ID命名规范
|
||||
|
||||
使用有意义的ID,建议格式:
|
||||
```
|
||||
{type}-{feature}-{date}
|
||||
```
|
||||
|
||||
示例:
|
||||
- `fix-400-error-20250123`
|
||||
- `feature-oauth-20250120`
|
||||
- `warning-maintenance-20250125`
|
||||
|
||||
### 2. 消息内容
|
||||
|
||||
- ✅ 简洁明了(20-50字)
|
||||
- ✅ 说明问题和解决方案
|
||||
- ✅ 使用友好的语气
|
||||
- ❌ 避免技术术语
|
||||
- ❌ 避免过长的文本
|
||||
|
||||
### 3. 优先级设置
|
||||
|
||||
- **Priority 1**: 紧急修复、重要功能
|
||||
- **Priority 2**: 一般更新、新功能
|
||||
- **Priority 3**: 次要信息、提示
|
||||
|
||||
### 4. 激活/停用管理
|
||||
|
||||
及时停用过期通知:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'old-notification',
|
||||
message: '已过期的通知',
|
||||
active: false, // 停用
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 1. 清除已显示记录(用于测试)
|
||||
|
||||
打开浏览器控制台:
|
||||
|
||||
```javascript
|
||||
// 清除已显示记录,再次刷新会重新显示所有通知
|
||||
NotificationService.clearShownNotifications();
|
||||
```
|
||||
|
||||
### 2. 自定义轮询间隔
|
||||
|
||||
编辑 `notification-service.js`:
|
||||
|
||||
```javascript
|
||||
constructor() {
|
||||
this.checkInterval = 3 * 60 * 1000; // 改为3分钟
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 手动触发检查
|
||||
|
||||
```javascript
|
||||
import NotificationService from './modules/notification-service.js';
|
||||
|
||||
// 手动检查新通知
|
||||
NotificationService.checkAndShowNotifications();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1: 通知不显示
|
||||
|
||||
**检查清单**:
|
||||
1. 确认 `notification.css` 已正确引入
|
||||
2. 检查浏览器控制台是否有错误
|
||||
3. 验证后端API返回正常
|
||||
4. 清除已显示记录后重试
|
||||
|
||||
**解决方案**:
|
||||
```javascript
|
||||
// 控制台执行
|
||||
NotificationService.clearShownNotifications();
|
||||
location.reload();
|
||||
```
|
||||
|
||||
### 问题2: 样式异常
|
||||
|
||||
**可能原因**:
|
||||
- CSP策略阻止样式加载
|
||||
- CSS文件路径错误
|
||||
|
||||
**解决方案**:
|
||||
检查 `index.html` 中的样式引入:
|
||||
```html
|
||||
<link rel="stylesheet" href="/src/styles/notification.css">
|
||||
```
|
||||
|
||||
### 问题3: API调用失败
|
||||
|
||||
**检查清单**:
|
||||
1. 确认Function已正确部署
|
||||
2. 检查网络请求是否被拦截
|
||||
3. 验证CORS配置
|
||||
|
||||
---
|
||||
|
||||
## 代码规范
|
||||
|
||||
### JavaScript
|
||||
|
||||
遵循项目统一规范:
|
||||
|
||||
```javascript
|
||||
// ✅ 使用单例模式
|
||||
const notificationManager = new NotificationManager();
|
||||
export default notificationManager;
|
||||
|
||||
// ✅ 使用Logger记录关键信息
|
||||
Logger.log('[NotificationService] 检查完成');
|
||||
|
||||
// ✅ 错误处理
|
||||
try {
|
||||
await this.checkAndShowNotifications();
|
||||
} catch (error) {
|
||||
Logger.error('[NotificationService] 检查失败:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
使用Design System变量:
|
||||
|
||||
```css
|
||||
.notification {
|
||||
border-radius: var(--radius-md, 12px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **懒加载**: 通知系统仅在需要时初始化
|
||||
2. **防抖**: 5分钟轮询间隔避免频繁请求
|
||||
3. **缓存**: LocalStorage记录已显示通知
|
||||
4. **轻量级**: CSS仅3KB,JS模块化加载
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **XSS防护**: 所有消息通过 `escapeHtml` 转义
|
||||
2. **CORS**: API仅允许同源请求
|
||||
3. **数据验证**: 严格校验通知格式
|
||||
4. **LocalStorage**: 仅存储通知ID,不存储敏感数据
|
||||
|
||||
---
|
||||
|
||||
## 未来扩展
|
||||
|
||||
### 计划功能
|
||||
|
||||
1. **持久化存储**: 使用数据库存储通知
|
||||
2. **用户偏好**: 允许用户关闭特定类型通知
|
||||
3. **国际化**: 支持多语言通知消息
|
||||
4. **通知中心**: 查看历史通知
|
||||
5. **通知分组**: 按类别分组显示
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
eSIM-Tools 通知系统提供了一个简单而强大的方式来向用户传达重要信息。通过合理使用通知类型和优先级,可以有效提升用户体验。
|
||||
|
||||
**关键优势**:
|
||||
- 🚀 简单易用
|
||||
- 🎨 美观优雅
|
||||
- ⚡ 性能优异
|
||||
- 🔒 安全可靠
|
||||
- 📱 响应式设计
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-01-23
|
||||
**维护者**: eSIM Tools Team
|
||||
12
index.html
12
index.html
@@ -31,6 +31,7 @@
|
||||
<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">
|
||||
|
||||
@@ -357,8 +358,15 @@
|
||||
|
||||
<!-- 国际化初始化 -->
|
||||
<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>
|
||||
|
||||
|
||||
82
netlify/functions/notifications.js
Normal file
82
netlify/functions/notifications.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 通知消息API
|
||||
* 提供系统通知消息的查询接口
|
||||
*/
|
||||
|
||||
const { withAuth } = require('./_shared/middleware');
|
||||
|
||||
// 通知消息数据(可以从JSON文件或数据库读取)
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: 'fix-400-error',
|
||||
message: '已修复Oauth交换时报错400问题,优化了MFA验证流程',
|
||||
type: 'success',
|
||||
timestamp: '2025-11-30T10:00:00Z',
|
||||
active: true,
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
id: 'new-feature-oauth',
|
||||
message: '新功能:支持OAuth 2.0 PKCE认证流程',
|
||||
type: 'info',
|
||||
timestamp: '2025-06-20T15:30:00Z',
|
||||
active: false,
|
||||
priority: 2
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取活跃通知
|
||||
*/
|
||||
function getActiveNotifications() {
|
||||
return NOTIFICATIONS
|
||||
.filter(n => n.active)
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新通知
|
||||
*/
|
||||
function getLatestNotification() {
|
||||
const active = getActiveNotifications();
|
||||
return active.length > 0 ? active[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主处理函数
|
||||
*/
|
||||
exports.handler = withAuth(async (event, context, { auth }) => {
|
||||
const { httpMethod, queryStringParameters } = event;
|
||||
|
||||
// 仅支持GET请求
|
||||
if (httpMethod !== 'GET') {
|
||||
return {
|
||||
statusCode: 405,
|
||||
body: JSON.stringify({ error: 'Method not allowed' })
|
||||
};
|
||||
}
|
||||
|
||||
const mode = queryStringParameters?.mode || 'all';
|
||||
|
||||
let data;
|
||||
switch (mode) {
|
||||
case 'latest':
|
||||
data = getLatestNotification();
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
data = getActiveNotifications();
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
};
|
||||
}, { requireAuth: false }); // 公开接口,不需要认证
|
||||
165
src/js/modules/notification-manager.js
Normal file
165
src/js/modules/notification-manager.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 通知管理器
|
||||
* 轻量级Toast通知系统,支持多种类型和自动消失
|
||||
*/
|
||||
|
||||
import Logger from './logger.js';
|
||||
|
||||
class NotificationManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.notifications = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通知容器
|
||||
*/
|
||||
init() {
|
||||
if (this.container) return;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'notification-container';
|
||||
this.container.setAttribute('aria-live', 'polite');
|
||||
this.container.setAttribute('aria-atomic', 'true');
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
* @param {Object} options - 通知配置
|
||||
* @param {string} options.message - 通知消息
|
||||
* @param {string} options.type - 通知类型 (success|warning|error|info)
|
||||
* @param {number} options.duration - 持续时间(ms),0表示不自动关闭
|
||||
* @param {boolean} options.closable - 是否可手动关闭
|
||||
*/
|
||||
show({ message, type = 'info', duration = 5000, closable = true }) {
|
||||
const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const notification = this.createNotification(id, message, type, closable);
|
||||
|
||||
this.container.appendChild(notification);
|
||||
this.notifications.set(id, notification);
|
||||
|
||||
// 触发入场动画
|
||||
requestAnimationFrame(() => {
|
||||
notification.classList.add('show');
|
||||
});
|
||||
|
||||
// 自动关闭
|
||||
if (duration > 0) {
|
||||
setTimeout(() => this.hide(id), duration);
|
||||
}
|
||||
|
||||
Logger.log(`[Notification] ${type}: ${message}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知元素
|
||||
*/
|
||||
createNotification(id, message, type, closable) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.setAttribute('role', 'alert');
|
||||
notification.setAttribute('data-id', id);
|
||||
|
||||
const icon = this.getIcon(type);
|
||||
const closeBtn = closable ? `
|
||||
<button class="notification-close" aria-label="关闭通知" data-id="${id}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<i class="notification-icon ${icon}"></i>
|
||||
<span class="notification-message">${this.escapeHtml(message)}</span>
|
||||
</div>
|
||||
${closeBtn}
|
||||
`;
|
||||
|
||||
if (closable) {
|
||||
notification.querySelector('.notification-close').addEventListener('click', () => {
|
||||
this.hide(id);
|
||||
});
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏通知
|
||||
*/
|
||||
hide(id) {
|
||||
const notification = this.notifications.get(id);
|
||||
if (!notification) return;
|
||||
|
||||
notification.classList.remove('show');
|
||||
notification.classList.add('hide');
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
this.notifications.delete(id);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有通知
|
||||
*/
|
||||
clearAll() {
|
||||
this.notifications.forEach((_, id) => this.hide(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标类名
|
||||
*/
|
||||
getIcon(type) {
|
||||
const icons = {
|
||||
success: 'fas fa-check-circle',
|
||||
warning: 'fas fa-exclamation-triangle',
|
||||
error: 'fas fa-times-circle',
|
||||
info: 'fas fa-info-circle'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML转义
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法
|
||||
*/
|
||||
success(message, duration = 5000) {
|
||||
return this.show({ message, type: 'success', duration });
|
||||
}
|
||||
|
||||
warning(message, duration = 5000) {
|
||||
return this.show({ message, type: 'warning', duration });
|
||||
}
|
||||
|
||||
error(message, duration = 7000) {
|
||||
return this.show({ message, type: 'error', duration });
|
||||
}
|
||||
|
||||
info(message, duration = 5000) {
|
||||
return this.show({ message, type: 'info', duration });
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式
|
||||
const notificationManager = new NotificationManager();
|
||||
|
||||
// 导出
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = notificationManager;
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.NotificationManager = notificationManager;
|
||||
}
|
||||
|
||||
export default notificationManager;
|
||||
133
src/js/modules/notification-service.js
Normal file
133
src/js/modules/notification-service.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 通知服务
|
||||
* 负责从后端获取通知消息并显示
|
||||
*/
|
||||
|
||||
import Logger from './logger.js';
|
||||
import NotificationManager from './notification-manager.js';
|
||||
|
||||
class NotificationService {
|
||||
constructor() {
|
||||
this.apiUrl = '/.netlify/functions/notifications';
|
||||
this.checkInterval = 5 * 60 * 1000; // 5分钟检查一次
|
||||
this.lastCheckTime = 0;
|
||||
this.shownNotifications = this.loadShownNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务
|
||||
*/
|
||||
async init() {
|
||||
await this.checkAndShowNotifications();
|
||||
this.startPeriodicCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并显示通知
|
||||
*/
|
||||
async checkAndShowNotifications() {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}?mode=latest`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const { data } = await response.json();
|
||||
if (data && !this.hasShown(data.id)) {
|
||||
this.showNotification(data);
|
||||
this.markAsShown(data.id);
|
||||
}
|
||||
|
||||
this.lastCheckTime = Date.now();
|
||||
Logger.log('[NotificationService] 检查完成');
|
||||
} catch (error) {
|
||||
Logger.error('[NotificationService] 检查失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
*/
|
||||
showNotification(notification) {
|
||||
const { message, type = 'info' } = notification;
|
||||
NotificationManager.show({
|
||||
message,
|
||||
type,
|
||||
duration: 8000,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已显示过
|
||||
*/
|
||||
hasShown(id) {
|
||||
return this.shownNotifications.includes(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已显示
|
||||
*/
|
||||
markAsShown(id) {
|
||||
if (!this.shownNotifications.includes(id)) {
|
||||
this.shownNotifications.push(id);
|
||||
this.saveShownNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已显示记录
|
||||
*/
|
||||
loadShownNotifications() {
|
||||
try {
|
||||
const stored = localStorage.getItem('esim_shown_notifications');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存已显示记录
|
||||
*/
|
||||
saveShownNotifications() {
|
||||
try {
|
||||
localStorage.setItem('esim_shown_notifications', JSON.stringify(this.shownNotifications));
|
||||
} catch (error) {
|
||||
Logger.warn('[NotificationService] 保存失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定期检查
|
||||
*/
|
||||
startPeriodicCheck() {
|
||||
setInterval(() => {
|
||||
const elapsed = Date.now() - this.lastCheckTime;
|
||||
if (elapsed >= this.checkInterval) {
|
||||
this.checkAndShowNotifications();
|
||||
}
|
||||
}, this.checkInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除已显示记录(用于测试)
|
||||
*/
|
||||
clearShownNotifications() {
|
||||
this.shownNotifications = [];
|
||||
localStorage.removeItem('esim_shown_notifications');
|
||||
Logger.log('[NotificationService] 已清除显示记录');
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式
|
||||
const notificationService = new NotificationService();
|
||||
|
||||
// 导出
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = notificationService;
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.NotificationService = notificationService;
|
||||
}
|
||||
|
||||
export default notificationService;
|
||||
137
src/styles/notification.css
Normal file
137
src/styles/notification.css
Normal file
@@ -0,0 +1,137 @@
|
||||
/* 通知系统样式 */
|
||||
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--radius-md, 12px);
|
||||
box-shadow: var(--shadow-lg, 0 8px 28px rgba(0,0,0,0.18));
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
pointer-events: auto;
|
||||
transform: translateX(calc(100% + 20px));
|
||||
opacity: 0;
|
||||
transition: transform var(--transition-base, 200ms) var(--ease-out, cubic-bezier(.22,.61,.36,1)),
|
||||
opacity var(--transition-base, 200ms) var(--ease-out, cubic-bezier(.22,.61,.36,1));
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notification.hide {
|
||||
transform: translateX(calc(100% + 20px));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
transition: opacity var(--transition-fast, 120ms);
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.notification-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notification-close:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 通知类型样式 */
|
||||
.notification-success {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.95), rgba(217, 119, 6, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(220, 38, 38, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.95), rgba(139, 92, 246, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动效 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.notification {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notification.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user