HTML5新手练习项目—番茄时钟(附源码)


版权声明

  • 本文原创作者:谷哥的小弟
  • 作者博客地址: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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷哥的小弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值