diff --git a/.gemini-clipboard/clipboard-1769085744299.png b/.gemini-clipboard/clipboard-1769085744299.png deleted file mode 100644 index 8c41fec..0000000 Binary files a/.gemini-clipboard/clipboard-1769085744299.png and /dev/null differ diff --git a/.gemini-clipboard/clipboard-1769087429425.png b/.gemini-clipboard/clipboard-1769087429425.png deleted file mode 100644 index 0d29001..0000000 Binary files a/.gemini-clipboard/clipboard-1769087429425.png and /dev/null differ diff --git a/.gemini-clipboard/clipboard-1769740914907.png b/.gemini-clipboard/clipboard-1769740914907.png new file mode 100644 index 0000000..b38ad02 Binary files /dev/null and b/.gemini-clipboard/clipboard-1769740914907.png differ diff --git a/.gemini-clipboard/clipboard-1769741905582.png b/.gemini-clipboard/clipboard-1769741905582.png new file mode 100644 index 0000000..05e118d Binary files /dev/null and b/.gemini-clipboard/clipboard-1769741905582.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6d573fc..eabb62f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,8 @@ +import { useEffect } from 'react' import { BrowserRouter, Routes, Route, Navigate, useParams, Outlet } from 'react-router-dom' -import { ConfigProvider } from 'antd' +import { ConfigProvider, theme } from 'antd' import zhCN from 'antd/locale/zh_CN' +import useThemeStore from '@/stores/themeStore' import Login from '@/pages/Login/Login' import ProjectList from '@/pages/ProjectList/ProjectList' import DocumentPage from '@/pages/Document/DocumentPage' @@ -35,8 +37,23 @@ function LayoutWrapper() { } function App() { + const { isDarkMode } = useThemeStore() + + useEffect(() => { + if (isDarkMode) { + document.body.classList.add('dark') + } else { + document.body.classList.remove('dark') + } + }, [isDarkMode]) + return ( - + } /> diff --git a/frontend/src/assets/logo-small.png b/frontend/src/assets/logo-small.png deleted file mode 100644 index 099c476..0000000 Binary files a/frontend/src/assets/logo-small.png and /dev/null differ diff --git a/frontend/src/components/MainLayout/AppHeader.css b/frontend/src/components/MainLayout/AppHeader.css index 10b0574..6429ab2 100644 --- a/frontend/src/components/MainLayout/AppHeader.css +++ b/frontend/src/components/MainLayout/AppHeader.css @@ -1,12 +1,13 @@ .app-header { - background: #fff; + background: var(--header-bg); padding: 0 24px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); height: 64px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid var(--border-color); + color: var(--text-color); } /* 左侧区域 */ @@ -25,26 +26,13 @@ transition: width 0.2s; } -.logo-small { - width: 40px; - height: 40px; - border-radius: 8px; - transition: all 0.2s; -} - -.logo-full { - height: 32px; - width: auto; - transition: all 0.2s; -} - .trigger { font-size: 18px; cursor: pointer; transition: color 0.3s; padding: 8px; border-radius: 4px; - color: rgba(0, 0, 0, 0.65); + color: var(--text-color-secondary); display: flex; align-items: center; } @@ -61,67 +49,29 @@ gap: 16px; } -.header-search { - border-radius: 16px; -} - .header-actions { display: flex; align-items: center; + gap: 16px; } -.header-icon { - font-size: 16px; - color: rgba(0, 0, 0, 0.65); - cursor: pointer; - transition: all 0.3s; - padding: 8px; - border-radius: 4px; +/* Icon Buttons */ +.header-icon-btn { display: flex; align-items: center; justify-content: center; -} - -.header-icon:hover { - color: #1677ff; - background: rgba(22, 119, 255, 0.08); -} - -.header-link { - font-size: 14px; - color: rgba(0, 0, 0, 0.65); + width: 40px; + height: 40px; + border-radius: 8px; cursor: pointer; - transition: all 0.3s; - padding: 6px 12px; - border-radius: 4px; - display: flex; - align-items: center; - gap: 4px; + color: var(--text-color-secondary); + transition: all 0.2s; + background: transparent; } -.header-link:hover { - color: #1677ff; - background: rgba(22, 119, 255, 0.08); -} - -.user-info { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - padding: 4px 8px; - border-radius: 4px; - transition: all 0.3s; -} - -.user-info:hover { - background: rgba(22, 119, 255, 0.08); -} - -.username { - font-size: 14px; - color: rgba(0, 0, 0, 0.88); - font-weight: 500; +.header-icon-btn:hover { + background-color: var(--item-hover-bg); + color: var(--text-color); } /* 通知面板样式 */ @@ -135,7 +85,7 @@ .popover-header { padding: 12px 16px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; @@ -144,6 +94,7 @@ .popover-header .title { font-weight: 600; font-size: 16px; + color: var(--text-color); } .notification-list { @@ -155,23 +106,33 @@ padding: 12px 16px !important; cursor: pointer; transition: background 0.3s; + background: var(--bg-color); } .notification-item:hover { - background: #f5f5f5; + background: var(--item-hover-bg); } .notification-item.unread { background: #e6f7ff; } +/* Dark mode adjustment for unread */ +body.dark .notification-item.unread { + background: #111d2c; +} + .notification-item.unread:hover { background: #bae7ff; } +body.dark .notification-item.unread:hover { + background: #112a45; +} + .content-text { font-size: 13px; - color: #595959; + color: var(--text-color-secondary); margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; @@ -181,16 +142,13 @@ .time { font-size: 12px; - color: #bfbfbf; + color: var(--text-color-secondary); + opacity: 0.8; margin-top: 4px; } .popover-footer { padding: 8px; - border-top: 1px solid #f0f0f0; + border-top: 1px solid var(--border-color); text-align: center; -} - -.ml-1 { - margin-left: 4px; } \ No newline at end of file diff --git a/frontend/src/components/MainLayout/AppHeader.jsx b/frontend/src/components/MainLayout/AppHeader.jsx index c5b88ad..ddb3bf7 100644 --- a/frontend/src/components/MainLayout/AppHeader.jsx +++ b/frontend/src/components/MainLayout/AppHeader.jsx @@ -1,68 +1,41 @@ import { useState, useEffect } from 'react' -import { Layout, Badge, Avatar, Dropdown, Space, Popover, List, Tabs, Button, Empty, Typography } from 'antd' +import { Layout, Badge, Avatar, Dropdown, Space, Popover, List, Tabs, Button, Empty, Typography, Segmented, Tooltip } from 'antd' import { useNavigate } from 'react-router-dom' import { MenuFoldOutlined, MenuUnfoldOutlined, BellOutlined, - QuestionCircleOutlined, - FileTextOutlined, - CustomerServiceOutlined, - UserOutlined, - CheckOutlined, ProjectOutlined, TeamOutlined, NotificationOutlined, + MoonOutlined, + SunOutlined, + GlobalOutlined } from '@ant-design/icons' import useUserStore from '@/stores/userStore' import useNotificationStore from '@/stores/notificationStore' +import useThemeStore from '@/stores/themeStore' import { getNotifications, getUnreadCount, markAsRead, markAllAsRead } from '@/api/notification' import Toast from '@/components/Toast/Toast' -import headerMenuData from '../../data/headerMenuData.json' import './AppHeader.css' const { Header } = Layout const { Text } = Typography -// 图标映射 -const iconMap = { - QuestionCircleOutlined: , - FileTextOutlined: , - CustomerServiceOutlined: , -} - -function AppHeader({ collapsed, onToggle }) { +function AppHeader({ collapsed, onToggle, showLogo = true }) { const navigate = useNavigate() - const { user, logout } = useUserStore() + const { user } = useUserStore() const { unreadCount, fetchUnreadCount, decrementUnreadCount, resetUnreadCount } = useNotificationStore() - - // 用户下拉菜单 - const userMenuItems = [ - { - key: 'profile', - label: '个人中心', - }, - { - key: 'settings', - label: '账户设置', - }, - { - type: 'divider', - }, - { - key: 'logout', - label: '退出登录', - }, - ] + const { isDarkMode, toggleTheme } = useThemeStore() const [notifications, setNotifications] = useState([]) const [loading, setLoading] = useState(false) const [popoverVisible, setPopoverVisible] = useState(false) + const [lang, setLang] = useState('zh') useEffect(() => { if (user) { fetchUnreadCount() - // 每 2 分钟轮询一次 const timer = setInterval(fetchUnreadCount, 120000) return () => clearInterval(timer) } @@ -83,7 +56,7 @@ function AppHeader({ collapsed, onToggle }) { const handleMarkRead = async (id) => { try { await markAsRead(id) - setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: 1 } : n)) + setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: true } : n)) decrementUnreadCount() } catch (error) { console.error('Mark read error:', error) @@ -93,7 +66,7 @@ function AppHeader({ collapsed, onToggle }) { const handleMarkAllRead = async () => { try { await markAllAsRead() - setNotifications(notifications.map(n => ({ ...n, is_read: 1 }))) + setNotifications(notifications.map(n => ({ ...n, is_read: true }))) resetUnreadCount() Toast.success('操作成功', '所有通知已标记为已读') } catch (error) { @@ -161,73 +134,46 @@ function AppHeader({ collapsed, onToggle }) { ) - const handleUserMenuClick = ({ key }) => { - if (key === 'logout') { - logout() - Toast.success('退出成功', '您已安全退出') - navigate('/login') - } else if (key === 'profile') { - navigate('/profile') - } else if (key === 'settings') { - Toast.info('开发中', '账户设置功能正在开发中') - } - } - - const handleHeaderMenuClick = (key) => { - console.log('Header menu clicked:', key) - if (key === 'support') { - Toast.info('开发中', '支持功能正在开发中') - } - } - - // 获取用户头像URL - const getUserAvatarUrl = () => { - if (!user?.avatar) return null - // avatar 字段存储的是相对路径,如:2/avatar/xxx.jpg - // 需要转换为 API 端点: /api/v1/auth/avatar/{user_id}/{filename} - 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 - } - return ( -
+
{/* 左侧:Logo + 折叠按钮 */} -
- {/* Logo 区域 */} -
- logo -

NexDocus

-
+ {showLogo && ( +
+ {/* Logo 区域 */} +
+ logo +

NexDocus

+
- {/* 折叠按钮 */} -
- {collapsed ? : } + {/* 折叠按钮 */} +
+ {collapsed ? : } +
-
+ )} + {!showLogo &&
} {/* Spacer if left is empty */} - {/* 右侧:功能按钮 + 用户信息 */} + {/* 右侧:功能按钮 */}
- {/* 功能图标 */} - {/* 动态渲染 header 菜单 */} - {headerMenuData.map((item) => ( -
handleHeaderMenuClick(item.key)} - > - {iconMap[item.icon]} - {item.label} -
- ))} + + {/* 1. 主题切换 */} +
+ {isDarkMode ? : } +
- {/* 消息中心 */} + {/* 2. 语言切换 */} + + + {/* 3. 消息通知 */} -
- - +
+ + - 消息
- {/* 用户下拉菜单 */} - -
- } src={getUserAvatarUrl()} /> - {user?.nickname || user?.username || 'User'} -
-
) } -export default AppHeader +export default AppHeader \ No newline at end of file diff --git a/frontend/src/components/MainLayout/AppSider.jsx b/frontend/src/components/MainLayout/AppSider.jsx index bfb8939..33da543 100644 --- a/frontend/src/components/MainLayout/AppSider.jsx +++ b/frontend/src/components/MainLayout/AppSider.jsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react' -import { Layout, Menu, Badge, message } from 'antd' import { useNavigate, useLocation } from 'react-router-dom' import { DashboardOutlined, @@ -14,34 +13,40 @@ import { FileTextOutlined, SafetyOutlined, TeamOutlined, + ProjectOutlined, + RocketOutlined, + ReadOutlined, + BookOutlined, } from '@ant-design/icons' +import { message } from 'antd' import { getUserMenus } from '@/api/menu' -import './AppSider.css' - -const { Sider } = Layout +import useUserStore from '@/stores/userStore' +import ModernSidebar from '../ModernSidebar/ModernSidebar' // 图标映射 const iconMap = { - DashboardOutlined, - DesktopOutlined, - GlobalOutlined, - CloudServerOutlined, - UserOutlined, - AppstoreOutlined, - SettingOutlined, - BlockOutlined, - FolderOutlined, - FileTextOutlined, - SafetyOutlined, - TeamOutlined, + DashboardOutlined: , + DesktopOutlined: , + GlobalOutlined: , + CloudServerOutlined: , + UserOutlined: , + AppstoreOutlined: , + SettingOutlined: , + BlockOutlined: , + FolderOutlined: , + FileTextOutlined: , + SafetyOutlined: , + TeamOutlined: , + ProjectOutlined: , + ReadOutlined: , + BookOutlined: , } function AppSider({ collapsed, onToggle }) { const navigate = useNavigate() const location = useLocation() - const [openKeys, setOpenKeys] = useState([]) - const [menuData, setMenuData] = useState([]) - const [loading, setLoading] = useState(true) + const { user, logout } = useUserStore() + const [menuGroups, setMenuGroups] = useState([]) // 加载菜单数据 useEffect(() => { @@ -52,120 +57,137 @@ function AppSider({ collapsed, onToggle }) { try { const res = await getUserMenus() if (res.data) { - setMenuData(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) message.error('加载菜单失败') - } finally { - setLoading(false) } } - // 根据当前路径获取应该打开的父菜单 - const getDefaultOpenKeys = () => { + 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 icon = typeof child.icon === 'string' ? (iconMap[child.icon] || ) : child.icon + return { + key: child.menu_code, + label: child.menu_name, + icon: icon, + path: child.path + } + }) + + groups.push({ + title: item.menu_name, // e.g. "系统管理" + items: groupItems + }) + } else { + // 一级菜单是叶子节点,放入默认组 + const icon = typeof item.icon === 'string' ? (iconMap[item.icon] || ) : item.icon + defaultGroup.items.push({ + key: item.menu_code, + label: item.menu_name, + icon: icon, + path: item.path + }) + } + }) + + // 如果默认组有内容,放在最前面 + if (defaultGroup.items.length > 0) { + groups.unshift(defaultGroup) + } + + setMenuGroups(groups) + } + + 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 - for (const item of menuData) { - if (item.children) { - const hasChild = item.children.some((c) => c.path === path) - if (hasChild) { - return [item.menu_code] - } - } - } - return [] - } - - // 监听菜单数据加载完成,初始化打开的父菜单 - useEffect(() => { - if (menuData.length > 0 && openKeys.length === 0) { - const defaultKeys = getDefaultOpenKeys() - setOpenKeys(defaultKeys) - } - }, [menuData]) - - const handleMenuClick = ({ key }) => { - // 查找对应的路径 - for (const item of menuData) { - if (item.menu_code === key && item.path) { - navigate(item.path) - return - } - if (item.children) { - const child = item.children.find((c) => c.menu_code === key) - if (child && child.path) { - navigate(child.path) - return - } - } - } - } - - const handleOpenChange = (keys) => { - setOpenKeys(keys) - } - - // 获取当前选中的菜单项 - const getSelectedKey = () => { - const path = location.pathname - for (const item of menuData) { - if (item.path === path) return item.menu_code - if (item.children) { - const child = item.children.find((c) => c.path === path) - if (child) return child.menu_code + // 遍历所有 items 找匹配 + for (const group of menuGroups) { + for (const item of group.items) { + if (item.path === path) return item.key } } return '' } - // 生成菜单项配置 - const getMenuItems = () => { - return menuData.map((item) => { - const IconComponent = iconMap[item.icon] - const icon = IconComponent ? : null + const logoNode = ( +
+ logo + {!collapsed && ( + NexDocus + )} +
+ ) - // 如果有子菜单 - if (item.children && item.children.length > 0) { - return { - key: item.menu_code, - icon: icon, - label: item.menu_name, - popupClassName: 'sider-submenu-popup', - children: item.children.map((child) => ({ - key: child.menu_code, - label: child.menu_name, - })), - } - } - - // 普通菜单项 - return { - key: item.menu_code, - icon: icon, - label: item.menu_name, - } - }) + // 获取用户头像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 ( - - {/* 菜单 */} - - + onCollapse={onToggle} + /> ) } diff --git a/frontend/src/components/MainLayout/MainLayout.css b/frontend/src/components/MainLayout/MainLayout.css index c79a46a..e71ef55 100644 --- a/frontend/src/components/MainLayout/MainLayout.css +++ b/frontend/src/components/MainLayout/MainLayout.css @@ -1,24 +1,27 @@ .main-layout { min-height: 100vh; display: flex; - flex-direction: column; - background: #fafafa; + flex-direction: row; /* Changed to row for Sider-Left layout */ + background: var(--bg-color-secondary); } .main-content-wrapper { display: flex; + flex-direction: column; flex: 1; - height: calc(100vh - 64px); - background: #fafafa; + height: 100vh; + background: var(--bg-color-secondary); + overflow: hidden; } .main-content { - background: #f5f5f5; + background: var(--bg-color-secondary); overflow-y: auto; flex: 1; + padding: 16px; } .content-wrapper { - padding: 8px; + padding: 0; min-height: 100%; -} +} \ No newline at end of file diff --git a/frontend/src/components/MainLayout/MainLayout.jsx b/frontend/src/components/MainLayout/MainLayout.jsx index 2c4ea0b..726c328 100644 --- a/frontend/src/components/MainLayout/MainLayout.jsx +++ b/frontend/src/components/MainLayout/MainLayout.jsx @@ -14,10 +14,10 @@ function MainLayout({ children }) { } return ( - - + + - +
{children} diff --git a/frontend/src/components/ModernSidebar/ModernSidebar.css b/frontend/src/components/ModernSidebar/ModernSidebar.css new file mode 100644 index 0000000..b22ec6b --- /dev/null +++ b/frontend/src/components/ModernSidebar/ModernSidebar.css @@ -0,0 +1,221 @@ +.modern-sidebar { + height: 100vh; + position: relative; + background: var(--sider-bg) !important; + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; +} + +.modern-sidebar .ant-layout-sider-children { + display: flex; + flex-direction: column; + height: 100%; +} + +/* Header */ +.modern-sidebar-header { + padding: 24px 20px; + position: relative; + display: flex; + align-items: center; + height: 80px; + flex-shrink: 0; +} + +.logo-container { + display: flex; + align-items: center; + overflow: hidden; + white-space: nowrap; +} + +/* Collapse Trigger */ +.collapse-trigger { + position: absolute; + right: -12px; + top: 32px; + width: 24px; + height: 24px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + color: var(--text-color-secondary); + transition: all 0.3s; +} + +.collapse-trigger:hover { + color: #1677ff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Menu Area */ +.modern-sidebar-menu { + flex: 1; + overflow-y: auto; + padding: 0 16px; +} + +.modern-sidebar-menu::-webkit-scrollbar { + width: 4px; +} + +.modern-sidebar-menu::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 2px; +} + +/* Menu Group */ +.menu-group { + margin-bottom: 24px; +} + +.group-title { + font-size: 12px; + color: var(--text-color-secondary); + font-weight: 600; + letter-spacing: 0.5px; + margin-bottom: 12px; + padding-left: 12px; + text-transform: uppercase; +} + +/* Menu Item */ +.modern-sidebar-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + margin-bottom: 8px; + cursor: pointer; + border-radius: 12px; /* Rounded corners */ + transition: all 0.2s; + color: var(--text-color); + font-weight: 500; +} + +.modern-sidebar-item:hover { + background-color: var(--item-hover-bg); + color: var(--text-color); +} + +.modern-sidebar-item.active { + background-color: #2563eb; /* Royal Blue */ + color: #fff; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); +} + +.item-content { + display: flex; + align-items: center; + gap: 12px; +} + +.item-icon { + font-size: 18px; + display: flex; + align-items: center; +} + +.item-label { + font-size: 14px; +} + +.item-arrow { + font-size: 12px; + opacity: 0.8; +} + +/* Collapsed State */ +.modern-sidebar-item.collapsed { + justify-content: center; + padding: 12px; + border-radius: 12px; +} + +/* Footer */ +.modern-sidebar-footer { + padding: 16px; + flex-shrink: 0; + background: var(--sider-bg); +} + +.footer-link { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-color-secondary); + font-size: 14px; + margin-bottom: 16px; + padding-left: 12px; + cursor: pointer; + transition: color 0.2s; +} + +.footer-link:hover { + color: var(--text-color); +} + +.footer-link.collapsed { + justify-content: center; + padding-left: 0; +} + +/* User Card */ +.user-card { + background-color: var(--bg-color-secondary); /* Light gray background */ + border-radius: 12px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + transition: all 0.2s; +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; + overflow: hidden; +} + +.user-details { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.user-name { + font-size: 14px; + font-weight: 600; + color: var(--text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-role { + font-size: 12px; + color: var(--text-color-secondary); + text-transform: uppercase; + font-weight: 500; +} + +.logout-btn { + color: var(--text-color-secondary); + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; +} + +.logout-btn:hover { + background-color: var(--border-color); + color: #ef4444; /* Red for logout */ +} \ No newline at end of file diff --git a/frontend/src/components/ModernSidebar/ModernSidebar.jsx b/frontend/src/components/ModernSidebar/ModernSidebar.jsx new file mode 100644 index 0000000..d4ce841 --- /dev/null +++ b/frontend/src/components/ModernSidebar/ModernSidebar.jsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { Layout, Avatar, Tooltip, Button } from 'antd'; +import { + MenuUnfoldOutlined, + MenuFoldOutlined, + LogoutOutlined, + QuestionCircleOutlined, + RightOutlined, + LeftOutlined +} from '@ant-design/icons'; +import './ModernSidebar.css'; + +const { Sider } = Layout; + +const ModernSidebar = ({ + logo, + menuGroups = [], + activeKey, + onNavigate, + user, + onLogout, + onProfileClick, + collapsed, + onCollapse, + width = 260, + collapsedWidth = 80, + className = '', + style = {} +}) => { + + const handleItemClick = (item) => { + if (onNavigate) { + onNavigate(item.key, item); + } + }; + + const renderMenuItem = (item) => { + const isActive = activeKey === item.key; + + // 如果是折叠状态,只显示图标,并使用Tooltip + if (collapsed) { + return ( + +
handleItemClick(item)} + > +
{item.icon}
+
+
+ ); + } + + return ( +
handleItemClick(item)} + > +
+
{item.icon}
+ {item.label} +
+ {isActive && } +
+ ); + }; + + return ( + + {/* 顶部 Logo 区域 */} +
+
+ {logo} +
+ {/* 折叠按钮 - 悬浮在边缘 */} +
onCollapse && onCollapse(!collapsed)} + > + {collapsed ? : } +
+
+ + {/* 菜单列表区域 */} +
+ {menuGroups.map((group, index) => ( +
+ {!collapsed && group.title && ( +
{group.title}
+ )} +
+ {group.items.map(item => renderMenuItem(item))} +
+
+ ))} +
+ + {/* 底部区域 */} +
+ {/* 帮助支持 */} + {!collapsed && ( +
+ + 帮助支持 +
+ )} + {collapsed && ( +
+ +
+ )} + + {/* 用户卡片 */} +
+
+ + {user?.name?.[0]?.toUpperCase() || 'U'} + + {!collapsed && ( +
+
{user?.name || 'User'}
+
{user?.role || 'Member'}
+
+ )} +
+ {!collapsed && ( +
+ +
+ )} +
+
+
+ ); +}; + +export default ModernSidebar; diff --git a/frontend/src/index.css b/frontend/src/index.css index b0435a7..bfa1d8f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,32 @@ +:root { + --bg-color: #fff; + --bg-color-secondary: #fafafa; + --text-color: #000000e0; + --text-color-secondary: #00000073; + --border-color: #f0f0f0; + --header-bg: #fff; + --sider-bg: #fff; + --item-hover-bg: #f5f5f5; + --card-bg: #fff; +} + +body.dark { + --bg-color: #141414; + --bg-color-secondary: #1f1f1f; + --text-color: #ffffffd9; + --text-color-secondary: #ffffff73; + --border-color: #303030; + --header-bg: #141414; + --sider-bg: #141414; + --item-hover-bg: #1f1f1f; + --card-bg: #1f1f1f; +} + +body { + background-color: var(--bg-color); + color: var(--text-color); +} + .search-highlight { background-color: #ffd54f !important; color: black !important; diff --git a/frontend/src/stores/themeStore.js b/frontend/src/stores/themeStore.js new file mode 100644 index 0000000..8c7a6cd --- /dev/null +++ b/frontend/src/stores/themeStore.js @@ -0,0 +1,17 @@ +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' + +const useThemeStore = create( + persist( + (set) => ({ + isDarkMode: false, + toggleTheme: () => set((state) => ({ isDarkMode: !state.isDarkMode })), + }), + { + name: 'theme-storage', + storage: createJSONStorage(() => localStorage), + } + ) +) + +export default useThemeStore diff --git a/frontend/src/stores/userStore.js b/frontend/src/stores/userStore.js index b132ed2..a98f69d 100644 --- a/frontend/src/stores/userStore.js +++ b/frontend/src/stores/userStore.js @@ -2,7 +2,7 @@ * 用户状态管理 Store */ import { create } from 'zustand' -import { persist } from 'zustand/middleware' +import { persist, createJSONStorage } from 'zustand/middleware' const useUserStore = create( persist( @@ -26,7 +26,7 @@ const useUserStore = create( }), { name: 'user-storage', - getStorage: () => localStorage, + storage: createJSONStorage(() => localStorage), } ) )