概述
开发中,某些时候会用到将内容区域折叠/展开面板,根据多数组件库的Collapse组件的使用,来自己实现一个Collapse组件,这个组件还和组件库中的menu组件的实现类似,实现这个组件对于menu组件的实现,差别就不大了。
最终效果
动图有些大,稍等下就能加载出来
实现功能点和原理
- 通过多数组件库的调研,Collapse组件需要提供的功能主要包括:
- 菜单的折叠动画效果
- 具备手风琴和普通的折叠效果
- 能监听到展开项的变化
- 组件的结构
- Collapse包含Collapse组件和Panel组件,一个外层包裹容器,一个菜单项组件。
- 对于折叠动画的实现我们需要手动自己封装transion组件,来实现元素隐藏的过渡效果(这里可以参阅vue-design组件库和element),他们对于组件折叠的过渡效果动画封装思路一致。
具体实现
文件结构
TransitionCollapse.vue
这个组件主要增强内置组件transition的功能,实现多数菜单折叠的过度
<template>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
>
<slot></slot>
</transition>
</template>
<script>
export default {
methods: {
beforeEnter(el) {
el.classList.add("collapse-transition");
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.overflow = "hidden";
el.style.height = "0";
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
enter(el) {
el.style.height = el.scrollHeight + "px";
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
},
afterEnter(el) {
el.classList.remove("collapse-transition");
el.style.height = "";
el.style.overflow = el.dataset.oldOverflow;
},
beforeLeave(el) {
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.height = el.scrollHeight + "px";
el.style.overflow = "hidden";
},
leave(el) {
el.classList.add("collapse-transition");
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
afterLeave(el) {
el.classList.remove("collapse-transition");
el.style.height = "";
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
},
},
};
</script>
<style lang="less">
.collapse-transition {
transition: all 0.3s ease-in-out;
}
</style>
Collapse.vue
<template>
<div class="g-collapse">
<slot></slot>
</div>
</template>
<script>
export default {
props: {
// 是否手风琴效果
accordion: {
type: Boolean,
default: false,
},
// v-model绑定的值,具体展开哪一个面板
value: {
type: [String, Array],
},
},
// 将当前实例注入到子组件
provide() {
return {
collapseInstace: this,
};
},
data() {
return {
// panel组件实例数组
childrenVnode: [],
//选中的value集合
activeList: [],
};
},
methods: {
// 每项点击的时候,处理函数
toggle(panelInstance) {
// 如果是手风琴开启,那么只展开当前项,其他的关闭
if (this.accordion) {
if (this.activeList.indexOf(panelInstance.name) > -1) {
this.activeList = [];
} else {
this.activeList = [panelInstance.name];
}
} else {
// 处理手风琴关闭的时候,点击的时候将状态置反
if (this.activeList.indexOf(panelInstance.name) == -1) {
this.activeList.push(panelInstance.name);
} else {
for (let i = 0; i < this.activeList.length; i++) {
if (this.activeList[i] == panelInstance.name) {
this.activeList.splice(i, 1);
break;
}
}
}
}
// 发布事件处理函数,便于用户检测当前活跃项变更(v-mode和@change)
this.$emit("input", this.activeList);
this.$emit("change", this.activeList);
},
// 设置子组件的折叠状态
setPanelInstanceStauts() {
const panelInstanceComponent = this.childrenVnode;
for (let i = 0; i < panelInstanceComponent.length; i++) {
if (this.activeList.indexOf(panelInstanceComponent[i].child.name) > -1) {
panelInstanceComponent[i].child.status = true;
} else {
panelInstanceComponent[i].child.status = false;
}
}
},
// 设置活跃集合
setValueActiveList() {
if (!Array.isArray(this.value)) {
this.activeList = [this.value];
}
},
},
mounted() {
// 保存子组件的实例
this.childrenVnode = this.$slots.default;
this.setValueActiveList();
},
watch: {
// value变化的时候,将其转化成数组形式
value(){
this.setValueActiveList();
},
// 活跃项变化,设置子菜单折叠状态
activeList() {
this.setPanelInstanceStauts();
},
},
};
</script>
<style lang="less">
.g-collapse {
border-bottom: 1px solid #dcdee2;
}
</style>
Panel.vue
<template>
<div class="g-collapse-panel-item" @click.stop="handlePanelItemClick">
<div class="title">
<i :class="['arrow', status ? 'arrow-open' : '']"></i>
<slot></slot>
</div>
<transition-collapse>
<div class="content" v-show="status">
<div class="inner-content">
<slot name="content"></slot>
</div>
</div>
</transition-collapse>
</div>
</template>
<script>
import { TransitionCollapse } from "../TransitionCollapse";
export default {
// 父组件实例
inject: ["collapseInstace"],
props: {
// 当前面板标识
name: {
type: [String, Number],
},
},
data() {
return {
status: false,
};
},
components: { TransitionCollapse },
methods: {
// 点击折叠状态交给父组件处理
handlePanelItemClick() {
this.$parent.toggle(this);
},
// 根据初始状态的value,设置展开项
init() {
if (this.name == this.collapseInstace.value) {
this.status = true;
}
},
},
mounted() {
// 初始化
this.init();
},
};
</script>
<style lang="less">
.g-collapse-panel-item {
margin-bottom: -1px;
.title {
height: 38px;
line-height: 38px;
padding-left: 36px;
color: #666;
cursor: pointer;
position: relative;
border: 1px solid #dcdee2;
background-color: #f7f7f7;
}
.content {
padding: 16px 36px;
border-left: 1px solid #dcdee2;
border-right: 1px solid #dcdee2;
}
.arrow {
width: 10px;
height: 10px;
border: 1px solid #666;
border-left: none;
border-bottom: none;
left: 10px;
top: 50%;
transform: translateY(-50%) rotate(45deg);
transform-origin: center center;
transition: transform 0.3s;
position: absolute;
}
.arrow-open {
transform: translateY(-50%) rotate(135deg);
}
}
</style>
index.js
按需导出组件
import Collapse from "./Collapse.vue";
import Panel from "./Panel.vue";
import TransitionCollapse from "./TransitionCollapse.vue";
export { Panel, Collapse,TransitionCollapse };
总结
对于Collapse的实现,有助于对vue中transition组件的理解,同时,对于后续menu组件的封装,我们可以打好基础知识。