You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮件批量通知工具</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
margin: 0; padding: 24px 16px;
background: #0b0f14;
color: #e5e7eb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial;
min-height: 100vh;
}
.container { max-width: 1200px; margin: auto; }
.card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
padding: 18px;
margin-bottom: 14px;
}
h2 { margin: 0 0 10px 0; font-size: 20px; }
h3 { margin: 10px 0 6px 0; font-size: 16px; }
.badge {
display: inline-block;
padding: 3px 10px; font-size: 12px;
border-radius: 999px;
background: rgba(59,130,246,0.15);
border: 1px solid rgba(59,130,246,0.35);
color: #60a5fa;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 10px;
}
.grid3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
margin-top: 10px;
}
input, textarea, select {
width: 100%; padding: 10px 12px;
border-radius: 10px;
background: #111827;
border: 1px solid rgba(255,255,255,0.12);
color: #e5e7eb;
outline: none; font-size: 14px; font-family: inherit;
}
textarea { min-height: 400px; resize: vertical; }
input:focus, textarea:focus { border-color: #2563eb; }
button {
margin-top: 12px; padding: 10px 14px;
border: none; border-radius: 10px;
cursor: pointer; font-weight: 600; font-size: 14px; margin-right: 8px;
}
.btn-primary { background: #2563eb; color: #fff; }
.btn-primary:hover { background: #1d4ed8; }
.btn-green { background: #22c55e; color: #0b0f14; }
.btn-green:hover { background: #16a34a; }
.btn-gray { background: #374151; color: #e5e7eb; }
.btn-gray:hover { background: #4b5563; }
.hint { font-size: 12px; color: #9ca3af; margin-top: 6px; }
/* Progress */
#progress-area { display: none; }
.progress-bar-bg {
width: 100%; height: 20px;
background: #1f2937; border-radius: 999px;
overflow: hidden; margin: 8px 0;
}
.progress-bar-fill {
height: 100%; width: 0%;
background: linear-gradient(90deg, #2563eb, #22c55e);
border-radius: 999px;
transition: width 0.5s;
}
.stat-row { display: flex; gap: 20px; font-size: 14px; margin: 6px 0; }
.stat-item { display: flex; align-items: center; gap: 4px; }
#result-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
#result-table th, #result-table td {
padding: 6px 10px; border-bottom: 1px solid rgba(255,255,255,0.06);
text-align: left;
}
.status-success { color: #22c55e; }
.status-failed { color: #ef4444; }
/* Toast & Loading */
#toast {
position: fixed; bottom: 30px; left: 50%;
transform: translateX(-50%);
padding: 10px 14px; border-radius: 10px;
font-size: 13px; display: none; z-index: 9999;
}
#loading {
display: none; position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
align-items: center; justify-content: center; z-index: 9998;
}
.spinner {
width: 40px; height: 40px;
border: 4px solid #374151;
border-top: 4px solid #60a5fa;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { 0%{transform:rotate(0)} 100%{transform:rotate(360deg)} }
@media (max-width: 600px) {
.grid, .grid3 { grid-template-columns: 1fr; }
body { padding: 16px 12px; }
textarea { min-height: 280px; }
button { width: 100%; margin-right: 0; margin-bottom: 8px; }
}
</style>
</head>
<body data-task-id="{{task_id or ""}}">
<div class="container">
<form method="post" action="/tool/mail_notify" enctype="multipart/form-data" id="sendForm">
<input type="hidden" name="action" id="action" value="send">
<div class="card">
<h2>📧 邮件批量通知工具 <span class="badge">SMTP · TXT Batch</span></h2>
<div class="hint">上传 TXT → 自动解析账号密码 → 批量发送邮件通知(含自动重试 × 3</div>
</div>
<div class="card">
<h3>🔧 SMTP 配置</h3>
<div class="grid3">
<input name="smtp_server" placeholder="SMTP服务器" value="{{smtp_server or 'smtp.ctvit.com.cn'}}">
<input name="smtp_port" placeholder="端口" value="{{smtp_port or '25'}}">
<input name="reply_to" placeholder="回复地址(可选)" value="{{reply_to or ''}}">
</div>
</div>
<div class="card">
<h3>📬 发件人</h3>
<div class="grid3">
<input name="sender_email" placeholder="发件邮箱" value="{{sender_email or ''}}">
<input name="sender_pass" type="password" placeholder="SMTP密码 / 授权码" value="" autocomplete="off">
<input name="subject" placeholder="邮件主题" value="{{subject or '账号及初始口令通知'}}">
</div>
<div class="hint">🔒 密码仅用于本次请求,不回传、不存储</div>
</div>
<div class="card">
<h3>📝 邮件模板</h3>
<textarea name="template_content">{% raw %}尊敬的老师/同事:
您好!
根据系统账号开通安排,您的账号已创建完成。现将相关登录信息通知如下,请妥善保存并注意账号安全。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
账号(邮箱):{{email}}
初始口令:{{password}}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请在首次登录后及时修改初始口令,并妥善保管个人账号信息。
此致
敬礼!
信息技术支持中心
{% endraw %}</textarea>
<div class="hint">支持变量:<code>{<!-- -->{email}}</code><code>{<!-- -->{password}}</code></div>
</div>
<div class="card">
<h3>📎 用户列表</h3>
<input type="file" name="user_file" id="user_file" required>
<div class="hint">TXT格式每行"邮箱 密码"(空格 / | / , / tab 均支持)</div>
<br>
<button class="btn-primary" type="submit" onclick="setAction('send')">🚀 开始群发</button>
<button class="btn-green" type="submit" onclick="setAction('test')">🧪 测试发送(发给自己)</button>
</div>
</form>
{% if message %}
<div class="card"><div>{{message}}</div></div>
{% endif %}
<!-- ========== 进度区域(异步群发时显示) ========== -->
<div id="progress-area" class="card">
<h3>⏳ 发送进度</h3>
<div class="stat-row">
<span class="stat-item">📤 总计:<strong id="p-total">0</strong></span>
<span class="stat-item">✅ 成功:<strong id="p-success" style="color:#22c55e">0</strong></span>
<span class="stat-item">❌ 失败:<strong id="p-failed" style="color:#ef4444">0</strong></span>
<span class="stat-item">📶 进度:<strong id="p-pct">0%</strong></span>
</div>
<div class="progress-bar-bg">
<div id="progress-fill" class="progress-bar-fill"></div>
</div>
<table id="result-table">
<thead><tr><th>邮箱</th><th>状态</th></tr></thead>
<tbody id="result-body"></tbody>
</table>
</div>
</div>
<div id="loading"><div class="spinner"></div></div>
<div id="toast"></div>
<script>
let pollTimer = null;
function setAction(type){
document.getElementById("action").value = type;
}
function showToast(msg, color){
const t = document.getElementById("toast");
t.innerText = msg;
t.style.background = color || "rgba(34,197,94,0.95)";
t.style.display = "block";
setTimeout(() => t.style.display = "none", 3000);
}
// 异步任务进度轮询
function startPoll(taskId){
const area = document.getElementById("progress-area");
area.style.display = "block";
document.getElementById("result-body").innerHTML = "";
document.getElementById("progress-fill").style.width = "0%";
if(pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
fetch("/tool/mail_notify/progress/" + taskId)
.then(r => r.json())
.then(d => {
if(d.error){
clearInterval(pollTimer);
return;
}
const total = d.total || 0;
const done = d.current || 0;
const success = d.success || 0;
const failed = d.failed || 0;
const pct = total > 0 ? Math.round(done / total * 100) : 0;
document.getElementById("p-total").innerText = total;
document.getElementById("p-success").innerText = success;
document.getElementById("p-failed").innerText = failed;
document.getElementById("p-pct").innerText = pct + "%";
document.getElementById("progress-fill").style.width = pct + "%";
// 结果列表
const results = d.results || [];
let html = "";
for(let i = results.length - 20; i < results.length; i++){
const r = results[i];
if(!r) continue;
const cls = r.status === "success" ? "status-success" : "status-failed";
html += "<tr><td>" + r.email + "</td><td class='" + cls + "'>" + r.status + "</td></tr>";
}
document.getElementById("result-body").innerHTML = html;
// 完成
if(d.status === "done" || d.status === "error"){
clearInterval(pollTimer);
pollTimer = null;
if(d.error){
showToast("❌ 发送异常:" + d.error, "rgba(239,68,68,0.95)");
} else if(failed > 0) {
showToast("✅ 发送完成:成功 " + success + " / 失败 " + failed, "rgba(34,197,94,0.95)");
} else {
showToast("🎉 全部发送成功!共 " + success + " 封", "rgba(34,197,94,0.95)");
}
}
})
.catch(e => {
// 忽略网络错误,继续轮询
});
}, 1500);
}
// 页面加载时检测 task_id 自动启动轮询
document.addEventListener("DOMContentLoaded", function(){
const taskId = document.body.getAttribute("data-task-id");
if(taskId && taskId.length === 32){
startPoll(taskId);
}
});
// 提交表单时显示 loading
document.getElementById("sendForm").addEventListener("submit", function(){
document.getElementById("loading").style.display = "flex";
});
</script>
</body>
</html>