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 (
+
+ )
+}
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
+}
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
+}
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
+}
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
+}
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
+}
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 (
+
+ )
+}
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
+}