From 7a5fb889c5392a75e26bee12c19e2ed70d8b3b3f Mon Sep 17 00:00:00 2001 From: licsber Date: Tue, 11 Nov 2025 11:07:25 +0800 Subject: [PATCH] init commit. --- README.md | 44 ++++++++ www/app.js | 211 +++++++++++++++++++++++++++++++++++++++ www/index.html | 46 +++++++++ www/style.css | 266 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 567 insertions(+) create mode 100644 README.md create mode 100644 www/app.js create mode 100644 www/index.html create mode 100644 www/style.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..3995b08 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# 任务日历系统 + +一个简洁美观的任务日历系统,可以标记每一天的任务完成状态。 + +## 功能特性 + +- 📅 **日历视图**:清晰的月份日历展示 +- ✅ **任务标记**:支持三种状态 + - ✓ 任务已完成(绿色) + - ○ 任务部分未完成(橙色) + - 未标记(灰色) +- 💾 **数据保存**:使用浏览器 localStorage 自动保存标记数据 +- 🎨 **简洁界面**:现代化的 UI 设计,响应式布局 +- 🖱️ **便捷操作**:点击日期即可切换标记状态 + +## 使用方法 + +1. 直接在浏览器中打开 `index.html` 文件 +2. 点击日历上的日期来切换标记状态: + - 第一次点击:标记为"已完成"(✓) + - 第二次点击:标记为"部分完成"(○) + - 第三次点击:清除标记 +3. 使用左右箭头按钮切换月份 +4. 点击"今天"按钮快速回到当前月份 + +## 文件结构 + +``` +timeline/ +├── index.html # 主页面 +├── style.css # 样式文件 +├── app.js # 应用逻辑 +└── README.md # 说明文档 +``` + +## 技术说明 + +- 纯 HTML/CSS/JavaScript 实现,无需构建工具 +- 使用 localStorage 保存数据,刷新页面后数据不会丢失 +- 响应式设计,支持移动端和桌面端 + +## 浏览器兼容性 + +支持所有现代浏览器(Chrome、Firefox、Safari、Edge 等) diff --git a/www/app.js b/www/app.js new file mode 100644 index 0000000..0515ffd --- /dev/null +++ b/www/app.js @@ -0,0 +1,211 @@ +// 日历状态管理 +class CalendarApp { + constructor() { + this.currentDate = new Date(); + this.markedDates = this.loadData(); + this.init(); + } + + init() { + this.renderCalendar(); + this.bindEvents(); + } + + // 获取月份的第一天是星期几(0=周日, 1=周一...) + getFirstDayOfMonth(date) { + const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); + return firstDay.getDay(); + } + + // 获取月份的天数 + getDaysInMonth(date) { + return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); + } + + // 格式化日期为 YYYY-MM-DD + formatDate(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + // 检查是否为今天 + isToday(date) { + const today = new Date(); + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + } + + // 获取日期标记状态 + getDateStatus(date) { + const dateStr = this.formatDate(date); + return this.markedDates[dateStr] || 'none'; + } + + // 切换日期标记状态 + toggleDateStatus(date) { + const dateStr = this.formatDate(date); + const currentStatus = this.markedDates[dateStr] || 'none'; + + // 状态循环: none -> completed -> partial -> none + let newStatus; + switch (currentStatus) { + case 'none': + newStatus = 'completed'; + break; + case 'completed': + newStatus = 'partial'; + break; + case 'partial': + newStatus = 'none'; + break; + default: + newStatus = 'none'; + } + + if (newStatus === 'none') { + delete this.markedDates[dateStr]; + } else { + this.markedDates[dateStr] = newStatus; + } + + this.saveData(); + this.renderCalendar(); + } + + // 渲染日历 + renderCalendar() { + const calendar = document.getElementById('calendar'); + calendar.innerHTML = ''; + + // 星期标题 + const weekdays = ['日', '一', '二', '三', '四', '五', '六']; + weekdays.forEach(day => { + const weekdayEl = document.createElement('div'); + weekdayEl.className = 'weekday'; + weekdayEl.textContent = day; + calendar.appendChild(weekdayEl); + }); + + // 更新月份显示 + const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', + '七月', '八月', '九月', '十月', '十一月', '十二月']; + const monthDisplay = document.getElementById('currentMonth'); + monthDisplay.textContent = `${this.currentDate.getFullYear()}年 ${monthNames[this.currentDate.getMonth()]}`; + + // 获取月份信息 + const firstDay = this.getFirstDayOfMonth(this.currentDate); + const daysInMonth = this.getDaysInMonth(this.currentDate); + const today = new Date(); + + // 添加上个月的日期(填充空白) + const prevMonth = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 0); + const daysInPrevMonth = prevMonth.getDate(); + + for (let i = firstDay - 1; i >= 0; i--) { + const day = daysInPrevMonth - i; + const date = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, day); + this.createDayElement(calendar, date, day, true); + } + + // 添加当前月的日期 + for (let day = 1; day <= daysInMonth; day++) { + const date = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), day); + this.createDayElement(calendar, date, day, false); + } + + // 添加下个月的日期(填充到完整周) + const totalCells = calendar.children.length - 7; // 减去星期标题 + const remainingCells = 42 - totalCells; // 6行 x 7列 = 42 + for (let day = 1; day <= remainingCells; day++) { + const date = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, day); + this.createDayElement(calendar, date, day, true); + } + } + + // 创建日期元素 + createDayElement(container, date, dayNumber, isOtherMonth) { + const dayEl = document.createElement('div'); + dayEl.className = 'day'; + + if (isOtherMonth) { + dayEl.classList.add('other-month'); + } + + if (this.isToday(date)) { + dayEl.classList.add('today'); + } + + const status = this.getDateStatus(date); + if (status !== 'none') { + dayEl.classList.add(status); + } + + const dayNumberEl = document.createElement('div'); + dayNumberEl.className = 'day-number'; + dayNumberEl.textContent = dayNumber; + dayEl.appendChild(dayNumberEl); + + if (status !== 'none') { + const markEl = document.createElement('div'); + markEl.className = 'day-mark'; + markEl.textContent = status === 'completed' ? '✓' : '○'; + dayEl.appendChild(markEl); + } + + // 添加点击事件 + dayEl.addEventListener('click', () => { + if (!isOtherMonth) { + this.toggleDateStatus(date); + } + }); + + container.appendChild(dayEl); + } + + // 绑定事件 + bindEvents() { + document.getElementById('prevMonth').addEventListener('click', () => { + this.currentDate.setMonth(this.currentDate.getMonth() - 1); + this.renderCalendar(); + }); + + document.getElementById('nextMonth').addEventListener('click', () => { + this.currentDate.setMonth(this.currentDate.getMonth() + 1); + this.renderCalendar(); + }); + + document.getElementById('todayBtn').addEventListener('click', () => { + this.currentDate = new Date(); + this.renderCalendar(); + }); + } + + // 保存数据到 localStorage + saveData() { + try { + localStorage.setItem('calendarMarkedDates', JSON.stringify(this.markedDates)); + } catch (e) { + console.error('保存数据失败:', e); + } + } + + // 从 localStorage 加载数据 + loadData() { + try { + const data = localStorage.getItem('calendarMarkedDates'); + return data ? JSON.parse(data) : {}; + } catch (e) { + console.error('加载数据失败:', e); + return {}; + } + } +} + +// 初始化应用 +document.addEventListener('DOMContentLoaded', () => { + new CalendarApp(); +}); + diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..b787507 --- /dev/null +++ b/www/index.html @@ -0,0 +1,46 @@ + + + + + + 任务日历系统 + + + +
+
+

