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:
2025-12-11 21:38:28 +08:00
parent f1cd5176de
commit 966572cc94
4 changed files with 2128 additions and 1 deletions

View File

@@ -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('stats')">统计</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('settings')">设置</button>
</div>
@@ -1199,6 +1200,111 @@
</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">
<h3 style="margin-bottom: 15px; font-size: 16px;">管理员账号设置</h3>
@@ -1218,6 +1324,106 @@
</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;">&times;</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>
@@ -1274,6 +1480,14 @@
if (tabName === 'feedbacks') {
loadFeedbacks();
}
// 切换到邮件配置标签时加载邮件相关数据
if (tabName === 'email') {
loadEmailSettings();
loadSmtpConfigs();
loadEmailStats();
loadEmailLogs();
}
}
// VIP functions
@@ -2551,6 +2765,458 @@
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>
</body>
</html>