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

# PisellImageCard 开发文档

## 1. 组件基本信息

### 1.1 组件名称

**PisellImageCard** - 图片卡片组件

### 1.2 组件版本号

- **版本**: v1.0.0
- **最后更新**: 2025-12-11

### 1.3 组件分类

- **分类**: 基础卡片组件（Card Components）
- **命名空间**: PisellCards.PisellImageCard
- **所属物料库**: @pisell/materials

---

## 2. 组件描述

PisellImageCard 是一个功能完整的图片展示卡片组件，基于 PisellBasicCardProps 扩展，提供了图片加载、失败处理、占位符、懒加载等完整功能。

### 2.1 核心特性

- ✅ 支持多种图片填充模式（cover/contain/fill/none）
- ✅ 支持图片加载失败兜底处理（errorSrc）
- ✅ 支持占位图展示（fallbackSrc）
- ✅ 支持文字降级展示（无图片时显示 alt 前 2 个字符）
- ✅ 支持图片懒加载配置
- ✅ 支持自定义尺寸（width/height）
- ✅ 继承 PisellBasicCardProps 的所有状态能力（active/disabled/loading）
- ✅ 支持点击事件（可选）

### 2.2 使用场景

- 商品列表中的商品图片
- 用户头像展示
- 图片画廊/相册
- 标签页/分类导航中的图片
- 任何需要展示图片的卡片场景

---

## 3. 组件需求点罗列

### 3.1 功能需求

| 需求项     | 优先级 | 描述                                         | 状态 |
| ---------- | ------ | -------------------------------------------- | ---- |
| 数据源展示 | P0     | 支持通过 dataSource 配置图片地址、替换文本等 | ✅   |
| 填充模式   | P0     | 支持 cover/contain/fill/none 四种填充模式    | ✅   |
| 尺寸控制   | P0     | 支持自定义 width 和 height，默认 100%        | ✅   |
| 占位图     | P0     | 支持首次加载时的占位图（fallbackSrc）        | ✅   |
| 失败兜底   | P0     | 支持加载失败后的兜底图（errorSrc）           | ✅   |
| 文字降级   | P0     | 无图片时显示 alt 前 2 个字符                 | ✅   |
| 懒加载     | P1     | 支持配置图片懒加载                           | ✅   |
| 状态继承   | P0     | 继承 PisellBasicCardProps 的状态能力         | ✅   |
| 点击事件   | P1     | 支持点击事件（可选）                         | ✅   |

### 3.2 非功能需求

| 需求项   | 描述                                    |
| -------- | --------------------------------------- |
| 性能     | 图片懒加载，减少初始加载压力            |
| 可扩展性 | 基于 PisellBasicCardProps，支持业务扩展 |
| 可维护性 | 代码结构清晰，职责分离                  |
| 可测试性 | 提供完整的单元测试用例                  |
| 兼容性   | 支持主流浏览器                          |

---

## 4. 组件 API 设计

### 4.1 Props 定义

```
interface PisellImageCardProps extends Omit<PisellBasicCardProps, 'onClick'> {
  /**
   * 图片数据源配置
   */
  dataSource?: {
    /** 图片地址 */
    src?: string;
    /** 替换文本（无图片时显示前2个字符） */
    alt?: string;
    /** 加载失败后的兜底图 */
    errorSrc?: string;
    /** 首次加载时的占位图 */
    fallbackSrc?: string;
  };

  /**
   * 图片填充模式
   * @default 'cover'
   */
  fillMode?: 'cover' | 'contain' | 'fill' | 'none';

  /**
   * 宽度
   * @default '100%'
   */
  width?: CSSProperties['width'];

  /**
   * 高度
   * @default '100%'
   */
  height?: CSSProperties['height'];

  /**
   * 是否启用懒加载
   * @default false
   */
  lazy?: boolean;

  /**
   * 点击事件（可选）
   */
  onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}
```

### 4.2 Props 详细说明

#### dataSource

- **类型**: `object`
- **必填**: 否
- **默认值**: `{}`
- **说明**: 图片数据源配置对象

##### dataSource.src

