---
title: 开发文档
order: 3
category: atom
---

# GraphicTextCard 开发文档

## 一、组件基本信息

- **组件名称**: GraphicTextCard
- **组件版本号**: 1.0.0
- **组件分类**: Data Display
- **组件编号**: pcp_dd_graphicTextCard_V1.0
- **组件类型**: Pro Components
- **父级命名空间**: PisellCards

## 二、组件描述

GraphicTextCard 是一个图文卡片组件，结合图标/图片、文本和状态标识，专为标签页、分类导航等场景设计。组件遵循 PisellCards 规范，继承 `PisellBasicCardProps`，提供统一的状态管理接口（active、disabled、loading）。

### 核心特性

- 支持 4 种样式变体（垂直图片、垂直图标、横向图文、卡片式）
- 支持 3 种尺寸（small、default、large）
- 支持 4 种状态（default、hover、active、disabled）
- 支持图片和图标两种图形区域显示方式
- 支持角标（红点、数字、文本）
- 支持自定义插槽（前置、后置、图形区域）
- 支持主题色变量
- 图形区域尺寸可配置
- 宽度自适应（内容撑满、固定宽度、撑满父容器）

## 三、组件需求点罗列

### 功能需求

1. ✅ 支持 4 种样式变体切换
2. ✅ 支持 3 种尺寸切换（small、default、large）
3. ✅ 支持 4 种状态（default、hover、active、disabled）
4. ✅ 支持图片显示（使用 img 标签）
5. ✅ 支持图标显示（使用 img 标签或 ReactNode）
6. ✅ 支持角标（Badge）显示（红点、数字、文本）
7. ✅ 支持点击事件
8. ✅ 支持选中状态管理
9. ✅ 支持前后插槽
10. ✅ 支持图形区域插槽
11. ✅ 支持宽度模式切换
12. ✅ 支持垂直和横向布局

### 样式需求

1. ✅ 支持自定义背景、圆角、边框、阴影、padding、gap
2. ✅ 支持自定义文本大小、粗细、颜色、显示行数
3. ✅ 支持自定义图形区域大小、圆角、背景
4. ✅ 支持自定义角标位置和样式
5. ✅ 支持主题色 CSS 变量（`var(--theme-color)`）
6. ✅ 文本超出显示省略号
7. ✅ 图形区域尺寸可配置

### 性能需求

1. ✅ 使用 `useMemo` 优化样式合并
2. ✅ 使用 `useCallback` 优化事件处理
3. ✅ 避免不必要的重新渲染

## 四、组件 API 设计

### Props 定义

```
export interface GraphicTextCardProps extends PisellBasicCardProps {
  // ========== 基础配置 ==========
  /** 卡片显示的文本内容（必填） */
  text: ReactNode;

  /** 卡片样式变体 */
  variant?: 'vertical-image' | 'vertical-icon' | 'horizontal' | 'card';

  /** 卡片尺寸 */
  size?: 'small' | 'default' | 'large';

  /** 是否可点击 */
  clickable?: boolean;

  // ========== 图形区域配置 ==========
  /** 图片地址（与 icon 二选一） */
  image?: string;

  /** 图标（ReactNode 或 图标组件，与 image 二选一） */
  icon?: ReactNode;

  /** 图形区域配置 */
  graphicConfig?: {
    /** 图形区域宽度 */
    width?: number;
    /** 图形区域高度 */
    height?: number;
    /** 图形区域圆角 */
    borderRadius?: number | string;
    /** 图形区域背景色 */
    background?: string;
    /** 图形区域边框 */
    border?: string;
    /** 图形区域阴影 */
    boxShadow?: string;
    /** 图形区域内边距 */
    padding?: number | string;
    /** 图标大小（当使用 icon 时） */
    iconSize?: number;
    /** 图标颜色（当使用 icon 时） */
    iconColor?: string;
    /** 自定义样式 */
    style?: CSSProperties;
  };

  /** 图形区域插槽（替换 image 和 icon） */
  graphicSlot?: ReactNode;

  // ========== 角标配置 ==========
  /** 角标配置 */
  badge?: {
    type?: 'dot' | 'number' | 'text';
    content?: ReactNode;
    position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
    offset?: [number, number];
    color?: string;
    style?: CSSProperties;
  };

  // ========== 插槽配置 ==========
  /** 前置插槽 */
  prefixSlot?: ReactNode;

  /** 后置插槽 */
  suffixSlot?: ReactNode;

  // ========== 文本配置 ==========
  /** 文本配置 */
  textConfig?: {
    fontSize?: number;
    fontWeight?: string | number;
    color?: string;
    maxLines?: number;
    ellipsis?: boolean;
    textAlign?: 'left' | 'center' | 'right';
  };

  // ========== 宽度配置 ==========
  /** 宽度模式 */
  widthMode?: 'auto' | 'fixed' | 'full';

  /** 固定宽度值（widthMode='fixed' 时有效） */
  width?: number | string;

  /** 最大宽度 */
  maxWidth?: number | string;

  // ========== 事件配置 ==========
  /** 点击事件 */
  onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;

  /** 自定义渲染函数 */
  render?: (props: GraphicTextCardProps) => ReactNode;
}
```

