vue实现多栏布局(拖拽改变盒子的宽度和高度)


项目中要实现分栏布局效果的需求,各区域的宽高可以随意改变(拖拽改变盒子的宽度和高度),并且每个区域可以实现最大化/最小化,并且底部实现类似vscode中的分栏图标按钮,点击随意折叠/展开每个区域。

 直接看效果

在CSDN中查找到,vue-split-pane组件,由于我有定制化的需求,就没有直接npm install vue-splitpane使用,而是重新写了一下。如果要了解vue-split-pane组件,源码链接:

GitHub - PanJiaChen/vue-split-pane: Split-Pane component built with vue2.0, can be split vertically or horizontally. http://panjiachen.github.io/split-pane/demo/index.html

demo的github地址:GitHub - MonkeyKingYY/vue-split-pane-demo

下面直接放代码:

APP.vue 

<template>
    <div id="app" class="app-main-contain">
        <div class="center-contain" style="width: 100%; height: calc(100% - 25px)">
            <SplitPane @resize="leftResize" :default-percent="leftPercent" split="vertical">
                <template slot="paneL">
                    <div class="title-contain" v-show="leftPercent > 0">
                        <p>资源导航</p>
                        <i class="iconfont icon-zuidahua" v-show="leftPercent > 0 && leftPercent < 100" @click="leftMaxFunc()"></i>
                        <i class="iconfont icon-zuixiaohua" v-show="leftPercent >= 100" @click="leftMinFunc()"></i>
                    </div>
                    1111111
                </template>
                <template slot="paneR">
                    <SplitPane split="vertical" @resize="rightResize" :default-percent="rightPercent">
                        <template slot="paneL">
                            <SplitPane split="horizontal" @resize="topBottomResize" :default-percent="topPercent">
                                <template slot="paneL">
                                    <div class="title-contain" v-show="topPercent > 0 && rightPercent > 0">
                                        <p>查询编辑器</p>
                                        <i class="iconfont icon-zuidahua" v-show="leftPercent !== 100 && rightPercent !== 100" @click="topMaxFunc()"></i>
                                        <i class="iconfont icon-zuixiaohua" v-show="leftPercent == 0 && topPercent == 100 && rightPercent == 100" @click="topMinFunc()"></i>
                                    </div>
                                    <div style="height: calc(100% - 25px)">2222222</div>
                                </template>
                                <template slot="paneR">
                                    <div class="title-contain" v-show="topPercent < 100 && rightPercent > 0">
                                        <p>结果区</p>
                                        <i class="iconfont icon-zuidahua" v-show="leftPercent !== 100 && topPercent !== 0 && rightPercent !== 0" @click="bottomMaxFunc()"></i>
                                        <i class="iconfont icon-zuixiaohua" v-show="leftPercent == 0 && topPercent == 0 && rightPercent >= 100" @click="bottomMinFunc()"></i>
                                    </div>
                                    <div v-show="topPercent < 100 && rightPercent > 0" style="height: calc(100% - 25px)">3333333</div>
                                </template>
                            </SplitPane>
                        </template>
                        <template slot="paneR">
                            <div class="title-contain" v-show="rightPercent < 100">
                                <p>辅助区</p>
                                <i class="iconfont icon-zuidahua" v-show="leftPercent !== 100 && rightPercent !== 0" @click="rightMaxFunc()"></i>
                                <i class="iconfont icon-zuixiaohua" v-show="leftPercent == 0 && rightPercent == 0" @click="rightMinFunc()"></i>
                            </div>
                            <div v-show="rightPercent < 100" style="height: calc(100% - 25px)">444444</div>
                        </template>
                    </SplitPane>
                </template>
            </SplitPane>
        </div>
        <div class="buttom-contain">
            <BottomTaskBar :rightPercent="rightPercent" :leftPercent="leftPercent" :topPercent="topPercent" @leftClose="leftClose" @rightClose="rightClose" @bottomClose="bottomClose" @openBottom="openBottom" @openLeft="openLeft" @openRight="openRight"></BottomTaskBar>
        </div>
    </div>
