2025-12-20 11:18:59 +00:00
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
|
import { useNavigate, useLocation } from 'react-router-dom'
|
|
|
|
|
|
import {
|
|
|
|
|
|
DashboardOutlined,
|
|
|
|
|
|
DesktopOutlined,
|
|
|
|
|
|
GlobalOutlined,
|
|
|
|
|
|
CloudServerOutlined,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
FileSearchOutlined,
|
2025-12-20 11:18:59 +00:00
|
|
|
|
UserOutlined,
|
|
|
|
|
|
AppstoreOutlined,
|
|
|
|
|
|
SettingOutlined,
|
|
|
|
|
|
BlockOutlined,
|
|
|
|
|
|
FolderOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
2025-12-23 05:02:10 +00:00
|
|
|
|
SafetyOutlined,
|
|
|
|
|
|
TeamOutlined,
|
2026-01-30 05:00:57 +00:00
|
|
|
|
ProjectOutlined,
|
|
|
|
|
|
RocketOutlined,
|
|
|
|
|
|
ReadOutlined,
|
|
|
|
|
|
BookOutlined,
|
2025-12-20 11:18:59 +00:00
|
|
|
|
} from '@ant-design/icons'
|
2026-01-30 05:00:57 +00:00
|
|
|
|
import { message } from 'antd'
|
2025-12-20 11:18:59 +00:00
|
|
|
|
import { getUserMenus } from '@/api/menu'
|
2026-01-30 05:00:57 +00:00
|
|
|
|
import useUserStore from '@/stores/userStore'
|
|
|
|
|
|
import ModernSidebar from '../ModernSidebar/ModernSidebar'
|
2025-12-20 11:18:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 图标映射
|
|
|
|
|
|
const iconMap = {
|
2026-01-30 05:00:57 +00:00
|
|
|
|
DashboardOutlined: <DashboardOutlined />,
|
|
|
|
|
|
DesktopOutlined: <DesktopOutlined />,
|
|
|
|
|
|
GlobalOutlined: <GlobalOutlined />,
|
|
|
|
|
|
CloudServerOutlined: <CloudServerOutlined />,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
FileSearchOutlined: <FileSearchOutlined />,
|
2026-01-30 05:00:57 +00:00
|
|
|
|
UserOutlined: <UserOutlined />,
|
|
|
|
|
|
AppstoreOutlined: <AppstoreOutlined />,
|
|
|
|
|
|
SettingOutlined: <SettingOutlined />,
|
|
|
|
|
|
BlockOutlined: <BlockOutlined />,
|
|
|
|
|
|
FolderOutlined: <FolderOutlined />,
|
|
|
|
|
|
FileTextOutlined: <FileTextOutlined />,
|
|
|
|
|
|
SafetyOutlined: <SafetyOutlined />,
|
|
|
|
|
|
TeamOutlined: <TeamOutlined />,
|
|
|
|
|
|
ProjectOutlined: <ProjectOutlined />,
|
|
|
|
|
|
ReadOutlined: <ReadOutlined />,
|
|
|
|
|
|
BookOutlined: <BookOutlined />,
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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: '/knowledge',
|
|
|
|
|
|
icon: 'ReadOutlined',
|
|
|
|
|
|
},
|
|
|
|
|
|
'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: '/knowledge',
|
|
|
|
|
|
icon: 'ReadOutlined',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 11:18:59 +00:00
|
|
|
|
function AppSider({ collapsed, onToggle }) {
|
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const location = useLocation()
|
2026-01-30 05:00:57 +00:00
|
|
|
|
const { user, logout } = useUserStore()
|
|
|
|
|
|
const [menuGroups, setMenuGroups] = useState([])
|
2025-12-20 11:18:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 加载菜单数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadMenus()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const loadMenus = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getUserMenus()
|
|
|
|
|
|
if (res.data) {
|
2026-01-30 05:00:57 +00:00
|
|
|
|
// 过滤菜单:只显示 type=1 (目录) 和 type=2 (菜单)
|
|
|
|
|
|
const validMenus = res.data.filter(item => [1, 2].includes(item.menu_type))
|
|
|
|
|
|
transformMenuData(validMenus)
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Load menus error:', error)
|
|
|
|
|
|
message.error('加载菜单失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
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 => {
|
2026-04-08 17:03:57 +00:00
|
|
|
|
const normalizedChild = normalizeMenuItem(child)
|
|
|
|
|
|
const icon = typeof normalizedChild.icon === 'string'
|
|
|
|
|
|
? (iconMap[normalizedChild.icon] || <AppstoreOutlined />)
|
|
|
|
|
|
: normalizedChild.icon
|
2026-01-30 05:00:57 +00:00
|
|
|
|
return {
|
2026-04-08 17:03:57 +00:00
|
|
|
|
key: normalizedChild.menu_code,
|
|
|
|
|
|
label: normalizedChild.menu_name,
|
2026-01-30 05:00:57 +00:00
|
|
|
|
icon: icon,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
path: normalizedChild.path
|
2026-01-30 05:00:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
groups.push({
|
|
|
|
|
|
title: item.menu_name, // e.g. "系统管理"
|
|
|
|
|
|
items: groupItems
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 一级菜单是叶子节点,放入默认组
|
2026-04-08 17:03:57 +00:00
|
|
|
|
const normalizedItem = normalizeMenuItem(item)
|
|
|
|
|
|
const icon = typeof normalizedItem.icon === 'string'
|
|
|
|
|
|
? (iconMap[normalizedItem.icon] || <AppstoreOutlined />)
|
|
|
|
|
|
: normalizedItem.icon
|
2026-01-30 05:00:57 +00:00
|
|
|
|
defaultGroup.items.push({
|
2026-04-08 17:03:57 +00:00
|
|
|
|
key: normalizedItem.menu_code,
|
|
|
|
|
|
label: normalizedItem.menu_name,
|
2026-01-30 05:00:57 +00:00
|
|
|
|
icon: icon,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
path: normalizedItem.path
|
2026-01-30 05:00:57 +00:00
|
|
|
|
})
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
2026-01-30 05:00:57 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 如果默认组有内容,放在最前面
|
|
|
|
|
|
if (defaultGroup.items.length > 0) {
|
|
|
|
|
|
groups.unshift(defaultGroup)
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
2026-01-30 05:00:57 +00:00
|
|
|
|
|
|
|
|
|
|
setMenuGroups(groups)
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:03:57 +00:00
|
|
|
|
const normalizeMenuItem = (item) => {
|
|
|
|
|
|
const builtInMeta = resolveBuiltInMenuMeta(item)
|
|
|
|
|
|
|
|
|
|
|
|
if (!builtInMeta) {
|
|
|
|
|
|
return item
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
icon: builtInMeta.icon || item.icon,
|
|
|
|
|
|
path: builtInMeta.path || item.path,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
const handleNavigate = (key, item) => {
|
|
|
|
|
|
if (item.path) {
|
|
|
|
|
|
navigate(item.path)
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
const handleLogout = () => {
|
|
|
|
|
|
logout()
|
|
|
|
|
|
navigate('/login')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleProfileClick = () => {
|
|
|
|
|
|
navigate('/profile')
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
// 获取当前激活的 key
|
|
|
|
|
|
// 简单匹配 path
|
|
|
|
|
|
const getActiveKey = () => {
|
2025-12-20 11:18:59 +00:00
|
|
|
|
const path = location.pathname
|
2026-01-30 05:00:57 +00:00
|
|
|
|
// 遍历所有 items 找匹配
|
|
|
|
|
|
for (const group of menuGroups) {
|
|
|
|
|
|
for (const item of group.items) {
|
|
|
|
|
|
if (item.path === path) return item.key
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
const logoNode = (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
|
|
|
|
|
<img src="/favicon.svg" alt="logo" style={{ width: 32, height: 32 }} />
|
|
|
|
|
|
{!collapsed && (
|
2026-01-31 03:41:20 +00:00
|
|
|
|
<span style={{ fontSize: 18, fontWeight: 'bold', color: 'var(--text-color)' }}>NexDocus</span>
|
2026-01-30 05:00:57 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
2025-12-20 11:18:59 +00:00
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
// 获取用户头像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
|
2025-12-20 11:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 05:00:57 +00:00
|
|
|
|
const userObj = user ? {
|
|
|
|
|
|
name: user.nickname || user.username,
|
|
|
|
|
|
role: user.role_name || 'Admin',
|
|
|
|
|
|
avatar: getUserAvatarUrl()
|
|
|
|
|
|
} : null
|
|
|
|
|
|
|
2025-12-20 11:18:59 +00:00
|
|
|
|
return (
|
2026-01-30 05:00:57 +00:00
|
|
|
|
<ModernSidebar
|
|
|
|
|
|
logo={logoNode}
|
|
|
|
|
|
menuGroups={menuGroups}
|
|
|
|
|
|
activeKey={getActiveKey()}
|
|
|
|
|
|
onNavigate={handleNavigate}
|
|
|
|
|
|
user={userObj}
|
|
|
|
|
|
onLogout={handleLogout}
|
|
|
|
|
|
onProfileClick={handleProfileClick}
|
2025-12-20 11:18:59 +00:00
|
|
|
|
collapsed={collapsed}
|
2026-01-30 05:00:57 +00:00
|
|
|
|
onCollapse={onToggle}
|
|
|
|
|
|
/>
|
2025-12-20 11:18:59 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default AppSider
|