nex_docus/frontend/src/components/MainLayout/AppSider.jsx

346 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import {
DashboardOutlined,
DesktopOutlined,
GlobalOutlined,
CloudServerOutlined,
FileSearchOutlined,
UserOutlined,
AppstoreOutlined,
SettingOutlined,
BlockOutlined,
FolderOutlined,
FileTextOutlined,
SafetyOutlined,
TeamOutlined,
ProjectOutlined,
RocketOutlined,
ReadOutlined,
BookOutlined,
CommentOutlined,
} from '@ant-design/icons'
import { getUserMenus } from '@/api/menu'
import Toast from '@/components/Toast/Toast'
import useUserStore from '@/stores/userStore'
import ModernSidebar from '../ModernSidebar/ModernSidebar'
// 图标映射
const iconMap = {
DashboardOutlined: <DashboardOutlined />,
DesktopOutlined: <DesktopOutlined />,
GlobalOutlined: <GlobalOutlined />,
CloudServerOutlined: <CloudServerOutlined />,
FileSearchOutlined: <FileSearchOutlined />,
UserOutlined: <UserOutlined />,
AppstoreOutlined: <AppstoreOutlined />,
SettingOutlined: <SettingOutlined />,
BlockOutlined: <BlockOutlined />,
FolderOutlined: <FolderOutlined />,
FileTextOutlined: <FileTextOutlined />,
SafetyOutlined: <SafetyOutlined />,
TeamOutlined: <TeamOutlined />,
ProjectOutlined: <ProjectOutlined />,
ReadOutlined: <ReadOutlined />,
BookOutlined: <BookOutlined />,
CommentOutlined: <CommentOutlined />,
}
const builtInMenuMetaMap = {
dashboard: {
path: '/dashboard',
icon: 'DashboardOutlined',
},
desktop: {
path: '/desktop',
icon: 'DesktopOutlined',
},
'projects:my': {
path: '/projects/my',
icon: 'FolderOutlined',
},
'projects:share': {
path: '/projects/share',
icon: 'TeamOutlined',
},
'knowledge:my': {
path: '/chat',
icon: 'CommentOutlined',
},
'system:users': {
path: '/system/users',
icon: 'UserOutlined',
},
'system:roles': {
path: '/system/roles',
icon: 'TeamOutlined',
},
'system:permissions': {
path: '/system/permissions',
icon: 'SafetyOutlined',
},
'system:model-configs': {
path: '/system/model-configs',
icon: 'CloudServerOutlined',
},
'system:logs': {
path: '/system/logs',
icon: 'FileSearchOutlined',
},
}
const resolveBuiltInMenuMeta = (item) => {
const directMeta = builtInMenuMetaMap[item.menu_code]
if (directMeta) {
return directMeta
}
if (item.path === '/dashboard' || item.menu_name === '管理面板') {
return {
path: '/dashboard',
icon: 'DashboardOutlined',
}
}
if (item.path === '/desktop' || item.menu_name === '个人桌面') {
return {
path: '/desktop',
icon: 'DesktopOutlined',
}
}
if (
item.path === '/projects' ||
item.path === '/projects/my' ||
item.menu_name === '我的项目' ||
item.menu_name === '项目空间'
) {
return {
path: '/projects/my',
icon: 'FolderOutlined',
}
}
if (item.path === '/projects/share' || item.menu_name === '参与项目') {
return {
path: '/projects/share',
icon: 'TeamOutlined',
}
}
if (item.path === '/knowledge' || item.path === '/knowledge/my' || item.menu_name === '我的知识库') {
return {
path: '/chat',
icon: 'CommentOutlined',
}
}
if (item.path === '/system/users' || item.menu_name === '用户管理') {
return {
path: '/system/users',
icon: 'UserOutlined',
}
}
if (item.path === '/system/roles' || item.menu_name === '角色管理') {
return {
path: '/system/roles',
icon: 'TeamOutlined',
}
}
if (item.path === '/system/permissions' || item.menu_name === '权限管理') {
return {
path: '/system/permissions',
icon: 'SafetyOutlined',
}
}
if (item.path === '/system/model-configs' || item.menu_name === '模型配置') {
return {
path: '/system/model-configs',
icon: 'CloudServerOutlined',
}
}
if (item.path === '/system/logs' || item.menu_name === '系统日志') {
return {
path: '/system/logs',
icon: 'FileSearchOutlined',
}
}
return null
}
function AppSider({ collapsed, onToggle }) {
const navigate = useNavigate()
const location = useLocation()
const { user, logout } = useUserStore()
const [menuGroups, setMenuGroups] = useState([])
// 加载菜单数据
useEffect(() => {
loadMenus()
}, [])
const loadMenus = async () => {
try {
const res = await getUserMenus()
if (res.data) {
// 过滤菜单:只显示 type=1 (目录) 和 type=2 (菜单)
const validMenus = res.data.filter(item => [1, 2].includes(item.menu_type))
transformMenuData(validMenus)
}
} catch (error) {
console.error('Load menus error:', error)
Toast.error('加载菜单失败')
}
}
const transformMenuData = (data) => {
const groups = []
// 默认组 (用于存放一级菜单即是叶子节点的情况)
const defaultGroup = {
title: '', // 空标题或 '通用'
items: []
}
data.forEach(item => {
// 检查是否有子菜单
const validChildren = item.children ? item.children.filter(child => [1, 2].includes(child.menu_type)) : []
if (validChildren.length > 0) {
// 一级菜单作为组标题
const groupItems = validChildren.map(child => {
const normalizedChild = normalizeMenuItem(child)
const icon = typeof normalizedChild.icon === 'string'
? (iconMap[normalizedChild.icon] || <AppstoreOutlined />)
: normalizedChild.icon
return {
key: normalizedChild.menu_code,
label: normalizedChild.menu_name,
icon: icon,
path: normalizedChild.path
}
})
groups.push({
title: item.menu_name, // e.g. "系统管理"
items: groupItems
})
} else {
// 一级菜单是叶子节点,放入默认组
const normalizedItem = normalizeMenuItem(item)
const icon = typeof normalizedItem.icon === 'string'
? (iconMap[normalizedItem.icon] || <AppstoreOutlined />)
: normalizedItem.icon
defaultGroup.items.push({
key: normalizedItem.menu_code,
label: normalizedItem.menu_name,
icon: icon,
path: normalizedItem.path
})
}
})
// 如果默认组有内容,放在最前面
if (defaultGroup.items.length > 0) {
groups.unshift(defaultGroup)
}
setMenuGroups(groups)
}
const normalizeMenuItem = (item) => {
const builtInMeta = resolveBuiltInMenuMeta(item)
if (!builtInMeta) {
return item
}
return {
...item,
icon: builtInMeta.icon || item.icon,
path: builtInMeta.path || item.path,
}
}
const handleNavigate = (key, item) => {
if (item.path) {
navigate(item.path)
}
}
const handleLogout = () => {
logout()
navigate('/login')
}
const handleProfileClick = () => {
navigate('/profile')
}
// 获取当前激活的 key
// 简单匹配 path
const getActiveKey = () => {
const path = location.pathname
// 遍历所有 items 找匹配
for (const group of menuGroups) {
for (const item of group.items) {
if (item.path === path) return item.key
}
}
return ''
}
const logoNode = (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<img src="/favicon.svg" alt="logo" style={{ width: 32, height: 32 }} />
{!collapsed && (
<span style={{ fontSize: 18, fontWeight: 'bold', color: 'var(--text-color)' }}>NexDocus</span>
)}
</div>
)
// 获取用户头像URL
const getUserAvatarUrl = () => {
if (!user?.avatar) return null
// avatar 字段存储的是相对路径2/avatar/xxx.jpg
// 需要转换为 API 端点: /api/v1/auth/avatar/{user_id}/{filename}
// 如果已经是 http 开头(第三方),则直接返回
if (user.avatar.startsWith('http')) return user.avatar
const parts = user.avatar.split('/')
if (parts.length >= 3) {
const userId = parts[0]
const filename = parts[2]
return `/api/v1/auth/avatar/${userId}/${filename}`
}
return null
}
const userObj = user ? {
name: user.nickname || user.username,
role: user.role_name || 'Admin',
avatar: getUserAvatarUrl()
} : null
return (
<ModernSidebar
logo={logoNode}
menuGroups={menuGroups}
activeKey={getActiveKey()}
onNavigate={handleNavigate}
user={userObj}
onLogout={handleLogout}
onProfileClick={handleProfileClick}
collapsed={collapsed}
onCollapse={onToggle}
/>
)
}
export default AppSider