- **类型**: `string`
- **必填**: 否
- **说明**: 图片地址，支持相对路径和绝对路径

##### dataSource.alt

- **类型**: `string`
- **必填**: 否
- **说明**: 图片替换文本，当图片未设置或加载失败时，显示 alt 的前 2 个字符

##### dataSource.errorSrc

- **类型**: `string`
- **必填**: 否
- **说明**: 图片加载失败后的兜底图地址

##### dataSource.fallbackSrc

- **类型**: `string`
- **必填**: 否
- **说明**: 首次加载时的占位图地址

#### fillMode

- **类型**: `'cover' | 'contain' | 'fill' | 'none'`
- **必填**: 否
- **默认值**: `'cover'`
- **说明**: 图片填充模式
  - `cover`: 覆盖容器，保持比例，可能裁剪
  - `contain`: 完整显示，保持比例，可能留白
  - `fill`: 拉伸填充，不保持比例
  - `none`: 原始尺寸，不缩放

#### width

- **类型**: `CSSProperties['width']`
- **必填**: 否
- **默认值**: `'100%'`
- **说明**: 组件宽度，支持任意 CSS 合法值（px、%、rem 等）

#### height

- **类型**: `CSSProperties['height']`
- **必填**: 否
- **默认值**: `'100%'`
- **说明**: 组件高度，支持任意 CSS 合法值（px、%、rem 等）

#### lazy

- **类型**: `boolean`
- **必填**: 否
- **默认值**: `false`
- **说明**: 是否启用图片懒加载（仅当图片进入视口时才加载）

#### onClick

- **类型**: `(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void`
- **必填**: 否
- **说明**: 点击事件回调函数

#### 继承的 PisellBasicCardProps 属性

除了上述属性，还继承了 PisellBasicCardProps 的以下属性：

- **active**: 激活状态配置
- **disabled**: 禁用状态配置
- **loading**: 加载状态配置
- **children**: 子元素
- **className**: 自定义类名
- **style**: 自定义样式
- ...其他 HTMLDivElement 属性

---

## 5. 组件模块拆解

### 5.1 组件结构图

```
PisellImageCard (主组件)
├── 状态管理层
│   ├── useImageLoad (图片加载状态管理 Hook)
│   │   ├── loading: 图片加载中
│   │   ├── error: 图片加载失败
│   │   └── loaded: 图片加载完成
│   └── usePisellImageCardStyle (样式合并 Hook)
│       ├── 合并基础样式
│       ├── 合并状态样式（active/disabled/loading）
│       └── 处理填充模式样式
│
├── UI 渲染层
│   ├── ImageDisplay (图片展示组件)
│   │   ├── 占位图展示（fallbackSrc）
│   │   ├── 图片加载（src）
│   │   └── 失败兜底（errorSrc）
│   └── TextFallback (文字降级组件)
│       └── 显示 alt 前 2 个字符
│
└── 工具函数层
    ├── getImageObjectFit (获取 object-fit 值)
    ├── getTruncatedText (截取前 2 个字符)
    └── mergeCardStyles (合并卡片样式)
```

### 5.2 核心模块说明

#### 5.2.1 主组件 (PisellImageCard.tsx)

- **职责**: 组件入口，负责 Props 解析和整体渲染逻辑
- **功能**:
  - 接收和解构 Props
  - 调用 Hooks 处理状态和样式
  - 渲染 UI 结构
  - 处理点击事件

#### 5.2.2 Hooks 层

##### useImageLoad Hook

- **职责**: 管理图片加载状态
- **功能**:
  - 监听图片加载事件
  - 处理加载成功/失败
  - 管理 loading/error/loaded 状态
  - 支持懒加载逻辑

##### usePisellImageCardStyle Hook

- **职责**: 处理样式合并逻辑
- **功能**:
  - 合并基础样式和自定义样式
  - 合并状态样式（active/disabled/loading）
  - 处理填充模式对应的 CSS
  - 生成最终的 style 对象

#### 5.2.3 组件层

##### ImageDisplay 子组件

- **职责**: 处理图片展示逻辑
- **功能**:
  - 展示占位图（loading 时）
  - 展示主图片（loaded 时）
  - 展示失败兜底图（error 时）
  - 应用填充模式样式