### PisellBasicCardProps 继承

```
export interface PisellBasicCardProps extends React.HTMLAttributes<HTMLDivElement> {
  /** 激活状态配置 */
  active?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: ReactNode;
  };

  /** 禁用状态配置 */
  disabled?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: ReactNode;
  };

  /** 加载状态配置 */
  loading?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: ReactNode;
  };

  children?: ReactNode;
}
```

## 五、组件模块拆解

### 主要模块

```
GraphicTextCard/
├── index.tsx                      # 导出文件
├── GraphicTextCard.tsx            # 组件主实现
├── GraphicTextCard.less           # 组件样式
├── GraphicTextCard.test.tsx       # 单元测试
├── types.ts                       # 类型定义
├── hooks/                         # 自定义 Hooks
│   ├── index.ts                   # Hooks 导出
│   ├── useGraphicTextCardStyle.ts # 样式合并 Hook
│   └── useGraphicTextCardClick.ts # 点击事件处理 Hook
├── components/                    # 子组件
│   ├── index.tsx                  # 子组件导出
│   ├── GraphicArea.tsx            # 图形区域组件
│   ├── TextContent.tsx            # 文本内容组件
│   └── BadgeWrapper.tsx           # 角标包装组件（复用 TextCard）
└── utils/                         # 工具函数
    ├── index.ts                   # 工具导出
    └── getDefaultGraphicSize.ts   # 获取默认图形尺寸
```

### 子组件职责

#### GraphicArea 组件

- 负责渲染图形区域（图片或图标）
- 处理图片加载和错误
- 支持自定义图形区域样式
- 支持图标大小和颜色配置

#### TextContent 组件

- 负责渲染主文本内容
- 处理文本省略逻辑
- 支持多行文本限制
- 支持文本对齐方式

#### BadgeWrapper 组件

- 复用 TextCard 的 BadgeWrapper 组件
- 负责角标的渲染和定位
- 支持红点、数字、文本三种类型
- 支持自定义位置和偏移

## 六、组件实现方案

### 6.1 样式变体实现

使用 CSS 变量 + Less 预处理器实现样式切换：

