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:
2025-12-11 22:20:29 +08:00
parent 0ccddd8c63
commit 29d4bdfbcb
5 changed files with 463 additions and 14 deletions

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Microsoft YaHei', Arial, sans-serif; background-color: #f5f5f5;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 30px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<!-- 头部 -->
<tr>
<td style="background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">知识管理平台</h1>
<p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 14px;">邮箱绑定验证</p>
</td>
</tr>
<!-- 内容 -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #333; font-size: 16px; margin: 0 0 20px 0;">
您好,<strong>{{ username }}</strong>
</p>
<p style="color: #666; font-size: 14px; line-height: 1.8; margin: 0 0 25px 0;">
您正在绑定此邮箱到您的账号。请点击下方按钮完成验证:
</p>
<!-- 验证按钮 -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 20px 0;">
<a href="{{ verify_url }}" style="display: inline-block; background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: #ffffff; text-decoration: none; padding: 15px 40px; border-radius: 30px; font-size: 16px; font-weight: bold; box-shadow: 0 4px 15px rgba(52, 152, 219, 0.4);">
验证邮箱
</a>
</td>
</tr>
</table>
<!-- 链接备用 -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; margin: 25px 0;">
<p style="color: #666; font-size: 12px; margin: 0 0 10px 0;">
如果按钮无法点击,请复制以下链接到浏览器打开:
</p>
<p style="color: #3498db; font-size: 12px; word-break: break-all; margin: 0;">
{{ verify_url }}
</p>
</div>
<!-- 安全提示 -->
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 25px 0; border-radius: 0 5px 5px 0;">
<p style="color: #856404; font-size: 13px; margin: 0;">
<strong>安全提示:</strong>此链接1小时内有效。如果这不是您的操作请忽略此邮件。
</p>
</div>
<p style="color: #999; font-size: 13px; margin: 20px 0 0 0;">
绑定成功后,您将可以通过此邮箱接收任务完成通知。
</p>
</td>
</tr>
<!-- 底部 -->
<tr>
<td style="background: #f8f9fa; padding: 20px 30px; border-radius: 0 0 10px 10px; text-align: center;">
<p style="color: #999; font-size: 12px; margin: 0;">
此邮件由系统自动发送,请勿直接回复。
</p>
<p style="color: #ccc; font-size: 11px; margin: 10px 0 0 0;">
&copy; 知识管理平台
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -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) {