2026-03-05 10:49:46 +08:00
|
|
|
<script setup lang="ts">
|
2026-03-09 16:08:38 +08:00
|
|
|
import { computed, ref, onMounted } from 'vue'
|
2026-03-05 10:49:46 +08:00
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
2026-03-07 13:53:35 +08:00
|
|
|
import { ElMessageBox } from 'element-plus'
|
2026-03-09 16:08:38 +08:00
|
|
|
import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge'
|
|
|
|
|
import { useDatabase } from '@/views/database/useDatabase'
|
2026-03-07 13:53:35 +08:00
|
|
|
|
|
|
|
|
// 下拉菜单展开状态
|
|
|
|
|
const userDropdownVisible = ref(false)
|
2026-03-05 10:49:46 +08:00
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
// 获取 Knowledge 数量
|
|
|
|
|
const knowledgeCount = ref(0)
|
|
|
|
|
const fetchKnowledgeCount = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const data = await fetchKnowledgeBases()
|
|
|
|
|
knowledgeCount.value = data?.length || 0
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to fetch knowledge count:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取 Database 数量
|
|
|
|
|
const { databases, fetchDatabases } = useDatabase()
|
|
|
|
|
const databaseCount = computed(() => databases.value?.length || 0)
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchKnowledgeCount()
|
|
|
|
|
fetchDatabases()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-05 10:49:46 +08:00
|
|
|
interface MenuItem {
|
|
|
|
|
name: string
|
|
|
|
|
icon: string
|
|
|
|
|
badge?: string | number
|
|
|
|
|
children?: MenuItem[]
|
|
|
|
|
path?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
const mainMenu = computed<MenuItem[]>(() => [
|
2026-03-05 10:49:46 +08:00
|
|
|
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
|
|
|
|
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
2026-03-09 16:08:38 +08:00
|
|
|
{ name: 'Script', icon: 'fa-code', path: '/script' },
|
|
|
|
|
{ name: 'Database', icon: 'fa-database', path: '/database', badge: databaseCount.value },
|
|
|
|
|
{ name: 'Knowledge', icon: 'fa-brain', path: '/knowledge', badge: knowledgeCount.value },
|
|
|
|
|
])
|
2026-03-05 10:49:46 +08:00
|
|
|
|
2026-03-09 15:43:07 +08:00
|
|
|
const middleMenu: MenuItem[] = [
|
|
|
|
|
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
|
|
|
|
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-05 10:49:46 +08:00
|
|
|
const bottomMenu: MenuItem[] = [
|
2026-03-07 13:53:35 +08:00
|
|
|
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
2026-03-05 10:49:46 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const bottomMenu2: MenuItem[] = [
|
2026-03-09 16:08:38 +08:00
|
|
|
{ name: 'Account', icon: 'fa-user', path: '/account' },
|
2026-03-05 10:49:46 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const activeMenu = computed(() => {
|
|
|
|
|
const currentPath = route.path
|
2026-03-07 13:53:35 +08:00
|
|
|
// Check main menu
|
2026-03-09 16:08:38 +08:00
|
|
|
const menuItem = mainMenu.value.find(item => item.path === currentPath)
|
2026-03-07 13:53:35 +08:00
|
|
|
if (menuItem) return menuItem.name
|
2026-03-09 16:08:38 +08:00
|
|
|
// Check middle menu (Skills, Tools)
|
|
|
|
|
const middleItem = middleMenu.find(item => item.path === currentPath)
|
|
|
|
|
if (middleItem) return middleItem.name
|
|
|
|
|
// Check bottom menu (Settings)
|
2026-03-07 13:53:35 +08:00
|
|
|
const bottomItem = bottomMenu.find(item => item.path === currentPath)
|
|
|
|
|
if (bottomItem) return bottomItem.name
|
2026-03-09 16:08:38 +08:00
|
|
|
// Check bottomMenu2 (Account)
|
|
|
|
|
const bottomItem2 = bottomMenu2.find(item => item.path === currentPath)
|
|
|
|
|
if (bottomItem2) return bottomItem2.name
|
2026-03-07 13:53:35 +08:00
|
|
|
return 'Dashboard'
|
2026-03-05 10:49:46 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const navigateTo = (item: MenuItem) => {
|
|
|
|
|
if (item.path) {
|
|
|
|
|
router.push(item.path)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 13:53:35 +08:00
|
|
|
|
|
|
|
|
// 用户菜单操作
|
|
|
|
|
const handleUserCommand = (command: string) => {
|
|
|
|
|
switch (command) {
|
|
|
|
|
case 'settings':
|
|
|
|
|
// 全局设置
|
|
|
|
|
router.push('/settings')
|
|
|
|
|
break
|
|
|
|
|
case 'userManagement':
|
|
|
|
|
// 用户管理
|
|
|
|
|
router.push('/user-management')
|
|
|
|
|
break
|
|
|
|
|
case 'logout':
|
|
|
|
|
// 退出登录
|
|
|
|
|
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
type: 'warning',
|
|
|
|
|
}).then(() => {
|
|
|
|
|
// 清除登录状态
|
|
|
|
|
localStorage.removeItem('token')
|
|
|
|
|
router.push('/login')
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-05 10:49:46 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<aside class="w-64 bg-dark-950 h-screen flex flex-col fixed left-0 top-0 overflow-y-auto scrollbar-hide z-10">
|
|
|
|
|
<!-- 顶部Logo与组织信息 -->
|
|
|
|
|
<div class="p-5 flex items-center gap-3">
|
|
|
|
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
|
|
|
|
<i class="fa-solid fa-basketball text-white"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="font-semibold text-base flex items-center gap-1 cursor-pointer">
|
|
|
|
|
Organization
|
|
|
|
|
<i class="fa-solid fa-chevron-down text-xs text-gray-500"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-sm text-gray-500">Saasfactor</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 导航菜单 -->
|
|
|
|
|
<nav class="flex-1 px-3 py-2">
|
|
|
|
|
<ul class="space-y-1">
|
2026-03-09 16:08:38 +08:00
|
|
|
<!-- Dashboard, Agents -->
|
|
|
|
|
<li v-for="item in mainMenu.slice(0, 2)" :key="item.name">
|
2026-03-05 10:49:46 +08:00
|
|
|
<a
|
|
|
|
|
href="#"
|
|
|
|
|
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
|
|
|
|
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
|
|
|
|
@click="navigateTo(item)"
|
2026-03-09 15:43:07 +08:00
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
|
|
|
|
<span>{{ item.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="item.badge" class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">{{ item.badge }}</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
<!-- 分隔线1 -->
|
2026-03-09 15:43:07 +08:00
|
|
|
<li class="my-4 border-t border-dark-500"></li>
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
<!-- Database, Knowledge -->
|
|
|
|
|
<li v-for="item in mainMenu.slice(2)" :key="item.name">
|
2026-03-09 15:43:07 +08:00
|
|
|
<a
|
|
|
|
|
href="#"
|
|
|
|
|
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
|
|
|
|
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
|
|
|
|
@click="navigateTo(item)"
|
2026-03-05 10:49:46 +08:00
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
|
|
|
|
<span>{{ item.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="item.badge" class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">{{ item.badge }}</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
<!-- 分隔线2 -->
|
2026-03-05 10:49:46 +08:00
|
|
|
<li class="my-4 border-t border-dark-500"></li>
|
|
|
|
|
|
2026-03-09 16:08:38 +08:00
|
|
|
<!-- Skills & Tools -->
|
|
|
|
|
<li v-for="item in middleMenu" :key="item.name">
|
|
|
|
|
<a
|
|
|
|
|
href="#"
|
|
|
|
|
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
|
|
|
|
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
|
|
|
|
@click="navigateTo(item)"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
|
|
|
|
<span>{{ item.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="item.badge" class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">{{ item.badge }}</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
|
2026-03-08 20:34:36 +08:00
|
|
|
<!-- Settings -->
|
2026-03-05 10:49:46 +08:00
|
|
|
<li v-for="item in bottomMenu" :key="item.name">
|
|
|
|
|
<a
|
|
|
|
|
href="#"
|
2026-03-07 13:53:35 +08:00
|
|
|
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
|
|
|
|
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
|
|
|
|
@click="navigateTo(item)"
|
2026-03-05 10:49:46 +08:00
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
|
|
|
|
<span>{{ item.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="item.name === 'Settings'" class="flex gap-1">
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
|
|
<!-- 分隔线 -->
|
|
|
|
|
<li class="my-4 border-t border-dark-500"></li>
|
|
|
|
|
|
2026-03-08 20:34:36 +08:00
|
|
|
<!-- Account -->
|
2026-03-05 10:49:46 +08:00
|
|
|
<li v-for="item in bottomMenu2" :key="item.name">
|
|
|
|
|
<a
|
|
|
|
|
href="#"
|
2026-03-09 16:08:38 +08:00
|
|
|
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
|
|
|
|
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
2026-03-07 13:53:35 +08:00
|
|
|
@click="navigateTo(item)"
|
2026-03-05 10:49:46 +08:00
|
|
|
>
|
2026-03-09 16:08:38 +08:00
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
|
|
|
|
<span>{{ item.name }}</span>
|
|
|
|
|
</div>
|
2026-03-05 10:49:46 +08:00
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<!-- 底部用户信息 -->
|
2026-03-07 13:53:35 +08:00
|
|
|
<div class="border-t border-dark-500">
|
|
|
|
|
<el-dropdown trigger="click" @command="handleUserCommand" class="user-dropdown" @visible-change="(v: boolean) => userDropdownVisible = v">
|
|
|
|
|
<div class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<img src="https://picsum.photos/id/64/40/40" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<div class="font-medium text-sm text-gray-300 truncate">Alex Smith</div>
|
|
|
|
|
<div class="text-xs text-gray-500 truncate">alex@gmail.com</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i>
|
2026-03-05 10:49:46 +08:00
|
|
|
</div>
|
2026-03-07 13:53:35 +08:00
|
|
|
<template #dropdown>
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
<el-dropdown-item command="settings">
|
|
|
|
|
<i class="fa-solid fa-gear w-4 text-center"></i>
|
|
|
|
|
Settings
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item command="userManagement">
|
|
|
|
|
<i class="fa-solid fa-user w-4 text-center"></i>
|
|
|
|
|
Users
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item divided command="logout">
|
|
|
|
|
<i class="fa-solid fa-arrow-right-from-bracket w-4 text-center"></i>
|
|
|
|
|
Sign Out
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dropdown>
|
2026-03-05 10:49:46 +08:00
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</template>
|
2026-03-07 13:53:35 +08:00
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.user-dropdown {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.user-dropdown .el-dropdown-menu {
|
|
|
|
|
background-color: #262626;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 6px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
}
|
|
|
|
|
.user-dropdown .el-dropdown-menu__item {
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
.user-dropdown .el-dropdown-menu__item:hover {
|
|
|
|
|
background-color: #F97316;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.user-dropdown .el-dropdown-menu__item--divided {
|
|
|
|
|
border-top: 1px solid #404040;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|