在这里插入图片描述

信息展示是应用的核心功能之一,把相关的信息聚合在一个容器里展示,用户看起来更清晰。Card 卡片组件就是这样一个容器,它有边框、圆角、阴影,能把内容和背景区分开来。这篇文章来聊聊在 Electron for OpenHarmony 项目中如何使用 Card 卡片组件。

Card 的设计理念

卡片式设计在现代 UI 中非常流行,从手机应用到网页,到处都能看到卡片的身影。卡片的好处是:

把相关信息聚合在一起,形成一个独立的信息单元;有明确的边界,和其他内容区分开;可以自由组合排列,适应不同的布局需求;交互友好,整个卡片都可以点击。

Element Plus 的 el-card 组件提供了一个简洁的卡片容器,支持头部、内容区域和多种阴影效果。

基础卡片结构

一个完整的卡片包含头部和内容区域:

<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>

<template>
  <el-card class="demo-card">
    <template #header>基础用法</template>
    <el-card style="max-width: 480px">
      <template #header>
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <span>卡片名称</span>
          <el-button type="primary" text>操作按钮</el-button>
        </div>
      </template>
      <p>卡片内容区域</p>
      <p>可以放置任意内容</p>
    </el-card>
  </el-card>
</template>

#header 插槽定义卡片头部,可以放标题、操作按钮等。头部和内容区域之间有一条分割线。卡片内部直接写的内容会显示在内容区域。

头部的布局用 flex 实现左右分布,左边是标题,右边是操作按钮。text 类型的按钮没有背景和边框,适合放在卡片头部这种空间有限的地方。

简单卡片

不需要头部的时候,直接写内容就行:

<el-card class="demo-card">
  <template #header>简单卡片</template>
  <el-card style="max-width: 480px">
    <p>没有头部的简单卡片</p>
    <p>只包含内容区域</p>
  </el-card>
</el-card>

没有 #header 插槽的卡片就是一个简单的内容容器,没有分割线,整个卡片都是内容区域。这种卡片适合展示简短的信息或者作为布局容器使用。

阴影效果

卡片的阴影可以控制什么时候显示:

<el-card class="demo-card">
  <template #header>阴影效果</template>
  <el-space wrap>
    <el-card shadow="always" style="width: 140px">总是显示</el-card>
    <el-card shadow="hover" style="width: 140px">悬停显示</el-card>
    <el-card shadow="never" style="width: 140px">从不显示</el-card>
  </el-space>
</el-card>

shadow 属性控制阴影的显示时机:

  • always:总是显示阴影,这是默认值
  • hover:鼠标悬停时才显示阴影,有交互感
  • never:从不显示阴影,卡片看起来更扁平

hover 效果在卡片列表中很常用,鼠标移上去卡片会"浮起来",暗示这个卡片可以点击。

卡片列表布局

卡片经常用来做列表展示,比如商品列表、文章列表:

<script setup lang="ts">
import { ref } from 'vue'

const items = ref([
  { id: 1, title: '文章标题一', desc: '这是文章的简介内容,可以是摘要或者描述信息。', date: '2024-01-15' },
  { id: 2, title: '文章标题二', desc: '这是另一篇文章的简介内容,展示卡片列表的效果。', date: '2024-01-14' },
  { id: 3, title: '文章标题三', desc: '第三篇文章的简介,卡片可以承载各种类型的内容。', date: '2024-01-13' },
])

const handleClick = (item: any) => {
  console.log('点击了:', item.title)
}
</script>

<template>
  <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
    <el-card 
      v-for="item in items" 
      :key="item.id" 
      shadow="hover" 
      style="cursor: pointer;"
      @click="handleClick(item)"
    >
      <template #header>
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <span style="font-weight: bold;">{{ item.title }}</span>
          <span style="color: #909399; font-size: 12px;">{{ item.date }}</span>
        </div>
      </template>
      <p style="color: #606266; line-height: 1.6;">{{ item.desc }}</p>
    </el-card>
  </div>
</template>

用 CSS Grid 实现自适应的卡片网格布局,grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) 让卡片自动填充,每个卡片最小 280px,剩余空间平均分配。

shadow="hover" 让卡片在悬停时显示阴影,配合 cursor: pointer 提示用户这个卡片可以点击。

图片卡片

卡片里放图片是很常见的需求:

<script setup lang="ts">
const products = [
  { id: 1, name: '商品名称', price: 99.00, image: 'https://via.placeholder.com/300x200' },
  { id: 2, name: '另一个商品', price: 199.00, image: 'https://via.placeholder.com/300x200' },
]
</script>

<template>
  <div style="display: flex; gap: 16px; flex-wrap: wrap;">
    <el-card v-for="product in products" :key="product.id" style="width: 240px;" shadow="hover" :body-style="{ padding: '0' }">
      <img :src="product.image" style="width: 100%; height: 160px; object-fit: cover;" />
      <div style="padding: 14px;">
        <p style="margin: 0 0 8px; font-size: 16px;">{{ product.name }}</p>
        <p style="margin: 0; color: #f56c6c; font-size: 18px; font-weight: bold;">¥{{ product.price }}</p>
        <el-button type="primary" size="small" style="margin-top: 12px; width: 100%;">加入购物车</el-button>
      </div>
    </el-card>
  </div>
</template>

:body-style="{ padding: '0' }" 去掉内容区域的默认内边距,让图片可以贴边显示。图片下方的信息区域自己加 padding 控制间距。

object-fit: cover 让图片保持比例填充容器,不会变形。

可折叠卡片

有时候卡片内容很长,需要折叠起来:

<script setup lang="ts">
import { ref } from 'vue'

const expanded = ref(false)
</script>

<template>
  <el-card style="max-width: 480px;">
    <template #header>
      <div style="display: flex; justify-content: space-between; align-items: center;">
        <span>可折叠卡片</span>
        <el-button type="primary" text @click="expanded = !expanded">
          {{ expanded ? '收起' : '展开' }}
        </el-button>
      </div>
    </template>
    <div>
      <p>这是始终显示的内容。</p>
      <div v-show="expanded" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ebeef5;">
        <p>这是展开后才显示的详细内容。</p>
        <p>可以放更多的信息在这里。</p>
        <p>用户点击展开按钮才能看到。</p>
      </div>
    </div>
  </el-card>
</template>

v-show 控制详细内容的显示隐藏,比 v-if 性能更好,因为 DOM 元素不会被销毁重建。头部的按钮文字根据展开状态动态变化。

加载状态

卡片内容在加载时可以显示骨架屏或 loading:

<script setup lang="ts">
import { ref } from 'vue'

const loading = ref(true)
const data = ref<any>(null)

// 模拟加载数据
setTimeout(() => {
  data.value = {
    title: '加载完成的标题',
    content: '这是加载完成后显示的内容。'
  }
  loading.value = false
}, 2000)
</script>

<template>
  <el-card style="max-width: 480px;" v-loading="loading">
    <template #header>
      <span>{{ data?.title || '加载中...' }}</span>
    </template>
    <p v-if="data">{{ data.content }}</p>
    <p v-else style="color: #909399;">正在加载数据...</p>
  </el-card>
</template>

v-loading 指令在卡片上显示加载遮罩,数据加载完成后自动消失。这是 Element Plus 提供的全局指令,任何元素都可以用。

卡片组

多个相关的卡片可以组合在一起:

<script setup lang="ts">
const stats = [
  { label: '总用户', value: '12,345', icon: 'User', color: '#409eff' },
  { label: '今日订单', value: '256', icon: 'ShoppingCart', color: '#67c23a' },
  { label: '待处理', value: '18', icon: 'Bell', color: '#e6a23c' },
  { label: '总收入', value: '¥89,012', icon: 'Money', color: '#f56c6c' },
]
</script>

<template>
  <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;">
    <el-card v-for="stat in stats" :key="stat.label" shadow="hover">
      <div style="display: flex; align-items: center; gap: 16px;">
        <div :style="{ 
          width: '48px', 
          height: '48px', 
          borderRadius: '8px', 
          backgroundColor: stat.color + '20',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }">
          <el-icon :size="24" :style="{ color: stat.color }">
            <component :is="stat.icon" />
          </el-icon>
        </div>
        <div>
          <p style="margin: 0; color: #909399; font-size: 14px;">{{ stat.label }}</p>
          <p style="margin: 4px 0 0; font-size: 24px; font-weight: bold;">{{ stat.value }}</p>
        </div>
      </div>
    </el-card>
  </div>