```
// GraphicTextCard.less
.graphic-text-card {
  display: inline-flex;
  position: relative;
  box-sizing: border-box;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  cursor: default;
  user-select: none;

  // ========== 可点击状态 ==========
  &--clickable {
    cursor: pointer;

    &:active {
      transform: scale(0.98);
    }
  }

  // ========== 尺寸变体 ==========
  &--small {
    font-size: 12px;
    line-height: 18px;
    gap: 4px;
  }

  &--default {
    font-size: 14px;
    line-height: 20px;
    gap: 6px;
  }

  &--large {
    font-size: 16px;
    line-height: 22px;
    gap: 8px;
  }

  // ========== 样式变体 1: 垂直布局-图片 ==========
  &--vertical-image {
    flex-direction: column;
    align-items: center;

    &.graphic-text-card--active {
      .graphic-text-card__text {
        color: var(--theme-color, #6941C6);
      }
    }

    &:hover:not(.graphic-text-card--disabled):not(.graphic-text-card--loading).graphic-text-card--clickable {
      .graphic-text-card__text {
        color: var(--Gray-900, #101828);
      }
    }
  }

  // ========== 样式变体 2: 垂直布局-图标 ==========
  &--vertical-icon {
    flex-direction: column;
    align-items: center;

    &.graphic-text-card--active {
      .graphic-text-card__graphic,
      .graphic-text-card__text {
        color: var(--theme-color, #7F56D9);
      }
    }

    &:hover:not(.graphic-text-card--disabled):not(.graphic-text-card--loading).graphic-text-card--clickable {
      .graphic-text-card__graphic,
      .graphic-text-card__text {
        color: var(--Gray-900, #101828);
      }
    }
  }

  // ========== 样式变体 3: 横向布局 ==========
  &--horizontal {
    flex-direction: row;
    align-items: center;
    padding: 4px;
    border-radius: 8px;
    border: 1px solid #E2E2E2;
    background: #FFFFFF;

    &.graphic-text-card--active {
      background: var(--theme-color, #7F56D9);
      border-color: var(--theme-color, #7F56D9);

      .graphic-text-card__text {
        color: #FFFFFF;
      }
    }

    &:hover:not(.graphic-text-card--disabled):not(.graphic-text-card--loading):not(.graphic-text-card--active).graphic-text-card--clickable {
      background: var(--Gray-50, #F9FAFB);
    }
  }

  // ========== 样式变体 4: 卡片式 ==========
  &--card {
    flex-direction: column;
    align-items: flex-start;
    padding: 4px;
    border-radius: 8px;
    border: 1px solid var(--Gray-600, #475467);
    background: var(--Gray-600, #475467);

    &.graphic-text-card--active {
      border: 4px solid var(--theme-color, #7F56DA);
      background: var(--theme-color, #7F56DA);
      padding: 4px;

      .graphic-text-card__content-wrapper {
        background: #FFFFFF;
      }

      .graphic-text-card__text {
        color: #000000;
      }
    }

    &:hover:not(.graphic-text-card--disabled):not(.graphic-text-card--loading):not(.graphic-text-card--active).graphic-text-card--clickable {
      background: var(--Gray-900, #101828);
      border-color: var(--Gray-900, #101828);
    }

    .graphic-text-card__text {
      color: #FFFFFF;
      text-align: center;
    }
  }

  // ========== 状态样式 ==========
  &--disabled {
    cursor: not-allowed;
    opacity: 0.4;
  }

  &--loading {
    cursor: wait;
    opacity: 0.6;
  }

  // ========== 图形区域 ==========
  &__graphic {
    position: relative;
    flex-shrink: 0;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }

  // ========== 文本区域 ==========
  &__text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }

  // ========== Extra 容器 ==========
  &__extra {
    margin-left: auto;
    flex-shrink: 0;
  }
}
```

### 6.2 图形区域默认尺寸

不同样式变体和尺寸对应的默认图形区域尺寸：

```
// utils/getDefaultGraphicSize.ts
export const getDefaultGraphicSize = (
  variant: GraphicTextCardVariant,
  size: GraphicTextCardSize
): { width: number; height: number } => {
  // 样式一和样式二（垂直布局）
  if (variant === 'vertical-image' || variant === 'vertical-icon') {
    const sizeMap = {
      small: { width: 28, height: 28 },
      default: { width: 36, height: 36 },
      large: { width: 44, height: 44 },
    };
    return sizeMap[size];
  }

  // 样式三（横向布局）
  if (variant === 'horizontal') {
    const sizeMap = {
      small: { width: 48, height: 40 },
      default: { width: 64, height: 52 },
      large: { width: 80, height: 64 },
    };
    return sizeMap[size];
  }

  // 样式四（卡片式）
  if (variant === 'card') {
    const sizeMap = {
      small: { width: 56, height: 56 },
      default: { width: 72, height: 60 },
      large: { width: 88, height: 72 },
    };
    return sizeMap[size];
  }

  return { width: 36, height: 36 };
};
```

### 6.3 图形区域组件实现

