uni-app x 折叠面板组件 uni-collapse 完整指南:手风琴模式、externalClass 样式定制与暗黑模式适配
uni-app x 折叠面板组件 uni-collapse 完整指南:手风琴模式、externalClass 样式定制与暗黑模式适配
折叠面板(Collapse)是移动端最常见的交互组件之一,用于在有限空间内收纳大量内容。本指南以 uni-app x(UTS 语法)生态中的 uni-collapse / uni-collapse-item 组件为核心,系统讲解其手风琴模式、标题栏与箭头的多层级样式定制(externalClass 体系)、具名插槽自定义标题、事件回调与暴露方法,并结合当前开源仓库的源码实现(uni-collapse.vue、uni-collapse-item.vue)剖析父子组件通信与展开动画的底层原理。读完本文,你将能够直接在自己的 uni-app x 页面中落地可复用的折叠面板,并完成暗黑模式适配。
组件概览:一个父容器 + 若干子面板
uni-collapse 折叠面板组件由两个部分组合而成:
uni-collapse:父容器,负责管理多个子面板的整体展开策略(是否开启手风琴模式);uni-collapse-item:子面板,每个子面板包含顶部标题栏和通过默认插槽(default slot)传入的内容区。
一个折叠面板组件可以包含若干个 uni-collapse-item。二者属于 uni ext component(扩展组件),使用前需在项目中引入对应插件后即可在页面中以组件标签形式直接使用。
基本用法:最小的可运行示例
最简单的用法如下:外层 uni-collapse 开启手风琴模式(accordion),内层通过 title 属性指定标题文字、通过 :open 控制默认展开状态:
<uni-collapse :accordion="true">
<uni-collapse-item title="标题 1" :open="true">
<view><text>内容 1</text></view>
</uni-collapse-item>
<uni-collapse-item title="标题 2">
<view><text>内容 2</text></view>
</uni-collapse-item>
</uni-collapse>
要点说明:
:open="true"表示页面加载后该面板默认展开;- 未设置
open的面板默认收起; - 内容区由默认插槽承载,可以是任意视图结构(
view、text、列表、表单等); - 手风琴模式下同一时间仅允许一个子项展开,点击已展开面板的标题会将其收起。
accordion 属性:手风琴模式与普通模式
uni-collapse 父容器目前仅有唯一的对外属性 accordion:
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| accordion | boolean | true | 是否开启手风琴模式,开启后同一时间仅允许一个子项展开 |
accordion=true(默认):同一时间只保持一个面板展开,展开新面板时自动收起其他已展开面板,适合"设置项分组""FAQ 问答"等场景;accordion=false:允许多个面板同时展开,适合"分类目录""多分组折叠列表"等场景。
从源码可以看到手风琴逻辑的实现位于父组件 uni-collapse.vue:
function toggle(elId:string) {
// 开启手风琴效果才回关闭其他
if (props.accordion && child_nodes.length > 0) {
child_nodes.forEach((item : ItemChildType) => {
const is_open = item.is_open.value as boolean
if (is_open && item.elId != elId) {
item.is_open.value = false
item.openOrClose(false)
}
})
}
}
即:仅当 accordion 为真时,父组件遍历注册表中的子面板,把"已展开且不是当前点击项"的面板全部收起。子面板在挂载时通过 provide('uni-collapse-register-child', registerChild) 注入的注册函数登记自身(含 is_open 响应式状态、唯一 elId 与 openOrClose 方法),父子组件间的协作类型定义在 item.type.uts 中:
export type ItemChildType = {
openCollapse ?: (show : boolean) => void
openOrClose: (show : boolean) => void
is_open : Ref<boolean>,
elId : string
}
uni-collapse-item 属性详解
uni-collapse-item 子面板的属性如下表:
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| title | string | "" | 折叠项标题文字 |
| open | boolean | false | 是否默认展开 |
| disabled | boolean | false | 是否禁用,禁用后点击标题不会展开或收起 |
| titleWrapClass | string(string.ClassString) | "" | 标题行(整行容器)的样式类 |
| titleClass | string(string.ClassString) | "" | 标题文字的基础样式类 |
| titleOpenClass | string(string.ClassString) | "" | 展开状态下叠加到标题文字的样式类 |
| titleDisabledClass | string(string.ClassString) | "" | 禁用状态下叠加到标题文字的样式类 |
| contentWrapClass | string(string.ClassString) | "" | 内容外层容器的样式类 |
| arrowClass | string(string.ClassString) | "" | 箭头图标的基础样式类 |
| arrowOpenClass | string(string.ClassString) | "" | 展开状态下叠加到箭头图标的样式类 |
| arrowDisabledClass | string(string.ClassString) | "" | 禁用状态下叠加到箭头图标的样式类 |
| @change | Event | - | 面板展开状态变化时触发,回调参数为 open: boolean |
其中,所有 *Class 属性的类型为 string.ClassString,即 UTS 中的样式类字符串类型。disabled 的语义在源码 uni-collapse-item.vue 中体现为:点击标题后首先判断 props.disabled,为真则直接 return,不响应点击、不触发展开或收起,也不会触发 change 事件。
标题栏与箭头的多层级样式定制(externalClass 体系)
组件将标题区拆分为"容器层 + 文字层 + 箭头层"三层结构,每一层都提供了独立的样式类入口,可以做到像素级定制:
- 标题栏容器可通过
title-wrap-class自定义背景等容器样式; - 标题文字可通过
title-class自定义基础文字样式; - 标题展开态可通过
title-open-class自定义激活文字样式,组件默认使用透明度区分状态,便于外部继续覆盖文字颜色; - 标题禁用态可通过
title-disabled-class自定义禁用文字样式,若同时存在展开态和禁用态,以禁用态为最终状态; - 内容外层容器可通过
content-wrap-class自定义背景等容器样式; - 标题栏右边默认带有箭头,可通过
arrow-class自定义箭头样式。该箭头是一个view,由 2 条直角边的边框构成,再通过旋转形成箭头形态; - 箭头展开态和禁用态可分别通过
arrow-open-class、arrow-disabled-class覆盖,组件默认同样使用透明度区分状态,便于外部继续覆盖箭头颜色; - 如果完全不想要默认包含的 title 和 arrow,可以传入名为
title的具名插槽来整体替换。
一个完整的 externalClass 使用示例:
<uni-collapse-item
title="自定义样式"
title-wrap-class="my-title-wrap-class"
title-class="my-title-class"
title-open-class="my-title-open-class"
content-wrap-class="my-content-wrap-class"
arrow-class="my-arrow-class"
arrow-open-class="my-arrow-open-class"
>
<view><text>内容区域</text></view>
</uni-collapse-item>
对应的样式类可以这样定义(示意):
.my-title-wrap-class {
background-color: #f0f9ff; /* 标题行背景 */
}
.my-title-class {
font-size: 16px;
font-weight: 700;
}
.my-title-open-class {
color: #2563eb; /* 展开态标题文字 */
}
.my-content-wrap-class {
background-color: #f8fafc; /* 内容区背景 */
}
.my-arrow-class {
width: 10px;
height: 10px;
border-right-width: 2px;
border-bottom-width: 2px;
}
.my-arrow-open-class {
border-right-color: #2563eb; /* 展开态箭头颜色 */
border-bottom-color: #2563eb;
}
箭头的实现原理
从源码 uni-collapse-item.vue 可以看到箭头默认样式的实现:一个 8×8 的 view,通过 border-right + border-bottom 两条边框构造直角,再配合 transform: rotate(45deg) 旋转出"向下"的箭头;展开态则旋转为 rotate(-135deg) 变为"向上",并带有 150ms 的 transition-property: transform 过渡动画。因此外部可以通过 arrow-class 覆盖其尺寸(宽高)、边框粗细与边框颜色,从而实现箭头放大、加粗、变色等效果。
具名插槽 title:完全自定义标题区域
当默认的"文字 + 箭头"组合无法满足需求(例如需要在标题前加图标、状态徽标、辅助说明文字)时,可以使用名为 title 的具名插槽整体替换标题栏。插槽会透出两个状态参数:
| 插槽名 | 说明 | 插槽参数 |
|---|---|---|
| default | 面板内容 | - |
| title | 自定义标题区域 | open: boolean、disabled: boolean |
<uni-collapse-item>
<template #title="{ open, disabled }">
<view class="custom-title">
<view class="custom-title-icon"></view>
<text class="custom-title-text">{{ open ? '已展开' : '未展开' }}</text>
</view>
</template>
<view><text>自定义标题内容</text></view>
</uni-collapse-item>
使用插槽时组件不再渲染默认的 title 文字与箭头,标题区域的布局、交互元素完全由你掌控;open 与 disabled 两个参数可以联动样式,例如:
<template #title="{ open, disabled }">
<view class="slot-title-wrap">
<view class="slot-title-icon" :class="{'slot-title-icon--open': open, 'slot-title-icon--disabled': disabled}"></view>
<text class="slot-title-text" :class="{'slot-title-text--open': open}">{{ open ? '已展开' : '点击展开' }}</text>
</view>
</template>
事件与 Expose 方法
事件(Events)
uni-collapse-item 提供一个 change 事件:
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 面板展开状态变化时触发 | open: boolean |
监听示例:
<uni-collapse-item title="面板 A" :open="true" @change="onPanelAChange">
<view><text>内容</text></view>
</uni-collapse-item>
function onPanelAChange(open : boolean) {
console.log('面板 A 状态:', open ? '展开' : '收起')
}
Expose 方法
uni-collapse-item 通过 defineExpose 对外暴露两个方法(见源码 uni-collapse-item.vue):
| 方法名 | 说明 |
|---|---|
| openCollapse(open: boolean) | 执行展开/收起,会遵循 disabled 和手风琴逻辑 |
| openOrClose(open: boolean) | 直接切换内容显示状态 |
两者的区别需要重点说明:
openCollapse是"完整路径":它先判断disabled(禁用则直接返回),再通知父容器执行手风琴收拢逻辑(通过collapseToggle(elId)关闭其他已展开面板),最后更新自身is_open并触发动画,因此它遵循禁用态与手风琴规则,是组件内部点击标题时的同一套逻辑;openOrClose是"直接路径":仅负责切换内容显示状态(display 与 transform/opacity 动画),不经过禁用判断,也不参与手风琴收拢。父组件在关闭其他面板时正是直接调用它的openOrClose(false)。
通过 ref 调用暴露方法的示例:
<uni-collapse-item ref="itemRef" title="示例面板">
<view><text>内容</text></view>
</uni-collapse-item>
const itemRef = ref<UniCollapseItemInstance | null>(null)
// 编程式展开(遵循手风琴与禁用逻辑)
itemRef.value?.openCollapse(true)
// 直接切换显示状态(跳过禁用与手风琴逻辑)
itemRef.value?.openOrClose(true)
源码级原理:父子通信与展开动画
父组件注册表与 provide/inject
uni-collapse 与 uni-collapse-item 之间通过 Vue 的 provide / inject 建立联系(uni-collapse.vue):
provide('uni-collapse-register-child', registerChild)
provide('k-collapse-child-toggle', toggle)
子组件在 onMounted 时调用 registerChild 将自身(is_open、elId、openOrClose)登记进父组件的 child_nodes 数组,从而让父组件能够在手风琴模式下统一调度所有子面板。这套"注册表 + 中央调度"的模式保证了手风琴逻辑收敛在父组件一处,子组件只需关心自身状态与动画。
展开/收起的动画实现
展开动画不是简单的条件渲染,而是"display 切换 + transform/opacity 过渡"的组合(uni-collapse-item.vue):
function openOrClose(open : boolean) {
setTimeout(() => {
box_is_open.value = !box_is_open.value
}, 10)
const bNode = boxRef.value?.style!;
const cNode = contentRef.value?.style!;
let hide = open ? 'flex' : 'none';
const opacity = open ? "1" : "0"
let ani_transform = open ? 'translateY(0)' : 'translateY(-100%)';
bNode.setProperty('display', hide);
nextTick(() => {
setTimeout(() => {
cNode.setProperty('transform', ani_transform);
cNode.setProperty('opacity', opacity);
}, 10)
})
}
对应样式中的关键设计:
- 内容外层
.uni-collapse-item__content默认display: none、overflow: hidden,展开时切换为flex; - 内容盒子
.uni-collapse-item__content-box默认transform: translateY(-100%)、opacity: 0,展开时过渡到translateY(0)、opacity: 1; - 过渡时长统一为 150ms(
transition-property: transform, opacity); - 这段逻辑在微信小程序端通过条件编译(
// #ifndef MP-WEIXIN)做了隔离,小程序端仅切换box_is_open状态。
暗黑模式适配
组件默认样式大量使用 CSS 变量(如 --list-background-color、--text-color、--active-color),基础视觉会随全局主题变量变化。若需要更精细的暗色定制,官方推荐的组合方式是"组件 class + externalClass 双通道":
- 用组件
class统一控制根节点和内容区等外层视觉(例如给根节点传入暗色卡片背景类); - 用
title-wrap-class、title-class、title-open-class、content-wrap-class、arrow-class、arrow-open-class等 externalClass 覆盖标题区、内容外层和箭头样式; - 默认展开态和禁用态主要通过透明度表达,减少颜色覆盖时的交叉冲突——即使外部覆盖了基础颜色,透明度仍能直观地区分展开/收起、可用/禁用状态。
示例:
<uni-collapse-item
class="dark-collapse-item"
title="暗色标题"
:open="true"
title-wrap-class="dark-title-wrap"
title-class="dark-title-text"
title-open-class="dark-title-text-open"
content-wrap-class="dark-content-wrap"
arrow-class="dark-arrow"
arrow-open-class="dark-arrow-open"
>
<view class="content-box dark-content-box">
<text class="content-text dark-content-text">暗色内容</text>
</view>
</uni-collapse-item>
.dark-collapse-item { background-color: #111827; }
.dark-title-wrap { background-color: #111827; }
.dark-title-text { color: #f8fafc; }
.dark-title-text-open { color: #60a5fa; }
.dark-content-wrap { background-color: #1f2937; }
.dark-arrow { border-right-color: #cbd5e1; border-bottom-color: #cbd5e1; }
.dark-arrow-open { border-right-color: #60a5fa; border-bottom-color: #60a5fa; }
禁用态示例(通过 title-disabled-class、arrow-disabled-class 表达弱化色):
<uni-collapse-item
class="dark-collapse-item-muted"
title="暗色禁用项"
:disabled="true"
title-wrap-class="dark-title-wrap-muted"
title-class="dark-title-text"
title-disabled-class="dark-title-text-disabled"
content-wrap-class="dark-content-wrap-muted"
arrow-class="dark-arrow"
arrow-disabled-class="dark-arrow-disabled"
>
<view class="content-box">禁用内容</view>
</uni-collapse-item>
兼容性与注意事项
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| 5.07 | 5.07 | 5.07 | 5.07 | 5.07 |
兼容性说明:上述版本号对应 HBuilderX / uni-app x 的运行基础版本要求,五个端(Web、微信小程序、Android、iOS、HarmonyOS)均要求 5.07 及以上。
注意事项:
- 本组件在 Android 上需 HBuilderX 5.09+,在低版本上箭头变色 + 旋转时,在 Android 上会消失,因此若目标包含 Android 低版本环境,请务必升级到 5.09 及以上;
disabled状态下面板不会响应点击,也不会触发展开/收起与change事件;- 当同时存在展开态与禁用态时,标题文字以禁用态样式为最终状态。
仓库中的真实使用场景
当前仓库的 tabBar 页面(component.uvue)就是一个典型的嵌套折叠菜单实践:外层 uni-collapse 承载一级菜单项,内层再嵌一层 uni-collapse 实现二级菜单分组,展示了折叠面板作为导航目录容器的用法:
<uni-collapse>
<uni-collapse-item ref="categoryRef" v-for="menuItem in menu" :key="menuItem.id" :title="menuItem.name" ...>
...
<uni-collapse v-else style="width: 100%;border-top: 1px solid #f0f0f0;">
<uni-collapse-item :title="childMenuItem.name" class="uni-panel" style="margin-bottom: 0;">
...
</uni-collapse-item>
</uni-collapse>
</uni-collapse-item>
</uni-collapse>
这意味着你可以直接借鉴该页面的嵌套结构,实现"分类 → 子分类 → 详情"的多级折叠导航。组件源码位于 src/components/uni-collapse 与 src/components/uni-collapse-item,examples 目录下的 hello-uvue 示例项目同样内置了 uni-collapse-item 的独立实现可供对照学习。
小结
uni-collapse / uni-collapse-item 是 uni-app x 中开箱即用的折叠面板方案:通过 accordion 一行属性即可在手风琴与多开模式间切换;externalClass 体系将标题容器、标题文字、内容区、箭头拆分为独立样式入口,支持展开态/禁用态叠加;title 具名插槽支持完全自定义标题结构;change 事件与 openCollapse / openOrClose 暴露方法让编程式控制成为可能。结合源码理解其"父组件注册表 + provide/inject 调度"与"display + transform/opacity 双阶段动画"的实现后,你就能在复杂业务中灵活扩展这套组件。