@@ -360,9 +360,14 @@
<h4 class="text-gray-200 text-sm font-medium flex items-center gap-2">
<i class="ri-mail-line text-green-400"></i> 邮件通知规则
</h4>
<button onclick="showAddEmailRuleModal()" class="btn bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs flex items-center gap-1 ">
<i class="ri-add-line"></i> 添加规则
</button>
<div class=" flex items-center gap-2 ">
<button onclick="showTestEmailModal()" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-xs flex items-center gap-1">
<i class="ri-send-plane-line"></i> 测试发送
</button>
<button onclick="showAddEmailRuleModal()" class="btn bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs flex items-center gap-1">
<i class="ri-add-line"></i> 添加规则
</button>
</div>
</div>
<div id="emailRulesList" class="space-y-2 text-sm">
<div class="text-gray-500 text-center py-4">暂无邮件通知规则</div>
@@ -671,20 +676,39 @@
</div>
<div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">监控资源 *</label>
<select id="emailRuleResource" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg ">
<option value="cpu">CPU 使用率</option >
<option value="memory">内存使用率</option >
<option value="disk">磁盘使用率</option >
</select >
<label class="block text-gray-400 text-sm mb-1">监控资源 *(可多选组合) </label>
<div class="grid grid-cols-2 gap-2 ">
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700" >
<input type="checkbox" value="cpu" class="email-rule-resource w-4 h-4" onchange="onResourceChange()" >
<i class="ri-cpu-line text-blue-400"></i >
<span class="text-gray-200 text-sm">CPU 使用率</span >
</label>
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
<input type="checkbox" value="cpu_temp" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
<i class="ri-temp-hot-line text-red-400"></i>
<span class="text-gray-200 text-sm">CPU 温度</span>
</label>
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
<input type="checkbox" value="memory" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
<i class="ri-database-2-line text-green-400"></i>
<span class="text-gray-200 text-sm">内存使用率</span>
</label>
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
<input type="checkbox" value="disk" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
<i class="ri-hard-drive-2-line text-orange-400"></i>
<span class="text-gray-200 text-sm">磁盘使用率</span>
</label>
</div>
<p class="text-gray-500 text-xs mt-1">勾选多项可随机组合监控,任一资源超标即触发通知</p>
</div>
<div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">触发阈值 *</label>
<div class="flex items-center gap-2">
<input type="number" id="emailRuleThreshold" required min="1" max="100" value="80" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
<span class="text-gray-400">% </span>
<input type="number" id="emailRuleThreshold" required min="1" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="80" >
<span class="text-gray-400" id="emailRuleThresholdUnit">% / °C </span>
</div>
<p class="text-gray-500 text-xs mt-1">对不同资源类型使用统一阈值( 如CPU用%则填百分比, 温度用°C则填温度值) </p>
</div>
<div class="mb-3">
@@ -744,6 +768,45 @@
</div>
</div>
<!-- 测试邮件发送模态框 -->
<div id="testEmailModal" class="modal-overlay hidden">
<div class="card rounded-xl modal-content p-6" style="max-width: 420px;">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg"><i class="ri-send-plane-line text-blue-400"></i> 测试邮件发送</h3>
<button onclick="closeTestEmailModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
</div>
<form id="testEmailForm" onsubmit="sendTestEmail(event)">
<div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">收件邮箱 *</label>
<input type="email" id="testEmailAddress" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="your@email.com">
</div>
<div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">邮件主题</label>
<input type="text" id="testEmailSubject" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="[测试邮件] 项目服务管理面板">
</div>
<div class="mb-4">
<label class="block text-gray-400 text-sm mb-1">邮件内容</label>
<textarea id="testEmailBody" rows="3" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">这是一封测试邮件,用于验证邮件通知功能是否正常工作。
发送自: 项目服务管理面板
时间: ${new Date().toLocaleString()}</textarea>
</div>
<div id="testEmailResult" class="mb-3 hidden"></div>
<div class="flex justify-end gap-2">
<button type="button" onclick="closeTestEmailModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
<button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-1">
<i class="ri-send-plane-line"></i> 发送测试
</button>
</div>
</form>
</div>
</div>
<!-- 导航按钮 -->
<div class="nav-buttons">
<button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
@@ -1498,7 +1561,8 @@
return;
}
const resourceNames = { 'cpu': 'CPU', 'memory': '内存', 'disk': '磁盘' };
const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘' };
const resourceUnits = { 'cpu': '%', 'cpu_temp': '°C', 'memory': '%', 'disk': '%' };
list.innerHTML = rules.map(rule => {
const silentInfo = rule.silentEnabled ? `静默:${rule.silentStart}-${rule.silentEnd}` : '';
@@ -1509,12 +1573,16 @@
else if (triggerType === 'duration') triggerDisplay = `持续${triggerParam}秒`;
else if (triggerType === 'average') triggerDisplay = `平均${triggerParam}次`;
// 显示资源列表(支持多资源和旧版单资源兼容)
const resources = rule.resources || (rule.resource ? [rule.resource] : []);
const resourceDisplay = resources.map(r => resourceNames[r] || r).join('+');
return `
<div class="flex items-center justify-between bg-gray-700/50 px-3 py-2 rounded-lg">
<div class="flex items-center gap-3 flex-wrap">
<span class="px-2 py-0.5 rounded text-xs ${rule.enabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}">${rule.enabled ? '启用' : '禁用'}</span>
<span class="text-gray-200">${rule.name}</span>
<span class="text-gray-400 text-xs">${resourceNames[rule.resource] } ≥ ${rule.threshold}% </span>
<span class="text-gray-400 text-xs">${resourceDisplay } ≥ ${rule.threshold}</span>
<span class="text-blue-400/70 text-xs"><i class="ri-flashlight-line"></i> ${triggerDisplay}</span>
<span class="text-gray-400 text-xs">→ ${rule.email}</span>
${silentInfo ? `<span class="text-yellow-400/70 text-xs"><i class="ri-moon-line"></i> ${silentInfo}</span>` : ''}
@@ -1532,8 +1600,10 @@
document.getElementById('emailRuleModalTitle').textContent = '添加邮件通知规则';
document.getElementById('emailRuleId').value = '';
document.getElementById('emailRuleName').value = '';
document.getElementById('emailRuleResource').value = 'cpu';
// 重置资源复选框
document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
document.getElementById('emailRuleThreshold').value = 80;
document.getElementById('emailRuleThresholdUnit').textContent = '% / °C';
document.getElementById('emailRuleTriggerType').value = 'single';
document.getElementById('emailRuleTriggerParam').value = 3;
document.getElementById('triggerParamsDiv').style.display = 'none';
@@ -1550,6 +1620,33 @@
document.getElementById('emailRuleModal').classList.add('hidden');
}
function getSelectedResources() {
const checkboxes = document.querySelectorAll('.email-rule-resource:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
function onResourceChange() {
const selected = getSelectedResources();
const unitEl = document.getElementById('emailRuleThresholdUnit');
const thresholdEl = document.getElementById('emailRuleThreshold');
if (selected.length === 0) {
unitEl.textContent = '% / °C';
} else if (selected.length === 1 && selected[0] === 'cpu_temp') {
unitEl.textContent = '°C';
if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80;
thresholdEl.max = 120;
thresholdEl.min = 30;
} else {
unitEl.textContent = '%';
thresholdEl.max = 100;
thresholdEl.min = 1;
if (selected.includes('cpu_temp')) {
unitEl.textContent = '% / °C';
}
}
}
function updateTriggerParams() {
const triggerType = document.getElementById('emailRuleTriggerType').value;
const paramsDiv = document.getElementById('triggerParamsDiv');
@@ -1598,7 +1695,15 @@
document.getElementById('emailRuleModalTitle').textContent = '编辑邮件通知规则';
document.getElementById('emailRuleId').value = rule.id;
document.getElementById('emailRuleName').value = rule.name;
document.getElementById('emailRuleResource').value = rule.resource;
// 重置所有复选框
document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
// 勾选已保存的资源( 支持新版resources数组和旧版resource字符串兼容)
const resources = rule.resources || (rule.resource ? [rule.resource] : []);
resources.forEach(r => {
const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
if (cb) cb.checked = true;
});
onResourceChange();
document.getElementById('emailRuleThreshold').value = rule.threshold;
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
@@ -1617,7 +1722,7 @@
const ruleId = document.getElementById('emailRuleId').value;
const name = document.getElementById('emailRuleName').value.trim();
const resource = document.getElementById('emailRuleResource').value ;
const resources = getSelectedResources() ;
const threshold = parseInt(document.getElementById('emailRuleThreshold').value);
const triggerType = document.getElementById('emailRuleTriggerType').value;
const triggerParam = parseInt(document.getElementById('emailRuleTriggerParam').value) || 3;
@@ -1633,19 +1738,24 @@
return;
}
if (resources.length === 0) {
alert('请至少选择一个监控资源');
return;
}
const rules = getEmailRules();
if (ruleId) {
// 编辑
const idx = rules.findIndex(r => r.id === ruleId);
if (idx >= 0) {
rules[idx] = { ...rules[idx], name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
rules[idx] = { ...rules[idx], name, resources , threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
}
} else {
// 新增
rules.push({
id: 'rule_' + Date.now(),
name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
name, resources , threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
lastSent: 0,
exceedCount: 0, // 连续超过次数
exceedDuration: 0, // 累计超过时间
@@ -1699,77 +1809,90 @@
}
}
// 检查阈值和触发机制
const value = data[rule.resource]?.percent ;
if (value === undefined) continue ;
// 检查阈值和触发机制(支持多资源)
const resources = rule.resources || (rule.resource ? [rule.resource] : []) ;
const resourceNames = { 'cpu': 'CPU使用率', 'cpu_temp': 'CPU温度', 'memory': '内存使用率', 'disk': '磁盘使用率' } ;
// 初始化触发相关数据
if (!rule.exceedCount) rule.exceedCount = 0 ;
if (!rule.exceedDuration) rule.exceedDuration = 0;
if (!rule.recentValues) rule.recentValues = [];
if (!rule.triggerType) rule.triggerType = 'single';
if (!rule.triggerParam) rule.triggerParam = 3;
// 收集所有超标的资源
const exceededResources = [] ;
const triggerType = rule.triggerType;
const triggerParam = rule.triggerParam ;
const isExceedThreshold = value >= rule.threshold;
let shouldTrigger = false ;
// 根据触发类型判断
switch (triggerType) {
case 'single':
// 一次性触发:超过阈值立即触发
if (isExceedThreshold) shouldTrigger = true;
break ;
for ( const resource of resources) {
let value ;
if (resource === 'cpu_temp') {
// CPU温度取两个Package中较高的那个
const t0 = data.cpu?.temp_package_0 ;
const t1 = data.cpu?.temp_package_1;
if (t0 === null && t1 === null) continue;
if (t0 === null) value = t1;
else if (t1 === null) value = t0;
else value = Math.max(t0, t1);
} else {
value = data[resource]?.percent ;
}
case ' continuous':
// 连续触发: 连续超过阈值N次
if (isExceedThreshold) {
rule.exceedCount++;
if (rule.exceedCount >= triggerParam) {
shouldTrigger = true;
rule.exceedCount = 0; // 触发后重置计数
}
} else {
rule.exceedCount = 0; // 未超过阈值时重置计数
}
break;
if (value === undefined || value === null) continue;
case 'duration':
// 持续时间触发: 累计超过阈值N秒
if (isExceedThreshold) {
// 增加累计时间( 假设实时监控间隔为2秒)
const intervalSeconds = realtimeInterval || 2;
rule.exceedDuration += intervalSeconds;
if (rule.exceedDuration >= triggerParam) {
shouldTrigger = true;
rule.exceedDuration = 0; // 触发后重置
}
} else {
rule.exceedDuration = 0; // 未超过阈值时重置累计时间
}
break;
const isExceedThreshold = value >= rule.threshold;
case 'average':
// 平均值触发: 最近N次平均值超过阈值
rule.recentValues.push(value) ;
if (rule.recentValues.length > triggerParam) {
rule. recentValues.shift(); // 保持最近N个值
}
if (rule.recentValues.length >= triggerParam) {
const avg = rule.recentValues.reduce((a, b) => a + b, 0) / rule.recentValues.length ;
if (avg >= rule.threshold) {
shouldTrigger = true;
// 初始化触发相关数据( key后缀避免不同资源冲突)
const ck = '_' + resource;
if (!rule['exceedCount' + ck]) rule['exceedCount' + ck] = 0 ;
if (! rule['exceedDuration' + ck]) rule['exceedDuration' + ck] = 0;
if (!rule['recentValues' + ck]) rule[' recentValues' + ck] = [];
const triggerType = rule.triggerType || 'single';
const triggerParam = rule.triggerParam || 3 ;
let resourceTriggered = false;
switch (triggerType) {
case 'single':
if (isExceedThreshold) resourceTriggered = true;
break;
case 'continuous':
if (isExceedThreshold) {
rule['exceedCount' + ck]++;
if (rule['exceedCount' + ck] >= triggerParam) {
resourceTriggered = true;
rule['exceedCount' + ck] = 0;
}
} else {
rule['exceedCount' + ck] = 0;
}
}
break;
break;
case 'duration':
if (isExceedThreshold) {
const intervalSec = realtimeInterval || 2;
rule['exceedDuration' + ck] += intervalSec;
if (rule['exceedDuration' + ck] >= triggerParam) {
resourceTriggered = true;
rule['exceedDuration' + ck] = 0;
}
} else {
rule['exceedDuration' + ck] = 0;
}
break;
case 'average':
rule['recentValues' + ck].push(value);
if (rule['recentValues' + ck].length > triggerParam) {
rule['recentValues' + ck].shift();
}
if (rule['recentValues' + ck].length >= triggerParam) {
const avg = rule['recentValues' + ck].reduce((a, b) => a + b, 0) / rule['recentValues' + ck].length;
if (avg >= rule.threshold) resourceTriggered = true;
}
break;
}
if (resourceTriggered) {
const unit = resource === 'cpu_temp' ? '°C' : '%';
exceededResources.push(`${resourceNames[resource]}: ${value.toFixed(1)}${unit} (阈值: ${rule.threshold}${unit})`);
}
}
// 保存更新的规则数据
saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r));
if (!shouldTrigger ) continue;
if (exceededResources.length === 0 ) continue;
// 发送邮件
try {
@@ -1779,7 +1902,7 @@
body: JSON.stringify({
to: rule.email,
subject: `[系统告警] ${rule.name}`,
body: `${rule.name} 触发告警\n\n${rule.resource.toUpperCase()} 使用率: ${value.toFixed(1 )}% \n阈值: ${rule.threshold}% \n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
body: `${rule.name} 触发告警\n\n${exceededResources.join('\n' )}\n\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
})
});
@@ -1794,6 +1917,65 @@
}
}
// ==================== 测试邮件发送 ====================
function showTestEmailModal() {
document.getElementById('testEmailAddress').value = '';
document.getElementById('testEmailSubject').value = '[测试邮件] 项目服务管理面板';
document.getElementById('testEmailBody').value = '这是一封测试邮件,用于验证邮件通知功能是否正常工作。\n\n发送自: 项目服务管理面板\n时间: ' + new Date().toLocaleString();
document.getElementById('testEmailResult').classList.add('hidden');
document.getElementById('testEmailModal').classList.remove('hidden');
}
function closeTestEmailModal() {
document.getElementById('testEmailModal').classList.add('hidden');
}
async function sendTestEmail(event) {
event.preventDefault();
const email = document.getElementById('testEmailAddress').value.trim();
const subject = document.getElementById('testEmailSubject').value.trim();
const body = document.getElementById('testEmailBody').value.trim();
const resultEl = document.getElementById('testEmailResult');
const submitBtn = event.target.querySelector('button[type="submit"]');
if (!email) {
alert('请输入收件邮箱');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> 发送中...';
resultEl.classList.add('hidden');
try {
const res = await fetch('/api/email/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: email, subject, body })
});
const data = await res.json();
if (res.ok) {
resultEl.className = 'mb-3 p-3 rounded-lg bg-green-500/20 text-green-400 text-sm';
resultEl.innerHTML = `<i class="ri-checkbox-circle-line"></i> 测试邮件发送成功!<br>收件人: ${email}`;
} else {
resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 发送失败: ${data.error || '未知错误'}`;
}
resultEl.classList.remove('hidden');
} catch (e) {
resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 网络错误: ${e.message}`;
resultEl.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="ri-send-plane-line"></i> 发送测试';
}
}
function showAddServiceModal() {
document.getElementById('serviceName').value = '';
document.getElementById('servicePort').value = '';