wot-design-uni 折叠面板(Collapse)完整实践:四种模式、异步展开钩子与源码级原理

原创2026-10-13 15:11:161,326 阅读
文章标签:前端UI组件移动开发小程序

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 响应式列表:

  1. 手风琴模式下直接 return:因为同一时间只能展开一项,"全部展开"没有意义;测试用例 wd-collapse.test.ts 验证了此时不会发出 update:modelValue;
  2. 布尔参数会被规整为 { expanded: options };
  3. 遍历所有子项,通过 item.$.exposed.getExpanded() 读取每个 CollapseItem 当前展开状态(该能力由 CollapseItemExpose 暴露):
    • expanded 未指定时,收集当前处于收起状态的项(即"反向切换");
    • skipDisabled: true 时,禁用项只有当前已展开的才会被保留在结果中,避免被强制收起;
    • 收集到的 names 最终整体作为新 modelValue 发出 update:modelValue 与 change。

官方演示页 collapse 演示 用五个按钮分别演示了这五种调用方式,可对照查看。

展开/收起动画的实现机制

从源码结构看,折叠动画并未使用 CSS max-height 之类近似值,而是基于真实测量的高度过渡:

  1. 点击标题后 handleClick 先 await updateExpand():此时经 getRect 测量内容容器 #collapseId 的真实高度,写入 height,等待一帧(pause())后把 expanded 置为真值(wd-collapse-item.vue);
  2. 计算属性 contentStyle 在 inited 后给内容包裹层加上 transition: height 0.3s ease-in-out,收起时 height: 0px,展开时 height: 实测高度(wd-collapse-item.vue);
  3. 过渡结束后 @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 数组任意增删展开项,而无需依赖组件实例方法。

参考路径

资源 路径
父组件实现 src/uni_modules/wot-design-uni/components/wd-collapse/wd-collapse.vue
父组件类型定义 src/uni_modules/wot-design-uni/components/wd-collapse/types.ts
子组件实现 src/uni_modules/wot-design-uni/components/wd-collapse-item/wd-collapse-item.vue
子组件类型定义 src/uni_modules/wot-design-uni/components/wd-collapse-item/types.ts
组件样式 src/uni_modules/wot-design-uni/components/wd-collapse/index.scss
单元测试 tests/components/wd-collapse.test.ts
官方演示页 src/subPages/collapse/Index.vue
官方文档 docs/component/collapse.md
登录后查看全文
wot-design-uni