任务日历

+
+ + + + +
+
+ +
+
+ + 任务已完成 +
+
+ + 部分未完成 +
+
+ + 未标记 +
+
+ +
+ +
+

💡 点击日期可切换标记状态:未标记 → 已完成 → 部分完成 → 未标记

+
+
+ + + + + diff --git a/www/style.css b/www/style.css new file mode 100644 index 0000000..3e575ae --- /dev/null +++ b/www/style.css @@ -0,0 +1,266 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + color: #333; +} + +.container { + max-width: 900px; + margin: 0 auto; + background: white; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + padding: 30px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + color: #667eea; + margin-bottom: 20px; + font-weight: 600; +} + +.controls { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + flex-wrap: wrap; +} + +.btn-nav { + background: #667eea; + color: white; + border: none; + width: 40px; + height: 40px; + border-radius: 50%; + font-size: 24px; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-nav:hover { + background: #5568d3; + transform: scale(1.1); +} + +.btn-nav:active { + transform: scale(0.95); +} + +.month-display { + font-size: 1.5em; + font-weight: 600; + color: #333; + min-width: 200px; +} + +.btn-today { + background: #764ba2; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-today:hover { + background: #653a8a; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4); +} + +.legend { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 25px; + padding: 15px; + background: #f8f9fa; + border-radius: 10px; + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.mark { + width: 24px; + height: 24px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 16px; +} + +.mark.completed { + background: #10b981; + color: white; +} + +.mark.partial { + background: #f59e0b; + color: white; +} + +.mark.none { + background: #e5e7eb; + border: 2px solid #d1d5db; +} + +.calendar { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + margin-bottom: 20px; +} + +.weekday { + text-align: center; + font-weight: 600; + color: #667eea; + padding: 12px; + font-size: 14px; + background: #f0f4ff; + border-radius: 8px; +} + +.day { + aspect-ratio: 1; + border: 2px solid #e5e7eb; + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + background: white; + position: relative; + padding: 8px; +} + +.day:hover { + border-color: #667eea; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); +} + +.day.other-month { + opacity: 0.4; + background: #f9fafb; +} + +.day.today { + border-color: #667eea; + background: #f0f4ff; + font-weight: 600; +} + +.day.completed { + background: #d1fae5; + border-color: #10b981; +} + +.day.completed .day-number { + color: #065f46; +} + +.day.partial { + background: #fef3c7; + border-color: #f59e0b; +} + +.day.partial .day-number { + color: #92400e; +} + +.day-number { + font-size: 16px; + font-weight: 500; + margin-bottom: 4px; +} + +.day-mark { + font-size: 20px; + font-weight: bold; + line-height: 1; +} + +.day.completed .day-mark { + color: #10b981; +} + +.day.partial .day-mark { + color: #f59e0b; +} + +.instructions { + text-align: center; + padding: 15px; + background: #f0f4ff; + border-radius: 10px; + color: #667eea; + font-size: 14px; +} + +@media (max-width: 768px) { + .container { + padding: 20px; + } + + header h1 { + font-size: 2em; + } + + .month-display { + font-size: 1.2em; + min-width: 150px; + } + + .calendar { + gap: 4px; + } + + .day { + padding: 4px; + } + + .day-number { + font-size: 14px; + } + + .day-mark { + font-size: 16px; + } + + .legend { + gap: 15px; + } +} +