Browse Source

完善菜单页面

main
dark 5 months ago
parent
commit
bf5707ba81
  1. 7
      package.json
  2. 9
      src/App.tsx
  3. 15
      src/components/breadcrumb/index.tsx
  4. 16
      src/components/config-provider/index.tsx
  5. 182
      src/components/draggable-panel/DraggablePanel.tsx
  6. 258
      src/components/draggable-panel/FixMode.tsx
  7. 182
      src/components/draggable-panel/FloatMode.tsx
  8. 3
      src/components/draggable-panel/index.ts
  9. 183
      src/components/draggable-panel/style.ts
  10. 5
      src/components/icon/action/ActionIcon.tsx
  11. 6
      src/components/icon/action/style.ts
  12. 9
      src/components/icon/index.tsx
  13. 1
      src/components/icon/picker/Display.tsx
  14. 9
      src/components/icon/types.ts
  15. 2
      src/i18n.ts
  16. 8
      src/index.css
  17. 9
      src/layout/RootLayout.tsx
  18. 21
      src/layout/style.ts
  19. 2
      src/pages/login/index.css
  20. 30
      src/pages/login/index.tsx
  21. 130
      src/pages/login/style.ts
  22. 32
      src/pages/system/menus/components/BatchButton.tsx
  23. 97
      src/pages/system/menus/components/ButtonTable.tsx
  24. 103
      src/pages/system/menus/components/MenuTree.tsx
  25. 66
      src/pages/system/menus/components/TreeNodeRender.tsx
  26. 171
      src/pages/system/menus/index.tsx
  27. 40
      src/pages/system/menus/store.ts
  28. 72
      src/pages/system/menus/style.ts
  29. 36
      src/request.ts
  30. 6
      src/service/base.ts
  31. 9
      src/store/system.ts
  32. 8
      src/store/user.ts
  33. 2
      src/theme/themes/antdTheme.ts
  34. 1
      src/theme/themes/token.ts
  35. 29
      src/utils/index.ts
  36. 253
      yarn.lock

7
package.json

