6726524c50
- 修复删除快照时 AttributeError: SQLAlchemy.get_or_404 (低版本兼容) - UI/标题/邮件主题从 'Palladium Z1 Monitor' 改为 'Palladium Monitor'
330 lines
14 KiB
HTML
330 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}邮件配置 — Palladium 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 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 %} |