web/src/App.vue
2025-05-14 21:58:26 +08:00

1602 lines
38 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 } from 'vue'
import { useUserStore } from './stores/user'
import { authApi } from './services/api'
const route = useRoute()
const isStandalonePage = computed(() => route.meta.standalone)
const userStore = useUserStore()
const isAuthenticated = computed(() => userStore.isAuthenticated)
const userInfo = computed(() => userStore.userInfo)
const showMobileMenu = ref(false)
const showUserMenu = ref(false)
const showLoginModal = ref(false)
const loginMode = ref('login') // 'login' 或 'register'
// 获取最新用户数据
const fetchUserInfo = async () => {
if (isAuthenticated.value) {
try {
await authApi.getUserInfo()
} catch (error) {
console.error('获取用户数据失败:', error)
}
}
}
// 表单数据
const formData = ref({
email: '',
password: '',
confirmPassword: '',
nickname: '',
verificationCode: '',
})
// 表单错误信息
const formErrors = ref({
email: '',
password: '',
confirmPassword: '',
nickname: '',
verificationCode: '',
})
// 验证码相关状态
const sendingCode = ref(false)
const countdown = ref(0)
// 加载状态
const isLoading = 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 validateForm = () => {
let isValid = true
formErrors.value = {
email: '',
password: '',
confirmPassword: '',
nickname: '',
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.confirmPassword) {
formErrors.value.confirmPassword = '请确认密码'
isValid = false
} else if (formData.value.confirmPassword !== formData.value.password) {
formErrors.value.confirmPassword = '两次输入的密码不一致'
isValid = false
}
// 验证昵称
if (!formData.value.nickname) {
formErrors.value.nickname = '请输入昵称'
isValid = false
}
// 验证验证码
if (!formData.value.verificationCode) {
formErrors.value.verificationCode = '请输入验证码'
isValid = false
}
}
return isValid
}
// 处理表单提交
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,
})
} else {
await authApi.register({
mail: formData.value.email,
nickname: formData.value.nickname,
password: formData.value.password,
verification_code: formData.value.verificationCode,
})
// 注册成功后自动登录
await authApi.login({
mail: formData.value.email,
password: formData.value.password,
})
}
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: '',
confirmPassword: '',
nickname: '',
verificationCode: '',
}
formErrors.value = {
email: '',
password: '',
confirmPassword: '',
nickname: '',
verificationCode: '',
}
}
const handleLogout = () => {
userStore.logout()
if (window.location.pathname.includes('/user')) {
window.location.href = '/'
}
showMobileMenu.value = false
showUserMenu.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
}
if (!target.closest('.user-info-box') && !target.closest('.user-menu')) {
showUserMenu.value = false
}
}
const openAuthModal = (mode: 'login' | 'register') => {
loginMode.value = mode
showLoginModal.value = true
}
onMounted(() => {
document.addEventListener('click', closeMenus)
fetchUserInfo() // 获取最新用户数据
})
onUnmounted(() => {
document.removeEventListener('click', closeMenus)
})
</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="24"
height="24"
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="/crypto-analysis" 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="M7 12l5-5 5 5M7 17l5-5 5 5" />
</svg>
<span class="agent-name">加密货币分析</span>
</RouterLink>
<RouterLink to="/astock-analysis" 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">A股分析</span>
</RouterLink>
</div>
<!-- 桌面端用户信息 -->
<div class="desktop-user-info" v-if="isAuthenticated">
<div class="user-info-box" @click.stop="toggleUserMenu" :aria-expanded="showUserMenu">
<div class="user-avatar">
<span>{{ userInfo?.nickname?.charAt(0) || 'U' }}</span>
</div>
<div class="user-info-details">
<span class="user-nickname" :title="userInfo?.nickname">{{
userInfo?.nickname
}}</span>
<div class="user-points" v-if="userInfo?.points !== undefined">
<span class="points-icon">✦</span>
<span class="points-value">{{ userInfo.points }} 积分</span>
</div>
</div>
<svg
class="dropdown-icon"
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<!-- 用户菜单 -->
<div class="user-menu" v-if="showUserMenu" @click.stop>
<button class="menu-item" @click="handleLogout">
<svg
class="menu-icon"
viewBox="0 0 24 24"
width="16"
height="16"
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 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="toggleUserMenu" :aria-expanded="showUserMenu">
<div class="user-avatar">
<span>{{ userInfo?.nickname?.charAt(0) || 'U' }}</span>
</div>
<div class="user-info-details">
<span class="user-nickname" :title="userInfo?.nickname">{{
userInfo?.nickname
}}</span>
<div class="user-points" v-if="userInfo?.points !== undefined">
<span class="points-icon">✦</span>
<span class="points-value">{{ userInfo.points }} 积分</span>
</div>
</div>
<svg
class="dropdown-icon"
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<!-- 用户菜单 -->
<div class="user-menu mobile" v-if="showUserMenu" @click.stop>
<button class="menu-item" @click="handleLogout">
<svg
class="menu-icon"
viewBox="0 0 24 24"
width="16"
height="16"
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 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-with-button" v-if="loginMode === 'register'">
<input
type="email"
id="email"
v-model="formData.email"
:class="{ error: formErrors.email }"
placeholder="请输入邮箱"
/>
<button
type="button"
class="code-button"
@click="sendVerificationCode"
:disabled="sendingCode || countdown > 0"
>
{{
countdown > 0
? `${countdown}秒后重试`
: sendingCode
? '发送中...'
: '获取验证码'
}}
</button>
</div>
<input
v-else
type="email"
id="email"
v-model="formData.email"
:class="{ error: formErrors.email }"
placeholder="请输入邮箱"
/>
<span class="error-message" v-if="formErrors.email">{{ formErrors.email }}</span>
</div>
<template v-if="loginMode === 'register'">
<div class="form-group">
<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="nickname">昵称</label>
<input
type="text"
id="nickname"
v-model="formData.nickname"
:class="{ error: formErrors.nickname }"
placeholder="请输入昵称"
/>
<span class="error-message" v-if="formErrors.nickname">{{
formErrors.nickname
}}</span>
</div>
</template>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="formData.password"
:class="{ error: formErrors.password }"
placeholder="请输入密码"
/>
<span class="error-message" v-if="formErrors.password">{{
formErrors.password
}}</span>
</div>
<div class="form-group" v-if="loginMode === 'register'">
<label for="confirm-password">确认密码</label>
<input
type="password"
id="confirm-password"
v-model="formData.confirmPassword"
:class="{ error: formErrors.confirmPassword }"
placeholder="请再次输入密码"
/>
<span class="error-message" v-if="formErrors.confirmPassword">{{
formErrors.confirmPassword
}}</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="auth-switch">
{{ loginMode === 'login' ? '还没有账号?' : '已有账号?' }}
<button
class="switch-button"
@click="loginMode = loginMode === 'login' ? 'register' : 'login'"
>
{{ loginMode === 'login' ? '立即注册' : '立即登录' }}
</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;
text-fill-color: transparent;
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: 1rem;
left: 1rem;
z-index: 1000;
background: none;
border: none;
color: var(--color-text-primary);
cursor: pointer;
padding: 0.75rem;
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);
}
.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%;
}
.mobile-user-info .user-points {
font-size: 0.85rem;
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: 0.3rem;
}
.mobile-user-info .user-points::before {
content: none;
}
.mobile-user-info .points-icon {
color: var(--color-accent);
font-size: 0.9rem;
}
.mobile-user-info .points-value {
white-space: nowrap;
}
/* 移除旧的退出按钮样式 */
.logout-button {
display: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.menu-button {
display: block;
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;
padding-top: 4.5rem;
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: 1.25rem;
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: 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;
}
.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: 3rem;
height: 3rem;
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.2rem;
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);
}
.desktop-user-info .user-info-box:hover .user-points {
color: var(--color-text-primary);
}
.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.75rem;
border: 1px solid var(--color-accent);
border-radius: var(--border-radius);
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
margin: 0.5rem 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;
}
.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 {
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);
}
.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-with-button {
display: flex;
gap: 0.5rem;
}
.input-with-button input {
flex: 1;
}
.code-button {
padding: 0 1rem;
border-radius: var(--border-radius);
background-color: var(--color-bg-elevated);
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s ease;
}
.code-button:hover:not(:disabled) {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.code-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
@media (max-width: 480px) {
.input-with-button {
flex-direction: column;
}
.code-button {
padding: 0.8rem;
}
}
.desktop-user-info .user-info-details {
display: flex;
flex-direction: column;
flex: 1;
padding: 0.25rem 0;
}
.desktop-user-info .user-nickname {
font-size: 1.05rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: 0.3rem;
transition: all 0.2s ease;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
.desktop-user-info .user-points {
font-size: 0.85rem;
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: 0.3rem;
transition: all 0.2s ease;
}
.desktop-user-info .user-points::before {
content: none;
}
.desktop-user-info .points-icon {
color: var(--color-accent);
font-size: 0.9rem;
}
.desktop-user-info .points-value {
white-space: nowrap;
}
</style>