Browse Source

显示当前用户信息

main
李金 7 months ago
parent
commit
363eec0b27
  1. 50
      src/components/avatar/index.tsx
  2. 7
      src/components/breadcrumb/index.tsx
  3. 18
      src/components/icon/index.tsx
  4. 3
      src/components/icon/picker/IconRender.tsx
  5. 2
      src/components/icon/types.ts
  6. 47
      src/layout/RootLayout.tsx
  7. 12
      src/layout/style.ts
  8. 6
      src/pages/system/menus/index.tsx
  9. 4
      src/service/system.ts
  10. 15
      src/store/user.ts

50
src/components/avatar/index.tsx

@ -0,0 +1,50 @@
import Icon from '@/components/icon'
import { useTranslation } from '@/i18n.ts'
import { currentUserAtom } from '@/store/user.ts'
import { Avatar as AntAvatar, Dropdown, Spin } from 'antd'
import { useAtomValue } from 'jotai'
const Avatar = () => {
const { t } = useTranslation()
const { data, isLoading } = useAtomValue(currentUserAtom)
return (
<div>
<Dropdown
key={'user'}
placement="bottomRight"
menu={{
items: [
{
key: 'logout',
icon: <Icon type={'park:Logout'}/>,
label: <span style={{
marginInlineStart: 8,
userSelect: 'none'
}}>{t('app.header.logout')}</span>,
},
],
}}
>
<Spin spinning={isLoading}>
<AntAvatar
key="avatar"
size={'small'}
src={data?.avatar || data?.nickname?.substring(0, 1)}>
{!data?.avatar && data?.nickname?.substring(0, 1)}
</AntAvatar>
<span key="name"
style={{
marginInlineStart: 8,
userSelect: 'none'
}}>
{data?.nickname}
</span>
</Spin>
</Dropdown>
</div>
)
}
export default Avatar

7
src/components/breadcrumb/index.tsx