##### TextFallback 子组件

- **职责**: 处理无图片时的文字降级展示
- **功能**:
  - 截取 alt 前 2 个字符
  - 应用文字样式
  - 居中展示

#### 5.2.4 工具函数层

##### getImageObjectFit

- **功能**: 根据 fillMode 返回对应的 CSS object-fit 值
- **输入**: `fillMode: 'cover' | 'contain' | 'fill' | 'none'`
- **输出**: `'cover' | 'contain' | 'fill' | 'none'`

##### getTruncatedText

- **功能**: 截取文本前 N 个字符
- **输入**: `text: string, maxLength: number`
- **输出**: `string`

##### mergeCardStyles

- **功能**: 合并卡片的多层样式
- **输入**: 基础样式、状态样式等
- **输出**: 合并后的 CSSProperties 对象

---

## 6. 组件实现方案

### 6.1 技术选型

| 技术       | 用途         | 说明                             |
| ---------- | ------------ | -------------------------------- |
| React 18   | UI 框架      | 使用函数组件 + Hooks             |
| TypeScript | 类型系统     | 严格模式，确保类型安全           |
| Less       | 样式预处理器 | 支持嵌套、变量等特性             |
| classnames | 类名合并     | 条件类名处理                     |
| Ant Design | UI 组件库    | 可选，如需要使用 Skeleton 等组件 |

### 6.2 核心实现逻辑

#### 6.2.1 图片加载状态管理

使用 `useImageLoad` Hook 管理图片加载的三种状态：

```
const useImageLoad = (src?: string, lazy?: boolean) => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    if (!src) {
      setLoading(false);
      return;
    }

    // 如果启用懒加载
    if (lazy && 'IntersectionObserver' in window) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            loadImage();
            observer.disconnect();
          }
        });
      });

      if (imgRef.current) {
        observer.observe(imgRef.current);
      }

      return () => observer.disconnect();
    } else {
      loadImage();
    }
  }, [src, lazy]);

  const loadImage = () => {
    const img = new Image();
    img.src = src!;
    img.onload = () => {
      setLoading(false);
      setError(false);
    };
    img.onerror = () => {
      setLoading(false);
      setError(true);
    };
  };

  return { loading, error, imgRef };
};
```

#### 6.2.2 图片展示优先级

图片展示遵循以下优先级：

1. **loading 状态**: 显示 `fallbackSrc`（占位图）
2. **error 状态**: 显示 `errorSrc`（失败兜底图）
3. **loaded 状态**: 显示 `src`（主图片）
4. **无图片**: 显示 `alt` 前 2 个字符

```
const getDisplaySource = () => {
  if (loading && fallbackSrc) return fallbackSrc;
  if (error && errorSrc) return errorSrc;
  if (src) return src;
  return null;
};
```

#### 6.2.3 文字降级逻辑

当没有图片可展示时，显示 alt 的前 2 个字符：

```
const getTruncatedText = (text?: string): string => {
  if (!text) return '';
  // 使用 substring 截取前 2 个字符
  return text.substring(0, 2);
};
```

#### 6.2.4 填充模式映射

将 fillMode 映射为 CSS object-fit：

```
const getImageObjectFit = (fillMode: string): string => {
  const modeMap = {
    cover: 'cover',
    contain: 'contain',
    fill: 'fill',
    none: 'none',
  };
  return modeMap[fillMode] || 'cover';
};
```

#### 6.2.5 样式合并策略

合并多层样式：

```
const usePisellImageCardStyle = (props) => {
  const { style, active, disabled, loading, width, height, fillMode } = props;

  return useMemo(() => {
    let mergedStyle: CSSProperties = {
      width: width || '100%',
      height: height || '100%',
      ...style,
    };

    // 合并状态样式
    if (loading?.value && loading?.style) {
      mergedStyle = { ...mergedStyle, ...loading.style };
    } else if (disabled?.value && disabled?.style) {
      mergedStyle = { ...mergedStyle, ...disabled.style };
    } else if (active?.value && active?.style) {
      mergedStyle = { ...mergedStyle, ...active.style };
    }

    return mergedStyle;
  }, [style, active, disabled, loading, width, height, fillMode]);
};
```