</template>

<script>
    import SplitPane from './components/SplitPane.vue'

    export default {
        name: 'index',
        components: {
            SplitPane,
            BottomTaskBar: () => import('./components/BottomTaskBar.vue'),
        },
        created() {
            this.leftPercent = parseInt(localStorage.getItem('leftPercent') || 20)
            this.rightPercent = parseInt(localStorage.getItem('rightPercent') || 70)
            this.topPercent = parseInt(localStorage.getItem('topPercent') || 70)
        },
        data() {
            return {
                GLayoutRoot: null,
                leftPercent: 20,
                rightPercent: 70,
                topPercent: 70,
                oldLeftPercent: 20,
                oldRightPercent: 70,
                oldTopPercent: 70,
            }
        },
        watch: {
            leftPercent: {
                handler(percent) {
                    console.log('watchleftPercent', percent)
                    localStorage.setItem('leftPercent', percent)
                },
                deep: true,
            },
            rightPercent: {
                handler(percent) {
                    console.log('watchrightPercent', percent)
                    localStorage.setItem('rightPercent', percent)
                },
                deep: true,
            },
            topPercent: {
                handler(percent) {
                    console.log('watchtopPercent', percent)
                    localStorage.setItem('topPercent', percent)
                },
                deep: true,
            },
        },
        methods: {
            leftResize(percent) {
                this.oldLeftPercent = percent
                this.leftPercent = percent
            },
            rightResize(percent) {
                this.oldRightPercent = percent
                this.rightPercent = percent
            },
            topBottomResize(percent) {
                this.oldTopPercent = percent
                this.topPercent = percent
            },
            leftClose() {
                this.leftPercent = 0
            },
            rightClose() {
                this.rightPercent = 100
            },
            bottomClose() {
                this.topPercent = 100
            },
            openBottom() {
                this.topPercent = this.oldTopPercent
            },
            openLeft() {
                this.leftPercent = this.oldLeftPercent
            },
            openRight() {
                this.rightPercent = this.oldRightPercent
            },
            leftMaxFunc() {
                this.leftPercent = 100
            },
            leftMinFunc() {
                this.leftPercent = this.oldLeftPercent
            },
            topMaxFunc() {
                this.topPercent = 100
                this.leftPercent = 0
                this.rightPercent = 100
            },
            topMinFunc() {
                this.topPercent = this.oldTopPercent
                this.leftPercent = this.oldLeftPercent
                this.rightPercent = this.oldRightPercent
            },
            bottomMaxFunc() {
                this.topPercent = 0
                this.leftPercent = 0
                this.rightPercent = 100
            },
            bottomMinFunc() {
                this.topPercent = this.oldTopPercent
                this.leftPercent = this.oldLeftPercent
                this.rightPercent = this.oldRightPercent
            },
            rightMaxFunc() {
                this.leftPercent = 0
                this.rightPercent = 0
            },
            rightMinFunc() {
                this.topPercent = this.oldTopPercent
                this.leftPercent = this.oldLeftPercent
                this.rightPercent = this.oldRightPercent
            },
        },
    }
</script>

<style scoped>
    .app-main-contain {
        height: 100%;
        display: flex;
        flex-direction: column;
    }

    .center-contain .title-contain {
        display: flex;
        align-items: center;
        padding: 0 10px 0 10px;
        justify-content: space-between;
        background-color: #eee;
    }

    .center-contain .title-contain p {
        margin: 0;
        height: 25px;
        line-height: 25px;
        font-size: 12px;
    }

    .center-contain .title-contain i {
        font-size: 14px;
        cursor: pointer;
    }

    .app-main-contain .buttom-contain {
        position: fixed;
        bottom: 0;
        height: 25px;
        width: 100%;
        background-color: #0074c8;
        z-index: 999999;
    }
</style>

SplitPane.vue:

