diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e77b010..fa34f64 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,57 @@ -export default function App() { +import { lazy, Suspense } from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import Skeleton from './components/ui/Skeleton' +import AppLayout from './components/layout/AppLayout' +import LoginPage from './pages/LoginPage' +import NotFoundPage from './pages/NotFoundPage' + +const DashboardPage = lazy(() => import('./pages/DashboardPage')) +const GatewayPage = lazy(() => import('./pages/GatewayPage')) +const HealthPage = lazy(() => import('./pages/HealthPage')) +const KanbanPage = lazy(() => import('./pages/KanbanPage')) +const CronPage = lazy(() => import('./pages/CronPage')) +const KeysPage = lazy(() => import('./pages/KeysPage')) +const EventArchivePage = lazy(() => import('./pages/EventArchivePage')) +const UserManagementPage = lazy(() => import('./pages/UserManagementPage')) +const AuditLogPage = lazy(() => import('./pages/AuditLogPage')) + +function Loading() { return ( -
-

Hive Manage v3

+
+
) } + +export default function App() { + return ( + + + } /> + } /> + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + + + ) +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..24af74a --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -0,0 +1,138 @@ +import { useState, type ReactNode } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' +import { useTheme } from '../../hooks/useTheme' +import Sidebar from './Sidebar' +import Header from './Header' +import MobileNav from './MobileNav' +import TabBar from './TabBar' + +function useMediaQuery(query: string) { + const [matches, setMatches] = useState(() => { + if (typeof window !== 'undefined') { + return window.matchMedia(query).matches + } + return false + }) + + if (typeof window !== 'undefined') { + const mql = window.matchMedia(query) + mql.addEventListener('change', (e) => setMatches(e.matches)) + } + + return matches +} + +export interface AppLayoutProps { + children: ReactNode + breadcrumb?: string + onlineCount?: number + runningCount?: number + onSearch?: (query: string) => any +} + +export default function AppLayout({ + children, + breadcrumb, + onlineCount = 0, + runningCount = 0, + onSearch, +}: AppLayoutProps) { + const { theme, toggle } = useTheme() + const navigate = useNavigate() + const location = useLocation() + const isMobile = useMediaQuery('(max-width: 767px)') + const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1023px)') + const [mobileNavOpen, setMobileNavOpen] = useState(false) + + const handleNavigate = (path: string) => { + navigate(path) + } + + const sidebarWidth = isTablet ? 180 : 220 + + if (isMobile) { + return ( +
+ {/* Mobile header */} +
+
+ + 知微蜂巢 +
+ +
+ + {/* Mobile content */} +
{children}
+ + {/* Mobile tab bar */} + + + {/* Mobile nav drawer */} + setMobileNavOpen(false)} + activePath={location.pathname} + onNavigate={handleNavigate} + /> +
+ ) + } + + return ( +
+ {/* Sidebar */} +
+ +
+ + {/* Header */} +
+
+
+ + {/* Main */} +
+ {children} +
+
+ ) +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..0df7563 --- /dev/null +++ b/frontend/src/components/layout/Header.tsx @@ -0,0 +1,80 @@ +import { useNavigate } from 'react-router-dom' +import Badge from '../ui/Badge' +import SearchBox from '../ui/SearchBox' + +export interface HeaderProps { + breadcrumb?: string + onlineCount?: number + runningCount?: number + onSearch?: (query: string) => any + theme: string + onToggleTheme: () => void +} + +export default function Header({ + breadcrumb, + onlineCount = 0, + runningCount = 0, + onSearch, + theme, + onToggleTheme, +}: HeaderProps) { + const navigate = useNavigate() + + return ( +
+ {/* Breadcrumb */} +
+ {breadcrumb && ( + <> + navigate('/dashboard')} + > + 首页 + + / + {breadcrumb} + + )} +
+ + {/* Spacer */} +
+ + {/* Search */} + {onSearch && ( + + )} + + {/* Status badges */} + 在线 {onlineCount} + 运行中 {runningCount} + + {/* Theme toggle */} + + + {/* User avatar */} +
+ A +
+
+ ) +} diff --git a/frontend/src/components/layout/MobileNav.tsx b/frontend/src/components/layout/MobileNav.tsx new file mode 100644 index 0000000..9ff5d38 --- /dev/null +++ b/frontend/src/components/layout/MobileNav.tsx @@ -0,0 +1,75 @@ +import { useEffect, useCallback, useState } from 'react' +import { createPortal } from 'react-dom' +import Sidebar from './Sidebar' + +export interface MobileNavProps { + open: boolean + onClose: () => void + activePath: string + onNavigate: (path: string) => void +} + +export default function MobileNav({ open, onClose, activePath, onNavigate }: MobileNavProps) { + const [mounted, setMounted] = useState(false) + const [visible, setVisible] = useState(false) + + useEffect(() => { + if (open) { + setMounted(true) + requestAnimationFrame(() => setVisible(true)) + } else { + setVisible(false) + const timer = setTimeout(() => setMounted(false), 250) + return () => clearTimeout(timer) + } + }, [open]) + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + }, + [onClose], + ) + + useEffect(() => { + if (open) { + document.addEventListener('keydown', handleKeyDown) + document.body.style.overflow = 'hidden' + } + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.body.style.overflow = '' + } + }, [open, handleKeyDown]) + + const handleNavigate = (path: string) => { + onNavigate(path) + onClose() + } + + if (!mounted) return null + + return createPortal( +
+ {/* Overlay */} +
+ {/* Drawer */} +
+ +
+
, + document.body, + ) +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2ce006b --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,123 @@ +import { type ReactNode } from 'react' +import Badge from '../ui/Badge' + +interface NavItem { + label: string + icon: string + path: string + badge?: number +} + +interface NavSection { + title: string + items: NavItem[] +} + +const sections: NavSection[] = [ + { + title: '蜂群态势', + items: [ + { label: '蜂群总览', icon: '📊', path: '/dashboard' }, + { label: 'Gateway', icon: '📡', path: '/gateway' }, + { label: '健康分', icon: '🏥', path: '/health' }, + { label: '工蜂列表', icon: '🤖', path: '/workers' }, + ], + }, + { + title: '任务编排', + items: [ + { label: '看板追踪', icon: '📋', path: '/kanban' }, + { label: 'Cron', icon: '⏰', path: '/cron' }, + { label: '事件管理', icon: '🔔', path: '/events' }, + ], + }, + { + title: '通信频道', + items: [{ label: '频道配置', icon: '📶', path: '/channels' }], + }, + { + title: '运维工具', + items: [ + { label: '密钥', icon: '🔑', path: '/keys' }, + { label: '文件', icon: '📁', path: '/files' }, + { label: '终端', icon: '💻', path: '/terminal' }, + { label: '日志', icon: '📜', path: '/logs' }, + { label: 'MCP接入', icon: '🔌', path: '/mcp-in' }, + { label: 'MCP对外', icon: '📤', path: '/mcp-out' }, + ], + }, + { + title: '知识库管理', + items: [{ label: '事件档案', icon: '📚', path: '/archive' }], + }, + { + title: '系统', + items: [{ label: '设置', icon: '⚙', path: '/settings' }], + }, +] + +export interface SidebarProps { + activePath: string + onNavigate: (path: string) => void +} + +export default function Sidebar({ activePath, onNavigate }: SidebarProps) { + return ( + + ) +} diff --git a/frontend/src/components/layout/TabBar.tsx b/frontend/src/components/layout/TabBar.tsx new file mode 100644 index 0000000..79e123c --- /dev/null +++ b/frontend/src/components/layout/TabBar.tsx @@ -0,0 +1,45 @@ +import { useNavigate } from 'react-router-dom' + +const tabs = [ + { label: '总览', icon: '📊', path: '/dashboard' }, + { label: 'Gateway', icon: '📡', path: '/gateway' }, + { label: 'Kanban', icon: '📋', path: '/kanban' }, + { label: 'Cron', icon: '⏰', path: '/cron' }, +] + +export interface TabBarProps { + activePath: string +} + +export default function TabBar({ activePath }: TabBarProps) { + const navigate = useNavigate() + + return ( +
+ {tabs.map(tab => { + const isActive = activePath === tab.path + return ( + + ) + })} +
+ ) +} diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts new file mode 100644 index 0000000..516a46d --- /dev/null +++ b/frontend/src/hooks/useTheme.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +export function useTheme() { + const [theme, setTheme] = useState(() => { + return (localStorage.getItem('hive-theme') as Theme) || 'system' + }) + + useEffect(() => { + const root = document.documentElement + if (theme === 'system') { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + root.setAttribute('data-theme', prefersDark ? 'dark' : 'light') + } else { + root.setAttribute('data-theme', theme) + } + localStorage.setItem('hive-theme', theme) + }, [theme]) + + const toggle = () => setTheme(t => (t === 'dark' ? 'light' : 'dark')) + return { theme, setTheme, toggle } +} diff --git a/frontend/src/pages/AuditLogPage.tsx b/frontend/src/pages/AuditLogPage.tsx new file mode 100644 index 0000000..8661817 --- /dev/null +++ b/frontend/src/pages/AuditLogPage.tsx @@ -0,0 +1,3 @@ +export default function AuditLogPage() { + return

审计日志

操作审计记录

+} diff --git a/frontend/src/pages/CronPage.tsx b/frontend/src/pages/CronPage.tsx new file mode 100644 index 0000000..16da6c3 --- /dev/null +++ b/frontend/src/pages/CronPage.tsx @@ -0,0 +1,3 @@ +export default function CronPage() { + return

Cron

Cron 任务管理

+} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..917f796 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,3 @@ +export default function DashboardPage() { + return

蜂群总览

Dashboard 仪表板

+} diff --git a/frontend/src/pages/EventArchivePage.tsx b/frontend/src/pages/EventArchivePage.tsx new file mode 100644 index 0000000..e1436a7 --- /dev/null +++ b/frontend/src/pages/EventArchivePage.tsx @@ -0,0 +1,3 @@ +export default function EventArchivePage() { + return

事件档案

事件归档查询

+} diff --git a/frontend/src/pages/GatewayPage.tsx b/frontend/src/pages/GatewayPage.tsx new file mode 100644 index 0000000..b569934 --- /dev/null +++ b/frontend/src/pages/GatewayPage.tsx @@ -0,0 +1,3 @@ +export default function GatewayPage() { + return

Gateway

Gateway 管理

+} diff --git a/frontend/src/pages/HealthPage.tsx b/frontend/src/pages/HealthPage.tsx new file mode 100644 index 0000000..4c458fc --- /dev/null +++ b/frontend/src/pages/HealthPage.tsx @@ -0,0 +1,3 @@ +export default function HealthPage() { + return

健康分

系统健康评分

+} diff --git a/frontend/src/pages/KanbanPage.tsx b/frontend/src/pages/KanbanPage.tsx new file mode 100644 index 0000000..4620a6b --- /dev/null +++ b/frontend/src/pages/KanbanPage.tsx @@ -0,0 +1,3 @@ +export default function KanbanPage() { + return

看板追踪

Kanban 任务看板

+} diff --git a/frontend/src/pages/KeysPage.tsx b/frontend/src/pages/KeysPage.tsx new file mode 100644 index 0000000..3f23d17 --- /dev/null +++ b/frontend/src/pages/KeysPage.tsx @@ -0,0 +1,3 @@ +export default function KeysPage() { + return

密钥管理

API 密钥管理

+} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..37a3e6b --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,15 @@ +export default function LoginPage() { + return ( +
+
+

+ 🐝 知微蜂巢 +

+

登录页面(待实现)

+
+
+ ) +} diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..c9a9b21 --- /dev/null +++ b/frontend/src/pages/NotFoundPage.tsx @@ -0,0 +1,10 @@ +export default function NotFoundPage() { + return ( +
+
+

404

+

页面不存在

+
+
+ ) +} diff --git a/frontend/src/pages/UserManagementPage.tsx b/frontend/src/pages/UserManagementPage.tsx new file mode 100644 index 0000000..768b32c --- /dev/null +++ b/frontend/src/pages/UserManagementPage.tsx @@ -0,0 +1,3 @@ +export default function UserManagementPage() { + return

用户管理

用户管理与权限

+}