通知API(Notification API)允许网页向用户显示桌面通知,即使网页不在前台运行。这对于即时通讯、邮件提醒、任务提醒等场景非常有用。
东巴文(db-w.cn) 认为:通知API让Web应用具备了原生应用的推送能力,是提升用户体验的重要工具。
| 特点 | 说明 |
|---|---|
| 桌面通知 | 在系统通知区域显示通知 |
| 后台运行 | 即使页面不在前台也能显示 |
| 自定义样式 | 可以自定义图标、标题、内容等 |
| 交互支持 | 支持点击、关闭等交互事件 |
| 权限管理 | 需要用户授权才能显示 |
// 通知类型
const NotificationTypes = {
Basic: '基本通知',
Image: '图片通知',
List: '列表通知',
Progress: '进度通知'
};
// 通知权限
const NotificationPermissions = {
default: '默认(未询问)',
granted: '已授权',
denied: '已拒绝'
};
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>通知权限管理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.permission-panel {
padding: 30px;
background: #f9f9f9;
border-radius: 10px;
text-align: center;
margin: 20px 0;
}
.permission-status {
font-size: 48px;
margin-bottom: 20px;
}
.permission-status.granted {
color: #28a745;
}
.permission-status.denied {
color: #dc3545;
}
.permission-status.default {
color: #ffc107;
}
.permission-text {
font-size: 18px;
color: #666;
margin-bottom: 20px;
}
button {
padding: 15px 30px;
font-size: 16px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #007bff;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>通知权限管理</h1>
<div class="permission-panel">
<div class="permission-status" id="permissionIcon">❓</div>
<div class="permission-text" id="permissionText">检查权限状态...</div>
<button id="requestBtn" onclick="requestPermission()">请求通知权限</button>
<button onclick="checkPermission()">检查权限</button>
</div>
<div class="info-box">
<strong>说明:</strong>
<ul>
<li>浏览器需要用户明确授权才能显示通知</li>
<li>用户可以随时在浏览器设置中更改权限</li>
<li>如果用户拒绝,需要引导用户手动开启</li>
</ul>
</div>
<script>
// 检查浏览器支持
if (!('Notification' in window)) {
document.getElementById('permissionText').textContent =
'您的浏览器不支持通知功能';
document.getElementById('requestBtn').disabled = true;
}
// 检查权限状态
function checkPermission() {
const permission = Notification.permission;
const icon = document.getElementById('permissionIcon');
const text = document.getElementById('permissionText');
const requestBtn = document.getElementById('requestBtn');
switch (permission) {
case 'granted':
icon.textContent = '✅';
icon.className = 'permission-status granted';
text.textContent = '通知权限已授权';
requestBtn.textContent = '发送测试通知';
requestBtn.onclick = sendTestNotification;
break;
case 'denied':
icon.textContent = '❌';
icon.className = 'permission-status denied';
text.textContent = '通知权限被拒绝。请在浏览器设置中手动开启。';
requestBtn.disabled = true;
break;
default:
icon.textContent = '❓';
icon.className = 'permission-status default';
text.textContent = '通知权限未设置';
requestBtn.textContent = '请求通知权限';
requestBtn.onclick = requestPermission;
requestBtn.disabled = false;
}
}
// 请求权限
function requestPermission() {
Notification.requestPermission()
.then(permission => {
checkPermission();
if (permission === 'granted') {
sendTestNotification();
}
});
}
// 发送测试通知
function sendTestNotification() {
const notification = new Notification('测试通知', {
body: '通知权限已成功授权!',
icon: 'https://db-w.cn/logo.png'
});
notification.onclick = function() {
window.focus();
notification.close();
};
}
// 初始检查
checkPermission();
</script>
</body>
</html>
// 基本通知
const notification = new Notification('通知标题', {
body: '通知内容',
icon: 'https://example.com/icon.png'
});
// 通知选项
const options = {
// 基本选项
body: '通知正文内容',
icon: '图标URL',
image: '大图片URL',
badge: '徽章URL',
// 方向和语言
dir: 'auto', // auto, ltr, rtl
lang: 'zh-CN',
// 标签和重新通知
tag: 'unique-id', // 相同tag的通知会替换
renotify: true, // 替换时是否重新通知
// 交互
requireInteraction: true, // 是否需要用户交互才能关闭
silent: false, // 是否静音
// 数据
data: {
url: 'https://example.com',
id: 123
},
// 操作按钮(Service Worker中)
actions: [
{
action: 'open',
title: '打开'
},
{
action: 'close',
title: '关闭'
}
],
// 振动(移动设备)
vibrate: [200, 100, 200]
};
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>创建通知示例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.form-group {
margin: 15px 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
textarea,
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
textarea {
height: 80px;
resize: vertical;
}
.checkbox-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-group label {
display: flex;
align-items: center;
font-weight: normal;
}
.checkbox-group input {
margin-right: 5px;
}
button {
padding: 12px 30px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}
button:hover {
background: #0056b3;
}
.notification-log {
margin-top: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 5px;
}
.log-entry {
padding: 10px;
margin: 5px 0;
background: white;
border-radius: 5px;
border-left: 4px solid #007bff;
}
.log-entry.click {
border-left-color: #28a745;
}
.log-entry.close {
border-left-color: #dc3545;
}
.log-entry.error {
border-left-color: #ffc107;
}
</style>
</head>
<body>
<h1>创建通知示例</h1>
<div class="form-group">
<label for="title">通知标题</label>
<input type="text" id="title" value="新消息通知" placeholder="输入通知标题">
</div>
<div class="form-group">
<label for="body">通知内容</label>
<textarea id="body" placeholder="输入通知内容">您有一条新消息,点击查看详情。</textarea>
</div>
<div class="form-group">
<label for="icon">图标URL(可选)</label>
<input type="text" id="icon" placeholder="https://example.com/icon.png">
</div>
<div class="form-group">
<label for="tag">标签(可选)</label>
<input type="text" id="tag" placeholder="相同标签的通知会替换">
</div>
<div class="form-group">
<label>通知选项</label>
<div class="checkbox-group">
<label>
<input type="checkbox" id="requireInteraction">
需要用户交互
</label>
<label>
<input type="checkbox" id="silent">
静音
</label>
<label>
<input type="checkbox" id="renotify">
替换时重新通知
</label>
</div>
</div>
<button onclick="createNotification()">创建通知</button>
<button onclick="createMultipleNotifications()">创建多个通知</button>
<div class="notification-log">
<h3>通知日志</h3>
<div id="log"></div>
</div>
<script>
// 检查权限
function checkPermission() {
if (!('Notification' in window)) {
addLog('错误', '浏览器不支持通知功能', 'error');
return false;
}
if (Notification.permission === 'denied') {
addLog('错误', '通知权限被拒绝', 'error');
return false;
}
if (Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
addLog('系统', '通知权限已授权');
}
});
return false;
}
return true;
}
// 创建通知
function createNotification() {
if (!checkPermission()) return;
const title = document.getElementById('title').value || '通知';
const options = {
body: document.getElementById('body').value,
icon: document.getElementById('icon').value || undefined,
tag: document.getElementById('tag').value || undefined,
requireInteraction: document.getElementById('requireInteraction').checked,
silent: document.getElementById('silent').checked,
renotify: document.getElementById('renotify').checked,
data: {
timestamp: Date.now()
}
};
try {
const notification = new Notification(title, options);
// 点击事件
notification.onclick = function(event) {
addLog('点击', `通知被点击: ${title}`, 'click');
window.focus();
notification.close();
};
// 关闭事件
notification.onclose = function() {
addLog('关闭', `通知已关闭: ${title}`, 'close');
};
// 错误事件
notification.onerror = function(error) {
addLog('错误', `通知错误: ${error}`, 'error');
};
addLog('创建', `通知已创建: ${title}`);
} catch (error) {
addLog('错误', `创建失败: ${error.message}`, 'error');
}
}
// 创建多个通知
function createMultipleNotifications() {
if (!checkPermission()) return;
const notifications = [
{ title: '消息通知', body: '您有3条新消息' },
{ title: '邮件通知', body: '您收到一封新邮件' },
{ title: '提醒通知', body: '会议将在10分钟后开始' }
];
notifications.forEach((n, i) => {
setTimeout(() => {
new Notification(n.title, {
body: n.body,
tag: `notification-${i}`
});
addLog('创建', `批量通知: ${n.title}`);
}, i * 500);
});
}
// 添加日志
function addLog(type, message, className = '') {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry ' + className;
entry.innerHTML = `
<strong>[${new Date().toLocaleTimeString()}]</strong>
<strong>${type}:</strong> ${message}
`;
log.insertBefore(entry, log.firstChild);
}
// 初始检查
if (!('Notification' in window)) {
addLog('错误', '浏览器不支持通知功能', 'error');
} else {
addLog('系统', `当前权限状态: ${Notification.permission}`);
}
</script>
</body>
</html>
// 创建通知并处理事件
const notification = new Notification('事件示例', {
body: '点击或关闭此通知',
requireInteraction: true
});
// 显示事件
notification.onshow = function(event) {
console.log('通知已显示');
};
// 点击事件
notification.onclick = function(event) {
console.log('通知被点击');
console.log('通知数据:', event.target.data);
// 打开窗口或切换焦点
window.focus();
// 关闭通知
notification.close();
// 执行其他操作
handleNotificationClick(event.target.data);
};
// 关闭事件
notification.onclose = function(event) {
console.log('通知已关闭');
};
// 错误事件
notification.onerror = function(event) {
console.error('通知错误:', event);
};
// 处理点击
function handleNotificationClick(data) {
if (data && data.url) {
window.open(data.url, '_blank');
}
}
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功');
});
}
// sw.js - Service Worker文件
self.addEventListener('push', function(event) {
const options = {
body: event.data ? event.data.text() : '新消息',
icon: '/icon.png',
badge: '/badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: '查看详情',
icon: '/check.png'
},
{
action: 'close',
title: '关闭',
icon: '/close.png'
}
]
};
event.waitUntil(
self.registration.showNotification('推送通知', options)
);
});
// 处理通知点击
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'explore') {
// 打开特定页面
event.waitUntil(
clients.openWindow('/details')
);
} else if (event.action === 'close') {
// 关闭通知
console.log('用户关闭了通知');
} else {
// 点击通知主体
event.waitUntil(
clients.openWindow('/')
);
}
});
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>通知中心 - 东巴文</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
padding: 30px 0;
}
.header h1 {
font-size: 36px;
margin-bottom: 10px;
}
.header p {
font-size: 18px;
opacity: 0.9;
}
.card {
background: white;
border-radius: 15px;
padding: 30px;
margin: 20px 0;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.card h2 {
margin-bottom: 20px;
color: #333;
}
.status-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.status-item {
text-align: center;
padding: 20px;
background: #f9f9f9;
border-radius: 10px;
}
.status-icon {
font-size: 48px;
margin-bottom: 10px;
}
.status-label {
font-size: 14px;
color: #666;
}
.status-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.notification-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.notification-btn {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
transition: transform 0.3s;
font-size: 16px;
}
.notification-btn:hover {
transform: translateY(-5px);
}
.notification-btn:active {
transform: translateY(0);
}
.notification-btn .icon {
font-size: 24px;
display: block;
margin-bottom: 10px;
}
.history {
max-height: 400px;
overflow-y: auto;
}
.history-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
transition: background 0.3s;
}
.history-item:hover {
background: #f9f9f9;
}
.history-icon {
font-size: 24px;
margin-right: 15px;
}
.history-content {
flex: 1;
}
.history-title {
font-weight: bold;
margin-bottom: 5px;
}
.history-body {
font-size: 14px;
color: #666;
}
.history-time {
font-size: 12px;
color: #999;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.permission-request {
text-align: center;
padding: 40px;
}
.permission-request button {
padding: 15px 40px;
font-size: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
margin-top: 20px;
}
.permission-request button:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔔 通知中心</h1>
<p>管理和发送桌面通知</p>
</div>
<div class="card">
<h2>权限状态</h2>
<div class="status-panel">
<div class="status-item">
<div class="status-icon" id="permissionIcon">❓</div>
<div class="status-label">权限状态</div>
<div class="status-value" id="permissionText">检查中...</div>
</div>
<div class="status-item">
<div class="status-icon">📊</div>
<div class="status-label">已发送通知</div>
<div class="status-value" id="sentCount">0</div>
</div>
<div class="status-item">
<div class="status-icon">👆</div>
<div class="status-label">已点击通知</div>
<div class="status-value" id="clickCount">0</div>
</div>
</div>
</div>
<div class="card" id="permissionCard" style="display: none;">
<div class="permission-request">
<h2>需要通知权限</h2>
<p>点击下方按钮授权通知权限</p>
<button onclick="requestPermission()">授权通知权限</button>
</div>
</div>
<div class="card" id="notificationPanel" style="display: none;">
<h2>发送通知</h2>
<div class="notification-types">
<button class="notification-btn" onclick="sendNotification('message')">
<span class="icon">💬</span>
消息通知
</button>
<button class="notification-btn" onclick="sendNotification('email')">
<span class="icon">📧</span>
邮件通知
</button>
<button class="notification-btn" onclick="sendNotification('reminder')">
<span class="icon">⏰</span>
提醒通知
</button>
<button class="notification-btn" onclick="sendNotification('alert')">
<span class="icon">⚠️</span>
警告通知
</button>
<button class="notification-btn" onclick="sendNotification('success')">
<span class="icon">✅</span>
成功通知
</button>
<button class="notification-btn" onclick="sendNotification('promotion')">
<span class="icon">🎉</span>
活动通知
</button>
</div>
</div>
<div class="card">
<h2>通知历史</h2>
<div class="history" id="history">
<div class="empty-state">暂无通知记录</div>
</div>
</div>
</div>
<script>
// 通知中心
class NotificationCenter {
constructor() {
this.sentCount = 0;
this.clickCount = 0;
this.history = [];
this.init();
}
init() {
this.checkPermission();
}
checkPermission() {
if (!('Notification' in window)) {
this.updatePermissionUI('unsupported');
return;
}
const permission = Notification.permission;
this.updatePermissionUI(permission);
if (permission === 'granted') {
document.getElementById('notificationPanel').style.display = 'block';
document.getElementById('permissionCard').style.display = 'none';
} else if (permission === 'default') {
document.getElementById('notificationPanel').style.display = 'none';
document.getElementById('permissionCard').style.display = 'block';
} else {
document.getElementById('notificationPanel').style.display = 'none';
document.getElementById('permissionCard').style.display = 'block';
}
}
requestPermission() {
Notification.requestPermission()
.then(permission => {
this.checkPermission();
if (permission === 'granted') {
this.sendWelcomeNotification();
}
});
}
updatePermissionUI(permission) {
const icon = document.getElementById('permissionIcon');
const text = document.getElementById('permissionText');
const permissionMap = {
granted: { icon: '✅', text: '已授权' },
denied: { icon: '❌', text: '已拒绝' },
default: { icon: '❓', text: '未设置' },
unsupported: { icon: '🚫', text: '不支持' }
};
const { icon: iconText, text: textValue } = permissionMap[permission];
icon.textContent = iconText;
text.textContent = textValue;
}
sendWelcomeNotification() {
const notification = new Notification('欢迎!', {
body: '通知功能已成功启用',
icon: 'https://db-w.cn/logo.png'
});
this.addToHistory('欢迎', '通知功能已成功启用', '✅');
}
sendNotification(type) {
if (Notification.permission !== 'granted') {
alert('请先授权通知权限');
return;
}
const notifications = {
message: {
title: '新消息',
body: '您有一条新消息,点击查看详情',
icon: '💬'
},
email: {
title: '新邮件',
body: '您收到一封来自张三的邮件',
icon: '📧'
},
reminder: {
title: '日程提醒',
body: '会议将在10分钟后开始',
icon: '⏰'
},
alert: {
title: '系统警告',
body: '您的存储空间即将用尽',
icon: '⚠️'
},
success: {
title: '操作成功',
body: '文件已成功上传到云端',
icon: '✅'
},
promotion: {
title: '限时活动',
body: '双十一大促,全场5折起!',
icon: '🎉'
}
};
const config = notifications[type];
const notification = new Notification(config.title, {
body: config.body,
tag: type,
requireInteraction: true,
data: {
type: type,
timestamp: Date.now()
}
});
// 更新计数
this.sentCount++;
document.getElementById('sentCount').textContent = this.sentCount;
// 添加历史
this.addToHistory(config.title, config.body, config.icon);
// 点击事件
notification.onclick = (event) => {
this.clickCount++;
document.getElementById('clickCount').textContent = this.clickCount;
window.focus();
notification.close();
};
}
addToHistory(title, body, icon) {
const history = document.getElementById('history');
// 移除空状态
const emptyState = history.querySelector('.empty-state');
if (emptyState) {
emptyState.remove();
}
const item = document.createElement('div');
item.className = 'history-item';
item.innerHTML = `
<div class="history-icon">${icon}</div>
<div class="history-content">
<div class="history-title">${title}</div>
<div class="history-body">${body}</div>
</div>
<div class="history-time">${new Date().toLocaleTimeString()}</div>
`;
history.insertBefore(item, history.firstChild);
// 保存到历史记录
this.history.unshift({
title,
body,
icon,
time: new Date()
});
// 限制历史记录数量
if (this.history.length > 50) {
this.history.pop();
}
}
}
// 初始化通知中心
const notificationCenter = new NotificationCenter();
</script>
</body>
</html>
// 推荐:在用户操作后请求权限
document.getElementById('enableNotifications').addEventListener('click', function() {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
// 显示欢迎通知
new Notification('感谢授权!', {
body: '您将收到重要通知'
});
}
});
});
// 不推荐:页面加载时立即请求
window.addEventListener('load', function() {
Notification.requestPermission(); // 用户可能不理解为什么要授权
});
// 推荐:简洁明了的通知
const goodNotification = new Notification('新消息', {
body: '张三: 你好,明天的会议改到下午3点',
icon: '/icon.png',
tag: 'message-123'
});
// 不推荐:过长或模糊的通知
const badNotification = new Notification('系统通知', {
body: '这是一条非常长的通知内容,包含了很多不必要的细节,用户可能没有耐心读完...', // 太长
// 没有图标,没有标签
});
// 推荐:控制通知频率
class NotificationManager {
constructor() {
this.lastNotificationTime = 0;
this.minInterval = 5000; // 最小间隔5秒
}
send(title, options) {
const now = Date.now();
if (now - this.lastNotificationTime < this.minInterval) {
console.log('通知发送过于频繁,已忽略');
return false;
}
this.lastNotificationTime = now;
return new Notification(title, options);
}
}
const manager = new NotificationManager();
manager.send('通知1', { body: '内容1' });
manager.send('通知2', { body: '内容2' }); // 可能被忽略
东巴文点评:通知API使用要遵循"少而精"原则,避免打扰用户。
问题1:以下哪个方法用于请求通知权限?
A. Notification.askPermission() B. Notification.requestPermission() C. Notification.getPermission() D. Notification.enable()
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:使用Notification.requestPermission()方法请求用户授权。该方法返回一个Promise,resolve值为'granted'、'denied'或'default'。
问题2:Notification.permission的值为'denied'表示什么?
A. 用户尚未决定 B. 用户已授权 C. 用户已拒绝 D. 浏览器不支持
<details> <summary>点击查看答案</summary>答案:C
东巴文解释:Notification.permission有三个可能的值:'default'(用户尚未决定)、'granted'(用户已授权)、'denied'(用户已拒绝)。'denied'表示用户明确拒绝了通知权限。
</details>任务:创建一个番茄钟应用,使用通知API在番茄钟结束时提醒用户。
<details> <summary>点击查看参考答案</summary><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄钟 - 东巴文</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.pomodoro-container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 400px;
width: 100%;
}
h1 {
color: #ee5a6f;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.timer {
position: relative;
width: 250px;
height: 250px;
margin: 0 auto 30px;
}
.timer-circle {
width: 100%;
height: 100%;
border-radius: 50%;
background: #f9f9f9;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.timer-progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.timer-text {
font-size: 48px;
font-weight: bold;
color: #333;
z-index: 1;
}
.timer-label {
font-size: 14px;
color: #999;
margin-top: 5px;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}
button {
padding: 15px 30px;
font-size: 16px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(238, 90, 111, 0.4);
}
.btn-secondary {
background: #f9f9f9;
color: #666;
}
.btn-secondary:hover {
background: #eee;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #ee5a6f;
}
.stat-label {
font-size: 12px;
color: #999;
}
.notification-toggle {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 10px;
}
.notification-toggle label {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
}
.notification-toggle input {
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<div class="pomodoro-container">
<h1>🍅 番茄钟</h1>
<p class="subtitle">专注工作,高效休息</p>
<div class="timer">
<div class="timer-circle">
<svg class="timer-progress" viewBox="0 0 250 250">
<circle cx="125" cy="125" r="120" fill="none" stroke="#eee" stroke-width="8"/>
<circle id="progressCircle" cx="125" cy="125" r="120" fill="none"
stroke="#ee5a6f" stroke-width="8"
stroke-linecap="round"
stroke-dasharray="753.98"
stroke-dashoffset="0"
transform="rotate(-90 125 125)"/>
</svg>
<div>
<div class="timer-text" id="timerDisplay">25:00</div>
<div class="timer-label" id="timerLabel">工作时间</div>
</div>
</div>
</div>
<div class="controls">
<button class="btn-primary" id="startBtn" onclick="startTimer()">开始</button>
<button class="btn-secondary" id="resetBtn" onclick="resetTimer()">重置</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="completedCount">0</div>
<div class="stat-label">已完成</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalTime">0</div>
<div class="stat-label">总时长(分钟)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="currentStreak">0</div>
<div class="stat-label">连续完成</div>
</div>
</div>
<div class="notification-toggle">
<label>
<input type="checkbox" id="notificationToggle" checked>
<span>启用桌面通知</span>
</label>
</div>
</div>
<script>
// 番茄钟类
class PomodoroTimer {
constructor() {
this.workDuration = 25 * 60; // 25分钟
this.breakDuration = 5 * 60; // 5分钟
this.timeLeft = this.workDuration;
this.isRunning = false;
this.isWorkTime = true;
this.timer = null;
this.completedCount = 0;
this.totalMinutes = 0;
this.currentStreak = 0;
this.init();
}
init() {
this.checkNotificationPermission();
this.updateDisplay();
}
checkNotificationPermission() {
if (!('Notification' in window)) {
document.getElementById('notificationToggle').disabled = true;
return;
}
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
start() {
if (this.isRunning) {
this.pause();
return;
}
this.isRunning = true;
document.getElementById('startBtn').textContent = '暂停';
this.timer = setInterval(() => {
this.timeLeft--;
this.updateDisplay();
if (this.timeLeft <= 0) {
this.complete();
}
}, 1000);
}
pause() {
this.isRunning = false;
clearInterval(this.timer);
document.getElementById('startBtn').textContent = '继续';
}
reset() {
this.isRunning = false;
clearInterval(this.timer);
this.timeLeft = this.isWorkTime ? this.workDuration : this.breakDuration;
document.getElementById('startBtn').textContent = '开始';
this.updateDisplay();
}
complete() {
this.isRunning = false;
clearInterval(this.timer);
if (this.isWorkTime) {
// 工作时间结束
this.completedCount++;
this.totalMinutes += 25;
this.currentStreak++;
this.updateStats();
this.sendNotification('工作完成!', '休息一下吧 🎉');
// 切换到休息时间
this.isWorkTime = false;
this.timeLeft = this.breakDuration;
document.getElementById('timerLabel').textContent = '休息时间';
} else {
// 休息时间结束
this.sendNotification('休息结束!', '开始新的番茄钟 🍅');
// 切换到工作时间
this.isWorkTime = true;
this.timeLeft = this.workDuration;
document.getElementById('timerLabel').textContent = '工作时间';
}
document.getElementById('startBtn').textContent = '开始';
this.updateDisplay();
}
updateDisplay() {
const minutes = Math.floor(this.timeLeft / 60);
const seconds = this.timeLeft % 60;
document.getElementById('timerDisplay').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// 更新进度圆环
const totalTime = this.isWorkTime ? this.workDuration : this.breakDuration;
const progress = (totalTime - this.timeLeft) / totalTime;
const circumference = 2 * Math.PI * 120;
const offset = circumference * (1 - progress);
document.getElementById('progressCircle').style.strokeDashoffset = offset;
}
updateStats() {
document.getElementById('completedCount').textContent = this.completedCount;
document.getElementById('totalTime').textContent = this.totalMinutes;
document.getElementById('currentStreak').textContent = this.currentStreak;
}
sendNotification(title, body) {
if (!document.getElementById('notificationToggle').checked) {
return;
}
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
body: body,
icon: '🍅',
tag: 'pomodoro',
requireInteraction: true
});
notification.onclick = () => {
window.focus();
notification.close();
};
}
}
}
// 初始化番茄钟
const pomodoro = new PomodoroTimer();
function startTimer() {
pomodoro.start();
}
function resetTimer() {
pomodoro.reset();
}
</script>
</body>
</html>
</details>
东巴文(db-w.cn) - 让编程学习更有趣、更高效!