diff --git a/frontend/src/layouts/AppLayout/AppLayout.module.css b/frontend/src/layouts/AppLayout/AppLayout.module.css new file mode 100644 index 0000000..18d56a9 --- /dev/null +++ b/frontend/src/layouts/AppLayout/AppLayout.module.css @@ -0,0 +1,208 @@ +/* REQ-USR-003: 应用外壳 scoped 样式。语义色用 var(--color-*); + * 顶栏 #1f1f23 / overlay #2b3137 等品牌深色底为外壳局部装饰,scoped 保留(D9)。 */ + +/* ===== app shell ===== */ +.app { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} +.stage { + flex: 1; + position: relative; + overflow: auto; + background: var(--color-bg-base); +} + +/* ===== TOP BAR(深色装饰 scoped) ===== */ +.topbar { + display: flex; + align-items: stretch; + height: 44px; + background: #1f1f23; /* 外壳局部装饰底色(D9:非语义 token,scoped 保留) */ + color: #ffffff; + position: relative; + z-index: 30; + flex-shrink: 0; +} +.logo { + width: 54px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: #0e1216; + cursor: pointer; +} +.logo svg { + width: 30px; + height: 30px; +} +.navBtn { + display: flex; + align-items: center; + gap: 6px; + padding: 0 18px; + color: #fff; + cursor: pointer; + font-size: 14px; + border: none; + background: transparent; + height: 100%; +} +.navBtn:hover { + background: #33363d; +} +.navBtnActive { + background: var(--color-primary); +} +.tabs { + display: flex; + align-items: stretch; + flex: 1; + min-width: 0; +} +.tab { + display: flex; + align-items: center; + gap: 8px; + padding: 0 18px; + cursor: pointer; + color: #cfd2d8; + font-size: 14px; + height: 100%; + border: none; + background: transparent; +} +.tabActive { + color: var(--color-primary); +} +.tabClose { + margin-left: 6px; + width: 16px; + height: 16px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + color: #9aa0a8; + border: none; + background: transparent; + cursor: pointer; +} +.tabClose:hover { + background: #3a3d44; + color: #fff; +} +.right { + display: flex; + align-items: center; + gap: 18px; + padding-right: 14px; +} +.rightIcon { + width: 18px; + height: 18px; + opacity: 0.9; + cursor: pointer; + display: inline-flex; + align-items: center; +} +.user { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + cursor: pointer; + color: #fff; + border: none; + background: transparent; +} +.more { + font-size: 18px; + letter-spacing: 2px; + cursor: pointer; + padding: 0 4px; +} + +/* ===== NAV OVERLAY(深色装饰 scoped,D9) ===== */ +.navOverlay { + position: absolute; + inset: 0; + z-index: 20; + display: flex; + color: #cfd3da; +} +.navOverlayMask { + position: absolute; + inset: 0; + background: transparent; +} +.navOverlayBody { + position: relative; + display: flex; + flex: 1; + background: #2b3137; /* overlay 深色底(scoped 装饰) */ +} +.navSide { + width: 200px; + background: #2b3137; + padding: 8px 0; + border-right: 1px solid #1e2226; + overflow: auto; +} +.navSideItem { + display: flex; + align-items: center; + gap: 10px; + padding: 11px 18px; + font-size: 14px; + color: #d3d6db; + cursor: pointer; +} +.navSideItem:hover { + background: #34393f; +} +.navSideItemActive { + color: var(--color-primary); + background: #34393f; +} +.navGrid { + flex: 1; + padding: 30px 40px; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 30px 40px; + align-content: start; + overflow: auto; +} +.navColTitle { + font-size: 15px; + color: #e8eaee; + font-weight: 500; + margin: 0 0 18px; + border-bottom: 1px solid #4a4f57; + padding-bottom: 10px; +} +.navLeaf { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 0; + color: #cfd3da; + font-size: 14px; + cursor: pointer; + border: none; + background: transparent; + text-align: left; + width: 100%; +} +.navLeaf:hover { + color: #fff; +} +.navStar { + color: var(--color-warning); +} diff --git a/frontend/src/layouts/AppLayout/NavOverlay.tsx b/frontend/src/layouts/AppLayout/NavOverlay.tsx new file mode 100644 index 0000000..3e8dbb0 --- /dev/null +++ b/frontend/src/layouts/AppLayout/NavOverlay.tsx @@ -0,0 +1,80 @@ +// REQ-USR-003: 全部导航总览浮层(BR7/BR8/D4)。受控 open;左 NAV_SIDE 列 + 右 NAV_COLS 网格。 +// 叶子据 routePath 分流:有 → onNavigate;无 → onPlaceholder(占位「功能开发中」由调用方提示)。 +import { useEffect } from 'react'; +import { NAV_SIDE, NAV_COLS } from './navConfig'; +import type { NavLeaf } from './navConfig'; +import styles from './AppLayout.module.css'; + +interface NavOverlayProps { + open: boolean; + onClose: () => void; + onNavigate: (routePath: string) => void; + onPlaceholder: () => void; +} + +export default function NavOverlay({ open, onClose, onNavigate, onPlaceholder }: NavOverlayProps) { + // Esc 关闭 + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open, onClose]); + + if (!open) return null; + + const handleLeaf = (leaf: NavLeaf) => { + if (leaf.routePath) onNavigate(leaf.routePath); + else onPlaceholder(); + }; + + return ( +