470 lines
8.9 KiB
Vue
470 lines
8.9 KiB
Vue
<script setup>
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { authAPI } from './services/api.js'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
const user = ref(null)
|
||
const loading = ref(true)
|
||
const showUserMenu = ref(false)
|
||
const sidebarCollapsed = ref(false)
|
||
const isLoggedIn = ref(false)
|
||
|
||
// 初始化检查登录状态
|
||
const checkAuth = () => {
|
||
isLoggedIn.value = !!localStorage.getItem('access_token')
|
||
}
|
||
|
||
// 获取用户信息
|
||
const fetchUserInfo = async () => {
|
||
checkAuth()
|
||
if (!isLoggedIn.value) {
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await authAPI.getMe()
|
||
if (response.success) {
|
||
user.value = response.data
|
||
localStorage.setItem('user', JSON.stringify(response.data))
|
||
}
|
||
} catch (err) {
|
||
console.error('获取用户信息失败:', err)
|
||
logout()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 登出
|
||
const logout = async () => {
|
||
try {
|
||
await authAPI.logout()
|
||
} catch (err) {
|
||
console.error('登出失败:', err)
|
||
} finally {
|
||
localStorage.removeItem('access_token')
|
||
localStorage.removeItem('user')
|
||
user.value = null
|
||
showUserMenu.value = false
|
||
isLoggedIn.value = false
|
||
router.push('/auth')
|
||
}
|
||
}
|
||
|
||
// 切换侧边栏
|
||
const toggleSidebar = () => {
|
||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||
}
|
||
|
||
// 导航高亮
|
||
const isActive = (path) => {
|
||
return route.path === path
|
||
}
|
||
|
||
// 监听路由变化时关闭菜单
|
||
watch(() => route.path, () => {
|
||
showUserMenu.value = false
|
||
})
|
||
|
||
// 监听登录状态变化
|
||
onMounted(() => {
|
||
fetchUserInfo()
|
||
|
||
// 监听 auth-change 事件
|
||
window.addEventListener('auth-change', () => {
|
||
checkAuth()
|
||
if (isLoggedIn.value) {
|
||
fetchUserInfo()
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div id="app" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
|
||
<!-- 侧边栏 -->
|
||
<aside class="sidebar" v-if="isLoggedIn">
|
||
<div class="sidebar-header">
|
||
<div class="brand">
|
||
<span class="brand-icon">✨</span>
|
||
<span class="brand-text">Luxx</span>
|
||
</div>
|
||
<button @click="toggleSidebar" class="toggle-btn">
|
||
{{ sidebarCollapsed ? '→' : '←' }}
|
||
</button>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav">
|
||
<router-link to="/" class="nav-item" :class="{ active: isActive('/') }">
|
||
<span class="nav-icon">🏠</span>
|
||
<span class="nav-text">首页</span>
|
||
</router-link>
|
||
|
||
<router-link to="/conversations" class="nav-item" :class="{ active: isActive('/conversations') }">
|
||
<span class="nav-icon">💬</span>
|
||
<span class="nav-text">会话</span>
|
||
</router-link>
|
||
|
||
<router-link to="/tools" class="nav-item" :class="{ active: isActive('/tools') }">
|
||
<span class="nav-icon">🛠️</span>
|
||
<span class="nav-text">工具</span>
|
||
</router-link>
|
||
|
||
<router-link to="/about" class="nav-item" :class="{ active: isActive('/about') }">
|
||
<span class="nav-icon">ℹ️</span>
|
||
<span class="nav-text">关于</span>
|
||
</router-link>
|
||
</nav>
|
||
|
||
<div class="sidebar-footer">
|
||
<div class="user-menu-container">
|
||
<button @click="showUserMenu = !showUserMenu" class="user-button">
|
||
<div class="user-avatar">
|
||
{{ user?.username?.charAt(0).toUpperCase() || 'U' }}
|
||
</div>
|
||
<div class="user-info">
|
||
<span class="user-name">{{ user?.username }}</span>
|
||
<span class="user-role">{{ user?.role || '用户' }}</span>
|
||
</div>
|
||
</button>
|
||
|
||
<div v-if="showUserMenu" class="user-dropdown">
|
||
<button @click="logout" class="dropdown-item">
|
||
<span>🚪</span> 退出登录
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<main class="main-content" :class="{ 'no-sidebar': !isLoggedIn }">
|
||
<router-view />
|
||
</main>
|
||
|
||
<!-- 点击外部关闭菜单 -->
|
||
<div
|
||
v-if="showUserMenu"
|
||
class="overlay"
|
||
@click="showUserMenu = false"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
#app {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
background: var(--bg);
|
||
}
|
||
|
||
/* 侧边栏 */
|
||
.sidebar {
|
||
width: 260px;
|
||
background: var(--bg);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
height: 100vh;
|
||
z-index: 100;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.sidebar-collapsed .sidebar {
|
||
width: 70px;
|
||
}
|
||
|
||
/* 侧边栏头部 */
|
||
.sidebar-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1.25rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.brand-icon {
|
||
font-size: 1.75rem;
|
||
}
|
||
|
||
.brand-text {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
background: linear-gradient(135deg, var(--accent) 0%, #8b5cf6 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.sidebar-collapsed .brand-text,
|
||
.sidebar-collapsed .toggle-btn {
|
||
display: none;
|
||
}
|
||
|
||
.toggle-btn {
|
||
background: var(--code-bg);
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.5rem 0.75rem;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.toggle-btn:hover {
|
||
background: var(--accent-bg);
|
||
}
|
||
|
||
/* 导航 */
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
padding: 1rem 0.75rem;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.875rem;
|
||
padding: 0.875rem 1rem;
|
||
border-radius: 10px;
|
||
text-decoration: none;
|
||
color: var(--text);
|
||
margin-bottom: 0.25rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: var(--code-bg);
|
||
color: var(--text-h);
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: var(--accent-bg);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.nav-icon {
|
||
font-size: 1.25rem;
|
||
width: 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-text {
|
||
font-size: 0.95rem;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sidebar-collapsed .nav-text {
|
||
display: none;
|
||
}
|
||
|
||
.sidebar-collapsed .nav-item {
|
||
justify-content: center;
|
||
padding: 0.875rem;
|
||
}
|
||
|
||
/* 侧边栏底部 */
|
||
.sidebar-footer {
|
||
padding: 1rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.user-menu-container {
|
||
position: relative;
|
||
}
|
||
|
||
.user-button {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
background: var(--code-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.user-button:hover {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, var(--accent) 0%, #8b5cf6 100%);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.user-name {
|
||
font-weight: 600;
|
||
color: var(--text-h);
|
||
font-size: 0.95rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.user-role {
|
||
font-size: 0.8rem;
|
||
color: var(--text);
|
||
}
|
||
|
||
.sidebar-collapsed .user-info {
|
||
display: none;
|
||
}
|
||
|
||
.sidebar-collapsed .user-button {
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 下拉菜单 */
|
||
.user-dropdown {
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
margin-bottom: 0.5rem;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
box-shadow: 0 -5px 20px var(--shadow);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
width: 100%;
|
||
padding: 0.875rem 1rem;
|
||
background: transparent;
|
||
border: none;
|
||
text-align: left;
|
||
font-size: 0.95rem;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: #fef2f2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.main-content {
|
||
flex: 1;
|
||
margin-left: 260px;
|
||
padding: 2rem;
|
||
min-height: 100vh;
|
||
transition: margin-left 0.3s ease;
|
||
}
|
||
|
||
.main-content.no-sidebar {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.sidebar-collapsed .main-content {
|
||
margin-left: 70px;
|
||
}
|
||
|
||
/* 遮罩层 */
|
||
.overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 99;
|
||
}
|
||
|
||
/* 响应式 */
|
||
@media (max-width: 1024px) {
|
||
.sidebar {
|
||
width: 70px;
|
||
}
|
||
|
||
.brand-text,
|
||
.toggle-btn,
|
||
.nav-text,
|
||
.user-info {
|
||
display: none !important;
|
||
}
|
||
|
||
.nav-item {
|
||
justify-content: center;
|
||
padding: 0.875rem;
|
||
}
|
||
|
||
.user-button {
|
||
justify-content: center;
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 70px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
transform: translateX(-100%);
|
||
width: 260px;
|
||
}
|
||
|
||
.brand-text,
|
||
.toggle-btn,
|
||
.nav-text,
|
||
.user-info {
|
||
display: flex !important;
|
||
}
|
||
|
||
.nav-item {
|
||
justify-content: flex-start;
|
||
padding: 0.875rem 1rem;
|
||
}
|
||
|
||
.user-button {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.sidebar-collapsed .sidebar {
|
||
transform: translateX(-100%);
|
||
}
|
||
}
|
||
</style>
|