feat: 前端配置和组件更新

- App.vue, Sidebar.vue
- main.ts, router/index.ts
- package.json, vite.config.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:26:33 +08:00
parent 03540fb9e9
commit 85b4c51fd7
6 changed files with 294 additions and 68 deletions

View File

@@ -13,7 +13,9 @@
"@vue-office/excel": "^1.7.14", "@vue-office/excel": "^1.7.14",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.13.3", "element-plus": "^2.13.3",
"lucide-vue-next": "^0.577.0",
"marked": "^17.0.4", "marked": "^17.0.4",
"monaco-editor": "^0.55.1",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
@@ -28,6 +30,7 @@
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.3", "typescript": "^5.4.3",
"vite": "^5.2.8", "vite": "^5.2.8",
"vite-plugin-monaco-editor": "^1.1.0",
"vue-tsc": "^2.0.7" "vue-tsc": "^2.0.7"
} }
} }

View File

@@ -5,7 +5,7 @@ import { ElConfigProvider } from 'element-plus'
import Sidebar from '@/components/Sidebar.vue' import Sidebar from '@/components/Sidebar.vue'
const route = useRoute() const route = useRoute()
const showSidebar = computed(() => route.path !== '/') const showSidebar = computed(() => route.path !== '/login' && route.path !== '/' && route.path !== '/signup')
</script> </script>
<template> <template>

View File

