feat: 实现邮箱验证码登录和腾讯云SES集成
- 实现邮箱验证码登录功能,支持自动注册新用户 - 修复验证码生成逻辑,确保前后端验证码一致 - 添加腾讯云SES webhook回调接口,支持6种邮件事件 - 配置ngrok内网穿透支持,允许外部访问 - 优化登录页面UI,采用全屏背景和居中布局 - 清理调试代码和未使用的导入 - 添加完整的配置文档和测试脚本
This commit is contained in:
@@ -4,7 +4,7 @@ import router from '@/router'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://api.yourdomain.com:8080/api',
|
||||
baseURL: 'http://localhost:8080/api',
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
|
||||
@@ -82,7 +82,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/profile' // 重定向到个人主页
|
||||
redirect: '/welcome' // 重定向到欢迎页面
|
||||
},
|
||||
{
|
||||
path: '/welcome',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
// 这里可以添加查看订单详情的逻辑
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user