小红书 PK 组件的分析与实现

552 阅读4分钟

PK 组件用于可视化展示两种或多种数据的对比结果,特别适用于需要直观展示竞争或对比分析的场景。

效果展示

小红书 PK 组件 image.png

我们将实现一个类似的 PK 组件

image.png

2. 组件分析

这个组件的难点主要在于样式的处理。我们可以先拆解一下问题:

  1. PK组件中的每一项形状处理
    • 除了首尾两项,其他项都是一个类似于平行四边形的形状,这可以通过将一个长方形在水平方向进行倾斜来实现。可以使用CSS的 transform: skewX() 函数来处理。
    • 可以统一设置圆角,保证整体的一致性。
  1. 首尾两端的特殊处理
    • 首尾两项需要单独设置圆角,以确保它们的外观与其他项有所不同。
  1. 文字的处理
    • 文字需要保持正向显示,因此可以对文字也应用transform: skewX(),但方向相反,角度相同,用来抵消项的倾斜效果。
  1. 每一项的宽度比例
    • 通过计算每个项的宽度比例(x/sum),将其转换成百分比形式,并使用flex-basis: ${props => props.$width}来设置每一项的宽度。

这样拆解之后,问题就变得容易解决了,可以开始编写代码了。一些状态的维护相对简单,就不详细赘述了,具体可以参考代码实现。

3. 具体实现

3.1 组件样式

这是基于组件分析的具体样式实现,可以重点关注一下

import styled from "styled-components"

const PkResultCardWrapper = styled.div<{
  len?: number
}>`
  width: 800px;
  display: flex;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: nowrap;
  text-align: center;
  font-weight: 500;
  ${(props) =>
    props.len === 1
      ? `
      &>div:first-child{
        border-radius: 10px 20px !important;
      }
      `
      : `
      &>div:first-child{
        border-radius: 10px 4px 8px 20px !important;
      }
      &>div:last-child{
        border-radius: 6px 20px 10px 6px !important;
      }
      `}
`

const CardItem = styled.div<{
  colors?: string[]
  $width?: string
}>`
  height: 32px;
  line-height: 30px;
  flex-basis: ${props => props.$width};
  border-radius: 6px !important;
  transform: skewX(-15deg);
  span {
    display: inline-block;
    transform: skewX(15deg);
  }
  ${(props) =>
    props.colors
      ? `
        color: ${props.colors?.[5]} !important;
        border: 1px solid ${props.colors?.[2]} !important;
        background: ${props.colors?.[0]} !important;
      `
      : ''}
`

3.2 颜色

组件使用了 Ant Design 的颜色库来定义不同的颜色,便于区分不同的数据项,增强视觉识别度。

import { green, orange, red, yellow } from "@ant-design/colors"

const dataKeyMapToColors: Record<string, string[]> = {
  'apple': red,
  'orange': orange,
  'mango': green
}

3.3 数据结构和计算

组件定义了一个 pkData 对象和一个 getSum 函数,用于存储和计算数据项的总和。这种设计使得组件可以轻松地扩展到更多的数据项。

export const pkData = {
  'apple': 5,
  'orange': 2
}

const getSum = (data: Record<string, number>) => {
  return Object.values(data).reduce((accumulator: number, currentValue: any) => accumulator + Number(currentValue || 0), 0)
}

3.4 结果选项生成

generatePkResultOptions 函数是组件的核心,它根据传入的数据对象生成一个结果数组,每个结果对象包含数据项的键、计数、文本、颜色和比例。

export const generatePkResultOptions = (data: Record<string, number>) => {
  const res = []
  const sum = getSum(data)
  for (const [key, value] of Object.entries(data)) {
    res.push({
      key,
      count: value,
      text: key,
      colors: dataKeyMapToColors[key],
      ratio: (value / sum)
    })
  }
  return res
}

3.5 PK 结果卡片组件

PkResultCard 是一个函数组件,它接收一个 options 数组作为参数,并渲染每个结果卡片。这个组件的设计允许它在不同的上下文中重用。

type PkResultCardProps = {
  options: PkResultCardOptions[]
}

export function PkResultCard({ options }: PkResultCardProps) {
  
  return (
    <PkResultCardWrapper len={options.length} >
      {options?.map(option => {
        const ratio = (Number(option.ratio) * 100).toFixed(2) + '%'
        return (
          <CardItem key={option.key} colors={option.colors} $width={ratio}>
            {<span>{`${option.text}: ${option.count} - ${ratio}`}</span>}
          </CardItem>
        )
      })}
    </PkResultCardWrapper>
  )
}

3.6 PK 卡片演示组件

PkCardDemo 是一个演示组件,它使用 React 的 useState 钩子来管理不同数据项的计数,并使用 useMemo 钩子来生成结果选项。这个组件提供了一个交互式的界面,允许用户动态地增加数据项的计数。

import React, { useMemo } from 'react'
import { generatePkResultOptions } from './data'
import { PkResultCard } from './PkResultCard'
import { Button, Space } from 'antd'

export function PkCardDemo() {
  const [apple, setApple] = React.useState(1)
  const [orange, setOrange] = React.useState(1)
  const [mango, setMango] = React.useState(1)

  const options = useMemo(() => generatePkResultOptions({ apple, orange, mango }), [apple, orange, mango])

  return (
    <Space direction='vertical'>
      <Space size={8}>
        <Button onClick={() => setApple(apple + 1)}>Apple + 1</Button>
        <Button onClick={() => setOrange(orange + 1)}>Orange + 1</Button>
        <Button onClick={() => setMango(mango + 1)}>Mango + 1</Button>
      </Space>
      <PkResultCard options={options} />
    </Space>
  )
}

4. 使用方法

4.1 引入组件

在需要使用 PK 组件的地方,引入 PkCardDemo

import { PkCardDemo } from './PkResultCard'

4.2 渲染组件

在 React 应用中渲染 PkCardDemo 组件。

<PkCardDemo />

4.3 定制和扩展

组件的设计允许用户根据需要定制颜色和样式。用户可以通过修改 dataKeyMapToColors 对象来改变不同数据项的颜色。此外,用户可以通过添加更多的数据项到 pkData 对象中来扩展组件。

5. 总结

PK 组件提供了一个直观的方式来展示不同数据项的对比结果。通过颜色编码和比例显示,用户可以快速理解不同数据项的相对大小。组件的设计允许灵活地添加或修改数据项,使其适用于多种场景。通过简单的配置和扩展,PK 组件可以轻松集成到任何需要数据对比展示的 React 应用中。