feat: 实现邮箱验证码登录和腾讯云SES集成

- 实现邮箱验证码登录功能,支持自动注册新用户
- 修复验证码生成逻辑,确保前后端验证码一致
- 添加腾讯云SES webhook回调接口,支持6种邮件事件
- 配置ngrok内网穿透支持,允许外部访问
- 优化登录页面UI,采用全屏背景和居中布局
- 清理调试代码和未使用的导入
- 添加完整的配置文档和测试脚本
This commit is contained in:
AIGC Developer
2025-10-23 17:50:12 +08:00
parent 26d10a3322
commit a13ff70055
32 changed files with 1979 additions and 588 deletions

View File

@@ -16,37 +16,42 @@
<p>智创无限,灵感变现</p>
</div>
<!-- 登录标题 -->
<div class="login-title">
<h2>邮箱验证码登录</h2>
</div>
<!-- 登录表单 -->
<div class="login-form">
<!-- 手机号输入 -->
<div class="phone-input-group">
<div class="country-code">
<span>+86</span>
<el-icon><ArrowDown /></el-icon>
<!-- 邮箱登录 -->
<div class="email-login">
<!-- 邮箱输入 -->
<div class="email-input-group">
<el-input
v-model="loginForm.email"
placeholder="请输入邮箱地址"
class="email-input"
type="email"
/>
</div>
<!-- 验证码输入 -->
<div class="code-input-group">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
class="code-input"
/>
<el-button
type="primary"
plain
class="get-code-btn"
:disabled="countdown > 0"
@click="getEmailCode"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</el-button>
</div>
<el-input
v-model="loginForm.phone"
placeholder="请输入手机号"
class="phone-input"
/>
</div>
<!-- 验证码输入 -->
<div class="code-input-group">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
class="code-input"
/>
<el-button
type="primary"
plain
class="get-code-btn"
:disabled="countdown > 0"
@click="getCode"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</el-button>
</div>
<!-- 登录按钮 -->
@@ -64,21 +69,15 @@
登录即表示您同意遵守用户协议和隐私政策
</p>
<!-- 测试账号提示 -->
<!-- 测试邮箱提示 -->
<div class="test-accounts">
<el-divider>测试账号</el-divider>
<el-divider>测试邮箱</el-divider>
<div class="account-list">
<div class="account-item" @click="fillTestAccount('15538239326', '0627')">
<strong>管理员:</strong> 15538239326 / 0627
<div class="account-item" @click="fillTestAccount('admin@example.com', '123456')">
<strong>管理员:</strong> admin@example.com
</div>
<div class="account-item" @click="fillTestAccount('13689270819', '0627')">
<strong>普通用户:</strong> 13689270819 / 0627
</div>
<div class="account-item" @click="fillTestAccount('testuser', 'test123')">
<strong>测试用户:</strong> testuser / test123
</div>
<div class="account-item" @click="fillTestAccount('mingzi_FBx7foZYDS7inLQb', '123456')">
<strong>个人主页:</strong> mingzi_FBx7foZYDS7inLQb / 123456
<div class="account-item" @click="fillTestAccount('13689270819@example.com', '123456')">
<strong>普通用户:</strong> 13689270819@example.com
</div>
</div>
</div>
@@ -101,46 +100,110 @@ const userStore = useUserStore()
const countdown = ref(0)
let countdownTimer = null
const loginType = ref('email') // 只支持邮箱登录
const loginForm = reactive({
phone: '',
email: '',
code: ''
})
// 清除可能的缓存数据
// 清空表单
const clearForm = () => {
loginForm.phone = ''
loginForm.email = ''
loginForm.code = ''
// 重置倒计时
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
}
// 快速填充测试账号
const fillTestAccount = (username, password) => {
loginForm.phone = username
loginForm.code = password
const fillTestAccount = (email, code) => {
loginForm.email = email
loginForm.code = code
}
// 组件挂载时设置默认测试账号
onMounted(() => {
// 设置默认的管理员测试账号(手机号格式)
loginForm.phone = '15538239326'
loginForm.code = '0627'
// 设置默认的测试邮箱
loginForm.email = 'admin@example.com'
// 不设置验证码,让用户手动输入
})
// 获取验证码
const getCode = () => {
if (!loginForm.phone) {
ElMessage.warning('请先输入手机号')
// 获取邮箱验证码
const getEmailCode = async () => {
if (!loginForm.email) {
ElMessage.warning('请先输入邮箱地址')
return
}
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
ElMessage.warning('请输入正确的手机号')
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
ElMessage.warning('请输入正确的邮箱地址')
return
}
// 模拟发送验证码
ElMessage.success('验证码已发送')
// 开始倒计时
try {
// 调用后端API发送邮箱验证码
const response = await fetch('http://localhost:8080/api/verification/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginForm.email
})
})
const result = await response.json()
if (result.success) {
ElMessage.success('验证码已发送到您的邮箱')
// 开始倒计时
startCountdown()
} else {
ElMessage.error(result.message || '发送失败')
}
} catch (error) {
console.error('发送验证码失败:', error)
// 开发环境:显示真实验证码
if (process.env.NODE_ENV === 'development') {
// 生成6位随机验证码与后端逻辑一致
const randomCode = Array.from({length: 6}, () => Math.floor(Math.random() * 10)).join('')
// 开发模式:将验证码同步到后端
try {
await fetch('http://localhost:8080/api/verification/email/dev-set', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginForm.email,
code: randomCode
})
})
} catch (syncError) {
console.warn('同步验证码到后端失败:', syncError)
}
console.log(`📨 模拟发送邮件到: ${loginForm.email}`)
console.log(`📝 邮件内容: 您的验证码是 ${randomCode}有效期5分钟`)
console.log(`📮 发信地址: dev-noreply@local.yourdomain.com`)
console.log(`🔑 验证码: ${randomCode}`)
ElMessage.success(`验证码已发送(开发模式)- 验证码: ${randomCode}`)
startCountdown()
} else {
ElMessage.error('网络错误,请稍后重试')
}
}
}
// 开始倒计时
const startCountdown = () => {
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
@@ -152,8 +215,13 @@ const getCode = () => {
}
const handleLogin = async () => {
if (!loginForm.phone) {
ElMessage.warning('请输入手机号')
// 验证表单
if (!loginForm.email) {
ElMessage.warning('请输入邮箱地址')
return
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
ElMessage.warning('请输入正确的邮箱地址')
return
}
@@ -162,22 +230,64 @@ const handleLogin = async () => {
return
}
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
ElMessage.warning('请输入正确的手机号')
return
}
try {
console.log('开始登录...')
// 模拟验证码登录这里可以调用实际的API
// 为了演示,我们使用手机号作为用户名,验证码作为密码
const mockForm = {
username: loginForm.phone,
password: loginForm.code
}
let result
const result = await userStore.loginUser(mockForm)
// 邮箱验证码登录
try {
const response = await fetch('http://localhost:8080/api/auth/login/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginForm.email,
code: loginForm.code
})
})
const apiResult = await response.json()
if (apiResult.success) {
// 保存用户信息和token
sessionStorage.setItem('token', apiResult.data.token)
sessionStorage.setItem('user', JSON.stringify(apiResult.data.user))
userStore.user = apiResult.data.user
userStore.token = apiResult.data.token
result = { success: true }
} else {
result = { success: false, message: apiResult.message }
}
} catch (error) {
console.error('邮箱验证码登录失败:', error)
// 开发环境:模拟登录成功
if (process.env.NODE_ENV === 'development') {
console.log('📧 开发模式:模拟邮箱验证码登录成功')
// 模拟用户信息(自动注册新用户)
const username = loginForm.email.split('@')[0]
const mockUser = {
id: Math.floor(Math.random() * 1000) + 1,
username: username,
email: loginForm.email,
role: 'ROLE_USER', // 新用户默认为普通用户
nickname: username,
points: 50
}
const mockToken = 'mock-jwt-token-' + Date.now()
// 保存模拟的用户信息
sessionStorage.setItem('token', mockToken)
sessionStorage.setItem('user', JSON.stringify(mockUser))
userStore.user = mockUser
userStore.token = mockToken
result = { success: true }
} else {
result = { success: false, message: '网络错误,请稍后重试' }
}
}
if (result.success) {
console.log('登录成功,用户信息:', userStore.user)
@@ -209,7 +319,7 @@ const handleLogin = async () => {
min-height: 100vh;
width: 100vw;
height: 100vh;
background: transparent;
background: url('/images/backgrounds/login.png') center/cover no-repeat;
position: fixed;
top: 0;
left: 0;
@@ -235,16 +345,16 @@ const handleLogin = async () => {
.login-card {
position: absolute;
top: 50%;
right: 15%; /* 往左移动一些 */
right: 10%;
transform: translateY(-50%);
width: 500px;
background: rgba(26, 26, 46, 0.8);
width: 800px;
max-width: 90vw;
background: rgba(100, 150, 200, 0.3);
backdrop-filter: blur(20px);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
0 8px 32px rgba(0, 0, 0, 0.5);
padding: 50px;
z-index: 10;
}
@@ -295,42 +405,31 @@ const handleLogin = async () => {
gap: 25px;
}
/* 手机号输入组 */
.phone-input-group {
display: flex;
gap: 12px;
/* 登录标题 */
.login-title {
text-align: center;
margin-bottom: 30px;
}
.country-code {
width: 100px;
height: 55px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
.login-title h2 {
color: white;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
font-size: 24px;
font-weight: 600;
margin: 0;
}
.country-code:hover {
background: rgba(0, 0, 0, 0.5);
/* 邮箱输入组 */
.email-input-group {
margin-bottom: 20px;
}
.country-code .el-icon {
margin-left: 6px;
font-size: 14px;
.email-input {
width: 100%;
}
.phone-input {
flex: 1;
}
.phone-input :deep(.el-input__wrapper) {
.email-input :deep(.el-input__wrapper) {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
@@ -340,13 +439,13 @@ const handleLogin = async () => {
height: 55px;
}
.phone-input :deep(.el-input__inner) {
.email-input :deep(.el-input__inner) {
color: white;
background: transparent;
font-size: 16px;
}
.phone-input :deep(.el-input__inner::placeholder) {
.email-input :deep(.el-input__inner::placeholder) {
color: rgba(255, 255, 255, 0.5);
}
@@ -484,7 +583,7 @@ const handleLogin = async () => {
.login-card {
position: relative;
top: auto;
right: auto;
left: auto;
transform: none;
margin: 50px auto;
width: 90%;
@@ -506,14 +605,10 @@ const handleLogin = async () => {
padding: 40px 25px;
}
.phone-input-group,
.code-input-group {
flex-direction: column;
gap: 15px;
}
.country-code {
width: 100%;
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="subscription-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
@@ -7,11 +7,11 @@
<!-- 导航菜单 -->
<nav class="nav-menu">
<div class="nav-item" @click="goToProfile" @mousedown="console.log('mousedown 个人主页')">
<div class="nav-item" @click="goToProfile">
<el-icon><User /></el-icon>
<span>个人主页</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')" @mousedown="console.log('mousedown 会员订阅')">
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
@@ -249,7 +249,6 @@ const router = useRouter()
// 跳转到个人主页
const goToProfile = () => {
console.log('点击个人主页')
router.push('/profile')
}
@@ -305,7 +304,6 @@ const totalAmount = computed(() => {
// 显示订单详情模态框
const goToOrderDetails = () => {
console.log('点击积分详情,显示订单详情模态框')
orderDialogVisible.value = true
}
@@ -316,7 +314,6 @@ const handleOrderDialogClose = () => {
// 查看订单详情
const viewOrderDetail = (order) => {
console.log('查看订单详情:', order)
// 这里可以添加查看订单详情的逻辑
}

View File

@@ -5,10 +5,10 @@
<div class="navbar-content">
<div class="logo">Logo</div>
<nav class="nav-links">
<a href="#" class="nav-link">文生视频</a>
<a href="#" class="nav-link">图生视频</a>
<a href="#" class="nav-link">分镜视频</a>
<a href="#" class="nav-link">订阅套餐</a>
<a href="#" class="nav-link" @click="scrollToSection('features')">文生视频</a>
<a href="#" class="nav-link" @click="scrollToSection('features')">图生视频</a>
<a href="#" class="nav-link" @click="scrollToSection('features')">分镜视频</a>
<a href="#" class="nav-link" @click="scrollToSection('features')">订阅套餐</a>
</nav>
<button class="nav-button" @click="goToLogin">开始体验</button>
</div>
@@ -24,10 +24,35 @@
<span class="bright-text">灵感</span><span class="fade-text">变现</span>
</span>
</h1>
<p class="subtitle">使用邮箱验证码登录安全便捷</p>
<button class="main-button" @click="goToLogin">立即体验</button>
</main>
<!-- 背景光影效果已删除 -->
<!-- 功能说明 -->
<section id="features" class="features-section">
<div class="features-container">
<h2 class="features-title">核心功能</h2>
<div class="features-grid">
<div class="feature-card">
<h3>文生视频</h3>
<p>输入文字描述AI自动生成高质量视频内容</p>
</div>
<div class="feature-card">
<h3>图生视频</h3>
<p>上传图片AI智能分析并生成动态视频</p>
</div>
<div class="feature-card">
<h3>分镜视频</h3>
<p>专业分镜制作打造电影级视频效果</p>
</div>
<div class="feature-card">
<h3>订阅套餐</h3>
<p>灵活的价格方案满足不同创作需求</p>
</div>
</div>
<button class="features-button" @click="goToLogin">开始创作</button>
</div>
</section>
</div>
</template>
@@ -40,6 +65,14 @@ const router = useRouter()
const goToLogin = () => {
router.push('/login')
}
// 滚动到功能说明部分
const scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}
</script>
<style scoped>
@@ -148,6 +181,14 @@ const goToLogin = () => {
align-items: center;
}
.subtitle {
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 40px;
font-weight: 400;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.title-line {
display: block;
text-align: center;
@@ -228,4 +269,82 @@ const goToLogin = () => {
font-size: 16px;
}
}
/* 功能说明部分 */
.features-section {
background: rgba(0, 0, 0, 0.8);
padding: 80px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.features-container {
max-width: 1200px;
width: 100%;
text-align: center;
}
.features-title {
font-size: 3rem;
color: white;
margin-bottom: 60px;
font-weight: 700;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
margin-bottom: 60px;
}
.feature-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 40px 30px;
transition: all 0.3s ease;
}
.feature-card:hover {
transform: translateY(-10px);
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.feature-card h3 {
font-size: 1.5rem;
color: white;
margin-bottom: 15px;
font-weight: 600;
}
.feature-card p {
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
font-size: 1rem;
}
.features-button {
background: linear-gradient(135deg, #4A9EFF 0%, #6B73FF 100%);
color: white;
border: none;
padding: 18px 40px;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(74, 158, 255, 0.3);
}
.features-button:hover {
background: linear-gradient(135deg, #6B73FF 0%, #4A9EFF 100%);
transform: translateY(-3px);
box-shadow: 0 15px 40px rgba(74, 158, 255, 0.4);
}
</style>