feat: layout components (Sidebar, Header, MobileNav, TabBar, AppLayout) with routing
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+53
-3
@@ -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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center" style={{background:'var(--bg-root)', color:'var(--text)'}}>
|
<div className="p-5">
|
||||||
<h1 className="text-2xl font-bold">Hive Manage v3</h1>
|
<Skeleton variant="card" count={2} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/404" element={<NotFoundPage />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<AppLayout>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="dashboard" element={<DashboardPage />} />
|
||||||
|
<Route path="gateway" element={<GatewayPage />} />
|
||||||
|
<Route path="health" element={<HealthPage />} />
|
||||||
|
<Route path="kanban" element={<KanbanPage />} />
|
||||||
|
<Route path="cron" element={<CronPage />} />
|
||||||
|
<Route path="keys" element={<KeysPage />} />
|
||||||
|
<Route path="events" element={<EventArchivePage />} />
|
||||||
|
<Route path="users" element={<UserManagementPage />} />
|
||||||
|
<Route path="audit" element={<AuditLogPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</AppLayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={<Navigate to="/404" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="min-h-screen flex flex-col" style={{ background: 'var(--bg-root)' }}>
|
||||||
|
{/* Mobile header */}
|
||||||
|
<header
|
||||||
|
className="flex items-center justify-between px-4 shrink-0"
|
||||||
|
style={{
|
||||||
|
height: 'var(--header-h)',
|
||||||
|
background: 'var(--bg-nav)',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileNavOpen(true)}
|
||||||
|
className="cursor-pointer bg-transparent border-none text-[20px] p-1"
|
||||||
|
style={{ color: 'var(--text)' }}
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
<span style={{ color: 'var(--honey)', fontWeight: 700, fontSize: 14 }}>知微蜂巢</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggle}
|
||||||
|
className="cursor-pointer bg-transparent border-none text-[18px] leading-none p-1"
|
||||||
|
title={theme === 'dark' ? '切换浅色模式' : '切换深色模式'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Mobile content */}
|
||||||
|
<main className="flex-1 overflow-y-auto p-3 pb-[64px]">{children}</main>
|
||||||
|
|
||||||
|
{/* Mobile tab bar */}
|
||||||
|
<TabBar activePath={location.pathname} />
|
||||||
|
|
||||||
|
{/* Mobile nav drawer */}
|
||||||
|
<MobileNav
|
||||||
|
open={mobileNavOpen}
|
||||||
|
onClose={() => setMobileNavOpen(false)}
|
||||||
|
activePath={location.pathname}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="min-h-screen"
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `${sidebarWidth}px 1fr`,
|
||||||
|
gridTemplateRows: 'var(--header-h) 1fr',
|
||||||
|
gridTemplateAreas: '"sidebar header" "sidebar main"',
|
||||||
|
background: 'var(--bg-root)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div style={{ gridArea: 'sidebar' }}>
|
||||||
|
<Sidebar activePath={location.pathname} onNavigate={handleNavigate} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ gridArea: 'header' }}>
|
||||||
|
<Header
|
||||||
|
breadcrumb={breadcrumb}
|
||||||
|
onlineCount={onlineCount}
|
||||||
|
runningCount={runningCount}
|
||||||
|
onSearch={onSearch}
|
||||||
|
theme={theme}
|
||||||
|
onToggleTheme={toggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main */}
|
||||||
|
<main
|
||||||
|
className="overflow-y-auto p-5"
|
||||||
|
style={{ gridArea: 'main', background: 'var(--bg-root)' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<header
|
||||||
|
className="flex items-center gap-3 px-4 shrink-0"
|
||||||
|
style={{
|
||||||
|
height: 'var(--header-h)',
|
||||||
|
background: 'var(--bg-nav)',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="flex items-center gap-2 text-[13px] min-w-0" style={{ color: 'var(--text-dim)' }}>
|
||||||
|
{breadcrumb && (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer hover:text-[var(--text)] transition-colors"
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
>
|
||||||
|
首页
|
||||||
|
</span>
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||||
|
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{breadcrumb}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
{onSearch && (
|
||||||
|
<SearchBox onSearch={onSearch} placeholder="搜索功能、任务、日志..." />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status badges */}
|
||||||
|
<Badge variant="success">在线 {onlineCount}</Badge>
|
||||||
|
<Badge variant="honey">运行中 {runningCount}</Badge>
|
||||||
|
|
||||||
|
{/* Theme toggle */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleTheme}
|
||||||
|
className="cursor-pointer bg-transparent border-none text-[18px] leading-none p-1 rounded hover:bg-[var(--bg-hover)] transition-colors"
|
||||||
|
title={theme === 'dark' ? '切换浅色模式' : '切换深色模式'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* User avatar */}
|
||||||
|
<div
|
||||||
|
className="w-[30px] h-[30px] rounded-full flex items-center justify-center text-[13px] font-semibold cursor-pointer shrink-0"
|
||||||
|
style={{ background: 'var(--honey-glow)', color: 'var(--honey)' }}
|
||||||
|
title="管理员"
|
||||||
|
>
|
||||||
|
A
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
<div className="fixed inset-0 z-[9999]">
|
||||||
|
{/* Overlay */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-250 ${
|
||||||
|
visible ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
{/* Drawer */}
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 left-0 h-full transition-transform duration-[250ms] ease-in-out`}
|
||||||
|
style={{
|
||||||
|
width: 280,
|
||||||
|
transform: visible ? 'translateX(0)' : 'translateX(-100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sidebar activePath={activePath} onNavigate={handleNavigate} />
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<nav className="flex flex-col h-full overflow-y-auto" style={{ background: 'var(--bg-nav)', borderRight: '1px solid var(--border)' }}>
|
||||||
|
{/* Logo */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 shrink-0 cursor-pointer px-5"
|
||||||
|
style={{ height: 'var(--header-h)' }}
|
||||||
|
onClick={() => onNavigate('/dashboard')}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 20 }}>🐝</span>
|
||||||
|
<span style={{ color: 'var(--honey)', fontWeight: 700, fontSize: 15, letterSpacing: 0.3 }}>
|
||||||
|
知微蜂巢
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav sections */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-3 pb-4">
|
||||||
|
{sections.map(section => (
|
||||||
|
<div key={section.title} className="mt-4">
|
||||||
|
<div
|
||||||
|
className="px-2 mb-1 text-[11px] font-semibold uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
|
{section.title}
|
||||||
|
</div>
|
||||||
|
{section.items.map(item => {
|
||||||
|
const isActive = activePath === item.path
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.path}
|
||||||
|
className="flex items-center justify-between rounded-[var(--radius-sm)] cursor-pointer px-2 py-[7px] text-[13px] transition-colors duration-100"
|
||||||
|
style={{
|
||||||
|
color: isActive ? 'var(--honey)' : 'var(--text-dim)',
|
||||||
|
fontWeight: isActive ? 600 : 400,
|
||||||
|
background: isActive ? 'var(--honey-glow)' : undefined,
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
if (!isActive) e.currentTarget.style.background = 'var(--bg-hover)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
if (!isActive) e.currentTarget.style.background = 'transparent'
|
||||||
|
}}
|
||||||
|
onClick={() => onNavigate(item.path)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span style={{ marginRight: 8 }}>{item.icon}</span>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{item.badge != null && item.badge > 0 && (
|
||||||
|
<Badge variant="danger">{item.badge}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-0 left-0 right-0 flex items-center z-50"
|
||||||
|
style={{
|
||||||
|
height: 56,
|
||||||
|
background: 'var(--bg-nav)',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map(tab => {
|
||||||
|
const isActive = activePath === tab.path
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.path}
|
||||||
|
onClick={() => navigate(tab.path)}
|
||||||
|
className="flex-1 flex flex-col items-center justify-center gap-[2px] cursor-pointer bg-transparent border-none transition-colors duration-100"
|
||||||
|
style={{
|
||||||
|
color: isActive ? 'var(--honey)' : 'var(--text-dim)',
|
||||||
|
fontWeight: isActive ? 600 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 20 }}>{tab.icon}</span>
|
||||||
|
<span style={{ fontSize: 11 }}>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function AuditLogPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>审计日志</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>操作审计记录</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function CronPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>Cron</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>Cron 任务管理</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function DashboardPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>蜂群总览</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>Dashboard 仪表板</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function EventArchivePage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>事件档案</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>事件归档查询</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function GatewayPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>Gateway</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>Gateway 管理</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function HealthPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>健康分</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>系统健康评分</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function KanbanPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>看板追踪</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>Kanban 任务看板</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function KeysPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>密钥管理</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>API 密钥管理</p></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--bg-root)' }}>
|
||||||
|
<div
|
||||||
|
className="p-8 rounded-[var(--radius)] w-[360px]"
|
||||||
|
style={{ background: 'var(--bg-card)', boxShadow: 'var(--shadow-lg)' }}
|
||||||
|
>
|
||||||
|
<h1 className="text-center mb-6" style={{ color: 'var(--honey)', fontSize: 20, fontWeight: 700 }}>
|
||||||
|
🐝 知微蜂巢
|
||||||
|
</h1>
|
||||||
|
<p className="text-center" style={{ color: 'var(--text-dim)', fontSize: 13 }}>登录页面(待实现)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function NotFoundPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--bg-root)' }}>
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 style={{ color: 'var(--text-muted)', fontSize: 48, fontWeight: 800 }}>404</h1>
|
||||||
|
<p style={{ color: 'var(--text-dim)', marginTop: 8 }}>页面不存在</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function UserManagementPage() {
|
||||||
|
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>用户管理</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>用户管理与权限</p></div>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user