现代前端项目中,折叠面板已经成为高频出现的交互组件,尤其在中后台管理系统、商品详情页和复杂表单里,它能将大量信息按优先级进行折叠收纳,帮助用户降低阅读负担。Vue 3 的组合式 API 让状态逻辑的拆分与复用变得更加自然,这为封装一个既可手风琴折叠又能多开展示的面板组件提供了良好基础。实现这一组件的关键在于明确父子组件之间的状态归属,并根据不同模式选择单值或数组结构来维护展开项。

组件架构设计与状态管理机制
折叠面板通常需要拆分为两个粒度的组件:外层容器组件和内层面板项组件。容器组件承担状态管理、模式判断以及事件派发等工作,而面板项组件只关心自身标题、内容插槽以及当前是否处于展开状态。这样的层级划分不仅让职责更加清晰,也便于后续扩展标题图标、禁用状态、自定义样式等能力。
如果让每个面板项组件各自维护展开状态,手风琴模式就会变得难以实现,因为面板项之间无法直接感知对方的展开与否。因此必须采用状态提升的思路,将当前展开项统一交由容器组件维护。子组件不再持有独立状态,而是通过外部传入的数据来判断自身应该如何渲染,并通过调用外部方法来请求状态变更。
在 Vue 3 中,如果通过逐层 props 和 emit 传递状态,当组件层级变深时维护成本会明显上升。为此可以使用 provide 与 inject 建立依赖注入通道。容器组件使用 provide 暴露当前活动项和切换函数,任意后代面板项都能通过 inject 读取,既能保持单向数据流,又避免了繁琐的参数透传。
// context.js:定义注入键,统一父子通信标识
export const collapseContextKey = Symbol('collapseContextKey');
// Collapse.vue:容器组件基础结构
import { provide, computed } from 'vue';
import { collapseContextKey } from './context';
const props = defineProps({
modelValue: {
type: [String, Number, Array],
default: []
},
accordion: {
type: Boolean,
default: false
}
});
const emits = defineEmits(['update:modelValue', 'change']);
// 根据模式将 modelValue 统一为数组形式
const activeNames = computed(() => {
if (Array.isArray(props.modelValue)) {
return props.modelValue;
}
if (props.accordion) {
return props.modelValue ? [props.modelValue] : [];
}
return [];
});
const toggleItem = (name) => {
// 具体的切换分支将在后续代码中展开
};
provide(collapseContextKey, {
activeNames,
toggleItem
});
手风琴模式的实现原理与代码落地
手风琴模式的核心约束是:同一时刻最多只允许一个面板展开。当用户点击一个新面板时,之前展开的面板必须自动收起,形成互斥效果。这种模式非常适合筛选面板、侧边导航、步骤说明等需要用户聚焦单一信息区域的场景。
在状态表示上,手风琴模式天然适合单值结构。容器组件内部维护一个字符串或数字值,用于记录当前展开面板的 name。当用户触发切换时,如果点击项与当前项相同,说明用户希望收起该面板,此时将状态置为空字符串;如果点击项与当前项不同,则直接将当前项替换为点击项。
面板项组件通过 inject 拿到容器组件暴露的 activeNames 与 toggleItem 后,只需要判断自身 name 是否存在于活动数组中。手风琴模式下活动数组最多只有一个元素,因此 includes 判断非常直观,不需要额外处理复杂的分支。
// Collapse.vue 中 toggleItem 的手风琴分支
const toggleItem = (name) => {
if (props.accordion) {
const currentName = activeNames.value[0] || '';
const nextValue = currentName === name ? '' : name;
emits('update:modelValue', nextValue);
emits('change', nextValue);
return;
}
// 多开模式分支将在下一小节完善
};
// CollapseItem.vue:面板项组件核心逻辑
import { inject, computed } from 'vue';
import { collapseContextKey } from './context';
const props = defineProps({
name: {
type: [String, Number],
required: true
},
title: {
type: String,
default: ''
}
});
const { activeNames, toggleItem } = inject(collapseContextKey);
const isActive = computed(() => {
return activeNames.value.includes(props.name);
});
const handleClick = () => {
toggleItem(props.name);
};
多开模式的设计与无缝切换方案
多开模式允许用户同时展开多个面板,各面板之间互不影响。这种模式适合商品参数、评价、售后详情等需要并行查看的内容区域。与手风琴模式不同,多开模式的状态必须使用数组来表示,数组中保存所有当前处于展开状态的面板名称。