```
// components/GraphicArea.tsx
import React from 'react';
import { GraphicAreaProps } from '../types';

const GraphicArea: React.FC<GraphicAreaProps> = (props) => {
  const {
    image,
    icon,
    config = {},
    className,
  } = props;

  const {
    width,
    height,
    borderRadius,
    background,
    border,
    boxShadow,
    padding,
    iconSize,
    iconColor,
    style: customStyle,
  } = config;

  const graphicStyle: React.CSSProperties = {
    width,
    height,
    borderRadius,
    background,
    border,
    boxShadow,
    padding,
    ...customStyle,
  };

  // 渲染图片
  if (image) {
    return (
      <div className={classNames('graphic-text-card__graphic', className)} style={graphicStyle}>
        <img src={image} alt="" />
      </div>
    );
  }

  // 渲染图标
  if (icon) {
    const iconStyle: React.CSSProperties = {
      fontSize: iconSize,
      color: iconColor,
    };

    return (
      <div className={classNames('graphic-text-card__graphic', className)} style={graphicStyle}>
        {typeof icon === 'string' ? (
          <img src={icon} alt="" style={iconStyle} />
        ) : (
          <span style={iconStyle}>{icon}</span>
        )}
      </div>
    );
  }

  return null;
};

export default GraphicArea;
```

### 6.4 状态合并逻辑

使用自定义 Hook `useGraphicTextCardStyle` 实现：

```
// hooks/useGraphicTextCardStyle.ts
export const useGraphicTextCardStyle = (props: GraphicTextCardProps) => {
  const {
    variant = 'vertical-image',
    size = 'default',
    active,
    disabled,
    loading,
    className,
    style,
    widthMode = 'auto',
    width,
    maxWidth = 400,
    clickable = true,
  } = props;

  // 样式合并
  const mergedStyle = useMemo(() => {
    let finalStyle: CSSProperties = { ...style };

    // 宽度处理
    if (widthMode === 'auto') {
      finalStyle.width = 'auto';
      finalStyle.maxWidth = maxWidth;
    } else if (widthMode === 'fixed' && width) {
      finalStyle.width = width;
    } else if (widthMode === 'full') {
      finalStyle.width = '100%';
    }

    // 状态样式合并（优先级：loading > disabled > active）
    if (loading?.value && loading?.style) {
      finalStyle = { ...finalStyle, ...loading.style };
    } else if (disabled?.value && disabled?.style) {
      finalStyle = { ...finalStyle, ...disabled.style };
    } else if (active?.value && active?.style) {
      finalStyle = { ...finalStyle, ...active.style };
    }

    return finalStyle;
  }, [style, active, disabled, loading, widthMode, width, maxWidth]);

  // 类名合并
  const mergedClassName = useMemo(() => {
    return classNames(
      'graphic-text-card',
      `graphic-text-card--${variant}`,
      `graphic-text-card--${size}`,
      className,
      {
        'graphic-text-card--active': active?.value,
        'graphic-text-card--disabled': disabled?.value,
        'graphic-text-card--loading': loading?.value,
        'graphic-text-card--clickable': clickable && !disabled?.value && !loading?.value,
      },
      active?.value && active?.className,
      disabled?.value && disabled?.className,
      loading?.value && loading?.className
    );
  }, [className, variant, size, active, disabled, loading, clickable]);

  return { mergedStyle, mergedClassName };
};
```

### 6.5 主组件实现

