// ============================================================ // HomeContent — 首页三列正文布局 + HomeProvider(Context + Provider + Hook) // 可拖拽调整三栏宽度,所有数据请求在登录后发送 // ============================================================ import React, { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { theme as antTheme, Empty } from 'antd'; import { useAppContext } from '@/components/AppProvider'; import type { ModelsListResponseBody } from '@/services/modules/models'; import type { TaskInfoItemResponseBody } from '@/services/modules/task'; import { LeftPanel } from './components/LeftPanel'; import { CenterPanel } from './components/CenterPanel'; import { RightPanel } from './components/RightPanel'; import { Typography } from 'antd'; const { Text } = Typography; // ---------- Context 类型 ---------- export interface HomeContextValue { /** 当前选中的模型 */ selectedModel: ModelsListResponseBody | null; setSelectedModel: (model: ModelsListResponseBody | null) => void; /** 任务记录分页 — 当前页码 */ taskPage: number; /** 任务记录分页 — 每页条数 */ taskPageSize: number; setTaskPage: (page: number) => void; setTaskPageSize: (size: number) => void; /** 当前选中的任务记录(用于右侧预览) */ selectedTask: TaskInfoItemResponseBody | null; setSelectedTask: (task: TaskInfoItemResponseBody | null) => void; /** 侧边栏折叠状态 */ isLeftCollapsed: boolean; toggleLeftCollapsed: () => void; } export const HomeContext = createContext(null); // ---------- Consumer Hook ---------- export function useHomeContext(): HomeContextValue { const ctx = useContext(HomeContext); if (!ctx) { throw new Error('useHomeContext() 必须在 内部调用'); } return ctx; } // ---------- 常量 ---------- /** 左列最小/默认宽度 */ const LEFT_MIN = 400; const LEFT_DEFAULT = 750; /** 右列最小/默认宽度 */ const RIGHT_MIN = 300; const RIGHT_DEFAULT = 480; /** 中列最小宽度 */ const CENTER_MIN = 300; /** 拖拽分隔条宽度 */ const DIVIDER_WIDTH = 6; /** 布局固定消耗:左右 padding(12*2) + 两个分隔条(6*2) */ const LAYOUT_OVERHEAD = 36; // 24 + 12 // ---------- 组件 ---------- export function HomeContent() { const { token } = antTheme.useToken(); const { isLoggedIn } = useAppContext(); // ======== HomeContext 状态 ======== const [selectedModel, setSelectedModel] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [taskPage, setTaskPage] = useState(1); const [taskPageSize, setTaskPageSize] = useState(10); const [isLeftCollapsed, setIsLeftCollapsed] = useState(false); const toggleLeftCollapsed = useCallback(() => { setIsLeftCollapsed((v) => !v); }, []); const handleSetSelectedModel = useCallback( (model: ModelsListResponseBody | null) => { setSelectedModel(model); setSelectedTask(null); }, [], ); const handleSetSelectedTask = useCallback( (task: TaskInfoItemResponseBody | null) => { setSelectedTask(task); }, [], ); const contextValue = useMemo(() => ({ selectedModel, setSelectedModel: handleSetSelectedModel, taskPage, taskPageSize, setTaskPage, setTaskPageSize, selectedTask, setSelectedTask: handleSetSelectedTask, isLeftCollapsed, toggleLeftCollapsed, }), [ selectedModel, handleSetSelectedModel, taskPage, taskPageSize, selectedTask, handleSetSelectedTask, isLeftCollapsed, toggleLeftCollapsed, ]); // ======== 拖拽调整列宽 ======== const [leftWidth, setLeftWidth] = useState(LEFT_DEFAULT); const [rightWidth, setRightWidth] = useState(RIGHT_DEFAULT); const containerRef = useRef(null); // 拖拽状态 const [dragging, setDragging] = useState<'left' | 'right' | null>(null); // 分隔条 hover 状态 const [hoveredDivider, setHoveredDivider] = useState<'left' | 'right' | null>(null); const dragState = useRef<{ startX: number; startWidth: number; }>({ startX: 0, startWidth: 0 }); // 用 ref 追踪最新列宽,避免拖拽 mousemove 闭包中读到过期值 const leftWidthRef = useRef(leftWidth); const rightWidthRef = useRef(rightWidth); const draggingRef = useRef(dragging); useEffect(() => { leftWidthRef.current = leftWidth; rightWidthRef.current = rightWidth; draggingRef.current = dragging; }, [leftWidth, rightWidth, dragging]); const handleDividerMouseDown = (side: 'left' | 'right') => (e: React.MouseEvent) => { e.preventDefault(); // eslint-disable-next-line react-hooks/refs dragState.current = { startX: e.clientX, startWidth: side === 'left' ? leftWidth : rightWidth }; setDragging(side); }; useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!dragging) return; const containerEl = containerRef.current; if (!containerEl) return; const containerWidth = containerEl.clientWidth; const ds = dragState.current; const delta = e.clientX - ds.startX; if (dragging === 'left') { // 左列最大宽度 = 容器宽 - 固定消耗 - 中列最小宽 - 右列当前宽 // 确保中列至少保留 CENTER_MIN、右列不被挤出视口 const maxLeft = containerWidth - LAYOUT_OVERHEAD - CENTER_MIN - rightWidthRef.current; const newWidth = Math.max(LEFT_MIN, Math.min(maxLeft, ds.startWidth + delta)); setLeftWidth(newWidth); } else { // 右列最大宽度同理:不能把中列挤到 CENTER_MIN 以下 const maxRight = containerWidth - LAYOUT_OVERHEAD - CENTER_MIN - leftWidthRef.current; const newWidth = Math.max(RIGHT_MIN, Math.min(maxRight, ds.startWidth - delta)); setRightWidth(newWidth); } }; const handleMouseUp = () => { setDragging(null); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [dragging]); // ======== 视口缩小自动收窄面板 ======== useEffect(() => { const el = containerRef.current; if (!el) return; const observer = new ResizeObserver(() => { // 拖拽中由 mousemove handler 处理约束,避免冲突 if (draggingRef.current) return; const containerWidth = el.clientWidth; const currentLeft = leftWidthRef.current; const currentRight = rightWidthRef.current; // 检查当前总宽度是否超出容器 const totalNeeded = currentLeft + LAYOUT_OVERHEAD + CENTER_MIN + currentRight; if (totalNeeded <= containerWidth) return; // 超出容器 → 优先收缩右侧面板,再收左侧 const deficit = totalNeeded - containerWidth; const rightSlack = currentRight - RIGHT_MIN; if (rightSlack >= deficit) { setRightWidth(currentRight - deficit); } else { setRightWidth(RIGHT_MIN); const remainingDeficit = deficit - rightSlack; setLeftWidth(Math.max(LEFT_MIN, currentLeft - remainingDeficit)); } }); observer.observe(el); return () => observer.disconnect(); }, []); // ======== 分隔条样式工厂 ======== const dividerStyle = (side: 'left' | 'right'): React.CSSProperties => { const isActive = dragging === side; const isHovered = hoveredDivider === side; return { width: DIVIDER_WIDTH, flexShrink: 0, cursor: 'col-resize', background: isActive ? token.colorPrimary : isHovered ? `${token.colorPrimary}60` : token.colorBorderSecondary, position: 'relative' as const, zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.15s linear', }; }; // ======== 未登录提示 ======== if (!isLoggedIn) { return (
请先登录后再使用首页功能 } image={Empty.PRESENTED_IMAGE_SIMPLE} />
); } // ======== 三列布局(可拖拽) ======== return (
{/* 左列 */} {!isLeftCollapsed && ( <>
{/* 左分隔条 */}
setHoveredDivider('left')} onMouseLeave={() => setHoveredDivider(null)} > {/* 拖拽手柄竖线 */}
)} {/* 中列 */}
{/* 右分隔条 */}
setHoveredDivider('right')} onMouseLeave={() => setHoveredDivider(null)} > {/* 拖拽手柄竖线 */}
{/* 右列 */}
); }