前言
大家好,最近在做项目的时候发现我们系统里到处都是各种卡片样式的 UI 元素,每次都要重写一遍真的很烦。于是我花了点时间,封装了一个通用的卡片组件,今天就来分享一下我的开发思路和实现过程。希望能对大家有所帮助!
需求分析
在开始写代码前,我先梳理了一下卡片组件的常见需求:
- 支持自定义标题、内容、底部操作区
- 可配置是否显示阴影、边框
- 支持加载状态
- 支持自定义样式
- 支持卡片展开/折叠功能
- 支持卡片的移除/关闭功能
组件设计
目录结构
|- CardComponent/
|- index.vue # 主组件文件
|- CardHeader.vue # 卡片头部组件
|- CardContent.vue # 卡片内容组件
|- CardFooter.vue # 卡片底部组件
|- index.js # 导出文件
|- types.js # TypeScript 类型定义
|- style.scss # 样式文件
实现代码
首先来看主组件 index.vue
的实现:
<template>
<div
class="v-card"
:class="[
`v-card--${shadow}-shadow`,
{
'v-card--bordered': bordered,
'is-loading': loading,
'is-collapsed': !expanded
}
]"
:style="customStyle"
>
<!-- 加载状态遮罩 -->
<div v-if="loading" class="v-card__loading-mask">
<div class="v-card__loading-spinner"></div>
</div>
<!-- 卡片头部 -->
<div v-if="$slots.header || title" class="v-card__header">
<slot name="header">
<div class="v-card__title">{{ title }}</div>
<div v-if="collapsible" class="v-card__collapse-btn" @click="toggleExpand">
<i :class="expanded ? 'icon-arrow-up' : 'icon-arrow-down'"></i>
</div>
<div v-if="closable" class="v-card__close-btn" @click="handleClose">
<i class="icon-close"></i>
</div>
</slot>
</div>
<!-- 卡片内容 -->
<div v-show="expanded" class="v-card__content">
<slot></slot>
</div>
<!-- 卡片底部 -->
<div v-if="$slots.footer && expanded" class="v-card__footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'VCard',
props: {
// 卡片标题
title: {
type: String,
default: ''
},
// 阴影显示时机
shadow: {
type: String,
default: 'always', // always, hover, never
validator: value => ['always', 'hover', 'never'].includes(value)
},
// 是否有边框
bordered: {
type: Boolean,
default: true
},
// 是否显示加载状态
loading: {
type: Boolean,
default: false
},
// 自定义样式
customStyle: {
type: Object,
default: () => ({})
},
// 是否可折叠
collapsible: {
type: Boolean,
default: false
},
// 默认是否展开
defaultExpanded: {
type: Boolean,
default: true
},
// 是否可关闭
closable: {
type: Boolean,
default: false
}
},
data() {
return {
expanded: this.defaultExpanded
};
},
methods: {
toggleExpand() {
this.expanded = !this.expanded;
this.$emit('collapse-change', this.expanded);
},
handleClose() {
this.$emit('close');
}
}
};
</script>
<style lang="scss" scoped>
.v-card {
position: relative;
background-color: #fff;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
&--always-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
&--hover-shadow {
&:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
&--never-shadow {
box-shadow: none;
}
&--bordered {
border: 1px solid #ebeef5;
}
&__header {
display: flex;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid #ebeef5;
}
&__title {
flex: 1;
font-size: 16px;
font-weight: bold;
color: #303133;
}
&__collapse-btn,
&__close-btn {
margin-left: 10px;
cursor: pointer;
color: #909399;
&:hover {
color: #409EFF;
}
}
&__content {
padding: 20px;
}
&__footer {
padding: 10px 20px;
border-top: 1px solid #ebeef5;
}
&__loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
&__loading-spinner {
width: 30px;
height: 30px;
border: 2px solid #409EFF;
border-radius: 50%;
border-left-color: transparent;
animation: spin 1s linear infinite;
}
&.is-loading {
pointer-events: none;
}
&.is-collapsed {
min-height: auto;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
使用方法
基础用法
<template>
<v-card title="我的卡片">
这里是卡片内容
</v-card>
</template>
<script>
import VCard from '@/components/CardComponent';
export default {
components: {
VCard
}
}
</script>
自定义头部和底部
<template>
<v-card>
<template #header>
<div class="custom-header">
<h3>自定义标题</h3>
<el-button size="small" type="primary">操作按钮</el-button>
</div>
</template>
<p>这是卡片的主要内容区域</p>
<template #footer>
<div class="custom-footer">
<el-button>取消</el-button>
<el-button type="primary">确定</el-button>
</div>
</template>
</v-card>
</template>
可折叠卡片
<template>
<v-card
title="可折叠卡片"
:collapsible="true"
:default-expanded="false"
@collapse-change="handleCollapseChange"
>
<p>这里是可以被折叠的内容</p>
</v-card>
</template>
<script>
export default {
methods: {
handleCollapseChange(expanded) {
console.log('卡片展开状态:', expanded);
}
}
}
</script>
加载状态
<template>
<v-card title="加载中的卡片" :loading="isLoading">
<p>这里是卡片内容</p>
</v-card>
</template>
<script>
export default {
data() {
return {
isLoading: true
}
},
mounted() {
// 模拟异步加载
setTimeout(() => {
this.isLoading = false;
}, 2000);
}
}
</script>
API 文档
Props
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 卡片标题 | String | — | ‘’ |
shadow | 设置阴影显示时机 | String | always / hover / never | always |
bordered | 是否显示边框 | Boolean | — | true |
loading | 是否显示加载状态 | Boolean | — | false |
customStyle | 自定义样式 | Object | — | {} |
collapsible | 是否可折叠 | Boolean | — | false |
defaultExpanded | 默认是否展开 | Boolean | — | true |
closable | 是否可关闭 | Boolean | — | false |
Events
事件名称 | 说明 | 回调参数 |
---|---|---|
collapse-change | 折叠状态发生变化时触发 | expanded: 是否展开 |
close | 点击关闭按钮时触发 | — |
Slots
插槽名称 | 说明 |
---|---|
default | 卡片内容 |
header | 卡片头部,会覆盖 title 属性 |
footer | 卡片底部 |
错误处理
在组件中,我添加了一些错误处理机制:
- 对
shadow
属性进行了验证,确保只能传入预定义的值 - 加载状态下禁用了交互操作,避免用户在数据未准备好时进行操作
- 使用
v-if
和v-show
的合理组合,避免不必要的 DOM 渲染
组件优化
为了提高组件的性能和可维护性,我做了以下优化:
- 按需渲染:使用
v-if
条件渲染不必要的元素,如头部和底部 - CSS 过渡:添加了过渡效果,使交互更加平滑
- 样式隔离:使用 scoped 样式,避免样式污染
- 合理的命名:使用 BEM 命名规范,使样式结构清晰
扩展思路
这个卡片组件还可以进一步扩展:
- 添加更多的主题样式,如成功、警告、危险等
- 支持卡片组,实现手风琴效果
- 添加拖拽功能,可以调整卡片位置
- 实现卡片的最大化/最小化功能
总结
通过封装这个卡片组件,我们可以在项目中快速复用,大大提高了开发效率。组件的设计考虑了通用性、可扩展性和易用性,适合在各种场景下使用。
希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论。
其实写这个组件的时候我还是踩了不少坑的,比如最开始没考虑到卡片折叠时内容的动画效果,后来发现直接用 v-show
切换会很生硬。还有就是在处理自定义样式的时候,一开始用的是 class 拼接的方式,后来发现直接用 style 对象会更灵活一些。
不管怎么说,这个组件在我们项目中已经用起来了,同事们都说用着挺方便的,也算是没白费这番功夫吧!
下一步我打算把这个组件发布到 npm 上,方便更多人使用。如果你有兴趣一起完善这个组件,欢迎联系我!