```
// GraphicTextCard.tsx
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { GraphicTextCardProps } from './types';
import { GraphicArea, TextContent, BadgeWrapper } from './components';
import { useGraphicTextCardStyle } from './hooks';
import { getDefaultGraphicSize } from './utils';
import './GraphicTextCard.less';

const GraphicTextCard: React.FC<GraphicTextCardProps> = (props) => {
  const {
    text,
    variant = 'vertical-image',
    size = 'default',
    image,
    icon,
    graphicConfig = {},
    graphicSlot,
    badge,
    prefixSlot,
    suffixSlot,
    textConfig,
    active,
    disabled,
    loading,
    children,
    render,
    onClick,
    ...restProps
  } = props;

  const { mergedStyle, mergedClassName } = useGraphicTextCardStyle(props);

  // 计算图形区域尺寸
  const graphicSize = useMemo(() => {
    const defaultSize = getDefaultGraphicSize(variant, size);
    return {
      width: graphicConfig.width ?? defaultSize.width,
      height: graphicConfig.height ?? defaultSize.height,
    };
  }, [variant, size, graphicConfig.width, graphicConfig.height]);

  // 处理点击事件
  const handleClick = useCallback(
    (event: React.MouseEvent<HTMLDivElement>) => {
      if (disabled?.value || loading?.value || !props.clickable) {
        event.preventDefault();
        return;
      }

      if (active?.onChange && !active.value) {
        active.onChange(true);
      }

      if (onClick) {
        onClick(event);
      }
    },
    [onClick, active, disabled, loading, props.clickable]
  );

  // 自定义渲染
  if (render) {
    return <>{render(props)}</>;
  }

  return (
    <BadgeWrapper badge={badge}>
      <div
        className={mergedClassName}
        style={mergedStyle}
        onClick={handleClick}
        data-active={active?.value}
        data-disabled={disabled?.value}
        data-loading={loading?.value}
        role="button"
        aria-disabled={disabled?.value}
        tabIndex={disabled?.value ? -1 : 0}
        {...restProps}
      >
        {/* 前置插槽 */}
        {prefixSlot}

        {/* 图形区域 */}
        {graphicSlot || (
          <GraphicArea
            image={image}
            icon={icon}
            config={{
              ...graphicConfig,
              width: graphicSize.width,
              height: graphicSize.height,
            }}
          />
        )}

        {/* 文本内容 */}
        <TextContent text={text} textConfig={textConfig} />

        {/* 后置插槽 */}
        {suffixSlot}

        {/* 状态 extra 渲染 */}
        {loading?.value && loading?.extra}
        {disabled?.value && disabled?.extra && !loading?.value}
        {active?.value && active?.extra && !loading?.value && !disabled?.value}

        {/* 子内容 */}
        {children}
      </div>
    </BadgeWrapper>
  );
};

// Provider 包装器（兼容 PisellCards 系统）
const Provider = (props: any) => <GraphicTextCard {...props?.props} {...props} />;

export default Provider;
```

## 七、组件组成结构图

```
┌─────────────────────────────────────────────────────┐
│           GraphicTextCard 主组件                     │
│  ┌───────────────────────────────────────────────┐  │
│  │         BadgeWrapper（角标包装）              │  │
│  │  ┌─────────────────────────────────────────┐  │  │
│  │  │      GraphicTextCard Container         │  │  │
│  │  │  ┌──────────────────────────────────┐  │  │  │
│  │  │  │      prefixSlot（前置插槽）      │  │  │  │
│  │  │  └──────────────────────────────────┘  │  │  │
│  │  │  ┌──────────────────────────────────┐  │  │  │
│  │  │  │   GraphicArea（图形区域组件）    │  │  │  │
│  │  │  │   - 图片渲染 (image)            │  │  │  │
│  │  │  │   - 图标渲染 (icon)             │  │  │  │
│  │  │  │   - 自定义样式支持              │  │  │  │
│  │  │  └──────────────────────────────────┘  │  │  │
│  │  │  ┌──────────────────────────────────┐  │  │  │
│  │  │  │   TextContent（文本内容组件）    │  │  │  │
│  │  │  │   - 文本省略处理                │  │  │  │
│  │  │  │   - 多行文本支持                │  │  │  │
│  │  │  │   - 对齐方式配置                │  │  │  │
│  │  │  └──────────────────────────────────┘  │  │  │
│  │  │  ┌──────────────────────────────────┐  │  │  │
│  │  │  │      suffixSlot（后置插槽）      │  │  │  │
│  │  │  └──────────────────────────────────┘  │  │  │
│  │  │  ┌──────────────────────────────────┐  │  │  │
│  │  │  │      extra（状态额外渲染）       │  │  │  │
│  │  │  └──────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
```

### 引用的现有组件

1. **BadgeWrapper** - 来自 `TextCard/components/BadgeWrapper`
   - 功能：角标渲染和定位
   - 基于：Ant Design Badge 组件

2. **PisellBasicCardProps** - 来自 `PisellCards/types`
   - 功能：提供统一的状态管理接口