</template>

这是一个常见的数据统计卡片组,每个卡片展示一个指标。图标用不同的颜色区分,背景色是主色的浅色版本(加了 20% 透明度)。

与鸿蒙原生能力结合

在 Electron for OpenHarmony 项目中,卡片可以和原生能力配合使用:

<script setup lang="ts">
import { ref } from 'vue'
import { useOhos } from '@/composables/useOhos'
import { ElMessage } from 'element-plus'

const { openFile, showNotification, clipboard } = useOhos()

const files = ref([
  { name: '文档.docx', size: '2.5 MB', type: 'document' },
  { name: '图片.png', size: '1.2 MB', type: 'image' },
  { name: '视频.mp4', size: '15.8 MB', type: 'video' },
])

const handleOpen = async (file: any) => {
  await showNotification('打开文件', `正在打开: ${file.name}`)
  ElMessage.success(`打开文件: ${file.name}`)
}

const handleCopyName = async (file: any) => {
  await clipboard.write(file.name)
  ElMessage.success('文件名已复制')
}

const handleAddFile = async () => {
  const selectedFiles = await openFile({
    title: '选择文件',
    filters: [{ name: '所有文件', extensions: ['*'] }]
  })
  if (selectedFiles && selectedFiles.length > 0) {
    ElMessage.success(`已选择: ${selectedFiles[0]}`)
  }
}
</script>

<template>
  <div>
    <el-button type="primary" @click="handleAddFile" style="margin-bottom: 16px;">
      添加文件
    </el-button>
    
    <div style="display: flex; flex-direction: column; gap: 12px;">
      <el-card v-for="file in files" :key="file.name" shadow="hover">
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <div style="display: flex; align-items: center; gap: 12px;">
            <el-icon :size="32" style="color: #409eff;">
              <Document />
            </el-icon>
            <div>
              <p style="margin: 0; font-weight: 500;">{{ file.name }}</p>
              <p style="margin: 4px 0 0; font-size: 12px; color: #909399;">{{ file.size }}</p>
            </div>
          </div>
          <el-space>
            <el-button size="small" @click="handleCopyName(file)">复制名称</el-button>
            <el-button type="primary" size="small" @click="handleOpen(file)">打开</el-button>
          </el-space>
        </div>
      </el-card>
    </div>
  </div>
</template>

这个例子展示了文件管理场景:点击添加文件按钮调用原生文件选择器;点击打开按钮发送系统通知并显示消息提示;点击复制名称按钮把文件名写入剪贴板。卡片作为文件信息的容器,每个文件一张卡片,结构清晰。

自定义卡片样式

Card 的样式可以通过 CSS 变量和属性调整:

<template>
  <el-card 
    style="
      --el-card-border-color: #409eff;
      --el-card-border-radius: 12px;
      --el-card-padding: 24px;
    "
    :body-style="{ 
      background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
      color: '#fff'
    }"
  >
    <h3 style="margin: 0 0 12px;">自定义样式卡片</h3>
    <p style="margin: 0; opacity: 0.9;">通过 CSS 变量和 body-style 可以实现各种自定义效果。</p>
  </el-card>
</template>

CSS 变量可以调整边框颜色、圆角、内边距等,:body-style 可以直接设置内容区域的样式,包括背景渐变、文字颜色等。

响应式卡片

在不同屏幕尺寸下调整卡片布局:

<template>
  <div class="card-grid">
    <el-card v-for="i in 6" :key="i" shadow="hover">
      <p>卡片 {{ i }}</p>
    </el-card>
  </div>
</template>

<style scoped>
.card-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(1, 1fr);
}

@media (min-width: 640px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
</style>

用媒体查询在不同屏幕宽度下显示不同的列数:小屏幕一列,中等屏幕两列,大屏幕三列。这样卡片在各种设备上都能有好的展示效果。

小结

Card 卡片是信息展示的基础容器组件,这篇文章介绍了它的各种用法:基础结构、简单卡片、阴影效果、卡片列表、图片卡片、可折叠卡片、加载状态、卡片组,以及与鸿蒙原生能力的结合。卡片的核心是把相关信息聚合在一个有边界的容器里,通过头部插槽和内容区域组织信息,通过阴影效果增加层次感。合理使用卡片可以让页面信息结构更清晰,用户体验更好。


欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