前言
这是一个基于HTML/CSS/JavaScript开发的每日打卡网页应用,由单个html文件构成,5种配色主题可选,响应式设计,采用本地存储记录数据。
项目于2025年8月份借助DeepSeek完成。

功能说明
1.基本功能
在页面中,用户可以添加、删除、清空打卡事项,还可以拖动排序。当用户单击某个事项卡片时,该卡片会变色以表示该项已完成,再次单击则会恢复。
2.扩展功能
(1)可以在页面上切换配色方案。目前有五套配色方案,可以在JavaScript中方便地切换,后续在CSS中添加新方案也很方便。
(2)采用响应式设计,在大屏幕上默认双列展示,在小屏幕上自动单列展示。
(3)采用本地存储记录数据,只要在同一台电脑的同一个浏览器上,就能保存打卡数据。
使用方式
考虑到便携性,我把html、CSS、JavaScript部分都整合在一个html文件中。
因此,复制代码到html文件中,双击打开即用。
在我的电脑(Win10,Edge浏览器)上测试通过。
备注:这是借助DeepSeek写的,代码细节我没有做过多研究,如有不当之处,敬请指正。
完整代码
html
<!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>
/* CSS变量定义 - 支持多主题 */
:root {
/* 默认主题 - 蓝色系 */
--primary-color: #4a6fa5;
--secondary-color: #6e9cd2;
--background-color: #f5f7fa;
--card-color: #ffffff;
--text-color: #333333;
--completed-color: #e8f5e9;
--border-color: #e0e0e0;
--shadow-color: rgba(0, 0, 0, 0.1);
--danger-color: #f44336;
--success-color: #4caf50;
}
/* 绿色主题 */
[data-theme="green"] {
--primary-color: #4caf50;
--secondary-color: #81c784;
--background-color: #f1f8e9;
--card-color: #ffffff;
--text-color: #33691e;
--completed-color: #e8f5e9;
--border-color: #c5e1a5;
--shadow-color: rgba(76, 175, 80, 0.2);
}
/* 紫色主题 */
[data-theme="purple"] {
--primary-color: #7e57c2;
--secondary-color: #b39ddb;
--background-color: #f3e5f5;
--card-color: #ffffff;
--text-color: #4527a0;
--completed-color: #ede7f6;
--border-color: #d1c4e9;
--shadow-color: rgba(126, 87, 194, 0.2);
}
/* 橙色主题 */
[data-theme="orange"] {
--primary-color: #ff9800;
--secondary-color: #ffb74d;
--background-color: #fff3e0;
--card-color: #ffffff;
--text-color: #e65100;
--completed-color: #ffe0b2;
--border-color: #ffcc80;
--shadow-color: rgba(255, 152, 0, 0.2);
}
/* 深色主题 */
[data-theme="dark"] {
--primary-color: #9c27b0;
--secondary-color: #ba68c8;
--background-color: #303030;
--card-color: #424242;
--text-color: #f5f5f5;
--completed-color: #6a1b9a;
--border-color: #616161;
--shadow-color: rgba(0, 0, 0, 0.4);
}
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 15px;
}
h1 {
color: var(--primary-color);
font-size: 2.2rem;
margin: 0;
}
/* 控制按钮组样式 */
.controls {
display: flex;
gap: 10px;
}
button {
padding: 10px 15px;
border: none;
border-radius: 5px;
background-color: var(--primary-color);
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s, transform 0.1s;
}
button:hover {
background-color: var(--secondary-color);
}
button:active {
transform: scale(0.98);
}
button.danger {
background-color: var(--danger-color);
}
button.danger:hover {
background-color: #e53935;
}
/* 主题选择器样式 */
.theme-selector {
position: relative;
display: inline-block;
}
.theme-selector select {
padding: 10px 15px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--card-color);
color: var(--text-color);
appearance: none;
cursor: pointer;
width: 150px;
}
/* 打卡项目列表样式 */
.habits-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
/* 打卡项目卡片样式 */
.habit-card {
background-color: var(--card-color);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 8px var(--shadow-color);
transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s;
cursor: pointer;
position: relative;
user-select: none;
}
.habit-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px var(--shadow-color);
}
.habit-card.completed {
background-color: var(--completed-color);
}
.habit-card .habit-name {
font-size: 1.2rem;
margin-bottom: 10px;
font-weight: 500;
}
.habit-card .delete-btn {
position: absolute;
top: 10px;
right: 10px;
width: 25px;
height: 25px;
border-radius: 50%;
background-color: rgba(244, 67, 54, 0.2);
color: var(--danger-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
opacity: 0;
transition: opacity 0.2s;
}
.habit-card:hover .delete-btn {
opacity: 1;
}
.habit-card .delete-btn:hover {
background-color: var(--danger-color);
color: white;
}
/* 添加新项目表单样式 */
.add-habit-form {
background-color: var(--card-color);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 8px var(--shadow-color);
margin-bottom: 30px;
}
.add-habit-form h2 {
margin-bottom: 15px;
color: var(--primary-color);
font-size: 1.5rem;
}
.form-group {
display: flex;
gap: 10px;
}
.form-group input {
flex: 1;
padding: 10px 15px;
border: 1px solid var(--border-color);
border-radius: 5px;
font-size: 1rem;
background-color: var(--card-color);
color: var(--text-color);
}
/* 响应式设计 */
@media (max-width: 600px) {
header {
flex-direction: column;
align-items: stretch;
text-align:center;
}
.controls {
justify-content: center;
}
.habits-list {
grid-template-columns: 1fr;
}
.form-group {
flex-direction: column;
}
}
/* 拖动时的样式 */
.habit-card.dragging {
opacity: 0.5;
box-shadow: 0 10px 20px var(--shadow-color);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>每日打卡</h1>
<div class="controls">
<div class="theme-selector">
<select id="theme-switcher">
<option value="default">默认主题</option>
<option value="green">绿色主题</option>
<option value="purple">紫色主题</option>
<option value="orange">橙色主题</option>
<option value="dark">深色主题</option>
</select>
</div>
<button id="reset-btn" class="danger">清空今日打卡</button>
</div>
</header>
<main>
<div class="add-habit-form">
<h2>添加新项目</h2>
<div class="form-group">
<input type="text" id="new-habit-input" placeholder="输入打卡项目名称">
<button id="add-habit-btn">添加</button>
</div>
</div>
<div class="habits-list" id="habits-container">
<!-- 打卡项目将通过JavaScript动态添加 -->
</div>
</main>
</div>
<script>
// 定义全局变量
let habits = []; // 存储所有打卡项目
let currentDate = new Date().toDateString(); // 当前日期,用于判断是否是新的一天
/**
* 初始化应用
* 从localStorage加载数据并设置事件监听器
*/
function initApp() {
// 从localStorage加载数据
loadFromLocalStorage();
// 检查是否是新的一天,如果是则重置打卡状态
checkForNewDay();
// 渲染打卡项目
renderHabits();
// 设置事件监听器
setupEventListeners();
}
/**
* 从localStorage加载数据
*/
function loadFromLocalStorage() {
// 尝试从localStorage加载打卡项目
const savedHabits = localStorage.getItem('dailyHabits');
if (savedHabits) {
habits = JSON.parse(savedHabits);
} else {
// 如果没有保存的数据,使用默认项目
habits = [
{ id: 1, name: '背单词', completed: false },
{ id: 2, name: '吃早饭', completed: false },
{ id: 3, name: '运动30分钟', completed: false }
];
saveToLocalStorage();
}
// 加载上次使用的主题
const savedTheme = localStorage.getItem('selectedTheme') || 'default';
document.documentElement.setAttribute('data-theme', savedTheme);
document.getElementById('theme-switcher').value = savedTheme;
// 加载上次使用的日期
const savedDate = localStorage.getItem('lastAccessDate');
if (savedDate) {
// 不需要做任何操作,我们已经在checkForNewDay中处理了
}
}
/**
* 检查是否是新的一天,如果是则重置所有打卡状态
*/
function checkForNewDay() {
const lastAccessDate = localStorage.getItem('lastAccessDate');
if (lastAccessDate !== currentDate) {
// 是新的一天,重置所有打卡状态
habits.forEach(habit => {
habit.completed = false;
});
saveToLocalStorage();
// 更新最后访问日期
localStorage.setItem('lastAccessDate', currentDate);
}
}
/**
* 保存数据到localStorage
*/
function saveToLocalStorage() {
localStorage.setItem('dailyHabits', JSON.stringify(habits));
}
/**
* 渲染所有打卡项目
*/
function renderHabits() {
const container = document.getElementById('habits-container');
container.innerHTML = ''; // 清空容器
habits.forEach(habit => {
const habitElement = createHabitElement(habit);
container.appendChild(habitElement);
});
}
/**
* 创建单个打卡项目的DOM元素
* @param {Object} habit 打卡项目对象
* @returns {HTMLElement} 打卡项目的DOM元素
*/
function createHabitElement(habit) {
const habitCard = document.createElement('div');
habitCard.className = `habit-card ${habit.completed ? 'completed' : ''}`;
habitCard.setAttribute('data-id', habit.id);
habitCard.draggable = true;
const habitName = document.createElement('div');
habitName.className = 'habit-name';
habitName.textContent = habit.name;
const deleteBtn = document.createElement('div');
deleteBtn.className = 'delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发打卡操作
deleteHabit(habit.id);
});
habitCard.appendChild(habitName);
habitCard.appendChild(deleteBtn);
// 添加点击事件(打卡/取消打卡)
habitCard.addEventListener('click', () => {
toggleHabitCompletion(habit.id);
});
// 添加拖拽事件
habitCard.addEventListener('dragstart', handleDragStart);
habitCard.addEventListener('dragover', handleDragOver);
habitCard.addEventListener('drop', handleDrop);
habitCard.addEventListener('dragend', handleDragEnd);
return habitCard;
}
/**
* 切换打卡项目的完成状态
* @param {number} id 打卡项目的ID
*/
function toggleHabitCompletion(id) {
const habit = habits.find(h => h.id === id);
if (habit) {
habit.completed = !habit.completed;
saveToLocalStorage();
renderHabits();
}
}
/**
* 删除打卡项目
* @param {number} id 要删除的打卡项目的ID
*/
function deleteHabit(id) {
if (confirm('确定要删除这个打卡项目吗?')) {
habits = habits.filter(habit => habit.id !== id);
saveToLocalStorage();
renderHabits();
}
}
/**
* 添加新的打卡项目
*/
function addNewHabit() {
const input = document.getElementById('new-habit-input');
const name = input.value.trim();
if (name) {
// 创建新项目
const newHabit = {
id: Date.now(), // 使用时间戳作为唯一ID
name: name,
completed: false
};
habits.push(newHabit);
saveToLocalStorage();
renderHabits();
// 清空输入框
input.value = '';
} else {
alert('请输入打卡项目名称');
}
}
/**
* 清空所有打卡状态(用于新的一天)
*/
function resetAllHabits() {
if (confirm('确定要清空今日打卡状态吗?')) {
habits.forEach(habit => {
habit.completed = false;
});
saveToLocalStorage();
renderHabits();
}
}
/**
* 切换主题
* @param {string} theme 主题名称
*/
function switchTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('selectedTheme', theme);
}
// 拖拽相关变量
let draggedItem = null;
/**
* 处理拖拽开始事件
* @param {DragEvent} e 拖拽事件对象
*/
function handleDragStart(e) {
draggedItem = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', this.getAttribute('data-id'));
}
/**
* 处理拖拽结束事件
*/
function handleDragEnd() {
this.classList.remove('dragging');
draggedItem = null;
}
/**
* 处理拖拽经过事件
* @param {DragEvent} e 拖拽事件对象
*/
function handleDragOver(e) {
e.preventDefault();
return false;
}
/**
* 处理放置事件
* @param {DragEvent} e 拖拽事件对象
*/
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
if (draggedItem !== this) {
// 获取拖拽项目和目标项目的ID
const draggedId = parseInt(draggedItem.getAttribute('data-id'));
const targetId = parseInt(this.getAttribute('data-id'));
// 找到两个项目在数组中的索引
const draggedIndex = habits.findIndex(h => h.id === draggedId);
const targetIndex = habits.findIndex(h => h.id === targetId);
if (draggedIndex !== -1 && targetIndex !== -1) {
// 重新排序数组
const [draggedHabit] = habits.splice(draggedIndex, 1);
habits.splice(targetIndex, 0, draggedHabit);
// 保存并重新渲染
saveToLocalStorage();
renderHabits();
}
}
return false;
}
/**
* 设置所有事件监听器
*/
function setupEventListeners() {
// 添加新项目按钮点击事件
document.getElementById('add-habit-btn').addEventListener('click', addNewHabit);
// 输入框回车键事件
document.getElementById('new-habit-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addNewHabit();
}
});
// 主题切换事件
document.getElementById('theme-switcher').addEventListener('change', (e) => {
switchTheme(e.target.value);
});
// 清空打卡按钮点击事件
document.getElementById('reset-btn').addEventListener('click', resetAllHabits);
}
// 当DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', initApp);
</script>
</body>
</html>