界面优化

This commit is contained in:
stardrophere
2026-03-12 19:38:14 +08:00
parent 19a61e6567
commit 6fbcf2c81b
5 changed files with 244 additions and 22 deletions
+8 -8
View File
@@ -5,7 +5,7 @@ from typing import Tuple
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import random
from app.api.dependencies import get_db from app.api.dependencies import get_db
from app.core.security import ( from app.core.security import (
create_access_token, 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: def _build_verification_email(code: str, purpose_text: str, expire_minutes: int) -> str:
return f""" return f"""
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #222;"> <div style="font-family: Arial, sans-serif; line-height: 1.6; color: #222;">
<h2 style="margin-bottom: 12px;">InsightRadar Email Verification</h2> <h2 style="margin-bottom: 12px;">InsightRadar 邮箱验证</h2>
<p>Your {purpose_text} verification code is:</p> <p>您的{purpose_text}验证码是:</p>
<p style="font-size: 28px; font-weight: bold; letter-spacing: 4px; color: #0b57d0;">{code}</p> <p style="font-size: 28px; font-weight: bold; letter-spacing: 4px; color: #0b57d0;">{code}</p>
<p>The code is valid for {expire_minutes} minutes. Do not share it with others.</p> <p>该验证码在 {expire_minutes} 分钟内有效。请勿泄露给他人。</p>
</div> </div>
""" """
@@ -152,8 +152,8 @@ async def send_register_code(payload: RegisterCodeSendRequest, db: Session = Dep
email_sent = await send_html_email( email_sent = await send_html_email(
to_email=email, to_email=email,
subject="InsightRadar Registration Code", subject=f"{code}InsightRadar 注册验证码",
html_content=_build_verification_email(code, "registration", REGISTER_CODE_EXPIRE_MINUTES), html_content=_build_verification_email(code, "注册", REGISTER_CODE_EXPIRE_MINUTES),
) )
if not email_sent: if not email_sent:
code_record.is_used = True 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( email_sent = await send_html_email(
to_email=email, to_email=email,
subject="InsightRadar Login Code", subject=f"{code}InsightRadar 登录验证码",
html_content=_build_verification_email(code, "login", LOGIN_CODE_EXPIRE_MINUTES), html_content=_build_verification_email(code, "登录", LOGIN_CODE_EXPIRE_MINUTES),
) )
if not email_sent: if not email_sent:
code_record.is_used = True code_record.is_used = True
+1 -1
View File
@@ -17,7 +17,7 @@ async def send_html_email(
to_email: str, to_email: str,
subject: str, subject: str,
html_content: str, html_content: str,
sender_name: str = "AI 新闻", sender_name: str = "AI 热点",
sender_email: str = None sender_email: str = None
) -> bool: ) -> bool:
"""底层纯异步发送邮件工具""" """底层纯异步发送邮件工具"""
+1 -1
View File
@@ -817,7 +817,7 @@ watch(() => route.query.event, (newId) => {
<section v-if="stats" class="widget-panel stats-widget"> <section v-if="stats" class="widget-panel stats-widget">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-item"> <div class="stat-item">
<p class="stat-label">爬虫集群状态</p> <p class="stat-label">爬虫状态</p>
<p class="stat-value"> <p class="stat-value">
<span class="status-dot-green"></span> <span class="status-dot-green"></span>
{{ stats.active_sources }} 个源运行中 {{ stats.active_sources }} 个源运行中
+117 -6
View File
@@ -19,6 +19,8 @@ const showPassword = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const successMessage = ref('') const successMessage = ref('')
const countdown = ref(0) const countdown = ref(0)
const isSubmitting = ref(false)
const isSendingCode = ref(false)
const form = reactive({ const form = reactive({
email: '', email: '',
@@ -83,6 +85,7 @@ async function handleSendLoginCode() {
return return
} }
isSendingCode.value = true
try { try {
const result = await authStore.sendLoginVerificationCode(form.email.trim()) const result = await authStore.sendLoginVerificationCode(form.email.trim())
successMessage.value = result.message || '验证码已发送' successMessage.value = result.message || '验证码已发送'
@@ -93,6 +96,8 @@ async function handleSendLoginCode() {
startCooldown(retryAfter) startCooldown(retryAfter)
} }
errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试'
} finally {
isSendingCode.value = false
} }
} }
@@ -106,6 +111,7 @@ async function handleSubmit() {
return return
} }
isSubmitting.value = true
try { try {
if (loginMode.value === 'password') { if (loginMode.value === 'password') {
await authStore.loginWithPassword({ await authStore.loginWithPassword({
@@ -123,6 +129,8 @@ async function handleSubmit() {
await router.replace(redirect) await router.replace(redirect)
} catch (error) { } catch (error) {
errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试' errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试'
} finally {
isSubmitting.value = false
} }
} }
@@ -146,6 +154,30 @@ onUnmounted(() => {
<p class="brand-desc"> <p class="brand-desc">
聚合多平台趋势自动完成热点归并与摘要你可以用密码登录也可以直接使用邮箱验证码快速登录 聚合多平台趋势自动完成热点归并与摘要你可以用密码登录也可以直接使用邮箱验证码快速登录
</p> </p>
<div class="feature-list">
<div class="feature-item">
<div class="feature-icon">🚀</div>
<div class="feature-text">
<strong>实时热搜追踪</strong>
<span>分钟级更新覆盖微博知乎抖音等主流平台</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🤖</div>
<div class="feature-text">
<strong>AI 智能聚类</strong>
<span>自动识别同源事件告别重复阅读的信息轰炸</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon"></div>
<div class="feature-text">
<strong>核心内容摘要</strong>
<span>一键获取事件全貌省时省力掌握核心脉络</span>
</div>
</div>
</div>
</div> </div>
<div class="ambient-glow"></div> <div class="ambient-glow"></div>
</aside> </aside>
@@ -204,9 +236,9 @@ onUnmounted(() => {
placeholder="请输入密码" placeholder="请输入密码"
autocomplete="current-password" autocomplete="current-password"
/> />
<button type="button" class="input-action-btn" @click="showPassword = !showPassword"> <!-- <button type="button" class="input-action-btn" @click="showPassword = !showPassword">
{{ showPassword ? '隐藏' : '显示' }} {{ showPassword ? '隐藏' : '显示' }}
</button> </button> -->
</div> </div>
</div> </div>
@@ -222,8 +254,8 @@ onUnmounted(() => {
placeholder="请输入 6 位验证码" placeholder="请输入 6 位验证码"
inputmode="numeric" inputmode="numeric"
/> />
<button type="button" class="input-action-btn" :disabled="!canSendCode" @click="handleSendLoginCode"> <button type="button" class="input-action-btn" :disabled="!canSendCode || isSendingCode" @click="handleSendLoginCode">
{{ countdown > 0 ? `${countdown}s` : '发送验证码' }} {{ isSendingCode ? '发送中...' : (countdown > 0 ? `${countdown}s` : '发送验证码') }}
</button> </button>
</div> </div>
</div> </div>
@@ -251,7 +283,7 @@ onUnmounted(() => {
</div> </div>
<button class="btn-primary" :disabled="authStore.loading" type="submit"> <button class="btn-primary" :disabled="authStore.loading" type="submit">
{{ authStore.loading ? '登录中...' : loginMode === 'password' ? '密码登录' : '邮箱验证码登录' }} {{ isSubmitting ? '登录中...' : (loginMode === 'password' ? '密码登录' : '邮箱验证码登录') }}
</button> </button>
</form> </form>
@@ -277,13 +309,38 @@ onUnmounted(() => {
.brand-panel { .brand-panel {
flex: 1; flex: 1;
display: none; display: none;
background: linear-gradient(135deg, var(--bg-surface), var(--bg-base)); background: var(--bg-surface);
border-right: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle);
padding: 80px; padding: 80px;
position: relative; position: relative;
overflow: hidden; 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) { @media (min-width: 900px) {
.brand-panel { .brand-panel {
display: flex; display: flex;
@@ -332,6 +389,60 @@ onUnmounted(() => {
line-height: 1.7; line-height: 1.7;
color: var(--text-secondary); color: var(--text-secondary);
font-weight: 500; 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 { .ambient-glow {
+117 -6
View File
@@ -24,6 +24,8 @@ const showConfirmPassword = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const successMessage = ref('') const successMessage = ref('')
const countdown = ref(0) const countdown = ref(0)
const isSubmitting = ref(false)
const isSendingCode = ref(false)
let countdownTimer: ReturnType<typeof setInterval> | null = null let countdownTimer: ReturnType<typeof setInterval> | null = null
@@ -71,6 +73,7 @@ async function handleSendCode() {
errorMessage.value = '请先输入有效邮箱' errorMessage.value = '请先输入有效邮箱'
return return
} }
isSendingCode.value = true
try { try {
const result = await authStore.sendCode(form.email.trim()) const result = await authStore.sendCode(form.email.trim())
successMessage.value = result.message || '验证码已发送' successMessage.value = result.message || '验证码已发送'
@@ -81,6 +84,8 @@ async function handleSendCode() {
startCooldown(retryAfter) startCooldown(retryAfter)
} }
errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试'
} finally {
isSendingCode.value = false
} }
} }
@@ -97,6 +102,7 @@ async function handleSubmit() {
successMessage.value = '' successMessage.value = ''
if (errorMessage.value) return if (errorMessage.value) return
isSubmitting.value = true
try { try {
await authStore.registerAccount({ await authStore.registerAccount({
email: form.email.trim(), email: form.email.trim(),
@@ -107,6 +113,8 @@ async function handleSubmit() {
await router.replace('/') await router.replace('/')
} catch (error) { } catch (error) {
errorMessage.value = error instanceof Error ? error.message : '注册失败,请稍后重试' errorMessage.value = error instanceof Error ? error.message : '注册失败,请稍后重试'
} finally {
isSubmitting.value = false
} }
} }
@@ -130,6 +138,30 @@ onUnmounted(() => {
<p class="brand-desc"> <p class="brand-desc">
只需几秒钟即可创建您的专属账号体验下一代全网事件聚合与 AI 洞察服务 只需几秒钟即可创建您的专属账号体验下一代全网事件聚合与 AI 洞察服务
</p> </p>
<div class="feature-list">
<div class="feature-item">
<div class="feature-icon">🔍</div>
<div class="feature-text">
<strong>个性化兴趣订阅</strong>
<span>定制你的专属信息流只看你关心的动态</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">📩</div>
<div class="feature-text">
<strong>每日邮件简报</strong>
<span>定时推送当日核心热点重要信息不遗漏</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">📊</div>
<div class="feature-text">
<strong>多维度事件追踪</strong>
<span>掌握事件起因发展脉络与各方观点</span>
</div>
</div>
</div>
</div> </div>
<div class="ambient-glow"></div> <div class="ambient-glow"></div>
</aside> </aside>
@@ -171,10 +203,10 @@ onUnmounted(() => {
<button <button
type="button" type="button"
class="input-action-btn" class="input-action-btn"
:disabled="!canSendCode" :disabled="!canSendCode || isSendingCode"
@click="handleSendCode" @click="handleSendCode"
> >
{{ countdown > 0 ? `${countdown}s 后重发` : '获取验证码' }} {{ isSendingCode ? '发送中...' : (countdown > 0 ? `${countdown}s 后重发` : '获取验证码') }}
</button> </button>
</div> </div>
</div> </div>
@@ -189,9 +221,9 @@ onUnmounted(() => {
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
placeholder="至少 8 位字符" placeholder="至少 8 位字符"
/> />
<button type="button" class="input-action-btn" @click="showPassword = !showPassword"> <!-- <button type="button" class="input-action-btn" @click="showPassword = !showPassword">
{{ showPassword ? '隐藏' : '显示' }} {{ showPassword ? '隐藏' : '显示' }}
</button> </button> -->
</div> </div>
</div> </div>
@@ -232,7 +264,7 @@ onUnmounted(() => {
</div> </div>
<button class="btn-primary" :disabled="authStore.loading" type="submit"> <button class="btn-primary" :disabled="authStore.loading" type="submit">
{{ authStore.loading ? '提交中...' : '注册并登录' }} {{ isSubmitting ? '提交中...' : '注册并登录' }}
</button> </button>
</form> </form>
@@ -258,11 +290,36 @@ onUnmounted(() => {
.brand-panel { .brand-panel {
flex: 1; flex: 1;
display: none; display: none;
background: linear-gradient(135deg, var(--bg-surface), var(--bg-base)); background: var(--bg-surface);
border-right: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle);
padding: 80px; padding: 80px;
position: relative; position: relative;
overflow: hidden; 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) { @media (min-width: 900px) {
@@ -314,6 +371,60 @@ onUnmounted(() => {
line-height: 1.7; line-height: 1.7;
color: var(--text-secondary); color: var(--text-secondary);
font-weight: 500; 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 { .ambient-glow {