@@ -1,16 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge' import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge'
import { useDatabase } from '@/views/database/useDatabase' import { useDatabase } from '@/views/database/useDatabase'
import { useAuth } from '@/composables/useAuth'
// 下拉菜单展开状态 // 下拉菜单展开状态
const userDropdownVisible = ref(false) const userDropdownVisible = ref(false)
// 退出确认弹窗状态
const showLogoutConfirm = ref(false)
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
// 获取当前用户信息
const { getUser, logout: authLogout } = useAuth()
const currentUser = ref(getUser())
// 计算用户首字母缩写
const userInitials = computed(() => {
const name = currentUser.value?.username || currentUser.value?.name || 'User'
const parts = name.split(' ')
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase()
}
return name.substring(0, 2).toUpperCase()
})
// 获取 Knowledge 数量 // 获取 Knowledge 数量
const knowledgeCount = ref(0) const knowledgeCount = ref(0)
const fetchKnowledgeCount = async () => { const fetchKnowledgeCount = async () => {
@@ -60,11 +77,11 @@ const group3 = computed(() => [
{ name: 'Memory', icon: 'fa-brain', path: '/memory' }, { name: 'Memory', icon: 'fa-brain', path: '/memory' },
]) ])
// 第4组: Dashboard, Account, Settings // 第4组: Dashboard, Models, Logs
const group4 = computed(() => [ const group4 = computed(() => [
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' }, { name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
{ name: 'Account', icon: 'fa-user', path: '/account' }, { name: 'Models', icon: 'fa-brain', path: '/settings' },
{ name: 'Settings', icon: 'fa-gear', path: '/settings' }, { name: 'Logs', icon: 'fa-file-lines', path: '/logs' },
]) ])
const activeMenu = computed(() => { const activeMenu = computed(() => {
@@ -83,31 +100,43 @@ const navigateTo = (item: MenuItem) => {
} }
} }
// 通知设置弹窗状态
const showNotificationSettings = ref(false)
// 用户菜单操作 // 用户菜单操作
const handleUserCommand = (command: string) => { const handleUserCommand = (command: string) => {
switch (command) { switch (command) {
case 'settings': case 'notifications':
// 全局设置 // 通知设置
router.push('/settings') showNotificationSettings.value = true
break
case 'account':
// 账户设置
router.push('/account')
break break
case 'userManagement': case 'userManagement':
// 用户管理 // 用户管理
router.push('/user-management') router.push('/user-management')
break break
case 'logout': case 'logout':
// 退出登录 // 退出登录 - 显示自定义确认弹窗
ElMessageBox.confirm('确定要退出登录吗?', '提示', { showLogoutConfirm.value = true
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 清除登录状态
localStorage.removeItem('token')
router.push('/login')
}).catch(() => {})
break break
} }
} }
// 确认退出登录
const confirmLogout = () => {
showLogoutConfirm.value = false
userDropdownVisible.value = false
authLogout()
router.push('/login')
}
// 取消退出
const cancelLogout = () => {
showLogoutConfirm.value = false
}
</script> </script>
<template> <template>
@@ -186,7 +215,7 @@ const handleUserCommand = (command: string) => {
<!-- 分隔线3 --> <!-- 分隔线3 -->
<li class="my-4 border-t border-dark-500"></li> <li class="my-4 border-t border-dark-500"></li>
<!-- 第4组: Dashboard, Account, Settings --> <!-- 第4组: Dashboard, Notifications, Account, Model Settings, Logs -->
<li v-for="item in group4" :key="item.name"> <li v-for="item in group4" :key="item.name">
<a <a
href="javascript:void(0)" href="javascript:void(0)"
@@ -198,11 +227,6 @@ const handleUserCommand = (command: string) => {
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i> <i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
</div> </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> </a>
</li> </li>
</ul> </ul>
@@ -210,65 +234,218 @@ const handleUserCommand = (command: string) => {
<!-- 底部用户信息 --> <!-- 底部用户信息 -->
<div class="border-t border-dark-500"> <div class="border-t border-dark-500">
<el-dropdown trigger="click" @command="handleUserCommand" class="user-dropdown" @visible-change="(v: boolean) => userDropdownVisible = v"> <div class="relative">
<div class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors"> <div
class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors"
@click="userDropdownVisible = !userDropdownVisible"
>
<div class="flex items-center gap-3"> <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="w-8 h-8 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center text-white font-medium text-xs">
{{ userInitials }}
</div>
<div class="min-w-0"> <div class="min-w-0">
<div class="font-medium text-sm text-gray-300 truncate">Alex Smith</div> <div class="font-medium text-sm text-gray-200 truncate">{{ currentUser?.username || currentUser?.name || 'User' }}</div>
<div class="text-xs text-gray-500 truncate">alex@gmail.com</div> <div class="text-xs text-gray-500 truncate">{{ currentUser?.email || 'user@example.com' }}</div>
</div> </div>
</div> </div>
<i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i> <i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i>
</div> </div>
<template #dropdown>
<el-dropdown-menu> <!-- 简洁下拉弹窗 -->
<el-dropdown-item command="settings"> <Transition name="dropdown-fade">
<i class="fa-solid fa-gear w-4 text-center"></i> <div v-if="userDropdownVisible" class="user-dropdown-panel" @click.stop>
Settings <div class="dropdown-menu">
</el-dropdown-item> <div class="menu-item" @click="handleUserCommand('notifications')">
<el-dropdown-item command="userManagement"> <i class="fa-solid fa-bell text-gray-400"></i>
<i class="fa-solid fa-user w-4 text-center"></i> <span class="text-gray-300">Notifications</span>
Users </div>
</el-dropdown-item>
<el-dropdown-item divided command="logout"> <div class="menu-item" @click="handleUserCommand('account')">
<i class="fa-solid fa-arrow-right-from-bracket w-4 text-center"></i> <i class="fa-solid fa-user text-gray-400"></i>
Sign Out <span class="text-gray-300">Account</span>
</el-dropdown-item> </div>
</el-dropdown-menu>
</template> <div class="menu-item">
</el-dropdown> <i class="fa-solid fa-circle-question text-gray-400"></i>
<span class="text-gray-300">Help & Support</span>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-footer">
<div class="menu-item logout" @click="handleUserCommand('logout')">
<i class="fa-solid fa-arrow-right-from-bracket text-gray-400"></i>
<span class="text-gray-300">Sign Out</span>
</div>
</div>
</div>
</Transition>
</div>
<!-- 点击外部关闭 -->
<div v-if="userDropdownVisible" class="fixed inset-0 z-40" @click="userDropdownVisible = false"></div>
</div> </div>
</aside> </aside>
<!-- 退出确认弹窗 -->
<Teleport to="body">
<Transition name="fade">
<div v-if="showLogoutConfirm" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelLogout">
<div class="bg-dark-700 rounded-xl w-full max-w-sm border border-dark-500 shadow-2xl" @click.stop>
<div class="p-6 text-center">
<div class="w-14 h-14 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-4">
<i class="fa-solid fa-arrow-right-from-bracket text-red-400 text-xl"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Sign Out</h3>
<p class="text-gray-400 text-sm">Are you sure you want to sign out?</p>
</div>
<div class="flex border-t border-dark-500">
<button @click="cancelLogout" class="flex-1 py-3 text-gray-400 hover:text-white hover:bg-dark-600 transition-colors">
Cancel
</button>
<button @click="confirmLogout" class="flex-1 py-3 text-red-400 hover:bg-red-500/10 transition-colors border-l border-dark-500">
Sign Out
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
<!-- 通知设置弹窗 -->
<Teleport to="body">
<Transition name="fade">
<div v-if="showNotificationSettings" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="showNotificationSettings = false">
<div class="bg-dark-700 rounded-xl w-full max-w-md border border-dark-500 shadow-2xl" @click.stop>
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Notification Settings</h3>
<button @click="showNotificationSettings = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div class="flex items-center justify-between py-3 border-b border-dark-500">
<div>
<div class="font-medium text-white">Email Notifications</div>
<div class="text-sm text-gray-400">Receive email for important updates</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" checked>
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-dark-500">
<div>
<div class="font-medium text-white">Push Notifications</div>
<div class="text-sm text-gray-400">Receive push notifications in browser</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" checked>
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-dark-500">
<div>
<div class="font-medium text-white">System Alerts</div>
<div class="text-sm text-gray-400">Get notified about system events</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" checked>
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
</label>
</div>
<div class="flex items-center justify-between py-3">
<div>
<div class="font-medium text-white">Agent Updates</div>
<div class="text-sm text-gray-400">Notifications about agent activities</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer">
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
</label>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button @click="showNotificationSettings = false" class="px-4 py-2 rounded-lg bg-primary-orange text-white hover:bg-orange-600 transition-colors">
Save Changes
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template> </template>
<style> <style>
.user-dropdown { /* 用户下拉弹窗 */
width: 100%; .user-dropdown-panel {
} position: absolute;
.user-dropdown .el-dropdown-menu { bottom: 100%;
background-color: #262626; left: 0;
border: none; right: 0;
padding: 6px; margin-bottom: 8px;
background: #1f1f1f;
border: 1px solid #404040;
border-radius: 10px; border-radius: 10px;
min-width: 200px; overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
z-index: 50;
} }
.user-dropdown .el-dropdown-menu__item {
color: white; .dropdown-menu {
padding: 8px 14px; padding: 6px;
border-radius: 6px; }
font-size: 13px;
.menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
font-size: 13px;
} }
.user-dropdown .el-dropdown-menu__item:hover {
background-color: #F97316; .menu-item:hover {
color: white; background: #333;
} }
.user-dropdown .el-dropdown-menu__item--divided {
border-top: 1px solid #404040; .dropdown-divider {
margin-top: 4px; height: 1px;
padding-top: 8px; background: #333;
margin: 4px 8px;
}
.dropdown-footer {
padding: 6px;
}
.menu-item.logout:hover {
background: rgba(239, 68, 68, 0.15);
}
/* 下拉动画 */
.dropdown-fade-enter-active,
.dropdown-fade-leave-active {
transition: all 0.15s ease;
}
.dropdown-fade-enter-from,
.dropdown-fade-leave-to {
opacity: 0;
transform: translateY(5px);
}
/* 退出确认弹窗动画 */
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
} }
</style> </style>

