Sidebar.tsx 4.27 KB
import { useState } from "react";
import {
  HomeOutlined,
  FolderOutlined,
  FileTextOutlined,
  SettingOutlined,
  DownOutlined,
  RightOutlined,
  SearchOutlined,
} from "@ant-design/icons";
import { NAV_TREE, type NavNode } from "@/utils/data";

interface Props {
  activeNodeId: string;
  onNodeClick: (node: NavNode) => void;
}

const iconFor = (icon?: string) => {
  switch (icon) {
    case "home":
      return <HomeOutlined />;
    case "doc":
      return <FileTextOutlined />;
    case "settings":
      return <SettingOutlined />;
    default:
      return <FolderOutlined />;
  }
};

export default function Sidebar({ activeNodeId, onNodeClick }: Props) {
  const [expanded, setExpanded] = useState<Record<string, boolean>>({
    kpi: true,
    quote: true,
    sys: true,
  });
  const [query, setQuery] = useState("");

  const toggle = (id: string) => setExpanded((s) => ({ ...s, [id]: !s[id] }));

  const renderNode = (node: NavNode, depth: number): React.ReactNode => {
    const hasChildren = !!(node.children && node.children.length);
    const isOpen = !!expanded[node.id];
    const active = node.id === activeNodeId;

    if (query && !node.label.toLowerCase().includes(query.toLowerCase()) && !hasChildren) {
      return null;
    }

    return (
      <div key={node.id}>
        <div
          onClick={() => {
            if (hasChildren) toggle(node.id);
            onNodeClick(node);
          }}
          style={{
            display: "flex",
            alignItems: "center",
            gap: 6,
            paddingLeft: 8 + depth * 14,
            paddingRight: 8,
            height: 26,
            fontSize: 12,
            cursor: "pointer",
            color: active ? "var(--accent-strong)" : "var(--text)",
            background: active ? "var(--selected)" : "transparent",
            borderLeft: active ? "2px solid var(--accent)" : "2px solid transparent",
            fontWeight: active ? 500 : 400,
          }}
          onMouseEnter={(e) => {
            if (!active) e.currentTarget.style.background = "var(--bg-row-hover)";
          }}
          onMouseLeave={(e) => {
            if (!active) e.currentTarget.style.background = "transparent";
          }}
        >
          <span style={{ width: 12, display: "inline-flex" }}>
            {hasChildren ? (
              isOpen ? (
                <DownOutlined style={{ fontSize: 9 }} />
              ) : (
                <RightOutlined style={{ fontSize: 9 }} />
              )
            ) : null}
          </span>
          <span style={{ display: "inline-flex", color: "var(--text-muted)" }}>
            {iconFor(node.icon)}
          </span>
          <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {node.label}
          </span>
          {node.badge && (
            <span
              style={{
                fontSize: 10,
                color: "var(--accent-strong)",
                background: "var(--accent-soft)",
                padding: "1px 5px",
                borderRadius: 2,
              }}
            >
              {node.badge}
            </span>
          )}
        </div>
        {hasChildren && isOpen && (
          <div>{node.children!.map((c) => renderNode(c, depth + 1))}</div>
        )}
      </div>
    );
  };

  return (
    <div
      style={{
        height: "100%",
        display: "flex",
        flexDirection: "column",
        background: "#fff",
        borderRight: "1px solid var(--border)",
      }}
    >
      <div
        style={{
          padding: 6,
          borderBottom: "1px solid var(--border)",
          display: "flex",
          alignItems: "center",
          gap: 6,
        }}
      >
        <SearchOutlined style={{ color: "var(--text-faint)", fontSize: 12 }} />
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="搜索菜单"
          style={{
            flex: 1,
            height: 22,
            border: "1px solid var(--border-input)",
            background: "var(--bg-input)",
            padding: "0 6px",
            fontSize: 12,
            outline: "none",
            fontFamily: "inherit",
          }}
        />
      </div>
      <div style={{ flex: 1, overflow: "auto" }}>{NAV_TREE.map((n) => renderNode(n, 0))}</div>
    </div>
  );
}