wot-design-uni 折叠面板(Collapse)完整实践:四种模式、异步展开钩子与源码级原理
wot-design-uni 折叠面板(Collapse)完整实践:四种模式、异步展开钩子与源码级原理
本文围绕 wot-design-uni 组件库的 Collapse 折叠面板组件展开:覆盖普通折叠、手风琴、禁用、异步更新、查看更多(View More)以及嵌套使用等全部官方用法,并深入解析父子组件通信、高度过渡动画、toggleAll 实例方法背后的实现机制,帮助你在 uni-app 项目中直接落地各种"折叠面板"需求。
wd-collapse 用于将一组内容放置在多个可折叠面板中,点击面板标题即可展开或收起内容。它通过 v-model 的三种数据类型驱动出三种形态:array(普通折叠,可多项同时展开)、string(手风琴,同一时间只展开一项)、boolean(查看更多,整体展开/收起)。本文基于组件源码(wd-collapse.vue、wd-collapse-item.vue)与官方演示页 collapse 演示 展开讲解。
基本使用:v-model 与 CollapseItem 的 name 标识
v-model 为绑定值,可以为 array 类型(普通折叠)、string 类型(手风琴)和 boolean 类型(查看更多)。CollapseItem 的 name 为必填项(源码中定义为 makeRequiredProp(String),见 collapse-item/types.ts),title 选填且可通过同名 slot 自定义,name 用于唯一标识折叠栏。
const value = ref<string[]>(['item1'])
<wd-collapse v-model="value">
<wd-collapse-item title="标签1" name="item1">这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item title="标签2" name="item2">这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item name="item3">
<template #title="{ expanded, disabled, isFirst }">
<view class="header">
<text style="color: red">通过 slot 自定义标题</text>
<text>{{ expanded ? '我展开了' : '我已收起' }}</text>
</view>
</template>
这是一条简单的示例文字。
</wd-collapse-item>
</wd-collapse>
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
源码解析:expanded 状态如何同步
title 插槽会收到 { expanded, disabled, isFirst } 三个作用域参数。从源码结构看(wd-collapse-item.vue):
expanded是组件内部ref,它由计算属性isSelected派生——将父级modelValue与自身name比对(string相等,或array中包含该name),再经watch触发updateExpand更新。因此"哪些面板是展开的"完全由受控的v-model决定,组件自身不持有独立状态;isFirst表示该项在父组件中的渲染索引是否为 0(由 useParent 提供的index计算),用于去掉第一项标题的上边框,渲染时对应wd-collapse-item__header-first类。
另外,父组件 wd-collapse 对绑定值有类型校验(wd-collapse.vue):手风琴模式下 value 必须是 string,否则会打印 accordion value must be string;非手风琴且非查看更多模式下必须是 Array,否则打印 value must be Array。这解释了为什么三种模式必须搭配对应类型的 v-model。
手风琴模式(accordion)
设置 accordion 属性即可切换为手风琴模式,此时同一时间只能展开一个面板:
<wd-collapse v-model="value" accordion>
<wd-collapse-item title="标签1" name="item1">这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item title="标签2" name="item2">这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item title="标签3" name="item3">这是一条简单的示例文字。</wd-collapse-item>
</wd-collapse>
const value = ref<string>('item1')
源码解析:toggle 的两种分支
父组件内部的 toggle 方法是两种模式的核心差异所在(wd-collapse.vue):
function toggle(name: string, expanded: boolean) {
const { accordion, modelValue } = props
if (accordion) {
// 手风琴:再次点击已展开项则传空串收起,否则切换为新项
updateChange(name === modelValue ? '' : name)
} else if (expanded) {
// 普通模式:向数组追加 name
updateChange((modelValue as string[]).concat(name))
} else {
// 普通模式:从数组中过滤掉 name
updateChange((modelValue as string[]).filter((activeName) => activeName !== name))
}
}
手风琴模式下绑定值是单个 string,点击新面板直接把 modelValue 替换为新 name(再次点击当前项则置为空串收起);普通模式下则在 string<a href="https://link.gitcode.com/i/5040ce70d79f22d1c6615732fc88a8de" target="_blank">] 上做追加与过滤。updateChange 会同时发出 update:modelValue 和 change 事件,后者参数为 { value }(见下文事件说明)。集成测试用例 [wd-collapse.test.ts 也验证了手风琴模式点击第二项后 update:modelValue 会变为 ['item2']。
禁用折叠栏(disabled)
给 CollapseItem 设置 disabled 属性可禁用某个折叠栏。被禁用项的根节点会带上 is-disabled 类,且点击标题时 handleClick 直接 return,不会触发任何状态变更(wd-collapse-item.vue)。
<wd-collapse v-model="value">
<wd-collapse-item title="标签1" name="item1">这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item title="标签2" name="item2" disabled>这是一条简单的示例文字。</wd-collapse-item>
<wd-collapse-item title="标签3" name="item3">这是一条简单的示例文字。</wd-collapse-item>
</wd-collapse>
测试用例(wd-collapse.test.ts)验证了点击禁用项后父组件不会发出 update:modelValue 事件。官方演示页还支持在 title 插槽中读取 disabled 参数来显示"已禁用"提示文案(见 collapse 演示)。
异步更新:beforeExpend 展开前钩子
通过给 wd-collapse-item 传入 beforeExpend 函数,可以在打开面板前进行校验和处理:返回 true 表示允许打开,返回 false 表示禁止打开;也支持返回 Promise,用于先调用接口拉取面板数据再展开。
<wd-collapse v-model="value">
<wd-collapse-item v-for="(item, index) in itemList" :before-expend="beforeExpend" :key="index" :title="item.title" :name="item.name">
{{ item.body }}
</wd-collapse-item>
</wd-collapse>
import { useToast } from '@/uni_modules/wot-design-uni'
const toast = useToast()
const value = ref<string[]>(['item1'])
const itemList = ref<Record<string, any>[]>([
{
title: '标签1',
name: 'item1',
body: '如订单处于暂停状态,进入"我的订单"页面,找到要取消的订单,点击"取消订单"按钮;选择订单取消原因后,点击"下一步"提交申请即可。'
},
{
title: '标签2',
name: 'item2',
body: '一般情况下,买家只能向商户申请退款,商户确认可以退款后,可以通过接口或者商户平台向微信支付发起退款申请。'
}
])
/**
* 折叠面板展开前回调方法
* @param name 被点击面板的 name
*/
function beforeExpend(name: string) {
const index = itemList.value.findIndex((item) => item.name === name)
if (index > -1) {
// 展开前把面板正文替换为最新拉取的数据(此处以赋值模拟)
itemList.value[index].body = '这里是重新获取的最新内容……'
}
return new Promise((resolve) => {
toast.loading('加载中')
setTimeout(() => {
toast.close()
resolve(true) // resolve(true) 允许展开;reject 则阻止展开
}, 500)
})
}
beforeExpend 执行时会收到一个参数:
| 参数名 | 说明 | 类型 |
|---|---|---|
| name | 唯一标识符 | String |
源码解析:钩子只在"展开"时触发
注意钩子的命名细节——源码中 handleBeforeExpand 会先计算 nextexpanded = !expanded.value,只有当面板处于将要展开(nextexpanded 为 true)且配置了 beforeExpend 时才会调用该函数(wd-collapse-item.vue)。也就是说:收起动作不会触发该钩子。
对返回值的处理逻辑为:
- 返回
false(falsy):内部reject(),handleClick的try/catch捕获后静默终止,面板保持收起,v-model不变; - 返回 Promise:等待其 resolve 后才继续执行
initRect()与toggle,reject 则同样阻止展开; - 返回
true或undefined:直接 resolve 放行。
测试文件 wd-collapse.test.ts 分别对返回 true、返回 false、返回 Promise 三种场景断言 beforeExpend 以面板 name('test')为参数被调用。
查看更多模式(viewmore)
wd-collapse 可以脱离 CollapseItem 单独使用。设置 viewmore 属性后,它会转化为"查看更多"的折叠类型,同时可以用 line-num 修改收起时的显示行数(默认 2 行)。此时 value 为 boolean 类型:false 表示收起(显示截断),true 表示展开全部。
<wd-collapse viewmore v-model="value">
这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。
</wd-collapse>
const value = ref<boolean>(false)
源码解析:line-clamp 截断的实现
查看更多模式在模板中单独渲染(wd-collapse.vue):
- 内容容器
wd-collapse__content在收起时(!modelValue)附加is-retract类,并通过内联样式-webkit-line-clamp: {contentLineNum}; -webkit-box-orient: vertical实现多行截断; contentLineNum是一个响应式变量,在onBeforeMount中根据viewmore && !modelValue ? lineNum : 0初始化(wd-collapse.vue)——已展开时置 0,截断失效;lineNum还有校验:lineNum必须大于 0,否则打印lineNum must greater than 0;- 底部展开按钮区域
wd-collapse__more点击时触发handleMore(),直接对modelValue取反并发出change事件(wd-collapse.vue); - 收起状态的样式
is-retract在 index.scss 中定义为display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden,配合内联的line-clamp完成截断,且组件已适配暗黑主题(wot-theme-dark下切换背景与文字色)。
查看更多 - 使用自定义插槽
查看更多模式下,可以使用插槽定义自己想要的折叠处样式:使用 use-more-slot 开启具名插槽 more,并使用外部样式类 custom-more-slot-class 为自定义插槽设置样式。源码中该插槽被包裹在带 customMoreSlotClass 的 view 内(wd-collapse.vue)。
:deep(.more-slot) {
color: red;
}
<wd-collapse viewmore v-model="value" @change="handleChange" use-more-slot custom-more-slot-class="more-slot">
具名插槽:这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。这是一条简单的示例文字。
<template #more>
<view>显示全部</view>
</template>
</wd-collapse>
嵌套使用
collapse 可以嵌套。由于 collapse-item 的内容容器存在默认的 padding,嵌套的 collapse 需要设置 custom-body-style 或 custom-body-class 来覆盖默认样式。
注意:
custom-body-style和custom-body-class在1.4.0及以上版本支持。
<view class="collapse">
<wd-collapse v-model="collapseRoot">
<wd-collapse-item custom-body-style="padding:0 0 0 14px" v-for="item in 5" :key="item" :title="`标签${item}`" :name="`${item}`">
<wd-collapse v-model="collapseList[item - 1]">
<wd-collapse-item
v-for="(item, index) in itemList"
:custom-class="index === 0 ? 'no-border' : ''"
:key="index"
:title="item.title"
:name="item.name"
>
{{ item.body }}
</wd-collapse-item>
</wd-collapse>
</wd-collapse-item>
</wd-collapse>
</view>
.collapse {
:deep() {
.no-border {
&::after {
display: none;
}
}
}
}
const collapseRoot = ref<string[]>(['0'])
const collapseList = ref<Array<string[]>>([['item1'], ['item2'], ['item3'], ['item4'], ['item5']])
要点:外层每一项的 v-model 来自 collapseRoot 数组,内层每个子 collapse 各自绑定 collapseList 中对应的独立数组,从而实现"外层选一个组、内层组内再折叠"的两级联动。custom-body-style="padding:0 0 0 14px" 正是为了消除内层折叠面板被外层 body 内边距撑出的空隙;no-border 类则用于抹掉嵌套后重复出现的分隔线。
命令式 API:toggleAll 批量切换
除了受控的 v-model,wd-collapse 还通过 defineExpose 暴露了 toggleAll 实例方法(wd-collapse.vue):传 true 为全部展开,false 为全部收起,不传参为全部切换。
| 方法名 | 说明 | 参数 | 最低版本 |
|---|---|---|---|
| toggleAll | 切换所有面板展开状态,传 true 为全部展开,false 为全部收起,不传参为全部切换 |
options?: CollapseToggleAllOptions |
0.2.6 |
CollapseToggleAllOptions 支持布尔值或选项对象:
| 参数名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| expanded | 是否展开,true 为展开,false 为收起 |
boolean | - |
| skipDisabled | 是否跳过禁用项 | boolean | false |
toggleAll 方法示例
<wd-collapse ref="collapse">...</wd-collapse>
import { ref } from 'vue'
import type { CollapseInstance } from '@/uni_modules/wot-design-uni/components/wd-collapse/types'
const collapseRef = ref<CollapseInstance>()
// 全部切换
collapseRef.value?.toggleAll()
// 全部展开
collapseRef.value?.toggleAll(true)
// 全部收起
collapseRef.value?.toggleAll(false)
// 全部切换,并跳过禁用项
collapseRef.value?.toggleAll({ skipDisabled: true })
// 全部展开,并跳过禁用项
collapseRef.value?.toggleAll({ expanded: true, skipDisabled: true })
源码解析:toggleAll 如何遍历子项
toggleAll 的实现(wd-collapse.vue)依赖 useChildren 维护的 children 响应式列表:
- 手风琴模式下直接 return:因为同一时间只能展开一项,"全部展开"没有意义;测试用例 wd-collapse.test.ts 验证了此时不会发出
update:modelValue; - 布尔参数会被规整为
{ expanded: options }; - 遍历所有子项,通过
item.$.exposed.getExpanded()读取每个 CollapseItem 当前展开状态(该能力由 CollapseItemExpose 暴露):expanded未指定时,收集当前处于收起状态的项(即"反向切换");skipDisabled: true时,禁用项只有当前已展开的才会被保留在结果中,避免被强制收起;- 收集到的
names最终整体作为新modelValue发出update:modelValue与change。
官方演示页 collapse 演示 用五个按钮分别演示了这五种调用方式,可对照查看。
展开/收起动画的实现机制
从源码结构看,折叠动画并未使用 CSS max-height 之类近似值,而是基于真实测量的高度过渡:
- 点击标题后
handleClick先await updateExpand():此时经getRect测量内容容器#collapseId的真实高度,写入height,等待一帧(pause())后把expanded置为真值(wd-collapse-item.vue); - 计算属性
contentStyle在inited后给内容包裹层加上transition: height 0.3s ease-in-out,收起时height: 0px,展开时height: 实测高度(wd-collapse-item.vue); - 过渡结束后
@transitionend触发handleTransitionEnd,把height重置为空串,释放固定高度,使内容可自适应后续变化(wd-collapse-item.vue)。
这也解释了为何 beforeExpend 要 await:先等异步数据就绪、内容渲染完成后测量高度,动画才不会失真。测试 handles transition end event 验证了 transitionend 后 height 被重置。
API 汇总
Collapse Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| value(v-model) | 绑定值 | string / array / boolean | - | - | - |
| accordion | 是否启用手风琴模式 | boolean | - | false | - |
| viewmore | 是否启用查看更多模式 | boolean | - | false | - |
| line-num | 查看更多模式收起时的显示行数 | number | - | 2 | - |
| use-more-slot | 查看更多模式下是否使用自定义插槽 | boolean | - | false | - |
CollapseItem Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| name | 折叠栏的唯一标识符(必填) | string | - | - | - |
| title | 折叠栏的标题,支持同名 slot 自定义内容 | string | - | '' | - |
| disabled | 禁用折叠栏 | boolean | - | false | - |
| before-expend | 打开前的回调函数,返回 false 可阻止打开,支持返回 Promise |
Function | - | - | - |
| custom-class | 自定义类名 | string | - | - | - |
| custom-title-class | 自定义标题类名 | string | - | - | 1.4.0 |
| custom-body-class | 自定义内容容器样式类 | string | - | - | 1.4.0 |
| custom-body-style | 自定义内容容器样式 | string | - | - | 1.4.0 |
Collapse Slots
| 名称 | 说明 | 最低版本 |
|---|---|---|
| title | 标题,便于开发者自定义标题(非 viewmore 场景使用) | 1.2.27 |
| more | 查看更多,便于开发者自定义"查看更多"类型的展开收起样式 | - |
Collapse Events
| 事件名称 | 说明 | 参数 |
|---|---|---|
| change | 绑定值变化时触发(点击展开/收起、查看更多切换、toggleAll 均会触发) | { value } |
外部样式类
| 类名 | 说明 | 最低版本 |
|---|---|---|
| custom-class | 根节点样式(Collapse 与 CollapseItem 均支持) | - |
| custom-title-class | 标题样式 | 1.4.0 |
| custom-body-class | 折叠面板内容容器样式 | 1.4.0 |
| custom-more-slot-class | 查看更多模式下的插槽外部自定义样式 | - |
说明:
baseProps(custom-class、custom-style)来自 common/props.ts,两个组件均已合并。
父子通信架构小结
从源码结构看,Collapse 采用 wot-design-uni 通用的 provide/inject 父子通信模式:
- 父组件 wd-collapse.vue 通过
useChildren(COLLAPSE_KEY)建立子项注册表,并provide自身props与toggle方法(types.ts 中定义了CollapseProvide); - 子组件 wd-collapse-item.vue 通过
useParent(COLLAPSE_KEY)拿到父实例与自身索引,isSelected依赖父级modelValue计算选中态,点击后回调collapse.toggle(name, !expanded); - 子项列表的排序由 useChildren 中的
sortChildren依据 vnode 顺序维护,保证toggleAll遍历时子项顺序与页面渲染顺序一致。
这种"状态全部上提到父级、子项只读派生 + 事件上抛"的设计,使得外部可以直接通过修改 v-model 数组任意增删展开项,而无需依赖组件实例方法。