| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>习惯追踪</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); |
| |
| body { |
| font-family: 'Noto Sans SC', sans-serif; |
| background-color: #f5f5f5; |
| max-width: 500px; |
| margin: 0 auto; |
| box-shadow: 0 0 20px rgba(0,0,0,0.1); |
| min-height: 100vh; |
| position: relative; |
| } |
| |
| .habit-card { |
| transition: all 0.3s ease; |
| } |
| |
| .habit-card:hover { |
| transform: translateY(-2px); |
| } |
| |
| .progress-bar { |
| height: 6px; |
| border-radius: 3px; |
| transition: width 0.5s ease; |
| } |
| |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: rgba(0,0,0,0.5); |
| z-index: 100; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .modal-content { |
| animation: modalFadeIn 0.3s ease; |
| } |
| |
| @keyframes modalFadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(20px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .floating-btn { |
| box-shadow: 0 4px 12px rgba(74, 222, 128, 0.3); |
| transition: all 0.3s ease; |
| } |
| |
| .floating-btn:hover { |
| transform: scale(1.1); |
| box-shadow: 0 6px 16px rgba(74, 222, 128, 0.4); |
| } |
| |
| .streak-fire { |
| animation: pulse 1.5s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.1); } |
| 100% { transform: scale(1); } |
| } |
| |
| |
| @keyframes fadeIn { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| |
| .animate-fade-in { |
| animation: fadeIn 0.3s ease; |
| } |
| |
| @keyframes fadeOut { |
| from { opacity: 1; } |
| to { opacity: 0; } |
| } |
| |
| .animate-fade-out { |
| animation: fadeOut 0.3s ease; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100"> |
| |
| <header class="bg-green-500 text-white p-4 shadow-md"> |
| <div class="flex justify-between items-center"> |
| <div> |
| <h1 class="text-2xl font-bold">习惯追踪</h1> |
| <p class="text-sm opacity-90">培养好习惯,一天一天来</p> |
| </div> |
| <div class="flex items-center space-x-3"> |
| <button id="stats-btn" class="p-2 rounded-full hover:bg-green-600 transition"> |
| <i class="fas fa-chart-line"></i> |
| </button> |
| <button id="settings-btn" class="p-2 rounded-full hover:bg-green-600 transition"> |
| <i class="fas fa-cog"></i> |
| </button> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <div class="bg-white p-3 flex justify-between items-center shadow-sm"> |
| <button id="prev-day" class="p-2 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-chevron-left"></i> |
| </button> |
| <div class="text-center"> |
| <h2 id="current-date" class="font-semibold text-lg">今天</h2> |
| <p id="current-day" class="text-sm text-gray-500">星期一, 6月5日</p> |
| </div> |
| <button id="next-day" class="p-2 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-chevron-right"></i> |
| </button> |
| </div> |
|
|
| |
| <main class="p-4 space-y-4"> |
| <div class="flex justify-between items-center"> |
| <h3 class="font-medium text-gray-700">今日习惯</h3> |
| <span id="completion-rate" class="text-sm font-medium text-green-500">0% 完成</span> |
| </div> |
| |
| <div id="habits-container" class="space-y-3"> |
| |
| </div> |
| |
| <div id="empty-state" class="text-center py-10"> |
| <i class="fas fa-clipboard-list text-4xl text-gray-300 mb-3"></i> |
| <h4 class="text-gray-500 font-medium">还没有习惯</h4> |
| <p class="text-gray-400 text-sm mt-1">添加你的第一个习惯开始追踪</p> |
| <button id="add-first-habit" class="mt-4 bg-green-500 text-white px-4 py-2 rounded-full font-medium hover:bg-green-600 transition"> |
| 添加习惯 |
| </button> |
| </div> |
| </main> |
|
|
| |
| <button id="add-habit-btn" class="fixed bottom-6 right-6 bg-green-500 text-white p-4 rounded-full shadow-lg floating-btn"> |
| <i class="fas fa-plus text-xl"></i> |
| </button> |
|
|
| |
| <div id="add-habit-modal" class="modal"> |
| <div class="modal-content bg-white rounded-lg w-full max-w-md mx-4"> |
| <div class="p-5"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-bold text-gray-800">添加新习惯</h3> |
| <button id="close-modal" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">习惯名称</label> |
| <input type="text" id="habit-name" placeholder="例如:晨跑" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">图标</label> |
| <div class="grid grid-cols-6 gap-2"> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="dumbbell"> |
| <i class="fas fa-dumbbell"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="book"> |
| <i class="fas fa-book"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="running"> |
| <i class="fas fa-running"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="bed"> |
| <i class="fas fa-bed"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="apple-alt"> |
| <i class="fas fa-apple-alt"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="water"> |
| <i class="fas fa-tint"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="meditation"> |
| <i class="fas fa-spa"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="code"> |
| <i class="fas fa-code"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="music"> |
| <i class="fas fa-music"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="journal"> |
| <i class="fas fa-book-open"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="pray"> |
| <i class="fas fa-hands-praying"></i> |
| </button> |
| <button class="habit-icon p-3 rounded-lg border hover:bg-gray-100" data-icon="custom"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">频率</label> |
| <select id="habit-frequency" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"> |
| <option value="daily">每日</option> |
| <option value="weekly">每周</option> |
| <option value="monthly">每月</option> |
| </select> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">提醒</label> |
| <div class="flex items-center space-x-2"> |
| <input type="time" id="habit-reminder" class="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"> |
| <button id="toggle-reminder" class="p-3 bg-gray-200 rounded-lg"> |
| <i class="fas fa-bell-slash text-gray-500"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div class="pt-2"> |
| <button id="save-habit" class="w-full bg-green-500 text-white py-3 rounded-lg font-medium hover:bg-green-600 transition"> |
| 保存习惯 |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="stats-modal" class="modal"> |
| <div class="modal-content bg-white rounded-lg w-full max-w-md mx-4"> |
| <div class="p-5"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-bold text-gray-800">你的进度</h3> |
| <button id="close-stats" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <div class="space-y-6"> |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="font-medium text-gray-700 mb-2">完成率</h4> |
| <div class="flex items-center space-x-3"> |
| <div class="w-full bg-gray-200 rounded-full h-4"> |
| <div id="overall-progress" class="bg-green-500 h-4 rounded-full" style="width: 0%"></div> |
| </div> |
| <span id="overall-percentage" class="text-sm font-medium text-gray-600">0%</span> |
| </div> |
| </div> |
| |
| <div> |
| <h4 class="font-medium text-gray-700 mb-3">当前连续记录</h4> |
| <div id="streaks-container" class="space-y-3"> |
| |
| </div> |
| </div> |
| |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="font-medium text-gray-700 mb-2">习惯详情</h4> |
| <div id="habits-breakdown" class="space-y-2"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| updateCurrentDate(); |
| |
| |
| const habits = JSON.parse(localStorage.getItem('habits')) || []; |
| |
| |
| if (habits.length > 0) { |
| document.getElementById('empty-state').style.display = 'none'; |
| renderHabits(habits); |
| updateCompletionRate(habits); |
| } |
| |
| |
| document.getElementById('add-habit-btn').addEventListener('click', openAddHabitModal); |
| document.getElementById('add-first-habit').addEventListener('click', openAddHabitModal); |
| document.getElementById('close-modal').addEventListener('click', closeAddHabitModal); |
| document.getElementById('save-habit').addEventListener('click', saveHabit); |
| document.getElementById('stats-btn').addEventListener('click', openStatsModal); |
| document.getElementById('close-stats').addEventListener('click', closeStatsModal); |
| document.getElementById('prev-day').addEventListener('click', navigateToPreviousDay); |
| document.getElementById('next-day').addEventListener('click', navigateToNextDay); |
| document.getElementById('toggle-reminder').addEventListener('click', toggleReminder); |
| |
| |
| const habitIcons = document.querySelectorAll('.habit-icon'); |
| habitIcons.forEach(icon => { |
| icon.addEventListener('click', function() { |
| |
| habitIcons.forEach(i => i.classList.remove('bg-green-100', 'border-green-500')); |
| |
| this.classList.add('bg-green-100', 'border-green-500'); |
| |
| this.dataset.selected = 'true'; |
| }); |
| }); |
| }); |
| |
| |
| let currentDisplayDate = new Date(); |
| |
| |
| function updateCurrentDate() { |
| const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; |
| const today = new Date(); |
| |
| if (currentDisplayDate.toDateString() === today.toDateString()) { |
| document.getElementById('current-date').textContent = '今天'; |
| } else { |
| document.getElementById('current-date').textContent = currentDisplayDate.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); |
| } |
| |
| document.getElementById('current-day').textContent = currentDisplayDate.toLocaleDateString('zh-CN', options); |
| } |
| |
| |
| function navigateToPreviousDay() { |
| currentDisplayDate.setDate(currentDisplayDate.getDate() - 1); |
| updateCurrentDate(); |
| |
| } |
| |
| |
| function navigateToNextDay() { |
| const today = new Date(); |
| if (currentDisplayDate.toDateString() === today.toDateString()) return; |
| |
| currentDisplayDate.setDate(currentDisplayDate.getDate() + 1); |
| updateCurrentDate(); |
| |
| } |
| |
| |
| function openAddHabitModal() { |
| document.getElementById('add-habit-modal').style.display = 'flex'; |
| } |
| |
| |
| function closeAddHabitModal() { |
| document.getElementById('add-habit-modal').style.display = 'none'; |
| |
| document.getElementById('habit-name').value = ''; |
| document.getElementById('habit-frequency').value = 'daily'; |
| document.getElementById('habit-reminder').value = ''; |
| |
| |
| const habitIcons = document.querySelectorAll('.habit-icon'); |
| habitIcons.forEach(icon => { |
| icon.classList.remove('bg-green-100', 'border-green-500'); |
| delete icon.dataset.selected; |
| }); |
| } |
| |
| |
| function toggleReminder() { |
| const btn = document.getElementById('toggle-reminder'); |
| const icon = btn.querySelector('i'); |
| |
| if (icon.classList.contains('fa-bell-slash')) { |
| icon.classList.remove('fa-bell-slash', 'text-gray-500'); |
| icon.classList.add('fa-bell', 'text-green-500'); |
| } else { |
| icon.classList.remove('fa-bell', 'text-green-500'); |
| icon.classList.add('fa-bell-slash', 'text-gray-500'); |
| } |
| } |
| |
| |
| function saveHabit() { |
| const name = document.getElementById('habit-name').value.trim(); |
| if (!name) { |
| alert('请输入习惯名称'); |
| return; |
| } |
| |
| |
| const selectedIcon = document.querySelector('.habit-icon[data-selected="true"]'); |
| const icon = selectedIcon ? selectedIcon.dataset.icon : 'plus'; |
| |
| const frequency = document.getElementById('habit-frequency').value; |
| |
| |
| const reminderBtn = document.getElementById('toggle-reminder'); |
| const reminderEnabled = !reminderBtn.querySelector('i').classList.contains('fa-bell-slash'); |
| const reminderTime = reminderEnabled ? document.getElementById('habit-reminder').value : null; |
| |
| |
| const newHabit = { |
| id: Date.now(), |
| name, |
| icon, |
| frequency, |
| reminder: reminderTime, |
| completedDates: [], |
| streak: 0, |
| bestStreak: 0 |
| }; |
| |
| |
| const habits = JSON.parse(localStorage.getItem('habits')) || []; |
| habits.push(newHabit); |
| |
| |
| localStorage.setItem('habits', JSON.stringify(habits)); |
| |
| |
| document.getElementById('empty-state').style.display = 'none'; |
| renderHabits(habits); |
| updateCompletionRate(habits); |
| closeAddHabitModal(); |
| |
| |
| showToast('习惯添加成功!'); |
| } |
| |
| |
| function renderHabits(habits) { |
| const container = document.getElementById('habits-container'); |
| container.innerHTML = ''; |
| |
| habits.forEach(habit => { |
| const today = new Date().toDateString(); |
| const isCompleted = habit.completedDates.includes(today); |
| |
| const habitCard = document.createElement('div'); |
| habitCard.className = 'habit-card bg-white p-4 rounded-lg shadow-sm border-l-4 border-green-500'; |
| habitCard.dataset.id = habit.id; |
| |
| habitCard.innerHTML = ` |
| <div class="flex justify-between items-start"> |
| <div class="flex items-center space-x-3"> |
| <div class="bg-green-100 p-3 rounded-full text-green-600"> |
| <i class="fas fa-${habit.icon}"></i> |
| </div> |
| <div> |
| <h4 class="font-medium">${habit.name}</h4> |
| <p class="text-xs text-gray-500">${habit.frequency === 'daily' ? '每日' : habit.frequency === 'weekly' ? '每周' : '每月'}</p> |
| </div> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <span class="text-xs font-medium ${habit.streak > 0 ? 'text-orange-500' : 'text-gray-400'}"> |
| ${habit.streak > 0 ? `${habit.streak} 天连续` : '暂无连续记录'} |
| </span> |
| ${habit.streak > 3 ? '<i class="fas fa-fire text-orange-500 streak-fire"></i>' : ''} |
| </div> |
| </div> |
| <div class="mt-3 flex justify-between items-center"> |
| <div class="w-full bg-gray-200 rounded-full h-1.5 mr-2"> |
| <div class="progress-bar bg-green-500 h-1.5 rounded-full" style="width: ${(habit.completedDates.length / 30) * 100}%"></div> |
| </div> |
| <button class="complete-btn p-2 rounded-full ${isCompleted ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-500 hover:bg-green-100 hover:text-green-500'}"> |
| <i class="fas fa-check"></i> |
| </button> |
| </div> |
| `; |
| |
| container.appendChild(habitCard); |
| |
| |
| const completeBtn = habitCard.querySelector('.complete-btn'); |
| completeBtn.addEventListener('click', function() { |
| toggleHabitCompletion(habit.id); |
| }); |
| }); |
| } |
| |
| |
| function toggleHabitCompletion(habitId) { |
| const habits = JSON.parse(localStorage.getItem('habits')) || []; |
| const habitIndex = habits.findIndex(h => h.id === habitId); |
| |
| if (habitIndex === -1) return; |
| |
| const today = new Date().toDateString(); |
| const completedDates = habits[habitIndex].completedDates; |
| const isCompleted = completedDates.includes(today); |
| |
| if (isCompleted) { |
| |
| habits[habitIndex].completedDates = completedDates.filter(d => d !== today); |
| |
| const yesterday = new Date(); |
| yesterday.setDate(yesterday.getDate() - 1); |
| if (completedDates.includes(yesterday.toDateString())) { |
| habits[habitIndex].streak = Math.max(0, habits[habitIndex].streak - 1); |
| } |
| } else { |
| |
| habits[habitIndex].completedDates.push(today); |
| |
| const yesterday = new Date(); |
| yesterday.setDate(yesterday.getDate() - 1); |
| if (completedDates.includes(yesterday.toDateString()) || completedDates.length === 0) { |
| habits[habitIndex].streak += 1; |
| if (habits[habitIndex].streak > habits[habitIndex].bestStreak) { |
| habits[habitIndex].bestStreak = habits[habitIndex].streak; |
| } |
| } else { |
| |
| habits[habitIndex].streak = 1; |
| } |
| } |
| |
| |
| localStorage.setItem('habits', JSON.stringify(habits)); |
| |
| |
| renderHabits(habits); |
| updateCompletionRate(habits); |
| |
| |
| showToast(isCompleted ? '习惯标记为未完成' : '习惯已完成!做得好!'); |
| } |
| |
| |
| function updateCompletionRate(habits) { |
| if (habits.length === 0) { |
| document.getElementById('completion-rate').textContent = '0% 完成'; |
| return; |
| } |
| |
| const today = new Date().toDateString(); |
| const completedCount = habits.filter(h => h.completedDates.includes(today)).length; |
| const completionRate = Math.round((completedCount / habits.length) * 100); |
| |
| document.getElementById('completion-rate').textContent = `${completionRate}% 完成`; |
| } |
| |
| |
| function openStatsModal() { |
| const habits = JSON.parse(localStorage.getItem('habits')) || []; |
| |
| if (habits.length === 0) { |
| showToast('没有习惯可以显示统计'); |
| return; |
| } |
| |
| |
| const totalPossibleCompletions = habits.length * 30; |
| let totalCompletions = 0; |
| |
| habits.forEach(habit => { |
| totalCompletions += habit.completedDates.length; |
| }); |
| |
| const overallCompletionRate = Math.round((totalCompletions / totalPossibleCompletions) * 100); |
| |
| |
| document.getElementById('overall-progress').style.width = `${overallCompletionRate}%`; |
| document.getElementById('overall-percentage').textContent = `${overallCompletionRate}%`; |
| |
| |
| const streaksContainer = document.getElementById('streaks-container'); |
| streaksContainer.innerHTML = ''; |
| |
| habits.forEach(habit => { |
| const streakItem = document.createElement('div'); |
| streakItem.className = 'flex justify-between items-center bg-gray-50 p-3 rounded-lg'; |
| streakItem.innerHTML = ` |
| <div class="flex items-center space-x-3"> |
| <div class="bg-green-100 p-2 rounded-full text-green-600"> |
| <i class="fas fa-${habit.icon}"></i> |
| </div> |
| <span class="font-medium">${habit.name}</span> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <span class="text-sm ${habit.streak > 0 ? 'text-orange-500 font-medium' : 'text-gray-500'}"> |
| ${habit.streak} 天${habit.streak !== 1 ? '' : ''} |
| </span> |
| ${habit.streak > 3 ? '<i class="fas fa-fire text-orange-500 streak-fire"></i>' : ''} |
| </div> |
| `; |
| streaksContainer.appendChild(streakItem); |
| }); |
| |
| |
| const breakdownContainer = document.getElementById('habits-breakdown'); |
| breakdownContainer.innerHTML = ''; |
| |
| habits.forEach(habit => { |
| const completionRate = Math.round((habit.completedDates.length / 30) * 100); |
| |
| const breakdownItem = document.createElement('div'); |
| breakdownItem.className = 'space-y-1'; |
| breakdownItem.innerHTML = ` |
| <div class="flex justify-between text-sm"> |
| <span>${habit.name}</span> |
| <span class="font-medium">${completionRate}%</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-1.5"> |
| <div class="bg-green-500 h-1.5 rounded-full" style="width: ${completionRate}%"></div> |
| </div> |
| `; |
| breakdownContainer.appendChild(breakdownItem); |
| }); |
| |
| document.getElementById('stats-modal').style.display = 'flex'; |
| } |
| |
| |
| function closeStatsModal() { |
| document.getElementById('stats-modal').style.display = 'none'; |
| } |
| |
| |
| function showToast(message) { |
| const toast = document.createElement('div'); |
| toast.className = 'fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg text-sm animate-fade-in'; |
| toast.textContent = message; |
| |
| document.body.appendChild(toast); |
| |
| setTimeout(() => { |
| toast.classList.add('animate-fade-out'); |
| setTimeout(() => { |
| toast.remove(); |
| }, 300); |
| }, 3000); |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=mulukong/xiguan" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |