1771 lines
70 KiB
Python
1771 lines
70 KiB
Python
"""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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// 标签管理
|
||
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) |