组件库Collapse 折叠面板
BreezliCollapse 折叠面板
Collapse.vue
script
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
| import { ref, provide, watch } from 'vue' import { each } from 'lodash-es' import type { CollapseItemName, CollapseProps, CollapseEmits } from './types' import { debugWarn } from '@veyra/utils' import { COLLAPSE_CTX_KEY } from './constants'
const COMPONENT_NAME = 'VrCollapse' as const
defineOptions({ name: COMPONENT_NAME, })
const props = defineProps<CollapseProps>() const emits = defineEmits<CollapseEmits>() const activeNames = ref<CollapseItemName[]>(props.modelValue)
if (props.accordion && activeNames.value.length > 1) { debugWarn(COMPONENT_NAME, 'accordion mode should only have one active item') }
function handleItemClick(item: CollapseItemName) { let _activeNames = [...activeNames.value] if (props.accordion) { _activeNames = [_activeNames[0] === item ? '' : item] updateActiveNames(_activeNames) return }
const index = _activeNames.indexOf(item) if (index > -1) { _activeNames.splice(index, 1) } else { _activeNames.push(item) } updateActiveNames(_activeNames) }
function updateActiveNames(val: CollapseItemName[]) { activeNames.value = val each(['update:modelValue', 'change'], (e) => emits(e as 'update:modelValue' & 'change', val) ) }
watch( () => props.modelValue, (val) => updateActiveNames(val) )
provide(COLLAPSE_CTX_KEY, { activeNames, handleItemClick, })
|
关键解释
defineOptions
最终导出组件名为 VrCollapse
,在 packages\core\components.ts
中以以下形式导入
1
| import { VrCollapse } from '@veyra/components'
|
props
和 modelValue
1
| >const props = defineProps<CollapseProps>()
|
接口定义:
1 2 3 4
| >export interface CollapseProps { >modelValue: CollapseItemName[] >accordion?: boolean >}
|
modelValue
通过 v-model
实现双向绑定,watch
监听其变化并同步到 activeNames
provide
上下文
1 2 3 4
| >provide(COLLAPSE_CTX_KEY, { >activeNames, >handleItemClick, >})
|
通过注入键 COLLAPSE_CTX_KEY
,子组件 CollapseItem
可以通过 inject
获取父组件的状态和方法
handleItemClick
逻辑
- 手风琴模式:仅允许一个展开项,点击时切换为当前项或清空
- 普通模式:点击时切换项的展开/收起状态
1 2 3
| >if (props.accordion) { _activeNames = [_activeNames[0] === item ? '' : item] >}
|
updateActiveNames
和 emits
同时触发 update:modelValue
(双向绑定)和 change
事件
1 2 3
| >each(['update:modelValue', 'change'], (e) => >emits(e as ..., val) >)
|
template
1 2 3
| <div class="vr-collapse"> <slot></slot> </div>
|
关键解释
容器组件
通过 <slot>
插入 CollapseItem
子组件,内容由子组件自行渲染
CollapseItem.vue
script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { inject, computed } from 'vue' import { COLLAPSE_CTX_KEY } from './constants' import transitionEvents from './transitionEvents' import VrIcon from '../Icon/Icon.vue'
defineOptions({ name: 'VrCollapseItem', })
const props = defineProps<CollapseItemProps>()
const ctx = inject(COLLAPSE_CTX_KEY)
const isActive = computed(() => ctx?.activeNames.value?.includes(props.name))
function handleClick() { if (props.disabled) return ctx?.handleItemClick(props.name) }
|
关键解释
inject
获取父组件上下文
1
| >const ctx = inject(COLLAPSE_CTX_KEY)
|
通过 COLLAPSE_CTX_KEY
注入键,获取父组件的 activeNames
和 handleItemClick
方法
isActive
计算属性
1
| >const isActive = computed(() => ctx?.activeNames.value?.includes(props.name))
|
根据父组件的 activeNames
判断当前项是否处于展开状态
handleClick
点击事件
- 检查
disabled
属性,禁用项不可点击
- 通过
ctx?.handleItemClick
通知父组件更新选中状态
template
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
| <div class="vr-collapse-item" :class="{ 'is-disabled': disabled, }"> <div class="vr-collapse-item__header" :id="`item-header-${name}`" :class="{ 'is-disabled': disabled, 'is-active': isActive, }" @click="handleClick"> <span class="vr-collapse-item__title"> <slot name="title"> {{ title }} </slot> </span> <vr-icon icon="angle-right" class="header-angle" /> </div> <transition name="slide" v-on="transitionEvents"> <div class="vr-collapse-item__wapper" v-show="isActive"> <div class="vr-collapse-item__content" :id="`item-content-${name}`"> <slot></slot> </div> </div> </transition> </div>
|
关键解释
transition
动画
1
| ><transition name="slide" v-on="transitionEvents">
|
引入 transitionEvents.ts
中定义的过渡钩子函数,控制高度变化实现折叠动画
v-show="isActive"
根据 isActive
状态决定是否显示内容区域
header-angle
图标
默认使用 angle-right
图标,方向可通过自定义图标修改
constants.ts
1 2 3 4 5
| import type { InjectionKey } from "vue"; import type { CollapseContext } from "./types";
export const COLLAPSE_CTX_KEY: InjectionKey<CollapseContext> = Symbol("collapseContext");
|
关键解释
注入键 COLLAPSE_CTX_KEY
允许 CollapseItem
通过 inject
获取父组件的 CollapseContext
,包含 activeNames
和 handleItemClick
方法
transitionEvents.ts
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
| const _setHeightZero = (el: HTMLElement) => (el.style.height = "0px"); const _setHeightScroll = (el: HTMLElement) => (el.style.height = `${el.scrollHeight}px`); const _setHeightEmpty = (el: HTMLElement) => (el.style.height = ""); const _setOverflowHidden = (el: HTMLElement) => (el.style.overflow = "hidden"); const _setOverflowEmpty = (el: HTMLElement) => (el.style.overflow = "");
const transitionEvents: Record<string, (el: HTMLElement) => void> = { beforeEnter(el) { _setHeightZero(el); _setOverflowHidden(el); }, enter: (el) => _setHeightScroll(el), afterEnter(el) { _setHeightEmpty(el); _setOverflowEmpty(el); }, beforeLeave(el) { _setHeightScroll(el); _setOverflowHidden(el); }, leave: (el) => _setHeightZero(el), afterLeave(el) { _setHeightEmpty(el); _setOverflowEmpty(el); }, };
export default transitionEvents;
|
关键解释
过渡动画实现
- beforeEnter/afterEnter:控制元素高度从
0
到 scrollHeight
再恢复默认
- beforeLeave/afterLeave:控制元素高度从
scrollHeight
到 0
再恢复默认
- overflow:过渡过程中设置
overflow: hidden
避免内容溢出
types.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import type { Ref } from "vue";
export type CollapseItemName = string | number;
export interface CollapseProps { modelValue: CollapseItemName[]; accordion?: boolean; }
export interface CollapseItemProps { name: CollapseItemName; title?: string; disabled?: boolean; }
export interface CollapseContext { activeNames: Ref<CollapseItemName[]>; handleItemClick(name: CollapseItemName): void; }
export interface CollapseEmits { (e: "update:modelValue", value: CollapseItemName[]): void; (e: "change", value: CollapseItemName[]): void; }
|
关键解释
CollapseContext
接口
1 2 3 4
| >export interface CollapseContext { >activeNames: Ref<CollapseItemName[]> >handleItemClick(name: CollapseItemName): void >}
|
定义父组件通过 provide
暴露给子组件的上下文数据和方法
CollapseItemProps
必要属性
用于判断是否被选中,支持字符串或数字类型
emits
事件说明
update:modelValue
:双向绑定更新值
change
:选中状态变化时触发
总结
Collapse 组件通过以下核心机制实现折叠面板功能:
- 双向绑定:通过
v-model
绑定 modelValue
,结合 update:modelValue
和 change
事件同步状态
- 手风琴模式:通过
accordion
属性限制仅展开一个项
- 状态共享:通过
provide/inject
实现父组件与子组件的状态和方法传递
- 动画效果:通过
transitionEvents
控制高度变化实现平滑过渡
使用示例:
1 2 3 4 5 6 7 8 9
| <vr-collapse v-model="activeNames" :accordion="true"> <vr-collapse-item v-for="item in items" :key="item.id" :name="item.id" :title="item.title"> {{ item.content }} </vr-collapse-item> </vr-collapse>
|