#### 6.2.6 状态类名处理

使用 classnames 库合并状态类名：

```
const className = classNames(
  'pisell-image-card',
  props.className,
  {
    'pisell-image-card--active': active?.value,
    'pisell-image-card--disabled': disabled?.value,
    'pisell-image-card--loading': loading?.value,
    [`pisell-image-card--${fillMode}`]: fillMode,
  },
  active?.className,
  disabled?.className,
  loading?.className
);
```

### 6.3 组件组成与引用

#### 6.3.1 使用的现有组件/库

- **React**: 核心 UI 库
- **classnames**: 类名合并工具
- 不依赖 Ant Design 组件（纯原生实现）

#### 6.3.2 自定义子组件

组件内部可拆分为以下子组件（可选，根据实现复杂度决定）：

1. **ImageDisplay**: 图片展示逻辑封装
2. **TextFallback**: 文字降级展示

或者直接在主组件内实现，保持简洁。

### 6.4 目录结构

```
PisellImageCard/
├── index.tsx                      # 组件导出
├── PisellImageCard.tsx            # 主组件实现
├── PisellImageCard.less           # 样式文件
├── PisellImageCard.test.tsx       # 单元测试
├── types.ts                       # 类型定义
├── hooks/                         # 自定义 Hooks
│   ├── index.ts                   # Hooks 导出
│   ├── useImageLoad.ts            # 图片加载 Hook
│   └── usePisellImageCardStyle.ts # 样式合并 Hook
├── utils/                         # 工具函数
│   ├── index.ts                   # 工具函数导出
│   ├── getImageObjectFit.ts       # 获取填充模式
│   ├── getTruncatedText.ts        # 文本截取
│   └── mergeCardStyles.ts         # 样式合并
├── components/                    # 子组件（可选）
│   └── index.tsx                  # 子组件导出
└── docs/                          # 文档
    ├── PisellImageCard.md
    ├── PisellImageCard.$tab-design.md
    ├── PisellImageCard.$tab-dev.md
    └── PisellImageCard.$tab-test.md
```

---

## 7. 样式设计方案

### 7.1 Less 变量设计

```
@prefix: pisell-image-card;

// 默认尺寸
@default-width: 100%;
@default-height: 100%;

// 状态颜色
@active-border-color: #1890ff;
@disabled-opacity: 0.6;

// 文字降级样式
@text-fallback-bg: #f0f0f0;
@text-fallback-color: #999;
@text-fallback-font-size: 24px;
```

### 7.2 核心样式结构

```
.pisell-image-card {
  position: relative;
  display: inline-block;
  overflow: hidden;

  &__image {
    width: 100%;
    height: 100%;
    display: block;
  }

  &__text-fallback {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: @text-fallback-bg;
    color: @text-fallback-color;
    font-size: @text-fallback-font-size;
  }

  // 填充模式
  &--cover .pisell-image-card__image {
    object-fit: cover;
  }

  &--contain .pisell-image-card__image {
    object-fit: contain;
  }

  &--fill .pisell-image-card__image {
    object-fit: fill;
  }

  &--none .pisell-image-card__image {
    object-fit: none;
  }

  // 激活状态
  &--active {
    border: 2px solid @active-border-color;
  }

  // 禁用状态
  &--disabled {
    opacity: @disabled-opacity;
    cursor: not-allowed;
  }

  // 加载状态
  &--loading {
    cursor: wait;
  }
}
```

---

## 8. 扩展性设计

### 8.1 支持的扩展点

1. **自定义样式**: 通过 `style` 和 `className` 注入自定义样式
2. **状态扩展**: 通过 `active`、`disabled`、`loading` 的 `extra` 字段扩展额外内容
3. **事件扩展**: 支持所有 HTMLDivElement 的原生事件
4. **子元素插槽**: 通过 `children` 注入自定义内容

### 8.2 业务扩展示例

如果业务需要扩展，可以基于 PisellImageCard 进一步封装：