3. **classNames** - 来自 `classnames` 包
   - 功能：动态类名合并

## 八、技术要点

### 8.1 使用 Ant Design 组件

- 使用 `Badge` 组件实现角标（通过 BadgeWrapper）
- 复用 Ant Design 的设计 Token

### 8.2 主题色适配

所有紫色系颜色使用 CSS 变量：

```
:root {
  --theme-color: #7F56D9;
  --theme-color-light: #B692F6;
  --theme-color-bg: #F9F5FF;
}
```

### 8.3 性能优化

- 使用 `useMemo` 缓存样式和类名计算
- 使用 `useCallback` 缓存事件处理函数
- 使用 `React.memo` 包装子组件（如需要）

### 8.4 可访问性

- 添加 `role="button"` 属性
- 添加 `aria-disabled` 属性
- 添加 `tabIndex` 支持键盘导航

### 8.5 类型安全

- 使用 TypeScript 严格模式
- 所有 Props 使用 Interface 定义
- 导出所有类型定义供外部使用

## 九、使用示例

### 基础用法（样式一）

```
<PisellCards.GraphicTextCard 
  text="Tab title" 
  image="https://example.com/image.png"
  variant="vertical-image"
/>
```

### 使用图标（样式二）

```
<PisellCards.GraphicTextCard
  text="Tabs"
  icon={<SmileIcon />}
  variant="vertical-icon"
/>
```

### 横向布局（样式三）

```
<PisellCards.GraphicTextCard
  text="Tab title"
  image="https://example.com/image.png"
  variant="horizontal"
  size="default"
/>
```

### 卡片式（样式四）

```
<PisellCards.GraphicTextCard
  text="Tab title"
  image="https://example.com/image.png"
  variant="card"
  active={{ value: true }}
/>
```

### 带角标

```
<PisellCards.GraphicTextCard
  text="Tab title"
  icon={<SmileIcon />}
  badge={{ type: 'number', content: 99 }}
  variant="vertical-icon"
/>
```

### 自定义图形区域

```
<PisellCards.GraphicTextCard
  text="Tab title"
  image="https://example.com/image.png"
  variant="vertical-image"
  graphicConfig={{
    width: 60,
    height: 60,
    borderRadius: 8,
    background: '#F9F5FF',
    padding: 8,
  }}
/>
```

### 完整配置

```
<PisellCards.GraphicTextCard
  text="Tab title"
  variant="horizontal"
  size="large"
  image="https://example.com/image.png"
  badge={{ type: 'number', content: 99 }}
  active={{
    value: true,
    onChange: (value) => console.log('Active changed:', value)
  }}
  onClick={(e) => console.log('Clicked')}
  widthMode="fixed"
  width={200}
  graphicConfig={{
    width: 80,
    height: 64,
    borderRadius: 8,
  }}
  textConfig={{
    fontSize: 16,
    fontWeight: 600,
    color: '#101828',
    textAlign: 'center',
  }}
/>
```

## 十、测试要点

### 单元测试

1. 测试不同 variant 渲染
2. 测试不同 size 渲染
3. 测试状态切换（active、disabled、loading）
4. 测试点击事件触发
5. 测试图片、图标渲染
6. 测试角标渲染
7. 测试插槽功能
8. 测试文本省略
9. 测试图形区域尺寸计算

### 快照测试

对每种样式变体和尺寸组合生成快照。

### 可访问性测试

使用 `@testing-library/react` 测试可访问性。

## 十一、注意事项

1. **不要使用 tailwind**，项目使用 Less 预处理器
2. **必须继承 PisellBasicCardProps**，保持与其他 Card 组件一致
3. **必须在 PisellCards 命名空间下导出**
4. **紫色系颜色使用 CSS 变量**，不要硬编码颜色值
5. **优先使用 Ant Design 组件**，避免重复造轮子
6. **代码必须添加详细的 JSDoc 注释**
7. **组件要足够通用和可扩展**
8. **图片和图标使用 img 标签渲染**
9. **复用 TextCard 的 BadgeWrapper 组件**
10. **图形区域尺寸可配置，提供合理的默认值**

