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