@ -22,14 +22,21 @@
"antd": "^5.16.1",
"antd-style": "^3.6.2",
"axios": "^1.6.8",
"bunshi": "^2.1.4",
"dayjs": "^1.11.10",
"fast-deep-equal": "^3.1.3",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jotai": "^2.8.0",
"jotai-scope": "^0.5.1",
"jotai-tanstack-query": "^0.8.5",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-layout-kit": "^1.9.0",
"react-rnd": "^10.4.2-test2",
"react-use": "^17.5.0",
"wonka": "^6.3.4"
},
"devDependencies": {

9
src/App.tsx

@ -1,10 +1,9 @@
import ErrorPage from '@/components/error/error.tsx'
import { AppContextProvider } from '@/context.ts'
import { initI18n } from '@/i18n.ts'
import { appAtom, appStore, changeLanguage } from '@/store/system.ts'
import { userMenuDataAtom } from '@/store/user.ts'
import { IAppData } from '@/types'
import { ConfigProvider } from 'antd'
import { ConfigProvider } from '@/components/config-provider'
import { Provider, useAtom, useAtomValue } from 'jotai'
import './App.css'
import { useEffect } from 'react'
@ -15,7 +14,7 @@ import PageLoading from '@/components/page-loading'
function App() {
const [ appData, ] = useAtom(appAtom)
const { data = [], isError, error, isLoading, refetch } = useAtomValue(userMenuDataAtom)
const { data = [], isLoading, refetch } = useAtomValue(userMenuDataAtom)
useEffect(() => {
initI18n()
@ -28,10 +27,6 @@ function App() {
}
}, [ appData.token ])
if (isError) {
console.error(error)
return <ErrorPage error={error}/>
}
if (isLoading) {
return <PageLoading/>

15
src/components/breadcrumb/index.tsx

@ -2,9 +2,9 @@ import { Breadcrumb, BreadcrumbProps, Dropdown } from 'antd'
import { Link, useNavigate } from '@tanstack/react-router'
import { DownOutlined } from '@ant-design/icons'
import { getIcon } from '@/components/icon'
import { useCallback } from 'react'
import { memo, useCallback } from 'react'
export const PageBreadcrumb = (props: BreadcrumbProps & {
export const PageBreadcrumb = memo((props: BreadcrumbProps & {
showIcon?: boolean;
}) => {
@ -26,7 +26,7 @@ export const PageBreadcrumb = (props: BreadcrumbProps & {
const items = route.children.map((item) => {
return {
...item,
key: item.path,
key: item.path || item.name,
label: item.name,
}
})
@ -40,9 +40,10 @@ export const PageBreadcrumb = (props: BreadcrumbProps & {
}}
trigger={[ 'hover' ]}>
{
!route.component ? <a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
(!route.component || !route.path)? <a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
<DownOutlined/></a>
: <Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span>
<DownOutlined/>
</Link>
@ -53,14 +54,16 @@ export const PageBreadcrumb = (props: BreadcrumbProps & {
)
}
return isLast ? (
return isLast || !route.path ? (
<span className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></span>
) : (
<Link to={`/${route.path}`}
preload={false}
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></Link>
)
}
return (
<>
<Breadcrumb {...other}
@ -69,6 +72,6 @@ export const PageBreadcrumb = (props: BreadcrumbProps & {
/>
</>
)
}
})
export default PageBreadcrumb

16
src/components/config-provider/index.tsx

@ -1,4 +1,4 @@
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'
@ -9,9 +9,9 @@ export const useProAntdTheme = (appearance: ThemeAppearance) => {
const themeConfig = createProAntdTheme(appearance)
const controlToken: Partial<AntdToken> = {
colorBgContainer: token?.colorFillQuaternary,
colorBorder: 'transparent',
controlOutline: 'transparent',
// colorBgContainer: token?.colorFillQuaternary,
// colorBorder: 'transparent',
// controlOutline: 'transparent',
}
themeConfig.components = {
@ -20,6 +20,8 @@ export const useProAntdTheme = (appearance: ThemeAppearance) => {
Select: controlToken,
Tree: {
colorBgContainer: 'transparent',
directoryNodeSelectedBg: '#e1f0ff',
directoryNodeSelectedColor: 'rgb(22, 62, 124)',
},
TreeSelect: controlToken,
}
@ -27,18 +29,18 @@ export const useProAntdTheme = (appearance: ThemeAppearance) => {
return themeConfig
}
export interface ConfigProviderProps {
export interface ConfigProviderProps extends AntdConfigProviderProps {
componentToken?: OverrideToken;
children: ReactNode;
}
export const ConfigProvider: FC<ConfigProviderProps> = ({ children, componentToken }) => {
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}>
<AntdConfigProvider theme={proTheme} {...props}>
<ThemeProvider
appearance={appearance}
themeMode={themeMode}

182
src/components/draggable-panel/DraggablePanel.tsx

@ -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 }

258
src/components/draggable-panel/FixMode.tsx

@ -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 }

182
src/components/draggable-panel/FloatMode.tsx

@ -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>
)
},
)

3
src/components/draggable-panel/index.ts

@ -0,0 +1,3 @@
export type { Position } from 'react-rnd';
export { Draggable as DraggablePanel } from './DraggablePanel';
export type { DraggablePanelProps } from './DraggablePanel';

183
src/components/draggable-panel/style.ts

@ -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%;
}
`,
),
};
});

5
src/components/icon/action/ActionIcon.tsx

@ -43,6 +43,8 @@ export interface ActionIconProps extends Omit<ButtonProps, 'title' | 'size'> {
* @default false
*/
arrow?: boolean;
bordered?: boolean;
}
const BaseActionIcon: FC<ActionIconProps> = ({
@ -52,12 +54,13 @@ const BaseActionIcon: FC<ActionIconProps> = ({
cursor,
onClick,
className,
bordered = false,
arrow = false,
size = 'default',
tooltipDelay = 0.5,
...restProps
}) => {
const { styles, cx } = useStyles({ size })
const { styles, cx } = useStyles({ size, bordered, className })
const Icon = (
<Button

6
src/components/icon/action/style.ts

@ -2,8 +2,9 @@ import { createStyles } from '@/theme'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const useStyles = createStyles(({ token, css, cx, prefixCls }, { size, className }) => {
export const useStyles = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token.proPrefix}-icon`
const { size, className,bordered } = props
const sizeBoundary =
typeof size === 'number'
? css`
@ -16,7 +17,8 @@ export const useStyles = createStyles(({ token, css, cx, prefixCls }, { size, cl
display: flex;
align-items: center;
justify-content: center;
border: 1px solid ${token.colorBorder};
${bordered ? `border: 1px solid ${token.colorBorder};` : ''}
&:hover {
color: ${token.colorText} !important;

9
src/components/icon/index.tsx

@ -1,16 +1,17 @@
import IconAll, { ALL_ICON_KEYS, IconType, IIconAllProps } from '@icon-park/react/es/all'
import React, { Fragment } from 'react'
import * as AntIcons from '@ant-design/icons/es/icons'
import { IconComponentProps } from '@ant-design/icons/es/components/Icon'
import IconItem from './picker/IconRender.tsx'
import { IconUnit } from './types.ts'
export function Icon(props: Partial<IIconAllProps | IconComponentProps>) {
export function Icon(props: IconUnit) {
const { type, ...other } = props
if(type && ['antd', 'park'].includes(type as string)){
return <IconItem {...props as any}/>
if (type && [ 'antd:', 'park:' ].includes(type as string)) {
const [ t, c ] = type.split(':')
return <IconItem {...props as any} type={t} componentName={c}/>
}
const AntIcon = AntIcons[type as keyof typeof AntIcons]

1
src/components/icon/picker/Display.tsx

@ -19,6 +19,7 @@ const Display = () => {
)
return ( <ActionIcon
bordered={true}
onClick={() => ctx.actions.togglePanel()}
icon={!ctx.state.icon ? DefaultIcon : <IconItem {...ctx.state.icon as any} />} />
)

9
src/components/icon/types.ts

@ -1,4 +1,3 @@
export interface ReactIcon {
type: 'antd' | 'park';
componentName: string;
@ -14,7 +13,13 @@ export interface IconfontIcon {
scriptUrl?: string;
}
export interface IconComponentProps {
type: string;
[key: string]: any;
}
/**
*
*/
export type IconUnit = ReactIcon | IconfontIcon;
export type IconUnit = ReactIcon | IconfontIcon | IconComponentProps

2
src/i18n.ts

@ -35,7 +35,7 @@ export const initI18n = (options?: InitOptions) => {
},
},
fallbackLng: 'zh',
debug: true,
debug: false,
detection: detectionOptions,
interpolation: {
escapeValue: false,

8
src/index.css

@ -0,0 +1,8 @@
body{
margin: 0;
padding: 0;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
body.login {
overflow: hidden;
}

9
src/layout/RootLayout.tsx

@ -6,11 +6,13 @@ import { userMenuDataAtom } from '@/store/user.ts'
import { MenuItem } from '@/types'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
import { CatchBoundary, Link, Outlet } from '@tanstack/react-router'
import { ConfigProvider, Dropdown } from 'antd'
import { Dropdown } from 'antd'
import { ConfigProvider} from '@/components/config-provider'
import { useState } from 'react'
import Icon from '../components/icon'
import defaultProps from './_defaultProps'
import { useAtomValue } from 'jotai'
import { useStyle } from '@/layout/style.ts'
//根据menuData生成Breadcrumb所需的数据
@ -37,15 +39,16 @@ const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
export default () => {
const { styles } = useStyle()
const { t } = useTranslation()
const { data: menuData = [], isLoading } = useAtomValue(userMenuDataAtom)
const items = getBreadcrumbData(menuData, location.pathname)
const [ pathname, setPathname ] = useState(location.pathname)
return (
<div
className={styles.container}
id="crazy-pro-layout"
style={{
height: '100vh',
@ -134,7 +137,7 @@ export default () => {
'navTheme': 'light',
'contentWidth': 'Fluid',
'fixSiderbar': true,
'colorPrimary': '#1677FF',
// 'colorPrimary': '#1677FF',
'siderMenuType': 'group',
// layout: 'side',
}}

21
src/layout/style.ts

@ -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,
}
})

2
src/pages/login/index.css

@ -18,7 +18,7 @@ body {
position: absolute;
top: 10px;
right: 10px;
/*color: #fff;*/
color: #fff;
font-size: 14px;
cursor: pointer;

30
src/pages/login/index.tsx

@ -2,14 +2,15 @@ import SelectLang from '@/components/select-lang'
import { createFileRoute, useSearch, useNavigate } from '@tanstack/react-router'
import { Button, Form, Input, message, Space } from 'antd'
import { useAtom } from 'jotai'
import './index.css'
import { useTranslation } from '@/i18n.ts'
import { loginAtom, loginFormAtom } from '@/store/user.ts'
import { memo, useEffect } from 'react'
import { memo, useEffect, useLayoutEffect } from 'react'
// import './index.css'
import { useStyles } from './style.ts'
const Login = memo(() => {
const { styles } = useStyles()
const navigator = useNavigate()
// @ts-ignore 从url中获取redirect参数
const search = useSearch({ form: '/login' })
@ -33,22 +34,32 @@ const Login = memo(() => {
}
}, [ isSuccess ])
useLayoutEffect(()=>{
document.body.className = 'login'
return ()=>{
document.body.className = document.body.className.replace('login', '')
}
}, [])
return (
<div className={'login-container'}>
<div className={'language'}>
<div className={styles.container}>
<div className={styles.language}>
<SelectLang/>
</div>
<div className={'loginBlock'}>
<div className={'innerBlock'}>
<div className={styles.loginBlock}>
<div className={styles.innerBlock}>
<div className={'desc'}>
<span className={'active'}>
<div className={styles.desc}>
<span className={styles.active}>
{t('login.title')}
</span>
</div>
<Form form={form}
disabled={isPending}
initialValues={values}
onValuesChange={(_, allValues) => {
setValues(allValues)
@ -83,7 +94,6 @@ const Login = memo(() => {
{t('login.submit')}
</Button>
</Form.Item>
</Form>
</div>
</div>

130
src/pages/login/style.ts

@ -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,
}
})

32
src/pages/system/menus/components/BatchButton.tsx

@ -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

97
src/pages/system/menus/components/ButtonTable.tsx

@ -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

103
src/pages/system/menus/components/MenuTree.tsx

@ -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

66
src/pages/system/menus/components/TreeNodeRender.tsx

@ -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>
)
})

171
src/pages/system/menus/index.tsx

@ -1,92 +1,149 @@
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 { styles } = useStyle()
const { t } = useTranslation()
const { data = [], isLoading } = useAtomValue(menuDataAtom)
const [ currentMenu, setCurrentMenu ] = useAtom(selectedMenuAtom)
const [ selectedKey, setSelectedKey ] = useAtom(selectedMenuIdAtom)
const flattenMenusRef = useRef<FlattenData<IMenu>[]>([])
const [ form ] = Form.useForm()
const { mutate, isPending, isSuccess, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom)
const currentMenu = useAtomValue<MenuItem>(selectedMenuAtom) ?? {}
useEffect(() => {
if (data.length) {
flattenMenusRef.current = flattenTree<IMenu>(data)
console.log(flattenMenusRef.current)
if (isSuccess) {
message.success(t('system.menus.saveSuccess', '保存成功'))
}
}, [ data ])
}, [ isError, isSuccess ])
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'}/>
breadcrumbRender={false} title={false} className={styles.container}>
</Space>
<Flexbox horizontal>
<DraggablePanel expandable={false} placement="left" maxWidth={800} style={{ width: '100%', }}>
<ProCard title={t('system.menus.title', '菜单')}
extra={
<BatchButton/>
}
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}
/>
>
<MenuTree form={form}/>
</ProCard>
<ProCard title={'配置'} style={{
overflowX: 'hidden'
}}>
</DraggablePanel>
<Flexbox className={styles.box}>
<Form
initialValues={currentMenu}
<Form form={form}
initialValues={currentMenu!}
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
wrapperCol={{ flex: 1 }}
colon={false}
style={{ maxWidth: 600 }}
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', '上级菜单')}>
<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>
</ProCard>
<ProCard title={'按钮'} colSpan={7}>
<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>
)
}

40
src/pages/system/menus/store.ts

@ -1,12 +1,14 @@
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) ],
@ -21,7 +23,7 @@ export const menuDataAtom = atomWithQuery<IPageResult<IMenu[]>>((get) => {
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 ] }) => {
@ -30,3 +32,35 @@ export const byIdMenuAtom = atomWithQuery((get) => ({
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)
}
}
})

72
src/pages/system/menus/style.ts

@ -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,
}
})

36
src/request.ts

@ -43,6 +43,12 @@ request.interceptors.response.use((response: AxiosResponse) => {
}
return response.data
case 401:
setToken('')
if (window.location.pathname === '/login') {
return
}
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
@ -53,14 +59,40 @@ request.interceptors.response.use((response: AxiosResponse) => {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
break
return
default:
message.error(response.data.message)
return Promise.reject(response)
}
}, (error) => {
console.log('error', error)
// console.log('error', error)
const { response } = error
if (response) {
switch (response.status) {
case 401:
if (window.location.pathname === '/login') {
return
}
setToken('')
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
default:
message.error(response.data.message)
return Promise.reject(response)
}
}
return Promise.reject(error)
})

6
src/service/base.ts

@ -6,13 +6,13 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
return {
list: (params?: TParams & IPage) => {
return request.post<TResult[]>(`${api}/list`, { ...options, params }).then(data => data.data)
return request.post<TResult[]>(`${api}/list`, { ...options, ...params }).then(data => data.data)
},
add: (data: TParams) => {
return request.post<TResult>(`${api}/add`, data, options)
},
update: (id: number, data: TParams) => {
return request.put(`${api}/${id}`, data, options)
update: (data: TParams) => {
return request.post(`${api}/edit`, data, options)
},
delete: (id: number) => {
return request.delete(`${api}/delete`, { ...options, params: { id } })

9
src/store/system.ts

@ -1,11 +1,8 @@
import { IAppData, MenuItem } from '@/types'
import { IMenu } from '@/types/menus'
import { atom, createStore } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { IAppData } from '@/types'
import { createStore } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import systemServ from '../service/system.ts'
import { changeLanguage as setLang } from 'i18next'
import { AxiosResponse } from 'axios'
import { userMenuDataAtom } from '@/store/user.ts'
/**
* app全局状态

8
src/store/user.ts

@ -1,7 +1,7 @@
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'
@ -28,10 +28,11 @@ export const loginAtom = atomWithMutation<any, LoginRequest>(() => ({
},
onSuccess: () => {
// console.log('login success', data)
}
},
retry: false,
}))
export const userMenuDataAtom = atomWithQuery((get) => ({
export const userMenuDataAtom = atomWithQuery<any, MenuItem[]>((get) => ({
enabled: false,
queryKey: [ 'user_menus', get(appAtom).token ],
queryFn: async () => {
@ -44,4 +45,5 @@ export const userMenuDataAtom = atomWithQuery((get) => ({
const queryClient = get(queryClientAtom)
return queryClient.getQueryData([ 'user_menus', get(appAtom).token ])
},
retry: false,
}))

2
src/theme/themes/antdTheme.ts

@ -4,7 +4,7 @@ import { proDarkAlgorithm } from '@/theme'
export const createProAntdTheme = (appearance: ThemeAppearance) => {
const themeConfig: ThemeConfig = {
algorithm: [ theme.compactAlgorithm ],
algorithm: [ theme.defaultAlgorithm ],
}
if (appearance === 'dark') {

1
src/theme/themes/token.ts

@ -18,6 +18,7 @@ export const getProToken: GetCustomToken<ProThemeToken> = () => ({
colorTypeBoolArray: '#D8C152',
colorTypeNumberArray: '#239BEF',
colorTypeStringArray: '#62AE8D',
})
export const themeToken = getProToken({} as any)

29
src/utils/index.ts

@ -65,29 +65,30 @@ const defaultTreeFieldNames: FiledNames = {
children: 'children'
}
export function flattenTree<T>(tree: TreeItem<T>[], fieldNames: {
key: string;
title: string;
children: string
} = defaultTreeFieldNames): FlattenData<T>[] {
export function flattenTree<T>(tree: TreeItem<T>[], fieldNames?: FiledNames) {
const result: FlattenData<T>[] = []
function flattenRecursive(item: TreeItem<T>, level: number) {
if (!fieldNames) {
fieldNames = defaultTreeFieldNames
}
function flattenRecursive(item: TreeItem<T>, level: number, fieldNames: FiledNames) {
const data: FlattenData<T> = {
...item,
key: item[fieldNames.key],
title: item[fieldNames.title],
key: item[fieldNames.key!],
title: item[fieldNames.title!],
level,
}
result.push(data)
if (item.children) {
item.children.forEach((child) => flattenRecursive(child, level + 1))
const children = item[fieldNames.children!]
if (children) {
children.forEach((child) => flattenRecursive(child, level + 1, fieldNames))
data.children = children
}
result.push(data)
}
tree.forEach((item) => flattenRecursive(item, 0))
tree.forEach((item) => flattenRecursive(item, 0, fieldNames))
return result
}

253
yarn.lock

@ -486,7 +486,7 @@
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-syntax-typescript" "^7.24.1"
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.1", "@babel/runtime@^7.24.4":
"@babel/runtime@^7", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.1", "@babel/runtime@^7.24.4":
version "7.24.4"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
@ -607,7 +607,7 @@
"@emotion/weak-memoize" "^0.3.1"
stylis "4.2.0"
"@emotion/css@^11.11.2":
"@emotion/css@^11", "@emotion/css@^11.11.2":
version "11.11.2"
resolved "https://registry.npmmirror.com/@emotion/css/-/css-11.11.2.tgz#e5fa081d0c6e335352e1bc2b05953b61832dca5a"
integrity sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==
@ -984,7 +984,7 @@
resolved "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
@ -1282,6 +1282,11 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/js-cookie@^2.2.6":
version "2.2.7"
resolved "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@ -1434,6 +1439,11 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.14.0"
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.npmmirror.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1654,6 +1664,11 @@ bundle-require@^4.0.1:
dependencies:
load-tsconfig "^0.2.3"
bunshi@^2.1.4:
version "2.1.4"
resolved "https://registry.npmmirror.com/bunshi/-/bunshi-2.1.4.tgz#1b7b99294ce101eb7167c6d98d79129b0cd2bf73"
integrity sha512-8kHpSsfFQu3Dj6nuVP5qr6wwqeXHbdu/hqrT1hdv6N4KcIQ0amHJYR0iSgjYhxazS6NlQb6JacP1/A7EJ+ZRAg==
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -1714,6 +1729,11 @@ client-only@^0.0.1:
resolved "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
@ -1785,7 +1805,7 @@ convert-source-map@^2.0.0:
resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-to-clipboard@^3.3.3:
copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
version "3.3.3"
resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
@ -1817,7 +1837,22 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
csstype@^3.0.2, csstype@^3.1.3:
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
dependencies:
hyphenate-style-name "^1.0.3"
css-tree@^1.1.2:
version "1.1.3"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
csstype@^3.0.2, csstype@^3.1.2, csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@ -1909,6 +1944,13 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.6:
version "2.1.4"
resolved "https://registry.npmmirror.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286"
integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==
dependencies:
stackframe "^1.3.4"
esbuild@^0.20.1:
version "0.20.2"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
@ -2084,6 +2126,26 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-loops@^1.1.3:
version "1.1.3"
resolved "https://registry.npmmirror.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75"
integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==
fast-memoize@^2.5.1:
version "2.5.2"
resolved "https://registry.npmmirror.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fast-shallow-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==
fastq@^1.6.0:
version "1.17.1"
resolved "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
@ -2281,6 +2343,11 @@ html-tokenize@^2.0.0:
readable-stream "~1.0.27-1"
through2 "~0.4.1"
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.npmmirror.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18next-browser-languagedetector@^7.2.1:
version "7.2.1"
resolved "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f"
@ -2326,6 +2393,14 @@ inherits@2, inherits@~2.0.1, inherits@~2.0.3:
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inline-style-prefixer@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz#991d550735d42069f528ac1bcdacd378d1305442"
integrity sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==
dependencies:
css-in-js-utils "^3.1.0"
fast-loops "^1.1.3"
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@ -2382,6 +2457,11 @@ isexe@^2.0.0:
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jotai-scope@^0.5.1:
version "0.5.1"
resolved "https://registry.npmmirror.com/jotai-scope/-/jotai-scope-0.5.1.tgz#a0eee96435262ff195901c420ca55b411639d8ef"
integrity sha512-GKaqtCj1Hv36nyl63PVm+s5jo+hQqz+wAb81iqA8VuUXp5ot4eXGOZt4Hc66lFZMP0N/yEOISMlazprmDI6kFA==
jotai-tanstack-query@^0.8.5:
version "0.8.5"
resolved "https://registry.npmmirror.com/jotai-tanstack-query/-/jotai-tanstack-query-0.8.5.tgz#12616f18a7623cc3786f3e2d7b1e1b2dc60394b2"
@ -2392,6 +2472,11 @@ jotai@^2.8.0:
resolved "https://registry.npmmirror.com/jotai/-/jotai-2.8.0.tgz#5a6585cd5576c400c2c5f8e157b83ad2ba70b2ab"
integrity sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g==
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -2516,6 +2601,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
[email protected]:
version "2.0.14"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@ -2585,6 +2675,20 @@ multipipe@^1.0.2:
duplexer2 "^0.1.2"
object-assign "^4.1.0"
nano-css@^5.6.1:
version "5.6.1"
resolved "https://registry.npmmirror.com/nano-css/-/nano-css-5.6.1.tgz#964120cb1af6cccaa6d0717a473ccd876b34c197"
integrity sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
css-tree "^1.1.2"
csstype "^3.1.2"
fastest-stable-stringify "^2.0.2"
inline-style-prefixer "^7.0.0"
rtl-css-js "^1.16.1"
stacktrace-js "^2.0.2"
stylis "^4.3.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
@ -2775,7 +2879,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
prop-types@^15.5.10:
prop-types@^15.5.10, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -3176,6 +3280,18 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
rc-resize-observer "^1.0.0"
rc-util "^5.36.0"
[email protected]:
version "6.9.6"
resolved "https://registry.npmmirror.com/re-resizable/-/re-resizable-6.9.6.tgz#b95d37e3821481b56ddfb1e12862940a791e827d"
integrity sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==
dependencies:
fast-memoize "^2.5.1"
re-resizable@^6.9.11:
version "6.9.11"
resolved "https://registry.npmmirror.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475"
integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ==
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -3184,6 +3300,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
[email protected]:
version "4.4.5"
resolved "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-i18next@^14.1.0:
version "14.1.0"
resolved "https://registry.npmmirror.com/react-i18next/-/react-i18next-14.1.0.tgz#44da74fbffd416f5d0c5307ef31735cf10cc91d9"
@ -3202,6 +3326,14 @@ react-is@^18.2.0:
resolved "https://registry.npmmirror.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-layout-kit@^1.9.0:
version "1.9.0"
resolved "https://registry.npmmirror.com/react-layout-kit/-/react-layout-kit-1.9.0.tgz#db04c27a048d76243c0a4dfb107f7aa0428e9c5f"
integrity sha512-YjFXGaWTemwagfdmqz1VxTATXCpQmDfW/giHOSQDrGsddyBC6MEFdj+kNHYY9WpRamQDjyzAIkOQaJ1KxmcC2g==
dependencies:
"@babel/runtime" "^7"
"@emotion/css" "^11"
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
@ -3212,6 +3344,15 @@ react-refresh@^0.14.0:
resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-rnd@^10.4.2-test2:
version "10.4.2-test2"
resolved "https://registry.npmmirror.com/react-rnd/-/react-rnd-10.4.2-test2.tgz#09e6bcb8fca0c40e60829b89380bd64a017cb4fb"
integrity sha512-gIlqsvhAM3mNz+QKa7n/fOkChHwU5Hq0hiSq4d+kq9Q7mW0KO9jp+M9ufHGuPykbWv1LST5ybrxPVyddN7oXZg==
dependencies:
re-resizable "6.9.6"
react-draggable "4.4.5"
tslib "2.3.1"
react-sticky-box@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/react-sticky-box/-/react-sticky-box-1.0.2.tgz#7e72a0f237bdf8270cec9254337f49519a411174"
@ -3219,6 +3360,31 @@ react-sticky-box@^1.0.2:
dependencies:
resize-observer-polyfill "^1.5.1"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.npmmirror.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@^17.5.0:
version "17.5.0"
resolved "https://registry.npmmirror.com/react-use/-/react-use-17.5.0.tgz#1fae45638828a338291efa0f0c61862db7ee6442"
integrity sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.6.1"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmmirror.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -3323,6 +3489,13 @@ rollup@^4.13.0:
"@rollup/rollup-win32-x64-msvc" "4.14.2"
fsevents "~2.3.2"
rtl-css-js@^1.16.1:
version "1.16.1"
resolved "https://registry.npmmirror.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80"
integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==
dependencies:
"@babel/runtime" "^7.1.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@ -3347,6 +3520,11 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
scroll-into-view-if-needed@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f"
@ -3366,6 +3544,11 @@ semver@^7.6.0:
dependencies:
lru-cache "^6.0.0"
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
@ -3393,11 +3576,50 @@ source-map-js@^1.2.0:
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
[email protected]:
version "0.5.6"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
stack-generator@^2.0.5:
version "2.0.10"
resolved "https://registry.npmmirror.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==
dependencies:
stackframe "^1.3.4"
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
stacktrace-gps@^3.0.4:
version "3.1.2"
resolved "https://registry.npmmirror.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0"
integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==
dependencies:
source-map "0.5.6"
stackframe "^1.3.4"
stacktrace-js@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b"
integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
error-stack-parser "^2.0.6"
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@ -3437,7 +3659,7 @@ [email protected]:
resolved "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
stylis@^4.0.13:
stylis@^4.0.13, stylis@^4.3.0:
version "4.3.1"
resolved "https://registry.npmmirror.com/stylis/-/stylis-4.3.1.tgz#ed8a9ebf9f76fe1e12d462f5cc3c4c980b23a7eb"
integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==
@ -3474,6 +3696,11 @@ text-table@^0.2.0:
resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
throttle-debounce@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933"
@ -3529,7 +3756,17 @@ ts-api-utils@^1.3.0:
resolved "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
tslib@^2.0.0, tslib@^2.0.3:
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.npmmirror.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
[email protected]:
version "2.3.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0:
version "2.6.2"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==

Loading…
Cancel
Save