一.完成静态的Collapse
Collapse.vue:
js
<template>
<div class="lu-collapse">
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'LuCollapse'
})
</script>
CollapseItem.vue:
js
<template>
<div class="lu-collapse-item" :class="{
'is-disabled': disabled
}">
<div class="lu-collapse-item__header" :id="`item-header-${name}`">
<slot name="title">{{ title }}</slot>
</div>
<div class="lu-collapse-item__content" :id="`item-content-${name}`">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import type { CollapseItemProps } from './types'
defineProps<CollapseItemProps>()
defineOptions({
name: 'LuCollapseItem'
})
</script>
相应的types.ts:
js
export interface CollapseItemProps {
name: string | number;
title?: string; //可选的可以不传
disabled?: boolean;
}
App.vue中使用:
js
<Collapse>
<CollapseItem name="a">
<template #title>
<h1>a title</h1>
</template>
<div> this is content a aaa </div>
</CollapseItem>
<CollapseItem name="b" title="nice title b item b">
<div> this is bbbbb test </div>
</CollapseItem>
<CollapseItem name="c" title="nice cccc">
<div> this is cccc test </div>
</CollapseItem>
</Collapse>
初步实现效果:

二.添加折叠效果
因为这里的子组件是在父组件的slot中的,不能通过prop属性来实现信息传递,可以使用provide和inject
在 Collapse
组件中
- 使用
ref
创建一个响应式变量activeNames
,用于存储当前激活的折叠项的名称。 - 定义一个方法
handleItemClick
,用于处理点击事件,切换折叠项的激活状态。 - 使用
provide
函数将activeNames
和handleItemClick
方法提供给子孙组件。这里使用了collapseContextKey
作为键,这是一个通过Symbol
创建的唯一标识符,用于确保提供和注入的上下文是唯一的。
javascript
const activeNames = ref<NameType[]>([]);
const handleItemClick = (item: NameType) => {
const index = activeNames.value.indexOf(item);
if (index > -1) {
activeNames.value.splice(index, 1);
} else {
activeNames.value.push(item);
}
};
provide(collapseContextKey, {
activeNames,
handleItemClick
});
在 CollapseItem
组件中
- 使用
inject
函数注入从父组件Collapse
提供的上下文。这允许访问activeNames
和handleItemClick
。 - 定义一个计算属性
isActive
,用于确定当前项是否激活,这是基于activeNames
的内容。 - 定义一个方法
handleClick
,当点击折叠项的头部时调用,如果项未被禁用,则调用注入的handleItemClick
方法。
javascript
const collapseContext = inject(collapseContextKey);
const isActive = computed(() => collapseContext?.activeNames.value.includes(props.name));
const handleClick = () => {
if (props.disabled) return;
collapseContext?.handleItemClick(props.name);
};
代码:
types.ts:
js
export interface CollapseContext {
activeNames: Ref<NameType[]>;
handleItemClick: (name: NameType) => void
}
//使用symbol创建独一无二的key
export const collapseContextKey: InjectionKey<CollapseContext> = Symbol('collapseContextKey')
CollapseItem.vue:
js
<template>
<div class="lu-collapse-item" :class="{
'is-disabled': disabled
}">
<div class="lu-collapse-item__header" :id="`item-header-${name}`" @click="handleClick">
<slot name="title">{{ title }}</slot>
</div>
<div class="lu-collapse-item__content" :id="`item-content-${name}`" v-show="isActive">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import type { CollapseItemProps } from './types'
import { inject, computed } from 'vue'
import { collapseContextKey } from './types'
const props = defineProps<CollapseItemProps>()
defineOptions({
name: 'LuCollapseItem'
})
const collapseContext = inject(collapseContextKey)
const isActive = computed(() => collapseContext?.activeNames.value.includes(props.name))
const handleClick = () => {
if (props.disabled) { return }
collapseContext?.handleItemClick(props.name)
}
</script>
<style>
.lu-collapse-item__header {
font-size: 30px;
}
</style>
Collapse.vue:
js
<template>
<div class="lu-collapse">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue' //创建响应式数组的时候会用到
import type { NameType } from './types'
import { collapseContextKey } from './types'
defineOptions({
name: 'LuCollapse'
})
//这里使用provide和inject传到子组件中,因为目标位置是在是在子组件中的slot中,
//所以无法使用props
const activeNames = ref<NameType[]>([])
const handleItemClick = (item: NameType) => {
const index = activeNames.value.indexOf(item)
if (index > -1) {
activeNames.value.splice(index, 1)
}
else {
activeNames.value.push(item)
}
}
provide(collapseContextKey, {
activeNames,
handleItemClick
})
</script>
三.实现v-model
官网的v-model: 组件 v-model | Vue.js 做两件事,添加属性,添加对应的事件 types.ts中先定义属性和事件
js
export interface CollapseProps {
modelValue: NameType[];
according?: boolean;
}
export interface CollapseEmits {
(e: 'update:modelValue', values: NameType[]): void;
(e: 'change', values: NameType[]): void;
}
Collapse.vue中使用:
js
<template>
<div class="lu-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: 'LuCollapse'
})
//v-model相关:使用在types中定义好的属性和事件
const props = defineProps<CollapseProps>()
const emits = defineEmits<CollapseEmits>()
//这里使用provide和inject传到子组件中,因为目标位置是在是在子组件中的slot中,
//所以无法使用props
const activeNames = ref<NameType[]>(props.modelValue) //传递modelValue给子组件
const handleItemClick = (item: NameType) => {
const index = activeNames.value.indexOf(item)
if (index > -1) {
activeNames.value.splice(index, 1)
}
else {
activeNames.value.push(item)
}
emits('update:modelValue', activeNames.value)
emits('change', activeNames.value)
}
provide(collapseContextKey, {
activeNames,
handleItemClick
})
</script>
App.vue中:
js
const openValue = ref(['a'])
...省略中间
<Collapse v-model="openValue">
<CollapseItem name="a">
<template #title>
<div>a title</div>
</template>
<div> this is content a aaa </div>
</CollapseItem>
<CollapseItem name="b" title="nice title b item b">
<div> this is bbbbb test </div>
</CollapseItem>
<CollapseItem name="c" title="nice cccc">
<div> this is cccc test </div>
</CollapseItem>
</Collapse>
为什么 openValue
的值可以传递给 modelValue
在 Vue 3 中,v-model
本质上是一种语法糖,用于创建一个双向绑定。在自定义组件中,v-model
通常通过 props
和 emit
事件来实现。在你的 Collapse
组件中,你已经定义了 modelValue
作为 props
并设置了 update:modelValue
事件,这是实现 v-model
的标准方式。
- Props 传递 : 当使用
v-model
时,Vue 会自动将绑定的值(在你的示例中是openValue
)传递给组件的modelValue
prop
。这是 Vue 3 中v-model
的默认行为,它简化了父子组件之间的数据传递。 - 事件触发 : 在
CollapseItem
组件中,当用户点击某个折叠项时,会触发handleItemClick
方法。这个方法会更新activeNames
(一个响应式引用),然后通过emit
触发update:modelValue
事件,并将新的activeNames
作为参数传递。这样,父组件就可以接收到这个事件,并更新其绑定的openValue
。
四.设置样式
js
.lu-collapse {
--lu-collapse-border-color: var(--lu-border-color-light);
--lu-collapse-header-height: 48px;
--lu-collapse-header-bg-color: var(--lu-fill-color-blank);
--lu-collapse-header-text-color: var(--lu-text-color-primary);
--lu-collapse-header-font-size: 13px;
--lu-collapse-content-bg-color: var(--lu-fill-color-blank);
--lu-collapse-content-font-size: 13px;
--lu-collapse-content-text-color: var(--lu-text-color-primary);
--lu-collapse-disabled-text-color: var(--lu-disabled-text-color);
--lu-collapse-disabled-border-color: var(--lu-border-color-lighter);
border-top: 1px solid var(--lu-collapse-border-color);
border-bottom: 1px solid var(--lu-collapse-border-color);
}
.lu-collapse-item__header {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--lu-collapse-header-height);
line-height: var(--lu-collapse-header-height);
background-color: var(--lu-collapse-header-bg-color);
color: var(--lu-collapse-header-text-color);
cursor: pointer;
font-size: var(--lu-collapse-header-font-size);
font-weight: 500;
transition: border-bottom-color var(--lu-transition-duration);
outline: none;
border-bottom: 1px solid var(--lu-collapse-border-color);
.is-disabled {
color: var(--lu-collapse-disabled-text-color);
cursor: not-allowed;
background-image: none;
}
.is-active {
border-bottom-color: transparent;
}
}
.lu-collapse-item__content {
will-change: height;
background-color: var(--lu-collapse-content-bg-color);
overflow: hidden;
box-sizing: border-box;
font-size: var(--lu-collapse-content-font-size);
color: var(--lu-collapse-content-text-color);
border-bottom: 1px solid var(--lu-collapse-border-color);
padding-bottom: 25px;
}
给item展示的时候添加一些动画:使用transition
js
<Transition name="fade">
<div class="lu-collapse-item__content" :id="`item-content-${name}`" v-show="isActive">
<slot></slot>
</div>
</Transition>
js
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s ease-in-out;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
实现滑动的效果