feat: 拆分模型/供应商为独立路由页面,侧边栏支持 SubMenu 分组

This commit is contained in:
2026-06-04 11:11:32 +08:00
parent f67cfa84ef
commit 61b479e2be
13 changed files with 689 additions and 353 deletions

View File

@@ -1,6 +1,7 @@
import type { MenuProps } from "antd";
import { Menu } from "antd";
import { useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import type { MenuItemConfig } from "../../../menu";
@@ -14,23 +15,101 @@ interface SidebarProps {
export function Sidebar({ menuItems }: SidebarProps) {
const navigate = useNavigate();
const location = useLocation();
const currentPath = location.pathname;
const currentItem = menuItems.find((item) => item.path === currentPath);
const selectedKeys = currentItem ? [currentItem.value] : [];
const antdMenuItems: MenuItem[] = menuItems.map((item) => ({
icon: item.icon,
key: item.value,
label: item.label,
}));
const rootSubmenuKeys = useMemo(() => getRootSubmenuKeys(menuItems), [menuItems]);
const antdMenuItems = useMemo(() => toAntdMenuItems(menuItems), [menuItems]);
const [openKeys, setOpenKeys] = useState<string[]>(() => {
return findAncestorKeys(menuItems, currentPath) ?? [];
});
const currentItem = findMenuItem(menuItems, currentPath);
const selectedKeys: string[] = currentItem ? [currentItem.value] : [];
const handleOpenChange: MenuProps["onOpenChange"] = (keys) => {
const latestOpenKey = keys.find((key) => !openKeys.includes(key));
if (latestOpenKey && rootSubmenuKeys.includes(latestOpenKey)) {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
} else {
setOpenKeys(keys);
}
};
const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
const item = menuItems.find((i) => i.value === key);
const item = findByValue(menuItems, key);
if (item) {
void navigate(item.path);
}
};
return <Menu items={antdMenuItems} mode="inline" onClick={handleMenuClick} selectedKeys={selectedKeys} />;
return (
<Menu
items={antdMenuItems}
mode="inline"
onClick={handleMenuClick}
onOpenChange={handleOpenChange}
openKeys={openKeys}
selectedKeys={selectedKeys}
/>
);
}
function findAncestorKeys(
items: readonly MenuItemConfig[],
path: string,
ancestors: string[] = [],
): string[] | undefined {
for (const item of items) {
if (item.path === path) return ancestors;
if (item.children) {
const result = findAncestorKeys(item.children, path, [...ancestors, item.value]);
if (result !== undefined) return result;
}
}
return undefined;
}
function findByValue(items: readonly MenuItemConfig[], value: string): MenuItemConfig | undefined {
for (const item of items) {
if (item.value === value) return item;
if (item.children) {
const found = findByValue(item.children, value);
if (found) return found;
}
}
return undefined;
}
function findMenuItem(items: readonly MenuItemConfig[], path: string): MenuItemConfig | undefined {
for (const item of items) {
if (item.path === path) return item;
if (item.children) {
const found = findMenuItem(item.children, path);
if (found) return found;
}
}
return undefined;
}
function getRootSubmenuKeys(items: readonly MenuItemConfig[]): string[] {
return items.filter((item) => item.children).map((item) => item.value);
}
function toAntdMenuItems(items: readonly MenuItemConfig[]): MenuItem[] {
return items.map((item): MenuItem => {
if (item.children && item.children.length > 0) {
return {
children: toAntdMenuItems(item.children),
icon: item.icon,
key: item.value,
label: item.label,
};
}
return {
icon: item.icon,
key: item.value,
label: item.label,
};
});
}