NavOverlay.tsx 2.52 KB
// 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 (
    <div
      className={styles.navOverlay}
      data-testid="nav-overlay"
      role="dialog"
      aria-modal="true"
    >
      {/* 遮罩:点击空白处关闭(仅自身触发,不冒泡子项) */}
      <div
        className={styles.navOverlayMask}
        data-testid="nav-overlay-mask"
        onClick={onClose}
        aria-hidden="true"
      />
      <div className={styles.navOverlayBody}>
        <div className={styles.navSide} data-testid="nav-side">
          {NAV_SIDE.map((s) => (
            <div
              key={s.key}
              className={`${styles.navSideItem} ${s.active ? styles.navSideItemActive : ''}`}
              aria-current={s.active ? 'true' : undefined}
            >
              {s.label}
            </div>
          ))}
        </div>
        <div className={styles.navGrid}>
          {NAV_COLS.map((col) => (
            <div key={col.title} className={styles.navCol}>
              <h3 className={styles.navColTitle}>{col.title}</h3>
              {col.items.map((leaf) => (
                <button
                  type="button"
                  key={leaf.label}
                  className={styles.navLeaf}
                  onClick={() => handleLeaf(leaf)}
                >
                  {leaf.label}
                  {leaf.star && <span className={styles.navStar}>★</span>}
                </button>
              ))}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}