dark
7 months ago
36 changed files with 2113 additions and 383 deletions
-
7package.json
-
9src/App.tsx
-
15src/components/breadcrumb/index.tsx
-
16src/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
-
5src/components/icon/action/ActionIcon.tsx
-
6src/components/icon/action/style.ts
-
9src/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
-
171src/pages/system/menus/index.tsx
-
40src/pages/system/menus/store.ts
-
72src/pages/system/menus/style.ts
-
36src/request.ts
-
6src/service/base.ts
-
9src/store/system.ts
-
8src/store/user.ts
-
2src/theme/themes/antdTheme.ts
-
1src/theme/themes/token.ts
-
29src/utils/index.ts
-
253yarn.lock
@ -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> |
|||
) |
|||
}) |
@ -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, |
|||
} |
|||
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue