init commit.
This commit is contained in:
44
README.md
Normal file
44
README.md
Normal file
@@ -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 等)
|
||||
211
www/app.js
Normal file
211
www/app.js
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
46
www/index.html
Normal file
46
www/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!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 rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>任务日历</h1>
|
||||
<div class="controls">
|
||||
<button id="prevMonth" class="btn-nav">‹</button>
|
||||
<span id="currentMonth" class="month-display"></span>
|
||||
<button id="nextMonth" class="btn-nav">›</button>
|
||||
<button id="todayBtn" class="btn-today">今天</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<span class="mark completed">✓</span>
|
||||
<span>任务已完成</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="mark partial">○</span>
|
||||
<span>部分未完成</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="mark none"></span>
|
||||
<span>未标记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar" id="calendar"></div>
|
||||
|
||||
<div class="instructions">
|
||||
<p>💡 点击日期可切换标记状态:未标记 → 已完成 → 部分完成 → 未标记</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
266
www/style.css
Normal file
266
www/style.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user