在前端开发中,组件库的使用能够极大地提高开发效率,ElementPlus 就是一款非常受欢迎的 Vue 组件库。今天我们来探讨如何模仿 ElementPlus 实现 Collapse 组件,并且实现普通模式以及手风琴效果模式。
一、什么是 Collapse 组件
Collapse 组件,也叫折叠面板组件,通常用于展示一些内容区域,这些区域在初始状态下可以被折叠起来,当用户点击标题时展开显示详细内容。这种组件在信息展示和页面布局中非常实用,能够有效节省页面空间,提升用户体验。
二、实现 Collapse 组件
(一)组件目录
components
├── Collapse
├── Collapse.vue
├── CollapseItem.vue
├── style.css
├── types.ts
(二)编写Collapse组件及折叠功能
Collapse.vue
<template>
<div class="yl-collapse">
<slot> </slot>
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue'
import type { NameType, CollapseProps, CollapseEmits } from './types'
import { collapseContextKey } from './types'
defineOptions({
name: 'YlCollapse',
})
const props = defineProps<CollapseProps>()
const emits = defineEmits<CollapseEmits>()
const activeNames = ref<NameType[]>(props.modelValue)
const handleItemClick = (item: NameType) => {
const index = activeNames.value.indexOf(item)
if (index > -1) {
//存在则删除数组中对应一项
activeNames.value.splice(index, 1)
} else {
//不存在,插入对应的name
activeNames.value.push(item)
}
emits('update:modelValue', activeNames.value)
emits('update:change', activeNames.value)
}
provide(collapseContextKey, {
activeNames,
handleItemClick,
})
</script>
CollapseItem.vue
<template>
<div
class="yl-collapse-item"
:class="{
'is-disabled': disabled,
}"
>
<div class="yl-collapse-item__header" :id="`item-header-${name}`" @click="handleClick">
<slot name="title"> {{ title }}</slot>
</div>
<div class="yl-collapse-item__content" :id="`item-content-${name}`" v-show="isActive">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { inject, computed } from 'vue'
import type { CollapseItemProps } from './types'
import { collapseContextKey } from './types'
defineOptions({
name: 'YlCollapseItem',
})
const props = defineProps<CollapseItemProps>()
const collapseContext = inject(collapseContextKey)
const isActive = computed(() => collapseContext?.activeNames.value.includes(props.name))
const handleClick = () => {
if (props.disabled) {
return
}
collapseContext?.handleItemClick(props.name)
}
</script>
类型定义(types.ts)
import type { Ref } from 'vue'
export type NameType = string | number
export interface CollapseItemProps {
name: NameType
title?: string
disabled?: boolean
}
export interface CollapseContext {
activeNames: Ref<NameType[]>
handleItemClick: (name: NameType) => void
}
导入组件(App.vue)
import Collapse from './components/Collapse/Collapse.vue'
import Item from './components/Collapse/CollapseItem.vue'
(三)实现手风琴特效
activeNames 是一个存储当前激活折叠项名称的响应式数组。当点击某个折叠项时,如果当前激活的折叠项名称与点击的折叠项名称相同,就将 activeNames 数组的值设为空字符串,表示关闭当前折叠项;否则,将 activeNames 数组的值更新为只包含当前点击的折叠项名称,从而实现手风琴效果,即每次只能有一个折叠项处于展开状态。
Collapse.vue
import { ref, provide, watch } from 'vue'
watch(
() => props.modelValue,
() => {
activeNames.value = props.modelValue
},
)
if (props.accordion && activeNames.value.length > 1) {
console.warn('accordion mode should only have one acitve item')
}
const handleItemClick = (item: NameType) => {
if (props.accordion) {
activeNames.value = [activeNames.value[0] === item ? '' : item]
} else {
const index = activeNames.value.indexOf(item)
//...
}
//...
}
(四)为Collapse组件添加样式并实现过渡效果
- 使用
Transition组件,在插入、更新或移除元素时,为元素添加过渡效果。结合 CSS 和过渡钩子函数,实现手风琴效果中内容区域展开和收起的平滑过渡。
style.css
.yl-collapse {
--yl-collapse-border-color: var(--yl-border-color-light);
--yl-collapse-header-height: 48px;
--yl-collapse-header-bg-color: var(--yl-fill-color-blank);
--yl-collapse-header-text-color: var(--yl-text-color-primary);
--yl-collapse-header-font-size: 13px;
--yl-collapse-content-bg-color: var(--yl-fill-color-blank);
--yl-collapse-content-font-size: 13px;
--yl-collapse-content-text-color: var(--yl-text-color-primary);
--yl-collapse-disabled-text-color: var(--yl-disabled-text-color);
--yl-collapse-disabled-border-color: var(--yl-border-color-lighter);
border-top: 1px solid var(--yl-collapse-border-color);
border-bottom: 1px solid var(--yl-collapse-border-color);
}
.yl-collapse-item__header {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--yl-collapse-header-height);
line-height: var(--yl-collapse-header-height);
background-color: var(--yl-collapse-header-bg-color);
color: var(--yl-collapse-header-text-color);
cursor: pointer;
font-size: var(--yl-collapse-header-font-size);
font-weight: 500;
transition: border-bottom-color var(--yl-transition-duration);
outline: none;
border-bottom: 1px solid var(--yl-collapse-border-color);
&.is-disabled {
color: var(--yl-collapse-disabled-text-color);
cursor: not-allowed;
background-image: none;
}
&.is-active {
border-bottom-color: transparent;
.header-angle {
transform: rotate(90deg);
}
}
.header-angle {
transition: transform var(--yl-transition-duration);
}
}
.yl-collapse-item__content {
will-change: height;
background-color: var(--yl-collapse-content-bg-color);
overflow: hidden;
box-sizing: border-box;
font-size: var(--yl-collapse-content-font-size);
color: var(--yl-collapse-content-text-color);
border-bottom: 1px solid var(--yl-collapse-border-color);
padding-bottom: 25px;
}
.slide-enter-active, .slide-leave-active {
transition: height var(--yl-transition-duration);
}
CollapseItem.vue
<div
class="yl-collapse-item__header"
:class="{
'is-disabled': disabled,
'is-active': isActive,
}"
//...
>
//...
</div>
<Transition name="slide" v-on="transitionEvents">
<div class="yl-collapse-item__wrapper" v-show="isActive">
<div class="yl-collapse-item__content" :id="`item-content-${name}`">
<slot></slot>
</div>
</div>
</Transition>
//...
const transitionEvents: Record<string, (el: HTMLElement) => void> = {
beforeEnter(el) {
el.style.height = '0px'
el.style.overflow = 'hidden'
},
enter(el) {
el.style.height = `${el.scrollHeight}px`
},
afterEnter(el) {
el.style.height = ''
el.style.overflow = ''
},
beforeLeave(el) {
el.style.height = `${el.scrollHeight}px`
el.style.overflow = 'hidden'
},
leave(el) {
el.style.height = '0px'
},
afterLeave(el) {
el.style.height = ''
el.style.overflow = ''
},
}
src/styles/index.css
@import '../components/Collapse/style.css';