@ -40,7 +40,8 @@ export const PageBreadcrumb = memo((props: BreadcrumbProps & {
}} }}
trigger={[ 'hover' ]}> trigger={[ 'hover' ]}>
{ {
(!route.component || !route.path)? <a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
(!route.component || !route.path) ?
<a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
<DownOutlined/></a> <DownOutlined/></a>
: <Link to={`/${route.path}`} : <Link to={`/${route.path}`}
preload={false} preload={false}
@ -65,12 +66,12 @@ export const PageBreadcrumb = memo((props: BreadcrumbProps & {
return ( return (
<>
<div style={{ userSelect: 'none' }}>
<Breadcrumb {...other} <Breadcrumb {...other}
items={items} items={items}
itemRender={itemRender} itemRender={itemRender}
/> />
</>
</div>
) )
}) })

18
src/components/icon/index.tsx

@ -5,13 +5,23 @@ import * as AntIcons from '@ant-design/icons/es/icons'
import IconItem from './picker/IconRender.tsx' import IconItem from './picker/IconRender.tsx'
import { IconUnit } from './types.ts' import { IconUnit } from './types.ts'
export function Icon(props: IconUnit) {
const { type, ...other } = props
type Prefix = 'antd:' | 'park:';
type IconType = `${Prefix}${string}`;
interface IconProps extends Pick<IconUnit, 'type'> {
type: IconType | IconUnit['type']
}
if (type && [ 'antd:', 'park:' ].includes(type as string)) {
function isAntdOrParkIcon(value: string): value is IconType {
return value.startsWith('antd:') || value.startsWith('park:')
}
export function Icon(props: IconProps) {
const { type, ...other } = props
if (type && isAntdOrParkIcon(type)) {
const [ t, c ] = type.split(':') const [ t, c ] = type.split(':')
return <IconItem {...props as any} type={t} componentName={c}/>
return <IconItem {...other as any} type={t} componentName={c}/>
} }
const AntIcon = AntIcons[type as keyof typeof AntIcons] const AntIcon = AntIcons[type as keyof typeof AntIcons]

3
src/components/icon/picker/IconRender.tsx

@ -22,6 +22,9 @@ const Render: FC<IconRenderProps> = memo(
case 'antd': case 'antd':
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const AIcon = AntdIcon[componentName!] const AIcon = AntdIcon[componentName!]
if (!AIcon) {
return null
}
return <AIcon {...props} /> return <AIcon {...props} />
case 'park': case 'park':

2
src/components/icon/types.ts

@ -22,4 +22,4 @@ export interface IconComponentProps {
/** /**
* *
*/ */
export type IconUnit = ReactIcon | IconfontIcon | IconComponentProps
export type IconUnit = IconComponentProps | ReactIcon | IconfontIcon

47
src/layout/RootLayout.tsx

@ -1,19 +1,21 @@
import Avatar from '@/components/avatar'
import PageBreadcrumb from '@/components/breadcrumb' import PageBreadcrumb from '@/components/breadcrumb'
import ErrorPage from '@/components/error/error.tsx' import ErrorPage from '@/components/error/error.tsx'
import SelectLang from '@/components/select-lang' import SelectLang from '@/components/select-lang'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { appAtom } from '@/store/system.ts'
import { userMenuDataAtom } from '@/store/user.ts' import { userMenuDataAtom } from '@/store/user.ts'
import { MenuItem } from '@/types' import { MenuItem } from '@/types'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components' import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
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 { Dropdown } from 'antd'
import { ConfigProvider} from '@/components/config-provider'
import { ConfigProvider } from '@/components/config-provider'
import { useState } from 'react' import { useState } from 'react'
import Icon from '../components/icon'
import defaultProps from './_defaultProps' import defaultProps from './_defaultProps'
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 en from 'antd/locale/en_US'
//根据menuData生成Breadcrumb所需的数据 //根据menuData生成Breadcrumb所需的数据
const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => { const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
@ -42,7 +44,7 @@ export default () => {
const { styles } = useStyle() const { styles } = useStyle()
const { t } = useTranslation() const { t } = useTranslation()
const { data: menuData = [], isLoading } = useAtomValue(userMenuDataAtom) const { data: menuData = [], isLoading } = useAtomValue(userMenuDataAtom)
const { language } = useAtomValue(appAtom)
const items = getBreadcrumbData(menuData, location.pathname) const items = getBreadcrumbData(menuData, location.pathname)
const [ pathname, setPathname ] = useState(location.pathname) const [ pathname, setPathname ] = useState(location.pathname)
@ -59,8 +61,9 @@ export default () => {
getResetKey={() => 'reset-page'} getResetKey={() => 'reset-page'}
errorComponent={ErrorPage} errorComponent={ErrorPage}
> >
<ProConfigProvider hashed={false}>
<ProConfigProvider hashed={false} intl={language === 'zh-CN' ? zhCNIntl : enUSIntl}>
<ConfigProvider <ConfigProvider
locale={language === 'zh-CN' ? zh : en}
getTargetContainer={() => { getTargetContainer={() => {
return document.getElementById('crazy-pro-layout') || document.body return document.getElementById('crazy-pro-layout') || document.body
}} }}
@ -88,25 +91,12 @@ export default () => {
collapsedShowGroupTitle: true, collapsedShowGroupTitle: true,
loading: isLoading, loading: isLoading,
}} }}
avatarProps={{ avatarProps={{
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
size: 'small',
title: '管理员',
render: (_, dom) => {
// src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
render: () => {
return ( return (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <Icon type={'Logout'}/>,
label: t('app.header.logout'),
},
],
}}
>
{dom}
</Dropdown>
<Avatar/>
) )
}, },
}} }}
@ -114,16 +104,19 @@ export default () => {
if (props.isMobile) return [] if (props.isMobile) return []
if (typeof window === 'undefined') return [] if (typeof window === 'undefined') return []
return [ return [
<SelectLang/>
<SelectLang/>,
] ]
}} }}
menuProps={{
className: styles.sideMenu,
}}
menuRender={(_, defaultDom) => ( menuRender={(_, defaultDom) => (
<>
<span style={{ userSelect: 'none' }}>
{defaultDom} {defaultDom}
</>
</span>
)} )}
menuItemRender={(item, dom) => { menuItemRender={(item, dom) => {
return <div onClick={() => {
return <div style={{ userSelect: 'none' }} onClick={() => {
setPathname(item.path || '/dashboard') setPathname(item.path || '/dashboard')
}} }}
> >

12
src/layout/style.ts

@ -5,7 +5,10 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
const container = { const container = {
[prefix]: css` [prefix]: css`
.ant-pro-global-header-logo,
.ant-pro-layout-bg-list {
user-select: none;
}
`, `,
} }
@ -13,9 +16,16 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
box-shadow: ${token.boxShadowSecondary}; box-shadow: ${token.boxShadowSecondary};
` `
const sideMenu = css`
.ant-pro-base-menu-inline-group .ant-menu-item-group-title .anticon {
margin-inline-end: 0;
}
`
return { return {
container: cx(container[prefix], props?.className), container: cx(container[prefix], props?.className),
pageContext, pageContext,
sideMenu,
} }
}) })

