From 363eec0b27b70c9e1253c38c0809589a3008d3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91?= Date: Mon, 22 Apr 2024 13:55:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=BD=93=E5=89=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/avatar/index.tsx | 50 +++++ src/components/breadcrumb/index.tsx | 111 +++++------ src/components/icon/index.tsx | 84 +++++---- src/components/icon/picker/IconRender.tsx | 51 +++--- src/components/icon/types.ts | 24 +-- src/layout/RootLayout.tsx | 49 +++-- src/layout/style.ts | 34 ++-- src/pages/system/menus/index.tsx | 294 +++++++++++++++--------------- src/service/system.ts | 36 ++-- src/store/user.ts | 67 ++++--- 10 files changed, 444 insertions(+), 356 deletions(-) create mode 100644 src/components/avatar/index.tsx diff --git a/src/components/avatar/index.tsx b/src/components/avatar/index.tsx new file mode 100644 index 0000000..6d85862 --- /dev/null +++ b/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 ( +
+ , + label: {t('app.header.logout')}, + }, + ], + }} + > + + + {!data?.avatar && data?.nickname?.substring(0, 1)} + + + {data?.nickname} + + + +
+ ) +} + +export default Avatar \ No newline at end of file diff --git a/src/components/breadcrumb/index.tsx b/src/components/breadcrumb/index.tsx index e4deeb1..f8a2222 100644 --- a/src/components/breadcrumb/index.tsx +++ b/src/components/breadcrumb/index.tsx @@ -5,73 +5,74 @@ import { getIcon } from '@/components/icon' import { memo, useCallback } from 'react' export const PageBreadcrumb = memo((props: BreadcrumbProps & { - showIcon?: boolean; + showIcon?: boolean; }) => { - const nav = useNavigate() - const { items = [], showIcon = true, ...other } = props + const nav = useNavigate() + const { items = [], showIcon = true, ...other } = props - const renderIcon = useCallback((icon: any) => { - if (icon && showIcon) { - return getIcon(icon) - } - return null - }, []) + const renderIcon = useCallback((icon: any) => { + if (icon && showIcon) { + return getIcon(icon) + } + return null + }, []) - const itemRender = (route) => { + const itemRender = (route) => { - const isLast = route?.path === items[items.length - 1]?.path + const isLast = route?.path === items[items.length - 1]?.path - if (route.children) { - const items = route.children.map((item) => { - return { - ...item, - key: item.path || item.name, - label: item.name, - } + if (route.children) { + const items = route.children.map((item) => { + return { + ...item, + key: item.path || item.name, + label: item.name, + } + }) + return ( + { + nav({ + to: e.key }) - return ( - { - nav({ - to: e.key - }) - } - }} - trigger={[ 'hover' ]}> - { - (!route.component || !route.path)? {renderIcon(route.icon)}{route.name} - - : {renderIcon(route.icon)}{route.name} - - - } - + } + }} + trigger={[ 'hover' ]}> + { + (!route.component || !route.path) ? + {renderIcon(route.icon)}{route.name} + + : {renderIcon(route.icon)}{route.name} + + + } - - ) - } - return isLast || !route.path ? ( - {renderIcon(route.icon)}{route.name} - ) : ( - {renderIcon(route.icon)}{route.name} - ) + + ) } - - return ( - <> - - + return isLast || !route.path ? ( + {renderIcon(route.icon)}{route.name} + ) : ( + {renderIcon(route.icon)}{route.name} ) + } + + + return ( +
+ +
+ ) }) export default PageBreadcrumb \ No newline at end of file diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index 73003b8..d5aead2 100644 --- a/src/components/icon/index.tsx +++ b/src/components/icon/index.tsx @@ -5,56 +5,66 @@ import * as AntIcons from '@ant-design/icons/es/icons' import IconItem from './picker/IconRender.tsx' import { IconUnit } from './types.ts' -export function Icon(props: IconUnit) { - const { type, ...other } = props +type Prefix = 'antd:' | 'park:'; +type IconType = `${Prefix}${string}`; - if (type && [ 'antd:', 'park:' ].includes(type as string)) { - const [ t, c ] = type.split(':') - return - } +interface IconProps extends Pick { + type: IconType | IconUnit['type'] +} - const AntIcon = AntIcons[type as keyof typeof AntIcons] - if (AntIcon) { - return - } +function isAntdOrParkIcon(value: string): value is IconType { + return value.startsWith('antd:') || value.startsWith('park:') +} - //如果是http或https链接,直接返回图片 - if (type && (type.startsWith('http') || type.startsWith('https') || type.startsWith('data:image'))) { - // @ts-ignore 没有办法把所有的属性都传递给img - return icon - } +export function Icon(props: IconProps) { + const { type, ...other } = props + if (type && isAntdOrParkIcon(type)) { + const [ t, c ] = type.split(':') + return + } - if (ALL_ICON_KEYS.indexOf(type as IconType) < 0) { - return null - } + const AntIcon = AntIcons[type as keyof typeof AntIcons] + if (AntIcon) { + return + } + + //如果是http或https链接,直接返回图片 + if (type && (type.startsWith('http') || type.startsWith('https') || type.startsWith('data:image'))) { + // @ts-ignore 没有办法把所有的属性都传递给img + return icon + } - return ( - - - - ) + if (ALL_ICON_KEYS.indexOf(type as IconType) < 0) { + return null + } + + return ( + + + + ) } // eslint-disable-next-line react-refresh/only-export-components export const getIcon = (type: string, props?: Partial) => { - if (React.isValidElement(type)) { - return type - } - //判断是否为json格式 - if (type && type.startsWith('{') && type.endsWith('}')) { - try { - const obj = JSON.parse(type) - type = obj.type - props = obj - } catch (e) { /* empty */ - } + if (React.isValidElement(type)) { + return type + } + //判断是否为json格式 + if (type && type.startsWith('{') && type.endsWith('}')) { + try { + const obj = JSON.parse(type) + type = obj.type + props = obj + } catch (e) { /* empty */ } + } - return + return } export default Icon \ No newline at end of file diff --git a/src/components/icon/picker/IconRender.tsx b/src/components/icon/picker/IconRender.tsx index 4e9a805..ef05f67 100644 --- a/src/components/icon/picker/IconRender.tsx +++ b/src/components/icon/picker/IconRender.tsx @@ -4,37 +4,40 @@ import { AntdIcon, ParkIcon, ALL_ICON_KEYS } from './icons.ts' import { IconType } from '@icon-park/react/es/all' export interface IconRenderProps { - type: 'antd' | 'park'; - componentName?: string; - props?: | { - type?: string; - } | any; + type: 'antd' | 'park'; + componentName?: string; + props?: | { + type?: string; + } | any; } export interface IIconRender { - (props: IconRenderProps): JSX.Element; + (props: IconRenderProps): JSX.Element; } const Render: FC = memo( - ({ type, componentName, props }) => { - switch (type) { - case 'antd': - // eslint-disable-next-line no-case-declarations - const AIcon = AntdIcon[componentName!] - return - - case 'park': - // eslint-disable-next-line no-case-declarations - if (ALL_ICON_KEYS.indexOf(componentName as IconType) < 0) { - return null - } - return - default: { - return null - } - } - }, + ({ type, componentName, props }) => { + switch (type) { + case 'antd': + // eslint-disable-next-line no-case-declarations + const AIcon = AntdIcon[componentName!] + if (!AIcon) { + return null + } + return + + case 'park': + // eslint-disable-next-line no-case-declarations + if (ALL_ICON_KEYS.indexOf(componentName as IconType) < 0) { + return null + } + return + default: { + return null + } + } + }, ) const IconIRender = Render as IIconRender diff --git a/src/components/icon/types.ts b/src/components/icon/types.ts index 9cb050a..d457ec5 100644 --- a/src/components/icon/types.ts +++ b/src/components/icon/types.ts @@ -1,25 +1,25 @@ export interface ReactIcon { - type: 'antd' | 'park'; - componentName: string; - props?: object; + type: 'antd' | 'park'; + componentName: string; + props?: object; } export interface IconfontIcon { - type: 'iconfont'; - componentName: string; - props: { - type: string; - }; - scriptUrl?: string; + type: 'iconfont'; + componentName: string; + props: { + type: string; + }; + scriptUrl?: string; } export interface IconComponentProps { - type: string; + type: string; - [key: string]: any; + [key: string]: any; } /** * 最基础的图标信息单元 */ -export type IconUnit = ReactIcon | IconfontIcon | IconComponentProps \ No newline at end of file +export type IconUnit = IconComponentProps | ReactIcon | IconfontIcon \ No newline at end of file diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx index a47ae2a..bd6f9ea 100644 --- a/src/layout/RootLayout.tsx +++ b/src/layout/RootLayout.tsx @@ -1,19 +1,21 @@ +import Avatar from '@/components/avatar' import PageBreadcrumb from '@/components/breadcrumb' import ErrorPage from '@/components/error/error.tsx' import SelectLang from '@/components/select-lang' import { useTranslation } from '@/i18n.ts' +import { appAtom } from '@/store/system.ts' import { userMenuDataAtom } from '@/store/user.ts' import { MenuItem } from '@/types' 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 { Dropdown } from 'antd' -import { ConfigProvider} from '@/components/config-provider' +import { ConfigProvider } from '@/components/config-provider' import { useState } from 'react' -import Icon from '../components/icon' import defaultProps from './_defaultProps' import { useAtomValue } from 'jotai' import { useStyle } from '@/layout/style.ts' - +import zh from 'antd/locale/zh_CN' +import en from 'antd/locale/en_US' //根据menuData生成Breadcrumb所需的数据 const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => { @@ -42,13 +44,13 @@ export default () => { const { styles } = useStyle() const { t } = useTranslation() const { data: menuData = [], isLoading } = useAtomValue(userMenuDataAtom) - + const { language } = useAtomValue(appAtom) const items = getBreadcrumbData(menuData, location.pathname) const [ pathname, setPathname ] = useState(location.pathname) return (
{ getResetKey={() => 'reset-page'} errorComponent={ErrorPage} > - + { return document.getElementById('crazy-pro-layout') || document.body }} @@ -88,25 +91,12 @@ export default () => { collapsedShowGroupTitle: true, loading: isLoading, }} + 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 ( - , - label: t('app.header.logout'), - }, - ], - }} - > - {dom} - + ) }, }} @@ -114,16 +104,19 @@ export default () => { if (props.isMobile) return [] if (typeof window === 'undefined') return [] return [ - + , ] }} + menuProps={{ + className: styles.sideMenu, + }} menuRender={(_, defaultDom) => ( - <> + {defaultDom} - + )} menuItemRender={(item, dom) => { - return
{ + return
{ setPathname(item.path || '/dashboard') }} > diff --git a/src/layout/style.ts b/src/layout/style.ts index 070a892..6f92f9a 100644 --- a/src/layout/style.ts +++ b/src/layout/style.ts @@ -1,21 +1,31 @@ import { createStyles } from '@/theme' export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { - const prefix = `${prefixCls}-${token?.proPrefix}-layout` + const prefix = `${prefixCls}-${token?.proPrefix}-layout` - const container = { - [prefix]: css` + const container = { + [prefix]: css` + .ant-pro-global-header-logo, + .ant-pro-layout-bg-list { + user-select: none; + } + `, + } - `, - } - - const pageContext = css` - box-shadow: ${token.boxShadowSecondary}; - ` + const pageContext = css` + box-shadow: ${token.boxShadowSecondary}; + ` - return { - container: cx(container[prefix], props?.className), - pageContext, + const sideMenu = css` + .ant-pro-base-menu-inline-group .ant-menu-item-group-title .anticon { + margin-inline-end: 0; } + ` + + return { + container: cx(container[prefix], props?.className), + pageContext, + sideMenu, + } }) \ No newline at end of file diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx index 7484454..b0a1338 100644 --- a/src/pages/system/menus/index.tsx +++ b/src/pages/system/menus/index.tsx @@ -17,154 +17,158 @@ import { createLazyFileRoute } from '@tanstack/react-router' const Menus = () => { - const { styles } = useStyle() - const { t } = useTranslation() - const [ form ] = Form.useForm() - const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom) - const { data = [] } = useAtomValue(menuDataAtom) - const currentMenu = useAtomValue(selectedMenuAtom) ?? {} - - useEffect(() => { - if (isSuccess) { - message.success(t('saveSuccess', '保存成功')) - } - - if (isError) { - notification.error({ - message: t('errorTitle', '错误'), - description: (error as any).message ?? t('saveFail', '保存失败'), - }) - } - - }, [ isError, isSuccess ]) - - return ( - - - - - - } - > - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - return prevValues.id !== curValues.id - }}> - - - - -
- -
-
-
- ) + const { styles } = useStyle() + const { t } = useTranslation() + const [ form ] = Form.useForm() + const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom) + const { data = [] } = useAtomValue(menuDataAtom) + const currentMenu = useAtomValue(selectedMenuAtom) ?? {} + + useEffect(() => { + if (isSuccess) { + message.success(t('saveSuccess', '保存成功')) + } + + if (isError) { + notification.error({ + message: t('errorTitle', '错误'), + description: (error as any).message ?? t('saveFail', '保存失败'), + }) + } + + }, [ isError, isSuccess ]) + + return ( + + + + + + } + > + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + return prevValues.id !== curValues.id + }}> + + + + +
+ +
+
+
+ ) } export const Route = createLazyFileRoute('/system/menus')({ - component: Menus + component: Menus }) diff --git a/src/service/system.ts b/src/service/system.ts index 4f2ab7e..44cb5c8 100644 --- a/src/service/system.ts +++ b/src/service/system.ts @@ -1,3 +1,4 @@ +import { IUserInfo } from '@/types/user' import request from '../request.ts' import { LoginRequest, LoginResponse } from '@/types/login' import { createCURD } from '@/service/base.ts' @@ -5,25 +6,28 @@ import { IMenu } from '@/types/menus' import { IRole } from '@/types/roles' const systemServ = { - dept: { - ...createCURD('/sys/dept') + dept: { + ...createCURD('/sys/dept') + }, + menus: { + ...createCURD('/sys/menu') + }, + login: (data: LoginRequest) => { + return request.post('/sys/login', data) + }, + user: { + current: () => { + return request.get('/sys/user/info') }, - menus: { - ...createCURD('/sys/menu') - }, - login: (data: LoginRequest) => { - return request.post('/sys/login', data) - }, - user: { - menus: () => { - return request.get('/sys/user/menus') - } - - }, - role: { - ...createCURD('/sys/role') + menus: () => { + return request.get('/sys/user/menus') } + }, + role: { + ...createCURD('/sys/role') + } + } diff --git a/src/store/user.ts b/src/store/user.ts index d27ff75..280d7a9 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,49 +1,62 @@ import { appAtom } from '@/store/system.ts' +import { IUserInfo } from '@/types/user' import { AxiosResponse } from 'axios' import { atom } from 'jotai/index' -import { IAuth, MenuItem } from '@/types' +import { IApiResult, IAuth, MenuItem } from '@/types' import { LoginRequest } from '@/types/login' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import systemServ from '@/service/system.ts' import { formatMenuData, isDev } from '@/utils' export const authAtom = atom({ - isLogin: false, - authKey: [] + isLogin: false, + authKey: [] }) const devLogin = { - username: 'SupperAdmin', - password: 'kk123456', - code: '123456' + username: 'SupperAdmin', + password: 'kk123456', + code: '123456' } export const loginFormAtom = atom({ - ...(isDev ? devLogin : {}) + ...(isDev ? devLogin : {}) } as LoginRequest) export const loginAtom = atomWithMutation(() => ({ - mutationKey: [ 'login' ], - mutationFn: async (params) => { - return await systemServ.login(params) - }, - onSuccess: () => { - // console.log('login success', data) - }, - retry: false, + mutationKey: [ 'login' ], + mutationFn: async (params) => { + return await systemServ.login(params) + }, + onSuccess: () => { + // console.log('login success', data) + }, + retry: false, })) -export const userMenuDataAtom = atomWithQuery((get) => ({ - enabled: false, - queryKey: [ 'user_menus', get(appAtom).token ], +export const currentUserAtom = atomWithQuery, any, IUserInfo>((get) => { + return { + queryKey: [ 'user_info', get(appAtom).token ], queryFn: async () => { - return await systemServ.user.menus() - }, - select: (data: AxiosResponse) => { - return formatMenuData(data.data.rows as any ?? []) + return await systemServ.user.current() }, - initialData: () => { - const queryClient = get(queryClientAtom) - return queryClient.getQueryData([ 'user_menus', get(appAtom).token ]) - }, - retry: false, + select: (data) => { + return data.data + } + } +}) + +export const userMenuDataAtom = atomWithQuery((get) => ({ + enabled: false, + queryKey: [ 'user_menus', get(appAtom).token ], + queryFn: async () => { + return await systemServ.user.menus() + }, + select: (data: AxiosResponse) => { + return formatMenuData(data.data.rows as any ?? []) + }, + initialData: () => { + const queryClient = get(queryClientAtom) + return queryClient.getQueryData([ 'user_menus', get(appAtom).token ]) + }, + retry: false, }))