import { useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react"; import { ChevronDown, ChevronUp, Square, SquareTerminal, Trash2, } from "lucide-react"; type DockedConsoleMode = "minimized" | "normal" | "maximized"; type DockedConsoleLevel = "info" | "success" | "warning" | "error"; type DockedConsoleEntry = { id: number; timestamp: string; level: DockedConsoleLevel; message: string; format: "message" | "xml"; }; type DockedConsoleProgress = { state: "idle" | "running" | "success" | "warning" | "error"; percent: number; message: string; startTime: number; simulatedTime: number; totalTime: number; }; type DockedSimulationConsoleProps = { entries: DockedConsoleEntry[]; mode: DockedConsoleMode; progress: DockedConsoleProgress; onClear: () => void; onModeChange: (mode: DockedConsoleMode) => void; onStop: () => void; stopPending: boolean; }; type ResizeSession = { pointerId: number; startHeight: number; startY: number; }; const CONSOLE_DOCK_HEIGHT_KEY = "system-simulation-flow:modeling-console-dock-height"; const DEFAULT_CONSOLE_DOCK_HEIGHT = 180; const MIN_CONSOLE_DOCK_HEIGHT = 120; const MAX_CONSOLE_DOCK_HEIGHT = 420; const MIN_MODELING_CANVAS_HEIGHT = 240; const COLLAPSED_CONSOLE_DOCK_HEIGHT = 40; const consoleLevelLabels: Record = { info: "信息", success: "完成", warning: "警告", error: "错误", }; function simulationProgressClockText(progress: DockedConsoleProgress) { const simulatedTime = Number.isFinite(progress.simulatedTime) ? progress.simulatedTime : progress.startTime; const totalTime = Number.isFinite(progress.totalTime) ? progress.totalTime : progress.startTime; return `${simulatedTime.toFixed(2)} s / ${totalTime.toFixed(2)} s`; } function maximumConsoleDockHeight(hostHeight = window.innerHeight) { return Math.max( MIN_CONSOLE_DOCK_HEIGHT, Math.min( MAX_CONSOLE_DOCK_HEIGHT, Math.round(hostHeight * 0.48), Math.max(MIN_CONSOLE_DOCK_HEIGHT, hostHeight - MIN_MODELING_CANVAS_HEIGHT), ), ); } function clampConsoleDockHeight(height: number, hostHeight = window.innerHeight) { return Math.min( maximumConsoleDockHeight(hostHeight), Math.max(MIN_CONSOLE_DOCK_HEIGHT, Math.round(height)), ); } function loadConsoleDockHeight() { try { const stored = Number(localStorage.getItem(CONSOLE_DOCK_HEIGHT_KEY)); return Number.isFinite(stored) && stored > 0 ? clampConsoleDockHeight(stored) : DEFAULT_CONSOLE_DOCK_HEIGHT; } catch { return DEFAULT_CONSOLE_DOCK_HEIGHT; } } function storeConsoleDockHeight(height: number) { try { localStorage.setItem(CONSOLE_DOCK_HEIGHT_KEY, String(height)); } catch { // 本地存储不可用时仍可在当前页面调整高度。 } } function collapsedSummary( entries: DockedConsoleEntry[], progress: DockedConsoleProgress, ) { if (progress.state === "running") { return `${progress.message} · ${simulationProgressClockText(progress)} · ${progress.percent}%`; } const latestEntry = entries.at(-1); if (!latestEntry) { return progress.message; } return latestEntry.format === "xml" ? "系统 XML 已生成" : latestEntry.message.replace(/\s+/g, " ").trim(); } export function DockedSimulationConsole({ entries, mode, progress, onClear, onModeChange, onStop, stopPending, }: DockedSimulationConsoleProps) { const dockRef = useRef(null); const logRef = useRef(null); const resizeSessionRef = useRef(null); const [height, setHeight] = useState(loadConsoleDockHeight); const [hostHeight, setHostHeight] = useState(() => window.innerHeight); const [resizing, setResizing] = useState(false); const collapsed = mode === "minimized"; const canStop = progress.state === "running" && !stopPending; const summary = useMemo( () => collapsedSummary(entries, progress), [entries, progress], ); useEffect(() => { if (!collapsed && logRef.current) { logRef.current.scrollTop = logRef.current.scrollHeight; } }, [collapsed, entries]); useEffect(() => { if (!collapsed) { storeConsoleDockHeight(height); } }, [collapsed, height]); useEffect(() => { const host = dockRef.current?.parentElement; const updateHostHeight = () => { const nextHostHeight = host?.clientHeight ?? window.innerHeight; setHostHeight(nextHostHeight); setHeight((current) => clampConsoleDockHeight(current, nextHostHeight)); }; updateHostHeight(); const observer = host && typeof ResizeObserver !== "undefined" ? new ResizeObserver(updateHostHeight) : null; if (host) { observer?.observe(host); } window.addEventListener("resize", updateHostHeight); return () => { observer?.disconnect(); window.removeEventListener("resize", updateHostHeight); }; }, []); const beginResize = (event: ReactPointerEvent) => { if (collapsed || event.button !== 0) { return; } event.preventDefault(); resizeSessionRef.current = { pointerId: event.pointerId, startHeight: height, startY: event.clientY, }; event.currentTarget.setPointerCapture(event.pointerId); setResizing(true); }; const resize = (event: ReactPointerEvent) => { const session = resizeSessionRef.current; if (!session || session.pointerId !== event.pointerId) { return; } event.preventDefault(); setHeight( clampConsoleDockHeight( session.startHeight + session.startY - event.clientY, hostHeight, ), ); }; const finishResize = (event: ReactPointerEvent) => { const session = resizeSessionRef.current; if (!session || session.pointerId !== event.pointerId) { return; } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } resizeSessionRef.current = null; setResizing(false); }; const consoleStyle = { "--simulation-console-dock-height": `${ collapsed ? COLLAPSED_CONSOLE_DOCK_HEIGHT : height }px`, } as CSSProperties; return ( ); }