2025-10-15 09:43:48 +08:00
|
|
|
import { useState, useRef, useEffect, ReactNode } from 'react';
|
|
|
|
|
import '../styles/ResizablePanel.css';
|
|
|
|
|
|
|
|
|
|
interface ResizablePanelProps {
|
|
|
|
|
direction: 'horizontal' | 'vertical';
|
|
|
|
|
leftOrTop: ReactNode;
|
|
|
|
|
rightOrBottom: ReactNode;
|
|
|
|
|
defaultSize?: number;
|
|
|
|
|
minSize?: number;
|
|
|
|
|
maxSize?: number;
|
|
|
|
|
side?: 'left' | 'right' | 'top' | 'bottom';
|
2025-10-15 20:26:40 +08:00
|
|
|
storageKey?: string;
|
2025-10-15 09:43:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ResizablePanel({
|
2025-11-02 23:50:41 +08:00
|
|
|
direction,
|
|
|
|
|
leftOrTop,
|
|
|
|
|
rightOrBottom,
|
|
|
|
|
defaultSize = 250,
|
|
|
|
|
minSize = 150,
|
|
|
|
|
maxSize = 600,
|
|
|
|
|
side = 'left',
|
|
|
|
|
storageKey
|
2025-10-15 09:43:48 +08:00
|
|
|
}: ResizablePanelProps) {
|
2025-11-02 23:50:41 +08:00
|
|
|
const getInitialSize = () => {
|
|
|
|
|
if (storageKey) {
|
|
|
|
|
const saved = localStorage.getItem(storageKey);
|
|
|
|
|
if (saved) {
|
|
|
|
|
const parsedSize = parseInt(saved, 10);
|
|
|
|
|
if (!isNaN(parsedSize)) {
|
|
|
|
|
return Math.max(minSize, Math.min(maxSize, parsedSize));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 20:26:40 +08:00
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
return defaultSize;
|
|
|
|
|
};
|
2025-10-15 20:26:40 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const [size, setSize] = useState(getInitialSize);
|
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (storageKey && !isDragging) {
|
|
|
|
|
localStorage.setItem(storageKey, size.toString());
|
|
|
|
|
}
|
|
|
|
|
}, [size, isDragging, storageKey]);
|
2025-10-15 20:26:40 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isDragging) return;
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
|
|
|
if (!containerRef.current) return;
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const rect = containerRef.current.getBoundingClientRect();
|
|
|
|
|
let newSize: number;
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
if (direction === 'horizontal') {
|
|
|
|
|
if (side === 'right') {
|
|
|
|
|
newSize = rect.right - e.clientX;
|
|
|
|
|
} else {
|
|
|
|
|
newSize = e.clientX - rect.left;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (side === 'bottom') {
|
|
|
|
|
newSize = rect.bottom - e.clientY;
|
|
|
|
|
} else {
|
|
|
|
|
newSize = e.clientY - rect.top;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
newSize = Math.max(minSize, Math.min(maxSize, newSize));
|
|
|
|
|
setSize(newSize);
|
|
|
|
|
};
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
};
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
|
|
|
document.addEventListener('mouseup', handleMouseUp);
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
|
};
|
|
|
|
|
}, [isDragging, direction, minSize, maxSize, side]);
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const handleMouseDown = () => {
|
|
|
|
|
setIsDragging(true);
|
|
|
|
|
};
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
const className = `resizable-panel resizable-panel-${direction}`;
|
|
|
|
|
const resizerClassName = `resizer resizer-${direction}`;
|
2025-10-15 09:43:48 +08:00
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
if (direction === 'horizontal') {
|
|
|
|
|
if (side === 'right') {
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className={className}>
|
|
|
|
|
<div className="panel-section" style={{ flex: 1 }}>
|
|
|
|
|
{leftOrTop}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={resizerClassName}
|
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
|
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="resizer-handle" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="panel-section" style={{ width: `${size}px` }}>
|
|
|
|
|
{rightOrBottom}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className={className}>
|
|
|
|
|
<div className="panel-section" style={{ width: `${size}px` }}>
|
|
|
|
|
{leftOrTop}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={resizerClassName}
|
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
|
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="resizer-handle" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="panel-section" style={{ flex: 1 }}>
|
|
|
|
|
{rightOrBottom}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-15 09:43:48 +08:00
|
|
|
} else {
|
2025-11-02 23:50:41 +08:00
|
|
|
if (side === 'bottom') {
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className={className}>
|
|
|
|
|
<div className="panel-section" style={{ flex: 1 }}>
|
|
|
|
|
{leftOrTop}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={resizerClassName}
|
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
|
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="resizer-handle" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="panel-section" style={{ height: `${size}px` }}>
|
|
|
|
|
{rightOrBottom}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className={className}>
|
|
|
|
|
<div className="panel-section" style={{ height: `${size}px` }}>
|
|
|
|
|
{leftOrTop}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={resizerClassName}
|
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
|
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="resizer-handle" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="panel-section" style={{ flex: 1 }}>
|
|
|
|
|
{rightOrBottom}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-15 09:43:48 +08:00
|
|
|
}
|
|
|
|
|
}
|