web/src/App.vue
2025-06-11 19:46:04 +08:00

2573 lines
65 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { RouterView, useRoute } from 'vue-router'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useUserStore } from './stores/user'
import { useThemeStore } from './stores/theme'
import { authApi, http } from './services/api'
const route = useRoute()
const isStandalonePage = computed(() => route.meta.standalone)
const userStore = useUserStore()
const themeStore = useThemeStore()
const isAuthenticated = computed(() => userStore.isAuthenticated)
const userInfo = computed(() => userStore.userInfo)
const showMobileMenu = ref(false)
// const showUserMenu = ref(false)
const showLoginModal = ref(false)
const showResetPasswordModal = ref(false)
const showUserInfoModal = ref(false)
const loginMode = ref('login') // 'login' 或 'register'
// 获取用户信息
const fetchUserInfo = async () => {
if (!isAuthenticated.value) return
try {
const response = await http.get(
`${import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'}/user/me`,
)
if (!response.ok) {
throw new Error('获取用户信息失败')
}
const data = await response.json()
// 使用 updateUserInfo 方法确保正确更新用户信息和本地存储
userStore.updateUserInfo(data)
} catch (error) {
console.error('获取用户数据失败:', error)
// 处理在http工具中已经实现这里不需要重复处理
}
}
// 设置定期刷新用户信息每10分钟
let userInfoRefreshInterval: number | undefined
const startUserInfoRefresh = () => {
if (userInfoRefreshInterval) return
userInfoRefreshInterval = window.setInterval(fetchUserInfo, 10 * 60 * 1000)
}
const stopUserInfoRefresh = () => {
if (userInfoRefreshInterval) {
clearInterval(userInfoRefreshInterval)
userInfoRefreshInterval = undefined
}
}
// 处理登录状态变化
watch(isAuthenticated, (newValue) => {
if (newValue) {
startUserInfoRefresh()
} else {
stopUserInfoRefresh()
}
})
// 表单数据
const formData = ref({
email: '',
password: '',
verificationCode: '',
})
// 重置密码表单数据
const resetPasswordData = ref({
email: '',
verificationCode: '',
newPassword: '',
})
// 表单错误信息
const formErrors = ref({
email: '',
password: '',
verificationCode: '',
})
// 重置密码表单错误信息
const resetPasswordErrors = ref({
email: '',
verificationCode: '',
newPassword: '',
})
// 密码显示状态
const showPassword = ref(false)
const showNewPassword = ref(false)
// 验证码相关状态
const sendingCode = ref(false)
const countdown = ref(0)
const resetCountdown = ref(0)
// 加载状态
const isLoading = ref(false)
const isResetLoading = ref(false)
// 发送验证码
const sendVerificationCode = async () => {
if (!formData.value.email) {
formErrors.value.email = '请输入邮箱'
return
}
try {
sendingCode.value = true
formErrors.value.email = ''
await authApi.sendVerificationCode(formData.value.email)
countdown.value = 60
// 倒计时
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (err) {
formErrors.value.email = err instanceof Error ? err.message : '发送验证码失败,请重试'
console.error('发送验证码失败:', err)
} finally {
sendingCode.value = false
}
}
// 发送重置密码验证码
const sendResetVerificationCode = async () => {
if (!resetPasswordData.value.email) {
resetPasswordErrors.value.email = '请输入邮箱'
return
}
try {
sendingCode.value = true
resetPasswordErrors.value.email = ''
// 调用发送验证码接口
const response = await fetch(
`${import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'}/user/send-verification-code`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mail: resetPasswordData.value.email }),
},
)
if (!response.ok) {
throw new Error('发送验证码失败,请重试')
}
resetCountdown.value = 60
// 倒计时
const timer = setInterval(() => {
resetCountdown.value--
if (resetCountdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (err) {
resetPasswordErrors.value.email = err instanceof Error ? err.message : '发送验证码失败,请重试'
console.error('发送重置密码验证码失败:', err)
} finally {
sendingCode.value = false
}
}
// 验证表单
const validateForm = () => {
let isValid = true
formErrors.value = {
email: '',
password: '',
verificationCode: '',
}
// 验证邮箱
if (!formData.value.email) {
formErrors.value.email = '请输入邮箱'
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
formErrors.value.email = '请输入有效的邮箱地址'
isValid = false
}
// 验证密码
if (!formData.value.password) {
formErrors.value.password = '请输入密码'
isValid = false
} else if (formData.value.password.length < 6) {
formErrors.value.password = '密码长度不能小于6位'
isValid = false
}
// 注册模式下验证验证码
if (loginMode.value === 'register') {
if (!formData.value.verificationCode) {
formErrors.value.verificationCode = '请输入验证码'
isValid = false
}
}
return isValid
}
// 验证重置密码表单
const validateResetPasswordForm = () => {
let isValid = true
resetPasswordErrors.value = {
email: '',
verificationCode: '',
newPassword: '',
}
// 验证邮箱
if (!resetPasswordData.value.email) {
resetPasswordErrors.value.email = '请输入邮箱'
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(resetPasswordData.value.email)) {
resetPasswordErrors.value.email = '请输入有效的邮箱地址'
isValid = false
}
// 验证验证码
if (!resetPasswordData.value.verificationCode) {
resetPasswordErrors.value.verificationCode = '请输入验证码'
isValid = false
}
// 验证新密码
if (!resetPasswordData.value.newPassword) {
resetPasswordErrors.value.newPassword = '请输入新密码'
isValid = false
} else if (resetPasswordData.value.newPassword.length < 6) {
resetPasswordErrors.value.newPassword = '密码长度不能小于6位'
isValid = false
}
return isValid
}
// 处理重置密码提交
const handleResetPasswordSubmit = async () => {
if (!validateResetPasswordForm()) return
isResetLoading.value = true
try {
// 调用重置密码接口
const response = await fetch(
`${import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'}/user/reset_password`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mail: resetPasswordData.value.email,
verification_code: resetPasswordData.value.verificationCode,
new_password: resetPasswordData.value.newPassword,
}),
},
)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || '重置密码失败')
}
// 重置密码成功,关闭模态框
closeResetPasswordModal()
// 提示用户重置成功,并打开登录模态框
alert('密码重置成功,请使用新密码登录')
openAuthModal('login')
} catch (error) {
console.error('重置密码失败:', error)
resetPasswordErrors.value.email =
error instanceof Error ? error.message : '重置密码失败,请检查输入信息'
} finally {
isResetLoading.value = false
}
}
// 处理表单提交
const handleSubmit = async () => {
if (!validateForm()) return
isLoading.value = true
try {
if (loginMode.value === 'login') {
await authApi.login({
mail: formData.value.email,
password: formData.value.password,
})
await fetchUserInfo() // 登录成功后获取用户信息
} else {
// 从邮箱中提取用户名作为昵称
const nickname = formData.value.email.split('@')[0]
await authApi.register({
mail: formData.value.email,
nickname: nickname,
password: formData.value.password,
verification_code: formData.value.verificationCode,
})
// 注册成功后自动登录
await authApi.login({
mail: formData.value.email,
password: formData.value.password,
})
await fetchUserInfo() // 注册并登录成功后获取用户信息
}
closeAuthModal()
} catch (error) {
console.error('认证失败:', error)
formErrors.value.email = error instanceof Error ? error.message : '认证失败,请检查输入信息'
} finally {
isLoading.value = false
}
}
const closeAuthModal = () => {
showLoginModal.value = false
loginMode.value = 'login'
// 重置表单和错误信息
formData.value = {
email: '',
password: '',
verificationCode: '',
}
formErrors.value = {
email: '',
password: '',
verificationCode: '',
}
}
const handleLogout = () => {
stopUserInfoRefresh() // 退出登录时停止定时刷新
userStore.logout()
if (window.location.pathname.includes('/user')) {
window.location.href = '/'
}
showMobileMenu.value = false
// showUserMenu.value = false
showUserInfoModal.value = false
}
const toggleMobileMenu = () => {
showMobileMenu.value = !showMobileMenu.value
}
// const toggleUserMenu = (event: Event) => {
// event.stopPropagation()
// showUserMenu.value = !showUserMenu.value
// }
// 点击其他地方关闭菜单
const closeMenus = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (!target.closest('.mobile-menu') && !target.closest('.menu-button')) {
showMobileMenu.value = false
}
}
const openAuthModal = (mode: 'login' | 'register') => {
loginMode.value = mode
showLoginModal.value = true
}
// 关闭重置密码模态框
const closeResetPasswordModal = () => {
showResetPasswordModal.value = false
// 重置表单和错误信息
resetPasswordData.value = {
email: '',
verificationCode: '',
newPassword: '',
}
resetPasswordErrors.value = {
email: '',
verificationCode: '',
newPassword: '',
}
}
// 打开重置密码模态框
const openResetPasswordModal = () => {
closeAuthModal() // 关闭登录模态框
showResetPasswordModal.value = true
}
// 打开用户信息模态框
const openUserInfoModal = () => {
showUserInfoModal.value = true
// showUserMenu.value = false
showMobileMenu.value = false
}
// 关闭用户信息模态框
const closeUserInfoModal = () => {
showUserInfoModal.value = false
}
// 格式化过期时间
const formatExpireTime = (expireTime: string | undefined) => {
if (!expireTime) return ''
const date = new Date(expireTime)
return date
.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.replace(/\//g, '/')
}
// 切换主题
// const toggleTheme = () => {
// themeStore.toggleTheme()
// }
onMounted(() => {
document.addEventListener('click', closeMenus)
if (isAuthenticated.value) {
fetchUserInfo() // 应用初始化时获取用户信息
startUserInfoRefresh() // 开始定期刷新
}
// 添加全局滚动监听,用于滚动条显示
let scrollTimer: number | undefined
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
if (target) {
target.classList.add('scrolling')
clearTimeout(scrollTimer)
scrollTimer = window.setTimeout(() => {
target.classList.remove('scrolling')
}, 1000)
}
}
// 为所有可滚动元素添加滚动监听
document.addEventListener('scroll', handleScroll, true)
// 清理函数
const cleanup = () => {
document.removeEventListener('scroll', handleScroll, true)
clearTimeout(scrollTimer)
}
// 保存清理函数到全局
;(globalThis as typeof globalThis & { __scrollCleanup?: () => void }).__scrollCleanup = cleanup
})
onUnmounted(() => {
document.removeEventListener('click', closeMenus)
stopUserInfoRefresh() // 组件卸载时停止定时刷新
// 清理滚动监听
const cleanup = (globalThis as typeof globalThis & { __scrollCleanup?: () => void })
.__scrollCleanup
if (cleanup) {
cleanup()
}
})
</script>
<template>
<!-- 如果是独立页面直接显示路由内容 -->
<RouterView v-if="isStandalonePage" />
<!-- 否则显示完整应用布局 -->
<div v-else class="app-container">
<!-- 移动端菜单按钮 -->
<button class="menu-button" @click="toggleMobileMenu">
<svg
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M3 12h18M3 6h18M3 18h18" />
</svg>
</button>
<!-- 左侧栏 -->
<aside class="sidebar" :class="{ 'sidebar-open': showMobileMenu }">
<div class="sidebar-content">
<!-- 网站标题 -->
<div class="site-title">
<span class="title-text">tradus</span>
<!-- <div class="subtitle-text">AI for Trading</div> -->
</div>
<!-- Agent 列表 -->
<div class="agent-list">
<RouterLink to="/" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
<span class="agent-name">首页</span>
</RouterLink>
<!-- <RouterLink to="/ai-agents" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
<span class="agent-name">AI分析智能体</span>
</RouterLink>
<RouterLink
to="/analysis-history"
class="agent-item"
@click="showMobileMenu = false"
v-if="isAuthenticated"
>
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span class="agent-name">分析历史</span>
</RouterLink> -->
<RouterLink
to="/ai-agent"
class="agent-item"
@click="showMobileMenu = false"
v-if="isAuthenticated"
>
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<span class="agent-name">AI 交易智能体</span>
</RouterLink>
<RouterLink to="/subscription" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/>
</svg>
<span class="agent-name">会员订阅</span>
</RouterLink>
<RouterLink to="/download" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span class="agent-name">App 下载</span>
</RouterLink>
<RouterLink to="/contact" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
></path>
</svg>
<span class="agent-name">联系我们</span>
</RouterLink>
</div>
<!-- 桌面端用户信息 -->
<div class="desktop-user-info" v-if="isAuthenticated">
<div class="user-info-box" @click.stop="openUserInfoModal">
<div class="user-avatar">
<span>{{ userInfo?.mail?.charAt(0)?.toUpperCase() || 'U' }}</span>
</div>
<div class="user-info-details">
<span class="user-email-display" :title="userInfo?.mail">{{
userInfo?.mail || '未设置邮箱'
}}</span>
<div class="user-member-tag" v-if="userInfo?.member_name">
<span class="member-tag" :class="{ 'svip-tag': userInfo?.member_name === 'SVIP' }">
{{ userInfo.member_name }}
</span>
</div>
</div>
<svg
class="info-icon"
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
></path>
</svg>
</div>
</div>
<div class="desktop-user-info" v-else>
<button class="auth-button" @click="openAuthModal('login')">登录</button>
<button class="auth-button register" @click="openAuthModal('register')">注册</button>
</div>
<!-- 移动端用户信息 -->
<div class="mobile-user-info" v-if="isAuthenticated">
<div class="user-info-box" @click.stop="openUserInfoModal">
<div class="user-avatar">
<span>{{ userInfo?.mail?.charAt(0)?.toUpperCase() || 'U' }}</span>
</div>
<div class="user-info-details">
<span class="user-email-display" :title="userInfo?.mail">{{
userInfo?.mail || '未设置邮箱'
}}</span>
<div class="user-member-tag" v-if="userInfo?.member_name">
<span class="member-tag" :class="{ 'svip-tag': userInfo?.member_name === 'SVIP' }">
{{ userInfo.member_name }}
</span>
</div>
</div>
<svg
class="info-icon"
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
></path>
</svg>
</div>
</div>
<div class="mobile-user-info" v-else>
<button class="auth-button" @click="openAuthModal('login')">登录</button>
<button class="auth-button register" @click="openAuthModal('register')">注册</button>
</div>
</div>
</aside>
<!-- 遮罩层 -->
<div class="sidebar-overlay" v-if="showMobileMenu" @click="showMobileMenu = false"></div>
<!-- 右侧对话区域 -->
<main class="chat-container">
<div class="chat-content">
<RouterView @openAuth="openAuthModal" />
</div>
</main>
<!-- 登录注册模态框 -->
<div class="modal-overlay" v-if="showLoginModal" @click="closeAuthModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h2>{{ loginMode === 'login' ? '登录' : '注册' }}</h2>
<button class="close-button" @click="closeAuthModal">
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-content">
<form class="auth-form" @submit.prevent="handleSubmit">
<div class="form-group">
<label for="email">邮箱</label>
<div class="input-container">
<input
type="email"
id="email"
v-model="formData.email"
:class="{ error: formErrors.email }"
placeholder="请输入邮箱"
/>
<button
v-if="loginMode === 'register'"
type="button"
class="code-button"
@click="sendVerificationCode"
:disabled="sendingCode || countdown > 0"
>
{{
countdown > 0
? `${countdown}秒后重试`
: sendingCode
? '发送中...'
: '获取验证码'
}}
</button>
</div>
<span class="error-message" v-if="formErrors.email">{{ formErrors.email }}</span>
</div>
<div class="form-group" v-if="loginMode === 'register'">
<label for="verification-code">邮件验证码</label>
<input
type="text"
id="verification-code"
v-model="formData.verificationCode"
:class="{ error: formErrors.verificationCode }"
placeholder="请输入验证码"
/>
<span class="error-message" v-if="formErrors.verificationCode">
{{ formErrors.verificationCode }}
</span>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="password-input-container">
<input
:type="showPassword ? 'text' : 'password'"
id="password"
v-model="formData.password"
:class="{ error: formErrors.password }"
placeholder="请输入密码"
/>
<button
type="button"
class="toggle-password-button"
@click="showPassword = !showPassword"
>
<svg
v-if="!showPassword"
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
<svg
v-else
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
/>
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
</button>
</div>
<span class="error-message" v-if="formErrors.password">{{
formErrors.password
}}</span>
</div>
<button type="submit" class="submit-button" :disabled="isLoading">
<span v-if="!isLoading">{{ loginMode === 'login' ? '登录' : '注册' }}</span>
<span v-else class="loading-spinner"></span>
</button>
</form>
<!-- 忘记密码链接 -->
<div class="forgot-password" v-if="loginMode === 'login'">
<button class="forgot-password-link" @click="openResetPasswordModal">忘记密码?</button>
</div>
<div class="auth-switch">
{{ loginMode === 'login' ? '还没有账号?' : '已有账号?' }}
<button
class="switch-button"
@click="loginMode = loginMode === 'login' ? 'register' : 'login'"
>
{{ loginMode === 'login' ? '立即注册' : '立即登录' }}
</button>
</div>
</div>
</div>
</div>
<!-- 忘记密码模态框 -->
<div class="modal-overlay" v-if="showResetPasswordModal" @click="closeResetPasswordModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h2>重置密码</h2>
<button class="close-button" @click="closeResetPasswordModal">
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-content">
<form class="auth-form" @submit.prevent="handleResetPasswordSubmit">
<div class="form-group">
<label for="reset-email">邮箱</label>
<div class="input-container">
<input
type="email"
id="reset-email"
v-model="resetPasswordData.email"
:class="{ error: resetPasswordErrors.email }"
placeholder="请输入邮箱"
/>
<button
type="button"
class="code-button"
@click="sendResetVerificationCode"
:disabled="sendingCode || resetCountdown > 0"
>
{{
resetCountdown > 0
? `${resetCountdown}秒后重试`
: sendingCode
? '发送中...'
: '获取验证码'
}}
</button>
</div>
<span class="error-message" v-if="resetPasswordErrors.email">{{
resetPasswordErrors.email
}}</span>
</div>
<div class="form-group">
<label for="reset-verification-code">邮件验证码</label>
<input
type="text"
id="reset-verification-code"
v-model="resetPasswordData.verificationCode"
:class="{ error: resetPasswordErrors.verificationCode }"
placeholder="请输入验证码"
/>
<span class="error-message" v-if="resetPasswordErrors.verificationCode">
{{ resetPasswordErrors.verificationCode }}
</span>
</div>
<div class="form-group">
<label for="new-password">新密码</label>
<div class="password-input-container">
<input
:type="showNewPassword ? 'text' : 'password'"
id="new-password"
v-model="resetPasswordData.newPassword"
:class="{ error: resetPasswordErrors.newPassword }"
placeholder="请输入新密码"
/>
<button
type="button"
class="toggle-password-button"
@click="showNewPassword = !showNewPassword"
>
<svg
v-if="!showNewPassword"
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
<svg
v-else
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
/>
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
</button>
</div>
<span class="error-message" v-if="resetPasswordErrors.newPassword">
{{ resetPasswordErrors.newPassword }}
</span>
</div>
<button type="submit" class="submit-button" :disabled="isResetLoading">
<span v-if="!isResetLoading">重置密码</span>
<span v-else class="loading-spinner"></span>
</button>
</form>
<div class="auth-switch">
<button class="switch-button" @click="closeResetPasswordModal()">关闭</button>
</div>
</div>
</div>
</div>
<!-- 用户信息模态框 -->
<div class="modal-overlay" v-if="showUserInfoModal" @click="closeUserInfoModal">
<div class="user-info-modal-container" @click.stop>
<div class="modal-header">
<h2>个人信息</h2>
<button class="close-button" @click="closeUserInfoModal">
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="user-info-modal-content">
<!-- 用户头像和基本信息 -->
<div class="user-profile-section">
<div class="user-avatar-large">
<span>{{ userInfo?.mail?.charAt(0)?.toUpperCase() || 'U' }}</span>
</div>
<div class="user-basic-info">
<h3 class="user-display-name">{{ userInfo?.mail || '未设置邮箱' }}</h3>
<div class="user-member-info" v-if="userInfo?.member_name">
<span class="member-tag" :class="{ 'svip-tag': userInfo?.member_name === 'SVIP' }">
{{ userInfo.member_name }}
</span>
</div>
<div class="user-expire-info" v-if="userInfo?.is_subscribed && userInfo?.expire_time">
<span class="expire-text"
>到期时间:{{ formatExpireTime(userInfo.expire_time) }}</span
>
</div>
</div>
</div>
<!-- 设置选项 -->
<div class="settings-section">
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">主题模式</div>
<div class="setting-description">选择您偏好的界面主题</div>
</div>
<div class="theme-toggle">
<button
class="theme-option"
:class="{ active: themeStore.currentTheme === 'light' }"
@click="themeStore.setTheme('light')"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
浅色
</button>
<button
class="theme-option"
:class="{ active: themeStore.currentTheme === 'dark' }"
@click="themeStore.setTheme('dark')"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
深色
</button>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="modal-actions">
<button class="logout-button" @click="handleLogout">
<svg
viewBox="0 0 24 24"
width="18"
height="18"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
退出登录
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
:root {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8fafc;
--color-bg-hover: rgba(51, 85, 255, 0.04);
--color-bg-active: rgba(51, 85, 255, 0.08);
--color-text-primary: #1e293b;
--color-text-secondary: #64748b;
--color-text-tertiary: #94a3b8;
--color-accent: #3355ff;
--color-accent-hover: #4466ff;
--color-accent-active: #2244ee;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 600;
--border-radius: 0.5rem;
--sidebar-width: 260px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
line-height: 1.6;
font-weight: var(--font-weight-regular);
height: 100vh;
}
.app-container {
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
}
/* 左侧栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
background-color: var(--color-bg-secondary);
border-right: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
}
.sidebar-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
/* 网站标题样式 */
.site-title {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.title-text {
font-size: 1.8rem;
font-weight: 800;
background: linear-gradient(135deg, var(--color-accent) 0%, #2244ee 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-family:
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
letter-spacing: -0.5px;
margin-bottom: 0.3rem;
display: block;
}
.subtitle-text {
font-size: 0.9rem;
color: var(--color-text-secondary);
font-weight: 400;
font-family:
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
letter-spacing: 0.5px;
text-transform: uppercase;
}
/* Agent 列表样式 */
.agent-list {
flex: 1;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
}
.agent-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--color-text-secondary);
text-decoration: none;
border-radius: 8px;
transition: all 0.2s ease;
}
.agent-item:hover {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
}
.agent-item.router-link-active {
background-color: var(--color-bg-active);
color: var(--color-accent);
}
.agent-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
stroke: currentColor;
}
.agent-name {
font-size: 0.95rem;
font-weight: 500;
letter-spacing: 0.01em;
}
/* 修改用户信息区域样式 */
.user-section {
position: fixed;
left: 0;
bottom: 0;
width: var(--sidebar-width);
background-color: var(--color-bg-elevated);
border-radius: 0;
padding: 1.25rem;
z-index: 100;
cursor: pointer;
transition: all 0.2s ease;
border-top: 1px solid var(--color-divider);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
display: none; /* 隐藏旧版用户信息栏 */
}
.user-section:hover {
background-color: var(--color-bg-secondary);
transform: translateY(-1px);
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
}
.user-info-box {
display: flex;
align-items: center;
gap: 1rem;
}
.user-avatar {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
background: var(--color-bg-hover);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: var(--color-accent);
font-weight: bold;
box-shadow: 0 2px 8px rgba(51, 85, 255, 0.2);
transition: all 0.2s ease;
}
.user-section:hover .user-avatar {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(51, 85, 255, 0.3);
}
.user-nickname {
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
/* 用户菜单样式 */
.user-menu {
position: absolute;
bottom: calc(100% - 0.5rem);
right: 1.25rem;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
min-width: 160px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 1100;
animation: menuSlideIn 0.25s ease;
transform-origin: bottom right;
}
@keyframes menuSlideIn {
from {
opacity: 0;
transform: translateY(8px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.menu-item {
width: 100%;
padding: 0.9rem 1.25rem;
border: none;
background: none;
color: var(--color-text-primary);
font-size: 0.95rem;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.75rem;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: var(--color-bg-hover);
color: var(--color-accent);
border-left-color: var(--color-accent);
}
.menu-item .menu-icon {
color: var(--color-text-secondary);
transition: all 0.2s ease;
}
.menu-item:hover .menu-icon {
color: var(--color-accent);
transform: translateX(2px);
}
/* 移动端用户菜单样式 */
.user-menu.mobile {
position: absolute;
bottom: 100%;
left: 1rem;
right: 1rem;
margin-bottom: 0.5rem;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 1100;
animation: mobilMenuSlideIn 0.25s ease;
}
@keyframes mobilMenuSlideIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 右侧对话区域样式 */
.chat-container {
flex: 1;
height: 100vh;
display: flex;
background-color: var(--color-bg-primary);
overflow: hidden;
}
.chat-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
/* 全局按钮样式 */
.btn {
padding: 0.8rem 1.6rem;
border-radius: var(--border-radius);
font-weight: var(--font-weight-medium);
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background-color: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-primary:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-border-hover);
}
.btn-secondary {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--color-bg-elevated);
color: var(--color-text-primary);
}
/* 全局卡片样式 */
.card {
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 1.5rem;
border: 1px solid var(--color-border);
transition: all 0.3s ease;
}
.card:hover {
border-color: var(--color-border-hover);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* 移动端菜单按钮 */
.menu-button {
display: none;
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 1000;
background: none;
border: none;
color: var(--color-text-primary);
cursor: pointer;
padding: 0.5rem;
border-radius: var(--border-radius);
background-color: var(--color-bg-secondary);
border: 1px solid rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
}
.menu-button:hover {
background-color: var(--color-bg-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.menu-button:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 遮罩层 */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 90;
backdrop-filter: blur(2px);
opacity: 0;
transition: opacity 0.3s ease;
}
.sidebar-overlay.sidebar-open {
opacity: 1;
}
/* 移动端用户信息 */
.mobile-user-info {
display: none;
padding: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.08);
margin-top: auto;
background-color: var(--color-bg-primary);
position: relative;
z-index: 1100;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.mobile-user-info .user-info-box {
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
padding: 0.75rem;
border-radius: var(--border-radius);
transition: all 0.3s ease;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid transparent;
}
.mobile-user-info .user-info-box:hover,
.mobile-user-info .user-info-box:active {
background-color: var(--color-bg-hover);
border-color: rgba(51, 85, 255, 0.1);
}
.mobile-user-info .user-avatar {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent) 0%, rgba(51, 85, 255, 0.8) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: white;
font-weight: bold;
box-shadow: 0 3px 10px rgba(51, 85, 255, 0.25);
border: 2px solid rgba(255, 255, 255, 0.2);
}
.mobile-user-info .user-info-details {
display: flex;
flex-direction: column;
flex: 1;
padding: 0.15rem 0;
}
.mobile-user-info .user-nickname {
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: 0.2rem;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
/* 移除旧的退出按钮样式 */
.logout-button {
display: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.menu-button {
display: flex;
z-index: 1001;
}
.desktop-user-info {
display: none !important;
}
.mobile-user-info {
display: block !important;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: var(--color-bg-primary);
border-top: 1px solid rgba(0, 0, 0, 0.08);
z-index: 10;
padding: 1rem;
}
.modal-container {
width: 95%;
}
.modal-header {
padding: 1.25rem;
}
.modal-content {
padding: 1.25rem;
}
.sidebar {
position: fixed;
left: -100%;
top: 0;
bottom: 0;
width: 80% !important;
max-width: 300px;
z-index: 1002;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background-color: var(--color-bg-primary);
box-shadow: none;
transform: translateX(0);
opacity: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.sidebar-content {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding-bottom: 80px; /* 为底部的用户信息留出空间 */
}
.sidebar-open {
left: 0;
opacity: 1;
box-shadow: 6px 0 25px rgba(0, 0, 0, 0.15);
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
z-index: 1000;
backdrop-filter: blur(2px);
opacity: 0;
transition: opacity 0.3s ease;
}
.sidebar-overlay.sidebar-open {
display: block;
opacity: 1;
}
.chat-container {
margin-left: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 当侧栏打开时,主内容区域右移并添加阴影效果 */
.sidebar-open + .sidebar-overlay + .chat-container {
transform: translateX(16px);
filter: brightness(0.95);
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.agent-item {
transform: translateX(-10px);
opacity: 0;
animation: slideIn 0.3s ease forwards;
}
.agent-item:nth-child(1) {
animation-delay: 0.1s;
}
.agent-item:nth-child(2) {
animation-delay: 0.2s;
}
@keyframes slideIn {
from {
transform: translateX(-10px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
}
/* 桌面端用户信息样式 */
.desktop-user-info {
display: flex;
flex-direction: column;
padding: 0.75rem;
margin-top: auto;
border-top: 1px solid rgba(0, 0, 0, 0.08);
background-color: var(--color-bg-secondary);
position: relative;
z-index: 1100;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.desktop-user-info .user-info-box {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
padding: 0.5rem;
border-radius: var(--border-radius);
transition: all 0.3s ease;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid transparent;
}
.desktop-user-info .user-info-box:hover {
background-color: var(--color-bg-hover);
border-color: rgba(51, 85, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.desktop-user-info .user-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent) 0%, rgba(51, 85, 255, 0.8) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: white;
font-weight: bold;
box-shadow: 0 3px 10px rgba(51, 85, 255, 0.25);
transition: all 0.3s ease;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.desktop-user-info .user-info-box:hover .user-avatar {
transform: scale(1.05) rotate(5deg);
box-shadow: 0 5px 15px rgba(51, 85, 255, 0.35);
}
.desktop-user-info .user-info-box:hover .user-nickname {
color: var(--color-accent);
}
.dropdown-icon {
color: var(--color-text-secondary);
transition: transform 0.2s ease;
}
.user-info-box:hover .dropdown-icon {
color: var(--color-text-primary);
}
.user-info-box[aria-expanded='true'] .dropdown-icon {
transform: rotate(180deg);
}
/* 登录注册链接样式 */
.desktop-user-info .auth-link {
display: block;
padding: 0.75rem 1rem;
color: var(--color-text-primary);
text-decoration: none;
border-radius: var(--border-radius);
transition: all 0.2s ease;
text-align: center;
margin: 0.5rem 0;
background-color: var(--color-bg-hover);
}
.desktop-user-info .auth-link:hover {
background-color: var(--color-bg-active);
color: var(--color-accent);
}
/* 认证按钮样式 */
.auth-button {
width: 100%;
padding: 0.6rem;
border: 1px solid var(--color-accent);
border-radius: var(--border-radius);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
margin: 0.25rem 0;
background: none;
color: var(--color-accent);
}
.auth-button:hover {
background-color: rgba(51, 85, 255, 0.04);
}
.auth-button.register {
background-color: var(--color-accent);
color: white;
}
.auth-button.register:hover {
background-color: var(--color-accent-hover);
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
backdrop-filter: blur(4px);
}
.modal-container {
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
width: 90%;
max-width: 400px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease;
border: 1px solid var(--color-border);
}
/* 深色主题下的通用模态框增强 */
[data-theme='dark'] .modal-container {
background-color: #1a1a1a;
border: 2px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.close-button {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
}
.modal-content {
padding: 1.5rem;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.95rem;
font-weight: 500;
color: var(--color-text-primary);
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 1rem;
transition: all 0.2s ease;
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
height: 42px;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(51, 85, 255, 0.1);
}
.submit-button {
margin-top: 1rem;
padding: 0.875rem;
background-color: var(--color-accent);
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.submit-button:hover {
background-color: var(--color-accent-hover);
}
.auth-switch {
margin-top: 1.5rem;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.95rem;
}
.switch-button {
background: none;
border: none;
color: var(--color-accent);
cursor: pointer;
font-weight: 500;
padding: 0;
margin-left: 0.5rem;
}
.switch-button:hover {
text-decoration: underline;
}
@keyframes modalSlideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* 表单错误样式 */
.form-group input.error {
border-color: #ff4d4f;
}
.form-group input.error:focus {
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.1);
}
.error-message {
color: #ff4d4f;
font-size: 0.85rem;
margin-top: 0.25rem;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #ffffff;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 禁用状态 */
.submit-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 输入框聚焦状态 */
.form-group input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(51, 85, 255, 0.1);
}
/* 验证码按钮样式 */
.input-container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.input-container input {
flex: 1;
}
@media (max-width: 480px) {
.input-container {
flex-direction: column;
}
.code-button {
width: 100%;
}
}
.desktop-user-info .user-info-details {
display: flex;
flex-direction: column;
flex: 1;
padding: 0.15rem 0;
}
.desktop-user-info .user-nickname {
font-size: 0.95rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: 0.2rem;
transition: all 0.2s ease;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
/* 全局滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 3px;
transition: background-color 0.3s ease;
}
/* 只在滚动时显示滚动条 */
::-webkit-scrollbar-thumb:hover,
*:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
/* 深色主题下的滚动条 */
[data-theme='dark'] ::-webkit-scrollbar-thumb:hover,
[data-theme='dark'] *:hover::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
*:hover {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
/* Firefox 深色主题 */
[data-theme='dark'] *:hover {
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
/* 滚动时显示滚动条 */
*::-webkit-scrollbar-thumb:active,
.scrolling::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3) !important;
}
[data-theme='dark'] *::-webkit-scrollbar-thumb:active,
[data-theme='dark'] .scrolling::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.4) !important;
}
/* 确保滚动行为平滑 */
html {
scroll-behavior: smooth;
}
.password-input-container {
position: relative;
width: 100%;
}
.password-input-container input {
width: 100%;
padding-right: 2.5rem; /* 为按钮留出空间 */
}
.toggle-password-button {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
padding: 0.5rem;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 1;
}
.toggle-password-button:hover {
color: var(--color-text-primary);
}
.input-container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.input-container input {
flex: 1;
}
@media (max-width: 480px) {
.input-container {
flex-direction: column;
}
.code-button {
width: 100%;
}
}
.code-button {
padding: 0 1rem;
border-radius: var(--border-radius);
background-color: var(--color-accent);
border: 1px solid var(--color-accent);
color: white;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
height: 42px;
}
.code-button:hover:not(:disabled) {
background-color: var(--color-accent-hover);
border-color: var(--color-accent-hover);
}
.code-button:disabled {
opacity: 0.7;
cursor: not-allowed;
background-color: var(--color-bg-secondary);
border-color: var(--color-border);
color: var(--color-text-secondary);
}
/* 忘记密码链接样式 */
.forgot-password {
text-align: right;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.forgot-password-link {
background: none;
border: none;
color: var(--color-accent);
font-size: 0.9rem;
cursor: pointer;
padding: 0;
text-decoration: none;
}
.forgot-password-link:hover {
text-decoration: underline;
}
/* 用户信息模态框样式 */
.user-info-modal-container {
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
width: 90%;
max-width: 480px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease;
overflow: hidden;
border: 1px solid var(--color-border);
}
/* 深色主题下的模态框增强 */
[data-theme='dark'] .user-info-modal-container {
background-color: #1a1a1a;
border: 2px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* 深色主题下的模态框背景遮罩增强 */
[data-theme='dark'] .modal-overlay {
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
}
.user-info-modal-content {
padding: 0;
}
.user-profile-section {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 2rem;
border-bottom: 1px solid var(--color-border);
}
.user-avatar-large {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent) 0%, rgba(51, 85, 255, 0.8) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
color: white;
box-shadow: 0 4px 12px rgba(51, 85, 255, 0.2);
}
.user-basic-info {
flex: 1;
}
.user-display-name {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
color: var(--color-text-primary);
}
.user-email {
font-size: 0.95rem;
margin: 0 0 0.75rem 0;
color: var(--color-text-secondary);
}
.settings-section {
padding: 1.5rem 2rem;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.setting-info {
flex: 1;
}
.setting-title {
font-size: 1rem;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 0.25rem;
}
.setting-description {
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.theme-toggle {
display: flex;
gap: 0.5rem;
background-color: var(--color-bg-secondary);
border-radius: 8px;
padding: 0.25rem;
}
.theme-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: none;
background: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.theme-option:hover {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
}
.theme-option.active {
background-color: var(--color-accent);
color: white;
}
.theme-option svg {
width: 16px;
height: 16px;
stroke: currentColor;
}
.modal-actions {
padding: 1.5rem 2rem;
border-top: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
}
.logout-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 0.875rem;
background-color: #ff4757;
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.logout-button:hover {
background-color: #ff3742;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 71, 87, 0.3);
}
.logout-button svg {
stroke: currentColor;
}
/* 响应式设计 */
@media (max-width: 768px) {
.user-info-modal-container {
width: 85%;
max-width: none;
}
.user-profile-section {
padding: 1.5rem;
gap: 1rem;
}
.user-avatar-large {
width: 3.5rem;
height: 3.5rem;
font-size: 1.5rem;
}
.user-display-name {
font-size: 1rem;
}
.settings-section {
padding: 1.25rem 1.5rem;
}
.modal-actions {
padding: 1.25rem 1.5rem;
}
.setting-item {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.theme-toggle {
align-self: stretch;
}
.theme-option {
flex: 1;
justify-content: center;
}
}
/* 更新info-icon样式 */
.info-icon {
color: var(--color-text-secondary);
transition: all 0.2s ease;
}
.user-info-box:hover .info-icon {
color: var(--color-accent);
transform: rotate(90deg);
}
/* 用户邮箱显示样式 */
.user-email-display {
font-size: 0.9rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
transition: all 0.2s ease;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
/* 用户会员标签样式 */
.user-member-tag {
margin-top: 0.25rem;
}
.member-tag {
display: inline-block;
padding: 0.2rem 0.5rem;
background: #3b82f6;
color: white;
font-size: 0.75rem;
font-weight: 500;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
.member-tag.svip-tag {
background: linear-gradient(135deg, #d4af37, #ffd700);
color: #2d1810;
box-shadow: 0 2px 4px rgba(212, 175, 55, 0.3);
}
.member-tag:hover {
transform: scale(1.05);
}
.member-tag.svip-tag:hover {
box-shadow: 0 4px 8px rgba(212, 175, 55, 0.4);
}
/* 模态框中的用户会员信息 */
.user-member-info {
margin: 0.5rem 0;
}
.user-expire-info {
margin-top: 0.5rem;
}
.expire-text {
font-size: 0.85rem;
color: var(--color-text-secondary);
font-weight: 400;
}
.desktop-user-info .user-info-box:hover .user-email-display {
color: var(--color-accent);
}
.mobile-user-info .user-info-box:hover .user-email-display {
color: var(--color-accent);
}
/* 深色主题下的模态框头部增强 */
[data-theme='dark'] .modal-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
background-color: rgba(255, 255, 255, 0.03);
}
/* 深色主题下的模态框内容区域 */
[data-theme='dark'] .modal-content {
background-color: #1a1a1a;
}
/* 深色主题下的用户信息模态框头部 */
[data-theme='dark'] .user-profile-section {
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
background-color: rgba(255, 255, 255, 0.03);
}
/* 深色主题下的设置区域 */
[data-theme='dark'] .settings-section {
background-color: #1a1a1a;
}
/* 深色主题下的模态框操作区域 */
[data-theme='dark'] .modal-actions {
border-top: 1px solid rgba(255, 255, 255, 0.15);
background-color: rgba(255, 255, 255, 0.03);
}
/* 小屏幕优化 */
@media (max-width: 480px) {
.menu-button {
top: 1rem;
left: 0.5rem;
width: 36px;
height: 36px;
padding: 0.4rem;
}
}
</style>