6
src/pages/system/menus/index.tsx

@ -42,7 +42,11 @@ const Menus = () => {
<PageContainer breadcrumbRender={false} title={false} className={styles.container}> <PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<Flexbox horizontal> <Flexbox horizontal>
<DraggablePanel expandable={false} placement="left" maxWidth={800} style={{ width: '100%', }}>
<DraggablePanel expandable={false}
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
>
<ProCard title={t('system.menus.title', '菜单')} <ProCard title={t('system.menus.title', '菜单')}
extra={ extra={
<BatchButton/> <BatchButton/>

4
src/service/system.ts

@ -1,3 +1,4 @@
import { IUserInfo } from '@/types/user'
import request from '../request.ts' import request from '../request.ts'
import { LoginRequest, LoginResponse } from '@/types/login' import { LoginRequest, LoginResponse } from '@/types/login'
import { createCURD } from '@/service/base.ts' import { createCURD } from '@/service/base.ts'
@ -15,6 +16,9 @@ const systemServ = {
return request.post<LoginResponse>('/sys/login', data) return request.post<LoginResponse>('/sys/login', data)
}, },
user: { user: {
current: () => {
return request.get<IUserInfo>('/sys/user/info')
},
menus: () => { menus: () => {
return request.get<IMenu[]>('/sys/user/menus') return request.get<IMenu[]>('/sys/user/menus')
} }

15
src/store/user.ts

@ -1,7 +1,8 @@
import { appAtom } from '@/store/system.ts' import { appAtom } from '@/store/system.ts'
import { IUserInfo } from '@/types/user'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import { atom } from 'jotai/index' import { atom } from 'jotai/index'
import { IAuth, MenuItem } from '@/types'
import { IApiResult, IAuth, MenuItem } from '@/types'
import { LoginRequest } from '@/types/login' import { LoginRequest } from '@/types/login'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import systemServ from '@/service/system.ts' import systemServ from '@/service/system.ts'
@ -32,6 +33,18 @@ export const loginAtom = atomWithMutation<any, LoginRequest>(() => ({
retry: false, retry: false,
})) }))
export const currentUserAtom = atomWithQuery<IApiResult<IUserInfo>, any, IUserInfo>((get) => {
return {
queryKey: [ 'user_info', get(appAtom).token ],
queryFn: async () => {
return await systemServ.user.current()
},
select: (data) => {
return data.data
}
}
})
export const userMenuDataAtom = atomWithQuery<any, MenuItem[]>((get) => ({ export const userMenuDataAtom = atomWithQuery<any, MenuItem[]>((get) => ({
enabled: false, enabled: false,
queryKey: [ 'user_menus', get(appAtom).token ], queryKey: [ 'user_menus', get(appAtom).token ],

Loading…
Cancel
Save