<template>
    <div :style="{ cursor, userSelect }" class="vue-splitter-container clearfix" @mouseup="onMouseUp" @mousemove="onMouseMove">
        <Pane class="splitter-pane splitter-paneL" :split="split" :style="{ [type]: percent + '%' }">
            <slot name="paneL"></slot>
        </Pane>

        <Resizer
            :className="className"
            :style="{ [resizeType]: percent + '%' }"
            :split="split"
            @mousedown.native="onMouseDown"
            @click.native="onClick"
        ></Resizer>

        <Pane class="splitter-pane splitter-paneR" :split="split" :style="{ [type]: 100 - percent + '%' }">
            <slot name="paneR"></slot>
        </Pane>
        <div class="vue-splitter-container-mask" v-if="active"></div>
    </div>
</template>

<script>
import Resizer from './Resizer.vue'
import Pane from './Pane.vue'

export default {
    name: 'splitPane',
    components: { Resizer, Pane },
    props: {
        minPercent: {
            type: Number,
            default: 0,
        },
        defaultPercent: {
            type: Number,
            default: 50,
        },
        split: {
            validator(value) {
                return ['vertical', 'horizontal'].indexOf(value) >= 0
            },
            required: true,
        },
        className: String,
    },
    computed: {
        userSelect() {
            return this.active ? 'none' : ''
        },
        cursor() {
            return this.active ? (this.split === 'vertical' ? 'col-resize' : 'row-resize') : ''
        },
    },
    watch: {
        defaultPercent(newValue, oldValue) {
            console.log(oldValue)
            this.percent = newValue
        },
    },
    data() {
        return {
            active: false,
            hasMoved: false,
            height: null,
            percent: this.defaultPercent,
            type: this.split === 'vertical' ? 'width' : 'height',
            resizeType: this.split === 'vertical' ? 'left' : 'top',
            firstTime: 0,
            lastTime: 0,
            key: false,
        }
    },
    methods: {
        onClick() {
            console.log(111)
            if (this.key) {
                this.percent = 50
                this.$emit('resize', this.percent)
                this.key = false
            }
        },
        onMouseDown() {
            console.log('mouseDown')
            this.firstTime = new Date().getTime()
            this.active = true
            this.hasMoved = false
        },
        onMouseUp() {
            console.log('mouseUp')
            this.active = false
            this.lastTime = new Date().getTime()
            if (this.lastTime - this.firstTime < 500) {
                this.key = true
                console.log(this.key)
            }
        },
        onMouseMove(e) {
            if (e.buttons === 0 || e.which === 0) {
                this.active = false
            }

            if (this.active) {
                let offset = 0
                let target = e.currentTarget
                if (this.split === 'vertical') {
                    while (target) {
                        offset += target.offsetLeft
                        target = target.offsetParent
                    }
                } else {
                    while (target) {
                        offset += target.offsetTop
                        target = target.offsetParent
                    }
                }

                const currentPage = this.split === 'vertical' ? e.pageX : e.pageY
                const targetOffset = this.split === 'vertical' ? e.currentTarget.offsetWidth : e.currentTarget.offsetHeight
                const percent = Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100

                if (percent > this.minPercent && percent < 100 - this.minPercent) {
                    this.percent = percent
                }

                this.$emit('resize', this.percent)
                this.hasMoved = true
            }
        },
    },
}
</script>

<style scoped>
.clearfix:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: ' ';
    clear: both;
    height: 0;
}

.vue-splitter-container {
    height: 100%;
    position: relative;
}

.vue-splitter-container-mask {
    z-index: 9999;
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
}
</style>

 Resizer.vue:

<template>
    <div :class="classes"></div>
</template>

<script>
  export default {
    props: {
      split: {
        validator(value) {
          return ['vertical', 'horizontal'].indexOf(value) >= 0
        },
        required: true
      },
      className: String
    },
    computed: {
      classes() {
        const classes = ['splitter-pane-resizer', this.split, this.className]
        return classes.join(' ')
      }
    }
  }
</script>

<style scoped>
.splitter-pane-resizer {
  -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    background: #000;
    position: absolute;
    opacity: .2;
    z-index: 1;
    -moz-background-clip: padding;
    -webkit-background-clip: padding;
    background-clip: padding-box;
}

