mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:56:36 +08:00
367 lines
7.6 KiB
Vue
367 lines
7.6 KiB
Vue
<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 = [
|
|
{ name: '全局热点池', icon: 'fa-solid fa-fire', route: '/' },
|
|
{ name: '公关修改追踪', icon: 'fa-solid fa-mask', route: '/revisions' },
|
|
{ name: '我的泛订阅', icon: 'fa-solid fa-rss', route: '/topics' },
|
|
{ name: 'AI 简报设置', icon: 'fa-solid fa-paper-plane', route: '/delivery' },
|
|
]
|
|
|
|
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">InsightRadar<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">
|
|
<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>
|
|
</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;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--status-success);
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
animation: pulse-dot 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
|
|
.logout-btn {
|
|
color: var(--text-secondary);
|
|
padding: 8px;
|
|
border-radius: var(--radius-md);
|
|
font-size: 14px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.logout-btn:hover {
|
|
color: var(--status-error);
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
/* ==========================================
|
|
主内容区
|
|
========================================== */
|
|
.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>
|