2025-01-07 02:29:29 +08:00
|
|
|
import { useAntToken } from '@/styles/useAntToken';
|
|
|
|
import { useMemoizedFn } from 'ahooks';
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import SplitPane, { Pane } from '@/components/layout/SplitPane';
|
|
|
|
import { createAutoSaveId } from './helper';
|
|
|
|
import Cookies from 'js-cookie';
|
2025-01-10 03:04:57 +08:00
|
|
|
import { createStyles } from 'antd-style';
|
2025-01-07 02:29:29 +08:00
|
|
|
|
|
|
|
export const AppSplitter: React.FC<{
|
|
|
|
leftChildren: React.ReactNode;
|
|
|
|
rightChildren: React.ReactNode;
|
|
|
|
autoSaveId: string;
|
|
|
|
defaultLayout: (string | number)[];
|
|
|
|
leftPanelMinSize?: number | string;
|
|
|
|
rightPanelMinSize?: number | string;
|
|
|
|
leftPanelMaxSize?: number | string;
|
|
|
|
rightPanelMaxSize?: number | string;
|
|
|
|
className?: string;
|
|
|
|
allowResize?: boolean;
|
|
|
|
split?: 'vertical' | 'horizontal';
|
|
|
|
splitterClassName?: string;
|
|
|
|
preserveSide: 'left' | 'right' | null;
|
|
|
|
rightHidden?: boolean;
|
|
|
|
leftHidden?: boolean;
|
|
|
|
style?: React.CSSProperties;
|
|
|
|
hideSplitter?: boolean;
|
|
|
|
}> = ({
|
|
|
|
style,
|
|
|
|
leftChildren,
|
|
|
|
preserveSide,
|
|
|
|
rightChildren,
|
|
|
|
autoSaveId,
|
|
|
|
defaultLayout,
|
|
|
|
leftPanelMinSize,
|
|
|
|
rightPanelMinSize,
|
2025-01-10 03:04:57 +08:00
|
|
|
split = 'vertical',
|
2025-01-07 02:29:29 +08:00
|
|
|
leftPanelMaxSize,
|
|
|
|
rightPanelMaxSize,
|
|
|
|
allowResize,
|
|
|
|
className,
|
|
|
|
splitterClassName,
|
|
|
|
leftHidden,
|
|
|
|
rightHidden,
|
|
|
|
hideSplitter
|
|
|
|
}) => {
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const [sizes, setSizes] = useState<(number | string)[]>(defaultLayout);
|
|
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
const hasHidden = useMemo(() => leftHidden || rightHidden, [leftHidden, rightHidden]);
|
|
|
|
const _allowResize = useMemo(() => (hasHidden ? false : allowResize), [hasHidden, allowResize]);
|
|
|
|
|
|
|
|
const _sizes = useMemo(
|
|
|
|
() => (hasHidden ? (leftHidden ? ['0px', 'auto'] : ['auto', '0px']) : sizes),
|
|
|
|
[hasHidden, leftHidden, sizes]
|
|
|
|
);
|
|
|
|
|
|
|
|
const memoizedLeftPaneStyle = useMemo(() => {
|
|
|
|
return {
|
|
|
|
display: leftHidden ? 'none' : undefined
|
|
|
|
};
|
|
|
|
}, [leftHidden]);
|
|
|
|
|
|
|
|
const memoizedRightPaneStyle = useMemo(() => {
|
|
|
|
return {
|
|
|
|
display: rightHidden ? 'none' : undefined
|
|
|
|
};
|
|
|
|
}, [rightHidden]);
|
|
|
|
|
|
|
|
const sashRender = useMemoizedFn((_: number, active: boolean) => (
|
|
|
|
<AppSplitterSash
|
|
|
|
hideSplitter={hideSplitter}
|
|
|
|
active={active}
|
|
|
|
splitterClassName={splitterClassName}
|
2025-01-10 03:04:57 +08:00
|
|
|
splitDirection={split}
|
2025-01-07 02:29:29 +08:00
|
|
|
/>
|
|
|
|
));
|
|
|
|
|
|
|
|
const onDragEnd = useMemoizedFn(() => {
|
|
|
|
setIsDragging(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
const onDragStart = useMemoizedFn(() => {
|
|
|
|
setIsDragging(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
const onChangePanels = useMemoizedFn((sizes: number[]) => {
|
|
|
|
if (!isDragging) return;
|
|
|
|
setSizes(sizes);
|
|
|
|
const key = createAutoSaveId(autoSaveId);
|
|
|
|
const sizesString = preserveSide === 'left' ? [sizes[0], 'auto'] : ['auto', sizes[1]];
|
|
|
|
Cookies.set(key, JSON.stringify(sizesString), { expires: 365 });
|
|
|
|
});
|
|
|
|
|
|
|
|
const onPreserveSide = useMemoizedFn(() => {
|
|
|
|
const [left, right] = sizes;
|
|
|
|
if (preserveSide === 'left') {
|
|
|
|
setSizes([left, 'auto']);
|
|
|
|
} else if (preserveSide === 'right') {
|
|
|
|
setSizes(['auto', right]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-01-10 03:04:57 +08:00
|
|
|
if (preserveSide && !hideSplitter && split === 'vertical') {
|
2025-01-07 02:29:29 +08:00
|
|
|
window.addEventListener('resize', onPreserveSide);
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener('resize', onPreserveSide);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, [preserveSide]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div ref={ref} className="h-full w-full">
|
|
|
|
<SplitPane
|
|
|
|
split={split}
|
|
|
|
className={`${className}`}
|
|
|
|
sizes={_sizes}
|
|
|
|
style={style}
|
|
|
|
allowResize={_allowResize}
|
|
|
|
onChange={onChangePanels}
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
resizerSize={3}
|
|
|
|
sashRender={sashRender}>
|
|
|
|
<Pane
|
|
|
|
style={memoizedLeftPaneStyle}
|
|
|
|
className="flex h-full flex-col"
|
|
|
|
minSize={leftPanelMinSize}
|
|
|
|
maxSize={leftPanelMaxSize}>
|
|
|
|
{leftHidden ? null : leftChildren}
|
|
|
|
</Pane>
|
|
|
|
<Pane
|
|
|
|
className="flex h-full flex-col"
|
|
|
|
style={memoizedRightPaneStyle}
|
|
|
|
minSize={rightPanelMinSize}
|
|
|
|
maxSize={rightPanelMaxSize}>
|
|
|
|
{rightHidden ? null : rightChildren}
|
|
|
|
</Pane>
|
|
|
|
</SplitPane>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
AppSplitter.displayName = 'AppSplitter';
|
|
|
|
|
|
|
|
const AppSplitterSash: React.FC<{
|
|
|
|
active: boolean;
|
|
|
|
splitterClassName?: string;
|
|
|
|
hideSplitter?: boolean;
|
2025-01-10 03:04:57 +08:00
|
|
|
splitDirection?: 'vertical' | 'horizontal';
|
|
|
|
}> = React.memo(
|
|
|
|
({ active, splitterClassName = '', hideSplitter = false, splitDirection = 'vertical' }) => {
|
|
|
|
const { styles, cx } = useStyles();
|
2025-01-07 02:29:29 +08:00
|
|
|
|
2025-01-10 03:04:57 +08:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={cx(
|
|
|
|
splitterClassName,
|
|
|
|
styles.splitter,
|
|
|
|
'absolute transition',
|
|
|
|
`cursor-${splitDirection}-resize`,
|
|
|
|
splitDirection === 'vertical' ? 'h-full w-[0.5px]' : 'h-[0.5px] w-full',
|
|
|
|
hideSplitter && 'hide',
|
|
|
|
active && 'active',
|
|
|
|
!active && 'inactive'
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
2025-01-07 02:29:29 +08:00
|
|
|
AppSplitterSash.displayName = 'AppSplitterSash';
|
2025-01-10 03:04:57 +08:00
|
|
|
|
|
|
|
const useStyles = createStyles(({ css, token }) => ({
|
|
|
|
splitter: css`
|
|
|
|
background: ${token.colorPrimary};
|
|
|
|
left: 1px;
|
|
|
|
|
|
|
|
&.hide {
|
|
|
|
background: transparent;
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
background: ${token.colorBorder};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&:not(.hide) {
|
|
|
|
&.active {
|
|
|
|
background: ${token.colorPrimary};
|
|
|
|
}
|
|
|
|
|
|
|
|
&.inactive {
|
|
|
|
background: ${token.colorBorder};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`
|
|
|
|
}));
|