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

# TextCard 开发文档

## 一、组件基本信息

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

## 二、组件描述

TextCard 是一个文本卡片组件，用于展示带图标、文本、标签和角标的信息项。组件遵循 PisellCards 规范，继承 `PisellBasicCardProps`，提供统一的状态管理接口（active、disabled、loading）。

### 核心特性

- 支持 6 种样式变体（纯文本、圆角背景、浅色背景等）
- 支持 3 种尺寸（small、default、large）
- 支持 4 种状态（default、hover、active、disabled）
- 支持前置图标、后置图标、后置标签、角标
- 支持自定义插槽（前置、后置）
- 支持主题色变量
- 宽度自适应（内容撑满、固定宽度、撑满父容器）

## 三、组件需求点罗列

### 功能需求

1. ✅ 支持 6 种样式变体切换
2. ✅ 支持 3 种尺寸切换（small、default、large）
3. ✅ 支持 4 种状态（default、hover、active、disabled）
4. ✅ 支持前置图标显示
5. ✅ 支持后置图标显示
6. ✅ 支持后置标签（Tag）显示
7. ✅ 支持角标（Badge）显示（红点、数字、文本）
8. ✅ 支持点击事件
9. ✅ 支持选中状态管理
10. ✅ 支持前后插槽
11. ✅ 支持宽度模式切换

### 样式需求

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

### 性能需求

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

## 四、组件 API 设计

### Props 定义

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

  /** 卡片样式变体 */
  variant?: 'text' | 'filled' | 'outlined' | 'light' | 'gradient' | 'dashed';

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

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

  // ========== 图标配置 ==========
  /** 前置图标 */
  prefixIcon?: ReactNode;

  /** 后置图标 */
  suffixIcon?: ReactNode;

  /** 图标配置 */
  iconConfig?: {
    size?: number;
    color?: string;
  };

  // ========== 标签配置 ==========
  /** 后置标签 */
  tag?: ReactNode;

  /** 标签配置 */
  tagConfig?: {
    color?: string;
    style?: CSSProperties;
  };

  // ========== 角标配置 ==========
  /** 角标配置 */
  badge?: {
    type?: 'dot' | 'number' | 'text';
    content?: ReactNode;
    offset?: [number, number];
    size?: 'small' | 'default';
    color?: string;
    style?: CSSProperties;
    showZero?: boolean;
    [key: string]: any;
  };

  // ========== 插槽配置 ==========
  /** 前置插槽（替换前置图标） */
  prefixSlot?: ReactNode;

  /** 后置插槽（替换后置图标和标签） */
  suffixSlot?: ReactNode;

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

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

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

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

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

  /** 自定义渲染函数 */
  render?: (props: TextCardProps) => 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;
}
```

## 五、组件模块拆解

### 主要模块

```
TextCard/
├── index.tsx                 # 导出文件
├── TextCard.tsx              # 组件主实现
├── TextCard.less             # 组件样式
├── TextCard.test.tsx         # 单元测试
├── types.ts                  # 类型定义
├── hooks/                    # 自定义 Hooks
│   ├── index.ts              # Hooks 导出
│   ├── useTextCardStyle.ts   # 样式合并 Hook
│   └── useTextCardClick.ts   # 点击事件处理 Hook
├── components/               # 子组件
│   ├── index.tsx             # 子组件导出
│   ├── TextContent.tsx       # 文本内容组件
│   ├── IconWrapper.tsx       # 图标包装组件
│   └── BadgeWrapper.tsx      # 角标包装组件
└── utils/                    # 工具函数
    ├── index.ts              # 工具导出
    └── getVariantStyle.ts    # 获取样式变体配置
