import * as d3 from 'd3'; import { isNarrowScreen } from '../utils/responsive'; export type LayoutState = { sidebar: { width: number; visible: boolean; }; }; export type LayoutControllerOptions = { sidebarState: LayoutState['sidebar']; sideBar: d3.Selection; sidebarBtn: d3.Selection; onSidebarToggle?: (visible: boolean) => void; onLayoutChange?: () => void; }; export class LayoutController { private options: LayoutControllerOptions; private isResizing = false; private startX = 0; private startWidth = 0; private leftPanelRatio = 0.5; // 左侧面板的比例,默认50% constructor(options: LayoutControllerOptions) { this.options = options; this.initialize(); } private initialize(): void { this.setupSidebar(); this.setupWindowResize(); this.setupPanelResizer(); this.reLayout(window.innerWidth, window.innerHeight); } private setupSidebar(): void { this.options.sidebarBtn.on('click', () => { const sb = this.options.sidebarState; sb.visible = !sb.visible; this.options.sidebarBtn.classed('on', sb.visible); this.options.sideBar.classed('hidden', !sb.visible); this.options.sideBar.style('right', sb.visible ? null : `-${this.options.sidebarState.width}px`); if (this.options.onSidebarToggle) { this.options.onSidebarToggle(sb.visible); } this.reLayout(); }); } private setupWindowResize(): void { window.onresize = () => { const w = window.innerWidth; const h = window.innerHeight; this.reLayout(w, h); if (this.options.onLayoutChange) { this.options.onLayoutChange(); } }; } public reLayout(w = window.innerWidth, h = window.innerHeight): void { d3.selectAll('.sidenav') .style('height', (h - 53) + 'px'); const sb = this.options.sidebarState; const mainWidth = w - (sb.visible ? sb.width : 0); // 检测是否是移动端/窄屏模式 const isMobile = isNarrowScreen(); const mainFrame = d3.selectAll('.main_frame'); if (isMobile) { // 移动端:不设置固定高度,让CSS的height: auto生效,允许body滚动 mainFrame .style('height', null) // 移除内联样式,让CSS生效 .style('width', null); // 移除内联宽度样式,让CSS生效 } else { // 桌面端:设置固定高度 mainFrame .style('height', (h - 53) + 'px') .style('width', mainWidth + 'px'); // 根据保存的比例重新计算左侧面板宽度 this.updateLeftPanelWidth(mainWidth); } } /** * 根据当前窗口宽度和保存的比例更新左侧面板宽度 */ private updateLeftPanelWidth(containerWidth: number): void { const leftPanel = d3.select('.left_panel'); if (leftPanel.empty()) return; // 计算可用宽度(减去分割线宽度8px) const availableWidth = containerWidth - 8; // 根据比例计算左侧面板宽度 const leftWidth = availableWidth * this.leftPanelRatio; // 确保宽度在最小和最大限制内 const minWidth = containerWidth * 0.1; const maxWidth = containerWidth * 0.9; const clampedWidth = Math.max(minWidth, Math.min(maxWidth, leftWidth)); // 更新比例(如果被限制,则更新比例以保持一致性) this.leftPanelRatio = clampedWidth / availableWidth; leftPanel.style('flex-basis', clampedWidth + 'px'); } private setupPanelResizer(): void { const resizer = d3.select('#resizer'); const leftPanel = d3.select('.left_panel'); // 初始化左侧面板宽度(使用默认比例50%) const sb = this.options.sidebarState; const mainWidth = window.innerWidth - (sb.visible ? sb.width : 0); this.updateLeftPanelWidth(mainWidth); resizer.on('mousedown', (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); this.isResizing = true; this.startX = event.clientX; // 获取当前左侧面板的实际宽度 const currentFlexBasis = leftPanel.style('flex-basis'); this.startWidth = parseInt(currentFlexBasis) || (mainWidth * this.leftPanelRatio); d3.select('body') .style('cursor', 'col-resize') .style('user-select', 'none'); d3.select(window) .on('mousemove.resizer', (ev: MouseEvent) => this.handleMouseMove(ev, leftPanel)) .on('mouseup.resizer', () => this.handleMouseUp()); }); } private handleMouseMove(event: MouseEvent, leftPanel: d3.Selection): void { if (!this.isResizing) return; event.preventDefault(); const sb = this.options.sidebarState; const containerWidth = window.innerWidth - (sb.visible ? sb.width : 0); const availableWidth = containerWidth - 8; // 减去分割线宽度 const deltaX = event.clientX - this.startX; const newWidth = Math.max( containerWidth * 0.1, Math.min(containerWidth * 0.9, this.startWidth + deltaX) ); // 更新左侧面板宽度 leftPanel.style('flex-basis', newWidth + 'px'); // 更新保存的比例 this.leftPanelRatio = newWidth / availableWidth; } private handleMouseUp(): void { if (!this.isResizing) return; this.isResizing = false; d3.select('body') .style('cursor', 'default') .style('user-select', 'auto'); d3.select(window) .on('mousemove.resizer', null) .on('mouseup.resizer', null); } }