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

# PisellCards 开发文档

## 项目结构

```
packages/materials/src/components/PisellCards/
├── index.tsx                          # 主入口，导出命名空间
├── PisellCards.tsx                    # 命名空间定义（占位组件）
├── PisellCards.less                   # 公共样式
├── PisellCards.test.tsx               # 测试文件
├── types.ts                           # PisellBasicCardProps 类型定义
├── components/                        # 所有卡片组件目录
│   └── TabCard/                       # TabCard 组件
│       ├── index.tsx                  # 导出文件
│       ├── TabCard.tsx                # 组件实现
│       ├── TabCard.less               # 组件样式
│       ├── TabCard.test.tsx           # 单元测试
│       └── types.ts                   # 类型定义
└── docs/                              # 文档目录
    ├── PisellCards.md                 # 使用文档
    ├── PisellCards.$tab-design.md     # 设计文档
    ├── PisellCards.$tab-dev.md        # 开发文档
    └── PisellCards.$tab-test.md       # 测试文档
```

## 核心实现

### 1. PisellBasicCardProps 类型定义

位置：`packages/materials/src/components/PisellCards/types.ts`

```
export interface PisellBasicCardProps
  extends React.HTMLAttributes<HTMLDivElement> {
  active?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: any;
  };
  disabled?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: any;
  };
  loading?: {
    value: boolean;
    style?: CSSProperties;
    className?: string;
    onChange?: (value: boolean) => void;
    extra?: any;
  };
  children?: ReactNode;
}
```

### 2. TabCard 组件实现

#### 类型扩展

```
export interface TabCardProps extends PisellBasicCardProps {
  tabKey: string | number;
  label: ReactNode;
  icon?: ReactNode;
  badge?: BadgeConfig;
  onClick?: (tabKey: string | number, event: React.MouseEvent) => void;
  direction?: 'horizontal' | 'vertical';
  size?: 'small' | 'medium' | 'large';
  render?: (props: TabCardProps) => ReactNode;
}
```

#### 状态合并逻辑

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

  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]);

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

#### 事件处理

```
const handleClick = useCallback(
  (e: React.MouseEvent) => {
    // loading 或 disabled 状态下不触发点击
    if (loading?.value || disabled?.value) {
      e.preventDefault();
      return;
    }

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

    // 触发 onClick
    if (onClick) {
      onClick(tabKey, e);
    }
  },
  [tabKey, onClick, active, loading, disabled]
);
```

#### Extra 渲染

```
const renderExtra = useCallback(() => {
  if (loading?.value && loading?.extra) {
    return <div className="tab-card__extra">{loading.extra}</div>;
  }
  if (disabled?.value && disabled?.extra) {
    return <div className="tab-card__extra">{disabled.extra}</div>;
  }
  if (active?.value && active?.extra) {
    return <div className="tab-card__extra">{active.extra}</div>;
  }
  return null;
}, [active, disabled, loading]);
```

### 3. 命名空间导出

位置：`packages/materials/src/components/PisellCards/index.tsx`

```
import TabCard from './components/TabCard';

// 命名空间导出
const PisellCards = {
  TabCard: TabCard,
};

export default PisellCards;

// 类型导出
export type { PisellBasicCardProps } from './types';
export type { TabCardProps, BadgeConfig } from './components/TabCard/types';
```

## 开发新卡片的步骤

### 1. 创建组件目录

```bash
mkdir -p packages/materials/src/components/PisellCards/components/YourCard
```

### 2. 定义类型

创建 `types.ts`：

```
import { PisellBasicCardProps } from '../../types';

export interface YourCardProps extends PisellBasicCardProps {
  // 添加你的业务字段
  field1: string;
  field2: number;
}
```

### 3. 实现组件

创建 `YourCard.tsx`：

```
import React, { useMemo, useCallback } from 'react';
import classNames from 'classnames';
import { YourCardProps } from './types';
import './YourCard.less';

const YourCard: React.FC<YourCardProps> = (props) => {
  const {
    field1,
    field2,
    active,
    disabled,
    loading,
    className,
    style,
    children,
    ...restProps
  } = props;

  // 状态合并逻辑
  const mergedStyle = useMemo(() => {
    let finalStyle = { ...style };
    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]);

  const mergedClassName = useMemo(() => {
    return classNames(
      'your-card',
      className,
      {
        'your-card--active': active?.value,
        'your-card--disabled': disabled?.value,
        'your-card--loading': loading?.value,
      },
      active?.value && active?.className,
      disabled?.value && disabled?.className,
      loading?.value && loading?.className
    );
  }, [className, active, disabled, loading]);

  return (
    <div
      className={mergedClassName}
      style={mergedStyle}
      data-active={active?.value}
      data-disabled={disabled?.value}
      data-loading={loading?.value}
      {...restProps}
    >
      {/* 你的业务内容 */}
      <div className="your-card__content">
        <div>{field1}</div>
        <div>{field2}</div>
      </div>

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

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

export default YourCard;
```

### 4. 创建导出文件

创建 `index.tsx`：

```
export { default } from './YourCard';
export type { YourCardProps } from './types';
```

### 5. 编写样式

创建 `YourCard.less`：

```less
.your-card {
  // 你的样式
  &--active {
    // 激活状态样式
  }
  &--disabled {
    // 禁用状态样式
  }
  &--loading {
    // 加载状态样式
  }
}
```

### 6. 更新命名空间导出

修改 `packages/materials/src/components/PisellCards/index.tsx`：

```
import YourCard from './components/YourCard';

const PisellCards = {
  TabCard: TabCard,
  YourCard: YourCard, // 添加这里
};

export type { YourCardProps } from './components/YourCard/types'; // 添加类型导出
```

### 7. 编写测试

创建 `YourCard.test.tsx`，参考 `TabCard.test.tsx`。

## 技术要点

### 1. 使用 useMemo 优化性能

所有样式和类名的合并都应该使用 `useMemo` 来避免不必要的重新计算。

### 2. 使用 useCallback 优化事件处理

事件处理函数应该使用 `useCallback` 来避免不必要的重新创建。

### 3. data 属性标识状态

使用 `data-*` 属性标识状态，方便 CSS 选择器和测试。

```
<div
  data-active={active?.value}
  data-disabled={disabled?.value}
  data-loading={loading?.value}
/>
```

### 4. 可访问性

添加合适的 ARIA 属性：

```
<div
  role="tab"
  aria-selected={active?.value}
  aria-disabled={disabled?.value}
  tabIndex={disabled?.value ? -1 : 0}
/>
```

### 5. 类名规范

- 使用 BEM 命名规范
- 组件类名：`your-card`
- 状态修饰符：`your-card--active`、`your-card--disabled`
- 元素类名：`your-card__content`、`your-card__icon`

## 调试技巧

### 1. 使用 React DevTools

查看组件的 props 和 state。

### 2. 使用 console.log

在关键位置打印日志：

```
console.log('mergedStyle:', mergedStyle);
console.log('mergedClassName:', mergedClassName);
```

### 3. 使用断点调试

在浏览器中设置断点调试逻辑。

## 常见问题

### 1. 样式不生效

检查是否正确导入了 `.less` 文件。

### 2. 状态优先级不正确

检查状态合并逻辑的 if-else 顺序。

### 3. 点击事件不触发

检查是否在 disabled 或 loading 状态下阻止了事件。

### 4. 类型错误

确保正确扩展了 `PisellBasicCardProps`。
