Files
palladium-monitor/templates/email_config.html
T
Your Name 89dc6b4449 feat: 邮件配置页面 + 数据库持久化
- 新增 /email 配置页面 + /api/email/config API (GET/POST)
- 新增 /api/email/test-send 测试邮件 + /api/email/reset 重置
- EmailConfig SQLite 表持久化配置,回退到环境变量
- mailer: get_config 优先读 DB,send_report 读取 DB 密码
- 修复 _send_smtp 登录使用环境变量密码的 bug
- 前端新增配置表单 (启用开关/SMTP/TLS/认证/发件人/收件人/时间)
- CSS 暗色主题样式 + 响应式适配
- 导航栏新增「设置」链接
2026-07-08 14:36:41 +08:00

330 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}邮件配置 — Palladium Z1 Monitor{% endblock %}
{% block content %}
<!-- ═══ 页面标题 ═══ -->
<div class="page-header">
<h1>📧 邮件配置</h1>
<p class="page-subtitle">配置每日监控报告邮件发送。设置保存后立即生效,无需重启。</p>
</div>
<!-- ═══ 状态卡片 ═══ -->
<div class="config-status-card" id="statusCard">
<div class="status-icon" id="statusIcon"></div>
<div class="status-text">
<div class="status-title" id="statusTitle">加载中...</div>
<div class="status-detail" id="statusDetail">正在获取当前配置...</div>
</div>
</div>
<!-- ═══ 配置表单 ═══ -->
<div class="config-card">
<div class="config-card-header">
<h2>⚙️ SMTP 配置</h2>
<span class="config-source-badge" id="configSource"></span>
</div>
<form id="emailForm" onsubmit="return false;">
<!-- 启用开关 -->
<div class="form-row toggle-row">
<label class="toggle-label">
<span class="toggle-title">启用每日邮件报告</span>
<span class="toggle-desc">开启后每天定时发送监控报告</span>
</label>
<label class="toggle-switch">
<input type="checkbox" id="cfg_enabled">
<span class="toggle-slider"></span>
</label>
</div>
<!-- SMTP 基础 -->
<div class="form-section">
<div class="form-group">
<label for="cfg_smtp_host">SMTP 服务器</label>
<input type="text" id="cfg_smtp_host" placeholder="smtp.example.com" autocomplete="off">
</div>
<div class="form-group form-group-small">
<label for="cfg_smtp_port">端口</label>
<input type="number" id="cfg_smtp_port" placeholder="587" min="1" max="65535">
</div>
<div class="form-group form-group-small">
<label>加密方式</label>
<div class="checkbox-group">
<label class="checkbox-label"><input type="checkbox" id="cfg_smtp_use_tls"> STARTTLS</label>
<label class="checkbox-label"><input type="checkbox" id="cfg_smtp_use_ssl"> SMTP SSL</label>
</div>
</div>
</div>
<!-- 认证 -->
<div class="form-section">
<div class="form-group">
<label for="cfg_smtp_user">用户名</label>
<input type="text" id="cfg_smtp_user" placeholder="your@email.com" autocomplete="off">
</div>
<div class="form-group">
<label for="cfg_smtp_password">密码 / 授权码</label>
<div class="password-input-wrap">
<input type="password" id="cfg_smtp_password" placeholder="输入密码">
<button type="button" class="password-toggle" onclick="togglePassword()">👁</button>
</div>
</div>
</div>
<!-- 发件人 -->
<div class="form-section">
<div class="form-group">
<label for="cfg_from_addr">发件邮箱</label>
<input type="email" id="cfg_from_addr" placeholder="monitor@example.com" autocomplete="off">
</div>
<div class="form-group">
<label for="cfg_from_name">发件人名称</label>
<input type="text" id="cfg_from_name" placeholder="Palladium Z1 Monitor">
</div>
</div>
<!-- 收件人 -->
<div class="form-section">
<div class="form-group full-width">
<label for="cfg_to_addrs">收件邮箱 <span class="label-hint">(多个邮箱用逗号分隔)</span></label>
<textarea id="cfg_to_addrs" rows="2" placeholder="admin@example.com, dev@example.com"></textarea>
</div>
</div>
<!-- 发送时间 -->
<div class="form-section">
<div class="form-group form-group-small">
<label for="cfg_send_hour">发送时间 (小时)</label>
<input type="number" id="cfg_send_hour" placeholder="9" min="0" max="23">
</div>
<div class="form-group form-group-small">
<label for="cfg_send_minute">发送时间 (分钟)</label>
<input type="number" id="cfg_send_minute" placeholder="0" min="0" max="59">
</div>
<div class="form-group">
<label for="cfg_monitor_base_url">监控服务器地址</label>
<input type="url" id="cfg_monitor_base_url" placeholder="http://127.0.0.1:5100">
</div>
</div>
</form>
<!-- 操作按钮 -->
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="saveConfig()">💾 保存配置</button>
<button type="button" class="btn btn-secondary" onclick="testSend()" id="testSendBtn">📤 发送测试邮件</button>
<a href="/api/email/preview" target="_blank" class="btn btn-secondary">👁 预览报告</a>
<button type="button" class="btn btn-outline" onclick="resetConfig()">🔄 恢复环境变量默认</button>
</div>
</div>
<!-- 提示信息 -->
<div class="info-card">
<h3>️ 说明</h3>
<ul>
<li>配置保存到数据库后立即可用,邮件调度器会自动读取新配置。</li>
<li>如果数据库和环境中都未配置,回退到环境变量 (SMTP_HOST, SMTP_USER 等)。</li>
<li>「恢复环境变量默认」会清除数据库中的配置,回退到环境变量。</li>
<li>手动发送测试邮件需先确保配置完整(无错误提示)。</li>
</ul>
</div>
{% endblock %}
{% block scripts %}
<script>
(function() {
const fields = [
'enabled', 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_password',
'smtp_use_tls', 'smtp_use_ssl', 'from_addr', 'from_name',
'to_addrs', 'send_hour', 'send_minute', 'monitor_base_url'
];
function setBusy(busy) {
const btns = document.querySelectorAll('.form-actions .btn');
btns.forEach(b => b.disabled = busy);
}
function showError(msg) {
document.getElementById('statusIcon').textContent = '❌';
document.getElementById('statusTitle').textContent = '配置错误';
document.getElementById('statusDetail').textContent = msg;
document.getElementById('statusCard').className = 'config-status-card status-error';
}
function showConfigured(enabled) {
document.getElementById('statusIcon').textContent = enabled ? '✅' : '⚠️';
document.getElementById('statusTitle').textContent = enabled
? '配置完整'
: '配置不完整';
document.getElementById('statusDetail').textContent = enabled
? '所有必填项已填写,邮件功能已就绪。'
: '缺少必要的配置项,邮件无法发送。';
document.getElementById('statusCard').className = 'config-status-card '
+ (enabled ? 'status-ok' : 'status-warning');
}
function showErrors(errs) {
document.getElementById('statusIcon').textContent = '⚠️';
document.getElementById('statusTitle').textContent = '配置不完整';
document.getElementById('statusDetail').textContent = errs.join('');
document.getElementById('statusCard').className = 'config-status-card status-warning';
}
function loadConfig() {
fetch('/api/email/config')
.then(r => r.json())
.then(data => {
if (!data.ok) {
showError(data.error || '获取配置失败');
return;
}
// 填充表单
document.getElementById('cfg_enabled').checked = !!data.enabled;
document.getElementById('cfg_smtp_host').value = data.smtp_host || '';
document.getElementById('cfg_smtp_port').value = data.smtp_port || 587;
document.getElementById('cfg_smtp_user').value = data.smtp_user || '';
document.getElementById('cfg_smtp_password').value = data.smtp_password || '';
document.getElementById('cfg_smtp_use_tls').checked = !!data.smtp_use_tls;
document.getElementById('cfg_smtp_use_ssl').checked = !!data.smtp_use_ssl;
document.getElementById('cfg_from_addr').value = data.from_addr || '';
document.getElementById('cfg_from_name').value = data.from_name || '';
document.getElementById('cfg_to_addrs').value = (data.to_addrs || []).join(', ');
document.getElementById('cfg_send_hour').value = data.send_hour != null ? data.send_hour : 9;
document.getElementById('cfg_send_minute').value = data.send_minute != null ? data.send_minute : 0;
document.getElementById('cfg_monitor_base_url').value = data.monitor_base_url || '';
// 显示状态
if (data.configured) {
showConfigured(data.enabled);
} else if (data.config_errors && data.config_errors.length > 0) {
showErrors(data.config_errors);
} else {
showConfigured(!!data.enabled);
}
// 来源标识
document.getElementById('configSource').textContent = '数据库配置';
})
.catch(err => {
showError('网络请求失败: ' + err.message);
});
}
window.saveConfig = function() {
setBusy(true);
const data = {};
data.enabled = document.getElementById('cfg_enabled').checked;
data.smtp_host = document.getElementById('cfg_smtp_host').value.trim();
data.smtp_port = parseInt(document.getElementById('cfg_smtp_port').value) || 587;
data.smtp_user = document.getElementById('cfg_smtp_user').value.trim();
data.smtp_password = document.getElementById('cfg_smtp_password').value;
data.smtp_use_tls = document.getElementById('cfg_smtp_use_tls').checked;
data.smtp_use_ssl = document.getElementById('cfg_smtp_use_ssl').checked;
data.from_addr = document.getElementById('cfg_from_addr').value.trim();
data.from_name = document.getElementById('cfg_from_name').value.trim();
const toRaw = document.getElementById('cfg_to_addrs').value.trim();
data.to_addrs = toRaw ? toRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
// 校验时间范围
let h = parseInt(document.getElementById('cfg_send_hour').value);
let m = parseInt(document.getElementById('cfg_send_minute').value);
if (isNaN(h) || h < 0 || h > 23) {
setBusy(false);
showError('发送时间(小时)必须在 0–23 之间');
document.getElementById('cfg_send_hour').focus();
return;
}
if (isNaN(m) || m < 0 || m > 59) {
setBusy(false);
showError('发送时间(分钟)必须在 0–59 之间');
document.getElementById('cfg_send_minute').focus();
return;
}
data.send_hour = h;
data.send_minute = m;
data.monitor_base_url = document.getElementById('cfg_monitor_base_url').value.trim();
fetch('/api/email/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(result => {
setBusy(false);
if (result.ok) {
if (result.configured) {
showConfigured(result.enabled);
} else if (result.config_errors && result.config_errors.length > 0) {
showErrors(result.config_errors);
}
} else {
showError(result.error || '保存失败');
}
})
.catch(err => {
setBusy(false);
showError('保存失败: ' + err.message);
});
};
window.testSend = function() {
if (!confirm('确定要发送一封测试邮件到当前配置的收件人吗?')) return;
setBusy(true);
const btn = document.getElementById('testSendBtn');
btn.textContent = '⏳ 发送中...';
fetch('/api/email/test-send', { method: 'POST' })
.then(r => r.json())
.then(result => {
setBusy(false);
btn.textContent = '📤 发送测试邮件';
if (result.ok) {
document.getElementById('statusIcon').textContent = '✅';
document.getElementById('statusTitle').textContent = '发送成功';
document.getElementById('statusDetail').textContent =
'已发送到: ' + (result.recipients || []).join(', ');
document.getElementById('statusCard').className = 'config-status-card status-ok';
} else {
showError(result.error || '发送失败');
}
})
.catch(err => {
setBusy(false);
btn.textContent = '📤 发送测试邮件';
showError('发送失败: ' + err.message);
});
};
window.resetConfig = function() {
if (!confirm('确定要清除数据库配置并回退到环境变量吗?')) return;
setBusy(true);
fetch('/api/email/reset', { method: 'POST' })
.then(r => r.json())
.then(result => {
setBusy(false);
if (result.ok) {
document.getElementById('configSource').textContent = '环境变量';
loadConfig(); // 重新加载
} else {
showError(result.error || '重置失败');
}
})
.catch(err => {
setBusy(false);
showError('重置失败: ' + err.message);
});
};
window.togglePassword = function() {
const inp = document.getElementById('cfg_smtp_password');
inp.type = inp.type === 'password' ? 'text' : 'password';
};
// 初始化
loadConfig();
})();
</script>
{% endblock %}