Browse Source

修改菜单布局

main
dark 3 months ago
parent
commit
077b2e3502
  1. 65
      src/components/breadcrumb/index.tsx
  2. 15
      src/components/breadcrumb/style.ts
  3. 4
      src/global.d.ts
  4. 258
      src/layout/RootLayout.tsx
  5. 46
      src/layout/style.ts
  6. 21
      src/routes.tsx
  7. 2
      src/store/system/user.ts
  8. 1
      src/types/system/menus.d.ts
  9. 24
      src/utils/index.ts

65
src/components/breadcrumb/index.tsx

@ -3,11 +3,13 @@ import { Link, useNavigate } from '@tanstack/react-router'
import { DownOutlined } from '@ant-design/icons' import { DownOutlined } from '@ant-design/icons'
import { getIcon } from '@/components/icon' import { getIcon } from '@/components/icon'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useStyle } from './style.ts'
export const PageBreadcrumb = memo((props: BreadcrumbProps & { export const PageBreadcrumb = memo((props: BreadcrumbProps & {
showIcon?: boolean; showIcon?: boolean;
}) => { }) => {
const { styles }= useStyle()
const nav = useNavigate() const nav = useNavigate()
const { items = [], showIcon = true, ...other } = props const { items = [], showIcon = true, ...other } = props
@ -27,51 +29,54 @@ export const PageBreadcrumb = memo((props: BreadcrumbProps & {
return { return {
...item, ...item,
key: item.path || item.name, key: item.path || item.name,
label: item.name,
label: <span className={'s-title'}>{item.name}</span>,
} }
}) })
return ( return (
<Dropdown menu={{
items, onClick: (e) => {
nav({
to: e.key
})
}
}}
trigger={[ 'hover' ]}>
{
(!route.component || !route.path) ?
<a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
<DownOutlined/></a>
: <Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
<DownOutlined/>
</Link>
}
<Dropdown
overlayClassName={styles.container}
menu={{
items, onClick: (e) => {
nav({
to: e.key
})
}
}}
trigger={[ 'hover' ]}>
{
(!route.component || !route.path) ?
<a className={'item'}>{renderIcon(route.icon)}<span className={'s-title'}>{route.name}</span>
<DownOutlined/></a>
: <Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span
className={'s-title'}>{route.name}</span>
<DownOutlined/>
</Link>
}
</Dropdown>
</Dropdown>
) )
} }
return isLast || !route.path ? ( return isLast || !route.path ? (
<span className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></span>
<span className={'item'}>{renderIcon(route.icon)}<span className={'s-title'}>{route.name}</span></span>
) : ( ) : (
<Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></Link>
<Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span className={'s-title'}>{route.name}</span></Link>
) )
} }
return ( return (
<div style={{ userSelect: 'none' }}>
<Breadcrumb {...other}
items={items}
itemRender={itemRender}
/>
</div>
<div style={{ userSelect: 'none' }}>
<Breadcrumb {...other}
items={items}
itemRender={itemRender}
/>
</div>
) )
}) })

15
src/components/breadcrumb/style.ts

