【Day14】数据展示元件 - Card

元件介绍

Card 是一个可以显示单个主题内容及操作的元件,通常这个主题内容包含图片、标题、描述或是一些操作。

例如在电商网站,一个商品或需要包含商品图片、商品名称、商品价格...等等资讯。在新闻网站,每则新闻也会有新闻的图片、标题、分类标签、浏览次数...等等资讯。或者我们在浏览 Youtube 的时候,每则影片也会有影片的封面图片、影片标题、影片创作者、观看次数、上架时间...等等资讯。

参考设计 & 属性分析

由於卡片的形式会因着需要呈现不同的主题、内容而有不同的需要,因此我们在这边先做一个基础的卡片,若未来有希望更多样的排版,可以再基於这个卡片做延伸。

卡片封面

我们常见的卡片形式通常会附上一张封面图片,所以会需要一个 props 来让我们传入卡片的封面。但除了图片可以当作卡片的方面以外,有时候也有可能是一个影片,或是一个 carousel,因此在 Antd 的设计并没有把卡片的封面定死成一定得要是图片,cover 这个 props 允许我们传入一个 ReactNode,这样也可以让封面拥有不同的可能性。

卡片内容

在 Antd 当中为了让卡片内容的呈现更加灵活,因此提供一个 Meta 元件,让我们可以处理卡片的 avatar, title 以及 description,并且这三者的型别都为 ReactNode。

卡片操作

在卡片的底部有时候我们会看见一些按钮让我们进行操作,Antd 已经设计好一个固定的样式,并透过 actions 这个 props 来传入,很有趣的是,actions 是一个 array of ReactNode,按照这个格式传入,便能够帮你处理好这些 action 按钮的排版。

关於 MUI 卡片

MUI 跟 Antd 设计上我觉得比较不同的是,可能考虑到卡片变化的多样性,他并不先预设立场觉得你的卡片一定要长怎麽样,他的设计比较像是他提供了很多的积木,你可以按照你的喜好来拼装你的卡片,因此我们可以看到 CardMedia 元件来帮助我们处理卡片封面的多媒体,CardContent 来帮助我们处理卡片内容,而 CardActions 来帮我们处理卡片的操作行为。

我觉得 Antd 及 MUI 两者的设计思维也是不一样。如果我们需要多一点的弹性,那我们程序码就很难简洁的做到,例如像 Antd 那样传一个 props 就可以搞定,所以 MUI 才会设计成积木拼装的卡片元件。但如果想要元件写起来简洁一点,那我们势必会需要对你的样式预设立场,传几个固定型别的 props 进去就能够迅速做出卡片,但多少会失去一些变化的弹性。

我自己的想法是,我们设计卡片的时候可以学习 MUI 那样,把每一个区块拆成可拼装的积木,所以我们就会拥有一系列卡片相关的元件模组可以来组装。然後假设我们今天要做的是商品卡片,这个商品卡片势必是会有一定程度的样式统一,那我们就用这些积木来拼装成一个商品卡片,以後的使用方式就是我们能直接将商品资料当作 props 来传入,就能快速生成商品卡片。

改天我们在会员管理页需要会员卡片,那我们也能够重新用这些卡片积木来拼装出一个会员卡片,以後在会员管理的地方我们就传入一些会员资料就能够快速生成这些会员卡片。

所以以上述为例,商品卡片不能直接拿来当作会员卡片使用,因为他们可能样式上的差异导致很难共用。但是由於下面基础的积木都是一样的,那至少我们可以共用这些积木来组装成不同的卡片。

介面设计

Card

属性 说明 类型 默认值
variant 变化模式 vertical, horizontal, horizontal-reverse vertical
cover 卡片封面媒体 ReactNode
footer 卡片置底页尾 ReactNode

Card Meta

属性 说明 类型 默认值
avatar 头像 ReactNode
title 卡片标题 ReactNode
description 卡片描述 ReactNode

元件实作

卡片的样式有千千万万种,因此我们希望我们的卡片可以在一定的框架下又保留一些弹性,因此我们的卡片结构会如下:

const Card = ({
  className, cover, variant,
  children, footer, ...props
}) => (
  <StyledCard className={className} $variant={variant} {...props}>
    <Cover className="card__cover">{cover}</Cover>
    <SpaceBetween>
      {children}
      {footer}
    </SpaceBetween>
  </StyledCard>
);

卡片封面媒体

首先我们来看 <Cover />,他是一个「卡片封面媒体」,常见的内容是一个主题图片,但其实也有机会他会是一个影片,或是一个轮播图片元件 Carousel...等等。

为了适应不同内容,我们乾脆让 cover 成为一个 props 从外部传入,这样就能够根据不同情境来变化。

