Files
InsightRadar/frontend/src/layouts/DashboardLayout.vue
T
stardrophere f4d9b2075c 改名
2026-04-02 01:25:30 +08:00

403 lines
8.7 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 { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import BrandLogo from '@/components/BrandLogo.vue'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
const sidebarOpen = ref(false)
const displayName = computed(() => authStore.user?.nickname || authStore.user?.email?.split('@')[0] || '用户')
const avatarUrl = computed(
() =>
authStore.user?.avatar_url ||
`https://ui-avatars.com/api/?name=${encodeURIComponent(displayName.value)}&background=6366f1&color=fff`,
)
const navItems = computed(() => {
const items = [
{ name: '全局热点池', icon: 'fa-solid fa-fire', route: '/' },
{ name: '事件追溯分析', icon: 'fa-solid fa-chart-line', route: '/search' },
{ name: '公关修改追踪', icon: 'fa-solid fa-mask', route: '/revisions' },
]
if (authStore.isAuthenticated) {
items.push({ name: '我的泛订阅', icon: 'fa-solid fa-rss', route: '/topics' })
items.push({ name: 'AI 简报设置', icon: 'fa-solid fa-paper-plane', route: '/delivery' })
}
return items
})
function isActive(path: string) {
return route.path === path
}
async function handleLogout() {
authStore.logout()
await router.replace('/login')
}
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
</script>
<template>
<div class="dashboard-shell">
<!-- 移动端侧边栏遮罩 -->
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
<!-- 侧边栏 -->
<aside class="sidebar" :class="{ open: sidebarOpen }">
<div class="sidebar-inner">
<!-- Logo -->
<div class="sidebar-logo">
<BrandLogo />
<span class="logo-text">聚势智见<span class="logo-dot">.AI</span></span>
</div>
<!-- 导航菜单 -->
<nav class="sidebar-nav">
<RouterLink
v-for="item in navItems"
:key="item.route"
:to="item.route"
class="nav-item"
:class="{ active: isActive(item.route) }"
@click="sidebarOpen = false"
>
<i :class="item.icon" class="nav-icon"></i>
<span>{{ item.name }}</span>
</RouterLink>
</nav>
</div>
<!-- 用户信息 -->
<div class="sidebar-user" v-if="authStore.isAuthenticated">
<img :src="avatarUrl" class="user-avatar" alt="头像" />
<div class="user-info">
<p class="user-name">{{ displayName }}</p>
<p class="user-status">
<span class="status-dot"></span>
已登录
</p>
</div>
<button class="logout-btn" title="退出登录" @click="handleLogout">
<i class="fa-solid fa-right-from-bracket"></i>
</button>
</div>
<div class="sidebar-user guest-user" v-else>
<div class="user-info">
<p class="user-name">游客您好</p>
<p class="user-status guest">
<span class="status-dot gray"></span>
未登录
</p>
</div>
<button class="login-btn" title="前往登录" @click="router.push('/login')">
<i class="fa-solid fa-arrow-right-to-bracket"></i>
</button>
</div>
</aside>
<!-- 主内容区 -->
<main class="main-area">
<!-- 顶部通栏 -->
<header class="top-header">
<button class="menu-toggle" @click="toggleSidebar">
<i class="fa-solid fa-bars"></i>
</button>
<div class="header-right">
<ThemeToggle />
</div>
</header>
<!-- 页面内容插槽 -->
<div class="page-content">
<RouterView v-slot="{ Component }">
<transition name="page-fade" mode="out-in">
<component :is="Component" />
</transition>
</RouterView>
</div>
</main>
</div>
</template>
<style scoped>
.dashboard-shell {
display: flex;
height: 100vh;
overflow: hidden;
}
/* ==========================================
侧边栏
========================================== */
.sidebar {
width: 260px;
min-width: 260px;
/* 增加侧边栏的毛玻璃高级感 */
background: var(--bg-surface);
backdrop-filter: var(--backdrop-blur);
-webkit-backdrop-filter: var(--backdrop-blur);
border-right: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 40;
transition: transform 0.3s ease;
}
.sidebar-inner {
flex: 1;
overflow-y: auto;
}
.sidebar-logo {
height: 64px;
display: flex;
align-items: center;
padding: 0 20px;
gap: 12px;
border-bottom: 1px solid var(--border-subtle);
}
.logo-text {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.02em;
}
.logo-dot {
color: var(--brand-primary);
}
/* 导航 */
.sidebar-nav {
padding: 16px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-item:hover {
color: var(--text-primary);
background: var(--bg-hover);
transform: translateX(4px);
}
.nav-item.active {
color: var(--brand-primary);
background: var(--brand-primary-alpha);
border-left: 3px solid var(--brand-primary);
padding-left: 13px; /* 减去 border 的 3px 保持布局不跳动 */
font-weight: 600;
}
.nav-icon {
width: 18px;
text-align: center;
font-size: 15px;
}
/* 用户区 */
.sidebar-user {
padding: 16px 20px;
border-top: 1px solid var(--border-subtle);
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 13px;
font-weight: 600;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-status {
font-size: 11px;
color: var(--status-success);
margin: 2px 0 0;
display: flex;
align-items: center;
gap: 4px;
}
.user-status.guest {
color: var(--text-placeholder);
}
.status-dot {
width: 6px;
height: 6px;
background: var(--status-success);
border-radius: 50%;
display: inline-block;
animation: pulse-dot 2s infinite;
}
.status-dot.gray {
background: var(--text-placeholder);
animation: none;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.logout-btn, .login-btn {
color: var(--text-secondary);
padding: 8px;
border-radius: var(--radius-md);
font-size: 14px;
transition: all 0.2s;
cursor: pointer;
background: transparent;
border: none;
}
.logout-btn:hover {
color: var(--status-error);
background: rgba(239, 68, 68, 0.1);
}
.login-btn:hover {
color: var(--brand-primary);
background: var(--brand-primary-alpha);
}
/* ==========================================
主内容区
========================================== */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.top-header {
height: 60px;
min-height: 60px;
/* 顶部导航毛玻璃 */
background: var(--bg-surface);
backdrop-filter: var(--backdrop-blur);
-webkit-backdrop-filter: var(--backdrop-blur);
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 10;
}
.menu-toggle {
display: none;
font-size: 18px;
color: var(--text-secondary);
padding: 8px;
border-radius: var(--radius-md);
}
.menu-toggle:hover {
background: var(--bg-input);
color: var(--text-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
margin-left: auto;
}
.page-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* ==========================================
移动端适配
========================================== */
.sidebar-overlay {
display: none;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 30;
}
.menu-toggle {
display: block;
}
.page-content {
padding: 16px;
}
}
/* 页面过渡动画 */
.page-fade-enter-active,
.page-fade-leave-active {
transition: opacity 0.2s ease;
}
.page-fade-enter-from,
.page-fade-leave-to {
opacity: 0;
}
</style>