好的,问题可以解构为两个核心部分:1. **功能性需求**:一个能够记录和展示“重要日子”(如纪念日、生日、截止日期等)的Web应用;2. **技术实现**:使用**Python**作为后端语言来实现。
基于此,方案推演如下:虽然可以直接用底层框架(如Flask、Django)从头搭建,但为了快速实现一个功能完整、界面美观的页面,更优的方案是结合一个轻量级Web框架(如Flask)和一个现代的前端组件库(如Vue.js)或模板。考虑到参考资料主要涉及Python爬虫,对于Web开发指导有限,我将基于通用的Python Web开发知识来构建,并引入一个实用的Python库`python-dateutil`来处理日期计算,以实现自动计算距离重要日子的剩余天数或已过去天数。
下面是一个完整、可运行的解决方案。该方案使用**Flask**作为后端框架,**SQLite**作为数据库存储,前端使用原生HTML/JavaScript配合简单的CSS实现交互和样式。
### 系统设计概览
| 模块 | 技术选型 | 说明 |
| :--- | :--- | :--- |
| **后端框架** | Flask | 轻量、灵活,适合快速构建Web应用 [ref_2][ref_3][ref_6]。 |
| **数据库** | SQLite | 无需额外服务,零配置,数据存储在单文件中。 |
| **前端** | HTML/CSS/JavaScript | 原生技术栈,易于理解和部署。 |
| **日期处理** | `datetime`, `dateutil` | 计算日期差,支持灵活的相对日期计算(如“每年重复”)。 |
| **项目结构** | 模块化组织 | 清晰分离路由、模型和模板。 |
### 核心功能
1. **添加重要日子**:输入事件名称、日期,并选择是否为每年重复事件。
2. **展示重要日子列表**:以表格形式展示,并自动计算“剩余天数”或“已过去天数”。
3. **删除重要日子**:移除不再需要记录的事件。
### 代码实现
#### 步骤1:环境准备与依赖安装
创建一个项目目录,并安装必要的Python包。
```bash
mkdir important_days_tracker && cd important_days_tracker
python -m venv venv
# Windows: venv\Scripts\activate
# Linux/Mac: source venv/bin/activate
pip install flask python-dateutil
```
#### 步骤2:数据库模型 (`models.py`)
首先定义数据模型,用于在SQLite数据库中创建表和进行数据操作。
```python
# models.py
import sqlite3
from datetime import date
class ImportantDay:
def __init__(self, db_path='important_days.db'):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库,创建表"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS important_days (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
day_date DATE NOT NULL,
is_yearly INTEGER DEFAULT 0 -- 0表示否,1表示是
)
''')
conn.commit()
def add_day(self, name, day_date, is_yearly=False):
"""添加一个新的重要日子"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO important_days (name, day_date, is_yearly)
VALUES (?, ?, ?)
''', (name, day_date, 1 if is_yearly else 0))
conn.commit()
return cursor.lastrowid
def get_all_days(self):
"""获取所有重要日子"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row # 以字典形式返回行
cursor = conn.cursor()
cursor.execute('SELECT * FROM important_days ORDER BY day_date')
return cursor.fetchall()
def delete_day(self, day_id):
"""根据ID删除一个重要日子"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('DELETE FROM important_days WHERE id = ?', (day_id,))
conn.commit()
return cursor.rowcount > 0
```
#### 步骤3:Flask应用主程序 (`app.py`)
这是应用的核心,负责处理HTTP请求,调用模型,并渲染页面。
```python
# app.py
from flask import Flask, render_template, request, jsonify, redirect, url_for
from models import ImportantDay
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
app = Flask(__name__)
db = ImportantDay()
def calculate_day_status(day_date_str, is_yearly):
"""
计算日期的状态。
返回一个字典,包含:
- `next_date`: 下一次发生的日期(如果每年重复,则计算今年或明年的日期)
- `days_away`: 距离今天的天数(正数为未来,负数为过去)
- `display_date`: 用于显示的原始日期
"""
today = date.today()
day_date = datetime.strptime(day_date_str, '%Y-%m-%d').date()
if is_yearly:
# 对于每年重复的日期,计算今年对应的日期
this_year_date = date(today.year, day_date.month, day_date.day)
if this_year_date >= today:
next_date = this_year_date
else:
# 如果今年的日期已经过去,则计算明年的
next_date = date(today.year + 1, day_date.month, day_date.day)
display_date = f"{day_date.month}月{day_date.day}日 (每年)"
else:
# 对于非重复日期,直接使用原日期
next_date = day_date
display_date = day_date.strftime('%Y年%m月%d日')
# 计算天数差
delta = (next_date - today).days
return {
'next_date': next_date.strftime('%Y-%m-%d'),
'days_away': delta,
'display_date': display_date
}
@app.route('/')
def index():
"""主页,展示所有重要日子"""
days_from_db = db.get_all_days()
days_for_display = []
for day in days_from_db:
status = calculate_day_status(day['day_date'], day['is_yearly'])
days_for_display.append({
'id': day['id'],
'name': day['name'],
'display_date': status['display_date'],
'days_away': status['days_away'],
'status_text': f"还有{status['days_away']}天" if status['days_away'] > 0 else f"已过去{-status['days_away']}天" if status['days_away'] < 0 else "就是今天!"
})
return render_template('index.html', days=days_for_display)
@app.route('/add', methods=['POST'])
def add_day():
"""处理添加重要日子的请求 (JSON API)"""
data = request.get_json()
name = data.get('name')
day_date = data.get('date')
is_yearly = data.get('is_yearly', False)
if not name or not day_date:
return jsonify({'success': False, 'error': '名称和日期不能为空'}), 400
try:
day_id = db.add_day(name, day_date, is_yearly)
# 计算新添加日子的状态,以便前端直接更新
status = calculate_day_status(day_date, is_yearly)
return jsonify({
'success': True,
'new_day': {
'id': day_id,
'name': name,
'display_date': status['display_date'],
'days_away': status['days_away'],
'status_text': f"还有{status['days_away']}天" if status['days_away'] > 0 else f"已过去{-status['days_away']}天" if status['days_away'] < 0 else "就是今天!"
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/delete/<int:day_id>', methods=['POST'])
def delete_day(day_id):
"""处理删除重要日子的请求"""
success = db.delete_day(day_id)
if success:
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': '记录未找到'}), 404
if __name__ == '__main__':
app.run(debug=True, port=5000)
```
#### 步骤4:前端HTML模板 (`templates/index.html`)
创建一个`templates`文件夹,并在其中创建`index.html`。这里使用了简单的内联CSS和JavaScript来实现交互。
```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>
* { box-sizing: border-box; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; }
body { max-width: 800px; margin: 40px auto; padding: 20px; background-color: #f9f9f9; color: #333; }
header { text-align: center; margin-bottom: 30px; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
.form-container { background: white; padding: 25px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); margin-bottom: 30px; }
.input-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
input[type="text"], input[type="date"] { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; }
.checkbox-group { display: flex; align-items: center; }
.checkbox-group input { width: auto; margin-right: 10px; }
button { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; }
button:hover { background-color: #2980b9; }
#addButton { width: 100%; }
#daysList { background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
th { background-color: #2c3e50; color: white; padding: 15px; text-align: left; }
td { padding: 15px; border-bottom: 1px solid #eee; }
tr:hover { background-color: #f5f9fc; }
.days-away { font-weight: bold; }
.days-away.future { color: #27ae60; }
.days-away.past { color: #e74c3c; }
.days-away.today { color: #f39c12; }
.delete-btn { background-color: #e74c3c; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; }
.delete-btn:hover { background-color: #c0392b; }
.empty-msg { text-align: center; padding: 40px; color: #95a5a6; font-style: italic; }
</style>
</head>
<body>
<header>
<h1>📅 重要日子记录本</h1>
<p>记录每一个值得纪念的瞬间</p>
</header>
<div class="form-container">
<h2>添加新日子</h2>
<form id="addForm">
<div class="input-group">
<label for="eventName">事件名称:</label>
<input type="text" id="eventName" placeholder="例如:结婚纪念日、项目截止" required>
</div>
<div class="input-group">
<label for="eventDate">日期:</label>
<input type="date" id="eventDate" required>
</div>
<div class="input-group checkbox-group">
<input type="checkbox" id="isYearly">
<label for="isYearly">每年重复(如生日、纪念日)</label>
</div>
<button type="submit" id="addButton">添加记录</button>
</form>
</div>
<div id="daysList">
<h2 style="padding: 20px 20px 0 20px; margin:0;">已记录的重要日子</h2>
<div id="tableContainer">
<!-- 表格将由JavaScript动态生成 -->
</div>
</div>
<script>
// 页面加载完成后,从后端获取并渲染日子列表
document.addEventListener('DOMContentLoaded', function() {
fetchDaysList();
});
// 监听表单提交事件,添加新日子
document.getElementById('addForm').addEventListener('submit', function(event) {
event.preventDefault(); // 阻止表单默认提交行为
const name = document.getElementById('eventName').value.trim();
const date = document.getElementById('eventDate').value;
const isYearly = document.getElementById('isYearly').checked;
if (!name || !date) {
alert('请填写完整信息!');
return;
}
fetch('/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, date, is_yearly: isYearly })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 清空表单
document.getElementById('addForm').reset();
// 在前端列表中添加新行
addDayToTable(data.new_day);
} else {
alert('添加失败: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('网络请求失败');
});
});
// 从后端获取日子列表并渲染表格
function fetchDaysList() {
// 因为主页(‘/’)已经渲染了初始数据,所以可以直接使用Flask模板传入的数据。
// 但为了示例完整性,这里提供一个通过API获取的备选方案。
// 实际中,我们直接使用下方 `renderDaysTable` 函数。
}
// 渲染日子表格的函数
function renderDaysTable(daysArray) {
const container = document.getElementById('tableContainer');
if (!daysArray || daysArray.length === 0) {
container.innerHTML = '<p class="empty-msg">还没有记录任何重要日子,快去添加一个吧!</p>';
return;
}
let tableHtml = `
<table>
<thead>
<tr>
<th>事件</th>
<th>日期</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
`;
daysArray.forEach(day => {
let statusClass = 'days-away ';
if (day.days_away > 0) statusClass += 'future';
else if (day.days_away < 0) statusClass += 'past';
else statusClass += 'today';
tableHtml += `
<tr id="row-${day.id}">
<td><strong>${escapeHtml(day.name)}</strong></td>
<td>${escapeHtml(day.display_date)}</td>
<td><span class="${statusClass}">${escapeHtml(day.status_text)}</span></td>
<td><button class="delete-btn" onclick="deleteDay(${day.id})">删除</button></td>
</tr>
`;
});
tableHtml += `</tbody></table>`;
container.innerHTML = tableHtml;
}
// 删除日子的函数
function deleteDay(dayId) {
if (!confirm('确定要删除这条记录吗?')) return;
fetch(`/delete/${dayId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById(`row-${dayId}`);
if (row) row.remove();
// 如果删除后列表为空,显示提示信息
const tbody = document.querySelector('#tableContainer tbody');
if (!tbody || tbody.children.length === 0) {
document.getElementById('tableContainer').innerHTML = '<p class="empty-msg">还没有记录任何重要日子,快去添加一个吧!</p>';
}
} else {
alert('删除失败: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('网络请求失败');
});
}
// 向表格中添加新行的函数(用于添加事件后)
function addDayToTable(day) {
const container = document.getElementById('tableContainer');
// 如果当前显示的是空状态提示,先清除它
if (container.querySelector('.empty-msg')) {
container.innerHTML = '<table><thead><tr><th>事件</th><th>日期</th><th>状态</th><th>操作</th></tr></thead><tbody id="daysTableBody"></tbody></table>';
}
const tbody = document.getElementById('daysTableBody') || container.querySelector('tbody');
if (!tbody) return;
let statusClass = 'days-away ';
if (day.days_away > 0) statusClass += 'future';
else if (day.days_away < 0) statusClass += 'past';
else statusClass += 'today';
const row = document.createElement('tr');
row.id = `row-${day.id}`;
row.innerHTML = `
<td><strong>${escapeHtml(day.name)}</strong></td>
<td>${escapeHtml(day.display_date)}</td>
<td><span class="${statusClass}">${escapeHtml(day.status_text)}</span></td>
<td><button class="delete-btn" onclick="deleteDay(${day.id})">删除</button></td>
`;
tbody.appendChild(row);
}
// 简单的HTML转义函数,防止XSS攻击
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始渲染:使用Flask模板传递过来的数据
const initialDays = {{ days | tojson | safe }};
renderDaysTable(initialDays);
</script>
</body>
</html>
```
### 运行与使用说明
1. **文件结构**:确保你的项目目录结构如下:
```
important_days_tracker/
├── app.py
├── models.py
├── templates/
│ └── index.html
└── important_days.db (运行后自动生成)
```
2. **启动应用**:在项目根目录下运行命令:
```bash
python app.py
```
控制台会输出类似 `* Running on http://127.0.0.1:5000` 的信息。
3. **访问应用**:打开浏览器,访问 `http://127.0.0.1:5000`。
4. **功能操作**:
* **添加**:在表单中输入事件名称,选择日期,勾选“每年重复”(如生日),点击“添加记录”。
* **查看**:页面下方表格会列出所有记录,并自动计算距离今天的天数。未来事件显示为绿色,过去事件显示为红色,当天事件显示为橙色。
* **删除**:点击每条记录右侧的“删除”按钮进行删除。
这个应用实现了核心的记录、计算和展示功能。你可以在此基础上进行扩展,例如增加编辑功能、为不同事件分类、设置提醒、导出数据,或者像参考资料中提到的爬虫项目一样,为它添加一个从某个网站自动获取特殊日期的功能 [ref_4]。这个项目的意义在于,通过亲手构建一个有用的工具,可以有效对抗开发中可能出现的“混日子”状态,因为它是一个明确、具体且有价值的目标,能带来持续的成就感 [ref_1]。