Download vetcopilot-app/src/components/Layout.tsx from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 4.36 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/Layout.tsx
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/components/Layout.tsx
-
curl -L -o Layout.tsx https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/Layout.tsx
4.36 kB
| import React, { useState, useEffect, useCallback } from 'react' | |
| import { useLocation } from 'react-router-dom' | |
| import Header from './Header' | |
| import Sidebar from './Sidebar' | |
| import ScrollToTop from './ScrollToTop' | |
| import KeyboardShortcutPanel from './KeyboardShortcutPanel' | |
| import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts' | |
| import { AnnounceProvider, useAnnounceContext } from '../contexts/AnnounceContext' | |
| import { useIdleTimer } from '../hooks/useIdleTimer' | |
| import { useTranslation } from '../i18n' | |
| interface LayoutProps { | |
| children: React.ReactNode | |
| } | |
| /** | |
| * 内部布局组件 - 在 AnnounceProvider 内部使用 context 和 hooks | |
| */ | |
| function LayoutInner({ children }: LayoutProps) { | |
| const [sidebarOpen, setSidebarOpen] = useState(false) | |
| const [shortcutPanelOpen, setShortcutPanelOpen] = useState(false) | |
| const location = useLocation() | |
| const { politeRef, assertiveRef } = useAnnounceContext() | |
| const { t } = useTranslation() | |
| // 注册全局键盘快捷键 | |
| useKeyboardShortcuts() | |
| // 空闲检测:15 分钟无操作后提示 | |
| useIdleTimer({ | |
| timeout: 15 * 60 * 1000, | |
| onIdle: () => { | |
| console.warn('[Layout] 用户长时间未操作') | |
| // 可通过向用户展示提示或触发自动锁定 | |
| }, | |
| enabled: true, | |
| }) | |
| // 监听快捷键面板触发事件 | |
| useEffect(() => { | |
| const handleShortcutPanel = (e: Event) => { | |
| e.preventDefault() | |
| setShortcutPanelOpen((prev) => !prev) | |
| } | |
| window.addEventListener('vetcopilot:shortcutPanel', handleShortcutPanel) | |
| return () => window.removeEventListener('vetcopilot:shortcutPanel', handleShortcutPanel) | |
| }, []) | |
| const toggleSidebar = () => setSidebarOpen(prev => !prev) | |
| const closeSidebar = () => setSidebarOpen(false) | |
| // 路由切换时关闭移动端侧边栏 | |
| useEffect(() => { | |
| setSidebarOpen(false) | |
| }, [location.pathname]) | |
| return ( | |
| <div className="h-screen flex flex-col"> | |
| {/* 屏幕阅读器通知区域 */} | |
| <div | |
| ref={politeRef} | |
| id="aria-live-polite" | |
| aria-live="polite" | |
| aria-atomic="true" | |
| className="sr-only" | |
| role="status" | |
| /> | |
| <div | |
| ref={assertiveRef} | |
| id="aria-live-assertive" | |
| aria-live="assertive" | |
| aria-atomic="true" | |
| className="sr-only" | |
| role="alert" | |
| /> | |
| <Header onToggleSidebar={toggleSidebar} /> | |
| {/* 移动端侧边栏遮罩 - 带动画 */} | |
| <div | |
| className={`fixed inset-0 z-40 md:hidden transition-all duration-300 ${ | |
| sidebarOpen | |
| ? 'bg-black/40 backdrop-blur-sm opacity-100 pointer-events-auto' | |
| : 'bg-transparent backdrop-blur-none opacity-0 pointer-events-none' | |
| }`} | |
| onClick={closeSidebar} | |
| aria-hidden="true" | |
| /> | |
| <div className="flex flex-1 overflow-hidden"> | |
| {/* 侧边栏:移动端 overlay 模式,桌面端固定 */} | |
| <div | |
| className={` | |
| fixed inset-y-0 left-0 z-40 transition-transform duration-300 ease-in-out | |
| md:static md:translate-x-0 | |
| ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'} | |
| `} | |
| > | |
| <Sidebar onClose={closeSidebar} /> | |
| </div> | |
| <main | |
| id="main-content" | |
| className="flex-1 overflow-auto" | |
| style={{ | |
| background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 50%, #f8fafc 100%)', | |
| backgroundAttachment: 'fixed', | |
| }} | |
| > | |
| <div className="min-h-full" style={{ | |
| backgroundImage: 'radial-gradient(#e2e8f0 0.5px, transparent 0.5px)', | |
| backgroundSize: '20px 20px', | |
| }}> | |
| {/* 页面路由过渡包装 */} | |
| <div key={location.pathname} className="crossfade-enter"> | |
| {children} | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| {/* 回到顶部按钮 */} | |
| <ScrollToTop /> | |
| {/* 快捷键面板 */} | |
| <KeyboardShortcutPanel | |
| isOpen={shortcutPanelOpen} | |
| onClose={() => setShortcutPanelOpen(false)} | |
| /> | |
| </div> | |
| ) | |
| } | |
| /** | |
| * Layout 根组件 - 包裹 AnnounceProvider 提供全局屏幕阅读器通知能力 | |
| */ | |
| export default function Layout({ children }: LayoutProps) { | |
| return ( | |
| <AnnounceProvider> | |
| <LayoutInner>{children}</LayoutInner> | |
| </AnnounceProvider> | |
| ) | |
| } | |