.splitter-pane-resizer.horizontal {
  height: 11px;
    margin: -5px 0;
    border-top: 5px solid rgba(255, 255, 255, 0);
    border-bottom: 5px solid rgba(255, 255, 255, 0);
    cursor: row-resize;
    width: 100%;
}

.splitter-pane-resizer.vertical {
  width: 11px;
    height: 100%;
    margin-left: -5px;
    border-left: 5px solid rgba(255, 255, 255, 0);
    border-right: 5px solid rgba(255, 255, 255, 0);
    cursor: col-resize;
}
</style>

Pane.vue:

<template>
    <div :class="classes">
        <slot></slot>
    </div>
</template>

<script>
  export default {
    name: 'Pane',
    props: {
      className: String
    },
    data() {
      const classes = [this.$parent.split, this.className]
      return {
        classes: classes.join(' '),
        percent: 50
      }
    }
  }
</script>

<style scoped>
.splitter-pane.vertical.splitter-paneL {
  position: absolute;
  left: 0px;
  height: 100%;
  padding-right: 3px;
}

.splitter-pane.vertical.splitter-paneR {
  position: absolute;
  right: 0px;
  height: 100%;
  padding-left: 3px;
}

.splitter-pane.horizontal.splitter-paneL {
  position: absolute;
  top: 0px;
  width: 100%;
}

.splitter-pane.horizontal.splitter-paneR {
  position: absolute;
  bottom: 0px;
  width: 100%;
  padding-top: 3px;
}
</style>

BottomTaskBar.vue:

<template>
    <div id="BottomTaskBar">
        <div class="img-contain">
            <img @click="leftClose" v-if="leftPercent !== 0" src="../assets/bottomTask/left_show.png" />
            <img @click="openLeft" v-if="leftPercent == 0" src="../assets/bottomTask/left_hidden.png" />
            <img @click="bottomClose" v-if="topPercent !== 100" src="../assets/bottomTask/bottom_show.png" />
            <img @click="openBottom" v-if="topPercent == 100" src="../assets/bottomTask/bottom_hidden.png" />
            <img @click="rightClose" v-if="rightPercent !== 100" src="../assets/bottomTask/right_show.png" style="margin-bottom: 1px" />
            <img @click="openRight" v-if="rightPercent == 100" src="../assets/bottomTask/right_hidden.png" />
        </div>
    </div>
</template>
<script>
export default {
    props: {
        leftPercent: {
            type: Number,
        },
        rightPercent: {
            type: Number,
        },
        topPercent: {
            type: Number,
        },
    },
    data() {
        return {}
    },
    methods: {
        leftClose() {
            this.$emit('leftClose')
        },
        rightClose() {
            this.$emit('rightClose')
        },
        bottomClose() {
            this.$emit('bottomClose')
        },
        openBottom() {
            this.$emit('openBottom')
        },
        openLeft() {
            this.$emit('openLeft')
        },
        openRight() {
            this.$emit('openRight')
        },
    },
}
</script>
<style scoped>
#BottomTaskBar {
    color: #fff;
    padding: 0 20px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
}

#BottomTaskBar p {
    margin: 0;
    height: 25px;
    line-height: 25px;
}
#BottomTaskBar .img-contain img {
    width: 20px;
    height: 20px;
    cursor: pointer;
    margin-right: 10px;
}
</style>

注意⚠️:静态资源包括图标和图标就不上传了,如果需要运行代码,可以直接从github上clone下来运行即可。关于这种类似vscode的分栏布局,曾经是让我很头疼的需求,解决了之后很开心。如果能帮助大家解决自己的问题,我会更开心😊。最后再次感谢这个插件:GitHub - PanJiaChen/vue-split-pane: Split-Pane component built with vue2.0, can be split vertically or horizontally. http://panjiachen.github.io/split-pane/demo/index.html

 如果帮助到您了,可以留下一个赞👍告诉我  


评论