dark
7 months ago
36 changed files with 2113 additions and 383 deletions
-
7package.json
-
11src/App.tsx
-
15src/components/breadcrumb/index.tsx
-
96src/components/config-provider/index.tsx
-
182src/components/draggable-panel/DraggablePanel.tsx
-
258src/components/draggable-panel/FixMode.tsx
-
182src/components/draggable-panel/FloatMode.tsx
-
3src/components/draggable-panel/index.ts
-
183src/components/draggable-panel/style.ts
-
179src/components/icon/action/ActionIcon.tsx
-
6src/components/icon/action/style.ts
-
11src/components/icon/index.tsx
-
1src/components/icon/picker/Display.tsx
-
9src/components/icon/types.ts
-
2src/i18n.ts
-
8src/index.css
-
9src/layout/RootLayout.tsx
-
21src/layout/style.ts
-
2src/pages/login/index.css
-
30src/pages/login/index.tsx
-
130src/pages/login/style.ts
-
32src/pages/system/menus/components/BatchButton.tsx
-
97src/pages/system/menus/components/ButtonTable.tsx
-
103src/pages/system/menus/components/MenuTree.tsx
-
66src/pages/system/menus/components/TreeNodeRender.tsx
-
229src/pages/system/menus/index.tsx
-
66src/pages/system/menus/store.ts
-
72src/pages/system/menus/style.ts
-
36src/request.ts
-
6src/service/base.ts
-
9src/store/system.ts
-
56src/store/user.ts
-
2src/theme/themes/antdTheme.ts
-
1src/theme/themes/token.ts
-
123src/utils/index.ts
-
253yarn.lock
@ -1,61 +1,63 @@ |
|||||
import { ConfigProvider as AntdConfigProvider } from 'antd' |
|
||||
|
import { ConfigProvider as AntdConfigProvider, ConfigProviderProps as AntdConfigProviderProps } from 'antd' |
||||
import { AntdToken, ThemeAppearance, useAntdToken, useThemeMode } from 'antd-style' |
import { AntdToken, ThemeAppearance, useAntdToken, useThemeMode } from 'antd-style' |
||||
import type { OverrideToken } from 'antd/es/theme/interface' |
import type { OverrideToken } from 'antd/es/theme/interface' |
||||
import type { FC, ReactNode } from 'react' |
import type { FC, ReactNode } from 'react' |
||||
import { ThemeProvider, createProAntdTheme, getProToken } from '@/theme' |
import { ThemeProvider, createProAntdTheme, getProToken } from '@/theme' |
||||
|
|
||||
export const useProAntdTheme = (appearance: ThemeAppearance) => { |
export const useProAntdTheme = (appearance: ThemeAppearance) => { |
||||
const token = useAntdToken() |
|
||||
const themeConfig = createProAntdTheme(appearance) |
|
||||
|
|
||||
const controlToken: Partial<AntdToken> = { |
|
||||
colorBgContainer: token?.colorFillQuaternary, |
|
||||
colorBorder: 'transparent', |
|
||||
controlOutline: 'transparent', |
|
||||
} |
|
||||
|
|
||||
themeConfig.components = { |
|
||||
Input: controlToken, |
|
||||
InputNumber: controlToken, |
|
||||
Select: controlToken, |
|
||||
Tree: { |
|
||||
colorBgContainer: 'transparent', |
|
||||
}, |
|
||||
TreeSelect: controlToken, |
|
||||
} |
|
||||
|
|
||||
return themeConfig |
|
||||
|
const token = useAntdToken() |
||||
|
const themeConfig = createProAntdTheme(appearance) |
||||
|
|
||||
|
const controlToken: Partial<AntdToken> = { |
||||
|
// colorBgContainer: token?.colorFillQuaternary,
|
||||
|
// colorBorder: 'transparent',
|
||||
|
// controlOutline: 'transparent',
|
||||
|
} |
||||
|
|
||||
|
themeConfig.components = { |
||||
|
Input: controlToken, |
||||
|
InputNumber: controlToken, |
||||
|
Select: controlToken, |
||||
|
Tree: { |
||||
|
colorBgContainer: 'transparent', |
||||
|
directoryNodeSelectedBg: '#e1f0ff', |
||||
|
directoryNodeSelectedColor: 'rgb(22, 62, 124)', |
||||
|
}, |
||||
|
TreeSelect: controlToken, |
||||
|
} |
||||
|
|
||||
|
return themeConfig |
||||
} |
} |
||||
|
|
||||
export interface ConfigProviderProps { |
|
||||
componentToken?: OverrideToken; |
|
||||
children: ReactNode; |
|
||||
|
export interface ConfigProviderProps extends AntdConfigProviderProps { |
||||
|
componentToken?: OverrideToken; |
||||
|
children: ReactNode; |
||||
} |
} |
||||
|
|
||||
export const ConfigProvider: FC<ConfigProviderProps> = ({ children, componentToken }) => { |
|
||||
const { appearance, themeMode } = useThemeMode() |
|
||||
const proTheme = useProAntdTheme(appearance) |
|
||||
proTheme.components = { ...proTheme.components, ...componentToken } |
|
||||
|
|
||||
return ( |
|
||||
<AntdConfigProvider theme={proTheme}> |
|
||||
<ThemeProvider |
|
||||
appearance={appearance} |
|
||||
themeMode={themeMode} |
|
||||
// 以下都是自定义主题
|
|
||||
theme={createProAntdTheme} |
|
||||
customToken={getProToken} |
|
||||
> |
|
||||
{children} |
|
||||
</ThemeProvider> |
|
||||
</AntdConfigProvider> |
|
||||
) |
|
||||
|
export const ConfigProvider: FC<ConfigProviderProps> = ({ children, componentToken, ...props }) => { |
||||
|
const { appearance, themeMode } = useThemeMode() |
||||
|
const proTheme = useProAntdTheme(appearance) |
||||
|
proTheme.components = { ...proTheme.components, ...componentToken } |
||||
|
|
||||
|
return ( |
||||
|
<AntdConfigProvider theme={proTheme} {...props}> |
||||
|
<ThemeProvider |
||||
|
appearance={appearance} |
||||
|
themeMode={themeMode} |
||||
|
// 以下都是自定义主题
|
||||
|
theme={createProAntdTheme} |
||||
|
customToken={getProToken} |
||||
|
> |
||||
|
{children} |
||||
|
</ThemeProvider> |
||||
|
</AntdConfigProvider> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export const withProvider = (Component) => (props) => { |
export const withProvider = (Component) => (props) => { |
||||
return ( |
|
||||
<ConfigProvider> |
|
||||
<Component {...props} /> |
|
||||
</ConfigProvider> |
|
||||
) |
|
||||
|
return ( |
||||
|
<ConfigProvider> |
||||
|
<Component {...props} /> |
||||
|
</ConfigProvider> |
||||
|
) |
||||
} |
} |
@ -0,0 +1,182 @@ |
|||||
|
import type { NumberSize, Size } from 're-resizable' |
||||
|
import type { CSSProperties, FC, ReactNode } from 'react' |
||||
|
import { memo } from 'react' |
||||
|
import type { Props as RndProps } from 'react-rnd' |
||||
|
import { withProvider } from '@/components/config-provider' |
||||
|
import { FixMode } from './FixMode' |
||||
|
import { FloatMode } from './FloatMode' |
||||
|
|
||||
|
export interface DraggablePanelProps { |
||||
|
/** |
||||
|
* 位置, |
||||
|
* 使用固定模式或者浮动窗口 |
||||
|
*/ |
||||
|
mode?: 'fixed' | 'float'; |
||||
|
|
||||
|
/** |
||||
|
* 固定模式下面板的朝向,默认放置在右侧 |
||||
|
* @default right |
||||
|
*/ |
||||
|
placement?: 'right' | 'left' | 'top' | 'bottom'; |
||||
|
|
||||
|
/** |
||||
|
* 最小宽度 |
||||
|
*/ |
||||
|
minWidth?: number; |
||||
|
/** |
||||
|
* 最小高度 |
||||
|
*/ |
||||
|
minHeight?: number; |
||||
|
/** |
||||
|
* 最大宽度 |
||||
|
*/ |
||||
|
maxWidth?: number; |
||||
|
/** |
||||
|
* 最大高度 |
||||
|
*/ |
||||
|
maxHeight?: number; |
||||
|
/** |
||||
|
* 控制可缩放区域 |
||||
|
*/ |
||||
|
resize?: RndProps['enableResizing']; |
||||
|
/** |
||||
|
* 面板尺寸 |
||||
|
* |
||||
|
*/ |
||||
|
size?: Partial<Size>; |
||||
|
onSizeChange?: (delta: NumberSize, size?: Size) => void; |
||||
|
/** |
||||
|
* 当用户在拖拽过程中触发 |
||||
|
* @param delta |
||||
|
* @param size |
||||
|
*/ |
||||
|
onSizeDragging?: (delta: NumberSize, size?: Size) => void; |
||||
|
/** |
||||
|
* 是否可展开 |
||||
|
* @default true |
||||
|
*/ |
||||
|
expandable?: boolean; |
||||
|
/** |
||||
|
* 当前是否是展开态 |
||||
|
*/ |
||||
|
isExpand?: boolean; |
||||
|
/** |
||||
|
* 展开是否可以变更 |
||||
|
* @param expand |
||||
|
*/ |
||||
|
onExpandChange?: (expand: boolean) => void; |
||||
|
/** |
||||
|
* 面板位置 |
||||
|
* 受控模式 |
||||
|
*/ |
||||
|
position?: RndProps['position']; |
||||
|
/** |
||||
|
* 面板默认尺寸 |
||||
|
* 固定模式下: width 320px height 100% |
||||
|
* 浮动模式下:width 320px height 400px |
||||
|
*/ |
||||
|
defaultSize?: Partial<Size>; |
||||
|
/** |
||||
|
* 面板默认位置悬浮模式下有效 |
||||
|
* @default [100,100] |
||||
|
*/ |
||||
|
defaultPosition?: RndProps['position']; |
||||
|
/** |
||||
|
* 位置变更回调 |
||||
|
*/ |
||||
|
onPositionChange?: (position: RndProps['position']) => void; |
||||
|
/** |
||||
|
* 样式 |
||||
|
*/ |
||||
|
style?: CSSProperties; |
||||
|
/** |
||||
|
* 类名 |
||||
|
*/ |
||||
|
className?: string; |
||||
|
/** |
||||
|
* 内容 |
||||
|
*/ |
||||
|
children: ReactNode; |
||||
|
} |
||||
|
|
||||
|
const Draggable: FC<DraggablePanelProps> = memo( |
||||
|
({ |
||||
|
children, |
||||
|
className, |
||||
|
mode, |
||||
|
placement, |
||||
|
resize, |
||||
|
style, |
||||
|
position, |
||||
|
onPositionChange, |
||||
|
size, |
||||
|
defaultSize, |
||||
|
defaultPosition, |
||||
|
minWidth, |
||||
|
minHeight, |
||||
|
maxHeight, |
||||
|
maxWidth, |
||||
|
onSizeChange, |
||||
|
onSizeDragging, |
||||
|
expandable = true, |
||||
|
isExpand, |
||||
|
onExpandChange, |
||||
|
}) => { |
||||
|
switch (mode) { |
||||
|
case 'fixed': |
||||
|
default: |
||||
|
return ( |
||||
|
<FixMode |
||||
|
// 尺寸
|
||||
|
size={size} |
||||
|
defaultSize={defaultSize} |
||||
|
onSizeDragging={onSizeDragging} |
||||
|
onSizeChange={onSizeChange} |
||||
|
minHeight={minHeight} |
||||
|
minWidth={minWidth} |
||||
|
maxHeight={maxHeight} |
||||
|
maxWidth={maxWidth} |
||||
|
// 缩放
|
||||
|
resize={resize} |
||||
|
onExpandChange={onExpandChange} |
||||
|
expandable={expandable} |
||||
|
isExpand={isExpand} |
||||
|
className={className} |
||||
|
placement={placement} |
||||
|
style={style} |
||||
|
> |
||||
|
{children} |
||||
|
</FixMode> |
||||
|
) |
||||
|
case 'float': |
||||
|
return ( |
||||
|
<FloatMode |
||||
|
// 坐标
|
||||
|
defaultPosition={defaultPosition} |
||||
|
position={position} |
||||
|
onPositionChange={onPositionChange} |
||||
|
// 尺寸
|
||||
|
minHeight={minHeight} |
||||
|
minWidth={minWidth} |
||||
|
maxHeight={maxHeight} |
||||
|
maxWidth={maxWidth} |
||||
|
defaultSize={defaultSize} |
||||
|
size={size} |
||||
|
onSizeDragging={onSizeDragging} |
||||
|
onSizeChange={onSizeChange} |
||||
|
// 缩放
|
||||
|
resize={resize} |
||||
|
canResizing={resize !== false} |
||||
|
className={className} |
||||
|
style={style} |
||||
|
> |
||||
|
{children} |
||||
|
</FloatMode> |
||||
|
) |
||||
|
} |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
const WithProviderDraggable: FC<DraggablePanelProps> = withProvider(Draggable) |
||||
|
|
||||
|
export { WithProviderDraggable as Draggable } |
@ -0,0 +1,258 @@ |
|||||
|
import type { Enable, NumberSize, Size } from 're-resizable' |
||||
|
import { HandleClassName, Resizable } from 're-resizable' |
||||
|
import type { CSSProperties, FC, ReactNode } from 'react' |
||||
|
import { memo, useMemo } from 'react' |
||||
|
import { Center } from 'react-layout-kit' |
||||
|
import type { Props as RndProps } from 'react-rnd' |
||||
|
import useControlledState from 'use-merge-value' |
||||
|
|
||||
|
import { DownOutlined, LeftOutlined, RightOutlined, UpOutlined } from '@ant-design/icons' |
||||
|
import { useStyle } from './style' |
||||
|
|
||||
|
export interface FixModePanelProps { |
||||
|
/** |
||||
|
* 位置, |
||||
|
* 使用固定模式或者浮动窗口 |
||||
|
*/ |
||||
|
mode?: 'fixed' | 'float'; |
||||
|
|
||||
|
/** |
||||
|
* 固定模式下面板的朝向,默认放置在右侧 |
||||
|
* @default right |
||||
|
*/ |
||||
|
placement: 'right' | 'left' | 'top' | 'bottom' | undefined; |
||||
|
|
||||
|
/** |
||||
|
* 最小宽度 |
||||
|
*/ |
||||
|
minWidth?: number; |
||||
|
/** |
||||
|
* 最小高度 |
||||
|
*/ |
||||
|
minHeight?: number; |
||||
|
|
||||
|
/** |
||||
|
* 最大宽度 |
||||
|
*/ |
||||
|
maxWidth?: number; |
||||
|
/** |
||||
|
* 最大高度 |
||||
|
*/ |
||||
|
maxHeight?: number; |
||||
|
/** |
||||
|
* 控制可缩放区域 |
||||
|
*/ |
||||
|
resize?: RndProps['enableResizing']; |
||||
|
/** |
||||
|
* 面板尺寸 |
||||
|
* |
||||
|
*/ |
||||
|
size?: Partial<Size>; |
||||
|
onSizeChange?: (delta: NumberSize, size?: Size) => void; |
||||
|
/** |
||||
|
* 当用户在拖拽过程中触发 |
||||
|
* @param delta |
||||
|
* @param size |
||||
|
*/ |
||||
|
onSizeDragging?: (delta: NumberSize, size?: Size) => void; |
||||
|
/** |
||||
|
* 是否可展开 |
||||
|
* @default true |
||||
|
*/ |
||||
|
expandable?: boolean; |
||||
|
/** |
||||
|
* 当前是否是展开态 |
||||
|
*/ |
||||
|
isExpand?: boolean; |
||||
|
/** |
||||
|
* 展开是否可以变更 |
||||
|
* @param expand |
||||
|
*/ |
||||
|
onExpandChange?: (expand: boolean) => void; |
||||
|
/** |
||||
|
* 面板位置 |
||||
|
* 受控模式 |
||||
|
*/ |
||||
|
position?: RndProps['position']; |
||||
|
/** |
||||
|
* 面板默认尺寸 |
||||
|
* 固定模式下: width 320px height 100% |
||||
|
* 浮动模式下:width 320px height 400px |
||||
|
*/ |
||||
|
defaultSize?: Partial<Size>; |
||||
|
/** |
||||
|
* 面板默认位置悬浮模式下有效 |
||||
|
* @default [100,100] |
||||
|
*/ |
||||
|
defaultPosition?: RndProps['position']; |
||||
|
/** |
||||
|
* 位置变更回调 |
||||
|
*/ |
||||
|
onPositionChange?: (position: RndProps['position']) => void; |
||||
|
/** |
||||
|
* 样式 |
||||
|
*/ |
||||
|
style?: CSSProperties; |
||||
|
className?: string; |
||||
|
/** |
||||
|
* 内容 |
||||
|
*/ |
||||
|
children: ReactNode; |
||||
|
} |
||||
|
|
||||
|
const DEFAULT_HEIGHT = 150 |
||||
|
const DEFAULT_WIDTH = 400 |
||||
|
|
||||
|
const reversePlacement = (placement: 'right' | 'left' | 'top' | 'bottom') => { |
||||
|
switch (placement) { |
||||
|
case 'bottom': |
||||
|
return 'top' |
||||
|
case 'top': |
||||
|
return 'bottom' |
||||
|
case 'right': |
||||
|
return 'left' |
||||
|
case 'left': |
||||
|
return 'right' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const FixMode: FC<FixModePanelProps> = memo<FixModePanelProps>( |
||||
|
({ |
||||
|
children, |
||||
|
placement = 'right', |
||||
|
resize, |
||||
|
style, |
||||
|
size, |
||||
|
defaultSize: customizeDefaultSize, |
||||
|
minWidth, |
||||
|
minHeight, |
||||
|
maxHeight, |
||||
|
maxWidth, |
||||
|
onSizeChange, |
||||
|
onSizeDragging, |
||||
|
expandable = true, |
||||
|
isExpand: expand, |
||||
|
onExpandChange, |
||||
|
className, |
||||
|
}) => { |
||||
|
const isVertical = placement === 'top' || placement === 'bottom' |
||||
|
|
||||
|
const { styles, cx } = useStyle() |
||||
|
|
||||
|
const [ isExpand, setIsExpand ] = useControlledState(true, { |
||||
|
value: expand, |
||||
|
onChange: onExpandChange, |
||||
|
}) |
||||
|
|
||||
|
// 只有配置了 resize 和 isExpand 属性后才可拖拽
|
||||
|
const canResizing = resize !== false && isExpand |
||||
|
|
||||
|
const resizeHandleClassNames: HandleClassName = useMemo(() => { |
||||
|
if (!canResizing) return {} |
||||
|
|
||||
|
return { |
||||
|
[reversePlacement(placement)]: styles[`${reversePlacement(placement)}Handle`], |
||||
|
} |
||||
|
}, [ canResizing, placement ]) |
||||
|
|
||||
|
const resizing = { |
||||
|
top: false, |
||||
|
bottom: false, |
||||
|
right: false, |
||||
|
left: false, |
||||
|
topRight: false, |
||||
|
bottomRight: false, |
||||
|
bottomLeft: false, |
||||
|
topLeft: false, |
||||
|
[reversePlacement(placement)]: true, |
||||
|
...(resize as Enable), |
||||
|
} |
||||
|
|
||||
|
const defaultSize: Size = useMemo(() => { |
||||
|
if (isVertical) |
||||
|
return { |
||||
|
width: '100%', |
||||
|
height: DEFAULT_HEIGHT, |
||||
|
...customizeDefaultSize, |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
width: DEFAULT_WIDTH, |
||||
|
height: '100%', |
||||
|
...customizeDefaultSize, |
||||
|
} |
||||
|
}, [ isVertical ]) |
||||
|
|
||||
|
const sizeProps = isExpand |
||||
|
? { |
||||
|
minWidth: typeof minWidth === 'number' ? Math.max(minWidth, 0) : 280, |
||||
|
minHeight: typeof minHeight === 'number' ? Math.max(minHeight, 0) : undefined, |
||||
|
maxHeight: typeof maxHeight === 'number' ? Math.max(maxHeight, 0) : undefined, |
||||
|
maxWidth: typeof maxWidth === 'number' ? Math.max(maxWidth, 0) : undefined, |
||||
|
defaultSize, |
||||
|
size: size as Size, |
||||
|
style, |
||||
|
} |
||||
|
: { |
||||
|
minWidth: 0, |
||||
|
minHeight: 0, |
||||
|
size: { width: 0, height: 0 }, |
||||
|
} |
||||
|
|
||||
|
const { Arrow, className: arrowPlacement } = useMemo(() => { |
||||
|
switch (placement) { |
||||
|
case 'top': |
||||
|
return { className: 'Bottom', Arrow: DownOutlined } |
||||
|
case 'bottom': |
||||
|
return { className: 'Top', Arrow: UpOutlined } |
||||
|
case 'right': |
||||
|
return { className: 'Left', Arrow: LeftOutlined } |
||||
|
case 'left': |
||||
|
return { className: 'Right', Arrow: RightOutlined } |
||||
|
} |
||||
|
}, [ styles, placement ]) |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className={cx(styles.container, className)} |
||||
|
style={{ [`border${arrowPlacement}Width`]: 1 }} |
||||
|
> |
||||
|
{expandable && ( |
||||
|
<Center |
||||
|
className={cx(styles[`toggle${arrowPlacement}`])} |
||||
|
onClick={() => { |
||||
|
setIsExpand(!isExpand) |
||||
|
}} |
||||
|
style={{ opacity: isExpand ? undefined : 1 }} |
||||
|
> |
||||
|
<Arrow rotate={isExpand ? 180 : 0}/> |
||||
|
</Center> |
||||
|
)} |
||||
|
{ |
||||
|
<Resizable |
||||
|
{...sizeProps} |
||||
|
className={styles.fixed} |
||||
|
enable={canResizing ? (resizing as Enable) : undefined} |
||||
|
handleClasses={resizeHandleClassNames} |
||||
|
onResizeStop={(_e, _direction, ref, delta) => { |
||||
|
onSizeChange?.(delta, { |
||||
|
width: ref.style.width, |
||||
|
height: ref.style.height, |
||||
|
}) |
||||
|
}} |
||||
|
onResize={(_, _direction, ref, delta) => { |
||||
|
onSizeDragging?.(delta, { |
||||
|
width: ref.style.width, |
||||
|
height: ref.style.height, |
||||
|
}) |
||||
|
}} |
||||
|
> |
||||
|
{children} |
||||
|
</Resizable> |
||||
|
} |
||||
|
</div> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
export { FixMode } |
@ -0,0 +1,182 @@ |
|||||
|
import type { Enable, NumberSize, Size } from 're-resizable' |
||||
|
import { HandleClassName } from 're-resizable' |
||||
|
import type { CSSProperties, FC, ReactNode } from 'react' |
||||
|
import { memo, useMemo } from 'react' |
||||
|
import type { Position, Props as RndProps } from 'react-rnd' |
||||
|
import { Rnd } from 'react-rnd' |
||||
|
|
||||
|
import { useStyle } from './style' |
||||
|
|
||||
|
export interface FloatProps { |
||||
|
/** |
||||
|
* 位置, |
||||
|
* 使用固定模式或者浮动窗口 |
||||
|
*/ |
||||
|
mode?: 'fixed' | 'float'; |
||||
|
|
||||
|
/** |
||||
|
* 面板的朝向,默认是 左右模式 |
||||
|
* @default horizontal |
||||
|
*/ |
||||
|
direction?: 'vertical' | 'horizontal'; |
||||
|
|
||||
|
/** |
||||
|
* 最小宽度 |
||||
|
*/ |
||||
|
minWidth?: number; |
||||
|
/** |
||||
|
* 最小高度 |
||||
|
*/ |
||||
|
minHeight?: number; |
||||
|
/** |
||||
|
* 最大宽度 |
||||
|
*/ |
||||
|
maxWidth?: number; |
||||
|
/** |
||||
|
* 最大高度 |
||||
|
*/ |
||||
|
maxHeight?: number; |
||||
|
/** |
||||
|
* 控制可缩放区域 |
||||
|
*/ |
||||
|
resize?: RndProps['enableResizing']; |
||||
|
/** |
||||
|
* 面板尺寸 |
||||
|
* |
||||
|
*/ |
||||
|
size?: Partial<Size>; |
||||
|
onSizeChange?: (delta: NumberSize, size?: Size) => void; |
||||
|
/** |
||||
|
* 当用户在拖拽过程中触发 |
||||
|
* @param delta |
||||
|
* @param size |
||||
|
*/ |
||||
|
onSizeDragging?: (delta: NumberSize, size?: Size) => void; |
||||
|
|
||||
|
canResizing?: boolean; |
||||
|
/** |
||||
|
* 面板位置 |
||||
|
* 受控模式 |
||||
|
*/ |
||||
|
position?: RndProps['position']; |
||||
|
/** |
||||
|
* 面板默认尺寸 |
||||
|
* 固定模式下: width 320px height 100% |
||||
|
* 浮动模式下:width 320px height 400px |
||||
|
*/ |
||||
|
defaultSize?: Partial<Size>; |
||||
|
/** |
||||
|
* 面板默认位置悬浮模式下有效 |
||||
|
* @default [100,100] |
||||
|
*/ |
||||
|
defaultPosition?: RndProps['position']; |
||||
|
/** |
||||
|
* 位置变更回调 |
||||
|
*/ |
||||
|
onPositionChange?: (position: RndProps['position']) => void; |
||||
|
/** |
||||
|
* 样式 |
||||
|
*/ |
||||
|
style?: CSSProperties; |
||||
|
/** |
||||
|
* 类名 |
||||
|
*/ |
||||
|
className?: string; |
||||
|
/** |
||||
|
* 内容 |
||||
|
*/ |
||||
|
children: ReactNode; |
||||
|
} |
||||
|
|
||||
|
const DEFAULT_HEIGHT = 300 |
||||
|
const DEFAULT_WIDTH = 400 |
||||
|
|
||||
|
export const FloatMode: FC<FloatProps> = memo( |
||||
|
({ |
||||
|
children, |
||||
|
direction, |
||||
|
resize, |
||||
|
style, |
||||
|
position, |
||||
|
onPositionChange, |
||||
|
size, |
||||
|
defaultSize: customizeDefaultSize, |
||||
|
defaultPosition: customizeDefaultPosition, |
||||
|
minWidth = 280, |
||||
|
minHeight = 200, |
||||
|
maxHeight, |
||||
|
maxWidth, |
||||
|
canResizing, |
||||
|
}) => { |
||||
|
const { styles } = useStyle() |
||||
|
|
||||
|
const resizeHandleClassNames: HandleClassName = useMemo(() => { |
||||
|
if (!canResizing) return {} |
||||
|
|
||||
|
return { |
||||
|
right: styles.rightHandle, |
||||
|
left: styles.leftHandle, |
||||
|
top: styles.topHandle, |
||||
|
bottom: styles.bottomHandle, |
||||
|
} |
||||
|
}, [ canResizing, direction ]) |
||||
|
|
||||
|
const resizing = useMemo(() => { |
||||
|
if (canResizing) return resize |
||||
|
|
||||
|
return { |
||||
|
top: true, |
||||
|
bottom: true, |
||||
|
right: true, |
||||
|
left: true, |
||||
|
topRight: true, |
||||
|
bottomRight: true, |
||||
|
bottomLeft: true, |
||||
|
topLeft: true, |
||||
|
...(resize as Enable), |
||||
|
} |
||||
|
}, [ canResizing, resize ]) |
||||
|
|
||||
|
const defaultSize: Size = { |
||||
|
width: DEFAULT_WIDTH, |
||||
|
height: DEFAULT_HEIGHT, |
||||
|
...customizeDefaultSize, |
||||
|
} |
||||
|
|
||||
|
const defaultPosition: Position = { |
||||
|
x: 100, |
||||
|
y: 100, |
||||
|
...customizeDefaultPosition, |
||||
|
} |
||||
|
|
||||
|
const sizeProps = { |
||||
|
minWidth: Math.max(minWidth, 0), |
||||
|
minHeight: Math.max(minHeight, 0), |
||||
|
maxHeight: maxHeight ? Math.max(maxHeight, 0) : undefined, |
||||
|
maxWidth: maxWidth ? Math.max(maxWidth, 0) : undefined, |
||||
|
defaultSize, |
||||
|
size: size as Size, |
||||
|
style, |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Rnd |
||||
|
position={position} |
||||
|
resizeHandleClasses={resizeHandleClassNames} |
||||
|
default={{ |
||||
|
...defaultPosition, |
||||
|
...defaultSize, |
||||
|
}} |
||||
|
onDragStop={(_e, data) => { |
||||
|
onPositionChange?.({ x: data.x, y: data.y }) |
||||
|
}} |
||||
|
bound={'parent'} |
||||
|
enableResizing={resizing} |
||||
|
{...sizeProps} |
||||
|
className={styles.float} |
||||
|
> |
||||
|
{children} |
||||
|
</Rnd> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,3 @@ |
|||||
|
export type { Position } from 'react-rnd'; |
||||
|
export { Draggable as DraggablePanel } from './DraggablePanel'; |
||||
|
export type { DraggablePanelProps } from './DraggablePanel'; |
@ -0,0 +1,183 @@ |
|||||
|
import { createStyles } from '@/theme'; |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-draggable-panel`; |
||||
|
const commonHandle = css`
|
||||
|
position: relative; |
||||
|
|
||||
|
&::before { |
||||
|
position: absolute; |
||||
|
z-index: 50; |
||||
|
transition: all 0.3s ease-in-out; |
||||
|
content: ''; |
||||
|
} |
||||
|
|
||||
|
&:hover, |
||||
|
&:active { |
||||
|
&::before { |
||||
|
background: ${token.colorPrimary}; |
||||
|
} |
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
const commonToggle = css`
|
||||
|
position: absolute; |
||||
|
opacity: 0; |
||||
|
z-index: 1001; |
||||
|
transition: opacity 0.1s; |
||||
|
|
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
background: ${token.colorBgElevated}; |
||||
|
border-width: 1px; |
||||
|
border-style: solid; |
||||
|
color: ${token.colorTextTertiary}; |
||||
|
border-color: ${token.colorBorder}; |
||||
|
|
||||
|
&:hover { |
||||
|
color: ${token.colorTextSecondary}; |
||||
|
background: ${token.colorFillQuaternary}; |
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
const offset = 17; |
||||
|
const toggleLength = 40; |
||||
|
const toggleShort = 16; |
||||
|
|
||||
|
return { |
||||
|
container: cx( |
||||
|
prefix, |
||||
|
css`
|
||||
|
flex-shrink: 0; |
||||
|
position: relative; |
||||
|
border: 0 solid ${token.colorSplit}; |
||||
|
|
||||
|
&:hover { |
||||
|
.${prefix}-toggle { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
`,
|
||||
|
), |
||||
|
toggleLeft: cx( |
||||
|
`${prefix}-toggle`, |
||||
|
`${prefix}-toggle-left`, |
||||
|
commonToggle, |
||||
|
css`
|
||||
|
width: ${toggleShort}px; |
||||
|
height: ${toggleLength}px; |
||||
|
left: -${offset}px; |
||||
|
top: 50%; |
||||
|
margin-top: -20px; |
||||
|
border-radius: 4px 0 0 4px; |
||||
|
border-right-width: 0; |
||||
|
`,
|
||||
|
), |
||||
|
toggleRight: cx( |
||||
|
`${prefix}-toggle`, |
||||
|
`${prefix}-toggle-right`, |
||||
|
commonToggle, |
||||
|
css`
|
||||
|
width: ${toggleShort}px; |
||||
|
height: ${toggleLength}px; |
||||
|
right: -${offset}px; |
||||
|
top: 50%; |
||||
|
margin-top: -20px; |
||||
|
border-radius: 0 4px 4px 0; |
||||
|
border-left-width: 0; |
||||
|
`,
|
||||
|
), |
||||
|
toggleTop: cx( |
||||
|
`${prefix}-toggle`, |
||||
|
`${prefix}-toggle-top`, |
||||
|
commonToggle, |
||||
|
css`
|
||||
|
height: ${toggleShort}px; |
||||
|
width: ${toggleLength}px; |
||||
|
top: -${offset}px; |
||||
|
left: 50%; |
||||
|
margin-left: -20px; |
||||
|
border-radius: 4px 4px 0 0; |
||||
|
border-bottom-width: 0; |
||||
|
`,
|
||||
|
), |
||||
|
toggleBottom: cx( |
||||
|
`${prefix}-toggle`, |
||||
|
`${prefix}-toggle-bottom`, |
||||
|
commonToggle, |
||||
|
css`
|
||||
|
height: 16px; |
||||
|
width: ${toggleLength}px; |
||||
|
bottom: -${offset}px; |
||||
|
left: 50%; |
||||
|
margin-left: -20px; |
||||
|
border-radius: 0 0 4px 4px; |
||||
|
border-top-width: 0; |
||||
|
`,
|
||||
|
), |
||||
|
fixed: cx( |
||||
|
`${prefix}-fixed`, |
||||
|
css`
|
||||
|
background: ${token.colorBgContainer}; |
||||
|
overflow: hidden; |
||||
|
`,
|
||||
|
), |
||||
|
float: cx( |
||||
|
`${prefix}-float`, |
||||
|
css`
|
||||
|
overflow: hidden; |
||||
|
border-radius: 8px; |
||||
|
background: ${token.colorBgElevated}; |
||||
|
box-shadow: ${token.boxShadowSecondary}; |
||||
|
z-index: 2000; |
||||
|
`,
|
||||
|
), |
||||
|
leftHandle: cx( |
||||
|
css`
|
||||
|
${commonHandle}; |
||||
|
|
||||
|
&::before { |
||||
|
left: 50%; |
||||
|
width: 2px; |
||||
|
height: 100%; |
||||
|
} |
||||
|
`,
|
||||
|
`${prefix}-left-handle`, |
||||
|
), |
||||
|
rightHandle: cx( |
||||
|
css`
|
||||
|
${commonHandle}; |
||||
|
&::before { |
||||
|
right: 50%; |
||||
|
width: 2px; |
||||
|
height: 100%; |
||||
|
} |
||||
|
`,
|
||||
|
`${prefix}-right-handle`, |
||||
|
), |
||||
|
topHandle: cx( |
||||
|
`${prefix}-top-handle`, |
||||
|
css`
|
||||
|
${commonHandle}; |
||||
|
|
||||
|
&::before { |
||||
|
top: 50%; |
||||
|
height: 2px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
`,
|
||||
|
), |
||||
|
bottomHandle: cx( |
||||
|
`${prefix}-bottom-handle`, |
||||
|
css`
|
||||
|
${commonHandle}; |
||||
|
|
||||
|
&::before { |
||||
|
bottom: 50%; |
||||
|
height: 2px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
`,
|
||||
|
), |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,8 @@ |
|||||
|
body{ |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; |
||||
|
} |
||||
|
body.login { |
||||
|
overflow: hidden; |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-layout` |
||||
|
|
||||
|
const container = { |
||||
|
[prefix]: css`
|
||||
|
|
||||
|
`,
|
||||
|
} |
||||
|
|
||||
|
const pageContext = css`
|
||||
|
box-shadow: ${token.boxShadowSecondary}; |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(container[prefix], props?.className), |
||||
|
pageContext, |
||||
|
} |
||||
|
|
||||
|
}) |
@ -0,0 +1,130 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
import loginBg from '@/assets/login.png' |
||||
|
|
||||
|
|
||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
// @ts-ignore
|
||||
|
export const useStyles = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
|
||||
|
const prefix = `${prefixCls}-${token.proPrefix}-login-page` |
||||
|
|
||||
|
|
||||
|
const container = css`
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 100vh; |
||||
|
background-image: url(${loginBg}); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: cover; |
||||
|
`
|
||||
|
const language = css`
|
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
right: 10px; |
||||
|
color: #fff; |
||||
|
font-size: 14px; |
||||
|
cursor: pointer; |
||||
|
`
|
||||
|
|
||||
|
|
||||
|
const loginBlock = css`
|
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 40px 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
`
|
||||
|
|
||||
|
const innerBlock = css`
|
||||
|
width: 356px; |
||||
|
margin: 0 auto; |
||||
|
`
|
||||
|
|
||||
|
const logo = css`
|
||||
|
height: 30px; |
||||
|
`
|
||||
|
|
||||
|
const infoLine = css`
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin: 0; |
||||
|
`
|
||||
|
|
||||
|
const infoLeft = css`
|
||||
|
color: #666; |
||||
|
font-size: 14px; |
||||
|
`
|
||||
|
|
||||
|
|
||||
|
const desc = css`
|
||||
|
margin: 24px 0; |
||||
|
color: #999; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
`
|
||||
|
const active = css`
|
||||
|
color: #333; |
||||
|
font-weight: bold; |
||||
|
font-size: 24px; |
||||
|
`
|
||||
|
|
||||
|
const innerBeforeInput = css`
|
||||
|
margin-left: 10px; |
||||
|
color: #999; |
||||
|
`
|
||||
|
|
||||
|
const line = css`
|
||||
|
margin-left: 10px; |
||||
|
`
|
||||
|
|
||||
|
const innerAfterInput = css`
|
||||
|
margin-right: 10px; |
||||
|
color: #999; |
||||
|
`
|
||||
|
|
||||
|
const lineR = css`
|
||||
|
margin-right: 10px; |
||||
|
vertical-align: middle; |
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const sendCode = css`
|
||||
|
max-width: 65px; |
||||
|
margin-right: 10px; |
||||
|
`
|
||||
|
|
||||
|
const otherLogin = css`
|
||||
|
color: #666; |
||||
|
font-size: 14px; |
||||
|
`
|
||||
|
|
||||
|
const icon = css`
|
||||
|
margin-left: 10px; |
||||
|
`
|
||||
|
|
||||
|
const submitBtn = css`
|
||||
|
width: 100%; |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix, container, props?.className ?? ''), |
||||
|
language, |
||||
|
loginBlock, |
||||
|
innerBlock, |
||||
|
logo, |
||||
|
infoLine, |
||||
|
infoLeft, |
||||
|
desc, |
||||
|
active, |
||||
|
innerBeforeInput, |
||||
|
line: cx( innerBeforeInput, line), |
||||
|
innerAfterInput, |
||||
|
lineR: cx(innerAfterInput, lineR), |
||||
|
sendCode, |
||||
|
otherLogin, |
||||
|
icon, |
||||
|
submitBtn, |
||||
|
} |
||||
|
}) |
@ -0,0 +1,32 @@ |
|||||
|
import { Button, Popconfirm } from 'antd' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
import { batchIdsAtom, deleteMenuAtom } from '../store.ts' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
|
||||
|
const BatchButton = () => { |
||||
|
|
||||
|
const { t } = useTranslation() |
||||
|
const { isPending, mutate, } = useAtomValue(deleteMenuAtom) |
||||
|
const ids = useAtomValue(batchIdsAtom) |
||||
|
if (ids.length === 0) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Popconfirm |
||||
|
onConfirm={()=>{ |
||||
|
mutate(ids as number[]) |
||||
|
}} |
||||
|
title={t('system.menus.batchDel.confirm', '确定要删除所选数据吗?')}> |
||||
|
<Button |
||||
|
type="primary" |
||||
|
danger={true} |
||||
|
size={'small'} |
||||
|
disabled={ids.length === 0} |
||||
|
loading={isPending} |
||||
|
>{t('t.system.menus.batchDel', '批量删除')}</Button> |
||||
|
</Popconfirm> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default BatchButton |
@ -0,0 +1,97 @@ |
|||||
|
import { EditableFormInstance, EditableProTable } from '@ant-design/pro-components' |
||||
|
import { useMemo, useRef, useState } from 'react' |
||||
|
import { IDataProps } from '@/types' |
||||
|
import { FormInstance } from 'antd/lib' |
||||
|
|
||||
|
type DataSourceType = { |
||||
|
id: number, |
||||
|
name: string, |
||||
|
code: string, |
||||
|
} |
||||
|
|
||||
|
const fixRowKey = (data: DataSourceType[]) => { |
||||
|
return data.map((item, index) => ({ ...item, id: item.id ?? index+1 })) |
||||
|
} |
||||
|
|
||||
|
const ButtonTable = (props: IDataProps & { |
||||
|
form: FormInstance |
||||
|
}) => { |
||||
|
|
||||
|
const { value, onChange } = props |
||||
|
const editorFormRef = useRef<EditableFormInstance<DataSourceType>>() |
||||
|
const [ editableKeys, setEditableRowKeys ] = useState<any[]>(() => { |
||||
|
return fixRowKey(value || []).map(item => item.id) |
||||
|
}) |
||||
|
|
||||
|
const values = fixRowKey(value || []) |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
|
||||
|
return [ |
||||
|
{ |
||||
|
title: 'id', |
||||
|
dataIndex: 'id', |
||||
|
hideInTable: true, |
||||
|
}, |
||||
|
{ |
||||
|
title: '名称', |
||||
|
dataIndex: 'label', |
||||
|
formItemProps: () => { |
||||
|
return { |
||||
|
rules: [ { required: true, message: '此项为必填项' } ], |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
title: '标识', |
||||
|
dataIndex: 'code', |
||||
|
formItemProps: () => { |
||||
|
return { |
||||
|
rules: [ { required: true, message: '此项为必填项' } ], |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
title: '操作', |
||||
|
valueType: 'option', |
||||
|
width: 80, |
||||
|
|
||||
|
} |
||||
|
] |
||||
|
|
||||
|
}, []) |
||||
|
|
||||
|
|
||||
|
return ( |
||||
|
|
||||
|
<EditableProTable<DataSourceType> |
||||
|
rowKey="id" |
||||
|
value={values } |
||||
|
onChange={onChange} |
||||
|
editableFormRef={editorFormRef} |
||||
|
recordCreatorProps={ |
||||
|
{ |
||||
|
newRecordType: 'dataSource', |
||||
|
record: () => { |
||||
|
return { id: ((value?? []).length + 1) } as DataSourceType |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
editable={{ |
||||
|
type: 'multiple', |
||||
|
editableKeys, |
||||
|
actionRender: (_row, _config, defaultDoms) => { |
||||
|
return [ defaultDoms.delete ] |
||||
|
}, |
||||
|
onValuesChange: (_record, recordList) => { |
||||
|
onChange?.(recordList) |
||||
|
}, |
||||
|
onChange: setEditableRowKeys, |
||||
|
}} |
||||
|
columns={columns as any} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default ButtonTable |
@ -0,0 +1,103 @@ |
|||||
|
import { Button, Empty, Spin, Tree } from 'antd' |
||||
|
import { PlusOutlined } from '@ant-design/icons' |
||||
|
import { MenuItem } from '@/types' |
||||
|
import { useStyle } from '../style.ts' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { useSetAtom } from 'jotai' |
||||
|
import { batchIdsAtom, menuDataAtom, selectedMenuAtom } from '../store.ts' |
||||
|
import { FormInstance } from 'antd/lib' |
||||
|
import { useAtomValue } from 'jotai/index' |
||||
|
import { TreeNodeRender } from '../components/TreeNodeRender.tsx' |
||||
|
import { useRef } from 'react' |
||||
|
import { flattenTree } from '@/utils' |
||||
|
import { useDeepCompareEffect } from 'react-use' |
||||
|
|
||||
|
const MenuTree = ({ form }: { form: FormInstance }) => { |
||||
|
|
||||
|
|
||||
|
const { styles } = useStyle() |
||||
|
const { t } = useTranslation() |
||||
|
const setCurrentMenu = useSetAtom(selectedMenuAtom) |
||||
|
const setIds = useSetAtom(batchIdsAtom) |
||||
|
const { data = [], isLoading } = useAtomValue(menuDataAtom) |
||||
|
const flattenMenusRef = useRef<MenuItem[]>([]) |
||||
|
|
||||
|
useDeepCompareEffect(() => { |
||||
|
|
||||
|
if (isLoading) return |
||||
|
|
||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
// @ts-ignore array
|
||||
|
if (data.length) { |
||||
|
// @ts-ignore flattenTree
|
||||
|
flattenMenusRef.current = flattenTree<MenuItem[]>(data as any) |
||||
|
// console.log(flattenMenusRef.current)
|
||||
|
} |
||||
|
|
||||
|
return () => { |
||||
|
setCurrentMenu({} as MenuItem) |
||||
|
} |
||||
|
|
||||
|
}, [ data, isLoading ]) |
||||
|
|
||||
|
const renderEmpty = () => { |
||||
|
if ((data as any).length > 0 || isLoading) return null |
||||
|
return <Empty description={t('system.menus.empty', '暂无数据,点击添加')}> |
||||
|
<Button type="default" |
||||
|
icon={<PlusOutlined/>} |
||||
|
onClick={() => { |
||||
|
const menu = { |
||||
|
parent_id: 0, |
||||
|
type: 'menu', |
||||
|
name: '', |
||||
|
title: '', |
||||
|
icon: '', |
||||
|
path: '', |
||||
|
component: '', |
||||
|
sort: 0, |
||||
|
id: 0, |
||||
|
} as MenuItem |
||||
|
setCurrentMenu(menu) |
||||
|
form.setFieldsValue(menu) |
||||
|
}} |
||||
|
> |
||||
|
{t('system.menus.add', '添加')} |
||||
|
</Button> |
||||
|
</Empty> |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Spin spinning={isLoading} style={{ minHeight: 200 }}> |
||||
|
{ |
||||
|
renderEmpty() |
||||
|
} |
||||
|
<Tree.DirectoryTree |
||||
|
className={styles.tree} |
||||
|
treeData={data as any} |
||||
|
defaultExpandAll={true} |
||||
|
draggable={true} |
||||
|
titleRender={(node) => { |
||||
|
return (<TreeNodeRender node={node as any} form={form}/>) |
||||
|
}} |
||||
|
fieldNames={{ |
||||
|
title: 'title', |
||||
|
key: 'id' |
||||
|
}} |
||||
|
onSelect={(item) => { |
||||
|
const current = flattenMenusRef.current?.find((menu) => menu.id === item[0]) |
||||
|
setCurrentMenu(current as MenuItem) |
||||
|
form.setFieldsValue({ ...current }) |
||||
|
}} |
||||
|
onCheck={(item) => { |
||||
|
setIds(item as number[]) |
||||
|
}} |
||||
|
checkable={true} |
||||
|
showIcon={false} |
||||
|
/> |
||||
|
</Spin> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default MenuTree |
@ -0,0 +1,66 @@ |
|||||
|
import { memo } from 'react' |
||||
|
import { MenuItem } from '@/types' |
||||
|
import { Popconfirm, Space, TreeDataNode } from 'antd' |
||||
|
import { FormInstance } from 'antd/lib' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { useStyle } from '../style.ts' |
||||
|
import { useAtomValue, useSetAtom } from 'jotai/index' |
||||
|
import { deleteMenuAtom, selectedMenuAtom } from '../store.ts' |
||||
|
import { PlusOutlined } from '@ant-design/icons' |
||||
|
import ActionIcon, { DeleteAction } from '@/components/icon/action' |
||||
|
|
||||
|
export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => { |
||||
|
const { title } = node |
||||
|
const { t } = useTranslation() |
||||
|
const { styles } = useStyle() |
||||
|
const { mutate, } = useAtomValue(deleteMenuAtom) |
||||
|
|
||||
|
const setMenuData = useSetAtom(selectedMenuAtom) |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.treeNode}> |
||||
|
<span>{title as any}</span> |
||||
|
<span className={'actions'}> |
||||
|
<Space size={'middle'}> |
||||
|
<ActionIcon |
||||
|
size={12} |
||||
|
icon={<PlusOutlined/>} |
||||
|
title={t('system.menus.add', '添加')} |
||||
|
onClick={(e) => { |
||||
|
// console.log('add')
|
||||
|
e.stopPropagation() |
||||
|
e.preventDefault() |
||||
|
|
||||
|
const menu = { |
||||
|
parent_id: node.id, |
||||
|
type: 'menu', |
||||
|
name: '', |
||||
|
title: '', |
||||
|
icon: '', |
||||
|
path: '', |
||||
|
component: '', |
||||
|
sort: 0, |
||||
|
id: 0, |
||||
|
} as MenuItem |
||||
|
setMenuData(menu) |
||||
|
form.setFieldsValue(menu) |
||||
|
|
||||
|
}}/> |
||||
|
<Popconfirm |
||||
|
title={t('system.menus.delConfirm', '确定要删除吗?')} |
||||
|
onConfirm={() => { |
||||
|
mutate([ (node as any).id ]) |
||||
|
}} |
||||
|
> |
||||
|
<DeleteAction |
||||
|
size={12} |
||||
|
onClick={(e) => { |
||||
|
e.stopPropagation() |
||||
|
e.stopPropagation() |
||||
|
}}/> |
||||
|
</Popconfirm> |
||||
|
</Space> |
||||
|
</span> |
||||
|
</div> |
||||
|
) |
||||
|
}) |
@ -1,98 +1,155 @@ |
|||||
import { useTranslation } from '@/i18n.ts' |
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 { PageContainer, ProCard } from '@ant-design/pro-components' |
||||
import { Button, Form, Input, Space, Tree } from 'antd' |
|
||||
import { useAtom, useAtomValue } from 'jotai' |
|
||||
import { useEffect, useRef } from 'react' |
|
||||
import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from './store.ts' |
|
||||
import { CloseOutlined, PlusOutlined } from '@ant-design/icons' |
|
||||
|
import { Button, Form, Input, message, Radio, TreeSelect } from 'antd' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
import { menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts' |
||||
import { createLazyFileRoute } from '@tanstack/react-router' |
import { createLazyFileRoute } from '@tanstack/react-router' |
||||
|
import { NumberPicker } from '@formily/antd-v5' |
||||
|
import IconPicker from '@/components/icon/picker' |
||||
|
import ButtonTable from './components/ButtonTable.tsx' |
||||
|
import { Flexbox } from 'react-layout-kit' |
||||
|
import { DraggablePanel } from '@/components/draggable-panel' |
||||
|
import { useStyle } from './style.ts' |
||||
|
import { MenuItem } from '@/types' |
||||
|
import MenuTree from './components/MenuTree.tsx' |
||||
|
import BatchButton from '@/pages/system/menus/components/BatchButton.tsx' |
||||
|
import { useEffect } from 'react' |
||||
|
|
||||
|
|
||||
const Menus = () => { |
const Menus = () => { |
||||
|
|
||||
const { t } = useTranslation() |
|
||||
const { data = [], isLoading } = useAtomValue(menuDataAtom) |
|
||||
const [ currentMenu, setCurrentMenu ] = useAtom(selectedMenuAtom) |
|
||||
const [ selectedKey, setSelectedKey ] = useAtom(selectedMenuIdAtom) |
|
||||
const flattenMenusRef = useRef<FlattenData<IMenu>[]>([]) |
|
||||
|
|
||||
useEffect(() => { |
|
||||
|
|
||||
if (data.length) { |
|
||||
flattenMenusRef.current = flattenTree<IMenu>(data) |
|
||||
console.log(flattenMenusRef.current) |
|
||||
} |
|
||||
|
|
||||
}, [ data ]) |
|
||||
|
|
||||
return ( |
|
||||
<PageContainer |
|
||||
breadcrumbRender={false} title={false}> |
|
||||
<ProCard split={'vertical'} |
|
||||
style={{ |
|
||||
height: 'calc(100vh - 164px)', |
|
||||
overflow: 'auto', |
|
||||
}} |
|
||||
> |
|
||||
<ProCard title={'导航'} |
|
||||
colSpan={'350px'} |
|
||||
extra={ |
|
||||
<Space> |
|
||||
<Button type="primary" size={'small'} icon={<PlusOutlined/>} shape={'circle'}/> |
|
||||
<Button type="default" danger={true} size={'small'} icon={<CloseOutlined/>} |
|
||||
shape={'circle'}/> |
|
||||
|
|
||||
</Space> |
|
||||
} |
|
||||
loading={isLoading}> |
|
||||
<Tree treeData={data} |
|
||||
fieldNames={{ |
|
||||
title: 'title', |
|
||||
key: 'id' |
|
||||
}} |
|
||||
onSelect={(item) => { |
|
||||
console.log(item) |
|
||||
setSelectedKey(item[0]) |
|
||||
setCurrentMenu(flattenMenusRef.current?.find((menu) => menu.id === item[0])) |
|
||||
}} |
|
||||
checkable={true} |
|
||||
showIcon={false} |
|
||||
/> |
|
||||
</ProCard> |
|
||||
<ProCard title={'配置'} style={{ |
|
||||
overflowX: 'hidden' |
|
||||
}}> |
|
||||
|
|
||||
<Form |
|
||||
initialValues={currentMenu} |
|
||||
labelCol={{ flex: '110px' }} |
|
||||
labelAlign="left" |
|
||||
labelWrap |
|
||||
wrapperCol={{ flex: 1 }} |
|
||||
colon={false} |
|
||||
style={{ maxWidth: 600 }} |
|
||||
> |
|
||||
<Form.Item label={t('system.menus.form.title', '菜单名称')} name={'title'}> |
|
||||
<Input/> |
|
||||
</Form.Item> |
|
||||
<Form.Item label={t('system.menus.form.parent', '上级菜单')}> |
|
||||
<Input/> |
|
||||
</Form.Item> |
|
||||
</Form> |
|
||||
|
|
||||
</ProCard> |
|
||||
<ProCard title={'按钮'} colSpan={7}> |
|
||||
|
|
||||
</ProCard> |
|
||||
</ProCard> |
|
||||
</PageContainer> |
|
||||
) |
|
||||
|
const { styles } = useStyle() |
||||
|
const { t } = useTranslation() |
||||
|
const [ form ] = Form.useForm() |
||||
|
const { mutate, isPending, isSuccess, isError } = useAtomValue(saveOrUpdateMenuAtom) |
||||
|
const { data = [] } = useAtomValue(menuDataAtom) |
||||
|
const currentMenu = useAtomValue<MenuItem>(selectedMenuAtom) ?? {} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (isSuccess) { |
||||
|
message.success(t('system.menus.saveSuccess', '保存成功')) |
||||
|
} |
||||
|
}, [ isError, isSuccess ]) |
||||
|
|
||||
|
return ( |
||||
|
<PageContainer |
||||
|
breadcrumbRender={false} title={false} className={styles.container}> |
||||
|
|
||||
|
<Flexbox horizontal> |
||||
|
<DraggablePanel expandable={false} placement="left" maxWidth={800} style={{ width: '100%', }}> |
||||
|
<ProCard title={t('system.menus.title', '菜单')} |
||||
|
extra={ |
||||
|
<BatchButton/> |
||||
|
} |
||||
|
> |
||||
|
<MenuTree form={form}/> |
||||
|
|
||||
|
</ProCard> |
||||
|
</DraggablePanel> |
||||
|
<Flexbox className={styles.box}> |
||||
|
|
||||
|
<Form form={form} |
||||
|
initialValues={currentMenu!} |
||||
|
labelCol={{ flex: '110px' }} |
||||
|
labelAlign="left" |
||||
|
labelWrap |
||||
|
wrapperCol={{ flex: 1 }} |
||||
|
colon={false} |
||||
|
className={styles.form} |
||||
|
> |
||||
|
|
||||
|
<ProCard title={t('system.menus.setting', '配置')} |
||||
|
className={styles.formSetting} |
||||
|
> |
||||
|
|
||||
|
<Form.Item hidden={true} label={t('system.menus.form.id', 'ID')} name={'id'}> |
||||
|
<Input disabled={true}/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.title', '菜单名称')} name={'title'}> |
||||
|
<Input/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}> |
||||
|
<TreeSelect |
||||
|
treeData={[ |
||||
|
{ id: 0, title: '顶级菜单', children: data as any }, |
||||
|
]} |
||||
|
treeDefaultExpandAll={true} |
||||
|
fieldNames={{ |
||||
|
label: 'title', |
||||
|
value: 'id' |
||||
|
}}/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.parent', '类型')} name={'type'}> |
||||
|
<Radio.Group |
||||
|
options={[ |
||||
|
{ label: t('system.menus.form.type.menu', '菜单'), value: 'menu' }, |
||||
|
{ |
||||
|
label: t('system.menus.form.type.iframe', 'iframe'), |
||||
|
value: 'iframe' |
||||
|
}, |
||||
|
{ label: t('system.menus.form.type.link', '外链'), value: 'link' }, |
||||
|
{ label: t('system.menus.form.type.button', '按钮'), value: 'button' }, |
||||
|
]} |
||||
|
optionType="button" |
||||
|
buttonStyle="solid" |
||||
|
/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.name', '别名')} name={'name'}> |
||||
|
<Input/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}> |
||||
|
<IconPicker placement={'left'}/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.sort', '排序')} name={'sort'}> |
||||
|
<NumberPicker/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={t('system.menus.form.path', '路由')} name={'path'}> |
||||
|
<Input/> |
||||
|
</Form.Item> |
||||
|
|
||||
|
<Form.Item label={t('system.menus.form.component', '视图')} |
||||
|
name={'component'} |
||||
|
help={t('system.menus.form.component.help', '视图路径,相对于src/pages')} |
||||
|
> |
||||
|
<Input addonBefore={'pages/'}/> |
||||
|
</Form.Item> |
||||
|
<Form.Item label={' '}> |
||||
|
<Button type="primary" |
||||
|
htmlType={'submit'} |
||||
|
loading={isPending} |
||||
|
onClick={() => { |
||||
|
form.validateFields().then((values) => { |
||||
|
mutate(values) |
||||
|
}) |
||||
|
}} |
||||
|
> |
||||
|
{t('system.menus.form.save', '保存')} |
||||
|
</Button> |
||||
|
</Form.Item> |
||||
|
|
||||
|
|
||||
|
</ProCard> |
||||
|
<ProCard title={t('system.menus.form.button', '按钮')} |
||||
|
className={styles.formButtons} |
||||
|
colSpan={8}> |
||||
|
<Form.Item noStyle={true} name={'button'} |
||||
|
shouldUpdate={(prevValues: MenuItem, curValues) => { |
||||
|
return prevValues.id !== curValues.id |
||||
|
}}> |
||||
|
<ButtonTable form={form} key={(currentMenu as any).id}/> |
||||
|
</Form.Item> |
||||
|
|
||||
|
</ProCard> |
||||
|
</Form> |
||||
|
|
||||
|
</Flexbox> |
||||
|
</Flexbox> |
||||
|
</PageContainer> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export const Route = createLazyFileRoute('/system/menus')({ |
export const Route = createLazyFileRoute('/system/menus')({ |
||||
component: Menus |
|
||||
|
component: Menus |
||||
}) |
}) |
||||
|
|
||||
|
|
@ -1,32 +1,66 @@ |
|||||
import systemServ from '@/service/system.ts' |
import systemServ from '@/service/system.ts' |
||||
import { IPage, IPageResult, MenuItem } from '@/types' |
import { IPage, IPageResult, MenuItem } from '@/types' |
||||
import { IMenu } from '@/types/menus' |
import { IMenu } from '@/types/menus' |
||||
import { atomWithQuery } from 'jotai-tanstack-query' |
|
||||
|
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' |
||||
import { atom } from 'jotai/index' |
import { atom } from 'jotai/index' |
||||
|
|
||||
export const menuPageAtom = atom<IPage>({}) |
export const menuPageAtom = atom<IPage>({}) |
||||
|
|
||||
export const menuDataAtom = atomWithQuery<IPageResult<IMenu[]>>((get) => { |
|
||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
// @ts-ignore fix get
|
||||
|
export const menuDataAtom = atomWithQuery<IPageResult<IMenu>>((get) => { |
||||
|
|
||||
return { |
|
||||
queryKey: [ 'menus', get(menuPageAtom) ], |
|
||||
queryFn: async ({ queryKey: [ , page ] }) => { |
|
||||
return await systemServ.menus.list(page) |
|
||||
}, |
|
||||
select: (data) => { |
|
||||
return data.rows ?? [] |
|
||||
|
return { |
||||
|
queryKey: [ 'menus', get(menuPageAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , page ] }) => { |
||||
|
return await systemServ.menus.list(page) |
||||
|
}, |
||||
|
select: (data) => { |
||||
|
return data.rows ?? [] |
||||
|
} |
||||
} |
} |
||||
} |
|
||||
}) |
}) |
||||
|
|
||||
|
|
||||
export const selectedMenuIdAtom = atom<number>(0) |
export const selectedMenuIdAtom = atom<number>(0) |
||||
export const selectedMenuAtom = atom<MenuItem | unknown>(undefined) |
|
||||
|
export const selectedMenuAtom = atom<MenuItem>({} as MenuItem) |
||||
export const byIdMenuAtom = atomWithQuery((get) => ({ |
export const byIdMenuAtom = atomWithQuery((get) => ({ |
||||
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], |
|
||||
queryFn: async ({ queryKey: [ , id ] }) => { |
|
||||
return await systemServ.menus.info(id as number) |
|
||||
}, |
|
||||
select: data => data.data, |
|
||||
|
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , id ] }) => { |
||||
|
return await systemServ.menus.info(id as number) |
||||
|
}, |
||||
|
select: data => data.data, |
||||
})) |
})) |
||||
|
|
||||
|
|
||||
|
export const saveOrUpdateMenuAtom = atomWithMutation((get) => { |
||||
|
|
||||
|
return { |
||||
|
mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], |
||||
|
mutationFn: async (data: IMenu) => { |
||||
|
if (data.id === 0) { |
||||
|
return await systemServ.menus.add(data) |
||||
|
} |
||||
|
return await systemServ.menus.update(data) |
||||
|
}, |
||||
|
onSuccess: (data) => { |
||||
|
console.log(data) |
||||
|
//更新列表
|
||||
|
get(queryClientAtom).refetchQueries([ 'menus', get(menuPageAtom) ]) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
export const batchIdsAtom = atom<number[]>([]) |
||||
|
|
||||
|
export const deleteMenuAtom = atomWithMutation((get) => { |
||||
|
return { |
||||
|
mutationKey: [ 'deleteMenu', get(batchIdsAtom) ], |
||||
|
mutationFn: async (ids?: number[]) => { |
||||
|
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) |
||||
|
}, |
||||
|
onSuccess: (data) => { |
||||
|
console.log(data) |
||||
|
} |
||||
|
} |
||||
|
}) |
@ -0,0 +1,72 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-menu-page`; |
||||
|
|
||||
|
const tree = css`
|
||||
|
.ant-tree { |
||||
|
overflow: auto; |
||||
|
height: 100%; |
||||
|
border-right: 1px solid ${token.colorBorder}; |
||||
|
background: ${token.colorBgContainer}; |
||||
|
|
||||
|
} |
||||
|
.ant-tree-directory .ant-tree-treenode-selected::before{ |
||||
|
background: ${token.colorBgTextHover}; |
||||
|
} |
||||
|
.ant-tree-treenode:before{ |
||||
|
border-radius: ${token.borderRadius}px; |
||||
|
} |
||||
|
`
|
||||
|
|
||||
|
const box = css`
|
||||
|
flex: 1; |
||||
|
background: ${token.colorBgContainer}; |
||||
|
`
|
||||
|
|
||||
|
const form = css`
|
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
min-width: 500px; |
||||
|
`
|
||||
|
|
||||
|
const formSetting = css`
|
||||
|
flex: 1; |
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const formButtons = css`
|
||||
|
width: 500px; |
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const treeNode = css`
|
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
|
||||
|
.actions{ |
||||
|
display: none; |
||||
|
padding: 0 10px; |
||||
|
} |
||||
|
|
||||
|
&:hover .actions{ { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
`
|
||||
|
const treeActions = css`
|
||||
|
|
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix), |
||||
|
box, |
||||
|
tree, |
||||
|
form, |
||||
|
treeNode, |
||||
|
treeActions, |
||||
|
formSetting, |
||||
|
formButtons, |
||||
|
} |
||||
|
}) |
@ -1,47 +1,49 @@ |
|||||
import { appAtom } from '@/store/system.ts' |
import { appAtom } from '@/store/system.ts' |
||||
import { AxiosResponse } from 'axios' |
import { AxiosResponse } from 'axios' |
||||
import { atom } from 'jotai/index' |
import { atom } from 'jotai/index' |
||||
import { IAuth } from '@/types' |
|
||||
|
import { 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' |
||||
import { formatMenuData, isDev } from '@/utils' |
import { formatMenuData, isDev } from '@/utils' |
||||
|
|
||||
export const authAtom = atom<IAuth>({ |
export const authAtom = atom<IAuth>({ |
||||
isLogin: false, |
|
||||
authKey: [] |
|
||||
|
isLogin: false, |
||||
|
authKey: [] |
||||
}) |
}) |
||||
|
|
||||
const devLogin = { |
const devLogin = { |
||||
username: 'SupperAdmin', |
|
||||
password: 'kk123456', |
|
||||
code: '123456' |
|
||||
|
username: 'SupperAdmin', |
||||
|
password: 'kk123456', |
||||
|
code: '123456' |
||||
} |
} |
||||
export const loginFormAtom = atom<LoginRequest>({ |
export const loginFormAtom = atom<LoginRequest>({ |
||||
...(isDev ? devLogin : {}) |
|
||||
|
...(isDev ? devLogin : {}) |
||||
} as LoginRequest) |
} as LoginRequest) |
||||
|
|
||||
export const loginAtom = atomWithMutation<any, LoginRequest>(() => ({ |
export const loginAtom = atomWithMutation<any, LoginRequest>(() => ({ |
||||
mutationKey: [ 'login' ], |
|
||||
mutationFn: async (params) => { |
|
||||
return await systemServ.login(params) |
|
||||
}, |
|
||||
onSuccess: () => { |
|
||||
// console.log('login success', data)
|
|
||||
} |
|
||||
|
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 ], |
|
||||
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 ]) |
|
||||
}, |
|
||||
|
export const userMenuDataAtom = atomWithQuery<any, MenuItem[]>((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, |
||||
})) |
})) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue