feat: 添加邮件功能第一阶段 - 邮件基础设施
新增功能: - 创建 email_service.py 邮件服务模块 - 支持多SMTP配置(主备切换、故障转移) - 发送纯文本/HTML邮件 - 发送带附件邮件(支持ZIP压缩) - 异步发送队列(多线程工作池) - 每日发送限额控制 - 发送日志记录和统计 - 数据库表结构 - smtp_configs: 多SMTP配置表 - email_settings: 全局邮件设置 - email_tokens: 邮件验证Token - email_logs: 邮件发送日志 - email_stats: 邮件发送统计 - API接口 - GET/POST /yuyx/api/email/settings: 全局邮件设置 - CRUD /yuyx/api/smtp/configs: SMTP配置管理 - POST /yuyx/api/smtp/configs/<id>/test: 测试SMTP连接 - POST /yuyx/api/smtp/configs/<id>/primary: 设为主配置 - GET /yuyx/api/email/stats: 邮件统计 - GET /yuyx/api/email/logs: 邮件日志 - POST /yuyx/api/email/logs/cleanup: 清理日志 - 后台管理页面 - 新增"邮件配置"Tab - 全局邮件开关、故障转移开关 - SMTP配置列表管理 - 添加/编辑SMTP配置弹窗 - 邮件发送统计展示 - 邮件日志查询和清理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ COPY browser_installer.py .
|
|||||||
COPY password_utils.py .
|
COPY password_utils.py .
|
||||||
COPY crypto_utils.py .
|
COPY crypto_utils.py .
|
||||||
COPY task_checkpoint.py .
|
COPY task_checkpoint.py .
|
||||||
|
COPY email_service.py .
|
||||||
|
|
||||||
# 复制新的优化模块
|
# 复制新的优化模块
|
||||||
COPY app_config.py .
|
COPY app_config.py .
|
||||||
|
|||||||
206
app.py
206
app.py
@@ -3472,6 +3472,198 @@ def checkpoint_abandon(task_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 邮件服务API ====================
|
||||||
|
import email_service
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/email/settings', methods=['GET'])
|
||||||
|
@admin_required
|
||||||
|
def get_email_settings_api():
|
||||||
|
"""获取全局邮件设置"""
|
||||||
|
try:
|
||||||
|
settings = email_service.get_email_settings()
|
||||||
|
return jsonify(settings)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取邮件设置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/email/settings', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def update_email_settings_api():
|
||||||
|
"""更新全局邮件设置"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
enabled = data.get('enabled', False)
|
||||||
|
failover_enabled = data.get('failover_enabled', True)
|
||||||
|
|
||||||
|
email_service.update_email_settings(enabled, failover_enabled)
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新邮件设置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs', methods=['GET'])
|
||||||
|
@admin_required
|
||||||
|
def get_smtp_configs_api():
|
||||||
|
"""获取所有SMTP配置列表"""
|
||||||
|
try:
|
||||||
|
configs = email_service.get_smtp_configs(include_password=False)
|
||||||
|
return jsonify(configs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取SMTP配置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def create_smtp_config_api():
|
||||||
|
"""创建SMTP配置"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# 验证必填字段
|
||||||
|
if not data.get('host'):
|
||||||
|
return jsonify({'error': 'SMTP服务器地址不能为空'}), 400
|
||||||
|
if not data.get('username'):
|
||||||
|
return jsonify({'error': 'SMTP用户名不能为空'}), 400
|
||||||
|
|
||||||
|
config_id = email_service.create_smtp_config(data)
|
||||||
|
return jsonify({'success': True, 'id': config_id})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建SMTP配置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['GET'])
|
||||||
|
@admin_required
|
||||||
|
def get_smtp_config_api(config_id):
|
||||||
|
"""获取单个SMTP配置详情"""
|
||||||
|
try:
|
||||||
|
config = email_service.get_smtp_config(config_id, include_password=False)
|
||||||
|
if not config:
|
||||||
|
return jsonify({'error': '配置不存在'}), 404
|
||||||
|
return jsonify(config)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取SMTP配置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['PUT'])
|
||||||
|
@admin_required
|
||||||
|
def update_smtp_config_api(config_id):
|
||||||
|
"""更新SMTP配置"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
if email_service.update_smtp_config(config_id, data):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'error': '更新失败'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新SMTP配置<EFBFBD><EFBFBD>败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['DELETE'])
|
||||||
|
@admin_required
|
||||||
|
def delete_smtp_config_api(config_id):
|
||||||
|
"""删除SMTP配置"""
|
||||||
|
try:
|
||||||
|
if email_service.delete_smtp_config(config_id):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'error': '删除失败'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除SMTP配置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs/<int:config_id>/test', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def test_smtp_config_api(config_id):
|
||||||
|
"""测试SMTP配置"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
test_email = data.get('email', '')
|
||||||
|
|
||||||
|
if not test_email:
|
||||||
|
return jsonify({'error': '请提供测试邮箱'}), 400
|
||||||
|
|
||||||
|
# 简单验证邮箱格式
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', test_email):
|
||||||
|
return jsonify({'error': '邮箱格式不正确'}), 400
|
||||||
|
|
||||||
|
result = email_service.test_smtp_config(config_id, test_email)
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"测试SMTP配置失败: {e}")
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/smtp/configs/<int:config_id>/primary', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def set_primary_smtp_config_api(config_id):
|
||||||
|
"""设置主SMTP配置"""
|
||||||
|
try:
|
||||||
|
if email_service.set_primary_smtp_config(config_id):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'error': '设置失败'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"设置主SMTP配置失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/email/stats', methods=['GET'])
|
||||||
|
@admin_required
|
||||||
|
def get_email_stats_api():
|
||||||
|
"""获取邮件发送统计"""
|
||||||
|
try:
|
||||||
|
stats = email_service.get_email_stats()
|
||||||
|
return jsonify(stats)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取邮件统计失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/email/logs', methods=['GET'])
|
||||||
|
@admin_required
|
||||||
|
def get_email_logs_api():
|
||||||
|
"""获取邮件发送日志"""
|
||||||
|
try:
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
page_size = request.args.get('page_size', 20, type=int)
|
||||||
|
email_type = request.args.get('type', None)
|
||||||
|
status = request.args.get('status', None)
|
||||||
|
|
||||||
|
# 限制page_size范围
|
||||||
|
page_size = min(max(page_size, 10), 100)
|
||||||
|
|
||||||
|
result = email_service.get_email_logs(page, page_size, email_type, status)
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取邮件日志失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yuyx/api/email/logs/cleanup', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def cleanup_email_logs_api():
|
||||||
|
"""清理过期邮件日志"""
|
||||||
|
try:
|
||||||
|
data = request.json or {}
|
||||||
|
days = data.get('days', 30)
|
||||||
|
|
||||||
|
# 限制days范围
|
||||||
|
days = min(max(days, 7), 365)
|
||||||
|
|
||||||
|
deleted = email_service.cleanup_email_logs(days)
|
||||||
|
return jsonify({'success': True, 'deleted': deleted})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"清理邮件日志失败: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# 初始化浏览器池(在后台线程中预热,不阻塞启动)
|
# 初始化浏览器池(在后台线程中预热,不阻塞启动)
|
||||||
# ==================== 用户定时任务API ====================
|
# ==================== 用户定时任务API ====================
|
||||||
|
|
||||||
@@ -3815,6 +4007,13 @@ def cleanup_on_exit():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 3.5 关闭邮件队列
|
||||||
|
print("- 关闭邮件队列...")
|
||||||
|
try:
|
||||||
|
email_service.shutdown_email_queue()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 4. 关闭数据库连接池
|
# 4. 关闭数据库连接池
|
||||||
print("- 关闭数据库连接池...")
|
print("- 关闭数据库连接池...")
|
||||||
try:
|
try:
|
||||||
@@ -3850,6 +4049,13 @@ if __name__ == '__main__':
|
|||||||
checkpoint_mgr = get_checkpoint_manager()
|
checkpoint_mgr = get_checkpoint_manager()
|
||||||
print("✓ 任务断点管理器已初始化")
|
print("✓ 任务断点管理器已初始化")
|
||||||
|
|
||||||
|
# 初始化邮件服务
|
||||||
|
try:
|
||||||
|
email_service.init_email_service()
|
||||||
|
print("✓ 邮件服务已初始化")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"警告: 邮件服务初始化失败: {e}")
|
||||||
|
|
||||||
# 启动内存清理调度器
|
# 启动内存清理调度器
|
||||||
start_cleanup_scheduler()
|
start_cleanup_scheduler()
|
||||||
|
|
||||||
|
|||||||
1254
email_service.py
Normal file
1254
email_service.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -732,6 +732,7 @@
|
|||||||
<button class="tab" onclick="switchTab('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></button>
|
<button class="tab" onclick="switchTab('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></button>
|
||||||
<button class="tab" onclick="switchTab('stats')">统计</button>
|
<button class="tab" onclick="switchTab('stats')">统计</button>
|
||||||
<button class="tab" onclick="switchTab('logs')">任务日志</button>
|
<button class="tab" onclick="switchTab('logs')">任务日志</button>
|
||||||
|
<button class="tab" onclick="switchTab('email')">邮件配置</button>
|
||||||
<button class="tab" onclick="switchTab('system')">系统配置</button>
|
<button class="tab" onclick="switchTab('system')">系统配置</button>
|
||||||
<button class="tab" onclick="switchTab('settings')">设置</button>
|
<button class="tab" onclick="switchTab('settings')">设置</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1199,6 +1200,111 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件配置 -->
|
||||||
|
<div id="tab-email" class="tab-content">
|
||||||
|
<h3 style="margin-bottom: 15px; font-size: 16px;">邮件功能设置</h3>
|
||||||
|
|
||||||
|
<!-- 全局设置 -->
|
||||||
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||||
|
<div class="form-group" style="margin-bottom: 10px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="emailEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none;">
|
||||||
|
启用邮件功能
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||||
|
开启后,系统将支持邮箱验证、密码重置邮件、任务完成通知等功能
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="failoverEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none;">
|
||||||
|
启用故障转移
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||||
|
开启后,主SMTP配置发送失败时自动切换到备用配置
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SMTP配置列表 -->
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
|
<h4 style="font-size: 14px; margin: 0;">SMTP配置列表</h4>
|
||||||
|
<button class="btn btn-primary" onclick="showSmtpModal()" style="padding: 8px 15px;">+ 添加配置</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="smtpConfigsList" style="margin-bottom: 20px;">
|
||||||
|
<div style="text-align: center; padding: 30px; color: #999;">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件统计 -->
|
||||||
|
<h4 style="font-size: 14px; margin: 20px 0 15px 0;">邮件发送统计</h4>
|
||||||
|
<div id="emailStats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px;">
|
||||||
|
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #667eea;" id="statTotalSent">0</div>
|
||||||
|
<div style="font-size: 12px; color: #666;">总发送</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #27ae60;" id="statTotalSuccess">0</div>
|
||||||
|
<div style="font-size: 12px; color: #666;">成功</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #e74c3c;" id="statTotalFailed">0</div>
|
||||||
|
<div style="font-size: 12px; color: #666;">失败</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #3498db;" id="statSuccessRate">0%</div>
|
||||||
|
<div style="font-size: 12px; color: #666;">成功率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 10px; margin-bottom: 20px;">
|
||||||
|
<div style="background: #fff3e0; padding: 10px; border-radius: 6px; text-align: center;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold;" id="statRegister">0</div>
|
||||||
|
<div style="font-size: 11px; color: #666;">注册验证</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #e3f2fd; padding: 10px; border-radius: 6px; text-align: center;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold;" id="statReset">0</div>
|
||||||
|
<div style="font-size: 11px; color: #666;">密码重置</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f3e5f5; padding: 10px; border-radius: 6px; text-align: center;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold;" id="statBind">0</div>
|
||||||
|
<div style="font-size: 11px; color: #666;">邮箱绑定</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #e8f5e9; padding: 10px; border-radius: 6px; text-align: center;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold;" id="statTaskComplete">0</div>
|
||||||
|
<div style="font-size: 11px; color: #666;">任务完成</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件日志 -->
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
|
<h4 style="font-size: 14px; margin: 0;">邮件发送日志</h4>
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<select id="emailLogTypeFilter" onchange="loadEmailLogs()" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px;">
|
||||||
|
<option value="">全部类型</option>
|
||||||
|
<option value="register">注册验证</option>
|
||||||
|
<option value="reset">密码重置</option>
|
||||||
|
<option value="bind">邮箱绑定</option>
|
||||||
|
<option value="task_complete">任务完成</option>
|
||||||
|
</select>
|
||||||
|
<select id="emailLogStatusFilter" onchange="loadEmailLogs()" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px;">
|
||||||
|
<option value="">全部状态</option>
|
||||||
|
<option value="success">成功</option>
|
||||||
|
<option value="failed">失败</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary" onclick="cleanupEmailLogs()" style="padding: 6px 12px; font-size: 12px;">清理日志</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="emailLogsList" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<div style="text-align: center; padding: 30px; color: #999;">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件日志分页 -->
|
||||||
|
<div id="emailLogsPagination" style="margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 系统设置 -->
|
<!-- 系统设置 -->
|
||||||
<div id="tab-settings" class="tab-content">
|
<div id="tab-settings" class="tab-content">
|
||||||
<h3 style="margin-bottom: 15px; font-size: 16px;">管理员账号设置</h3>
|
<h3 style="margin-bottom: 15px; font-size: 16px;">管理员账号设置</h3>
|
||||||
@@ -1218,6 +1324,106 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SMTP配置弹窗 -->
|
||||||
|
<div id="smtpModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; overflow-y: auto;">
|
||||||
|
<div style="background: white; max-width: 500px; margin: 50px auto; border-radius: 10px; overflow: hidden;">
|
||||||
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3 style="margin: 0; font-size: 16px;" id="smtpModalTitle">添加SMTP配置</h3>
|
||||||
|
<button onclick="hideSmtpModal()" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer;">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<input type="hidden" id="smtpConfigId">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>配置名称</label>
|
||||||
|
<input type="text" id="smtpName" placeholder="如:QQ邮箱、163邮箱">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="smtpEnabled" checked style="width: auto; max-width: none;">
|
||||||
|
启用此配置
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>SMTP服务器</label>
|
||||||
|
<input type="text" id="smtpHost" placeholder="如:smtp.qq.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>端口</label>
|
||||||
|
<input type="number" id="smtpPort" value="465">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input type="text" id="smtpUsername" placeholder="SMTP账号">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码/授权码</label>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<input type="password" id="smtpPassword" placeholder="SMTP密码或授权码" style="flex: 1;">
|
||||||
|
<button type="button" onclick="togglePasswordVisibility('smtpPassword')" class="btn btn-secondary" style="padding: 8px 12px;">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="smtpUseSsl" checked style="width: auto; max-width: none;">
|
||||||
|
使用SSL
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="smtpUseTls" style="width: auto; max-width: none;">
|
||||||
|
使用TLS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>发件人名称</label>
|
||||||
|
<input type="text" id="smtpSenderName" placeholder="如:知识管理平台" value="知识管理平台">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>发件人邮箱</label>
|
||||||
|
<input type="text" id="smtpSenderEmail" placeholder="留空则使用用户名">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>每日限额</label>
|
||||||
|
<input type="number" id="smtpDailyLimit" value="0" min="0">
|
||||||
|
<div style="font-size: 11px; color: #666; margin-top: 3px;">0表示无限制</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>优先级</label>
|
||||||
|
<input type="number" id="smtpPriority" value="0" min="0">
|
||||||
|
<div style="font-size: 11px; color: #666; margin-top: 3px;">数字越小越优先</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap;">
|
||||||
|
<button class="btn btn-secondary" onclick="testSmtpConfig()" style="flex: 1;">测试连接</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveSmtpConfig()" style="flex: 1;">保存</button>
|
||||||
|
<button class="btn" onclick="hideSmtpModal()" style="flex: 1; background: #eee;">取消</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="smtpEditActions" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button class="btn" onclick="setPrimarySmtp()" style="flex: 1; background: #fff3e0; color: #e65100;">设为主配置</button>
|
||||||
|
<button class="btn" onclick="deleteSmtpConfig()" style="flex: 1; background: #ffebee; color: #c62828;">删除配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 通知组件 -->
|
<!-- 通知组件 -->
|
||||||
<div id="notification" class="notification"></div>
|
<div id="notification" class="notification"></div>
|
||||||
|
|
||||||
@@ -1274,6 +1480,14 @@
|
|||||||
if (tabName === 'feedbacks') {
|
if (tabName === 'feedbacks') {
|
||||||
loadFeedbacks();
|
loadFeedbacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换到邮件配置标签时加载邮件相关数据
|
||||||
|
if (tabName === 'email') {
|
||||||
|
loadEmailSettings();
|
||||||
|
loadSmtpConfigs();
|
||||||
|
loadEmailStats();
|
||||||
|
loadEmailLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIP functions
|
// VIP functions
|
||||||
@@ -2551,6 +2765,458 @@
|
|||||||
showNotification('删除失败: ' + error.message, 'error');
|
showNotification('删除失败: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 邮件配置功能 ====================
|
||||||
|
|
||||||
|
let smtpConfigs = [];
|
||||||
|
let currentEmailLogPage = 1;
|
||||||
|
let totalEmailLogPages = 1;
|
||||||
|
|
||||||
|
// 加载邮件设置
|
||||||
|
async function loadEmailSettings() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/email/settings');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
document.getElementById('emailEnabled').checked = data.enabled;
|
||||||
|
document.getElementById('failoverEnabled').checked = data.failover_enabled;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载邮件设置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新邮件设置
|
||||||
|
async function updateEmailSettings() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/email/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: document.getElementById('emailEnabled').checked,
|
||||||
|
failover_enabled: document.getElementById('failoverEnabled').checked
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification('邮件设置已更新', 'success');
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
showNotification('更新失败: ' + data.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('更新失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载SMTP配置列表
|
||||||
|
async function loadSmtpConfigs() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/smtp/configs');
|
||||||
|
if (response.ok) {
|
||||||
|
smtpConfigs = await response.json();
|
||||||
|
renderSmtpConfigs();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载SMTP配置失败:', error);
|
||||||
|
document.getElementById('smtpConfigsList').innerHTML =
|
||||||
|
'<div style="text-align: center; padding: 30px; color: #e74c3c;">加载失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染SMTP配置列表
|
||||||
|
function renderSmtpConfigs() {
|
||||||
|
const container = document.getElementById('smtpConfigsList');
|
||||||
|
|
||||||
|
if (smtpConfigs.length === 0) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999; background: #f8f9fa; border-radius: 8px;">暂无SMTP配置,请点击"添加配置"创建</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="table-container"><table style="width: 100%;"><thead><tr>';
|
||||||
|
html += '<th style="width: 60px;">状态</th>';
|
||||||
|
html += '<th>名称</th>';
|
||||||
|
html += '<th>服务器</th>';
|
||||||
|
html += '<th>今日/限额</th>';
|
||||||
|
html += '<th>成功率</th>';
|
||||||
|
html += '<th style="width: 80px;">操作</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
smtpConfigs.forEach(config => {
|
||||||
|
const statusIcon = config.is_primary ? '⭐主' :
|
||||||
|
(config.enabled ? '✓备用' : '✗禁用');
|
||||||
|
const statusClass = config.is_primary ? 'color: #f39c12;' :
|
||||||
|
(config.enabled ? 'color: #27ae60;' : 'color: #95a5a6;');
|
||||||
|
|
||||||
|
const dailyText = config.daily_limit > 0 ?
|
||||||
|
`${config.daily_sent}/${config.daily_limit}` : `${config.daily_sent}/∞`;
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
html += `<td style="${statusClass} font-weight: bold;">${statusIcon}</td>`;
|
||||||
|
html += `<td><strong>${config.name}</strong></td>`;
|
||||||
|
html += `<td>${config.host}:${config.port}</td>`;
|
||||||
|
html += `<td>${dailyText}</td>`;
|
||||||
|
html += `<td>${config.success_rate}%</td>`;
|
||||||
|
html += `<td><button class="btn btn-small btn-secondary" onclick="editSmtpConfig(${config.id})">编辑</button></td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示SMTP配置弹窗
|
||||||
|
function showSmtpModal(configId = null) {
|
||||||
|
document.getElementById('smtpModal').style.display = 'block';
|
||||||
|
|
||||||
|
if (configId) {
|
||||||
|
// 编辑模式
|
||||||
|
document.getElementById('smtpModalTitle').textContent = '编辑SMTP配置';
|
||||||
|
document.getElementById('smtpEditActions').style.display = 'block';
|
||||||
|
|
||||||
|
const config = smtpConfigs.find(c => c.id === configId);
|
||||||
|
if (config) {
|
||||||
|
document.getElementById('smtpConfigId').value = config.id;
|
||||||
|
document.getElementById('smtpName').value = config.name;
|
||||||
|
document.getElementById('smtpEnabled').checked = config.enabled;
|
||||||
|
document.getElementById('smtpHost').value = config.host;
|
||||||
|
document.getElementById('smtpPort').value = config.port;
|
||||||
|
document.getElementById('smtpUsername').value = config.username;
|
||||||
|
document.getElementById('smtpPassword').value = '';
|
||||||
|
document.getElementById('smtpPassword').placeholder = config.has_password ? '留空保持不变' : 'SMTP密码或授权码';
|
||||||
|
document.getElementById('smtpUseSsl').checked = config.use_ssl;
|
||||||
|
document.getElementById('smtpUseTls').checked = config.use_tls;
|
||||||
|
document.getElementById('smtpSenderName').value = config.sender_name;
|
||||||
|
document.getElementById('smtpSenderEmail').value = config.sender_email;
|
||||||
|
document.getElementById('smtpDailyLimit').value = config.daily_limit;
|
||||||
|
document.getElementById('smtpPriority').value = config.priority;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 添加模式
|
||||||
|
document.getElementById('smtpModalTitle').textContent = '添加SMTP配置';
|
||||||
|
document.getElementById('smtpEditActions').style.display = 'none';
|
||||||
|
document.getElementById('smtpConfigId').value = '';
|
||||||
|
document.getElementById('smtpName').value = '';
|
||||||
|
document.getElementById('smtpEnabled').checked = true;
|
||||||
|
document.getElementById('smtpHost').value = '';
|
||||||
|
document.getElementById('smtpPort').value = 465;
|
||||||
|
document.getElementById('smtpUsername').value = '';
|
||||||
|
document.getElementById('smtpPassword').value = '';
|
||||||
|
document.getElementById('smtpPassword').placeholder = 'SMTP密码或授权码';
|
||||||
|
document.getElementById('smtpUseSsl').checked = true;
|
||||||
|
document.getElementById('smtpUseTls').checked = false;
|
||||||
|
document.getElementById('smtpSenderName').value = '知识管理平台';
|
||||||
|
document.getElementById('smtpSenderEmail').value = '';
|
||||||
|
document.getElementById('smtpDailyLimit').value = 0;
|
||||||
|
document.getElementById('smtpPriority').value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏SMTP配置弹窗
|
||||||
|
function hideSmtpModal() {
|
||||||
|
document.getElementById('smtpModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑SMTP配置
|
||||||
|
function editSmtpConfig(configId) {
|
||||||
|
showSmtpModal(configId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存SMTP配置
|
||||||
|
async function saveSmtpConfig() {
|
||||||
|
const configId = document.getElementById('smtpConfigId').value;
|
||||||
|
const host = document.getElementById('smtpHost').value.trim();
|
||||||
|
const username = document.getElementById('smtpUsername').value.trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
showNotification('请输入SMTP服务器地址', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!username) {
|
||||||
|
showNotification('请输入SMTP用户名', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: document.getElementById('smtpName').value.trim() || '默认配置',
|
||||||
|
enabled: document.getElementById('smtpEnabled').checked,
|
||||||
|
host: host,
|
||||||
|
port: parseInt(document.getElementById('smtpPort').value) || 465,
|
||||||
|
username: username,
|
||||||
|
use_ssl: document.getElementById('smtpUseSsl').checked,
|
||||||
|
use_tls: document.getElementById('smtpUseTls').checked,
|
||||||
|
sender_name: document.getElementById('smtpSenderName').value.trim(),
|
||||||
|
sender_email: document.getElementById('smtpSenderEmail').value.trim(),
|
||||||
|
daily_limit: parseInt(document.getElementById('smtpDailyLimit').value) || 0,
|
||||||
|
priority: parseInt(document.getElementById('smtpPriority').value) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const password = document.getElementById('smtpPassword').value;
|
||||||
|
if (password) {
|
||||||
|
data.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (configId) {
|
||||||
|
// 更新
|
||||||
|
response = await fetch('/yuyx/api/smtp/configs/' + configId, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
if (!password) {
|
||||||
|
showNotification('新建配置需要输入密码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response = await fetch('/yuyx/api/smtp/configs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification(configId ? '配置已更新' : '配置已添加', 'success');
|
||||||
|
hideSmtpModal();
|
||||||
|
loadSmtpConfigs();
|
||||||
|
} else {
|
||||||
|
showNotification('保存失败: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('保存失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试SMTP配置
|
||||||
|
async function testSmtpConfig() {
|
||||||
|
const configId = document.getElementById('smtpConfigId').value;
|
||||||
|
if (!configId) {
|
||||||
|
showNotification('请先保存配置再测试', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testEmail = prompt('请输入测试接收邮箱:');
|
||||||
|
if (!testEmail) return;
|
||||||
|
|
||||||
|
showNotification('正在发送测试邮件...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/smtp/configs/' + configId + '/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: testEmail })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('测试邮件发送成功!请检查收件箱', 'success');
|
||||||
|
} else {
|
||||||
|
showNotification('测试失败: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('测试失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设为主配置
|
||||||
|
async function setPrimarySmtp() {
|
||||||
|
const configId = document.getElementById('smtpConfigId').value;
|
||||||
|
if (!configId) return;
|
||||||
|
|
||||||
|
if (!confirm('确定要将此配置设为主配置吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/smtp/configs/' + configId + '/primary', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification('已设为主配置', 'success');
|
||||||
|
hideSmtpModal();
|
||||||
|
loadSmtpConfigs();
|
||||||
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
|
showNotification('设置失败: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('设置失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除SMTP配置
|
||||||
|
async function deleteSmtpConfig() {
|
||||||
|
const configId = document.getElementById('smtpConfigId').value;
|
||||||
|
if (!configId) return;
|
||||||
|
|
||||||
|
if (!confirm('确定要删除此SMTP配置吗?此操作不可恢复!')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/smtp/configs/' + configId, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification('配置已删除', 'success');
|
||||||
|
hideSmtpModal();
|
||||||
|
loadSmtpConfigs();
|
||||||
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
|
showNotification('删除失败: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('删除失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载邮件统计
|
||||||
|
async function loadEmailStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/email/stats');
|
||||||
|
if (response.ok) {
|
||||||
|
const stats = await response.json();
|
||||||
|
document.getElementById('statTotalSent').textContent = stats.total_sent || 0;
|
||||||
|
document.getElementById('statTotalSuccess').textContent = stats.total_success || 0;
|
||||||
|
document.getElementById('statTotalFailed').textContent = stats.total_failed || 0;
|
||||||
|
document.getElementById('statSuccessRate').textContent = (stats.success_rate || 0) + '%';
|
||||||
|
document.getElementById('statRegister').textContent = stats.register_sent || 0;
|
||||||
|
document.getElementById('statReset').textContent = stats.reset_sent || 0;
|
||||||
|
document.getElementById('statBind').textContent = stats.bind_sent || 0;
|
||||||
|
document.getElementById('statTaskComplete').textContent = stats.task_complete_sent || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载邮件统计失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载邮件日志
|
||||||
|
async function loadEmailLogs(page = 1) {
|
||||||
|
currentEmailLogPage = page;
|
||||||
|
|
||||||
|
const typeFilter = document.getElementById('emailLogTypeFilter').value;
|
||||||
|
const statusFilter = document.getElementById('emailLogStatusFilter').value;
|
||||||
|
|
||||||
|
let url = `/yuyx/api/email/logs?page=${page}&page_size=15`;
|
||||||
|
if (typeFilter) url += `&type=${typeFilter}`;
|
||||||
|
if (statusFilter) url += `&status=${statusFilter}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
totalEmailLogPages = data.total_pages;
|
||||||
|
renderEmailLogs(data.logs);
|
||||||
|
renderEmailLogsPagination(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载邮件日志失败:', error);
|
||||||
|
document.getElementById('emailLogsList').innerHTML =
|
||||||
|
'<div style="text-align: center; padding: 30px; color: #e74c3c;">加载失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染邮件日志
|
||||||
|
function renderEmailLogs(logs) {
|
||||||
|
const container = document.getElementById('emailLogsList');
|
||||||
|
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999;">暂无邮件日志</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap = {
|
||||||
|
'register': '注册验证',
|
||||||
|
'reset': '密码重置',
|
||||||
|
'bind': '邮箱绑定',
|
||||||
|
'task_complete': '任务完成'
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = '<div class="table-container"><table style="width: 100%; font-size: 12px;"><thead><tr>';
|
||||||
|
html += '<th>时间</th><th>收件人</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
logs.forEach(log => {
|
||||||
|
const statusClass = log.status === 'success' ? 'color: #27ae60;' : 'color: #e74c3c;';
|
||||||
|
const statusText = log.status === 'success' ? '成功' : '失败';
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
html += `<td style="white-space: nowrap;">${log.created_at}</td>`;
|
||||||
|
html += `<td>${log.email_to}</td>`;
|
||||||
|
html += `<td>${typeMap[log.email_type] || log.email_type}</td>`;
|
||||||
|
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${log.subject}">${log.subject}</td>`;
|
||||||
|
html += `<td style="${statusClass} font-weight: bold;">${statusText}</td>`;
|
||||||
|
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${log.error_message || ''}">${log.error_message || '-'}</td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染邮件日志分页
|
||||||
|
function renderEmailLogsPagination(data) {
|
||||||
|
const container = document.getElementById('emailLogsPagination');
|
||||||
|
|
||||||
|
if (data.total_pages <= 1) {
|
||||||
|
container.innerHTML = `<span style="font-size: 12px; color: #999;">共 ${data.total} 条记录</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(1)" ${data.page <= 1 ? 'disabled' : ''} style="padding: 6px 12px;">首页</button>`;
|
||||||
|
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.page - 1})" ${data.page <= 1 ? 'disabled' : ''} style="padding: 6px 12px;">上一页</button>`;
|
||||||
|
html += `<span style="font-size: 13px; color: #666;">第 ${data.page} 页 / 共 ${data.total_pages} 页</span>`;
|
||||||
|
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.page + 1})" ${data.page >= data.total_pages ? 'disabled' : ''} style="padding: 6px 12px;">下一页</button>`;
|
||||||
|
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.total_pages})" ${data.page >= data.total_pages ? 'disabled' : ''} style="padding: 6px 12px;">末页</button>`;
|
||||||
|
html += `<span style="font-size: 12px; color: #999; margin-left: 10px;">共 ${data.total} 条记录</span>`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理邮件日志
|
||||||
|
async function cleanupEmailLogs() {
|
||||||
|
const days = prompt('请输入保留天数(将删除该天数之前的日志):', '30');
|
||||||
|
if (!days) return;
|
||||||
|
|
||||||
|
const daysNum = parseInt(days);
|
||||||
|
if (isNaN(daysNum) || daysNum < 7) {
|
||||||
|
showNotification('天数必须大于等于7', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`确定要删除 ${daysNum} 天之前的邮件日志吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/yuyx/api/email/logs/cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ days: daysNum })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification(`已清理 ${result.deleted} 条日志`, 'success');
|
||||||
|
loadEmailLogs();
|
||||||
|
} else {
|
||||||
|
showNotification('清理失败: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('清理失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换密码显示
|
||||||
|
function togglePasswordVisibility(inputId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user