我们这边 cover 是以图片为例子,因此使用起来的样子如下:

<Card
  cover={<img src="https://....jpg" alt="" />}
>
  {children}
</Card>

卡片内容

卡片的内容也是有各种可能,因此类似於 cover 一样,我们也是让他由 props 来传入。
但是如果我们什麽东西都不确定,什麽东西都由 props 传入,那乾脆就需要元件了不是吗?

因此 children 当中,我们也可以把一些卡片常见的样式包成元件,以便重复使用,这边举一个例子是 <Meta />

Meta 是一个包含 avatar, title, description 的元件:

这个样式他很常见,但却不一定总是会出现,因此把他绑死进 children 就不一定适合每个情境,因此乾脆把 Meta 独立抽出来,当有需要的时候,再把他放进去 children 里面,因此我们卡片如果使用 Meta 的话,可以像这样做:

import Card from '../components/Card';
import Meta from '../components/Card/Meta';

<Card
  cover={<img src="https://....jpg" alt="" />}
>
  <CardContent>
    <Meta
      avatarUrl="https://.../choose1.png"
      title="2021 iThome 铁人赛"
      description="唤醒心中最强大的铁人"
    />
    {...其他卡片内容...}
  </CardContent>
</Card>

以下面 Hahow 课程卡片来说,有些课程是募资课,他有募资进度条、开课状态...等等,另一种类型的卡片可能是非募资课,他会需要标示评价、课程时数、上课同学数、价钱等等。

募资卡片及非募资卡片 cover image、title、avatar 样式是一样的,但其他不同的地方,我们可以用上面 Meta 范例一样的方式来组装,需要募资进度的时候,就在 children 里面放入 <Progress />,非募资课,那我们就放入其他课程资料 <CourseInfo /><Price /> ...等等。

这样的话,我们就能够适度的共用,又能够保留适度的弹性。

卡片置底页尾

我们可以看到 Antd 他也把卡片操作组另外独立成一个 props 来传入,而不是放在 children 里面:

<Card
  cover={...}
  actions={[
    <SettingOutlined key="setting" />,
    <EditOutlined key="edit" />,
    <EllipsisOutlined key="ellipsis" />,
  ]}
>
  {children}
</Card>

Antd 这样的好处是让我们可以只传入 icon 的阵列,就产生下面的操作按钮。
不过其实有时候我们不想要这样的样式,我比较希望是有一个 props 让我传入的 element 可以保持置底就好,其他样式我可以自己另外决定,我觉得这样的自由度比 actions 大,所以我想要设计成这样:

<Card
  cover={...}
  footer={(
    <Actions>
      <ThumbUpIcon />
      <ShareIcon />
      <NotificationsIcon />
    </Actions>
  )}
>
  {children}
</Card>

这样我就可以在底部乱放我想要放的东西啦!

变化模式

除了直式的卡片以外,有时候我们会看到横式的卡片,横式的话有时候 cover 在左边,有些在右边,因此我定义了三种变化模式 vertical, horizontal, horizontal-reverse

这边我们可以采用 FormControl 那篇我们有介绍过的 flex 布局的属性 flex-direction 来达成:

const verticalStyle = css`
  display: inline-flex;
  flex-direction: column;
`;

const horizontalStyle = css`
  display: flex;
`;

const horizontalReverseStyle = css`
  display: flex;
  flex-direction: row-reverse;
`;

这样在同样的 DOM 结构下,我们一样能够做到不同方向性的卡片了,以下是我们今天的成果:


Card 元件原始码:
Source code

Meta 元件原始码:
Source code

Storybook:
Card


<<:  #12 No-code 之旅 — 在 Next.js 专案中显示 RSS 的资料 ft. RSS Parser

>>:  TypeScript 能手养成之旅 Day 11 明文型别(Literal Types)

[重构倒数第01天] - Vue的表单自动暂存

前言 该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系...

Day27. Blue Prism进化中的宝可梦–BP增加稳定与弹性的调校

有一阵子流行的宝可梦Go的App寻宝游戏, 吸引不少粉丝信徒的膜拜, 玩家们都希望自己手上的宝可梦能...

[DAY 06]物品拍卖价格查询功能(4/4)

今天终於能把查询物品拍卖价格网址这功能讲完了(汗) 物品拍卖价格网址目前热门的是universali...

Logger 与 Extension Generator for Kotlin

Logger 在 compile time 的时候,不像我们一般再开发的时候很容易的去 log 一些...

[Day 28] 使用ChromeDriver来做单元测试(一)

Laravel Dusk提供了一个自动化的测试API, 不用安装Selenium等软件, 直接用独立...