版权声明
- 本文原创作者:谷哥的小弟
- 作者博客地址:http://blog.youkuaiyun.com/lfdfhl

效果展示



项目源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄专注时钟</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--focus-color: #ff7675;
--break-color: #00b894;
--bg-gradient-focus: linear-gradient(135deg, #fab1a0 0%, #ff7675 100%);
--bg-gradient-break: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
--glass-bg: rgba(255, 255, 255, 0.85);
--text-main: #2d3436;
--text-secondary: #636e72;
--shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
}
body {
font-family: 'Inter', sans-serif;
background: #f0f2f5;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
transition: background 0.5s ease;
}
body.focus-mode { background: var(--bg-gradient-focus); }
body.break-mode { background: var(--bg-gradient-break); }
.container {
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-radius: 30px;
padding: 40px;
width: 100%;
max-width: 450px;
box-shadow: var(--shadow);
text-align: center;
animation: floatUp 0.6s ease-out;
}
@keyframes floatUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
color: var(--text-main);
margin-bottom: 20px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
/* 计时器圆环 */
.timer-container {
position: relative;
width: 260px;
height: 260px;
margin: 0 auto 30px;
}
.timer-svg {
transform: rotate(-90deg);
width: 100%;
height: 100%;
}
.timer-circle-bg {
fill: none;
stroke: #dfe6e9;
stroke-width: 8;
}
.timer-circle-progress {
fill: none;
stroke: var(--focus-color);
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 754; /* 2 * PI * r (r=120) */
stroke-dashoffset: 0;
transition: stroke-dashoffset 1s linear, stroke 0.3s ease;
}
.timer-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
}
.time-display {
font-size: 3.5em;
font-weight: 700;
color: var(--text-main);
font-variant-numeric: tabular-nums;
}
.status-text {
font-size: 0.9em;
color: var(--text-secondary);
margin-top: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* 任务输入 */
.task-input-group {
margin-bottom: 25px;
position: relative;
}
.task-input {
width: 100%;
padding: 12px 20px;
border: 2px solid transparent;
background: rgba(255, 255, 255, 0.5);
border-radius: 15px;
font-size: 1em;
text-align: center;
outline: none;
transition: all 0.3s;
color: var(--text-main);
}
.task-input:focus {
background: white;
border-color: rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
/* 控制按钮 */
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 12px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--text-main);
color: white;
flex: 2;
justify-content: center;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.btn-secondary {
background: rgba(0,0,0,0.05);
color: var(--text-main);
flex: 1;
justify-content: center;
}
.btn-secondary:hover {
background: rgba(0,0,0,0.1);
}
/* --- 纯 CSS 图标样式 --- */
.icon {
width: 14px;
height: 14px;
display: inline-block;
position: relative;
}
/* 播放图标 (圆角三角形) */
.icon-play {
width: 0;
height: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-left: 14px solid currentColor;
border-radius: 2px; /* 圆角效果 */
transform: translateX(2px); /* 视觉居中修正 */
}
/* 暂停图标 (两个圆角矩形) */
.icon-pause {
width: 14px;
height: 16px;
border-left: 4px solid currentColor;
border-right: 4px solid currentColor;
border-radius: 2px;
}
/* 重置图标 (圆角正方形 + 箭头) */
.icon-reset {
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-radius: 3px;
position: relative;
}
/* 箭头部分 */
.icon-reset::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
border-top: 2px solid currentColor;
border-left: 2px solid currentColor;
transform: translate(-50%, -50%) rotate(-45deg); /* 简单的左上箭头 */
}
/* 模式切换 */
.mode-toggles {
display: flex;
background: rgba(0,0,0,0.05);
padding: 5px;
border-radius: 15px;
margin-bottom: 25px;
}
.mode-btn {
flex: 1;
padding: 10px;
border: none;
background: transparent;
border-radius: 10px;
cursor: pointer;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s;
}
.mode-btn.active {
background: white;
color: var(--text-main);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
/* 统计列表 */
.stats-section {
border-top: 1px solid rgba(0,0,0,0.05);
padding-top: 20px;
text-align: left;
}
.stats-header {
font-size: 0.9em;
color: var(--text-secondary);
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.log-list {
list-style: none;
max-height: 150px;
overflow-y: auto;
}
.log-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid rgba(0,0,0,0.03);
font-size: 0.9em;
animation: fadeIn 0.3s ease;
}
.log-time { color: var(--text-secondary); font-size: 0.8em; }
.log-duration { font-weight: 600; color: var(--text-main); }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 滚动条 */
.log-list::-webkit-scrollbar { width: 4px; }
.log-list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
</style>
</head>
<body class="focus-mode">
<div class="container">
<h1><i class="fas fa-clock"></i> 番茄时钟</h1>
<div class="mode-toggles">
<button class="mode-btn active" id="focusBtn" onclick="switchMode('focus')">专注 (25分)</button>
<button class="mode-btn" id="breakBtn" onclick="switchMode('break')">休息 (5分)</button>
</div>
<div class="timer-container">
<svg class="timer-svg" viewBox="0 0 260 260">
<circle class="timer-circle-bg" cx="130" cy="130" r="120"></circle>
<circle class="timer-circle-progress" id="progressRing" cx="130" cy="130" r="120"></circle>
</svg>
<div class="timer-text">
<div class="time-display" id="timeDisplay">25:00</div>
<div class="status-text" id="statusText">准备专注</div>
</div>
</div>
<div class="task-input-group">
<input type="text" class="task-input" id="taskInput" placeholder="当前专注任务...">
</div>
<div class="controls">
<button class="btn btn-primary" id="startBtn" onclick="toggleTimer()">
<span class="icon icon-play"></span> 开始
</button>
<button class="btn btn-secondary" onclick="resetTimer()">
<span class="icon icon-reset"></span> 重置
</button>
</div>
<div class="stats-section">
<div class="stats-header">
<span>今日专注记录</span>
<span id="totalTime">0 分钟</span>
</div>
<ul class="log-list" id="logList">
<!-- 记录将在这里生成 -->
</ul>
</div>
</div>
<script>
// 配置
const MODES = {
focus: { time: 25 * 60, color: '#ff7675', label: '专注中...' },
break: { time: 5 * 60, color: '#00b894', label: '休息中...' }
};
// 状态变量
let currentMode = 'focus';
let timeLeft = MODES.focus.time;
let isRunning = false;
let timerId = null;
let totalTimeFocused = 0;
// DOM 元素
const timeDisplay = document.getElementById('timeDisplay');
const statusText = document.getElementById('statusText');
const progressRing = document.getElementById('progressRing');
const startBtn = document.getElementById('startBtn');
const body = document.body;
const taskInput = document.getElementById('taskInput');
const logList = document.getElementById('logList');
const totalTimeEl = document.getElementById('totalTime');
const focusBtn = document.getElementById('focusBtn');
const breakBtn = document.getElementById('breakBtn');
// 圆环周长
const circumference = 2 * Math.PI * 120;
progressRing.style.strokeDasharray = `${circumference} ${circumference}`;
// 初始化
updateTimerDisplay();
setProgress(0); // 初始全满
// 切换模式
function switchMode(mode) {
if (isRunning) {
if(!confirm("计时器正在运行,确定要切换模式吗?")) return;
stopTimer();
}
currentMode = mode;
timeLeft = MODES[mode].time;
// 更新 UI 样式
if (mode === 'focus') {
body.className = 'focus-mode';
progressRing.style.stroke = MODES.focus.color;
focusBtn.classList.add('active');
breakBtn.classList.remove('active');
} else {
body.className = 'break-mode';
progressRing.style.stroke = MODES.break.color;
breakBtn.classList.add('active');
focusBtn.classList.remove('active');
}
statusText.innerText = '准备开始';
updateStartButtonState(false);
updateTimerDisplay();
setProgress(0);
}
// 开始/暂停
function toggleTimer() {
if (isRunning) {
stopTimer();
} else {
startTimer();
}
}
function startTimer() {
isRunning = true;
statusText.innerText = MODES[currentMode].label;
updateStartButtonState(true);
taskInput.disabled = true;
timerId = setInterval(() => {
timeLeft--;
updateTimerDisplay();
// 计算进度比例 (1 - 剩余/总时间)
const totalTime = MODES[currentMode].time;
const progress = 1 - (timeLeft / totalTime);
setProgress(progress);
if (timeLeft <= 0) {
completeTimer();
}
}, 1000);
}
function stopTimer() {
isRunning = false;
clearInterval(timerId);
statusText.innerText = '已暂停';
updateStartButtonState(false);
taskInput.disabled = false;
}
function resetTimer() {
stopTimer();
timeLeft = MODES[currentMode].time;
statusText.innerText = '准备开始';
updateStartButtonState(false);
updateTimerDisplay();
setProgress(0);
}
function completeTimer() {
stopTimer();
playNotificationSound();
statusText.innerText = '完成!';
if (currentMode === 'focus') {
const taskName = taskInput.value.trim() || '无标题任务';
addLog(taskName, MODES.focus.time / 60);
totalTimeFocused += (MODES.focus.time / 60);
totalTimeEl.innerText = `${totalTimeFocused} 分钟`;
// 自动切换到休息模式提示
setTimeout(() => {
if(confirm("专注完成!是否开始休息?")) {
switchMode('break');
startTimer();
}
}, 100);
} else {
// 休息结束
if(confirm("休息结束!是否开始新的专注?")) {
switchMode('focus');
}
}
}
// 更新时间显示
function updateTimerDisplay() {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
timeDisplay.innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
document.title = `(${timeDisplay.innerText}) 番茄时钟`;
}
// 设置圆环进度
function setProgress(percent) {
const offset = circumference - (percent * circumference);
progressRing.style.strokeDashoffset = offset;
}
// 更新开始按钮的图标和文字
function updateStartButtonState(running) {
if (running) {
startBtn.innerHTML = '<span class="icon icon-pause"></span> 暂停';
startBtn.classList.remove('btn-primary');
startBtn.classList.add('btn-secondary');
} else {
startBtn.innerHTML = '<span class="icon icon-play"></span> 开始';
startBtn.classList.add('btn-primary');
startBtn.classList.remove('btn-secondary');
}
}
// 添加记录
function addLog(task, duration) {
const li = document.createElement('li');
li.className = 'log-item';
const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'});
li.innerHTML = `
<span>${task}</span>
<div>
<span class="log-duration">+${duration}分钟</span>
<span class="log-time">${timeStr}</span>
</div>
`;
logList.insertBefore(li, logList.firstChild);
}
// 播放提示音 (使用 Web Audio API,无需外部文件)
function playNotificationSound() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.value = 880; // A5
// 简单的 "哔-哔" 效果
gain.gain.setValueAtTime(0.1, ctx.currentTime);
osc.start(ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + 0.5);
osc.stop(ctx.currentTime + 0.5);
setTimeout(() => {
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.frequency.value = 880;
gain2.gain.setValueAtTime(0.1, ctx.currentTime);
osc2.start(ctx.currentTime);
gain2.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + 0.5);
osc2.stop(ctx.currentTime + 0.5);
}, 600);
}
</script>
</body>
</html>
969

被折叠的 条评论
为什么被折叠?