```
interface ProductImageCardProps extends PisellImageCardProps {
  badge?: ReactNode; // 商品角标
  discount?: string; // 折扣信息
}

const ProductImageCard: React.FC<ProductImageCardProps> = (props) => {
  const { badge, discount, ...imageCardProps } = props;

  return (
    <PisellImageCard {...imageCardProps}>
      {badge && <div className="badge">{badge}</div>}
      {discount && <div className="discount">{discount}</div>}
    </PisellImageCard>
  );
};
```

---

## 9. 性能优化方案

### 9.1 懒加载

通过 IntersectionObserver API 实现图片懒加载：

- 只有当图片进入视口时才开始加载
- 减少初始加载压力
- 提升页面首屏渲染速度

### 9.2 记忆化

使用 `useMemo` 缓存计算结果：

- 样式对象合并结果缓存
- 类名计算结果缓存
- 避免不必要的重新计算

### 9.3 事件处理优化

使用 `useCallback` 缓存事件处理函数：

```
const handleClick = useCallback((e) => {
  if (disabled?.value) return;
  onClick?.(e);
}, [disabled, onClick]);
```

---

## 10. 测试策略

### 10.1 单元测试覆盖点

- Props 默认值测试
- 图片加载成功场景
- 图片加载失败场景
- 文字降级展示测试
- 状态样式合并测试
- 懒加载功能测试
- 点击事件测试

### 10.2 快照测试

- 基础渲染快照
- 各种状态下的快照（active/disabled/loading）
- 各种填充模式的快照

---

## 11. 注意事项

### 11.1 开发注意事项

1. **onClick 属性处理**: 从 PisellBasicCardProps 中 Omit 掉 onClick，重新定义为可选
2. **文字截取**: 使用 `substring(0, 2)` 严格截取前 2 个字符
3. **默认值处理**: width、height 默认为 `100%`，fillMode 默认为 `cover`
4. **懒加载兼容性**: 检测浏览器是否支持 IntersectionObserver，不支持则降级为直接加载
5. **状态优先级**: loading > disabled > active

### 11.2 样式注意事项

1. **object-fit 兼容性**: 确保目标浏览器支持 object-fit 属性
2. **文字降级样式**: 通过 CSS 控制，保持灵活性
3. **响应式**: 支持百分比、rem 等相对单位

### 11.3 可访问性

1. **alt 属性**: 确保图片有 alt 属性，提升可访问性
2. **语义化**: 使用语义化的 HTML 标签和属性
3. **键盘导航**: 如果有 onClick，确保支持键盘操作

---

## 12. 开发检查清单

- [ ] Props 是否基于 PisellBasicCardProps 扩展（Omit onClick）？
- [ ] 是否实现了 active、disabled、loading 状态的样式合并？
- [ ] 是否实现了图片加载状态管理（loading/error/loaded）？
- [ ] 是否实现了占位图和失败兜底图逻辑？
- [ ] 是否实现了文字降级（alt 前 2 个字符）？
- [ ] 是否支持懒加载配置？
- [ ] 是否支持填充模式配置（默认 cover）？
- [ ] width 和 height 默认值是否为 100%？
- [ ] onClick 是否为可选属性？
- [ ] 是否添加了 data-\* 属性标识状态？
- [ ] 组件是否导出到 PisellCards 命名空间？
- [ ] 类型是否在 index.tsx 中导出？
- [ ] 是否编写了单元测试？
- [ ] 是否编写了使用文档？
- [ ] 样式是否使用了 Less 预处理器？
- [ ] 是否添加了必要的注释（JSDoc）？

---

## 13. 后续优化方向

### 13.1 功能增强

- 支持图片预览功能
- 支持图片点击放大
- 支持多图展示（轮播）
- 支持图片滤镜效果

### 13.2 性能优化

- 图片压缩/格式转换（WebP）
- 响应式图片（srcset）
- 渐进式加载
- 缓存策略

### 13.3 交互增强

- 图片加载进度展示
- 加载动画效果
- hover 效果配置
- 手势操作支持（移动端）

---

**文档版本**: v1.0.0  
**最后更新**: 2025-12-11  
**作者**: AI Assistant