@ -0,0 +1,15 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-header-breadcrumb`
const container = css`
.ant-dropdown-menu-title-content {
padding-inline-start: 10px;
}
`
return {
container: cx(prefix, props?.className, container)
}
}) as any

4
src/global.d.ts

@ -38,8 +38,8 @@ export type IApiResult<T = any> = {
} }
export type TreeItem<T> = { export type TreeItem<T> = {
[key: keyof T]: T[keyof T];
children?: TreeItem<T>[]; children?: TreeItem<T>[];
[key: keyof T]: T[keyof T];
} }
export type FlattenData<T> = TreeItem<T> & { export type FlattenData<T> = TreeItem<T> & {
@ -47,6 +47,8 @@ export type FlattenData<T> = TreeItem<T> & {
title?: string, title?: string,
label?: string, label?: string,
level?: number, level?: number,
[key: keyof T]: T[keyof T];
} }
export type FiledNames = { export type FiledNames = {

258
src/layout/RootLayout.tsx

@ -9,12 +9,13 @@ import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
import { zhCNIntl, enUSIntl } from '@ant-design/pro-provider/es/intl' import { zhCNIntl, enUSIntl } from '@ant-design/pro-provider/es/intl'
import { CatchBoundary, Link, Outlet } from '@tanstack/react-router' import { CatchBoundary, Link, Outlet } from '@tanstack/react-router'
import { ConfigProvider } from '@/components/config-provider' import { ConfigProvider } from '@/components/config-provider'
import { useState } from 'react'
import defaultProps from './_defaultProps'
import { useEffect, useRef, useState } from 'react'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { useStyle } from '@/layout/style.ts' import { useStyle } from '@/layout/style.ts'
import zh from 'antd/locale/zh_CN' import zh from 'antd/locale/zh_CN'
import en from 'antd/locale/en_US' import en from 'antd/locale/en_US'
import type { MenuDataItem } from '@ant-design/pro-layout/es/typing'
import { flattenTree } from '@/utils'
//根据menuData生成Breadcrumb所需的数据 //根据menuData生成Breadcrumb所需的数据
const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => { const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
@ -46,98 +47,181 @@ export default () => {
const items = getBreadcrumbData(menuData, location.pathname) const items = getBreadcrumbData(menuData, location.pathname)
const [ pathname, setPathname ] = useState(location.pathname) const [ pathname, setPathname ] = useState(location.pathname)
const menusFlatten = useRef<MenuItem[]>()
if (!menusFlatten.current) {
menusFlatten.current = flattenTree<MenuItem>(menuData, { key: 'id', title: 'name' })
}
const [ rootMenuKeys, setRootMenuKeys ] = useState<string[]>(() => {
const item = menusFlatten.current?.find(item => item.path === location.pathname)
return item ? item.parentName : []
})
const childMenuRef = useRef<MenuItem[]>([])
childMenuRef.current = menuData.find(item => {
return item.key === rootMenuKeys?.[0]
})?.children || []
useEffect(() => {
const item = menusFlatten.current?.find(item => item.path === location.pathname)
if (item && item.key !== rootMenuKeys?.[0]) {
setRootMenuKeys(item.parentName)
}
}, [ location.pathname ])
return ( return (
<div
className={styles.container}
id="crazy-pro-layout"
style={{
height: '100vh',
// overflow: 'auto',
}}
>
<CatchBoundary
getResetKey={() => 'reset-page'}
errorComponent={ErrorPage}
>
<ProConfigProvider hashed={false} intl={language === 'zh-CN' ? zhCNIntl : enUSIntl}>
<ConfigProvider
locale={language === 'zh-CN' ? zh : en}
getTargetContainer={() => {
return document.getElementById('crazy-pro-layout') || document.body
}}
<div
className={styles.container}
id="crazy-pro-layout"
style={{
height: '100vh',
// overflow: 'auto',
}}
> >
<ProLayout
headerContentRender={() => <PageBreadcrumb
className={'top-breadcrumb'}
showIcon={false}
items={items}/>}
title="Crazy Pro"
{...defaultProps}
route={{
path: '/',
routes: menuData
}}
location={{
pathname,
}}
token={{
header: {
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)',
},
}}
menu={{
collapsedShowGroupTitle: true,
loading: isLoading,
}}
<CatchBoundary
getResetKey={() => 'reset-page'}
errorComponent={ErrorPage}
>
<ProConfigProvider hashed={false} intl={language === 'zh-CN' ? zhCNIntl : enUSIntl}>
<ConfigProvider
locale={language === 'zh-CN' ? zh : en}
getTargetContainer={() => {
return document.getElementById('crazy-pro-layout') || document.body
}}
>
<ProLayout
token={{
header: {
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)',
},
}}
fixedHeader={true}
headerContentRender={() => <PageBreadcrumb
className={'top-breadcrumb'}
showIcon={false}
items={items}/>}
title="Crazy Pro"
layout={'mix'}
fixSiderbar={true}
siderWidth={100}
collapsedButtonRender={false}
// collapsed={false}
postMenuData={() => {
return menuData.map(item => ({
...item,
children: [],
})) as any
}}
route={
{
path: '/',
routes: menuData.map(item => ({
...item,
// path: item.path ?? `/${item.key}`,
children: [],
// routes: undefined
}))
}
}
location={
{
pathname,
}
}
menu={{
collapsedShowGroupTitle: true,
avatarProps={{
// src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
render: () => {
return (
<Avatar/>
)
},
}}
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
return [
<SelectLang/>,
]
}}
menuProps={{
className: styles.sideMenu,
}}
menuRender={(_, defaultDom) => (
<span style={{ userSelect: 'none' }}>
{defaultDom}
}}
menuItemRender={(item: MenuDataItem) => {
return <span style={{ userSelect: 'none' }} onClick={() => {
setRootMenuKeys([ (item as any).key || 'dashboard' ])
setPathname(item.path || '/dashboard')
}}
>
<Link to={item.path} className={'menu-link'} target={item.type === 'url' ? '_blank' : '_self'}>
<span>{item.icon}</span>
<span>{item.name}</span>
</Link>
</span> </span>
)}
menuItemRender={(item, dom) => {
return <span style={{ userSelect: 'none' }} onClick={() => {
setPathname(item.path || '/dashboard')
}}
>
}}
avatarProps={{
// src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
render: () => {
return (
<Avatar/>
)
},
}}
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
return [
<SelectLang/>,
]
}}
menuProps={{
className: styles.mySiderMenu,
selectedKeys: rootMenuKeys,
}}
// navTheme={'light'}
contentStyle={{ paddingBlock: 0, paddingInline: 0 }}
>
<ProLayout
className={styles.mySider}
headerRender={false}
hasSiderMenu={false}
postMenuData={() => {
return (childMenuRef.current || []) as any
}}
route={{
path: '/',
routes: menuData
}}
location={{
pathname,
}}
token={{
header: {
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)',
},
}}
menuProps={{
className: styles.sideMenu,
}}
menu={{
hideMenuWhenCollapsed: false,
// collapsedShowGroupTitle: true,
loading: isLoading,
}}
menuRender={childMenuRef.current?.length ? undefined : false}
menuItemRender={(item, dom) => {
return <span style={{ userSelect: 'none' }} onClick={() => {
setPathname(item.path || '/dashboard')
}}
>
<Link to={item.path} target={item.type === 'url' ? '_blank' : '_self'}> <Link to={item.path} target={item.type === 'url' ? '_blank' : '_self'}>
{dom} {dom}
</Link> </Link>
</span> </span>
}}
{...{
'layout': 'mix',
'navTheme': 'light',
'contentWidth': 'Fluid',
'fixSiderbar': true,
// 'colorPrimary': '#1677FF',
'siderMenuType': 'group',
// layout: 'side',
}}
>
<Outlet/>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</CatchBoundary>
</div>
}}
{...{
'layout': 'mix',
'navTheme': 'light',
'contentWidth': 'Fluid',
'fixSiderbar': false,
// 'colorPrimary': '#1677FF',
// 'siderMenuType': 'group',
// layout: 'side',
}}
>
<Outlet/>
</ProLayout>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</CatchBoundary>
</div>
) )
} }

46
src/layout/style.ts

@ -13,7 +13,7 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
} }
.ant-menu-inline-collapsed >.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item{ .ant-menu-inline-collapsed >.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item{
padding-inline-start: 0;
//padding-inline-start: 0;
} }
`, `,
@ -25,10 +25,50 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
const sideMenu = css` const sideMenu = css`
.ant-pro-base-menu-inline-group .ant-menu-item-group-title .anticon { .ant-pro-base-menu-inline-group .ant-menu-item-group-title .anticon {
margin-inline-end: 0;
//margin-inline-end: 0;
} }
` `
const mySider = css`
.ant-layout-sider-children{
//background-color: #001529;
padding-block-start: 10px!important;
}
.ant-menu-inline-collapsed >.ant-menu-item{
padding-inline: calc(50% - 19px - 4px);
}
`
const mySiderMenu = css`
padding-top: 10px;
.ant-menu-item{
padding-inline: unset;
padding-left: unset!important;
flex-direction: column;
height: 64px;
line-height: 64px;
}
.ant-menu-item-selected{
background-color: rgb(210, 229, 255);
color: rgb(37, 59, 125);
}
.ant-menu-item >.ant-menu-title-content{
text-overflow: unset!important;
overflow: unset!important;
flex: auto!important;
align-content: center;
.menu-link{
display: flex;
flex-direction: column;
line-height: normal;
align-items: center;
}
}
`
const box = css` const box = css`
flex: 1; flex: 1;
background: ${token.colorBgContainer}; background: ${token.colorBgContainer};
@ -44,6 +84,8 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
authHeight, authHeight,
pageContext, pageContext,
sideMenu, sideMenu,
mySider,
mySiderMenu,
} }
}) })

21
src/routes.tsx

@ -228,7 +228,26 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
return createRoute({ return createRoute({
...options, ...options,
// @ts-ignore fix import // @ts-ignore fix import
component: lazyRouteComponent(() => (modules[`./pages/${component}/index.tsx`] || modules[`./pages/${component}/index.jsx`])?.()),
component: lazyRouteComponent(() => {
//处理最后可能包含index || index.tsx || index.jsx
if (component.endsWith('.tsx')) {
component = component.replace(/\.tsx$/, '')
}
if (component.endsWith('.jsx')) {
component = component.replace(/\.jsx$/, '')
}
if (component.endsWith('/index')) {
component = component.replace(/\/index$/, '')
}
const module = modules[`./pages/${component}/index.tsx`] || modules[`./pages/${component}/index.jsx`]
if (!module) {
return NotFound
}
return module()
}),
notFoundComponent: NotFound, notFoundComponent: NotFound,
}) })
} }

2
src/store/system/user.ts

@ -63,7 +63,7 @@ export const userMenuDataAtom = atomWithQuery<IApiResult<IPageResult<System.IMen
return await systemServ.user.menus() return await systemServ.user.menus()
}, },
select: (data) => { select: (data) => {
return formatMenuData(data.data.rows as any ?? [])
return formatMenuData(data.data.rows as any ?? [], [])
}, },
retry: false, retry: false,
})) }))

1
src/types/system/menus.d.ts

@ -15,6 +15,7 @@ export interface IMenu {
sort: number, sort: number,
code: string, code: string,
name: string, name: string,
parentName: string[],
title: string, title: string,
component: string, component: string,
icon: string | any, icon: string | any,

24
src/utils/index.ts

@ -1,5 +1,5 @@
import { IMenu } from '@/types/system/menus' import { IMenu } from '@/types/system/menus'
import { FiledNames, FlattenData, MenuItem, TreeItem } from '@/global'
import { FiledNames, FlattenData, MenuItem } from '@/global'
import { getIcon } from '@/components/icon' import { getIcon } from '@/components/icon'
import { TreeDataNode } from 'antd' import { TreeDataNode } from 'antd'
@ -7,7 +7,7 @@ import { TreeDataNode } from 'antd'
export const isDev = import.meta.env.MODE === 'development' export const isDev = import.meta.env.MODE === 'development'
// 格式化菜单数据, 把children转换成routes // 格式化菜单数据, 把children转换成routes
export const formatMenuData = (data: IMenu[]) => {
export const formatMenuData = (data: IMenu[], parentName: string[]) => {
const result: MenuItem[] = [] const result: MenuItem[] = []
for (const item of data) { for (const item of data) {
if (item.icon && typeof item.icon === 'string') { if (item.icon && typeof item.icon === 'string') {
@ -17,7 +17,8 @@ export const formatMenuData = (data: IMenu[]) => {
result.push({ result.push({
...item, ...item,
key: item.name, key: item.name,
name: item.title
name: item.title,
parentName,
}) })
} else { } else {
const { children, name, ...other } = item const { children, name, ...other } = item
@ -25,8 +26,8 @@ export const formatMenuData = (data: IMenu[]) => {
...other, ...other,
key: name, key: name,
name: other.title, name: other.title,
children: formatMenuData(children),
routes: formatMenuData(children),
children: formatMenuData(children, [ ...parentName, name ]),
routes: formatMenuData(children, [ ...parentName, name ]),
}) })
} }
@ -65,14 +66,15 @@ const defaultTreeFieldNames: FiledNames = {
children: 'children' children: 'children'
} }
export function flattenTree<T>(tree: TreeItem<T>[], fieldNames?: FiledNames) {
const result: FlattenData<T>[] = []
export function flattenTree<T>(tree: T[], fieldNames?: FiledNames) {
const result: T[] = []
if (!fieldNames) {
fieldNames = defaultTreeFieldNames
fieldNames = {
...defaultTreeFieldNames,
...fieldNames
} }
function flattenRecursive(item: TreeItem<T>, level: number, fieldNames: FiledNames) {
function flattenRecursive(item: T, level: number, fieldNames: FiledNames) {
const data: FlattenData<T> = { const data: FlattenData<T> = {
...item, ...item,
@ -85,7 +87,7 @@ export function flattenTree<T>(tree: TreeItem<T>[], fieldNames?: FiledNames) {
children.forEach((child) => flattenRecursive(child, level + 1, fieldNames)) children.forEach((child) => flattenRecursive(child, level + 1, fieldNames))
data.children = children data.children = children
} }
result.push(data)
result.push(data as T)
} }
tree.forEach((item) => flattenRecursive(item, 0, fieldNames)) tree.forEach((item) => flattenRecursive(item, 0, fieldNames))

Loading…
Cancel
Save