```

### 子组件职责

#### TextContent 组件

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

#### IconWrapper 组件

- 统一图标渲染逻辑
- 支持自定义图标大小和颜色
- 兼容 ReactNode 和图标组件

#### BadgeWrapper 组件

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

## 六、组件实现方案

### 6.1 样式变体实现

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

```
// TextCard.less
.text-card {
  display: inline-flex;
  align-items: center;
  position: relative;
  cursor: pointer;
  transition: all 0.3s;

  // ========== 尺寸变体 ==========
  &--small {
    padding: var(--text-card-padding-small, 8px 12px);
    font-size: var(--text-card-font-size-small, 14px);
    line-height: var(--text-card-line-height-small, 22px);
    border-radius: var(--text-card-border-radius-small, 4px);
  }

  &--default {
    padding: var(--text-card-padding-default, 12px 16px);
    font-size: var(--text-card-font-size-default, 14px);
    line-height: var(--text-card-line-height-default, 22px);
    border-radius: var(--text-card-border-radius-default, 6px);
  }

  &--large {
    padding: var(--text-card-padding-large, 16px 20px);
    font-size: var(--text-card-font-size-large, 16px);
    line-height: var(--text-card-line-height-large, 24px);
    border-radius: var(--text-card-border-radius-large, 8px);
  }

  // ========== 样式变体 1: 纯文本 ==========
  &--text {
    padding: 0;
    background: transparent;
    border: none;

    &.text-card--active {
      color: var(--theme-color, #7F56D9);
      font-weight: 600;
    }

    &:hover:not(.text-card--disabled) {
      color: var(--theme-color-light, #B692F6);
    }
  }

  // ========== 样式变体 2: 圆角背景 ==========
  &--filled {
    border-radius: 40px;

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

    &:hover:not(.text-card--disabled):not(.text-card--active) {
      background: #F2F4F7;
    }
  }

  // ========== 样式变体 3: 浅色背景 ==========
  &--light {
    &.text-card--active {
      background: var(--theme-color-bg, #F9F5FF);
      color: var(--theme-color, #7F56D9);
      font-weight: 600;
    }

    &:hover:not(.text-card--disabled):not(.text-card--active) {
      background: #F2F4F7;
    }
  }

  // ========== 状态样式 ==========
  &--disabled {
    cursor: not-allowed;
    opacity: 0.4;
    color: rgba(0, 0, 0, 0.25);
  }
}
```

### 6.2 状态合并逻辑

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

```
// hooks/useTextCardStyle.ts
export const useTextCardStyle = (props: TextCardProps) => {
  const {
    variant = 'text',
    size = 'default',
    active,
    disabled,
    loading,
    className,
    style,
    widthMode = 'auto',
    width,
    maxWidth = 400
  } = 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%';
      finalStyle.minWidth = 28;
    }

    // 状态样式合并（优先级：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(
      'text-card',
      `text-card--${variant}`,
      `text-card--${size}`,
      className,
      {
        'text-card--active': active?.value,
        'text-card--disabled': disabled?.value,
        'text-card--loading': loading?.value,
      },
      active?.value && active?.className,
      disabled?.value && disabled?.className,
      loading?.value && loading?.className
    );
  }, [className, variant, size, active, disabled, loading]);

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

### 6.3 点击事件处理

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

```
// hooks/useTextCardClick.ts
export const useTextCardClick = (props: TextCardProps) => {
  const { onClick, active, disabled, loading, clickable = true } = props;

  const handleClick = useCallback(
    (event: React.MouseEvent<HTMLDivElement>) => {
      // 禁用或加载状态下不触发点击
      if (disabled?.value || loading?.value || !clickable) {
        event.preventDefault();
        return;
      }

      // 触发 active.onChange（如果提供且未激活）
      if (active?.onChange && !active.value) {
        active.onChange(true);
      }

      // 触发 onClick
      if (onClick) {
        onClick(event);
      }
    },
    [onClick, active, disabled, loading, clickable]
  );

  return handleClick;
};
```

### 6.4 文本省略处理

使用 CSS + 文本配置实现：

```
// TextContent.tsx
const TextContent: React.FC<TextContentProps> = (props) => {
  const {
    text,
    textConfig = {},
    className
  } = props;

  const {
    maxLines = 1,
    ellipsis = true,
    fontSize,
    fontWeight,
    color
  } = textConfig;

  const textStyle: CSSProperties = {
    fontSize,
    fontWeight,
    color,
    ...(ellipsis && maxLines === 1 ? {
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      whiteSpace: 'nowrap',
    } : {}),
    ...(ellipsis && maxLines > 1 ? {
      display: '-webkit-box',
      WebkitLineClamp: maxLines,
      WebkitBoxOrient: 'vertical',
      overflow: 'hidden',
    } : {}),
  };

  return (
    <div className={classNames('text-card__content', className)} style={textStyle}>
      {text}
    </div>
  );
};
```

### 6.5 角标渲染

使用 Ant Design Badge 组件封装，位置通过 `offset` 控制，大小通过 `size` 控制，零值显示通过 `showZero` 控制：

```
// BadgeWrapper.tsx
import React, { CSSProperties } from 'react';
import { Badge } from 'antd';

const BadgeWrapper: React.FC<BadgeWrapperProps> = (props) => {
  const { badge, children } = props;

  if (!badge) {
    return <>{children}</>;
  }

  const { type = 'dot', content, offset, size, color, style, showZero = false, ...rest } = badge;

  // 根据 type 渲染不同的角标
  if (type === 'dot') {
    return (
      <Badge dot color={color} offset={offset} size={size} style={style} {...rest}>
        {children}
      </Badge>
    );
  }

  if (type === 'number' || type === 'text') {
    return (
      <Badge
        count={content}
        color={color}
        offset={offset}
        size={size}
        style={style}
        showZero={showZero}
        {...rest}
      >
        {children}
      </Badge>
    );
  }

  return <>{children}</>;
};
```

**角标位置控制说明**：

- 角标位置通过 `offset` 控制，不设置 `offset` 时默认在右上角
- `offset` 格式为 `[x, y]`，正数向右/下偏移，负数向左/上偏移
- 例如：`offset: [0, 0]` 为默认位置，`offset: [-10, 0]` 向左偏移，`offset: [0, 10]` 向下偏移

**角标大小控制说明**：

- 角标大小通过 `size` 属性控制，可选值：`'small' | 'default'`
- `size='small'` 时显示小尺寸角标
- `size='default'` 时显示默认尺寸角标
- 不设置 `size` 时使用默认尺寸

**角标显示零值说明**：

- 通过 `showZero` 属性控制数字角标是否显示零值
- `showZero=true` 时，当 `content` 为 0 时显示角标
- `showZero=false` 时，当 `content` 为 0 时不显示角标（默认）
- 非零值不受 `showZero` 影响，正常显示

### 6.6 主组件实现

```
// TextCard.tsx
import React from 'react';
import { TextCardProps } from './types';
import { useTextCardStyle } from './hooks/useTextCardStyle';
import { useTextCardClick } from './hooks/useTextCardClick';
import TextContent from './components/TextContent';
import IconWrapper from './components/IconWrapper';
import BadgeWrapper from './components/BadgeWrapper';
import './TextCard.less';

const TextCard: React.FC<TextCardProps> = (props) => {
  const {
    text,
    prefixIcon,
    suffixIcon,
    tag,
    badge,
    prefixSlot,
    suffixSlot,
    iconConfig,
    tagConfig,
    textConfig,
    active,
    disabled,
    loading,
    children,
    render,
    ...restProps
  } = props;

  const { mergedStyle, mergedClassName } = useTextCardStyle(props);
  const handleClick = useTextCardClick(props);

  // 自定义渲染
  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 || (prefixIcon && (
          <IconWrapper icon={prefixIcon} config={iconConfig} position="prefix" />
        ))}

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

        {/* 后置插槽或标签+图标 */}
        {suffixSlot || (
          <>
            {tag && <div className="text-card__tag" style={tagConfig?.style}>{tag}</div>}
            {suffixIcon && (
              <IconWrapper icon={suffixIcon} config={iconConfig} position="suffix" />
            )}
          </>
        )}

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

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

export default TextCard;
```

## 七、技术要点

### 7.1 使用 Ant Design 组件

- 使用 `Badge` 组件实现角标
- 使用 `Tag` 组件（可选）实现后置标签
- 复用 Ant Design 的设计 Token

### 7.2 主题色适配

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

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

### 7.3 性能优化

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

### 7.4 可访问性

- 添加 `role="button"` 属性
- 添加 `aria-disabled` 属性
- 添加 `tabIndex` 支持键盘导航
- 支持 `onKeyDown` 事件（空格键、回车键触发点击）

### 7.5 类型安全

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

## 八、使用示例

### 基础用法

```
<PisellCards.TextCard text="Tab title" />
```

### 带图标

```
<PisellCards.TextCard
  text="Password"
  prefixIcon={<BellIcon />}
  suffixIcon={<ChevronDownIcon />}
/>
```

### 带标签

```
<PisellCards.TextCard
  text="Password"
  tag={<Badge count={2} />}
/>
```

### 带角标

```
<PisellCards.TextCard
  text="Password"
  badge={{ type: 'number', content: 99 }}
/>
```

### 激活状态

```
<PisellCards.TextCard
  text="Tab title"
  active={{ value: true }}
/>
```

### 完整配置

```
<PisellCards.TextCard
  text="Tab title"
  variant="filled"
  size="large"
  prefixIcon={<BellIcon />}
  suffixIcon={<ChevronDownIcon />}
  badge={{ type: 'number', content: 99 }}
  active={{
    value: true,
    onChange: (value) => console.log('Active changed:', value)
  }}
  onClick={(e) => console.log('Clicked')}
  widthMode="fixed"
  width={200}
/>
```

## 九、测试要点

### 单元测试

1. 测试不同 variant 渲染
2. 测试不同 size 渲染
3. 测试状态切换（active、disabled、loading）
4. 测试点击事件触发
5. 测试图标、标签、角标渲染
6. 测试插槽功能
7. 测试文本省略

### 快照测试

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

### 可访问性测试

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

## 十、注意事项

1. **不要使用 tailwind**，项目使用 Less 预处理器
2. **必须继承 PisellBasicCardProps**，保持与其他 Card 组件一致
3. **必须在 PisellCards 命名空间下导出**
4. **紫色系颜色使用 CSS 变量**，不要硬编码颜色值
5. **优先使用 Ant Design 组件**，避免重复造轮子
6. **代码必须添加详细的 JSDoc 注释**
7. **组件要足够通用和可扩展**
