Files
ele-HeiXiu/src/pages/home/panels/LeftPanel.tsx
YoungestSongMo 0e6d996718 refactor: 重组 components/ 和 pages/ 文件结构,按职责分层
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)
2026-06-15 11:19:06 +08:00

190 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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>
);
}