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 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<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) => { |
|||
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 { FlattenData } from '@/types' |
|||
import { IMenu } from '@/types/menus' |
|||
import { flattenTree } from '@/utils' |
|||
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 { 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 { 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')({ |
|||
component: Menus |
|||
component: Menus |
|||
}) |
|||
|
|||
|
@ -1,32 +1,66 @@ |
|||
import systemServ from '@/service/system.ts' |
|||
import { IPage, IPageResult, MenuItem } from '@/types' |
|||
import { IMenu } from '@/types/menus' |
|||
import { atomWithQuery } from 'jotai-tanstack-query' |
|||
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' |
|||
import { atom } from 'jotai/index' |
|||
|
|||
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 selectedMenuAtom = atom<MenuItem | unknown>(undefined) |
|||
export const selectedMenuAtom = atom<MenuItem>({} as MenuItem) |
|||
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 { AxiosResponse } from 'axios' |
|||
import { atom } from 'jotai/index' |
|||
import { IAuth } from '@/types' |
|||
import { 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<IAuth>({ |
|||
isLogin: false, |
|||
authKey: [] |
|||
isLogin: false, |
|||
authKey: [] |
|||
}) |
|||
|
|||
const devLogin = { |
|||
username: 'SupperAdmin', |
|||
password: 'kk123456', |
|||
code: '123456' |
|||
username: 'SupperAdmin', |
|||
password: 'kk123456', |
|||
code: '123456' |
|||
} |
|||
export const loginFormAtom = atom<LoginRequest>({ |
|||
...(isDev ? devLogin : {}) |
|||
...(isDev ? devLogin : {}) |
|||
} as 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