diff --git a/package.json b/package.json
index 8b71454..2e6fac3 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@tanstack/react-query": "^5.29.2",
"@tanstack/react-router": "^1.26.20",
"antd": "^5.16.1",
+ "antd-style": "^3.6.2",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"i18next": "^23.11.2",
diff --git a/src/App.tsx b/src/App.tsx
index 24bb8ff..7a9c2c3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,8 @@
+import ErrorPage from '@/components/error/error.tsx'
import { AppContextProvider } from '@/context.ts'
import { initI18n } from '@/i18n.ts'
-import { appAtom, appStore, menuDataAtom, changeLanguage } from '@/store/system.ts'
+import { appAtom, appStore, changeLanguage } from '@/store/system.ts'
+import { userMenuDataAtom } from '@/store/user.ts'
import { IAppData } from '@/types'
import { ConfigProvider } from 'antd'
import { Provider, useAtom, useAtomValue } from 'jotai'
@@ -13,18 +15,25 @@ import PageLoading from '@/components/page-loading'
function App() {
const [ appData, ] = useAtom(appAtom)
- const { data, isError, error, isPending } = useAtomValue(menuDataAtom)
+ const { data = [], isError, error, isLoading, refetch } = useAtomValue(userMenuDataAtom)
useEffect(() => {
initI18n()
}, [])
+
+ useEffect(() => {
+ if (appData.token) {
+ refetch().then()
+ }
+ }, [ appData.token ])
+
if (isError) {
console.error(error)
- return
Error
+ return
}
- if (isPending) {
+ if (isLoading) {
return
}
diff --git a/src/components/config-provider/index.tsx b/src/components/config-provider/index.tsx
new file mode 100644
index 0000000..817b545
--- /dev/null
+++ b/src/components/config-provider/index.tsx
@@ -0,0 +1,61 @@
+import { ConfigProvider as AntdConfigProvider } from 'antd'
+import { AntdToken, ThemeAppearance, useAntdToken, useThemeMode } from 'antd-style'
+import type { OverrideToken } from 'antd/es/theme/interface'
+import type { FC, ReactNode } from 'react'
+import { ThemeProvider, createProAntdTheme, getProToken } from '@/theme'
+
+export const useProAntdTheme = (appearance: ThemeAppearance) => {
+ const token = useAntdToken()
+ const themeConfig = createProAntdTheme(appearance)
+
+ const controlToken: Partial = {
+ colorBgContainer: token?.colorFillQuaternary,
+ colorBorder: 'transparent',
+ controlOutline: 'transparent',
+ }
+
+ themeConfig.components = {
+ Input: controlToken,
+ InputNumber: controlToken,
+ Select: controlToken,
+ Tree: {
+ colorBgContainer: 'transparent',
+ },
+ TreeSelect: controlToken,
+ }
+
+ return themeConfig
+}
+
+export interface ConfigProviderProps {
+ componentToken?: OverrideToken;
+ children: ReactNode;
+}
+
+export const ConfigProvider: FC = ({ children, componentToken }) => {
+ const { appearance, themeMode } = useThemeMode()
+ const proTheme = useProAntdTheme(appearance)
+ proTheme.components = { ...proTheme.components, ...componentToken }
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export const withProvider = (Component) => (props) => {
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/icon/action/ActionIcon.tsx b/src/components/icon/action/ActionIcon.tsx
new file mode 100644
index 0000000..d08a4fe
--- /dev/null
+++ b/src/components/icon/action/ActionIcon.tsx
@@ -0,0 +1,111 @@
+import type { ButtonProps, TooltipProps } from 'antd'
+import { Button, Tooltip } from 'antd'
+import type { CSSProperties, FC } from 'react'
+import { ConfigProvider } from '@/components/config-provider'
+import { useStyles } from './style'
+
+/**
+ * @title 动作图标属性
+ * @description 继承自 `Button` 组件所有属性,除了 `title`, `type` 和 `size`
+ */
+export interface ActionIconProps extends Omit {
+ /**
+ * @title 鼠标类型
+ */
+ cursor?: CSSProperties['cursor'];
+ /**
+ * @title 动作提示
+ */
+ title?: TooltipProps['title'];
+ /**
+ * @title 提示位置
+ */
+ placement?: TooltipProps['placement'];
+ /**
+ * @title 图标
+ */
+ icon: ButtonProps['icon'];
+ /**
+ * @title 点击回调
+ */
+ onClick?: ButtonProps['onClick'];
+ /**
+ * @title 图标尺寸
+ */
+ size?: 'default' | 'large' | number;
+ /**
+ * @description 鼠标移入时候的延迟tooltip时间,默认 0.5
+ * @default 0.5
+ */
+ tooltipDelay?: number;
+ /**
+ * @description 是否展示小箭头,默认不展示
+ * @default false
+ */
+ arrow?: boolean;
+}
+
+const BaseActionIcon: FC = ({
+ placement,
+ title,
+ icon,
+ cursor,
+ onClick,
+ className,
+ arrow = false,
+ size = 'default',
+ tooltipDelay = 0.5,
+ ...restProps
+ }) => {
+ const { styles, cx } = useStyles({ size })
+
+ const Icon = (
+
+ )
+
+ return (
+ <>
+ {!title ? (
+ Icon
+ ) : (
+
+ {Icon}
+
+ )}
+ >
+ )
+}
+
+const ActionIcon = (props: ActionIconProps) => {
+ const { size } = props || {}
+ const { theme: token } = useStyles({ size })
+
+ return (
+
+
+
+ )
+}
+export default ActionIcon
\ No newline at end of file
diff --git a/src/components/icon/action/Icons.tsx b/src/components/icon/action/Icons.tsx
new file mode 100644
index 0000000..1bdbe55
--- /dev/null
+++ b/src/components/icon/action/Icons.tsx
@@ -0,0 +1,37 @@
+import { DeleteFilled, EditFilled } from '@ant-design/icons'
+import type { FC } from 'react'
+
+import type { ActionIconProps } from './ActionIcon'
+import ActionIcon from './ActionIcon'
+
+export type IconsProps = Omit;
+
+const HandleIcon = (
+
+)
+
+const CollapseIcon = (
+
+)
+
+export const CollapseAction: FC = (props) => (
+
+)
+
+export const HandleAction: FC = (props) => (
+
+)
+
+export const DeleteAction: FC = (props) => (
+ } {...props} />
+)
+
+export const EditAction: FC = (props) => (
+ } {...props} />
+)
\ No newline at end of file
diff --git a/src/components/icon/action/index.tsx b/src/components/icon/action/index.tsx
new file mode 100644
index 0000000..71b3e30
--- /dev/null
+++ b/src/components/icon/action/index.tsx
@@ -0,0 +1,8 @@
+import ActionIcon from './ActionIcon'
+
+export { default as ActionIcon } from './ActionIcon'
+export type { ActionIconProps } from './ActionIcon'
+export * from './Icons'
+// 内部使用统一图标语义
+export type { IconsProps } from './Icons'
+export default ActionIcon
\ No newline at end of file
diff --git a/src/components/icon/action/style.ts b/src/components/icon/action/style.ts
new file mode 100644
index 0000000..2ea030c
--- /dev/null
+++ b/src/components/icon/action/style.ts
@@ -0,0 +1,37 @@
+import { createStyles } from '@/theme'
+
+export const useStyles = createStyles(({ token, css, cx, prefixCls }, { size, className }) => {
+ const prefix = `${prefixCls}-${token.proPrefix}-icon`
+ const sizeBoundary =
+ typeof size === 'number'
+ ? css`
+ width: ${size}px !important;
+ height: ${size}px !important;
+ `
+ : ''
+
+ const button = css`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: ${token.colorText} !important;
+ }
+
+ &:active {
+ scale: 0.8;
+ color: ${token.colorText};
+ }
+
+ transition: color 600ms ${token.motionEaseOut}, scale 400ms ${token.motionEaseOut},
+ background-color 100ms ${token.motionEaseOut};
+ `
+
+ return {
+ container: cx(prefix, button, sizeBoundary, className),
+ tooltip: css`
+ pointer-events: none;
+ `,
+ }
+})
\ No newline at end of file
diff --git a/src/components/icon/picker/Display.tsx b/src/components/icon/picker/Display.tsx
new file mode 100644
index 0000000..64bfe76
--- /dev/null
+++ b/src/components/icon/picker/Display.tsx
@@ -0,0 +1,26 @@
+import { useToken } from '@/theme'
+import React from 'react'
+import Icon from '../index.tsx'
+
+const Display = () => {
+ const token = useToken()
+ const DefaultIcon = (
+
+ )
+
+ return (
+ }
+ />
+ )
+}
+
+export default Display
\ No newline at end of file
diff --git a/src/components/icon/picker/IconRender.tsx b/src/components/icon/picker/IconRender.tsx
new file mode 100644
index 0000000..755cc3a
--- /dev/null
+++ b/src/components/icon/picker/IconRender.tsx
@@ -0,0 +1,42 @@
+import type { FC } from 'react'
+import { memo } from 'react'
+import AntdIcons from '../contents/antdIcons'
+import { customIconList, registerCustomIcon } from '../contents/customIcons'
+
+export interface IconRenderProps {
+ type: 'antd' | 'iconfont' | 'custom';
+ componentName?: string;
+ props?:
+ | {
+ type?: string;
+ }
+ | any;
+ scriptUrl?: string;
+}
+
+export interface IIconRender {
+ (props: IconRenderProps): JSX.Element;
+
+}
+
+const Render: FC = memo(
+ ({ type, componentName, props, scriptUrl }) => {
+ switch (type) {
+ case 'antd':
+ const Icon = AntdIcons[componentName]
+ return
+
+ case 'iconfont':
+ const Iconfont = AntdIcons.createFromIconfontCN({
+ scriptUrl,
+ })
+ return
+ }
+ },
+)
+
+const IconIRender = Render as IIconRender
+
+IconIRender.registerCustomIcon = registerCustomIcon
+
+export default IconIRender
\ No newline at end of file
diff --git a/src/components/icon/picker/index.tsx b/src/components/icon/picker/index.tsx
new file mode 100644
index 0000000..cff2cf8
--- /dev/null
+++ b/src/components/icon/picker/index.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+
+
+const Index = () => {
+ return (
+
+
+
+ )
+}
+
+export default Index
\ No newline at end of file
diff --git a/src/components/loading/FetchLoading.tsx b/src/components/loading/FetchLoading.tsx
new file mode 100644
index 0000000..20392d0
--- /dev/null
+++ b/src/components/loading/FetchLoading.tsx
@@ -0,0 +1,15 @@
+import { useIsFetching, } from '@tanstack/react-query'
+import Loading from './index.tsx'
+
+
+const FetchLoading = () => {
+
+ const isFetching = useIsFetching()
+ return (
+ <>
+ 0}/>
+ >
+ )
+}
+
+export default FetchLoading
\ No newline at end of file
diff --git a/src/components/loading/index.tsx b/src/components/loading/index.tsx
new file mode 100644
index 0000000..dae467b
--- /dev/null
+++ b/src/components/loading/index.tsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import { useStyles } from './style.ts'
+
+interface ILoading {
+ loading: boolean,
+ className?: string
+}
+
+export const Loading = ({ loading, className }: ILoading) => {
+
+ const { styles, cx } = useStyles({ className })
+
+ return (
+
+ )
+}
+
+export default Loading
\ No newline at end of file
diff --git a/src/components/loading/style.ts b/src/components/loading/style.ts
new file mode 100644
index 0000000..aa6f69a
--- /dev/null
+++ b/src/components/loading/style.ts
@@ -0,0 +1,81 @@
+import { createStyles } from '@/theme'
+
+// Define styles using createStyles
+export const useStyles = createStyles(({ token, css, cx, prefixCls }, { className }) => {
+ const prefix = `${prefixCls}-${token.proPrefix}-loading`
+
+
+ return {
+ container: cx(prefix, className),
+ base: css`
+
+ --tw-translate-x: 0;
+ --tw-translate-y: -100%;
+
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 300px;
+ transition: all 300ms;
+ z-index: 1130;
+ background: radial-gradient(closest-side, rgba(0, 10, 40, 0.2) 0%, rgba(0, 0, 0, 0) 100%);
+ transform: translate(0, var(--tw-translate-y)) rotate(0) skew(0) skewY(0) scaleX(0) scaleY(0);
+
+ &.dark {
+ height: 200px;
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 100%;
+ }
+ `,
+ hidden: css`
+ opacity: 0;
+ transform: translateY(-100%);
+ `,
+ visible: css`
+ opacity: 1;
+ transform: translateY(-35%);
+ `,
+ centeredElement: css`
+ --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, .05), 0 4px 6px -2px rgba(0, 0, 0, .03);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -2px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+ padding: 0.5rem;
+ background-color: rgba(255, 255, 255, 0.8);
+ border-radius: 0.5rem;
+ --tw-translate-y: 30px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+ --tw-translate-x: -50%;
+ top: 50%;
+ left: 50%;
+ position: absolute;
+ `,
+ svgIcon: css`
+ display: block;
+ vertical-align: middle;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 0;
+ height: 30px;
+ width: 30px;
+ color: rgb(17 24 39 / var(--tw-text-opacity));
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+ animation-timeline: auto;
+ animation-range-start: normal;
+ animation-range-end: normal;
+ animation: 1s linear 0s infinite normal none running spin;
+
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+ `
+ }
+})
+
+
diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx
index 6d88870..82a8d92 100644
--- a/src/layout/RootLayout.tsx
+++ b/src/layout/RootLayout.tsx
@@ -2,13 +2,15 @@ import PageBreadcrumb from '@/components/breadcrumb'
import ErrorPage from '@/components/error/error.tsx'
import SelectLang from '@/components/select-lang'
import { useTranslation } from '@/i18n.ts'
+import { userMenuDataAtom } from '@/store/user.ts'
import { MenuItem } from '@/types'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
-import { CatchBoundary, Link, Outlet, useRouteContext } from '@tanstack/react-router'
+import { CatchBoundary, Link, Outlet } from '@tanstack/react-router'
import { ConfigProvider, Dropdown } from 'antd'
import { useState } from 'react'
import Icon from '../components/icon'
import defaultProps from './_defaultProps'
+import { useAtomValue } from 'jotai'
//根据menuData生成Breadcrumb所需的数据
@@ -36,11 +38,7 @@ const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
export default () => {
const { t } = useTranslation()
- const { menuData } = useRouteContext({
- from: undefined,
- strict: false,
- select: (state) => state
- })
+ const { data: menuData = [], isLoading } = useAtomValue(userMenuDataAtom)
const items = getBreadcrumbData(menuData, location.pathname)
@@ -85,6 +83,7 @@ export default () => {
}}
menu={{
collapsedShowGroupTitle: true,
+ loading: isLoading,
}}
avatarProps={{
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx
index accd27c..9cdb3b2 100644
--- a/src/pages/system/menus/index.tsx
+++ b/src/pages/system/menus/index.tsx
@@ -1,65 +1,98 @@
+import { useTranslation } from '@/i18n.ts'
+import { FlattenData } from '@/types'
+import { IMenu } from '@/types/menus'
+import { flattenTree } from '@/utils'
import { PageContainer, ProCard } from '@ant-design/pro-components'
-import { Button, Space, Tree } from 'antd'
+import { Button, Form, Input, Space, Tree } from 'antd'
import { useAtom, useAtomValue } from 'jotai'
-import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from '@/store/system.ts'
-import { formatterMenuData } from '@/utils/uuid.ts'
+import { useEffect, useRef } from 'react'
+import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from './store.ts'
import { CloseOutlined, PlusOutlined } from '@ant-design/icons'
-import { createLazyFileRoute } from '@tanstack/react-router'
+import { createLazyFileRoute } from '@tanstack/react-router'
const Menus = () => {
- const { data, isLoading } = useAtomValue(menuDataAtom)
- const [ currentMenu, setCurrentMenu ] = useAtom(selectedMenuAtom)
- const [ selectedKey, setSelectedKey ] = useAtom(selectedMenuIdAtom)
+ const { t } = useTranslation()
+ const { data = [], isLoading } = useAtomValue(menuDataAtom)
+ const [ currentMenu, setCurrentMenu ] = useAtom(selectedMenuAtom)
+ const [ selectedKey, setSelectedKey ] = useAtom(selectedMenuIdAtom)
+ const flattenMenusRef = useRef[]>([])
- const treeData = formatterMenuData(data!)
+ useEffect(() => {
- return (
-
-
-
-
-
+ return (
+
+
+
+ } shape={'circle'}/>
+ }
+ shape={'circle'}/>
- {selectedKey}
- {
- JSON.stringify(currentMenu)
- }
+
+ }
+ loading={isLoading}>
+ {
+ console.log(item)
+ setSelectedKey(item[0])
+ setCurrentMenu(flattenMenusRef.current?.find((menu) => menu.id === item[0]))
+ }}
+ checkable={true}
+ showIcon={false}
+ />
+
+
-
-
+
+
+
+
+
+
+
-
-
-
- )
+
+
+
+
+
+
+ )
}
-export const Route = createLazyFileRoute("/system/menus")({
- component: Menus
+export const Route = createLazyFileRoute('/system/menus')({
+ component: Menus
})
diff --git a/src/pages/system/menus/store.ts b/src/pages/system/menus/store.ts
new file mode 100644
index 0000000..a76013c
--- /dev/null
+++ b/src/pages/system/menus/store.ts
@@ -0,0 +1,32 @@
+import systemServ from '@/service/system.ts'
+import { IPage, IPageResult, MenuItem } from '@/types'
+import { IMenu } from '@/types/menus'
+import { atomWithQuery } from 'jotai-tanstack-query'
+import { atom } from 'jotai/index'
+
+export const menuPageAtom = atom({})
+
+export const menuDataAtom = atomWithQuery>((get) => {
+
+ return {
+ queryKey: [ 'menus', get(menuPageAtom) ],
+ queryFn: async ({ queryKey: [ , page ] }) => {
+ return await systemServ.menus.list(page)
+ },
+ select: (data) => {
+ return data.rows ?? []
+ }
+ }
+})
+
+
+export const selectedMenuIdAtom = atom(0)
+export const selectedMenuAtom = atom