components/ 拆为三层:
- providers/ — 状态管理(App/Theme/SettingsProvider)
- shell/ — 应用外壳(Layout/NavBar/PageDispatcher)
- ui/ — 通用 UI 原子(FloatingPanel/LegalTextModal)
pages/ 重命名 + 重组:
- headers/ → panels/ NavBar 触发的面板页
- login/ → auth/ 认证模块(含注册/忘记密码)
- settings/blocks/ → sections/
- home/components/ → panels/ features/ controls/ hooks/
按职责分:panels(布局壳) + features(功能面板) + controls(表单控件) + hooks
其他清理:
- 删除空占位 Test3.tsx
- HomeContext 从 HomeContent 拆出独立文件
- uploadValidation 内联到 ImageInput
- 修复过时注释(useUI → controls)
190 lines
5.9 KiB
TypeScript
190 lines
5.9 KiB
TypeScript
// ============================================================
|
||
// LeftPanel — 左列容器(模型选择 + 可拖拽分割线 + 任务搜索 + 任务记录)
|
||
// 纵向拖拽分隔线支持鼠标调整上下区域高度比例
|
||
//
|
||
// 职责:布局 + edition 分发,子组件自行管理数据和搜索状态
|
||
// ============================================================
|
||
|
||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||
import { theme as antTheme } from 'antd';
|
||
import { ModelSelector } from '../features/ModelSelector';
|
||
import { TaskHistory } from '../features/TaskHistory';
|
||
import { useModelList } from '@/hooks/use-api';
|
||
|
||
// ---------- 常量 ----------
|
||
|
||
/** 模型选择区最小高度 */
|
||
const TOP_MIN = 120;
|
||
/** 任务列表区最小高度 */
|
||
const BOTTOM_MIN = 200;
|
||
/** 默认模型选择区百分比 */
|
||
const TOP_DEFAULT_PCT = 0.40;
|
||
/** 拖拽分隔条高度 */
|
||
const DIVIDER_HEIGHT = 6;
|
||
|
||
// ---------- 组件 ----------
|
||
|
||
export function LeftPanel() {
|
||
const { token } = antTheme.useToken();
|
||
|
||
// 面板容器引用
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
||
// ======== 模型数据(提升到父组件,避免两个子组件重复请求) ========
|
||
const { data: models, loading, error, groupedModels, refetch } = useModelList();
|
||
|
||
// 模型选择区高度(像素)
|
||
const [topHeight, setTopHeight] = useState<number | null>(null);
|
||
// 拖拽状态
|
||
const [dragging, setDragging] = useState(false);
|
||
// hover 状态
|
||
const [hovered, setHovered] = useState(false);
|
||
|
||
const dragState = useRef<{
|
||
startY: number;
|
||
startHeight: number;
|
||
}>({ startY: 0, startHeight: 0 });
|
||
|
||
// 初始化:按百分比计算顶部高度
|
||
const initHeight = useCallback(() => {
|
||
const el = containerRef.current;
|
||
if (!el) return;
|
||
const total = el.clientHeight;
|
||
setTopHeight(Math.max(TOP_MIN, Math.round(total * TOP_DEFAULT_PCT)));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
initHeight();
|
||
}, [initHeight]);
|
||
|
||
// 响应容器 resize(窗口大小变化时重新按比例计算)
|
||
useEffect(() => {
|
||
const el = containerRef.current;
|
||
if (!el) return;
|
||
const observer = new ResizeObserver(() => {
|
||
if (!dragging) {
|
||
const total = el.clientHeight;
|
||
setTopHeight((prev) => {
|
||
if (prev === null) return Math.max(TOP_MIN, Math.round(total * TOP_DEFAULT_PCT));
|
||
// 保持原有比例
|
||
const ratio = prev / total;
|
||
return Math.max(TOP_MIN, Math.round(total * Math.min(ratio, 0.8)));
|
||
});
|
||
}
|
||
});
|
||
observer.observe(el);
|
||
return () => observer.disconnect();
|
||
}, [dragging]);
|
||
|
||
// 拖拽事件
|
||
const handleDividerMouseDown = (e: React.MouseEvent) => {
|
||
e.preventDefault();
|
||
dragState.current = { startY: e.clientY, startHeight: topHeight ?? 200 };
|
||
setDragging(true);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (!dragging) return;
|
||
|
||
const el = containerRef.current;
|
||
if (!el) return;
|
||
|
||
const total = el.clientHeight;
|
||
const ds = dragState.current;
|
||
const delta = e.clientY - ds.startY;
|
||
const newTop = Math.max(TOP_MIN, Math.min(total - BOTTOM_MIN, ds.startHeight + delta));
|
||
setTopHeight(newTop);
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
setDragging(false);
|
||
};
|
||
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
|
||
return () => {
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
};
|
||
}, [dragging]);
|
||
|
||
// 分隔条样式
|
||
const isActive = dragging;
|
||
const isHovered = hovered;
|
||
|
||
return (
|
||
<div
|
||
ref={containerRef}
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
height: '100%',
|
||
overflow: 'hidden',
|
||
borderRadius: 8,
|
||
border: `1px solid ${token.colorBorderSecondary}`,
|
||
}}
|
||
>
|
||
{/* 上方:模型选择(高度由拖拽控制) */}
|
||
<div
|
||
style={{
|
||
height: topHeight ?? '40%',
|
||
overflow: 'hidden',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<ModelSelector
|
||
models={models}
|
||
loading={loading}
|
||
error={error}
|
||
groupedModels={groupedModels}
|
||
refetch={refetch}
|
||
/>
|
||
</div>
|
||
|
||
{/* 纵向拖拽分隔条 */}
|
||
<div
|
||
onMouseDown={handleDividerMouseDown}
|
||
style={{
|
||
height: DIVIDER_HEIGHT,
|
||
flexShrink: 0,
|
||
cursor: 'row-resize',
|
||
background: isActive
|
||
? token.colorPrimary
|
||
: isHovered
|
||
? `${token.colorPrimary}60`
|
||
: token.colorBorderSecondary,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
transition: 'background 0.15s linear',
|
||
zIndex: 10,
|
||
}}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
>
|
||
{/* 拖拽手柄横线 */}
|
||
<div style={{
|
||
height: 2,
|
||
width: 32,
|
||
borderRadius: 1,
|
||
background: isActive
|
||
? '#fff'
|
||
: isHovered
|
||
? token.colorPrimary
|
||
: token.colorBorder,
|
||
transition: 'background 0.15s linear',
|
||
}} />
|
||
</div>
|
||
|
||
{/* 下方:任务记录(剩余高度) */}
|
||
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||
<TaskHistory allModels={models} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|