从零开始打造一个通用的 Vue 卡片组件
前言
大家好,最近在做项目的时候发现我们系统里到处都是各种卡片样式的 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, nevervalidator: 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 上,方便更多人使用。如果你有兴趣一起完善这个组件,欢迎联系我!
相关文章:
从零开始打造一个通用的 Vue 卡片组件
前言 大家好,最近在做项目的时候发现我们系统里到处都是各种卡片样式的 UI 元素,每次都要重写一遍真的很烦。于是我花了点时间,封装了一个通用的卡片组件,今天就来分享一下我的开发思路和实现过程。希望能对大家有所帮助…...
选择排序算法OpenMP并行优化
一 选择排序算法原理 时间复杂度,O(n 2)。 每次从未排序序列中选择最小元素,交换到已排序序列末尾。 二 具体步骤 1)初始状态 已排序区间为空,未排序区间为[0,n-1]。 2)第i次迭代 在未排序区间[i, n-1]中找最小值索引min_idx 交换arr[i]与arr[min_idx]。 3)重复…...
Debian系统grub新增启动项
参考链接 给grub添加自定义启动项_linux grub定制 启动项名称自定义-CSDN博客 www.cnblogs.com 1. boot里面的grub.cfg 使用vim打开boot里面的grub.cfg sudo vim /boot/grub/grub.cfg 这时候会看到文件最上方的提示 2. 真正配置grub的文件 从刚才看到的文件提示中&#x…...
VSCode快捷键整理
VSCode快捷键整理 文章目录 VSCode快捷键整理1-VSCode 常用快捷键1-界面操作2-单词移动3-删除操作4-编程相关5-多光标操作6-文件、符号、函数跳转7-鼠标操作8-自动补全操作9-代码折叠操作 1-VSCode 常用快捷键 1-界面操作 文件资源管理器:Ctrl Shift E 跨文件搜…...
刘火良 FreeRTOS内核实现与应用之1——列表学习
重要数据 节点的命名都以_ITEM后缀进行,链表取消了后缀,直接LIST 普通的节点数据类型 /* 节点结构体定义 */ struct xLIST_ITEM { TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */ struct xLIST_I…...
本地部署Navidrome个人云音乐平台随时随地畅听本地音乐文件
文章目录 前言1. 安装Docker2. 创建并启动Navidrome容器3. 公网远程访问本地Navidrome3.1 内网穿透工具安装3.2 创建远程连接公网地址3.3 使用固定公网地址远程访问 前言 今天我要给大家安利一个超酷的私有化音乐神器——Navidrome!它不仅让你随时随地畅享本地音乐…...
数据集构建与训练前准备
训练数据集目录结构与格式 作者笨蛋学法,先将其公式化,后面逐步自己进行修改,读者觉得看不懂可以理解成,由结果去推过程,下面的这个yaml文件就是结果,我们去推需要的文件夹(名字可以不固定,但是…...
jenkins+ant+jmeter生成的测试报告空白
Jenkins能正常构建成功,但是打开Jenkins上的测试报告,则显示空白 在网上找了很多文章,结果跟别人对比测试报告的配置,发现自己跟别人写的不一样 所以跟着别人改,改成一样的再试试 结果,好家伙࿰…...
利用阿里云Atlas地区选择器与Plotly.js实现数据可视化与交互
在数据科学与可视化领域,交互式图表和地图应用越来越成为数据分析和展示的重要手段。本文将介绍如何结合阿里云Atlas地区选择器与Plotly.js,创建动态交互式的数据可视化应用。 一、阿里云Atlas地区选择器简介 阿里云Atlas是阿里云的一款数据可视化产品…...
行为级建模
1、结构化过程语句 verilog有两种结构化过程语句: always initial verilog本质上是并发的。 //声明初值//方法一 reg clk ; initialclk 1b0 ;//方法二 reg clk 1b0 ;2、过程赋值语句 阻塞赋值 非阻塞赋值 非阻塞赋值可以避免竞争:…...
linux安装java8 sdk,使用 tar.gz安装包手动安装
1. 下载 Java 8 SDK 首先,需要从 Oracle 的官方网站或 OpenJDK 的网站下载 Java 8 的 .tar.gz 文件。并上传到服务器 2. 解压 JDK 下载完成后,使用 tar 命令解压文件。打开服务器终端,然后使用以下命令: tar -xvzf jdk-8uXXX-…...
6.聊天室环境安装 - Ubuntu22.04 - elasticsearch(es)的安装和使用
目录 介绍安装安装kibana安装ES客户端使用 介绍 Elasticsearch, 简称 ES,它是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,res…...
【python爬虫】酷狗音乐爬取练习
注意:本次爬取的音乐仅有1分钟试听,仅作学习爬虫的原理,完整音乐需要自行下载客户端。 一、 初步分析 登陆酷狗音乐后随机选取一首歌,在请求里发现一段mp3文件,复制网址,确实是我们需要的url。 复制音频的…...
计算机视觉cv2入门之图像空域滤波(待补充)
空域滤波 空域滤波是指利用像素及像素领域组成的空间进行图像增强的方法。这里之所以用滤波这个词,是因为借助了频域里的概念。事实上空域滤波技术的效果与频域滤波技术的效果可以是等价的,而且有些原理和方法也常借助频域概念来解释。 原理和分类 空域滤波是在图…...
杂项知识笔记搜集
1.pygame pygame可以画出来图形界面,pygame Python仓库 PyGame游戏编程_游戏程序设计csdn-CSDN博客 2.V4L2库 V4L2是Linux上的Camera采集器的框架 Video for Linux ,是从Linux2.1版本开始支持的。HDMI视频采集卡采集到的视频通过USB3.0输出࿰…...
代码随想录算法训练营第六十一天 | 108. 冗余连接 109. 冗余连接II
108. 冗余连接 题目链接:KamaCoder 文档讲解:代码随想录 状态:AC Java代码: import java.util.*;class Main {public static int[] father;public static void main(String[] args) {Scanner scan new Scanner(System.in);int n…...
选择排序算法的SIMD优化
一、优化原理 将查找数组最小值索引的SIMD优化的函数嵌入选择排序主循环,优化最耗时的最小值查找环节,同时保留选择排序的交换逻辑。 二、关键改造步骤 1)最小值查找模块化 复用SIMD优化的 find_min_index_simd函数。 2)动态子数组处理 每次循环处理 arr[i..n-1] 子数…...
Java本地方法根据线上地址下载图片到本地然后返回本地可以访问的地址
【需求】A和B两台服务器,A是云服务器,B是本地服务器,A和B上部署了一模一样的springboot代码,代码分为前后端,其中在B上前端请求先请求B的后端然后B转发到A的后端,然后A返回给B,B再返回给B的前端…...
游戏引擎学习第149天
今日回顾与计划 在今天的直播中,我们将继续进行游戏的开发工作,目标是完成资产文件(pack file)的测试版本。目前,游戏的资源(如位图和声音文件)是直接从磁盘加载的,而我们正在将其转…...
SpringBoot Test详解
目录 spring-boot-starter-test 1、概述2、常用注解 2.1、配置类型的注解2.2、Mock类型的注解2.3、自动配置类型的注解2.4、启动测试类型的注解2.5、相似注解的区别和联系 3、SpringBootTest和Junit的使用 3.1、单元测试3.2、集成测试 4、MockMvc 4.1、简单示例4.2、自动配置4…...
PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)!
PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)! 当我们成功接入大模型时,可以选中任意代码区域进行解答,共分为三个区域,分别是选中区域、提问区域以及回答区域,我…...
升级到碳纤维齿轮是否值得?
引言:当齿轮开始“减肥” 在F1赛车的变速箱里,一个齿轮的重量减轻100克,就能让圈速提升0.1秒; 在无人机旋翼传动系统中,轻量化齿轮可延长续航时间15%; 甚至在高端机械腕表中,碳纤维齿轮的引入…...
基于SpringBoot+Vue的瑜伽课体验课预约系统【附源码】
基于SpringBootVue的瑜伽课体验课预约系统 一、系统技术说明二、运行说明三、系统的演示四、系统的核心代码演示 一、系统技术说明 框架:SpringbootVue 数据库:mysql 5.7(一定要5.7版本) 数据库工具:Navicat11 开发软…...
文章被检测出是AI写的怎么办?
随着人工智能技术的飞速发展,AI辅助写作工具逐渐普及,为学生、科研人员以及创作者带来了诸多便利。然而,随之而来的是对学术诚信和内容原创性的担忧。当文章被检测出是AI写作时,应该如何应对?本文将探讨这一问题&#…...
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14基础固定表头示例
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…...
【英伟达AI论文】多模态大型语言模型的高效长视频理解
摘要:近年来,基于视频的多模态大型语言模型(Video-LLMs)通过将视频处理为图像帧序列,显著提升了视频理解能力。然而,许多现有方法在视觉主干网络中独立处理各帧,缺乏显式的时序建模,…...
[Lc10_hash] 总结 | 两数之和 | 字符重排 | 存在重复元素 i ii | 字母异位词分组
目录 1.介绍 2.两数之和 题解 3.面试题 01.02. 判定是否互为字符重排 题解 4.存在重复元素 题解 5.存在重复元素 II 题解 ⭕6.字母异位词分组 题解 1.介绍 哈希表是什么? 存储数据的容器前文:[C_] set | map | unordered_map 有什么用呢?…...
缓存之美:Guava Cache 相比于 Caffeine 差在哪里?
大家好,我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理,并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易,我们还是在开篇介绍它的原理: Guava Cache 通过分段(…...
小组件适配屏幕主题色
iOS 18 新增Home screen Tint Color(色调)选择,用户可以通过以下方式自定义主屏幕颜色,并且小组件,APP 图标也会跟随改颜色。 比如说意料之外的小组件(不兼容) 白色部分内部应该还有其他显示内…...
IO学习---->线程
1.创建两个线程,分支线程1拷贝文件的前一部分,分支线程2拷贝文件的后一部分 #include <head.h> sem_t sem; long half_size 0; // 全局变量,供所有线程共享void* product(void *arg) {FILE *src fopen("IO.text", "…...
