从零开始打造一个通用的 Vue 卡片组件

news2025/3/11 14:43:16

前言

大家好,最近在做项目的时候发现我们系统里到处都是各种卡片样式的 UI 元素,每次都要重写一遍真的很烦。于是我花了点时间,封装了一个通用的卡片组件,今天就来分享一下我的开发思路和实现过程。希望能对大家有所帮助!

需求分析

在开始写代码前,我先梳理了一下卡片组件的常见需求:

  1. 支持自定义标题、内容、底部操作区
  2. 可配置是否显示阴影、边框
  3. 支持加载状态
  4. 支持自定义样式
  5. 支持卡片展开/折叠功能
  6. 支持卡片的移除/关闭功能

组件设计

目录结构

|- 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设置阴影显示时机Stringalways / hover / neveralways
bordered是否显示边框Booleantrue
loading是否显示加载状态Booleanfalse
customStyle自定义样式Object{}
collapsible是否可折叠Booleanfalse
defaultExpanded默认是否展开Booleantrue
closable是否可关闭Booleanfalse

Events

事件名称说明回调参数
collapse-change折叠状态发生变化时触发expanded: 是否展开
close点击关闭按钮时触发

Slots

插槽名称说明
default卡片内容
header卡片头部,会覆盖 title 属性
footer卡片底部

错误处理

在组件中,我添加了一些错误处理机制:

  1. shadow 属性进行了验证,确保只能传入预定义的值
  2. 加载状态下禁用了交互操作,避免用户在数据未准备好时进行操作
  3. 使用 v-ifv-show 的合理组合,避免不必要的 DOM 渲染

组件优化

为了提高组件的性能和可维护性,我做了以下优化:

  1. 按需渲染:使用 v-if 条件渲染不必要的元素,如头部和底部
  2. CSS 过渡:添加了过渡效果,使交互更加平滑
  3. 样式隔离:使用 scoped 样式,避免样式污染
  4. 合理的命名:使用 BEM 命名规范,使样式结构清晰

扩展思路

这个卡片组件还可以进一步扩展:

  1. 添加更多的主题样式,如成功、警告、危险等
  2. 支持卡片组,实现手风琴效果
  3. 添加拖拽功能,可以调整卡片位置
  4. 实现卡片的最大化/最小化功能

总结

通过封装这个卡片组件,我们可以在项目中快速复用,大大提高了开发效率。组件的设计考虑了通用性、可扩展性和易用性,适合在各种场景下使用。

希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论。


其实写这个组件的时候我还是踩了不少坑的,比如最开始没考虑到卡片折叠时内容的动画效果,后来发现直接用 v-show 切换会很生硬。还有就是在处理自定义样式的时候,一开始用的是 class 拼接的方式,后来发现直接用 style 对象会更灵活一些。

不管怎么说,这个组件在我们项目中已经用起来了,同事们都说用着挺方便的,也算是没白费这番功夫吧!

下一步我打算把这个组件发布到 npm 上,方便更多人使用。如果你有兴趣一起完善这个组件,欢迎联系我!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2313264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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-界面操作 文件资源管理器&#xff1a;Ctrl Shift E 跨文件搜…

刘火良 FreeRTOS内核实现与应用之1——列表学习

重要数据 节点的命名都以_ITEM后缀进行&#xff0c;链表取消了后缀&#xff0c;直接LIST 普通的节点数据类型 /* 节点结构体定义 */ struct xLIST_ITEM { TickType_t xItemValue; /* 辅助值&#xff0c;用于帮助节点做顺序排列 */ struct xLIST_I…

本地部署Navidrome个人云音乐平台随时随地畅听本地音乐文件

文章目录 前言1. 安装Docker2. 创建并启动Navidrome容器3. 公网远程访问本地Navidrome3.1 内网穿透工具安装3.2 创建远程连接公网地址3.3 使用固定公网地址远程访问 前言 今天我要给大家安利一个超酷的私有化音乐神器——Navidrome&#xff01;它不仅让你随时随地畅享本地音乐…

数据集构建与训练前准备

训练数据集目录结构与格式 作者笨蛋学法&#xff0c;先将其公式化&#xff0c;后面逐步自己进行修改&#xff0c;读者觉得看不懂可以理解成&#xff0c;由结果去推过程&#xff0c;下面的这个yaml文件就是结果&#xff0c;我们去推需要的文件夹(名字可以不固定&#xff0c;但是…

jenkins+ant+jmeter生成的测试报告空白

Jenkins能正常构建成功&#xff0c;但是打开Jenkins上的测试报告&#xff0c;则显示空白 在网上找了很多文章&#xff0c;结果跟别人对比测试报告的配置&#xff0c;发现自己跟别人写的不一样 所以跟着别人改&#xff0c;改成一样的再试试 结果&#xff0c;好家伙&#xff0…

利用阿里云Atlas地区选择器与Plotly.js实现数据可视化与交互