View File

@@ -1,4 +1,4 @@
import { createApp } from 'vue' import { createApp, type Component } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
@@ -6,8 +6,18 @@ import router from './router'
import './assets/styles/index.css' import './assets/styles/index.css'
import App from './App.vue' import App from './App.vue'
// 全局注册 Lucide 图标
import * as iconModule from 'lucide-vue-next'
const app = createApp(App) const app = createApp(App)
// 注册所有图标为全局组件(过滤非组件)
for (const [name, icon] of Object.entries(iconModule)) {
if (typeof icon === 'object' && icon !== null && 'render' in icon) {
app.component(name, icon as Component)
}
}
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)

View File

@@ -16,11 +16,23 @@ import Settings from '@/views/Settings.vue'
import Account from '@/views/Account.vue' import Account from '@/views/Account.vue'
import Logs from '@/views/Logs.vue' import Logs from '@/views/Logs.vue'
// 需要登录才能访问的路由
const protectedRoutes = ['/dashboard', '/chat', '/agents', '/team', '/mcp', '/tools', '/database', '/script', '/plan', '/memory', '/knowledge', '/settings', '/account', '/logs']
// 检查是否已登录
const isAuthenticated = () => {
return !!localStorage.getItem('token')
}
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
redirect: () => isAuthenticated() ? '/dashboard' : '/login'
},
{
path: '/login',
name: 'login', name: 'login',
component: Login component: Login
}, },
@@ -102,4 +114,24 @@ const router = createRouter({
] ]
}) })
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuth = isAuthenticated()
const isLoginPage = to.path === '/login' || to.path === '/signup'
// 如果未登录且不是访问登录/注册页,跳转到登录页
if (!isAuth && !isLoginPage) {
next('/login')
return
}
// 如果已登录且访问登录/注册页,跳转到仪表盘
if (isAuth && isLoginPage) {
next('/dashboard')
return
}
next()
})
export default router export default router

View File

@@ -1,9 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { resolve } from 'path' import { resolve } from 'path'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [
vue(),
(monacoEditorPlugin as any).default({})
],
resolve: { resolve: {
alias: { alias: {
'@': resolve(__dirname, 'src') '@': resolve(__dirname, 'src')