Collapse 折叠面板

Collapse 折叠面板

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 {
// 不存在,插入对应 name
_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'

propsmodelValue

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]
>}

updateActiveNamesemits

同时触发 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 注入键,获取父组件的 activeNameshandleItemClick 方法

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,包含 activeNameshandleItemClick 方法


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:控制元素高度从 0scrollHeight 再恢复默认
  • beforeLeave/afterLeave:控制元素高度从 scrollHeight0 再恢复默认
  • 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 必要属性

1
>name: CollapseItemName // 必须唯一标识符

用于判断是否被选中,支持字符串或数字类型

emits 事件说明

  • update:modelValue:双向绑定更新值
  • change:选中状态变化时触发

总结

Collapse 组件通过以下核心机制实现折叠面板功能:

  1. 双向绑定:通过 v-model 绑定 modelValue,结合 update:modelValuechange 事件同步状态
  2. 手风琴模式:通过 accordion 属性限制仅展开一个项
  3. 状态共享:通过 provide/inject 实现父组件与子组件的状态和方法传递
  4. 动画效果:通过 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>