Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c99eca35f0 | |||
| 47b195ed1c | |||
| 1f1528979c | |||
| bcb24e474d | |||
| a19b260ef7 | |||
| 1d8ce5cfb9 | |||
| cb2cbd4c6b | |||
| c0d221c2a3 | |||
| 6b775c99f9 | |||
| 061bfa3b55 | |||
| 2eccc11369 |
27
README.md
27
README.md
@@ -141,6 +141,33 @@ xian-favor/
|
|||||||
|
|
||||||
## 版本历史
|
## 版本历史
|
||||||
|
|
||||||
|
- **v2.1.0** (2026-04-16): 待办截止时间支持日期+时间
|
||||||
|
- 截止日期改为日期时间选择器
|
||||||
|
- 列表显示友好格式:今天 18:30、明天 09:00 等
|
||||||
|
- 详情页显示完整格式:2026年4月16日 18:30
|
||||||
|
- 后端支持多种日期格式解析
|
||||||
|
- 只有日期的待办视为当天 23:59 到期
|
||||||
|
- **v2.0.1** (2026-04-16): 转换弹窗优化
|
||||||
|
- 内容预览保留换行格式,提高可读性
|
||||||
|
- 转换方式默认改为复制创建
|
||||||
|
- **v2.0.0** (2026-04-16): 收藏转待办功能(大版本更新)
|
||||||
|
- 新增 `/api/items/<id>/convert` API
|
||||||
|
- **直接转换**:原收藏变为待办,数据合并
|
||||||
|
- **复制创建**:保留原收藏,新建待办任务
|
||||||
|
- 文本/链接/专栏类型可转换
|
||||||
|
- 内容自动合并到待办备注
|
||||||
|
- 前端列表和详情页添加转换按钮
|
||||||
|
- 弹窗配置:标题、状态、优先级、截止日期
|
||||||
|
- v1.11.0 (2026-04-16): 已完成待办可重新打开
|
||||||
|
- 新增 `/api/items/<id>/reopen` 接口
|
||||||
|
- 前端列表已完成待办显示重新打开按钮(↻图标)
|
||||||
|
- 可恢复为 pending 或 in_progress 状态
|
||||||
|
- v1.10.0 (2026-04-16): 网页端待办到期提醒功能
|
||||||
|
- 新增提醒 API `/api/reminders`
|
||||||
|
- 获取已过期、今天到期、即将到期的待办
|
||||||
|
- 前端顶部提醒栏显示提醒数量和概要
|
||||||
|
- 弹窗详情支持快速完成待办
|
||||||
|
- 每5分钟自动刷新提醒
|
||||||
- v1.9.2 (2026-04-14): 邮件发送历史记录
|
- v1.9.2 (2026-04-14): 邮件发送历史记录
|
||||||
- 详情页面显示邮件发送记录(发送邮箱、时间)
|
- 详情页面显示邮件发送记录(发送邮箱、时间)
|
||||||
- 支持多次发送,显示多条记录
|
- 支持多次发送,显示多条记录
|
||||||
|
|||||||
@@ -112,6 +112,93 @@ def complete_item(item_id):
|
|||||||
return jsonify({'success': True, 'data': item})
|
return jsonify({'success': True, 'data': item})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/items/<int:item_id>/reopen', methods=['POST'])
|
||||||
|
def reopen_item(item_id):
|
||||||
|
"""重新打开待办"""
|
||||||
|
item = db.get_item(item_id)
|
||||||
|
if not item:
|
||||||
|
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||||
|
|
||||||
|
if item['type'] != 'todo':
|
||||||
|
return jsonify({'success': False, 'error': '不是待办事项'}), 400
|
||||||
|
|
||||||
|
# 获取请求中的目标状态,默认为 pending
|
||||||
|
data = request.get_json() or {}
|
||||||
|
new_status = data.get('status', 'pending')
|
||||||
|
|
||||||
|
if new_status not in ['pending', 'in_progress']:
|
||||||
|
return jsonify({'success': False, 'error': '无效状态'}), 400
|
||||||
|
|
||||||
|
db.update_item(item_id, status=new_status)
|
||||||
|
item = db.get_item(item_id)
|
||||||
|
return jsonify({'success': True, 'data': item})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/items/<int:item_id>/convert', methods=['POST'])
|
||||||
|
def convert_item(item_id):
|
||||||
|
"""将收藏转换为待办"""
|
||||||
|
item = db.get_item(item_id)
|
||||||
|
if not item:
|
||||||
|
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||||
|
|
||||||
|
# 不能转换已经是待办的
|
||||||
|
if item['type'] == 'todo':
|
||||||
|
return jsonify({'success': False, 'error': '已经是待办事项'}), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
mode = data.get('mode', 'convert') # convert 或 copy
|
||||||
|
|
||||||
|
if mode not in ['convert', 'copy']:
|
||||||
|
return jsonify({'success': False, 'error': '无效转换模式'}), 400
|
||||||
|
|
||||||
|
# 构建待办数据
|
||||||
|
todo_data = {
|
||||||
|
'type': 'todo',
|
||||||
|
'title': data.get('title') or item['title'] or f'{item["type"]}收藏',
|
||||||
|
'status': data.get('status', 'pending'),
|
||||||
|
'priority': data.get('priority', 'medium'),
|
||||||
|
'due_date': data.get('due_date'),
|
||||||
|
'tags': item['tags'], # 继承原标签
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据原类型处理内容
|
||||||
|
if item['type'] == 'text':
|
||||||
|
# 文本:content 放到 note
|
||||||
|
todo_data['note'] = item['content'] or ''
|
||||||
|
if item['note']:
|
||||||
|
todo_data['note'] += '\n\n' + item['note']
|
||||||
|
elif item['type'] == 'link':
|
||||||
|
# 链接:url 放到 note,保留链接可点击
|
||||||
|
todo_data['note'] = f'链接: {item["url"]}\n\n'
|
||||||
|
if item['content']:
|
||||||
|
todo_data['note'] += item['content'] + '\n\n'
|
||||||
|
if item['note']:
|
||||||
|
todo_data['note'] += item['note']
|
||||||
|
# url 字段也保留(方便后续操作)
|
||||||
|
todo_data['content'] = item['url']
|
||||||
|
elif item['type'] == 'column':
|
||||||
|
# 专栏:url + source 放到 note
|
||||||
|
todo_data['note'] = f'专栏: {item["url"]}\n'
|
||||||
|
if item['source']:
|
||||||
|
todo_data['note'] += f'来源: {item["source"]}\n\n'
|
||||||
|
if item['content']:
|
||||||
|
todo_data['note'] += item['content'] + '\n\n'
|
||||||
|
if item['note']:
|
||||||
|
todo_data['note'] += item['note']
|
||||||
|
todo_data['content'] = item['url']
|
||||||
|
|
||||||
|
if mode == 'convert':
|
||||||
|
# 直接转换:更新原条目
|
||||||
|
db.update_item(item_id, **todo_data)
|
||||||
|
result = db.get_item(item_id)
|
||||||
|
return jsonify({'success': True, 'data': result, 'mode': 'convert'})
|
||||||
|
else:
|
||||||
|
# 复制创建:新建待办,原条目保留
|
||||||
|
new_id = db.create_item(**todo_data)
|
||||||
|
result = db.get_item(new_id)
|
||||||
|
return jsonify({'success': True, 'data': result, 'mode': 'copy', 'original_id': item_id})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/tags', methods=['GET'])
|
@app.route('/api/tags', methods=['GET'])
|
||||||
def list_tags():
|
def list_tags():
|
||||||
"""列出标签"""
|
"""列出标签"""
|
||||||
@@ -167,6 +254,13 @@ def get_stats():
|
|||||||
return jsonify({'success': True, 'data': stats})
|
return jsonify({'success': True, 'data': stats})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/reminders', methods=['GET'])
|
||||||
|
def get_reminders():
|
||||||
|
"""获取提醒信息"""
|
||||||
|
reminders = db.get_reminders()
|
||||||
|
return jsonify({'success': True, 'data': reminders})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/ai-process', methods=['POST'])
|
@app.route('/api/ai-process', methods=['POST'])
|
||||||
def ai_process():
|
def ai_process():
|
||||||
"""AI处理文本"""
|
"""AI处理文本"""
|
||||||
@@ -490,6 +584,16 @@ INDEX_TEMPLATE = '''
|
|||||||
|
|
||||||
<!-- 主内容 -->
|
<!-- 主内容 -->
|
||||||
<div class="col-md-10 content">
|
<div class="col-md-10 content">
|
||||||
|
<!-- 提醒栏 -->
|
||||||
|
<div id="reminderBar" class="alert alert-warning alert-dismissible fade show mb-3" style="display:none;" role="alert">
|
||||||
|
<i class="bi bi-bell-fill"></i>
|
||||||
|
<span id="reminderText">有待办事项需要关注</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning ms-2" onclick="showRemindersModal()">
|
||||||
|
<i class="bi bi-eye"></i> 查看详情
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" onclick="dismissReminderBar()"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 顶部操作栏 -->
|
<!-- 顶部操作栏 -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
@@ -505,12 +609,21 @@ INDEX_TEMPLATE = '''
|
|||||||
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
||||||
<i class="bi bi-robot"></i> AI添加
|
<i class="bi bi-robot"></i> AI添加
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary me-1" onclick="showAddModal('text')" title="添加文本">
|
||||||
|
<i class="bi bi-file-text"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-success me-1" onclick="showAddModal('link')" title="添加链接">
|
||||||
|
<i class="bi bi-link-45deg"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-warning me-1" onclick="showAddModal('todo')" title="添加待办">
|
||||||
|
<i class="bi bi-check2-square"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary me-1" onclick="showAddModal('column')" title="添加专栏">
|
||||||
|
<i class="bi bi-newspaper"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
|
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
|
||||||
<i class="bi bi-download"></i> 导出
|
<i class="bi bi-download"></i> 导出
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
|
||||||
<i class="bi bi-plus-lg"></i> 添加
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
@@ -563,20 +676,15 @@ INDEX_TEMPLATE = '''
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">添加条目</h5>
|
<h5 class="modal-title">
|
||||||
|
<span id="addModalIcon"></span>
|
||||||
|
<span id="addModalTitle">添加条目</span>
|
||||||
|
</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="addForm">
|
<form id="addForm">
|
||||||
<div class="mb-3">
|
<input type="hidden" id="addType" value="text">
|
||||||
<label class="form-label">类型</label>
|
|
||||||
<select id="addType" class="form-select">
|
|
||||||
<option value="text">📝 文本</option>
|
|
||||||
<option value="link">🔗 链接</option>
|
|
||||||
<option value="column">📰 专栏</option>
|
|
||||||
<option value="todo">✅ 待办</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">标题</label>
|
<label class="form-label">标题</label>
|
||||||
<input type="text" id="addTitle" class="form-control" placeholder="可选">
|
<input type="text" id="addTitle" class="form-control" placeholder="可选">
|
||||||
@@ -587,11 +695,11 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="urlGroup" style="display:none;">
|
<div class="mb-3" id="urlGroup" style="display:none;">
|
||||||
<label class="form-label">URL</label>
|
<label class="form-label">URL</label>
|
||||||
<input type="url" id="addUrl" class="form-control">
|
<input type="url" id="addUrl" class="form-control" placeholder="https://...">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="sourceGroup" style="display:none;">
|
<div class="mb-3" id="sourceGroup" style="display:none;">
|
||||||
<label class="form-label">来源</label>
|
<label class="form-label">来源</label>
|
||||||
<input type="text" id="addSource" class="form-control">
|
<input type="text" id="addSource" class="form-control" placeholder="专栏来源">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="todoFields" style="display:none;">
|
<div class="mb-3" id="todoFields" style="display:none;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -614,8 +722,8 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<label class="form-label">截止日期</label>
|
<label class="form-label">截止时间</label>
|
||||||
<input type="date" id="addDueDate" class="form-control">
|
<input type="datetime-local" id="addDueDate" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -652,6 +760,9 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="detailConvertBtn" onclick="showConvertModalFromDetail()" style="display:none;">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> 转为待办
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="openEditModalFromDetail()">
|
<button type="button" class="btn btn-outline-primary" onclick="openEditModalFromDetail()">
|
||||||
<i class="bi bi-pencil"></i> 编辑
|
<i class="bi bi-pencil"></i> 编辑
|
||||||
</button>
|
</button>
|
||||||
@@ -718,8 +829,8 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<label class="form-label">截止日期</label>
|
<label class="form-label">截止时间</label>
|
||||||
<input type="date" id="editDueDate" class="form-control">
|
<input type="datetime-local" id="editDueDate" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -881,6 +992,100 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 提醒详情模态框 -->
|
||||||
|
<div class="modal fade" id="remindersModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning text-dark">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-bell-fill"></i> 待办提醒</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="remindersContent">
|
||||||
|
<!-- 动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 转换为待办模态框 -->
|
||||||
|
<div class="modal fade" id="convertModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-arrow-repeat"></i> 转为待办</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="convertItemId">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">转换方式</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="convertMode" id="modeConvert" value="convert">
|
||||||
|
<label class="form-check-label" for="modeConvert">
|
||||||
|
<strong>直接转换</strong> - 原收藏变为待办,数据合并
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="convertMode" id="modeCopy" value="copy" checked>
|
||||||
|
<label class="form-check-label" for="modeCopy">
|
||||||
|
<strong>复制创建</strong> - 保留原收藏,新建待办任务
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">待办标题</label>
|
||||||
|
<input type="text" id="convertTitle" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">状态</label>
|
||||||
|
<select id="convertStatus" class="form-select">
|
||||||
|
<option value="pending">⏳ 待处理</option>
|
||||||
|
<option value="in_progress">🔄 进行中</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">优先级</label>
|
||||||
|
<select id="convertPriority" class="form-select">
|
||||||
|
<option value="low">🟢 低</option>
|
||||||
|
<option value="medium" selected>🟡 中</option>
|
||||||
|
<option value="high">🟠 高</option>
|
||||||
|
<option value="urgent">🔴 紧急</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">截止时间</label>
|
||||||
|
<input type="datetime-local" id="convertDueDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">内容预览(转换后会合并到待办备注)</label>
|
||||||
|
<div id="convertPreview" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;">
|
||||||
|
<!-- 动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="executeConvert()">
|
||||||
|
<i class="bi bi-check"></i> 确认转换
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
@@ -891,6 +1096,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await loadStats(); // 先加载统计,确保总数可用
|
await loadStats(); // 先加载统计,确保总数可用
|
||||||
loadItems();
|
loadItems();
|
||||||
loadTags();
|
loadTags();
|
||||||
|
loadReminders(); // 加载提醒
|
||||||
|
|
||||||
|
// 定时刷新提醒(每5分钟)
|
||||||
|
setInterval(loadReminders, 5 * 60 * 1000);
|
||||||
|
|
||||||
// 标签输入自动提示
|
// 标签输入自动提示
|
||||||
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
|
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
|
||||||
@@ -899,15 +1108,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// 标签搜索实时过滤
|
// 标签搜索实时过滤
|
||||||
document.getElementById('tagSearch')?.addEventListener('input', debounce(loadTagManagerList, 300));
|
document.getElementById('tagSearch')?.addEventListener('input', debounce(loadTagManagerList, 300));
|
||||||
|
|
||||||
// 类型切换时显示/隐藏字段
|
|
||||||
document.getElementById('addType').addEventListener('change', (e) => {
|
|
||||||
const type = e.target.value;
|
|
||||||
document.getElementById('contentGroup').style.display = type === 'text' ? 'block' : 'none';
|
|
||||||
document.getElementById('urlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
|
|
||||||
document.getElementById('sourceGroup').style.display = type === 'column' ? 'block' : 'none';
|
|
||||||
document.getElementById('todoFields').style.display = type === 'todo' ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 编辑时类型切换
|
// 编辑时类型切换
|
||||||
document.getElementById('editType').addEventListener('change', (e) => {
|
document.getElementById('editType').addEventListener('change', (e) => {
|
||||||
updateEditFieldsByType(e.target.value);
|
updateEditFieldsByType(e.target.value);
|
||||||
@@ -976,19 +1176,22 @@ function renderItems(items) {
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div style="flex: 1; min-width: 0;">
|
<div style="flex: 1; min-width: 0;">
|
||||||
<h6 class="card-title text-truncate mb-1">
|
<h6 class="card-title text-truncate mb-1 ${item.type === 'todo' && item.status === 'completed' ? 'text-muted' : ''}">
|
||||||
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
|
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
|
||||||
|
${item.type === 'todo' && item.status === 'completed' ? ' ✓' : ''}
|
||||||
</h6>
|
</h6>
|
||||||
<p class="card-text text-muted small mb-0 text-truncate" style="font-size:12px;">
|
<p class="card-text text-muted small mb-0 text-truncate" style="font-size:12px;">
|
||||||
${item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ''}
|
${item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ''}
|
||||||
${item.type === 'todo' ? `${getStatusLabelShort(item.status)} ${getPriorityLabelShort(item.priority)} ${item.due_date ? '📅' + item.due_date : ''}` : ''}
|
${item.type === 'todo' ? `${getStatusLabelShort(item.status)} ${getPriorityLabelShort(item.priority)} ${item.due_date ? '📅' + formatDueDate(item.due_date) : ''}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();">
|
<div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();">
|
||||||
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')}
|
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')}
|
||||||
|
${item.type !== 'todo' ? `<button class="btn btn-sm btn-outline-secondary py-0 px-1" onclick="showConvertModal(${item.id})" title="转为待办"><i class="bi bi-arrow-repeat" style="font-size:11px;"></i></button>` : ''}
|
||||||
<button class="btn btn-sm btn-outline-info py-0 px-1" onclick="showSendEmailModal(${item.id})" title="发送邮件"><i class="bi bi-envelope" style="font-size:11px;"></i></button>
|
<button class="btn btn-sm btn-outline-info py-0 px-1" onclick="showSendEmailModal(${item.id})" title="发送邮件"><i class="bi bi-envelope" style="font-size:11px;"></i></button>
|
||||||
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button>
|
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button>
|
||||||
${item.type === 'todo' && item.status !== 'completed' ? `<button class="btn btn-sm btn-outline-success py-0 px-1" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg" style="font-size:11px;"></i></button>` : ''}
|
${item.type === 'todo' && item.status !== 'completed' ? `<button class="btn btn-sm btn-outline-success py-0 px-1" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg" style="font-size:11px;"></i></button>` : ''}
|
||||||
|
${item.type === 'todo' && item.status === 'completed' ? `<button class="btn btn-sm btn-outline-warning py-0 px-1" onclick="reopenItem(${item.id})" title="重新打开"><i class="bi bi-arrow-counterclockwise" style="font-size:11px;"></i></button>` : ''}
|
||||||
<button class="btn btn-sm btn-outline-danger py-0 px-1" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash" style="font-size:11px;"></i></button>
|
<button class="btn btn-sm btn-outline-danger py-0 px-1" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash" style="font-size:11px;"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1062,6 +1265,37 @@ async function refreshData() {
|
|||||||
loadItems(currentPage);
|
loadItems(currentPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 添加功能 ============
|
||||||
|
|
||||||
|
// 快捷添加按钮
|
||||||
|
function showAddModal(type) {
|
||||||
|
// 设置类型
|
||||||
|
document.getElementById('addType').value = type;
|
||||||
|
|
||||||
|
// 设置弹窗标题和图标
|
||||||
|
const typeInfo = {
|
||||||
|
text: { icon: '📝', title: '添加文本' },
|
||||||
|
link: { icon: '🔗', title: '添加链接' },
|
||||||
|
column: { icon: '📰', title: '添加专栏' },
|
||||||
|
todo: { icon: '✅', title: '添加待办' }
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('addModalIcon').textContent = typeInfo[type].icon;
|
||||||
|
document.getElementById('addModalTitle').textContent = typeInfo[type].title;
|
||||||
|
|
||||||
|
// 显示/隐藏对应字段
|
||||||
|
document.getElementById('contentGroup').style.display = type === 'text' ? 'block' : 'none';
|
||||||
|
document.getElementById('urlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
|
||||||
|
document.getElementById('sourceGroup').style.display = type === 'column' ? 'block' : 'none';
|
||||||
|
document.getElementById('todoFields').style.display = type === 'todo' ? 'block' : 'none';
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
document.getElementById('addForm').reset();
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
new bootstrap.Modal(document.getElementById('addModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
// 添加条目
|
// 添加条目
|
||||||
async function addItem() {
|
async function addItem() {
|
||||||
const type = document.getElementById('addType').value;
|
const type = document.getElementById('addType').value;
|
||||||
@@ -1097,6 +1331,94 @@ async function completeItem(id) {
|
|||||||
refreshData();
|
refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新打开待办
|
||||||
|
async function reopenItem(id) {
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}/reopen`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status: 'pending' })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
refreshData();
|
||||||
|
loadReminders(); // 刷新提醒
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 转换为待办 ============
|
||||||
|
|
||||||
|
let convertItemData = null;
|
||||||
|
|
||||||
|
async function showConvertModal(itemId) {
|
||||||
|
const res = await fetch(`${API_BASE}/items/${itemId}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.success) return;
|
||||||
|
|
||||||
|
convertItemData = data.data;
|
||||||
|
document.getElementById('convertItemId').value = itemId;
|
||||||
|
|
||||||
|
// 设置默认标题
|
||||||
|
document.getElementById('convertTitle').value = convertItemData.title || '';
|
||||||
|
|
||||||
|
// 重置选项(默认复制创建)
|
||||||
|
document.getElementById('modeCopy').checked = true;
|
||||||
|
document.getElementById('convertStatus').value = 'pending';
|
||||||
|
document.getElementById('convertPriority').value = 'medium';
|
||||||
|
document.getElementById('convertDueDate').value = '';
|
||||||
|
|
||||||
|
// 显示内容预览
|
||||||
|
let preview = '';
|
||||||
|
if (convertItemData.type === 'text') {
|
||||||
|
preview = convertItemData.content || '';
|
||||||
|
} else if (convertItemData.type === 'link') {
|
||||||
|
preview = `链接: ${convertItemData.url}`;
|
||||||
|
if (convertItemData.content) preview += `\n${convertItemData.content}`;
|
||||||
|
} else if (convertItemData.type === 'column') {
|
||||||
|
preview = `专栏: ${convertItemData.url}`;
|
||||||
|
if (convertItemData.source) preview += `\n来源: ${convertItemData.source}`;
|
||||||
|
}
|
||||||
|
if (convertItemData.note) preview += `\n\n备注: ${convertItemData.note}`;
|
||||||
|
|
||||||
|
document.getElementById('convertPreview').textContent = preview || '(无内容)';
|
||||||
|
|
||||||
|
new bootstrap.Modal(document.getElementById('convertModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeConvert() {
|
||||||
|
const itemId = document.getElementById('convertItemId').value;
|
||||||
|
const mode = document.querySelector('input[name="convertMode"]:checked').value;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
mode: mode,
|
||||||
|
title: document.getElementById('convertTitle').value,
|
||||||
|
status: document.getElementById('convertStatus').value,
|
||||||
|
priority: document.getElementById('convertPriority').value,
|
||||||
|
due_date: document.getElementById('convertDueDate').value || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/items/${itemId}/convert`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('convertModal')).hide();
|
||||||
|
|
||||||
|
if (mode === 'copy') {
|
||||||
|
alert(`已创建新待办,原收藏保留\n新待办ID: ${result.data.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshData();
|
||||||
|
loadReminders();
|
||||||
|
} else {
|
||||||
|
alert(result.error || '转换失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除条目
|
// 删除条目
|
||||||
async function deleteItem(id) {
|
async function deleteItem(id) {
|
||||||
if (!confirm('确认删除?')) return;
|
if (!confirm('确认删除?')) return;
|
||||||
@@ -1119,6 +1441,10 @@ async function showDetail(id) {
|
|||||||
document.getElementById('detailTypeIcon').textContent = getTypeIcon(item.type);
|
document.getElementById('detailTypeIcon').textContent = getTypeIcon(item.type);
|
||||||
document.getElementById('detailTitle').textContent = item.title || '(无标题)';
|
document.getElementById('detailTitle').textContent = item.title || '(无标题)';
|
||||||
|
|
||||||
|
// 非待办类型显示转换按钮
|
||||||
|
const convertBtn = document.getElementById('detailConvertBtn');
|
||||||
|
convertBtn.style.display = item.type !== 'todo' ? 'inline-block' : 'none';
|
||||||
|
|
||||||
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
|
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
|
||||||
|
|
||||||
if (item.url) {
|
if (item.url) {
|
||||||
@@ -1137,7 +1463,7 @@ async function showDetail(id) {
|
|||||||
html += `<div class="mb-3"><strong>状态:</strong> ${getStatusLabel(item.status)}</div>`;
|
html += `<div class="mb-3"><strong>状态:</strong> ${getStatusLabel(item.status)}</div>`;
|
||||||
html += `<div class="mb-3"><strong>优先级:</strong> ${getPriorityLabel(item.priority)}</div>`;
|
html += `<div class="mb-3"><strong>优先级:</strong> ${getPriorityLabel(item.priority)}</div>`;
|
||||||
if (item.due_date) {
|
if (item.due_date) {
|
||||||
html += `<div class="mb-3"><strong>截止日期:</strong> ${item.due_date}</div>`;
|
html += `<div class="mb-3"><strong>截止时间:</strong> ${formatDueDateFull(item.due_date)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,6 +1496,12 @@ async function showDetail(id) {
|
|||||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从详情页打开转换
|
||||||
|
function showConvertModalFromDetail() {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||||
|
setTimeout(() => showConvertModal(currentDetailId), 300);
|
||||||
|
}
|
||||||
|
|
||||||
// 从详情页打开编辑
|
// 从详情页打开编辑
|
||||||
function openEditModalFromDetail() {
|
function openEditModalFromDetail() {
|
||||||
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||||
@@ -1203,7 +1535,21 @@ async function openEditModal(id) {
|
|||||||
if (type === 'todo') {
|
if (type === 'todo') {
|
||||||
document.getElementById('editStatus').value = item.status;
|
document.getElementById('editStatus').value = item.status;
|
||||||
document.getElementById('editPriority').value = item.priority;
|
document.getElementById('editPriority').value = item.priority;
|
||||||
document.getElementById('editDueDate').value = item.due_date || '';
|
// 处理 datetime-local 格式
|
||||||
|
if (item.due_date) {
|
||||||
|
// 尝试解析日期时间格式
|
||||||
|
let dueDate = item.due_date;
|
||||||
|
if (dueDate.includes('T')) {
|
||||||
|
// ISO 格式,截取到分钟
|
||||||
|
dueDate = dueDate.substring(0, 16);
|
||||||
|
} else if (dueDate.length === 10) {
|
||||||
|
// 只有日期,添加默认时间 00:00
|
||||||
|
dueDate = dueDate + 'T00:00';
|
||||||
|
}
|
||||||
|
document.getElementById('editDueDate').value = dueDate;
|
||||||
|
} else {
|
||||||
|
document.getElementById('editDueDate').value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new bootstrap.Modal(document.getElementById('editModal')).show();
|
new bootstrap.Modal(document.getElementById('editModal')).show();
|
||||||
@@ -1286,6 +1632,64 @@ function formatDate(dateStr) {
|
|||||||
return new Date(dateStr).toLocaleString('zh-CN');
|
return new Date(dateStr).toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化截止时间(友好显示)
|
||||||
|
function formatDueDate(dueDate) {
|
||||||
|
if (!dueDate) return '';
|
||||||
|
|
||||||
|
let date;
|
||||||
|
if (dueDate.includes('T')) {
|
||||||
|
date = new Date(dueDate);
|
||||||
|
} else if (dueDate.length === 10) {
|
||||||
|
// 只有日期
|
||||||
|
date = new Date(dueDate + 'T00:00:00');
|
||||||
|
} else {
|
||||||
|
date = new Date(dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化为友好格式
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
const targetDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
const daysDiff = Math.floor((targetDay - today) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
let datePart;
|
||||||
|
if (daysDiff === 0) {
|
||||||
|
datePart = '今天';
|
||||||
|
} else if (daysDiff === 1) {
|
||||||
|
datePart = '明天';
|
||||||
|
} else if (daysDiff === -1) {
|
||||||
|
datePart = '昨天';
|
||||||
|
} else {
|
||||||
|
datePart = date.toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timePart = date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
|
||||||
|
|
||||||
|
return `${datePart} ${timePart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化截止时间(完整显示)
|
||||||
|
function formatDueDateFull(dueDate) {
|
||||||
|
if (!dueDate) return '';
|
||||||
|
|
||||||
|
let date;
|
||||||
|
if (dueDate.includes('T')) {
|
||||||
|
date = new Date(dueDate);
|
||||||
|
} else if (dueDate.length === 10) {
|
||||||
|
date = new Date(dueDate + 'T00:00:00');
|
||||||
|
} else {
|
||||||
|
date = new Date(dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
@@ -1785,6 +2189,165 @@ function debounce(fn, delay) {
|
|||||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 提醒功能 ============
|
||||||
|
|
||||||
|
let reminderData = null;
|
||||||
|
let reminderDismissed = false;
|
||||||
|
|
||||||
|
async function loadReminders() {
|
||||||
|
const res = await fetch(`${API_BASE}/reminders`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
reminderData = data.data;
|
||||||
|
updateReminderBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReminderBar() {
|
||||||
|
if (!reminderData || reminderDismissed) return;
|
||||||
|
|
||||||
|
const total = reminderData.total;
|
||||||
|
if (total === 0) {
|
||||||
|
document.getElementById('reminderBar').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提醒栏
|
||||||
|
document.getElementById('reminderBar').style.display = 'block';
|
||||||
|
|
||||||
|
// 构建提醒文本
|
||||||
|
let text = '';
|
||||||
|
if (reminderData.overdue.length > 0) {
|
||||||
|
text += `<span class="text-danger fw-bold">${reminderData.overdue.length}个已过期</span> `;
|
||||||
|
}
|
||||||
|
if (reminderData.due_today.length > 0) {
|
||||||
|
text += `<span class="text-warning fw-bold">${reminderData.due_today.length}个今天到期</span> `;
|
||||||
|
}
|
||||||
|
if (reminderData.due_soon.length > 0) {
|
||||||
|
text += `<span>${reminderData.due_soon.length}个即将到期</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('reminderText').innerHTML = text;
|
||||||
|
|
||||||
|
// 更新提醒角标样式
|
||||||
|
const bar = document.getElementById('reminderBar');
|
||||||
|
if (reminderData.overdue.length > 0) {
|
||||||
|
bar.className = 'alert alert-danger alert-dismissible fade show mb-3';
|
||||||
|
} else if (reminderData.due_today.length > 0) {
|
||||||
|
bar.className = 'alert alert-warning alert-dismissible fade show mb-3';
|
||||||
|
} else {
|
||||||
|
bar.className = 'alert alert-info alert-dismissible fade show mb-3';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissReminderBar() {
|
||||||
|
reminderDismissed = true;
|
||||||
|
document.getElementById('reminderBar').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRemindersModal() {
|
||||||
|
if (!reminderData) return;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// 已过期
|
||||||
|
if (reminderData.overdue.length > 0) {
|
||||||
|
html += `<div class="mb-3"><h6 class="text-danger"><i class="bi bi-exclamation-circle"></i> 已过期 (${reminderData.overdue.length})</h6>`;
|
||||||
|
html += `<div class="list-group">`;
|
||||||
|
reminderData.overdue.forEach(item => {
|
||||||
|
html += `<div class="list-group-item list-group-item-danger d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>${item.title || '(无标题)'}</strong>
|
||||||
|
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)} | 已过期 ${item.days_overdue} 天</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="openEditModal(${item.id}); bootstrap.Modal.getInstance(document.getElementById('remindersModal')).hide();" title="编辑">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今天到期
|
||||||
|
if (reminderData.due_today.length > 0) {
|
||||||
|
html += `<div class="mb-3"><h6 class="text-warning"><i class="bi bi-clock-fill"></i> 今天到期 (${reminderData.due_today.length})</h6>`;
|
||||||
|
html += `<div class="list-group">`;
|
||||||
|
reminderData.due_today.forEach(item => {
|
||||||
|
html += `<div class="list-group-item list-group-item-warning d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>${item.title || '(无标题)'}</strong>
|
||||||
|
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="openEditModal(${item.id}); bootstrap.Modal.getInstance(document.getElementById('remindersModal')).hide();" title="编辑">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 即将到期
|
||||||
|
if (reminderData.due_soon.length > 0) {
|
||||||
|
html += `<div class="mb-3"><h6 class="text-info"><i class="bi bi-hourglass-split"></i> 即将到期 (${reminderData.due_soon.length})</h6>`;
|
||||||
|
html += `<div class="list-group">`;
|
||||||
|
reminderData.due_soon.forEach(item => {
|
||||||
|
html += `<div class="list-group-item list-group-item-info d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>${item.title || '(无标题)'}</strong>
|
||||||
|
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="openEditModal(${item.id}); bootstrap.Modal.getInstance(document.getElementById('remindersModal')).hide();" title="编辑">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
html = '<div class="text-center text-muted py-3">暂无待办提醒</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('remindersContent').innerHTML = html;
|
||||||
|
new bootstrap.Modal(document.getElementById('remindersModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeReminder(id) {
|
||||||
|
await fetch(`${API_BASE}/items/${id}/done`, { method: 'POST' });
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await loadReminders();
|
||||||
|
refreshData();
|
||||||
|
|
||||||
|
// 如果弹窗打开,更新显示
|
||||||
|
const modalEl = document.getElementById('remindersModal');
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
if (modal) {
|
||||||
|
// 检查是否还有提醒
|
||||||
|
if (reminderData && reminderData.total > 0) {
|
||||||
|
showRemindersModal();
|
||||||
|
} else {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -434,6 +434,74 @@ class Database:
|
|||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
# ============ 提醒相关 ============
|
||||||
|
|
||||||
|
def get_reminders(self) -> Dict[str, Any]:
|
||||||
|
"""获取提醒信息:即将到期和已过期的待办"""
|
||||||
|
self._ensure_init()
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
reminders = {
|
||||||
|
'overdue': [], # 已过期
|
||||||
|
'due_today': [], # 今天到期
|
||||||
|
'due_soon': [] # 24小时内到期(不含今天)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查询未完成的待办(有截止日期的)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM items
|
||||||
|
WHERE type = 'todo'
|
||||||
|
AND status != 'completed'
|
||||||
|
AND due_date IS NOT NULL
|
||||||
|
AND due_date != ''
|
||||||
|
ORDER BY due_date ASC
|
||||||
|
""")
|
||||||
|
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
item = dict(row)
|
||||||
|
item['tags'] = self._get_item_tags(conn, item['id'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
due_date_str = item['due_date']
|
||||||
|
# 支持多种日期格式
|
||||||
|
if 'T' in due_date_str:
|
||||||
|
# ISO 格式:2026-04-16T14:30
|
||||||
|
due_date = datetime.strptime(due_date_str[:16], '%Y-%m-%dT%H:%M')
|
||||||
|
elif len(due_date_str) == 10:
|
||||||
|
# 只有日期:2026-04-16,视为当天 23:59:59
|
||||||
|
due_date = datetime.strptime(due_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
|
||||||
|
else:
|
||||||
|
# 其他格式,尝试解析
|
||||||
|
due_date = datetime.strptime(due_date_str.split('.')[0], '%Y-%m-%dT%H:%M:%S')
|
||||||
|
|
||||||
|
# 计算距离到期的时间
|
||||||
|
time_left = due_date - now
|
||||||
|
|
||||||
|
if time_left.total_seconds() < 0:
|
||||||
|
# 已过期
|
||||||
|
days_overdue = abs(int(time_left.total_seconds() / 86400))
|
||||||
|
item['days_overdue'] = days_overdue
|
||||||
|
reminders['overdue'].append(item)
|
||||||
|
elif time_left.total_seconds() < 86400: # 24小时内
|
||||||
|
# 判断是今天还是明天
|
||||||
|
if due_date.date() == now.date():
|
||||||
|
reminders['due_today'].append(item)
|
||||||
|
else:
|
||||||
|
reminders['due_soon'].append(item)
|
||||||
|
elif due_date.date() == now.date():
|
||||||
|
# 今天到期(超过24小时的情况,比如现在凌晨,截止时间是晚上)
|
||||||
|
reminders['due_today'].append(item)
|
||||||
|
except (ValueError, AttributeError) as e:
|
||||||
|
# 日期格式错误,跳过
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 统计总数
|
||||||
|
reminders['total'] = len(reminders['overdue']) + len(reminders['due_today']) + len(reminders['due_soon'])
|
||||||
|
|
||||||
|
return reminders
|
||||||
|
|
||||||
|
|
||||||
# 全局数据库实例
|
# 全局数据库实例
|
||||||
db = Database()
|
db = Database()
|
||||||
Reference in New Issue
Block a user