Vue3折叠面板(Collapse)
可自定义设置以下属性:
折叠面板数据(items),可使用 slot 替换指定 key 的 header、content、arrow、extra、lang,类型:Item[],默认 []
当前激活 tab 面板的 key(v-model:activeKey),类型:number[] | number | string[] | string | null,默认 null,传入 string | number 类型时,即为手风琴模式
带边框风格的折叠面板(bordered),类型:boolean,默认 true
是否禁用展开收起(disabled),类型:boolean,默认 false
使折叠面板透明且无边框(ghost),类型:boolean,默认 false
设置面板标题的样式(headerStyle),类型:CSSProperties,默认 {}
设置面板内容的样式(contentStyle),类型:CSSProperties,默认 {}
设置面板容器的样式(collapseStyle),类型:CSSProperties,默认 {}
自定义箭头切换图标(arrow),类型:VNode | Slot,默认 undefined
是否展示箭头(showArrow),类型:boolean,默认 true
箭头位置(arrowPlacement),类型:'left' | 'right',默认 'left'
设置面板箭头的样式(arrowStyle),类型:CSSProperties,默认 {}
面板标题右侧的额外内容(extra),类型:string | slot,默认 undefined
面板右上角固定内容(lang),例如标识 language 标识,类型:string | slot,默认 undefined
是否可复制面板内容(copyable),类型:boolean,默认 false
复制按钮属性配置(copyProps),参考 Button Props,类型:object,默认 {}
复制按钮文本(copyText),类型:string,默认 'Copy'
已复制按钮文本(copiedText),类型:string,默认 'Copied'
效果如下图:
在线预览
①创建折叠面板组件Collapse.vue:
其中引入使用了以下组件和工具函数:
- Vue3按钮(Button)
使用requestAnimationFrame模拟实现setTimeout和setInterval
<script setup lang="ts"> import { ref, watchEffect } from 'vue' import type { CSSProperties, VNode, Slot } from 'vue' import Button from 'components/button' import { debounce } from 'components/utils' export interface Item { key?: string | number // 对应 activeKey,如果没有传入 key 属性,则默认使用数据索引 (0,1,2...) 绑定 disabled?: boolean // 是否禁用展开收起 header?: string // 面板标题 string | slot headerStyle?: CSSProperties // 设置面板标题的样式 content?: string // 面板内容 string | slot contentStyle?: CSSProperties // 设置面板内容的样式 collapseStyle?: CSSProperties // 设置面板容器的样式 arrow?: VNode // 自定义箭头切换图标 showArrow?: boolean // 是否展示箭头 arrowPlacement?: 'left' | 'right' // 箭头位置 arrowStyle?: CSSProperties // 设置面板箭头的样式 extra?: string // 面板标题右侧的额外内容 string | slot lang?: string // 面板右上角固定内容,例如 language 标识 string | slot copyable?: boolean // 是否可复制面板内容 copyProps?: object // 复制按钮属性配置,参考 Button Props copyText?: string // 复制按钮文本 copiedText?: string // 已复制按钮文本 [propName: string]: any // 用于包含带有任意数量的其他属性 } export interface Props { items?: Item[] // 折叠面板数据,可使用 slot 替换指定 key 的 header、content、arrow、extra、lang activeKey?: string[] | string | number[] | number | null // (v-model) 当前激活 tab 面板的 key,传入 string | number 类型时,即为手风琴模式 bordered?: boolean // 带边框风格的折叠面板 disabled?: boolean // 是否禁用展开收起,较低优先级 ghost?: boolean // 使折叠面板透明且无边框 headerStyle?: CSSProperties // 设置面板标题的样式,较低优先级 contentStyle?: CSSProperties // 设置面板内容的样式,较低优先级 collapseStyle?: CSSProperties // 设置面板容器的样式,较低优先级 arrow?: VNode | Slot // 自定义箭头切换图标,较低优先级 vnode | slot showArrow?: boolean // 是否展示箭头,较低优先级 arrowPlacement?: 'left' | 'right' // 箭头位置,较低优先级 arrowStyle?: CSSProperties // 设置面板箭头的样式,较低优先级 extra?: string // 面板标题右侧的额外内容,较低优先级 string | slot lang?: string // 面板右上角固定内容,例如 language 标识,较低优先级 string | slot copyable?: boolean // 是否可复制面板内容,较低优先级 copyProps?: object // 复制按钮属性配置,参考 Button Props,较低优先级 copyText?: string // 复制按钮文本,较低优先级 copiedText?: string // 已复制按钮文本,较低优先级 } const props = withDefaults(defineProps<Props>(), { items: () => [], activeKey: null, bordered: true, disabled: false, ghost: false, headerStyle: () => ({}), contentStyle: () => ({}), collapseStyle: () => ({}), arrow: undefined, showArrow: true, arrowPlacement: 'left', arrowStyle: () => ({}), extra: undefined, lang: undefined, copyable: false, copyProps: () => ({}), copyText: 'Copy', copiedText: 'Copied' }) const contentRef = ref() // 面板内容的模板引用 const copyBtnTxt = ref<string>() // 复制按钮文本 const copyBtnClickedKeys = ref<(string | number)[]>([]) // 被点击的复制按钮的 key const emits = defineEmits(['update:activeKey', 'change']) watchEffect(() => { copyBtnTxt.value = props.copyText }) function getComputedValue(item: Item, key: keyof Props) { let computedValue = props[key as keyof Props] if (item?.[key as keyof Item] !== undefined) { computedValue = item[key as keyof Item] } return computedValue } function getComputedKey(key: number | string | undefined, index: number) { if (key !== undefined) { return key } return index } function setProperties(el: Element) { ;(el as HTMLElement).style.height = (el.lastElementChild as HTMLElement).offsetHeight + (props.bordered && !props.ghost ? 1 : 0) + 'px' ;(el as HTMLElement).style.opacity = '1' } function removeProperties(el: Element) { ;(el as HTMLElement).style.removeProperty('height') ;(el as HTMLElement).style.removeProperty('opacity') } function emitValue(value: any) { emits('update:activeKey', value) emits('change', value) } function onClick(key: string | number) { if (activeCheck(key)) { if (Array.isArray(props.activeKey)) { const res = (props.activeKey as any[]).filter((actKey: string | number) => actKey !== key) emitValue(res) } else { emitValue(null) } } else { if (Array.isArray(props.activeKey)) { emitValue([...props.activeKey, key]) } else { emitValue(key) } } } function activeCheck(key: string | number): boolean { if (Array.isArray(props.activeKey)) { return (props.activeKey as any[]).includes(key) } else { return props.activeKey === key } } function getCopyBtnTxt(item: Item, index: number) { const copyText = getComputedValue(item, 'copyText') const copiedText = getComputedValue(item, 'copiedText') if (copyBtnClickedKeys.value.includes(getComputedKey(item.key, index))) { return copiedText } else { return copyText } } const debounceRestCopyBtn = debounce((key: string | number) => { copyBtnClickedKeys.value = copyBtnClickedKeys.value.filter((clickedKey: string | number) => clickedKey !== key) }, 3000) function onCopy(index: number, key: string | number) { navigator.clipboard.writeText(contentRef.value[index].innerText || '').then( () => { if (!copyBtnClickedKeys.value.includes(key)) { copyBtnClickedKeys.value.push(key) } /* clipboard successfully set */ debounceRestCopyBtn(key) }, (err) => { /* clipboard write failed */ console.log('copy failed', err) } ) } </script> <template> <div class="collapse-wrap" :class="{ 'collapse-borderless': !bordered, 'collapse-ghost': ghost }" > <div class="collapse-item" :class="{ 'collapse-arrow-left': getComputedValue(item, 'arrowPlacement') === 'left', 'collapse-arrow-right': getComputedValue(item, 'arrowPlacement') === 'right', 'collapse-item-disabled': getComputedValue(item, 'disabled') }" :style="getComputedValue(item, 'collapseStyle') as CSSProperties" v-for="(item, index) in items" :key="index" > <div tabindex="0" class="collapse-header-wrap" :class="{ 'collapse-header-no-arrow': getComputedValue(item, 'showArrow') }" :style="getComputedValue(item, 'headerStyle') as CSSProperties" @click="getComputedValue(item, 'disabled') ? () => false : onClick(getComputedKey(item.key, index))" @keydown.enter="onClick(getComputedKey(item.key, index))" > <div v-if="getComputedValue(item, 'showArrow')" class="collapse-arrow" :style="getComputedValue(item, 'arrowStyle') as CSSProperties" > <slot name="arrow" :item="item" :key="getComputedKey(item.key, index)" :active="activeCheck(getComputedKey(item.key, index))" > <component v-if="getComputedValue(item, 'arrow')" :is="getComputedValue(item, 'arrow')" class="arrow-svg" :class="{ 'arrow-rotate': activeCheck(getComputedKey(item.key, index)) }" /> <svg v-else class="arrow-svg" :class="{ 'arrow-rotate': activeCheck(getComputedKey(item.key, index)) }" focusable="false" ><script setup lang="ts"> import Collapse from './Collapse.vue' import { ref, watchEffect, h } from 'vue' import { ArrowRightOutlined, DoubleRightOutlined, RightCircleFilled, StarOutlined, StarFilled } from '@ant-design/icons-vue' import type { CollapseProps, CollapseItem, RadioOption } from 'vue-amazing-ui' const collapseItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '2', header: 'This is panel header 2', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '3', header: 'This is panel header 3', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const disabledCollapseItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '2', header: 'This is panel header 2', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '3', disabled: true, header: 'This is panel header 3', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const nestCollapseItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const customStyleItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '2', header: 'This is panel header 2', headerStyle: { fontSize: '16px', color: '#1677ff' }, contentStyle: { padding: '16px 24px', color: '#eb2f96' }, content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '3', header: 'This is panel header 3', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const customArrowItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', arrow: h(ArrowRightOutlined), arrowStyle: { color: '#1677ff' }, content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '2', header: 'This is panel header 2', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '3', header: 'This is panel header 3', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const extraCollapseItems = ref<CollapseItem[]>([ { key: '1', header: 'This is panel header 1', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '2', header: 'This is panel header 2', extra: 'Extra', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' }, { key: '3', header: 'This is panel header 3', content: 'A dog is a type of domesticated animal. Known for its loyalty and faithfulness,it can be found as a welcome guest in many households across the world.' } ]) const positionOptions = ref<RadioOption[]>([ { label: 'left', value: 'left' }, { label: 'right', value: 'right' } ]) const activeKey = ref<CollapseProps['activeKey']>(['1']) const accordionActiveKey = ref<CollapseProps['activeKey']>('1') const borderlessActiveKey = ref<CollapseProps['activeKey']>(['1']) const disabledActiveKey = ref<CollapseProps['activeKey']>(['1']) const disabledItemActiveKey = ref<CollapseProps['activeKey']>(['1']) const ghostActiveKey = ref<CollapseProps['activeKey']>(['1']) const nestOuterActiveKey = ref<CollapseProps['activeKey']>(['1']) const nestInnerActiveKey = ref<CollapseProps['activeKey']>(['1']) const customActiveKey = ref<CollapseProps['activeKey']>(['1']) const customStyleActiveKey = ref<CollapseProps['activeKey']>(['1']) const customArrowActiveKey1 = ref<CollapseProps['activeKey']>(['1']) const customArrowActiveKey2 = ref<CollapseProps['activeKey']>(['1']) const hideArrowActiveKey = ref<CollapseProps['activeKey']>(['1']) const arrowPlaceActiveKey = ref<CollapseProps['activeKey']>(['1']) const arrowPlacement = ref<CollapseProps['arrowPlacement']>('right') const extraActiveKey = ref<CollapseProps['activeKey']>(['1']) const copyableActiveKey = ref<CollapseProps['activeKey']>(['1']) const customCopyActiveKey = ref<CollapseProps['activeKey']>(['1']) watchEffect(() => { console.log('activeKey', activeKey.value) }) watchEffect(() => { console.log('accordionActiveKey', accordionActiveKey.value) }) watchEffect(() => { console.log('borderlessActiveKey', borderlessActiveKey.value) }) watchEffect(() => { console.log('disabledItemActiveKey', disabledItemActiveKey.value) }) watchEffect(() => { console.log('ghostActiveKey', ghostActiveKey.value) }) watchEffect(() => { console.log('nestOuterActiveKey', nestOuterActiveKey.value) }) watchEffect(() => { console.log('nestInnerActiveKey', nestInnerActiveKey.value) }) watchEffect(() => { console.log('customActiveKey', customActiveKey.value) }) watchEffect(() => { console.log('customStyleActiveKey', customStyleActiveKey.value) }) watchEffect(() => { console.log('customArrowActiveKey1', customArrowActiveKey1.value) }) watchEffect(() => { console.log('customArrowActiveKey2', customArrowActiveKey2.value) }) watchEffect(() => { console.log('hideArrowActiveKey', hideArrowActiveKey.value) }) watchEffect(() => { console.log('arrowPlaceActiveKey', arrowPlaceActiveKey.value) }) watchEffect(() => { console.log('arrowPlacement', arrowPlacement.value) }) watchEffect(() => { console.log('extraActiveKey', extraActiveKey.value) }) watchEffect(() => { console.log('copyableActiveKey', copyableActiveKey.value) }) watchEffect(() => { console.log('customCopyActiveKey', customCopyActiveKey.value) }) function onChange(key: number | string) { console.log('change', key) } function handleClick(key: string | number) { console.log('key', key) } </script> <template> <div> <h1>{{ $route.name }} {{ $route.meta.title }}</h1> <h2 class="mt30 mb10">基本使用</h2> <h3 class="mb10">activeKey 传入 number[] | string[],所有面板可同时展开</h3> <Collapse :items="collapseItems" v-model:active-key="activeKey" @change="onChange" /> <h2 class="mt30 mb10">'手风琴'</h2> <h3 class="mb10">只允许单个内容区域展开,只需 activeKey 传入 number | string 即可</h3> <Collapse :items="collapseItems" v-model:active-key="accordionActiveKey" /> <h2 class="mt30 mb10">无边框</h2> <Collapse :items="collapseItems" v-model:active-key="borderlessActiveKey" :bordered="false" /> <h2 class="mt30 mb10">禁用</h2> <Flex vertical> <Collapse disabled :items="collapseItems" v-model:active-key="disabledActiveKey" /> <Collapse :items="disabledCollapseItems" v-model:active-key="disabledItemActiveKey" /> </Flex> <h2 class="mt30 mb10">幽灵折叠面板</h2> <Collapse :items="collapseItems" v-model:active-key="ghostActiveKey" ghost /> <h2 class="mt30 mb10">面板嵌套</h2> <Collapse :items="collapseItems" v-model:active-key="nestOuterActiveKey"> <template #content="{ key }"> <Collapse v-if="key === '1'" :items="nestCollapseItems" v-model:active-key="nestInnerActiveKey" /> </template> </Collapse> <h2 class="mt30 mb10">自定义面板</h2> <h3 class="mb10">自定义各个面板的背景色、圆角、边距</h3> <Collapse style="background-color: #fff" :items="collapseItems" v-model:active-key="customActiveKey" :bordered="false" :collapse-style="{ backgroundColor: '#f7f7f7', borderRadius: '8px', marginBottom: '16px', border: 0 }" /> <h2 class="mt30 mb10">自定义样式</h2> <Collapse :items="customStyleItems" v-model:active-key="customStyleActiveKey" :header-style="{ fontSize: '16px', color: '#ff6900' }" :content-style="{ padding: '16px 24px', color: '#09c8ce' }" :arrow-style="{ fontSize: '14px', height: '25px' }" /> <h2 class="mt30 mb10">自定义箭头</h2> <Flex vertical> <Collapse :items="customArrowItems" :arrow-style="{ color: '#ff6900' }" v-model:active-key="customArrowActiveKey1" /> <Collapse :items="customArrowItems" :arrow-style="{ color: '#ff6900' }" v-model:active-key="customArrowActiveKey2"> <template #arrow="{ key, active }"> <DoubleRightOutlined v-if="key === '2'" :rotate="active ? 90 : 0" /> <RightCircleFilled v-if="key === '3'" :rotate="active ? 90 : 0" /> </template> </Collapse> </Flex> <h2 class="mt30 mb10">隐藏箭头</h2> <Collapse :items="collapseItems" v-model:active-key="hideArrowActiveKey" :show-arrow="false" /> <h2 class="mt30 mb10">箭头位置</h2> <Flex vertical> <Radio :options="positionOptions" v-model:value="arrowPlacement" button button-style="solid" /> <Collapse :items="collapseItems" v-model:active-key="arrowPlaceActiveKey" :arrow-placement="arrowPlacement" /> </Flex> <h2 class="mt30 mb10">面板额外内容</h2> <Collapse :items="extraCollapseItems" v-model:active-key="extraActiveKey"> <template #extra="{ key }"> <StarFilled v-if="key === '1'" @click.stop="handleClick(key)" /> <StarOutlined v-if="key === '3'" @click.stop="handleClick(key)" /> </template> </Collapse> <h2 class="mt30 mb10">可复制</h2> <Collapse :items="collapseItems" v-model:active-key="copyableActiveKey" lang="template" copyable /> <h2 class="mt30 mb10">自定义复制按钮</h2> <Collapse :items="collapseItems" v-model:active-key="customCopyActiveKey" lang="javascript" copyable copy-text="复制" copied-text="已复制" :copy-props="{ ghost: true }" > <template #lang="{ key }"> <span v-if="key === '2'">typescript</span> </template> </Collapse> </div> </template>