diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py index 0ed2ba3..01e7120 100644 --- a/backend/app/api/endpoints/auth.py +++ b/backend/app/api/endpoints/auth.py @@ -5,7 +5,7 @@ from typing import Tuple from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session - +import random from app.api.dependencies import get_db from app.core.security import ( create_access_token, @@ -52,10 +52,10 @@ def _normalize_email(email: str) -> str: def _build_verification_email(code: str, purpose_text: str, expire_minutes: int) -> str: return f"""
-

InsightRadar Email Verification

-

Your {purpose_text} verification code is:

+

InsightRadar 邮箱验证

+

您的{purpose_text}验证码是:

{code}

-

The code is valid for {expire_minutes} minutes. Do not share it with others.

+

该验证码在 {expire_minutes} 分钟内有效。请勿泄露给他人。

""" @@ -152,8 +152,8 @@ async def send_register_code(payload: RegisterCodeSendRequest, db: Session = Dep email_sent = await send_html_email( to_email=email, - subject="InsightRadar Registration Code", - html_content=_build_verification_email(code, "registration", REGISTER_CODE_EXPIRE_MINUTES), + subject=f"【{code}】InsightRadar 注册验证码", + html_content=_build_verification_email(code, "注册", REGISTER_CODE_EXPIRE_MINUTES), ) if not email_sent: code_record.is_used = True @@ -186,8 +186,8 @@ async def send_login_code(payload: LoginCodeSendRequest, db: Session = Depends(g email_sent = await send_html_email( to_email=email, - subject="InsightRadar Login Code", - html_content=_build_verification_email(code, "login", LOGIN_CODE_EXPIRE_MINUTES), + subject=f"【{code}】InsightRadar 登录验证码", + html_content=_build_verification_email(code, "登录", LOGIN_CODE_EXPIRE_MINUTES), ) if not email_sent: code_record.is_used = True diff --git a/backend/app/utils/email_utils.py b/backend/app/utils/email_utils.py index 0dd94a2..5e0a9b7 100644 --- a/backend/app/utils/email_utils.py +++ b/backend/app/utils/email_utils.py @@ -17,7 +17,7 @@ async def send_html_email( to_email: str, subject: str, html_content: str, - sender_name: str = "AI 新闻", + sender_name: str = "AI 热点", sender_email: str = None ) -> bool: """底层纯异步发送邮件工具""" diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index c7c2e74..d8557f6 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -817,7 +817,7 @@ watch(() => route.query.event, (newId) => {
-

爬虫集群状态

+

爬虫状态

{{ stats.active_sources }} 个源运行中 diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 96cda2f..d02b31c 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -19,6 +19,8 @@ const showPassword = ref(false) const errorMessage = ref('') const successMessage = ref('') const countdown = ref(0) +const isSubmitting = ref(false) +const isSendingCode = ref(false) const form = reactive({ email: '', @@ -83,6 +85,7 @@ async function handleSendLoginCode() { return } + isSendingCode.value = true try { const result = await authStore.sendLoginVerificationCode(form.email.trim()) successMessage.value = result.message || '验证码已发送' @@ -93,6 +96,8 @@ async function handleSendLoginCode() { startCooldown(retryAfter) } errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' + } finally { + isSendingCode.value = false } } @@ -106,6 +111,7 @@ async function handleSubmit() { return } + isSubmitting.value = true try { if (loginMode.value === 'password') { await authStore.loginWithPassword({ @@ -123,6 +129,8 @@ async function handleSubmit() { await router.replace(redirect) } catch (error) { errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试' + } finally { + isSubmitting.value = false } } @@ -146,6 +154,30 @@ onUnmounted(() => {

聚合多平台趋势,自动完成热点归并与摘要。你可以用密码登录,也可以直接使用邮箱验证码快速登录。

+ +
+
+
🚀
+
+ 实时热搜追踪 + 分钟级更新,覆盖微博、知乎、抖音等主流平台 +
+
+
+
🤖
+
+ AI 智能聚类 + 自动识别同源事件,告别重复阅读的信息轰炸 +
+
+
+
+
+ 核心内容摘要 + 一键获取事件全貌,省时省力掌握核心脉络 +
+
+
@@ -204,9 +236,9 @@ onUnmounted(() => { placeholder="请输入密码" autocomplete="current-password" /> -
@@ -222,8 +254,8 @@ onUnmounted(() => { placeholder="请输入 6 位验证码" inputmode="numeric" /> - @@ -251,7 +283,7 @@ onUnmounted(() => { @@ -277,13 +309,38 @@ onUnmounted(() => { .brand-panel { flex: 1; display: none; - background: linear-gradient(135deg, var(--bg-surface), var(--bg-base)); + background: var(--bg-surface); border-right: 1px solid var(--border-subtle); padding: 80px; position: relative; overflow: hidden; } +/* 动态网格背景 */ +.brand-panel::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background-image: + linear-gradient(rgba(128, 128, 128, 0.15) 1px, transparent 1px), + linear-gradient(90deg, rgba(128, 128, 128, 0.15) 1px, transparent 1px); + background-size: 32px 32px; + background-position: center; + z-index: 0; + pointer-events: none; + animation: grid-move 20s linear infinite; + mask-image: radial-gradient(circle at center, black 40%, transparent 80%); + -webkit-mask-image: radial-gradient(circle at center, black 40%, transparent 80%); +} + +@keyframes grid-move { + 0% { transform: translate(0, 0); } + 100% { transform: translate(32px, 32px); } +} + @media (min-width: 900px) { .brand-panel { display: flex; @@ -332,6 +389,60 @@ onUnmounted(() => { line-height: 1.7; color: var(--text-secondary); font-weight: 500; + margin-bottom: 40px; +} + +.feature-list { + display: flex; + flex-direction: column; + gap: 24px; + margin-top: 32px; +} + +.feature-item { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-lg); + transition: transform 0.3s ease, background 0.3s ease; +} + +.feature-item:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.06); +} + +.feature-icon { + font-size: 24px; + background: rgba(255, 255, 255, 0.1); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + flex-shrink: 0; +} + +.feature-text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.feature-text strong { + color: var(--text-primary); + font-size: 16px; + font-weight: 600; +} + +.feature-text span { + color: var(--text-secondary); + font-size: 14px; + line-height: 1.5; } .ambient-glow { diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue index 0eede4c..1d445e7 100644 --- a/frontend/src/views/RegisterView.vue +++ b/frontend/src/views/RegisterView.vue @@ -24,6 +24,8 @@ const showConfirmPassword = ref(false) const errorMessage = ref('') const successMessage = ref('') const countdown = ref(0) +const isSubmitting = ref(false) +const isSendingCode = ref(false) let countdownTimer: ReturnType | null = null @@ -71,6 +73,7 @@ async function handleSendCode() { errorMessage.value = '请先输入有效邮箱' return } + isSendingCode.value = true try { const result = await authStore.sendCode(form.email.trim()) successMessage.value = result.message || '验证码已发送' @@ -81,6 +84,8 @@ async function handleSendCode() { startCooldown(retryAfter) } errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' + } finally { + isSendingCode.value = false } } @@ -97,6 +102,7 @@ async function handleSubmit() { successMessage.value = '' if (errorMessage.value) return + isSubmitting.value = true try { await authStore.registerAccount({ email: form.email.trim(), @@ -107,6 +113,8 @@ async function handleSubmit() { await router.replace('/') } catch (error) { errorMessage.value = error instanceof Error ? error.message : '注册失败,请稍后重试' + } finally { + isSubmitting.value = false } } @@ -130,6 +138,30 @@ onUnmounted(() => {

只需几秒钟即可创建您的专属账号,体验下一代全网事件聚合与 AI 洞察服务。

+ +
+
+
🔍
+
+ 个性化兴趣订阅 + 定制你的专属信息流,只看你关心的动态 +
+
+
+
📩
+
+ 每日邮件简报 + 定时推送当日核心热点,重要信息不遗漏 +
+
+
+
📊
+
+ 多维度事件追踪 + 掌握事件起因、发展脉络与各方观点 +
+
+
@@ -171,10 +203,10 @@ onUnmounted(() => { @@ -189,9 +221,9 @@ onUnmounted(() => { :type="showPassword ? 'text' : 'password'" placeholder="至少 8 位字符" /> - @@ -258,11 +290,36 @@ onUnmounted(() => { .brand-panel { flex: 1; display: none; - background: linear-gradient(135deg, var(--bg-surface), var(--bg-base)); + background: var(--bg-surface); border-right: 1px solid var(--border-subtle); padding: 80px; position: relative; overflow: hidden; + /* 确保伪元素不会遮挡内容,且以正常图层渲染 */ + isolation: isolate; +} + +/* 动态网格线背景,营造科技空间感 */ +.brand-panel::before { + content: ""; + position: absolute; + top: -50%; left: -50%; right: -50%; bottom: -50%; + background-image: + linear-gradient(rgba(128, 128, 128, 0.15) 1px, transparent 1px), + linear-gradient(90deg, rgba(128, 128, 128, 0.15) 1px, transparent 1px); + background-size: 32px 32px; + background-position: center center; + z-index: -1; + pointer-events: none; + /* 添加缓慢移动动画 */ + animation: grid-move 20s linear infinite; + mask-image: radial-gradient(circle at center, black 40%, transparent 80%); + -webkit-mask-image: radial-gradient(circle at center, black 40%, transparent 80%); +} + +@keyframes grid-move { + 0% { transform: translate(0, 0); } + 100% { transform: translate(32px, 32px); } } @media (min-width: 900px) { @@ -314,6 +371,60 @@ onUnmounted(() => { line-height: 1.7; color: var(--text-secondary); font-weight: 500; + margin-bottom: 40px; +} + +.feature-list { + display: flex; + flex-direction: column; + gap: 24px; + margin-top: 32px; +} + +.feature-item { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-lg); + transition: transform 0.3s ease, background 0.3s ease; +} + +.feature-item:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.06); +} + +.feature-icon { + font-size: 24px; + background: rgba(255, 255, 255, 0.1); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + flex-shrink: 0; +} + +.feature-text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.feature-text strong { + color: var(--text-primary); + font-size: 16px; + font-weight: 600; +} + +.feature-text span { + color: var(--text-secondary); + font-size: 14px; + line-height: 1.5; } .ambient-glow {