Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eccc11369 | |||
| d874577458 | |||
| bc59fb39db | |||
| fba2c1290d | |||
| 6490ceff08 | |||
| e13fa0e337 | |||
| 3eae1595aa | |||
| 6f0905d83a | |||
| 033b7509d7 | |||
| 8f9a8d2ad5 | |||
| ffe7adcad2 | |||
| 963dd1846b |
22
README.md
22
README.md
@@ -96,6 +96,11 @@ API端点:
|
||||
| `/api/tags` | GET | 列出标签 |
|
||||
| `/api/tags` | POST | 创建标签 |
|
||||
| `/api/tags/<id>` | DELETE | 删除标签 |
|
||||
| `/api/emails` | GET | 列出邮箱 |
|
||||
| `/api/emails` | POST | 创建邮箱 |
|
||||
| `/api/emails/<id>` | PUT | 更新邮箱 |
|
||||
| `/api/emails/<id>` | DELETE | 删除邮箱 |
|
||||
| `/api/send-email` | POST | 发送收藏到邮箱 |
|
||||
| `/api/stats` | GET | 统计信息 |
|
||||
| `/api/search` | GET | 搜索 (参数 q=关键词) |
|
||||
|
||||
@@ -136,6 +141,23 @@ xian-favor/
|
||||
|
||||
## 版本历史
|
||||
|
||||
- v1.9.2 (2026-04-14): 邮件发送历史记录
|
||||
- 详情页面显示邮件发送记录(发送邮箱、时间)
|
||||
- 支持多次发送,显示多条记录
|
||||
- 发送成功/失败状态标记
|
||||
- v1.9.0 (2026-04-14): 发送邮件功能 + 邮箱管理
|
||||
- 每个收藏卡片添加"发送邮件"按钮
|
||||
- 选择已有邮箱或输入新邮箱发送
|
||||
- 新邮箱自动保存到邮箱管理
|
||||
- 邮箱管理页面:添加、编辑、删除邮箱
|
||||
- SMTP配置支持环境变量(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS)
|
||||
- v1.8.0 (2026-04-13): AI自动添加功能,智能识别文本类型并整理数据
|
||||
- 首页新增"AI添加"按钮
|
||||
- 粘贴文本/链接/笔记等,AI自动识别类型并提取关键信息
|
||||
- 支持文本、链接、专栏、待办四种类型自动识别
|
||||
- 自动生成标签和标题
|
||||
- v1.7.0 (2026-04-13): 编辑收藏时支持更改类型,动态显示对应字段
|
||||
- v1.6.0 (2026-04-13): 标签管理添加编辑功能,支持修改标签名称
|
||||
- v1.5.1 (2026-04-13): 标签管理添加搜索功能,实时过滤标签列表
|
||||
- v1.5.0 (2026-04-13): 首页添加分页功能,每页20条记录
|
||||
- v1.4.0 (2026-04-13): 首页添加导出按钮,一键导出所有收藏为JSON文件
|
||||
|
||||
BIN
xian_favor/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
xian_favor/__pycache__/api.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/api.cpython-310.pyc
Normal file
Binary file not shown.
BIN
xian_favor/__pycache__/cli.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/cli.cpython-310.pyc
Normal file
Binary file not shown.
BIN
xian_favor/__pycache__/config.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
xian_favor/__pycache__/db.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/db.cpython-310.pyc
Normal file
Binary file not shown.
@@ -65,6 +65,9 @@ def get_item(item_id):
|
||||
item = db.get_item(item_id)
|
||||
if not item:
|
||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||
# 获取邮件发送历史
|
||||
email_logs = db.get_email_logs(item_id)
|
||||
item['email_logs'] = email_logs
|
||||
return jsonify({'success': True, 'data': item})
|
||||
|
||||
|
||||
@@ -164,6 +167,89 @@ def get_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'])
|
||||
def ai_process():
|
||||
"""AI处理文本"""
|
||||
import requests
|
||||
|
||||
data = request.get_json()
|
||||
text = data.get('text', '').strip()
|
||||
|
||||
if not text:
|
||||
return jsonify({'success': False, 'error': '请输入文本内容'}), 400
|
||||
|
||||
# 大模型配置
|
||||
llm_url = "http://192.168.2.17:19007/v1/chat/completions"
|
||||
llm_key = "xxxx"
|
||||
|
||||
prompt = f"""请分析以下文本内容,识别其类型并提取关键信息。
|
||||
|
||||
文本内容:
|
||||
{text}
|
||||
|
||||
请按以下JSON格式返回结果(只返回JSON,不要其他内容):
|
||||
{{
|
||||
"type": "text/link/column/todo",
|
||||
"title": "提取的标题(简短概括)",
|
||||
"content": "主要内容(如果是文本类型)",
|
||||
"url": "如果是链接或专栏,提取URL",
|
||||
"source": "如果是专栏,提取来源",
|
||||
"tags": ["相关标签1", "标签2"],
|
||||
"note": "补充说明或备注",
|
||||
"status": "如果是待办,默认pending",
|
||||
"priority": "如果是待办,默认medium"
|
||||
}}
|
||||
|
||||
类型判断规则:
|
||||
- link: 包含http/https链接,且不是专栏订阅地址
|
||||
- column: 专栏订阅地址或RSS链接
|
||||
- todo: 包含任务、待办、提醒等关键词
|
||||
- text: 其他文本内容"""
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
llm_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {llm_key}"
|
||||
},
|
||||
json={
|
||||
"model": "auto",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.3
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return jsonify({'success': False, 'error': f'模型调用失败: {response.status_code}'}), 500
|
||||
|
||||
result = response.json()
|
||||
content = result['choices'][0]['message']['content']
|
||||
|
||||
# 解析JSON
|
||||
import json
|
||||
import re
|
||||
|
||||
# 提取JSON部分
|
||||
json_match = re.search(r'\{.*\}', content, re.DOTALL)
|
||||
if json_match:
|
||||
parsed = json.loads(json_match.group())
|
||||
return jsonify({'success': True, 'data': parsed})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '无法解析模型返回'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/search', methods=['GET'])
|
||||
def search_items():
|
||||
"""搜索条目"""
|
||||
@@ -179,6 +265,165 @@ def search_items():
|
||||
return jsonify({'success': True, 'data': items})
|
||||
|
||||
|
||||
# ============ 邮箱管理 API ============
|
||||
|
||||
@app.route('/api/emails', methods=['GET'])
|
||||
def list_emails():
|
||||
"""列出所有邮箱"""
|
||||
emails = db.list_emails()
|
||||
return jsonify({'success': True, 'data': emails})
|
||||
|
||||
|
||||
@app.route('/api/emails', methods=['POST'])
|
||||
def create_email():
|
||||
"""创建邮箱"""
|
||||
data = request.get_json()
|
||||
email_addr = data.get('email', '').strip()
|
||||
|
||||
if not email_addr:
|
||||
return jsonify({'success': False, 'error': '邮箱地址不能为空'}), 400
|
||||
|
||||
# 验证邮箱格式
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', email_addr):
|
||||
return jsonify({'success': False, 'error': '邮箱格式不正确'}), 400
|
||||
|
||||
try:
|
||||
email_id = db.create_email(email_addr, data.get('name'))
|
||||
return jsonify({'success': True, 'data': {'id': email_id, 'email': email_addr}}), 201
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/emails/<int:email_id>', methods=['PUT'])
|
||||
def update_email(email_id):
|
||||
"""更新邮箱"""
|
||||
data = request.get_json()
|
||||
|
||||
try:
|
||||
if db.update_email(email_id, email=data.get('email'), name=data.get('name')):
|
||||
email = db.get_email(email_id)
|
||||
return jsonify({'success': True, 'data': email})
|
||||
return jsonify({'success': False, 'error': '邮箱不存在或地址已存在'}), 404
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/emails/<int:email_id>', methods=['DELETE'])
|
||||
def delete_email(email_id):
|
||||
"""删除邮箱"""
|
||||
if db.delete_email(email_id):
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'success': False, 'error': '邮箱不存在'}), 404
|
||||
|
||||
|
||||
@app.route('/api/send-email', methods=['POST'])
|
||||
def send_email():
|
||||
"""发送收藏内容到邮箱"""
|
||||
data = request.get_json()
|
||||
item_id = data.get('item_id')
|
||||
email_addr = data.get('email', '').strip()
|
||||
|
||||
if not item_id or not email_addr:
|
||||
return jsonify({'success': False, 'error': '缺少参数'}), 400
|
||||
|
||||
# 获取收藏内容
|
||||
item = db.get_item(item_id)
|
||||
if not item:
|
||||
return jsonify({'success': False, 'error': '收藏不存在'}), 404
|
||||
|
||||
# 如果是新邮箱,自动保存
|
||||
import re
|
||||
if re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', email_addr):
|
||||
db.create_email(email_addr)
|
||||
|
||||
# 构建邮件内容
|
||||
type_labels = {'text': '文本笔记', 'link': '链接收藏', 'column': '专栏订阅', 'todo': '待办事项'}
|
||||
subject = f"【Xian Favor】{item['title'] or type_labels.get(item['type'], '收藏')}"
|
||||
|
||||
body_lines = [
|
||||
f"类型: {type_labels.get(item['type'], item['type'])}",
|
||||
f"标题: {item['title'] or '(无标题)'}",
|
||||
""
|
||||
]
|
||||
|
||||
if item['url']:
|
||||
body_lines.append(f"链接: {item['url']}")
|
||||
body_lines.append("")
|
||||
|
||||
if item['content']:
|
||||
body_lines.append("内容:")
|
||||
body_lines.append(item['content'])
|
||||
body_lines.append("")
|
||||
|
||||
if item['source']:
|
||||
body_lines.append(f"来源: {item['source']}")
|
||||
body_lines.append("")
|
||||
|
||||
if item['type'] == 'todo':
|
||||
status_labels = {'pending': '待处理', 'in_progress': '进行中', 'completed': '已完成'}
|
||||
priority_labels = {'low': '低', 'medium': '中', 'high': '高', 'urgent': '紧急'}
|
||||
body_lines.append(f"状态: {status_labels.get(item['status'], item['status'])}")
|
||||
body_lines.append(f"优先级: {priority_labels.get(item['priority'], item['priority'])}")
|
||||
if item['due_date']:
|
||||
body_lines.append(f"截止日期: {item['due_date']}")
|
||||
body_lines.append("")
|
||||
|
||||
if item['tags']:
|
||||
body_lines.append(f"标签: {', '.join(item['tags'])}")
|
||||
body_lines.append("")
|
||||
|
||||
if item['note']:
|
||||
body_lines.append("详情/备注:")
|
||||
body_lines.append(item['note'])
|
||||
body_lines.append("")
|
||||
|
||||
body_lines.append(f"创建时间: {item['created_at']}")
|
||||
body_lines.append("---")
|
||||
body_lines.append("来自 Xian Favor 收藏系统")
|
||||
|
||||
body = "\n".join(body_lines)
|
||||
|
||||
# 调用邮件发送
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
|
||||
# SMTP配置:端口587无SSL
|
||||
smtp_host = 'mail.tphai.com'
|
||||
smtp_port = 587
|
||||
smtp_user = 'favor@tphai.com'
|
||||
smtp_pass = 'favor@!'
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = smtp_user
|
||||
msg['To'] = email_addr
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S +0800')
|
||||
msg['Reply-To'] = email_addr
|
||||
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 直接连接,无SSL
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.ehlo()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.sendmail(smtp_user, email_addr, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
# 记录发送日志
|
||||
db.log_email_send(item_id, email_addr, success=True)
|
||||
|
||||
return jsonify({'success': True, 'message': f'已发送到 {email_addr}'})
|
||||
|
||||
except Exception as e:
|
||||
# 记录失败日志
|
||||
db.log_email_send(item_id, email_addr, success=False)
|
||||
return jsonify({'success': False, 'error': f'发送失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ============ Web 页面 ============
|
||||
|
||||
@app.route('/')
|
||||
@@ -246,11 +491,22 @@ INDEX_TEMPLATE = '''
|
||||
<a href="#" data-filter="completed"><i class="bi bi-check-circle"></i> 已完成</a>
|
||||
<hr class="border-secondary">
|
||||
<a href="#" onclick="showTagManager(); return false;"><i class="bi bi-tags"></i> 标签管理</a>
|
||||
<a href="#" onclick="showEmailManager(); return false;"><i class="bi bi-envelope"></i> 邮箱管理</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<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 gap-2">
|
||||
@@ -263,6 +519,9 @@ INDEX_TEMPLATE = '''
|
||||
<option value="todo">待办</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
||||
<i class="bi bi-robot"></i> AI添加
|
||||
</button>
|
||||
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
|
||||
<i class="bi bi-download"></i> 导出
|
||||
</button>
|
||||
@@ -432,7 +691,12 @@ INDEX_TEMPLATE = '''
|
||||
<input type="hidden" id="editId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">类型</label>
|
||||
<input type="text" id="editType" class="form-control" readonly>
|
||||
<select id="editType" 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">
|
||||
<label class="form-label">标题</label>
|
||||
@@ -528,6 +792,132 @@ INDEX_TEMPLATE = '''
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邮箱管理模态框 -->
|
||||
<div class="modal fade" id="emailManagerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-envelope"></i> 邮箱管理</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<input type="text" id="newEmailAddr" class="form-control" placeholder="邮箱地址">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<input type="text" id="newEmailName" class="form-control" placeholder="备注名称(可选)">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button class="btn btn-primary w-100" onclick="createEmail()"><i class="bi bi-plus"></i> 添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailListContainer">
|
||||
<!-- 动态填充 -->
|
||||
</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="sendEmailModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-envelope"></i> 发送到邮箱</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="sendEmailItemId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">选择已有邮箱</label>
|
||||
<select id="sendEmailSelect" class="form-select">
|
||||
<option value="">-- 选择邮箱 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">或输入新邮箱</label>
|
||||
<input type="email" id="sendEmailInput" class="form-control" placeholder="输入邮箱地址">
|
||||
</div>
|
||||
<div id="sendEmailLoading" style="display:none;">
|
||||
<div class="text-center py-2">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<div class="mt-2">正在发送...</div>
|
||||
</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="sendItemEmail()">
|
||||
<i class="bi bi-send"></i> 发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI自动添加模态框 -->
|
||||
<div class="modal fade" id="aiAddModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-robot"></i> AI自动添加</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">输入文本内容</label>
|
||||
<textarea id="aiInputText" class="form-control" rows="6" placeholder="粘贴文本、链接、笔记等,AI会自动识别并整理..."></textarea>
|
||||
</div>
|
||||
<div id="aiResult" style="display:none;">
|
||||
<hr>
|
||||
<h6>识别结果:</h6>
|
||||
<div id="aiResultContent" class="border rounded p-3 bg-light"></div>
|
||||
</div>
|
||||
<div id="aiLoading" style="display:none;">
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<div class="mt-2">AI正在分析...</div>
|
||||
</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" id="aiProcessBtn" onclick="processAIInput()">
|
||||
<i class="bi bi-magic"></i> 分析并添加
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="aiConfirmBtn" style="display:none;" onclick="confirmAIAdd()">
|
||||
<i class="bi bi-check"></i> 确认添加
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const API_BASE = '/api';
|
||||
@@ -538,6 +928,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadStats(); // 先加载统计,确保总数可用
|
||||
loadItems();
|
||||
loadTags();
|
||||
loadReminders(); // 加载提醒
|
||||
|
||||
// 定时刷新提醒(每5分钟)
|
||||
setInterval(loadReminders, 5 * 60 * 1000);
|
||||
|
||||
// 标签输入自动提示
|
||||
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
|
||||
@@ -555,6 +949,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
document.getElementById('todoFields').style.display = type === 'todo' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// 编辑时类型切换
|
||||
document.getElementById('editType').addEventListener('change', (e) => {
|
||||
updateEditFieldsByType(e.target.value);
|
||||
});
|
||||
|
||||
// 搜索
|
||||
document.getElementById('searchInput').addEventListener('input', debounce(loadItems, 300));
|
||||
|
||||
@@ -628,6 +1027,7 @@ function renderItems(items) {
|
||||
</div>
|
||||
<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('')}
|
||||
<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>
|
||||
${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>` : ''}
|
||||
<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>
|
||||
@@ -792,6 +1192,20 @@ async function showDetail(id) {
|
||||
|
||||
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
|
||||
|
||||
// 邮件发送历史
|
||||
if (item.email_logs && item.email_logs.length > 0) {
|
||||
html += `<hr><div class="mb-3"><strong>📧 邮件发送记录:</strong></div>`;
|
||||
html += `<div class="border rounded p-2 bg-light">`;
|
||||
item.email_logs.forEach(log => {
|
||||
const statusIcon = log.success ? '✅' : '❌';
|
||||
html += `<div class="d-flex justify-content-between align-items-center py-1 border-bottom">
|
||||
<span>${statusIcon} 发送到: <strong>${log.email}</strong></span>
|
||||
<span class="text-muted small">${formatDate(log.sent_at)}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
document.getElementById('detailContent').innerHTML = html;
|
||||
|
||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||
@@ -815,14 +1229,11 @@ async function openEditModal(id) {
|
||||
const type = item.type;
|
||||
|
||||
document.getElementById('editId').value = id;
|
||||
document.getElementById('editType').value = getTypeLabel(type);
|
||||
document.getElementById('editType').value = type;
|
||||
document.getElementById('editTitle').value = item.title || '';
|
||||
|
||||
// 根据类型显示/隐藏字段
|
||||
document.getElementById('editContentGroup').style.display = type === 'text' ? 'block' : 'none';
|
||||
document.getElementById('editUrlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
|
||||
document.getElementById('editSourceGroup').style.display = type === 'column' ? 'block' : 'none';
|
||||
document.getElementById('editTodoFields').style.display = type === 'todo' ? 'block' : 'none';
|
||||
updateEditFieldsByType(type);
|
||||
|
||||
document.getElementById('editContent').value = item.content || '';
|
||||
document.getElementById('editUrl').value = item.url || '';
|
||||
@@ -839,16 +1250,21 @@ async function openEditModal(id) {
|
||||
new bootstrap.Modal(document.getElementById('editModal')).show();
|
||||
}
|
||||
|
||||
// 根据类型更新编辑表单字段显示
|
||||
function updateEditFieldsByType(type) {
|
||||
document.getElementById('editContentGroup').style.display = type === 'text' ? 'block' : 'none';
|
||||
document.getElementById('editUrlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
|
||||
document.getElementById('editSourceGroup').style.display = type === 'column' ? 'block' : 'none';
|
||||
document.getElementById('editTodoFields').style.display = type === 'todo' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
async function saveEdit() {
|
||||
const id = document.getElementById('editId').value;
|
||||
|
||||
// 获取当前条目的类型
|
||||
const detailRes = await fetch(`${API_BASE}/items/${currentDetailId}`);
|
||||
const detailData = await detailRes.json();
|
||||
const type = detailData.data.type;
|
||||
const type = document.getElementById('editType').value; // 从下拉框获取新类型
|
||||
|
||||
const data = {
|
||||
type: type, // 包含类型变更
|
||||
title: document.getElementById('editTitle').value,
|
||||
content: type === 'text' ? document.getElementById('editContent').value : null,
|
||||
url: ['link', 'column'].includes(type) ? document.getElementById('editUrl').value : null,
|
||||
@@ -1120,6 +1536,289 @@ async function exportData() {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// AI自动添加
|
||||
let aiParsedData = null;
|
||||
|
||||
function showAIAddModal() {
|
||||
document.getElementById('aiInputText').value = '';
|
||||
document.getElementById('aiResult').style.display = 'none';
|
||||
document.getElementById('aiLoading').style.display = 'none';
|
||||
document.getElementById('aiProcessBtn').style.display = 'inline-block';
|
||||
document.getElementById('aiConfirmBtn').style.display = 'none';
|
||||
aiParsedData = null;
|
||||
new bootstrap.Modal(document.getElementById('aiAddModal')).show();
|
||||
}
|
||||
|
||||
async function processAIInput() {
|
||||
const text = document.getElementById('aiInputText').value.trim();
|
||||
if (!text) {
|
||||
alert('请输入文本内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载
|
||||
document.getElementById('aiLoading').style.display = 'block';
|
||||
document.getElementById('aiProcessBtn').disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/ai-process`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('aiLoading').style.display = 'none';
|
||||
document.getElementById('aiProcessBtn').disabled = false;
|
||||
|
||||
if (!data.success) {
|
||||
alert('AI分析失败: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
aiParsedData = data.data;
|
||||
|
||||
// 显示结果
|
||||
const typeLabels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
|
||||
let html = `<div><strong>类型:</strong> ${typeLabels[aiParsedData.type] || aiParsedData.type}</div>`;
|
||||
if (aiParsedData.title) html += `<div><strong>标题:</strong> ${aiParsedData.title}</div>`;
|
||||
if (aiParsedData.url) html += `<div><strong>URL:</strong> ${aiParsedData.url}</div>`;
|
||||
if (aiParsedData.content) html += `<div><strong>内容:</strong> ${aiParsedData.content.substring(0, 200)}${aiParsedData.content.length > 200 ? '...' : ''}</div>`;
|
||||
if (aiParsedData.tags && aiParsedData.tags.length) html += `<div><strong>标签:</strong> ${aiParsedData.tags.join(', ')}</div>`;
|
||||
if (aiParsedData.note) html += `<div><strong>备注:</strong> ${aiParsedData.note.substring(0, 100)}${aiParsedData.note.length > 100 ? '...' : ''}</div>`;
|
||||
if (aiParsedData.type === 'todo') {
|
||||
const statusLabels = { pending: '⏳ 待处理', in_progress: '🔄 进行中', completed: '✅ 已完成' };
|
||||
const priorityLabels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
|
||||
html += `<div><strong>状态:</strong> ${statusLabels[aiParsedData.status] || 'pending'}</div>`;
|
||||
html += `<div><strong>优先级:</strong> ${priorityLabels[aiParsedData.priority] || 'medium'}</div>`;
|
||||
}
|
||||
|
||||
document.getElementById('aiResultContent').innerHTML = html;
|
||||
document.getElementById('aiResult').style.display = 'block';
|
||||
document.getElementById('aiProcessBtn').style.display = 'none';
|
||||
document.getElementById('aiConfirmBtn').style.display = 'inline-block';
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('aiLoading').style.display = 'none';
|
||||
document.getElementById('aiProcessBtn').disabled = false;
|
||||
alert('请求失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmAIAdd() {
|
||||
if (!aiParsedData) return;
|
||||
|
||||
const res = await fetch(`${API_BASE}/items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(aiParsedData)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('aiAddModal')).hide();
|
||||
refreshData();
|
||||
alert('添加成功!');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert('添加失败: ' + data.error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 邮箱管理 ============
|
||||
|
||||
let allEmails = [];
|
||||
|
||||
async function loadEmails() {
|
||||
const res = await fetch(`${API_BASE}/emails`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
allEmails = data.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function showEmailManager() {
|
||||
await loadEmailManagerList();
|
||||
new bootstrap.Modal(document.getElementById('emailManagerModal')).show();
|
||||
}
|
||||
|
||||
async function loadEmailManagerList() {
|
||||
const res = await fetch(`${API_BASE}/emails`);
|
||||
const data = await res.json();
|
||||
if (!data.success) return;
|
||||
|
||||
allEmails = data.data;
|
||||
const container = document.getElementById('emailListContainer');
|
||||
|
||||
if (!data.data.length) {
|
||||
container.innerHTML = '<div class="text-center text-muted py-3">暂无邮箱</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = data.data.map(email => `
|
||||
<div class="d-flex justify-content-between align-items-center p-2 border-bottom" id="email-row-${email.id}">
|
||||
<div id="email-display-${email.id}">
|
||||
<span class="fw-bold">${email.email}</span>
|
||||
${email.name ? `<span class="text-muted ms-2">(${email.name})</span>` : ''}
|
||||
</div>
|
||||
<div id="email-edit-${email.id}" style="display:none;">
|
||||
<input type="email" class="form-control form-control-sm me-2" id="edit-email-addr-${email.id}" value="${email.email}" style="width:180px;">
|
||||
<input type="text" class="form-control form-control-sm me-2" id="edit-email-name-${email.id}" value="${email.name || ''}" style="width:120px;">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" id="email-edit-btn-${email.id}" onclick="showEditEmail(${email.id})" title="编辑">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-success" id="email-save-btn-${email.id}" style="display:none;" onclick="saveEditEmail(${email.id})" title="保存">
|
||||
<i class="bi bi-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="email-cancel-btn-${email.id}" style="display:none;" onclick="cancelEditEmail(${email.id})" title="取消">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteEmailManager(${email.id}, '${email.email}')" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function createEmail() {
|
||||
const emailAddr = document.getElementById('newEmailAddr').value.trim();
|
||||
const emailName = document.getElementById('newEmailName').value.trim();
|
||||
|
||||
if (!emailAddr) {
|
||||
alert('请输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/emails`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailAddr, name: emailName })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.getElementById('newEmailAddr').value = '';
|
||||
document.getElementById('newEmailName').value = '';
|
||||
loadEmailManagerList();
|
||||
} else {
|
||||
alert(data.error || '添加失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEmailManager(id, email) {
|
||||
if (!confirm(`确认删除邮箱 "${email}"?`)) return;
|
||||
|
||||
await fetch(`${API_BASE}/emails/${id}`, { method: 'DELETE' });
|
||||
loadEmailManagerList();
|
||||
loadEmails();
|
||||
}
|
||||
|
||||
function showEditEmail(id) {
|
||||
document.getElementById(`email-display-${id}`).style.display = 'none';
|
||||
document.getElementById(`email-edit-${id}`).style.display = 'flex';
|
||||
document.getElementById(`email-edit-btn-${id}`).style.display = 'none';
|
||||
document.getElementById(`email-save-btn-${id}`).style.display = 'inline-block';
|
||||
document.getElementById(`email-cancel-btn-${id}`).style.display = 'inline-block';
|
||||
document.getElementById(`edit-email-addr-${id}`).focus();
|
||||
}
|
||||
|
||||
function cancelEditEmail(id) {
|
||||
const email = allEmails.find(e => e.id === id);
|
||||
if (email) {
|
||||
document.getElementById(`edit-email-addr-${id}`).value = email.email;
|
||||
document.getElementById(`edit-email-name-${id}`).value = email.name || '';
|
||||
}
|
||||
document.getElementById(`email-display-${id}`).style.display = 'block';
|
||||
document.getElementById(`email-edit-${id}`).style.display = 'none';
|
||||
document.getElementById(`email-edit-btn-${id}`).style.display = 'inline-block';
|
||||
document.getElementById(`email-save-btn-${id}`).style.display = 'none';
|
||||
document.getElementById(`email-cancel-btn-${id}`).style.display = 'none';
|
||||
}
|
||||
|
||||
async function saveEditEmail(id) {
|
||||
const emailAddr = document.getElementById(`edit-email-addr-${id}`).value.trim();
|
||||
const emailName = document.getElementById(`edit-email-name-${id}`).value.trim();
|
||||
|
||||
if (!emailAddr) {
|
||||
alert('邮箱地址不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/emails/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailAddr, name: emailName })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
loadEmailManagerList();
|
||||
loadEmails();
|
||||
} else {
|
||||
alert(data.error || '更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 发送邮件 ============
|
||||
|
||||
async function showSendEmailModal(itemId) {
|
||||
document.getElementById('sendEmailItemId').value = itemId;
|
||||
document.getElementById('sendEmailInput').value = '';
|
||||
document.getElementById('sendEmailLoading').style.display = 'none';
|
||||
|
||||
// 加载已有邮箱
|
||||
await loadEmails();
|
||||
const select = document.getElementById('sendEmailSelect');
|
||||
select.innerHTML = '<option value="">-- 选择邮箱 --</option>' +
|
||||
allEmails.map(e => `<option value="${e.email}">${e.email}${e.name ? ` (${e.name})` : ''}</option>`).join('');
|
||||
|
||||
new bootstrap.Modal(document.getElementById('sendEmailModal')).show();
|
||||
}
|
||||
|
||||
async function sendItemEmail() {
|
||||
const itemId = document.getElementById('sendEmailItemId').value;
|
||||
let emailAddr = document.getElementById('sendEmailSelect').value;
|
||||
|
||||
// 如果没选择,使用输入的邮箱
|
||||
if (!emailAddr) {
|
||||
emailAddr = document.getElementById('sendEmailInput').value.trim();
|
||||
}
|
||||
|
||||
if (!emailAddr) {
|
||||
alert('请选择或输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载
|
||||
document.getElementById('sendEmailLoading').style.display = 'block';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/send-email`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_id: parseInt(itemId), email: emailAddr })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
document.getElementById('sendEmailLoading').style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('sendEmailModal')).hide();
|
||||
alert(data.message || '发送成功!');
|
||||
// 刷新邮箱列表(新邮箱自动保存)
|
||||
loadEmails();
|
||||
} else {
|
||||
alert(data.error || '发送失败');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('sendEmailLoading').style.display = 'none';
|
||||
alert('请求失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
let timer;
|
||||
return function(...args) {
|
||||
@@ -1127,6 +1826,165 @@ function debounce(fn, 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">截止: ${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">截止: ${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">截止: ${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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
159
xian_favor/db.py
159
xian_favor/db.py
@@ -80,6 +80,28 @@ class Database:
|
||||
)
|
||||
""")
|
||||
|
||||
# 邮箱表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS emails (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# 邮件发送记录表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS email_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item_id INTEGER NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
sent_at TEXT NOT NULL,
|
||||
success INTEGER DEFAULT 1,
|
||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_type ON items(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_status ON items(status)")
|
||||
@@ -302,6 +324,90 @@ class Database:
|
||||
""", (item_id,))
|
||||
return [row['name'] for row in cursor.fetchall()]
|
||||
|
||||
# ============ Email 操作 ============
|
||||
|
||||
def create_email(self, email: str, name: str = None) -> int:
|
||||
"""创建邮箱"""
|
||||
now = datetime.now().isoformat()
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("INSERT INTO emails (email, name, created_at) VALUES (?, ?, ?)",
|
||||
(email, name, now))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except sqlite3.IntegrityError:
|
||||
# 邮箱已存在,返回已有ID
|
||||
cursor.execute("SELECT id FROM emails WHERE email = ?", (email,))
|
||||
return cursor.fetchone()['id']
|
||||
|
||||
def list_emails(self) -> List[Dict[str, Any]]:
|
||||
"""列出所有邮箱"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM emails ORDER BY created_at DESC")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def get_email(self, email_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取单个邮箱"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM emails WHERE id = ?", (email_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def update_email(self, email_id: int, email: str = None, name: str = None) -> bool:
|
||||
"""更新邮箱"""
|
||||
now = datetime.now().isoformat()
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
if email:
|
||||
# 检查邮箱是否已存在(排除自己)
|
||||
cursor.execute("SELECT id FROM emails WHERE email = ? AND id != ?", (email, email_id))
|
||||
if cursor.fetchone():
|
||||
return False # 邎箱已存在
|
||||
if email and name:
|
||||
cursor.execute("UPDATE emails SET email = ?, name = ? WHERE id = ?", (email, name, email_id))
|
||||
elif email:
|
||||
cursor.execute("UPDATE emails SET email = ? WHERE id = ?", (email, email_id))
|
||||
elif name:
|
||||
cursor.execute("UPDATE emails SET name = ? WHERE id = ?", (name, email_id))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def delete_email(self, email_id: int) -> bool:
|
||||
"""删除邮箱"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM emails WHERE id = ?", (email_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
# ============ 邮件日志 操作 ============
|
||||
|
||||
def log_email_send(self, item_id: int, email: str, success: bool = True) -> int:
|
||||
"""记录邮件发送"""
|
||||
now = datetime.now().isoformat()
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO email_logs (item_id, email, sent_at, success)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (item_id, email, now, 1 if success else 0))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def get_email_logs(self, item_id: int) -> List[Dict[str, Any]]:
|
||||
"""获取收藏的邮件发送记录"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM email_logs
|
||||
WHERE item_id = ?
|
||||
ORDER BY sent_at DESC
|
||||
""", (item_id,))
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def stats(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
self._ensure_init()
|
||||
@@ -327,6 +433,59 @@ class Database:
|
||||
stats['tags'] = cursor.fetchone()['count']
|
||||
|
||||
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 = datetime.strptime(item['due_date'], '%Y-%m-%d')
|
||||
# 计算距离到期的时间
|
||||
days_left = (due_date.date() - now.date()).days
|
||||
|
||||
if days_left < 0:
|
||||
# 已过期
|
||||
item['days_overdue'] = abs(days_left)
|
||||
reminders['overdue'].append(item)
|
||||
elif days_left == 0:
|
||||
# 今天到期
|
||||
reminders['due_today'].append(item)
|
||||
elif days_left == 1:
|
||||
# 明天到期(24小时内)
|
||||
reminders['due_soon'].append(item)
|
||||
except ValueError:
|
||||
# 日期格式错误,跳过
|
||||
continue
|
||||
|
||||
# 统计总数
|
||||
reminders['total'] = len(reminders['overdue']) + len(reminders['due_today']) + len(reminders['due_soon'])
|
||||
|
||||
return reminders
|
||||
|
||||
|
||||
# 全局数据库实例
|
||||
|
||||
Reference in New Issue
Block a user