Files
xian-favor/xian_favor/api.py

1771 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""API服务"""
from flask import Flask, request, jsonify, render_template_string
from flask_cors import CORS
import os
from .db import db
from .config import API_HOST, API_PORT, ITEM_TYPES, TODO_STATUS, PRIORITY_LEVELS
app = Flask(__name__,
template_folder=os.path.join(os.path.dirname(__file__), '../web/templates'),
static_folder=os.path.join(os.path.dirname(__file__), '../web/static'))
CORS(app)
# ============ API 路由 ============
@app.route('/api/items', methods=['GET'])
def list_items():
"""列出条目"""
items = db.list_items(
type=request.args.get('type'),
status=request.args.get('status'),
tag=request.args.get('tag'),
keyword=request.args.get('keyword'),
limit=int(request.args.get('limit', 50)),
offset=int(request.args.get('offset', 0))
)
return jsonify({'success': True, 'data': items})
@app.route('/api/items', methods=['POST'])
def create_item():
"""创建条目"""
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '无数据'}), 400
item_type = data.get('type', 'text')
if item_type not in ITEM_TYPES:
return jsonify({'success': False, 'error': f'无效类型: {item_type}'}), 400
try:
item_id = db.create_item(
type=item_type,
title=data.get('title'),
content=data.get('content'),
url=data.get('url'),
source=data.get('source'),
status=data.get('status', 'pending'),
priority=data.get('priority', 'medium'),
due_date=data.get('due_date'),
note=data.get('note'),
tags=data.get('tags', [])
)
item = db.get_item(item_id)
return jsonify({'success': True, 'data': item}), 201
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/items/<int:item_id>', methods=['GET'])
def get_item(item_id):
"""获取条目"""
item = db.get_item(item_id)
if not item:
return jsonify({'success': False, 'error': '条目不存在'}), 404
return jsonify({'success': True, 'data': item})
@app.route('/api/items/<int:item_id>', methods=['PUT'])
def update_item(item_id):
"""更新条目"""
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '无数据'}), 400
try:
if db.update_item(item_id, **data):
item = db.get_item(item_id)
return jsonify({'success': True, 'data': item})
else:
return jsonify({'success': False, 'error': '条目不存在或无变化'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/items/<int:item_id>', methods=['DELETE'])
def delete_item(item_id):
"""删除条目"""
if db.delete_item(item_id):
return jsonify({'success': True})
return jsonify({'success': False, 'error': '条目不存在'}), 404
@app.route('/api/items/<int:item_id>/done', methods=['POST'])
def complete_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
db.update_item(item_id, status='completed')
item = db.get_item(item_id)
return jsonify({'success': True, 'data': item})
@app.route('/api/tags', methods=['GET'])
def list_tags():
"""列出标签"""
tags = db.list_tags()
return jsonify({'success': True, 'data': tags})
@app.route('/api/tags', methods=['POST'])
def create_tag():
"""创建标签"""
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'error': '标签名不能为空'}), 400
try:
tag_id = db.create_tag(name, data.get('color', '#3498db'))
return jsonify({'success': True, 'data': {'id': tag_id, 'name': name}}), 201
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/tags/<int:tag_id>', methods=['PUT'])
def update_tag(tag_id):
"""更新标签"""
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'error': '标签名不能为空'}), 400
try:
if db.update_tag(tag_id, name):
return jsonify({'success': True, 'data': {'id': tag_id, 'name': name}})
return jsonify({'success': False, 'error': '标签不存在或名称已存在'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/tags/<int:tag_id>', methods=['DELETE'])
def delete_tag(tag_id):
"""删除标签"""
if db.delete_tag(tag_id=tag_id):
return jsonify({'success': True})
return jsonify({'success': False, 'error': '标签不存在'}), 404
@app.route('/api/stats', methods=['GET'])
def get_stats():
"""获取统计"""
stats = db.stats()
return jsonify({'success': True, 'data': stats})
@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():
"""搜索条目"""
keyword = request.args.get('q', '')
if not keyword:
return jsonify({'success': False, 'error': '请提供搜索关键词'}), 400
items = db.list_items(
keyword=keyword,
type=request.args.get('type'),
limit=int(request.args.get('limit', 50))
)
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
# SMTP配置从环境变量或配置文件获取
smtp_host = os.environ.get('SMTP_HOST', 'smtp.exmail.qq.com')
smtp_port = int(os.environ.get('SMTP_PORT', 465))
smtp_user = os.environ.get('SMTP_USER', 'wlq@tphai.com')
smtp_pass = os.environ.get('SMTP_PASS', '')
if not smtp_pass:
return jsonify({'success': False, 'error': 'SMTP密码未配置请设置环境变量 SMTP_PASS'}), 500
msg = MIMEMultipart()
msg['From'] = smtp_user
msg['To'] = email_addr
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain', 'utf-8'))
with smtplib.SMTP_SSL(smtp_host, smtp_port) as server:
server.login(smtp_user, smtp_pass)
server.sendmail(smtp_user, email_addr, msg.as_string())
return jsonify({'success': True, 'message': f'已发送到 {email_addr}'})
except Exception as e:
return jsonify({'success': False, 'error': f'发送失败: {str(e)}'}), 500
# ============ Web 页面 ============
@app.route('/')
def index():
"""主页"""
return render_template_string(INDEX_TEMPLATE)
# ============ Web 模板 ============
INDEX_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xian Favor - 收藏系统</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⭐</text></svg>">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background: #f8f9fa; }
.sidebar { height: 100vh; background: #343a40; color: #fff; }
.sidebar a { color: #adb5bd; text-decoration: none; padding: 10px 20px; display: block; }
.sidebar a:hover, .sidebar a.active { background: #495057; color: #fff; }
.content { padding: 20px; }
.card { margin-bottom: 8px; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-body { padding: 10px 15px; }
.tag { margin-right: 5px; }
.item-card { font-size: 14px; }
.item-card h6 { font-size: 14px; margin-bottom: 4px; }
.item-card p { margin-bottom: 4px; }
.item-card .text-muted.small { font-size: 12px; }
.type-text { border-left: 4px solid #17a2b8; }
.type-link { border-left: 4px solid #28a745; }
.type-column { border-left: 4px solid #6f42c1; }
.type-todo { border-left: 4px solid #ffc107; }
.status-pending { color: #ffc107; }
.status-in_progress { color: #17a2b8; }
.status-completed { color: #28a745; text-decoration: line-through; }
.priority-low { color: #6c757d; }
.priority-medium { color: #ffc107; }
.priority-high { color: #fd7e14; }
.priority-urgent { color: #dc3545; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-2 sidebar p-0">
<div class="p-3 border-bottom border-secondary">
<h5><i class="bi bi-bookmark-star"></i> Xian Favor</h5>
</div>
<nav>
<a href="#" class="active" data-filter="all"><i class="bi bi-inbox"></i> 全部</a>
<a href="#" data-filter="text"><i class="bi bi-file-text"></i> 文本</a>
<a href="#" data-filter="link"><i class="bi bi-link-45deg"></i> 链接</a>
<a href="#" data-filter="column"><i class="bi bi-newspaper"></i> 专栏</a>
<a href="#" data-filter="todo"><i class="bi bi-check2-square"></i> 待办</a>
<hr class="border-secondary">
<a href="#" data-filter="pending"><i class="bi bi-clock"></i> 待处理</a>
<a href="#" data-filter="in_progress"><i class="bi bi-arrow-repeat"></i> 进行中</a>
<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 class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex gap-2">
<input type="text" id="searchInput" class="form-control" placeholder="搜索..." style="width: 300px;">
<select id="typeFilter" class="form-select" style="width: 120px;">
<option value="">全部类型</option>
<option value="text">文本</option>
<option value="link">链接</option>
<option value="column">专栏</option>
<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>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
<!-- 统计卡片 -->
<div class="row mb-4" id="statsCards">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h6>总条目</h6>
<h3 id="statTotal">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<h6>待处理</h6>
<h3 id="statPending">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h6>进行中</h6>
<h3 id="statProgress">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h6>已完成</h6>
<h3 id="statCompleted">0</h3>
</div>
</div>
</div>
</div>
<!-- 列表 -->
<div id="itemList"></div>
<!-- 分页 -->
<div id="pagination" class="d-flex justify-content-center mt-3"></div>
</div>
</div>
</div>
<!-- 添加模态框 -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加条目</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addForm">
<div class="mb-3">
<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">
<label class="form-label">标题</label>
<input type="text" id="addTitle" class="form-control" placeholder="可选">
</div>
<div class="mb-3" id="contentGroup">
<label class="form-label">内容</label>
<textarea id="addContent" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3" id="urlGroup" style="display:none;">
<label class="form-label">URL</label>
<input type="url" id="addUrl" class="form-control">
</div>
<div class="mb-3" id="sourceGroup" style="display:none;">
<label class="form-label">来源</label>
<input type="text" id="addSource" class="form-control">
</div>
<div class="mb-3" id="todoFields" style="display:none;">
<div class="row">
<div class="col">
<label class="form-label">状态</label>
<select id="addStatus" class="form-select">
<option value="pending">⏳ 待处理</option>
<option value="in_progress">🔄 进行中</option>
<option value="completed">✅ 已完成</option>
</select>
</div>
<div class="col">
<label class="form-label">优先级</label>
<select id="addPriority" class="form-select">
<option value="low">🟢 低</option>
<option value="medium" selected>🟡 中</option>
<option value="high">🟠 高</option>
<option value="urgent">🔴 紧急</option>
</select>
</div>
</div>
<div class="mt-3">
<label class="form-label">截止日期</label>
<input type="date" id="addDueDate" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label">标签 (逗号分隔)</label>
<input type="text" id="addTags" class="form-control" placeholder="标签1, 标签2" list="tagList">
<datalist id="tagList"></datalist>
<div id="addTagSuggestions" class="mt-1"></div>
</div>
<div class="mb-3">
<label class="form-label">详情/备注</label>
<textarea id="addNote" class="form-control" rows="5"></textarea>
</div>
</form>
</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="addItem()">添加</button>
</div>
</div>
</div>
</div>
<!-- 详情模态框 -->
<div class="modal fade" id="detailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><span id="detailTypeIcon"></span> <span id="detailTitle"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="detailContent">
<!-- 动态填充 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" onclick="openEditModalFromDetail()">
<i class="bi bi-pencil"></i> 编辑
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 编辑模态框 -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑条目</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editId">
<div class="mb-3">
<label class="form-label">类型</label>
<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>
<input type="text" id="editTitle" class="form-control">
</div>
<div class="mb-3" id="editContentGroup">
<label class="form-label">内容</label>
<textarea id="editContent" class="form-control" rows="5"></textarea>
</div>
<div class="mb-3" id="editUrlGroup" style="display:none;">
<label class="form-label">URL</label>
<input type="url" id="editUrl" class="form-control">
</div>
<div class="mb-3" id="editSourceGroup" style="display:none;">
<label class="form-label">来源</label>
<input type="text" id="editSource" class="form-control">
</div>
<div class="mb-3" id="editTodoFields" style="display:none;">
<div class="row">
<div class="col">
<label class="form-label">状态</label>
<select id="editStatus" class="form-select">
<option value="pending">⏳ 待处理</option>
<option value="in_progress">🔄 进行中</option>
<option value="completed">✅ 已完成</option>
</select>
</div>
<div class="col">
<label class="form-label">优先级</label>
<select id="editPriority" class="form-select">
<option value="low">🟢 低</option>
<option value="medium">🟡 中</option>
<option value="high">🟠 高</option>
<option value="urgent">🔴 紧急</option>
</select>
</div>
</div>
<div class="mt-3">
<label class="form-label">截止日期</label>
<input type="date" id="editDueDate" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label">标签 (逗号分隔)</label>
<input type="text" id="editTags" class="form-control" placeholder="标签1, 标签2" list="tagList">
<div id="editTagSuggestions" class="mt-1"></div>
</div>
<div class="mb-3">
<label class="form-label">详情/备注</label>
<textarea id="editNote" class="form-control" rows="5"></textarea>
</div>
</form>
</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="saveEdit()">保存</button>
</div>
</div>
</div>
</div>
<!-- 标签管理模态框 -->
<div class="modal fade" id="tagManagerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-tags"></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">
<div class="input-group">
<input type="text" id="tagSearch" class="form-control" placeholder="搜索标签...">
<button class="btn btn-outline-secondary" onclick="loadTagManagerList()"><i class="bi bi-search"></i></button>
</div>
</div>
<div class="col">
<div class="input-group">
<input type="text" id="newTagName" class="form-control" placeholder="新标签名称">
<button class="btn btn-primary" onclick="createTag()"><i class="bi bi-plus"></i> 创建</button>
</div>
</div>
</div>
<div id="tagListContainer">
<!-- 动态填充 -->
</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="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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
let currentFilter = { type: '', status: '' };
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
await loadStats(); // 先加载统计,确保总数可用
loadItems();
loadTags();
// 标签输入自动提示
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
document.getElementById('editTags').addEventListener('input', showTagSuggestionsEdit);
// 标签搜索实时过滤
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) => {
updateEditFieldsByType(e.target.value);
});
// 搜索
document.getElementById('searchInput').addEventListener('input', debounce(loadItems, 300));
// 类型过滤
document.getElementById('typeFilter').addEventListener('change', (e) => {
currentFilter.type = e.target.value;
loadItems();
});
// 侧边栏过滤
document.querySelectorAll('.sidebar a').forEach(a => {
a.addEventListener('click', (e) => {
e.preventDefault();
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
a.classList.add('active');
const filter = a.dataset.filter;
if (['text', 'link', 'column', 'todo'].includes(filter)) {
currentFilter = { type: filter, status: '' };
} else if (['pending', 'in_progress', 'completed'].includes(filter)) {
currentFilter = { type: 'todo', status: filter };
} else {
currentFilter = { type: '', status: '' };
}
loadItems();
});
});
});
// 加载列表
let currentPage = 1;
const pageSize = 20;
async function loadItems(page = 1) {
currentPage = page;
const keyword = document.getElementById('searchInput').value;
let url = `${API_BASE}/items?limit=${pageSize}&offset=${(page-1)*pageSize}`;
if (currentFilter.type) url += `&type=${currentFilter.type}`;
if (currentFilter.status) url += `&status=${currentFilter.status}`;
if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`;
const res = await fetch(url);
const data = await res.json();
if (data.success) {
renderItems(data.data);
renderPagination(data.data.length, page);
}
}
// 渲染列表
function renderItems(items) {
const container = document.getElementById('itemList');
if (!items.length) {
container.innerHTML = '<div class="text-center text-muted py-5">暂无数据</div>';
return;
}
container.innerHTML = items.map(item => `
<div class="card type-${item.type} item-card" style="cursor: pointer;" onclick="showDetail(${item.id})">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div style="flex: 1; min-width: 0;">
<h6 class="card-title text-truncate mb-1">
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
</h6>
<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.type === 'todo' ? `${getStatusLabelShort(item.status)} ${getPriorityLabelShort(item.priority)} ${item.due_date ? '📅' + item.due_date : ''}` : ''}
</p>
</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>
</div>
</div>
</div>
</div>
`).join('');
}
// 渲染分页
function renderPagination(itemCount, page) {
const container = document.getElementById('pagination');
const total = parseInt(document.getElementById('statTotal').textContent);
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '<nav><ul class="pagination">';
// 上一页
html += `<li class="page-item ${page === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadItems(${page-1}); return false;">«</a>
</li>`;
// 页码最多显示5个
const startPage = Math.max(1, page - 2);
const endPage = Math.min(totalPages, page + 2);
if (startPage > 1) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="loadItems(1); return false;">1</a></li>`;
if (startPage > 2) html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
for (let p = startPage; p <= endPage; p++) {
html += `<li class="page-item ${p === page ? 'active' : ''}">
<a class="page-link" href="#" onclick="loadItems(${p}); return false;">${p}</a>
</li>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
html += `<li class="page-item"><a class="page-link" href="#" onclick="loadItems(${totalPages}); return false;">${totalPages}</a></li>`;
}
// 下一页
html += `<li class="page-item ${page === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadItems(${page+1}); return false;">»</a>
</li>`;
html += '</ul></nav>';
container.innerHTML = html;
}
// 加载统计
async function loadStats() {
const res = await fetch(`${API_BASE}/stats`);
const data = await res.json();
if (data.success) {
document.getElementById('statTotal').textContent = data.data.total;
document.getElementById('statPending').textContent = data.data.todo_status?.pending || 0;
document.getElementById('statProgress').textContent = data.data.todo_status?.in_progress || 0;
document.getElementById('statCompleted').textContent = data.data.todo_status?.completed || 0;
}
}
// 刷新数据(统计+列表)
async function refreshData() {
await loadStats();
loadItems(currentPage);
}
// 添加条目
async function addItem() {
const type = document.getElementById('addType').value;
const data = {
type,
title: document.getElementById('addTitle').value,
content: type === 'text' ? document.getElementById('addContent').value : null,
url: ['link', 'column'].includes(type) ? document.getElementById('addUrl').value : null,
source: type === 'column' ? document.getElementById('addSource').value : null,
status: type === 'todo' ? document.getElementById('addStatus').value : null,
priority: type === 'todo' ? document.getElementById('addPriority').value : null,
due_date: type === 'todo' ? document.getElementById('addDueDate').value : null,
note: document.getElementById('addNote').value,
tags: document.getElementById('addTags').value.split(',').map(t => t.trim()).filter(t => t)
};
const res = await fetch(`${API_BASE}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
document.getElementById('addForm').reset();
refreshData();
}
}
// 完成待办
async function completeItem(id) {
await fetch(`${API_BASE}/items/${id}/done`, { method: 'POST' });
refreshData();
}
// 删除条目
async function deleteItem(id) {
if (!confirm('确认删除?')) return;
await fetch(`${API_BASE}/items/${id}`, { method: 'DELETE' });
refreshData();
}
// 当前查看的条目ID
let currentDetailId = null;
// 显示详情
async function showDetail(id) {
currentDetailId = id;
const res = await fetch(`${API_BASE}/items/${id}`);
const data = await res.json();
if (!data.success) return;
const item = data.data;
document.getElementById('detailTypeIcon').textContent = getTypeIcon(item.type);
document.getElementById('detailTitle').textContent = item.title || '(无标题)';
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
if (item.url) {
html += `<div class="mb-3"><strong>URL:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>`;
}
if (item.content) {
html += `<div class="mb-3"><strong>内容:</strong><br><div class="border rounded p-3 bg-light" style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(item.content)}</div></div>`;
}
if (item.source) {
html += `<div class="mb-3"><strong>来源:</strong> ${escapeHtml(item.source)}</div>`;
}
if (item.type === 'todo') {
html += `<div class="mb-3"><strong>状态:</strong> ${getStatusLabel(item.status)}</div>`;
html += `<div class="mb-3"><strong>优先级:</strong> ${getPriorityLabel(item.priority)}</div>`;
if (item.due_date) {
html += `<div class="mb-3"><strong>截止日期:</strong> ${item.due_date}</div>`;
}
}
if (item.tags.length) {
html += `<div class="mb-3"><strong>标签:</strong> ${item.tags.map(t => `<span class="badge bg-secondary">${escapeHtml(t)}</span>`).join(' ')}</div>`;
}
if (item.note) {
html += `<div class="mb-3"><strong>详情/备注:</strong><br><div class="border rounded p-3 bg-light" style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(item.note)}</div></div>`;
}
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
document.getElementById('detailContent').innerHTML = html;
new bootstrap.Modal(document.getElementById('detailModal')).show();
}
// 从详情页打开编辑
function openEditModalFromDetail() {
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
setTimeout(() => openEditModal(currentDetailId), 300);
}
// 打开编辑模态框
async function openEditModal(id) {
currentDetailId = id;
const res = await fetch(`${API_BASE}/items/${id}`);
const data = await res.json();
if (!data.success) return;
const item = data.data;
const type = item.type;
document.getElementById('editId').value = id;
document.getElementById('editType').value = type;
document.getElementById('editTitle').value = item.title || '';
// 根据类型显示/隐藏字段
updateEditFieldsByType(type);
document.getElementById('editContent').value = item.content || '';
document.getElementById('editUrl').value = item.url || '';
document.getElementById('editSource').value = item.source || '';
document.getElementById('editTags').value = item.tags.join(', ');
document.getElementById('editNote').value = item.note || '';
if (type === 'todo') {
document.getElementById('editStatus').value = item.status;
document.getElementById('editPriority').value = item.priority;
document.getElementById('editDueDate').value = item.due_date || '';
}
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 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,
source: type === 'column' ? document.getElementById('editSource').value : null,
status: type === 'todo' ? document.getElementById('editStatus').value : null,
priority: type === 'todo' ? document.getElementById('editPriority').value : null,
due_date: type === 'todo' ? document.getElementById('editDueDate').value : null,
note: document.getElementById('editNote').value,
tags: document.getElementById('editTags').value.split(',').map(t => t.trim()).filter(t => t)
};
const res = await fetch(`${API_BASE}/items/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
refreshData();
}
}
// 工具函数
function getTypeIcon(type) {
const icons = { text: '📝', link: '🔗', column: '📰', todo: '' };
return icons[type] || '📄';
}
function getTypeLabel(type) {
const labels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
return labels[type] || type;
}
function getStatusLabel(status) {
const labels = { pending: '⏳ 待处理', in_progress: '🔄 进行中', completed: '✅ 已完成' };
return labels[status] || status;
}
function getStatusLabelShort(status) {
const labels = { pending: '', in_progress: '🔄', completed: '' };
return labels[status] || status;
}
function getPriorityLabelShort(priority) {
const labels = { low: '🟢', medium: '🟡', high: '🟠', urgent: '🔴' };
return labels[priority] || '';
}
function getPriorityLabel(priority) {
const labels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
return labels[priority] || priority;
}
function truncate(str, len) {
return str && str.length > len ? str.substring(0, len) + '...' : str || '';
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleString('zh-CN');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 标签管理
let allTags = [];
async function loadTags() {
const res = await fetch(`${API_BASE}/tags`);
const data = await res.json();
if (data.success) {
allTags = data.data.map(t => t.name);
updateTagDatalist();
}
}
function updateTagDatalist() {
const datalist = document.getElementById('tagList');
datalist.innerHTML = allTags.map(t => `<option value="${t}">`).join('');
}
function showTagSuggestions(e) {
const input = e.target.value;
const parts = input.split(',');
const current = parts[parts.length - 1].trim().toLowerCase();
const container = document.getElementById('addTagSuggestions');
if (!current) {
container.innerHTML = '';
return;
}
const suggestions = allTags.filter(t => t.toLowerCase().includes(current) && !parts.slice(0, -1).includes(t));
container.innerHTML = suggestions.slice(0, 5).map(t =>
`<span class="badge bg-light text-dark me-1" style="cursor:pointer;" onclick="addTagToInput('addTags', '${t}')">${t}</span>`
).join('');
}
function showTagSuggestionsEdit(e) {
const input = e.target.value;
const parts = input.split(',');
const current = parts[parts.length - 1].trim().toLowerCase();
const container = document.getElementById('editTagSuggestions');
if (!current) {
container.innerHTML = '';
return;
}
const suggestions = allTags.filter(t => t.toLowerCase().includes(current) && !parts.slice(0, -1).includes(t));
container.innerHTML = suggestions.slice(0, 5).map(t =>
`<span class="badge bg-light text-dark me-1" style="cursor:pointer;" onclick="addTagToInput('editTags', '${t}')">${t}</span>`
).join('');
}
function addTagToInput(inputId, tag) {
const input = document.getElementById(inputId);
const parts = input.value.split(',');
parts[parts.length - 1] = tag;
input.value = parts.join(', ') + ', ';
// 清空提示
if (inputId === 'addTags') {
document.getElementById('addTagSuggestions').innerHTML = '';
} else {
document.getElementById('editTagSuggestions').innerHTML = '';
}
}
async function showTagManager() {
await loadTagManagerList();
new bootstrap.Modal(document.getElementById('tagManagerModal')).show();
}
async function loadTagManagerList() {
const res = await fetch(`${API_BASE}/tags`);
const data = await res.json();
if (!data.success) return;
const container = document.getElementById('tagListContainer');
// 搜索过滤
const searchKeyword = document.getElementById('tagSearch').value.trim().toLowerCase();
let tags = data.data;
if (searchKeyword) {
tags = tags.filter(t => t.name.toLowerCase().includes(searchKeyword));
}
if (!tags.length) {
container.innerHTML = '<div class="text-center text-muted py-3">暂无标签</div>';
return;
}
container.innerHTML = tags.map(tag => `
<div class="d-flex justify-content-between align-items-center p-2 border-bottom" id="tag-row-${tag.id}">
<div id="tag-display-${tag.id}">
<span class="badge bg-secondary">${tag.name}</span>
<span class="text-muted small ms-2">${tag.item_count || 0} 个条目</span>
</div>
<div id="tag-edit-${tag.id}" style="display:none;">
<input type="text" class="form-control form-control-sm" id="edit-tag-name-${tag.id}" value="${tag.name}" style="width:150px;">
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" id="tag-edit-btn-${tag.id}" onclick="showEditTag(${tag.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-success" id="tag-save-btn-${tag.id}" style="display:none;" onclick="saveEditTag(${tag.id})" title="保存">
<i class="bi bi-check"></i>
</button>
<button class="btn btn-outline-secondary" id="tag-cancel-btn-${tag.id}" style="display:none;" onclick="cancelEditTag(${tag.id}, '${tag.name}')" title="取消">
<i class="bi bi-x"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteTagManager(${tag.id}, '${tag.name}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`).join('');
}
async function createTag() {
const name = document.getElementById('newTagName').value.trim();
if (!name) return;
const res = await fetch(`${API_BASE}/tags`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
if (res.ok) {
document.getElementById('newTagName').value = '';
loadTagManagerList();
loadTags();
}
}
async function deleteTagManager(id, name) {
if (!confirm(`确认删除标签 "${name}"?此操作将移除所有条目中的该标签。`)) return;
await fetch(`${API_BASE}/tags/${id}`, { method: 'DELETE' });
loadTagManagerList();
loadTags();
loadItems();
}
// 编辑标签
function showEditTag(id) {
document.getElementById(`tag-display-${id}`).style.display = 'none';
document.getElementById(`tag-edit-${id}`).style.display = 'block';
document.getElementById(`tag-edit-btn-${id}`).style.display = 'none';
document.getElementById(`tag-save-btn-${id}`).style.display = 'inline-block';
document.getElementById(`tag-cancel-btn-${id}`).style.display = 'inline-block';
document.getElementById(`edit-tag-name-${id}`).focus();
}
function cancelEditTag(id, oldName) {
document.getElementById(`edit-tag-name-${id}`).value = oldName;
document.getElementById(`tag-display-${id}`).style.display = 'block';
document.getElementById(`tag-edit-${id}`).style.display = 'none';
document.getElementById(`tag-edit-btn-${id}`).style.display = 'inline-block';
document.getElementById(`tag-save-btn-${id}`).style.display = 'none';
document.getElementById(`tag-cancel-btn-${id}`).style.display = 'none';
}
async function saveEditTag(id) {
const newName = document.getElementById(`edit-tag-name-${id}`).value.trim();
if (!newName) return;
const res = await fetch(`${API_BASE}/tags/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
loadTagManagerList();
loadTags();
loadItems();
} else {
const data = await res.json();
alert(data.error || '更新失败');
}
}
// 导出数据
async function exportData() {
const res = await fetch(`${API_BASE}/items?limit=1000`);
const data = await res.json();
if (!data.success) {
alert('导出失败');
return;
}
// 格式化JSON
const jsonStr = JSON.stringify(data.data, null, 2);
// 创建下载
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `xian_favor_export_${new Date().toISOString().slice(0,10)}.json`;
a.click();
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) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
</script>
</body>
</html>
'''
def start_server(host: str = API_HOST, port: int = API_PORT):
"""启动服务"""
app.run(host=host, port=port, debug=False)