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);
    }
}