NutUI Collapse 折叠面板组件完全指南:v-model 双向绑定、手风琴模式与主题定制
NutUI Collapse 折叠面板组件完全指南:v-model 双向绑定、手风琴模式与主题定制
折叠面板(Collapse)是移动端页面中最常见的交互组件之一:把内容收进多个可折叠的区块中,用户点击标题即可展开或收起,从而在有限屏幕内承载更多信息。本文以京东风格移动端 Vue 组件库 NutUI 中的 Collapse 组件为对象,完整讲解其安装方式、六类核心用法、完整 API 参数、事件回调与 CSS 变量主题定制方案,并结合仓库源码剖析其 provide/inject 通信与高度过渡动画的底层实现,帮助你直接照搬到真实项目中。
组件概述
Collapse 折叠面板由两个组件组成:
NutCollapse:外层容器,负责维护「当前展开面板」的状态,并通过 Vue 的provide向下传递控制方法;NutCollapseItem:单个面板,负责渲染标题栏与内容区,并通过inject获取容器的状态来实现展开/收起。
官方对它的定位非常明确:将内容放置在多个折叠面板中,点击面板标题即可展开或收缩内容。在 NutUI 仓库中,H5 端实现位于 collapse.vue 与 collapse-item.vue,Taro 多端版本位于同目录的 .taro.vue 文件,二者逻辑完全一致,仅 DOM 渲染方式不同。
安装与引入
Collapse 和 CollapseItem 是两个独立组件,需要分别注册。NutUI 支持按需引入,示例代码如下:
import { createApp } from 'vue'
import { Collapse, CollapseItem } from '@nutui/nutui'
const app = createApp()
app.use(Collapse)
app.use(CollapseItem)
如果使用 Taro 多端小程序,则将包名替换为 @nutui/nutui-taro,引入方式完全一致(参见 doc.taro.md)。
从源码看,两个组件的入口文件都通过 withInstall 包装后再导出,因此既可以 app.use() 全局注册,也可以 import { Collapse } 后直接局部使用:
- Collapse 入口见 collapse/index.ts(H5)与 collapse/index.taro.ts(Taro);
- CollapseItem 入口见 collapseitem/index.ts。
基础用法
使用折叠面板的前提是每个 CollapseItem 必须设置 name 属性作为唯一标识符,后续的 v-model 绑定、事件回调都依赖这个 name 来定位具体面板。官方基础示例(demo/basic.vue)展示了标题栏的三种写法:
<template>
<nut-collapse>
<nut-collapse-item name="name1" title="Title 1"> Content 1 </nut-collapse-item>
<nut-collapse-item name="name2" title="Title 2" value="Description"> Content 2 </nut-collapse-item>
<nut-collapse-item name="name3">
<template #title> Title 3 </template>
<template #value> Description </template>
Content 3
</nut-collapse-item>
</nut-collapse>
</template>
- 第 1 个面板:只通过
title属性传入左侧标题; - 第 2 个面板:通过
value属性传入右侧描述文字; - 第 3 个面板:通过具名插槽
#title/#value自定义标题栏两侧内容。
注意:当 title / value 同时存在属性与插槽两种传入方式时,属性的优先级更高(源码中 $slots.title 存在时优先渲染插槽,否则渲染属性值)。
v-model 双向绑定与 change 事件
默认(非手风琴)模式下,v-model 绑定的是一个数组,数组中的每一项对应一个展开面板的 name。官方示例(demo/data.vue):
<template>
<nut-collapse v-model="val" @change="change">
<nut-collapse-item name="name1" title="Title 1"> Content 1 </nut-collapse-item>
<nut-collapse-item name="name2" title="Title 2"> Content 2 </nut-collapse-item>
<nut-collapse-item name="name3" title="Title 3"> Content 3 </nut-collapse-item>
</nut-collapse>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const val = ref(['name2', 'name3'])
const change = (modelValue: string[], name: string, status: boolean) => {
console.log(modelValue, name, status)
}
</script>
初始状态即展开 name2 与 name3 两个面板。每次切换面板都会触发 change 事件,回调参数为:
| 参数 | 说明 |
|---|---|
modelValue |
切换后的完整展开列表(或手风琴模式下的单个 name) |
currName |
本次被点击切换的面板 name |
status |
本次操作后的展开状态,true 表示展开,false 表示收起 |
底层实现:provide/inject 通信
在 collapse.vue 源码中,容器组件维护一个内部状态 innerValue,并通过 COLLAPSE_KEY(定义于 types.ts)向所有子面板注入两个方法:
provide(COLLAPSE_KEY, {
updateVal, // 切换面板时由 CollapseItem 调用
isExpanded // 判断某个 name 当前是否展开
})
点击某个面板标题时,CollapseItem 执行 parent.updateVal(props.name);容器收到 name 后根据模式更新值并触发 update:modelValue 与 change:
- 非手风琴模式:若 name 已在数组中则过滤移除(收起),否则追加(展开);
- 手风琴模式:若当前展开的正是该 name 则置空(收起),否则切换为该 name;
- 当 v-model 未传入且非手风琴模式时,内部默认初始化为空数组
[];若传入非数组类型,源码会输出console.warn('[NutUI] <Collapse> 未开启手风琴模式时 v-model 应为数组')警告。
手风琴模式(Accordion)
通过给 NutCollapse 设置 accordion 属性即可开启手风琴模式:最多只能展开一个面板,此时 v-model 绑定的是单个 name 值(string | number)而非数组。官方示例(demo/accordion.vue):
<template>
<nut-collapse v-model="val" accordion @change="change">
<nut-collapse-item name="name1" title="Title 1"> Content 1 </nut-collapse-item>
<nut-collapse-item name="name2" title="Title 2"> Content 2 </nut-collapse-item>
<nut-collapse-item name="name3" title="Title 3"> Content 3 </nut-collapse-item>
</nut-collapse>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const val = ref(['name2', 'name3'])
const change = (modelValue: string[], name: string, status: boolean) => {
console.log(modelValue, name, status)
}
</script>
提示:示例中初始值仍为数组,手风琴模式下实际只会生效第一个匹配的面板;更规范的写法是直接
const val = ref('name2')。
在 collapse.spec.tsx 的 should props accordion 测试用例中,同时展开了两个面板的初始值,最终断言展开的标题图标(.nut-collapse-item__title-icon--expanded)数量为 1,验证了手风琴模式下「最多展开一个」的约束;随后点击另一个面板,展开数量仍为 1,证明切换是互斥的。
自定义折叠图标
折叠图标支持两种自定义方式(官方示例 demo/custom.vue):
方式一:slots.icon 插槽(4.2.0 版本新增)
<template>
<nut-collapse>
<nut-collapse-item name="name1" title="Title 1">
<template #icon> <Notice /> </template>
Content 1
</nut-collapse-item>
</nut-collapse>
</template>
<script setup lang="ts">
import { Notice } from '@nutui/icons-vue'
</script>
方式二:props.icon 属性
<template>
<nut-collapse>
<nut-collapse-item name="name2" title="Title 2" :icon="Follow"> Content 2 </nut-collapse-item>
</nut-collapse>
</template>
<script setup lang="ts">
import { Follow } from '@nutui/icons-vue'
</script>
两者均接收 @nutui/icons-vue 图标组件。从 collapse-item.vue 源码看,其渲染优先级为:$slots.icon 插槽存在时渲染插槽内容,否则通过 renderIcon(icon) 动态渲染传入的图标组件;未传入任何图标时,默认使用 DownArrow(向下箭头)图标。
与图标相关的还有 rotate 属性:点击折叠/展开时图标的旋转角度,仅在自定义图标模式下生效,默认值为 180(即展开时默认图标旋转 180°)。容器源码通过行内样式 transform: rotate(...) 控制旋转,同时 index.scss 中为展开状态定义了 .nut-collapse-item__title-icon--expanded 类名,测试用例也通过该类名断言面板展开状态。
禁用状态
给单个 CollapseItem 设置 disabled 属性后,该面板标题栏将不可点击,样式上呈现灰色并取消点击效果。官方示例(demo/disabled.vue):
<template>
<nut-collapse>
<nut-collapse-item name="name1" title="Title 1"> Content 1 </nut-collapse-item>
<nut-collapse-item name="name2" title="Title 2" disabled> Content 2 </nut-collapse-item>
<nut-collapse-item name="name3" title="Title 3" disabled> Content 3 </nut-collapse-item>
</nut-collapse>
</template>
从源码看,禁用通过两个层面实现:
- 样式层:标题栏追加
.nut-collapse-item__title--disabled类,对应 index.scss 中的pointer-events: none与color: $collapse-item-disabled-color,彻底阻断点击并置灰; - 测试层:collapse.spec.tsx 的
should nut-collapse-item props用例断言了禁用标题的类名包含--disabled。
设置固定内容(不折叠)
extra 插槽用于在标题下方渲染固定内容——该区域始终展示,不随面板展开/收起。适合放置常驻的操作按钮、提示信息等。官方示例(demo/extra.vue):
<template>
<nut-collapse>
<nut-collapse-item name="name1" title="Title 1">
<template #extra> Extra Content </template>
Content 1
</nut-collapse-item>
<nut-collapse-item name="name2" title="Title 2"> Content 2 </nut-collapse-item>
</nut-collapse>
</template>
从 collapse-item.vue 源码可以看到,extra 内容渲染在独立的 .nut-collapse__item-extraWrapper 容器中,该容器在 index.scss 中被设置为 height: auto,与参与高度动画的 .nut-collapse__item-wrapper 相互独立,因此永远不会被折叠。
完整 API 一览
Collapse Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model | 当前展开面板的 name |
手风琴模式:string | number 非手风琴模式:(string | number)[] |
- |
| accordion | 是否开启手风琴模式 | boolean | false |
CollapseItem Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name | 唯一标识符,必填 | string | number | -1 |
| title | 标题栏左侧内容,支持插槽传入(props 传入的优先级更高) |
string | - |
| value | 标题栏右侧内容,支持插槽传入(props 传入的优先级更高) |
string | - |
| icon | 标题栏左侧图标组件,等同于 nutui-icon 组件 |
- | - |
| label | 标题栏描述信息 | number | string | - |
| rotate | 点击折叠和展开的旋转角度,在自定义图标模式下生效 | string | number | 180 |
| disabled | 标题栏是否禁用 | boolean | false |
| border | 是否显示边框 | boolean | true |
CollapseItem Slots
| 名称 | 说明 |
|---|---|
| title | 标题栏左侧内容插槽 |
| value | 标题栏右侧内容插槽 |
| extra | 设置标题下固定内容(不折叠) |
| icon(4.2.0) | 设置自定义图标 |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 切换面板时触发 | (modelValue, currName, status) |
类型定义(v4.3.1)
从 v4.3.1 起,组件导出完整的 TypeScript 类型定义,可直接从包中导入:
import type {
CollapseValue,
CollapseProps,
CollapseInstance,
CollapseItemProps,
CollapseItemInstance
} from '@nutui/nutui';
其中 CollapseValue 定义于 types.ts:
export type CollapseValue = string | number | (string | number)[]
这恰好对应手风琴(单个 name)与非手风琴(name 数组)两种模式下的 v-model 值类型。CollapseProps / CollapseItemProps 由各组件源码中的 defineProps 类型直接导出,CollapseInstance / CollapseItemInstance 则是组件实例类型(ComponentPublicInstance & InstanceType<typeof 组件>),见 collapse/index.ts 与 collapseitem/index.ts。
展开动画的源码级实现
Collapse 的展开/收起动画并非简单地切换 v-show,而是通过测量内容高度 + CSS transition 过渡 height 实现的,核心逻辑位于 collapse-item.vue:
- 容器通过
watch(expanded)监听面板展开状态; - 展开时(open):先将高度置为
0px,在双重requestAnimationFrame后测量内容实际高度contentRef.offsetHeight,再写入具体像素值,触发height 0.3s ease-in-out过渡(见 index.scss 的 transition 定义); - 收起时(close):先把高度定格为当前内容高度,再在下一帧置为
0px; - 过渡结束后通过
transitionend事件将高度设为auto,避免固定像素值限制内容自适应。
这也是为什么展开状态的内容区 DOM 类名为 .nut-collapse__item-wrapper__content、而收起/展开切换的测试(见 collapse.spec.tsx 的 should props active 用例)通过断言展开图标类名来验证状态的原因。Taro 版本 collapse-item.taro.vue 实现了相同的逻辑,只是测量改用 Taro 的 API 并生成带随机 id 的内容节点。
主题定制:CSS 变量
Collapse 组件将全部关键样式提取为 CSS 变量,可通过 NutUI 的 ConfigProvider 组件 或直接覆盖变量实现主题定制。完整变量清单如下:
| 名称 | 默认值 |
|---|---|
| --nut-collapse-item-padding | 13px 36px 13px 26px |
| --nut-collapse-item-color | #666666 |
| --nut-collapse-item-disabled-color | #c8c9cc |
| --nut-collapse-item-icon-color | #666666 |
| --nut-collapse-item-font-size | var(--nut-font-size-2) |
| --nut-collapse-item-line-height | 24px |
| --nut-collapse-item-sub-title-color | #666666 |
| --nut-collapse-wrapper-content-padding | 12px 26px |
| --nut-collapse-wrapper-empty-content-padding | 0 26px |
| --nut-collapse-wrapper-content-color | #666666 |
| --nut-collapse-wrapper-content-font-size | var(--nut-font-size-2) |
| --nut-collapse-wrapper-content-line-height | 1.5 |
| --nut-collapse-wrapper-content-background-color | var(--nut-white) |
这些变量与 collapseitem/index.scss 中引用的 SCSS 变量(如 $collapse-item-padding、$collapse-item-disabled-color、$collapse-wrapper-content-padding 等)一一对应,是组件样式体系的一等公民。除了变量化定制,该样式文件还内置了深色主题适配(.nut-theme-dark)与细线边框(.nut-collapse-item__border 下基于 ::after 伪元素 + scaleY(0.5) 实现的 1px 边框)。
例如将面板间距与内容配色整体调整为品牌风格:
:root {
--nut-collapse-item-padding: 16px 40px 16px 30px;
--nut-collapse-item-color: #1a1a1a;
--nut-collapse-wrapper-content-background-color: #f7f8fa;
}
总结
NutUI 的 Collapse 折叠面板组件以 name 唯一标识 + v-model 状态驱动为核心设计,通过 provide/inject 实现容器与子面板的解耦通信,既支持多面板数组展开,也支持手风琴互斥模式;配合 title / value / icon / extra 插槽与属性、disabled 禁用、rotate 旋转角等细节能力,以及全套 CSS 变量主题定制,可以覆盖绝大多数移动端折叠交互场景。文中涉及的所有示例均来自仓库 demo 目录,接口行为均有 collapse.spec.tsx 测试用例与 collapse.vue、collapse-item.vue 源码支撑,读者可放心在项目中直接复用。