From f4dd228a677dc579b297ef3d01ddcbd4f73ba9ed Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 01:01:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=8F=92=E4=BB=B6=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E6=94=B9=E4=B8=BA=E4=B8=89=E7=BA=A7=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=BB=93=E6=9E=84=20=E2=80=94=20=E6=8C=89=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=90=8D=E5=88=86=E7=BB=84=E5=8F=AF=E6=8A=98=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 插件菜单从扁平列表改为三级结构: 插件(分组)→ 插件名(可折叠子标题)→ 页面列表 - store 新增 PluginMenuGroup 类型和 pluginMenuGroups getter - MainLayout 新增 SidebarSubMenu 组件,支持展开/收起 - 折叠侧边栏时子菜单显示插件图标 + tooltip - 子菜单项增加缩进样式区分层级 - CRM 插件 name 改为 "CRM" 避免与页面标题重名 --- apps/web/src/index.css | 44 +++++++++ apps/web/src/layouts/MainLayout.tsx | 136 +++++++++++++++++++++------- apps/web/src/stores/plugin.ts | 27 +++++- crates/erp-plugin-crm/plugin.toml | 2 +- 4 files changed, 173 insertions(+), 36 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b9dfc40..ec1c1b7 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -658,6 +658,50 @@ body { margin-left: 12px; } +/* Sidebar sub-menu (plugin group) */ +.erp-sidebar-submenu-title { + display: flex; + align-items: center; + height: 32px; + margin: 6px 8px 2px 8px; + padding: 0 12px; + border-radius: 6px; + cursor: pointer; + color: #94A3B8; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + transition: all 0.15s; + user-select: none; +} + +.erp-sidebar-submenu-title:hover { + background: rgba(255, 255, 255, 0.06); + color: #E2E8F0; +} + +.erp-sidebar-submenu-title-active { + color: #A5B4FC; +} + +.erp-sidebar-submenu-arrow { + display: flex; + align-items: center; + margin-right: 8px; + font-size: 10px; +} + +.erp-sidebar-submenu-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.erp-sidebar-item-indented { + padding-left: 36px; +} + /* Main layout */ .erp-main-layout { transition: margin-left 0.2s cubic-bezier(0.4, 0, 0.2, 1); diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index 01f165c..3fd7b43 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -1,4 +1,4 @@ -import { useCallback, memo, useEffect } from 'react'; +import { useCallback, useState, memo, useEffect } from 'react'; import { Layout, Avatar, Space, Dropdown, Tooltip, theme } from 'antd'; import { HomeOutlined, @@ -18,11 +18,13 @@ import { TeamOutlined, TableOutlined, TagsOutlined, + RightOutlined, } from '@ant-design/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAppStore } from '../stores/app'; import { useAuthStore } from '../stores/auth'; import { usePluginStore } from '../stores/plugin'; +import type { PluginMenuGroup } from '../stores/plugin'; import NotificationPanel from '../components/NotificationPanel'; const { Header, Sider, Content, Footer } = Layout; @@ -67,17 +69,19 @@ const SidebarMenuItem = memo(function SidebarMenuItem({ isActive, collapsed, onClick, + indented, }: { item: MenuItem; isActive: boolean; collapsed: boolean; onClick: () => void; + indented?: boolean; }) { return (
{item.icon} {!collapsed && {item.label}} @@ -86,10 +90,95 @@ const SidebarMenuItem = memo(function SidebarMenuItem({ ); }); +// 动态图标映射 +const pluginIconMap: Record = { + AppstoreOutlined: , + team: , + TeamOutlined: , + user: , + UserOutlined: , + message: , + MessageOutlined: , + tags: , + TagsOutlined: , + apartment: , + ApartmentOutlined: , + TableOutlined: , + DashboardOutlined: , +}; + +function getPluginIcon(iconName: string): React.ReactNode { + return pluginIconMap[iconName] || ; +} + +// 插件子菜单组 — 可折叠二级标题 + 三级菜单项 +const SidebarSubMenu = memo(function SidebarSubMenu({ + group, + collapsed, + currentPath, + onNavigate, +}: { + group: PluginMenuGroup; + collapsed: boolean; + currentPath: string; + onNavigate: (key: string) => void; +}) { + const [expanded, setExpanded] = useState(true); + const hasActive = group.items.some((item) => currentPath === item.key); + + if (collapsed) { + // 折叠模式:显示插件图标,Tooltip 列出所有子项 + const tooltipContent = group.items.map((item) => item.label).join(' / '); + return ( + +
{ + const first = group.items[0]; + if (first) onNavigate(first.key); + }} + className={`erp-sidebar-item ${hasActive ? 'erp-sidebar-item-active' : ''}`} + > + +
+
+ ); + } + + return ( +
+
setExpanded((e) => !e)} + > + + + + {group.pluginName} +
+ {expanded && group.items.map((item) => ( + onNavigate(item.key)} + indented + /> + ))} +
+ ); +}); + export default function MainLayout({ children }: { children: React.ReactNode }) { const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore(); const { user, logout } = useAuthStore(); - const { pluginMenuItems, fetchPlugins } = usePluginStore(); + const { pluginMenuItems, pluginMenuGroups, fetchPlugins } = usePluginStore(); theme.useToken(); const navigate = useNavigate(); const location = useLocation(); @@ -173,40 +262,19 @@ export default function MainLayout({ children }: { children: React.ReactNode })
{/* 菜单组:插件 */} - {pluginMenuItems.length > 0 && ( + {pluginMenuGroups.length > 0 && ( <> {!sidebarCollapsed &&
插件
}
- {pluginMenuItems.map((item) => { - // 动态图标映射 - const iconMap: Record = { - AppstoreOutlined: , - team: , - TeamOutlined: , - user: , - UserOutlined: , - message: , - MessageOutlined: , - tags: , - TagsOutlined: , - apartment: , - ApartmentOutlined: , - TableOutlined: , - }; - return ( - , - label: item.label, - }} - isActive={currentPath === item.key} - collapsed={sidebarCollapsed} - onClick={() => navigate(item.key)} - /> - ); - })} + {pluginMenuGroups.map((group) => ( + navigate(key)} + /> + ))}
)} diff --git a/apps/web/src/stores/plugin.ts b/apps/web/src/stores/plugin.ts index fe653cf..769d5b1 100644 --- a/apps/web/src/stores/plugin.ts +++ b/apps/web/src/stores/plugin.ts @@ -9,13 +9,19 @@ export interface PluginMenuItem { pluginId: string; entity?: string; pageType: 'crud' | 'tree' | 'tabs' | 'detail' | 'graph' | 'dashboard'; - menuGroup?: string; +} + +export interface PluginMenuGroup { + pluginId: string; + pluginName: string; + items: PluginMenuItem[]; } interface PluginStore { plugins: PluginInfo[]; loading: boolean; pluginMenuItems: PluginMenuItem[]; + pluginMenuGroups: PluginMenuGroup[]; schemaCache: Record; fetchPlugins: (page?: number, status?: PluginStatus) => Promise; refreshMenuItems: () => void; @@ -25,6 +31,7 @@ export const usePluginStore = create((set, get) => ({ plugins: [], loading: false, pluginMenuItems: [], + pluginMenuGroups: [], schemaCache: {}, fetchPlugins: async (page = 1, status?: PluginStatus) => { @@ -124,5 +131,23 @@ export const usePluginStore = create((set, get) => ({ } set({ pluginMenuItems: items }); + + // 按 pluginId 分组生成三级菜单(复用上方已解构的 plugins) + const groupMap = new Map(); + for (const item of items) { + const list = groupMap.get(item.pluginId) || []; + list.push(item); + groupMap.set(item.pluginId, list); + } + const groups: PluginMenuGroup[] = []; + for (const [pluginId, groupItems] of groupMap) { + const plugin = plugins.find((p) => p.id === pluginId); + groups.push({ + pluginId, + pluginName: plugin?.name || pluginId, + items: groupItems, + }); + } + set({ pluginMenuGroups: groups }); }, })); diff --git a/crates/erp-plugin-crm/plugin.toml b/crates/erp-plugin-crm/plugin.toml index 6d7b4f1..31604e7 100644 --- a/crates/erp-plugin-crm/plugin.toml +++ b/crates/erp-plugin-crm/plugin.toml @@ -1,6 +1,6 @@ [metadata] id = "erp-crm" -name = "客户管理" +name = "CRM" version = "0.1.0" description = "客户关系管理插件 — ERP 平台第一个行业插件" author = "ERP Team"