简单记录一个vue3手风琴组件
<!-- 统计面板(通用容器) 职责:统一每个统计模块的标题/折叠/筛选区/正文布局, 对应原 mv_statistics.html 中 .bannerbor + .panelDiv 折叠 的结构。 - title: 面板标题(可点击折叠/展开) - note: 红色注释(如下穿/跨越管长来源说明) - #filters 具名插槽:筛选下拉/日期等控件 - 默认插槽:图表 + 表格正文 --> <template> <div class="stat-panel"> <div class="panel-head"> <span class="title" @click="collapsed = !collapsed"> {{ title }} <i class="arrow" :class="{ 'arrow--up': !collapsed }"></i> </span> <div class="filters" @click.stop> <slot name="filters"></slot> </div> </div> <div class="panel-body" v-show="!collapsed"> <div v-if="note" class="note">{{ note }}</div> <slot></slot> </div> </div> </template> <script lang="ts" setup> import { ref } from "vue"; defineProps({ // 面板标题 title: { type: String, default: "", }, // 红色提示语(可选) note: { type: String, default: "", }, }); // 是否折叠面板(对应原页面点击 panelDiv 切换显隐) const collapsed = ref(false); // 占位:供父级 ResizeObserver 统一调用 sizeChange const sizeChange = () => {}; defineExpose({ sizeChange }); </script> <style lang="scss" scoped> .stat-panel { background: #ffffff; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.1); box-sizing: border-box; & + .stat-panel { margin-top: 20px; } .panel-head { display: flex; align-items: center; padding: 16px 20px; .title { position: relative; flex-shrink: 0; color: #161c21; font-size: 18px; line-height: 24px; cursor: pointer; user-select: none; padding-left: 12px; &::before { display: block; content: ""; position: absolute; left: 0; top: 50%; width: 4px; height: 16px; transform: translateY(-50%); background: #3d7dff; } .arrow { display: inline-block; width: 0; height: 0; margin-left: 12px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #404a53; transition: transform 0.2s; vertical-align: middle; } .arrow--up { transform: rotate(180deg); } } .filters { margin-left: auto; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } } .panel-body { padding: 0 20px 20px; box-sizing: border-box; } .note { color: red; font-size: 12px; margin-bottom: 10px; } } </style>

好,现在你已经学会走路了,那么我们接下来就要开始和苏炳添苏神一起比赛百米赛跑了!!!
这个 StatPanel 组件虽然代码量不大,但非常经典,涵盖了 Vue 3 日常开发中大量高频且核心的知识点。我们可以从以下四个维度进行拆解: 1. 组件通信与插槽(Slots) 具名插槽(Named Slots):
代码中使用了 <slot name="filters"></slot>。这允许父组件在特定的位置(如面板右上角)注入自定义的筛选控件,而不影响面板的整体结构。 默认插槽(Default Slot):
<slot></slot> 用于承载面板的核心内容(如图表、表格)。这种“外壳固定,内容自定义”的设计是构建高阶容器组件(Layout Components)的标准做法。 事件冒泡控制:
在 <div class="filters" @click.stop> 中使用了 .stop 修饰符。因为外层 title 绑定了点击折叠事件,如果不阻止冒泡,用户点击筛选区时也会触发面板的折叠。 2. Vue 3 核心语法(Composition API) <script setup> 语法糖:
组件使用了 Vue 3 推荐的编译时语法糖,省去了 setup() 函数的包裹,代码更简洁。 响应式状态:
使用 ref(false) 来管理面板的折叠状态 collapsed。 Props 定义:
使用了 defineProps 来接收父组件传入的 title 和 note,并设置了默认值,保证了组件的健壮性。 暴露方法(defineExpose):
通过 defineExpose({ sizeChange }) 将组件内部的方法暴露给父组件。结合注释可以看出,这是为了配合父级使用 ResizeObserver 监听尺寸变化时,能够主动调用子组件的刷新逻辑(这在图表类组件中非常常见)。 3. 模板指令与交互 条件渲染 vs 显示隐藏: v-show="!collapsed" 用于面板正文。因为面板内容可能包含图表等重量级元素,使用 v-show 可以在折叠时隐藏内容,但保留 DOM 节点,避免展开时重新渲染图表导致性能损耗。 v-if="note" 用于红色提示语。因为提示语是轻量级文本,不存在时直接不渲染 DOM 即可。 动态 Class 绑定::class="{ 'arrow--up': !collapsed }" 使用了对象语法,根据状态动态切换 CSS 类名,实现箭头的旋转动画。 4. CSS 布局与样式隔离 Scoped 样式:<style lang="scss" scoped> 确保样式只作用于当前组件,避免污染全局。 Flexbox 弹性布局:.panel-head 使用了 display: flex 和 align-items: center 实现标题和筛选区的水平对齐。 自动右对齐:.filters 使用了 margin-left: auto,这是 Flex 布局中非常巧妙的技巧,无需使用 justify-content: space-between 就能将筛选区推到最右侧。 相邻兄弟选择器:& + .stat-panel { margin-top: 20px; } 表示只有当两个 .stat-panel 紧挨着时,才给后者加上外边距。这避免了给第一个或最后一个面板加多余间距的麻烦。 伪元素装饰:.title::before 使用伪元素在标题左侧画了一个蓝色的竖条,这是一种不增加额外 DOM 节点的纯 CSS 装饰技巧。

浙公网安备 33010602011771号