deotalandAi/apps/frontend/src/components/layout/AppSidebar.vue

701 lines
15 KiB
Vue

<template>
<aside class="app-sidebar" :class="sidebarClasses">
<!-- 导航菜单 -->
<nav class="sidebar-nav">
<ul class="nav-list">
<li v-for="item in coreMenuItems" :key="item.id">
<router-link
:to="item.path"
class="nav-item"
:class="{ 'active': isActiveRoute(item.path) }"
@click="handleNavClick(item)"
>
<div class="nav-icon">
<BrainIcon v-if="item.icon === 'BrainIcon'" />
<DashboardIcon v-else-if="item.icon === 'DashboardIcon'" />
<CreationIcon v-else-if="item.icon === 'CreationIcon'" />
<GalleryIcon v-else-if="item.icon === 'GalleryIcon'" />
<OrdersIcon v-else-if="item.icon === 'OrdersIcon'" />
</div>
<transition name="fade">
<span v-if="!collapsed" class="nav-text">{{ item.label }}</span>
</transition>
<transition name="fade">
<span v-if="!collapsed && item.badge" class="nav-badge">{{ item.badge }}</span>
</transition>
</router-link>
</li>
</ul>
</nav>
<!-- 侧边栏底部 -->
<div class="sidebar-footer">
<div class="user-profile" v-if="currentUser && !collapsed">
<div class="user-avatar-container">
<el-avatar :size="32" :src="currentUser.avatar">
<UserIcon />
</el-avatar>
<div class="online-status"></div>
</div>
<div class="user-info">
<p class="user-role">{{ getRoleDisplayName(currentUser.role) }}</p>
</div>
</div>
<!-- 折叠状态下的用户头像 -->
<div v-if="currentUser && collapsed" class="user-profile-collapsed">
<div class="user-avatar-container">
<el-avatar :size="32" :src="currentUser.avatar">
<UserIcon />
</el-avatar>
<div class="online-status"></div>
</div>
</div>
</div>
</aside>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
// 图标组件
import {
Cpu as BrainIcon,
House as DashboardIcon,
Folder as CreationIcon,
Picture as GalleryIcon,
ShoppingCart as OrdersIcon,
User as UserIcon,
Document as DocumentIcon,
VideoPlay as VideoIcon,
ChatDotRound as ChatIcon,
DataAnalysis as AnalyticsIcon,
Folder as ProjectIcon,
Bell as NotificationIcon,
Key as ApiIcon
} from '@element-plus/icons-vue'
export default {
name: 'AppSidebar',
components: {
BrainIcon,
DashboardIcon,
CreationIcon,
GalleryIcon,
OrdersIcon,
UserIcon,
DocumentIcon,
VideoIcon,
ChatIcon,
AnalyticsIcon,
ProjectIcon,
NotificationIcon,
ApiIcon
},
props: {
collapsed: {
type: Boolean,
default: false
}
},
emits: ['navigate'],
setup(props, { emit }) {
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
// 响应式状态
const isMobile = ref(window.innerWidth < 768)
// 计算属性
const currentUser = computed(() => authStore.user)
const sidebarClasses = computed(() => ({
'sidebar-mobile': isMobile.value
}))
// 核心菜单项 (6个主要功能)
const coreMenuItems = computed(() => [
{
id: 'dashboard',
path: '/',
label: t('sidebar.dashboard'),
icon: 'DashboardIcon',
badge: null
},
{
id: 'creation-workspace',
path: '/creation-workspace',
label: t('sidebar.creationWorkspace'),
icon: 'CreationIcon',
badge: null
},
{
id: 'agent-management',
path: '/agent-management',
label: t('sidebar.agentManagement.title'),
icon: 'BrainIcon',
badge: null
},
{
id: 'order-management',
path: '/order-management',
label: t('sidebar.orderManagement'),
icon: 'OrdersIcon',
badge: null
},
])
// 判断当前路由是否激活
const isActiveRoute = (path) => {
if (path === '/') {
return route.path === '/'
}
return route.path.startsWith(path)
}
// 处理导航点击
const handleNavClick = (item) => {
emit('navigate', item)
}
// 获取角色显示名称
const getRoleDisplayName = (role) => {
const roleMap = {
'creator': t('roles.creator'),
'admin': t('roles.admin'),
'viewer': t('roles.viewer')
}
return roleMap[role] || role
}
// 监听窗口大小变化
const handleResize = () => {
isMobile.value = window.innerWidth < 768
}
// 初始化和清理
window.addEventListener('resize', handleResize)
return {
t,
isMobile,
currentUser,
sidebarClasses,
coreMenuItems,
isActiveRoute,
handleNavClick,
getRoleDisplayName
}
}
}
</script>
<style scoped>
.app-sidebar {
display: flex;
flex-direction: column;
width: 120px;
height: 100%;
background: var(--sidebar-bg, #ffffff);
border-right: 1px solid var(--border-color, #e5e7eb);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
z-index: 100;
}
/* 侧边栏头部 */
.sidebar-header {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border-color, #e5e7eb);
height: 64px;
flex-shrink: 0;
}
/* 导航菜单 */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 20px 12px;
}
.nav-section {
margin-bottom: 24px;
}
.nav-section:last-child {
margin-bottom: 0;
}
.nav-section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #6b7280);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 8px 16px;
padding: 0;
}
.nav-list {
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px 12px;
margin: 6px 0;
border-radius: 14px;
color: var(--text-secondary, #6b7280);
text-decoration: none;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
overflow: hidden;
min-height: 80px;
width: 100%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(107, 70, 193, 0.05);
box-shadow:
0 2px 8px rgba(107, 70, 193, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
cursor: pointer;
}
.nav-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(107, 70, 193, 0.03) 0%, rgba(147, 51, 234, 0.02) 100%);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 14px;
}
.nav-item:hover {
background: rgba(107, 70, 193, 0.08);
color: #6B46C1;
transform: translateY(-3px) scale(1.02);
box-shadow:
0 12px 32px rgba(107, 70, 193, 0.2),
0 4px 12px rgba(107, 70, 193, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border-color: rgba(107, 70, 193, 0.3);
}
.nav-item:hover::before {
opacity: 1;
}
.nav-item:active {
transform: translateY(-1px) scale(0.99);
transition: all 0.15s ease;
}
.nav-item.active {
background: linear-gradient(135deg,
rgba(107, 70, 193, 0.18) 0%,
rgba(147, 51, 234, 0.15) 50%,
rgba(168, 85, 247, 0.12) 100%);
color: #6B46C1;
font-weight: 600;
border-color: rgba(107, 70, 193, 0.4);
box-shadow:
0 16px 40px rgba(107, 70, 193, 0.3),
0 6px 20px rgba(107, 70, 193, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 0 0 1px rgba(107, 70, 193, 0.1);
backdrop-filter: blur(12px) saturate(1.2);
}
.nav-item.active::before {
opacity: 1;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.05) 100%);
}
.nav-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-bottom: 10px;
flex-shrink: 0;
position: relative;
z-index: 1;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.nav-icon svg {
width: 24px;
height: 24px;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
filter: drop-shadow(0 2px 4px rgba(107, 70, 193, 0.15));
}
.nav-item:hover .nav-icon svg {
transform: scale(1.1);
filter: drop-shadow(0 4px 8px rgba(107, 70, 193, 0.3));
}
.nav-item.active .nav-icon svg {
filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5));
}
.nav-text {
font-size: 13px;
font-weight: 500;
text-align: center;
white-space: normal;
line-height: 1.4;
word-wrap: break-word;
hyphens: auto;
max-width: 100%;
position: relative;
z-index: 1;
letter-spacing: 0.01em;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.nav-badge {
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
background: #ef4444;
color: white;
font-size: 11px;
font-weight: 600;
border-radius: 10px;
padding: 0 6px;
margin-left: auto;
}
/* 侧边栏底部 */
.sidebar-footer {
padding: 24px 16px 16px;
border-top: 1px solid rgba(107, 70, 193, 0.08);
flex-shrink: 0;
backdrop-filter: blur(12px);
position: relative;
}
.sidebar-footer::before {
content: '';
position: absolute;
top: 0;
left: 16px;
right: 16px;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(107, 70, 193, 0.2) 20%,
rgba(107, 70, 193, 0.4) 50%,
rgba(107, 70, 193, 0.2) 80%,
transparent 100%);
}
.user-profile {
display: flex;
align-items: center;
flex-direction: column;
gap: 6px;
padding: 16px;
border-radius: 16px;
backdrop-filter: blur(16px);
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
overflow: hidden;
}
.user-profile:active {
transform: translateY(-1px) scale(1.01);
}
.user-avatar-container {
position: relative;
padding: 2px;
border-radius: 50%;
transition: all 0.3s ease;
}
.user-profile:hover .user-avatar-container {
box-shadow: 0 0 12px rgba(107, 70, 193, 0.2);
}
.online-status {
position: absolute;
bottom: 4px;
right: 2px;
width: 12px;
height: 12px;
background: #10B981;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 4px rgba(16, 185, 129, 0.4);
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
gap: 4px;
}
.user-name {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--text-primary, #1f2937);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.02em;
line-height: 1.3;
max-width: 100%;
background: linear-gradient(135deg, var(--text-primary, #1f2937), var(--text-secondary, #6b7280));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.user-role {
margin: 0;
font-size: 11px;
color: var(--text-secondary, #6b7280);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
line-height: 1.2;
padding: 2px 8px;
background: rgba(107, 70, 193, 0.08);
border-radius: 8px;
border: 1px solid rgba(107, 70, 193, 0.1);
transition: all 0.3s ease;
}
.user-profile:hover .user-role {
background: rgba(107, 70, 193, 0.12);
border-color: rgba(107, 70, 193, 0.18);
color: #8B5CF6;
}
.user-profile-collapsed {
display: flex;
justify-content: center;
padding: 12px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(107, 70, 193, 0.12);
border-radius: 16px;
backdrop-filter: blur(16px);
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
overflow: hidden;
}
.user-profile-collapsed::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.8) 50%,
transparent 100%);
}
.user-profile-collapsed:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(107, 70, 193, 0.2);
transform: translateY(-2px) scale(1.05);
box-shadow:
0 8px 32px rgba(107, 70, 193, 0.12),
0 2px 8px rgba(107, 70, 193, 0.08);
}
.user-profile-collapsed .user-avatar-container {
position: relative;
padding: 2px;
border-radius: 50%;
background: linear-gradient(135deg,
rgba(107, 70, 193, 0.1) 0%,
rgba(139, 92, 246, 0.2) 100%);
transition: all 0.3s ease;
}
.user-profile-collapsed:hover .user-avatar-container {
background: linear-gradient(135deg,
rgba(107, 70, 193, 0.2) 0%,
rgba(139, 92, 246, 0.3) 100%);
box-shadow: 0 0 12px rgba(107, 70, 193, 0.2);
}
/* 动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 移动端样式 */
.sidebar-mobile {
position: fixed;
left: 0;
top: 0;
z-index: 1000;
transform: translateX(-100%);
box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2);
transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
backdrop-filter: blur(12px);
border-right: 1px solid rgba(107, 70, 193, 0.1);
}
.sidebar-mobile.show {
transform: translateX(0);
}
/* 平板端优化 */
@media (min-width: 768px) and (max-width: 1024px) {
.app-sidebar {
width: 140px;
}
.nav-item {
padding: 18px 14px;
min-height: 84px;
}
.nav-icon {
width: 36px;
height: 36px;
margin-bottom: 12px;
}
.nav-icon svg {
width: 26px;
height: 26px;
}
.nav-text {
font-size: 14px;
}
}
/* 小屏设备优化 */
@media (max-width: 767px) {
.app-sidebar {
width: 100%;
max-width: 320px;
}
.nav-item {
padding: 16px 20px;
min-height: 72px;
margin: 8px 16px;
}
.nav-icon {
width: 32px;
height: 32px;
margin-bottom: 8px;
}
.nav-icon svg {
width: 22px;
height: 22px;
}
.nav-text {
font-size: 15px;
}
}
/* 深色主题 */
.dark .app-sidebar {
--sidebar-bg: #1f2937;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--border-color: #374151;
--hover-bg: rgba(255, 255, 255, 0.1);
}
.dark .nav-item.active {
background: rgba(139, 92, 246, 0.2);
color: #ffffff;
}
.dark .nav-item.active .nav-text {
color: #ffffff;
}
.dark .nav-item.active .nav-icon svg {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
}
.dark .nav-item.active .nav-icon {
color: #ffffff;
}
.dark .nav-item.active::before {
background: #8B5CF6;
}
.dark .logo-icon {
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 4px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: var(--border-color, #e5e7eb);
border-radius: 2px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary, #6b7280);
}
</style>