feat: 添加邮件功能第五阶段 - 用户邮箱绑定
1. 添加邮箱绑定验证邮件模板 (templates/email/bind_email.html) 2. 在email_service.py中添加: - send_bind_email_verification() 发送绑定验证邮件 - verify_bind_email_token() 验证绑定Token 3. 在database.py中添加: - update_user_email() 更新用户邮箱 4. 在app.py中添加API: - GET /api/user/email - 获取用户邮箱信息 - POST /api/user/bind-email - 发送绑定验证邮件 - GET /api/verify-bind-email/<token> - 验证绑定Token - POST /api/user/unbind-email - 解绑邮箱 5. 更新templates/index.html: - 将"修改密码"弹窗改为"个人设置" - 添加邮箱绑定/解绑功能UI - 显示邮箱状态(未绑定/待验证/已验证) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -742,27 +742,54 @@
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<div class="modal-overlay" id="changePasswordModal">
|
||||
<div class="modal" style="max-width: 400px;">
|
||||
<div class="modal" style="max-width: 450px;">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">修改密码</h3>
|
||||
<h3 class="modal-title">个人设置</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">当前密码</label>
|
||||
<input type="password" class="form-input" id="currentPassword" placeholder="请输入当前密码">
|
||||
<!-- 邮箱绑定 -->
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span style="font-weight: bold; color: #333;">邮箱绑定</span>
|
||||
<span id="emailStatus" style="font-size: 12px;"></span>
|
||||
</div>
|
||||
<div id="emailBindSection">
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="email" class="form-input" id="bindEmail" placeholder="请输入邮箱地址" style="flex: 1;">
|
||||
<button class="btn btn-primary" id="btnBindEmail" onclick="bindEmail()" style="white-space: nowrap;">绑定</button>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 8px;">
|
||||
绑定邮箱后可接收任务完成通知
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailBoundSection" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span id="boundEmail" style="color: #333;"></span>
|
||||
<button class="btn btn-text" onclick="unbindEmail()" style="color: #e74c3c;">解绑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">新密码</label>
|
||||
<input type="password" class="form-input" id="newPassword" placeholder="请输入新密码(至少6位)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">确认新密码</label>
|
||||
<input type="password" class="form-input" id="confirmPassword" placeholder="请再次输入新密码">
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<div style="font-weight: bold; color: #333; margin-bottom: 15px;">修改密码</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">当前密码</label>
|
||||
<input type="password" class="form-input" id="currentPassword" placeholder="请输入当前密码">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">新密码</label>
|
||||
<input type="password" class="form-input" id="newPassword" placeholder="请输入新密码(至少6位)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">确认新密码</label>
|
||||
<input type="password" class="form-input" id="confirmPassword" placeholder="请再次输入新密码">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="changePassword()" style="width: 100%;">确认修改密码</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-text" onclick="closeModal('changePasswordModal')">取消</button>
|
||||
<button class="btn btn-primary" onclick="changePassword()">确认修改</button>
|
||||
<button class="btn btn-text" onclick="closeModal('changePasswordModal')">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2047,6 +2074,111 @@
|
||||
.catch(() => showToast('网络错误', 'error'));
|
||||
}
|
||||
|
||||
// 邮箱绑定相关
|
||||
function loadUserEmail() {
|
||||
fetch('/api/user/email')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const bindSection = document.getElementById('emailBindSection');
|
||||
const boundSection = document.getElementById('emailBoundSection');
|
||||
const statusSpan = document.getElementById('emailStatus');
|
||||
const boundEmail = document.getElementById('boundEmail');
|
||||
const bindInput = document.getElementById('bindEmail');
|
||||
|
||||
if (data.email && data.email_verified) {
|
||||
bindSection.style.display = 'none';
|
||||
boundSection.style.display = 'block';
|
||||
boundEmail.textContent = data.email;
|
||||
statusSpan.innerHTML = '<span style="color: #27ae60;">已验证</span>';
|
||||
} else if (data.email) {
|
||||
bindSection.style.display = 'block';
|
||||
boundSection.style.display = 'none';
|
||||
bindInput.value = data.email;
|
||||
statusSpan.innerHTML = '<span style="color: #f39c12;">待验证</span>';
|
||||
} else {
|
||||
bindSection.style.display = 'block';
|
||||
boundSection.style.display = 'none';
|
||||
bindInput.value = '';
|
||||
statusSpan.innerHTML = '<span style="color: #999;">未绑定</span>';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function bindEmail() {
|
||||
const email = document.getElementById('bindEmail').value.trim();
|
||||
if (!email) {
|
||||
showToast('请输入邮箱地址', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showToast('请输入有效的邮箱地址', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnBindEmail');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '发送中...';
|
||||
|
||||
fetch('/api/user/bind-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email })
|
||||
})
|
||||
.then(r => r.json().then(data => ({ ok: r.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '绑定';
|
||||
if (ok && data.success) {
|
||||
showToast('验证邮件已发送,请查收', 'success');
|
||||
document.getElementById('emailStatus').innerHTML = '<span style="color: #f39c12;">待验证</span>';
|
||||
} else {
|
||||
showToast(data.error || '发送失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '绑定';
|
||||
showToast('网络错误', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function unbindEmail() {
|
||||
if (!confirm('确定要解绑邮箱吗?解绑后将无法接收任务通知邮件。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/user/unbind-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(r => r.json().then(data => ({ ok: r.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
if (ok && data.success) {
|
||||
showToast('邮箱已解绑', 'success');
|
||||
loadUserEmail();
|
||||
} else {
|
||||
showToast(data.error || '解绑失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('网络错误', 'error'));
|
||||
}
|
||||
|
||||
// 打开设置弹窗时加载邮箱信息
|
||||
const originalOpenModal = window.openModal;
|
||||
window.openModal = function(modalId) {
|
||||
if (modalId === 'changePasswordModal') {
|
||||
loadUserEmail();
|
||||
}
|
||||
if (originalOpenModal) {
|
||||
originalOpenModal(modalId);
|
||||
} else {
|
||||
document.getElementById(modalId).classList.add('active');
|
||||
}
|
||||
};
|
||||
|
||||
// 点击overlay关闭弹窗
|
||||
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
|
||||
Reference in New Issue
Block a user