File size: 6,336 Bytes
e5d8d3a 40400a1 e5d8d3a 40400a1 e5d8d3a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
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<any, unknown, any, any>;
sidebarBtn: d3.Selection<any, unknown, any, any>;
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<any, unknown, any, any>): 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);
}
}
|