仿 ElementPlus 组件库(二)—— Collapse 组件实现

445 阅读3分钟

在前端开发中,组件库的使用能够极大地提高开发效率,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';