在数据科学与可视化领域&#xff0c;交互式图表和地图应用越来越成为数据分析和展示的重要手段。本文将介绍如何结合阿里云Atlas地区选择器与Plotly.js&#xff0c;创建动态交互式的数据可视化应用。 一、阿里云Atlas地区选择器简介 阿里云Atlas是阿里云的一款数据可视化产品…

linux安装java8 sdk,使用 tar.gz安装包手动安装

1. 下载 Java 8 SDK 首先&#xff0c;需要从 Oracle 的官方网站或 OpenJDK 的网站下载 Java 8 的 .tar.gz 文件。并上传到服务器 2. 解压 JDK 下载完成后&#xff0c;使用 tar 命令解压文件。打开服务器终端&#xff0c;然后使用以下命令&#xff1a; tar -xvzf jdk-8uXXX-…

6.聊天室环境安装 - Ubuntu22.04 - elasticsearch(es)的安装和使用

目录 介绍安装安装kibana安装ES客户端使用 介绍 Elasticsearch&#xff0c; 简称 ES&#xff0c;它是个开源分布式搜索引擎&#xff0c;它的特点有&#xff1a;分布式&#xff0c;零配置&#xff0c;自动发现&#xff0c;索引自动分片&#xff0c;索引副本机制&#xff0c;res…

【python爬虫】酷狗音乐爬取练习

注意&#xff1a;本次爬取的音乐仅有1分钟试听&#xff0c;仅作学习爬虫的原理&#xff0c;完整音乐需要自行下载客户端。 一、 初步分析 登陆酷狗音乐后随机选取一首歌&#xff0c;在请求里发现一段mp3文件&#xff0c;复制网址&#xff0c;确实是我们需要的url。 复制音频的…

计算机视觉cv2入门之图像空域滤波(待补充)

空域滤波 空域滤波是指利用像素及像素领域组成的空间进行图像增强的方法。这里之所以用滤波这个词,是因为借助了频域里的概念。事实上空域滤波技术的效果与频域滤波技术的效果可以是等价的&#xff0c;而且有些原理和方法也常借助频域概念来解释。 原理和分类 空域滤波是在图…

游戏引擎学习第149天

今日回顾与计划 在今天的直播中&#xff0c;我们将继续进行游戏的开发工作&#xff0c;目标是完成资产文件&#xff08;pack file&#xff09;的测试版本。目前&#xff0c;游戏的资源&#xff08;如位图和声音文件&#xff09;是直接从磁盘加载的&#xff0c;而我们正在将其转…

PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)!

PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程&#xff08;通用&#xff09;&#xff01; 当我们成功接入大模型时&#xff0c;可以选中任意代码区域进行解答&#xff0c;共分为三个区域&#xff0c;分别是选中区域、提问区域以及回答区域&#xff0c;我…

升级到碳纤维齿轮是否值得?

引言&#xff1a;当齿轮开始“减肥” 在F1赛车的变速箱里&#xff0c;一个齿轮的重量减轻100克&#xff0c;就能让圈速提升0.1秒&#xff1b; 在无人机旋翼传动系统中&#xff0c;轻量化齿轮可延长续航时间15%&#xff1b; 甚至在高端机械腕表中&#xff0c;碳纤维齿轮的引入…

基于SpringBoot+Vue的瑜伽课体验课预约系统【附源码】

基于SpringBootVue的瑜伽课体验课预约系统 一、系统技术说明二、运行说明三、系统的演示四、系统的核心代码演示 一、系统技术说明 框架&#xff1a;SpringbootVue 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软…

文章被检测出是AI写的怎么办?

随着人工智能技术的飞速发展&#xff0c;AI辅助写作工具逐渐普及&#xff0c;为学生、科研人员以及创作者带来了诸多便利。然而&#xff0c;随之而来的是对学术诚信和内容原创性的担忧。当文章被检测出是AI写作时&#xff0c;应该如何应对&#xff1f;本文将探讨这一问题&#…

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14基础固定表头示例

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

【英伟达AI论文】多模态大型语言模型的高效长视频理解

摘要&#xff1a;近年来&#xff0c;基于视频的多模态大型语言模型&#xff08;Video-LLMs&#xff09;通过将视频处理为图像帧序列&#xff0c;显著提升了视频理解能力。然而&#xff0c;许多现有方法在视觉主干网络中独立处理各帧&#xff0c;缺乏显式的时序建模&#xff0c;…

[Lc10_hash] 总结 | 两数之和 | 字符重排 | 存在重复元素 i ii | 字母异位词分组

目录 1.介绍 2.两数之和 题解 3.面试题 01.02. 判定是否互为字符重排 题解 4.存在重复元素 题解 5.存在重复元素 II 题解 ⭕6.字母异位词分组 题解 1.介绍 哈希表是什么? 存储数据的容器前文&#xff1a;[C_] set | map | unordered_map 有什么用呢&#xff1f;…

缓存之美:Guava Cache 相比于 Caffeine 差在哪里?

大家好&#xff0c;我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理&#xff0c;并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易&#xff0c;我们还是在开篇介绍它的原理&#xff1a; Guava Cache 通过分段&#xff08;…