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

# PisellGridPro 开发文档

## 项目结构

```
packages/materials/src/components/pisellGridPro/
├── index.tsx                          # 主入口，导出复合组件
├── PisellGridPro.tsx                  # 主容器组件
├── index.less                         # 主容器样式
├── Header/                            # 页头组件
│   ├── index.tsx                      # 组件实现
│   └── index.less                     # 组件样式
├── ToolBar/                           # 工具栏组件
│   ├── index.tsx
│   └── index.less
├── DataCard/                          # 数据卡片组件
│   ├── index.tsx                      # 主组件
│   ├── index.less                     # 主样式
│   ├── types.ts                       # 类型定义
│   └── components/                    # 子组件
│       └── StatCard/                  # 统计卡片
│           ├── index.tsx
│           └── index.less
├── View/                              # 内容视图组件
│   ├── index.tsx
│   └── index.less
├── Footer/                            # 页脚组件
│   ├── index.tsx
│   └── index.less
└── docs/                              # 文档目录
    ├── PisellGridPro.md
    ├── PisellGridPro.$tab-design.md
    ├── PisellGridPro.$tab-dev.md
    └── PisellGridPro.$tab-test.md
```

## 核心实现

### 1. 主容器组件 (PisellGridPro)

位置：`packages/materials/src/components/pisellGridPro/PisellGridPro.tsx`

```
import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface PisellGridProps {
  className?: string;
  children?: React.ReactNode;
  style?: React.CSSProperties;
}

const PisellGridPro = (props: PisellGridProps) => {
  const { className, style, children } = props;
  const containerClassName = classNames('pisell-grid-pro', className);

  return (
    <div className={containerClassName} style={style}>
      {children}
    </div>
  );
};

export default PisellGridPro;
```

**实现要点**：

- 简单的容器组件，主要提供布局能力
- 使用 classNames 合并类名
- 支持自定义 className 和 style

**样式实现**：

```less
.pisell-grid-pro {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100vh;
  background: #ffffff;
  overflow: hidden;
  padding: 0 24px;

  > * {
    flex-shrink: 0; // 子组件默认不收缩
  }
}
```

### 2. Header 组件实现

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

```
import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface HeaderProps {
  title?: React.ReactNode;
  badge?: React.ReactNode;
  subTitle?: React.ReactNode;
  actionsList?: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
}

const Header: React.FC<HeaderProps> = (props) => {
  const { title, badge, subTitle, actionsList, className, style } = props;

  /** 组件类名 */
  const headerClassName = classNames('pisell-grid-pro-header', className);

  return (
    <div className={headerClassName} style={style}>
      {/* 左侧内容区域 */}
      <div className="pisell-grid-pro-header-left">
        {/* 标题区域 */}
        <div className="pisell-grid-pro-header-title-wrapper">
          <div className="pisell-grid-pro-header-title">{title || ''}</div>
          {/* 标题旁边的标签 */}
          {badge && <div className="pisell-grid-pro-header-badge">{badge}</div>}
        </div>

        {/* 副标题区域 */}
        {subTitle && (
          <div className="pisell-grid-pro-header-subtitle">{subTitle}</div>
        )}
      </div>

      {/* 右侧操作按钮区域 */}
      {actionsList && (
        <div className="pisell-grid-pro-header-actions">{actionsList}</div>
      )}
    </div>
  );
};

export default Header;
```

**实现要点**：

- **布局结构**: 左右分布，左侧标题信息，右侧操作按钮
- **层次结构**: 标题 + 徽章在同一行，副标题在下一行
- **条件渲染**: badge 和 subTitle 只在有值时渲染
- **灵活内容**: 所有内容都是 ReactNode，支持自定义

**样式实现**：

```less
.pisell-grid-pro-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 0;
  border-bottom: 1px solid #f0f0f0;

  &-left {
    flex: 1;
  }

  &-title-wrapper {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  &-title {
    font-size: 20px;
    font-weight: 600;
    color: #000;
  }

  &-badge {
    display: flex;
    align-items: center;
  }

  &-subtitle {
    margin-top: 4px;
    font-size: 14px;
    color: #666;
  }

  &-actions {
    display: flex;
    gap: 8px;
  }
}
```

### 3. ToolBar 组件实现

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

```
import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface ToolBarProps {
  className?: string;
  style?: React.CSSProperties;
  tabs?: React.ReactNode;
  topLeft?: React.ReactNode;
  topRight?: React.ReactNode;
  bottomLeft?: React.ReactNode;
  bottomRight?: React.ReactNode;
}

const Toolbar: React.FC<ToolBarProps> = (props) => {
  const { className, style, tabs, topLeft, topRight, bottomLeft, bottomRight } =
    props;

  const toolbarClassName = classNames('pisell-grid-pro-toolbar', className);

  return (
    <div className={toolbarClassName} style={style}>
      {/* 标签页行 */}
      {tabs && <div className="pisell-grid-pro-toolbar-row">{tabs}</div>}

      {/* 顶部行 */}
      {(topLeft || topRight) && (
        <div className="pisell-grid-pro-toolbar-row toolbar-row">
          <div className="toolbar-row-left">{topLeft}</div>
          <div className="toolbar-row-right">{topRight}</div>
        </div>
      )}

      {/* 底部行 */}
      {(bottomLeft || bottomRight) && (
        <div className="pisell-grid-pro-toolbar-row toolbar-row">
          <div className="toolbar-row-left">{bottomLeft}</div>
          <div className="toolbar-row-right">{bottomRight}</div>
        </div>
      )}
    </div>
  );
};

export default Toolbar;
```

**实现要点**：

- **多行布局**: 支持标签页行 + 上下两行共 3 行
- **条件渲染**: 每行只在有内容时才渲染
- **左右对齐**: 每行都是 flex 布局，左右对齐
- **语义化类名**: 使用 toolbar-row 表示工具栏行

**样式实现**：

```less
.pisell-grid-pro-toolbar {
  padding: 12px 0;

  &-row {
    &:not(:last-child) {
      margin-bottom: 12px;
    }
  }

  .toolbar-row {
    display: flex;
    align-items: center;
    justify-content: space-between;

    &-left,
    &-right {
      display: flex;
      align-items: center;
      gap: 8px;
    }
  }
}
```

### 4. DataCard 组件实现

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

```
import React from 'react';
import classNames from 'classnames';
import StatCard from './components/StatCard';
import { DataCardProps } from './types';
import './index.less';

const DataCard = (props: DataCardProps) => {
  const { dataItems = [], className, style } = props;

  /** 容器类名 */
  const dataCardClassName = classNames('pisell-grid-pro-datacard', className);

  /** 如果没有数据，不渲染 */
  if (dataItems.length === 0) {
    return null;
  }

  return (
    <div className={dataCardClassName} style={style}>
      {dataItems.map((item) => {
        const { key, ...rest } = item;
        return <StatCard key={key} {...rest} />;
      })}
    </div>
  );
};

export default DataCard;
```

**实现要点**：

- **数据驱动**: 通过 dataItems 数组渲染多个 StatCard
- **空状态处理**: dataItems 为空时返回 null
- **Key 处理**: 使用 item.key 作为 React key
- **Props 透传**: 将 StatCardItem 的其他属性透传给 StatCard

**样式实现**：

```less
.pisell-grid-pro-datacard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  padding: 16px 0;
}
```

**设计要点**：

- **Grid 布局**: 使用 CSS Grid 自动适配列数
- **响应式**: auto-fit + minmax 实现响应式布局
- **最小宽度**: 每个卡片最小 240px，确保内容可读

### 5. StatCard 子组件实现

位置：`packages/materials/src/components/pisellGridPro/DataCard/components/StatCard/index.tsx`

```
import React from 'react';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { StatCardItem } from '../../types';
import './index.less';

const StatCard: React.FC<StatCardItem> = (props) => {
  const {
    title,
    value,
    compareValue,
    trend,
    comparePrefix = 'v.s. pre',
    showCompare = true,
    style,
    className,
    onClick,
  } = props;

  /** 卡片类名 */
  const cardClassName = classNames(
    'stat-card',
    {
      'stat-card-clickable': !!onClick,  // 可点击状态
    },
    className
  );

  /** 对比信息类名 */
  const compareClassName = classNames('stat-card-compare', {
    'trend-up': trend === 'up',
    'trend-down': trend === 'down',
  });

  /**
   * 渲染趋势图标
   */
  const renderTrendIcon = () => {
    if (!trend) return null;

    if (trend === 'up') {
      return <ArrowUpOutlined className="trend-icon" />;
    }

    if (trend === 'down') {
      return <ArrowDownOutlined className="trend-icon" />;
    }

    return null;
  };

  return (
    <div className={cardClassName} style={style} onClick={onClick}>
      {/* 标题 */}
      {title && <div className="stat-card-title">{title}</div>}

      {/* 内容区域 */}
      <div className="stat-card-content">
        {/* 主数值 */}
        <div className="stat-card-value">{value}</div>

        {/* 对比信息 */}
        {showCompare && compareValue && (
          <div className={compareClassName}>
            <span className="compare-prefix">{comparePrefix}</span>
            <span className="compare-value">{compareValue}</span>
            {renderTrendIcon()}
          </div>
        )}
      </div>
    </div>
  );
};

export default StatCard;
```

**实现要点**：

- **可点击状态**: 根据 onClick 是否存在添加 clickable 类名
- **趋势显示**: 根据 trend 属性显示不同颜色和图标
- **条件渲染**: 只在 showCompare 为 true 且有 compareValue 时显示对比信息
- **默认值**: comparePrefix 默认为 'v.s. pre'，showCompare 默认为 true

**样式实现**：

```less
.stat-card {
  padding: 20px;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #f0f0f0;
  transition: all 0.3s;

  &-clickable {
    cursor: pointer;

    &:hover {
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      transform: translateY(-2px);
    }
  }

  &-title {
    font-size: 14px;
    color: #666;
    margin-bottom: 12px;
  }

  &-content {
    display: flex;
    align-items: baseline;
    gap: 12px;
  }

  &-value {
    font-size: 28px;
    font-weight: 600;
    color: #000;
  }

  &-compare {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 14px;

    .compare-prefix {
      color: #999;
    }

    &.trend-up {
      color: #52c41a; // 绿色

      .trend-icon {
        color: #52c41a;
      }
    }

    &.trend-down {
      color: #ff4d4f; // 红色

      .trend-icon {
        color: #ff4d4f;
      }
    }
  }
}
```

### 6. View 组件实现

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

```
import { PisellScrollView } from '../../PisellScrollView';
import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface ViewProps {
  className?: string;
  children?: React.ReactNode;
  style?: React.CSSProperties;
}

const View = (props: ViewProps) => {
  const { className, style, children } = props;

  const viewClassName = classNames('pisell-grid-pro-view', className);

  return (
    <div className={viewClassName} style={style}>
      <PisellScrollView height="100%" overflow="y">
        {children}
      </PisellScrollView>
    </div>
  );
};

export default View;
```

**实现要点**：

- **内置滚动**: 使用 PisellScrollView 提供滚动能力
- **100% 高度**: PisellScrollView 设置为 100% 高度，占满 View 容器
- **垂直滚动**: overflow="y" 只允许垂直滚动

**样式实现**：

```less
.pisell-grid-pro-view {
  flex: 1; // 占据剩余空间
  overflow: hidden; // 隐藏溢出
  min-height: 0; // 解决 flex 子元素高度问题
}
```

**关键点**：

- `flex: 1` 使 View 自动占据剩余空间
- `overflow: hidden` 防止内容溢出影响整体布局
- `min-height: 0` 解决 Flex 布局中子元素高度计算问题

### 7. Footer 组件实现

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

```
import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface FooterProps {
  className?: string;
  children?: React.ReactNode;
  style?: React.CSSProperties;
}

const Footer = (props: FooterProps) => {
  const { className, style, children } = props;

  /** 容器类名 */
  const footerClassName = classNames('pisell-grid-pro-footer', className);

  return (
    <div className={footerClassName} style={style}>
      {children}
    </div>
  );
};

export default Footer;
```

**实现要点**：

- 简单的容器组件，提供一致的样式和间距
- 完全由业务控制内部内容

**样式实现**：

```less
.pisell-grid-pro-footer {
  padding: 16px 0;
  border-top: 1px solid #f0f0f0;
}
```

### 8. 复合组件导出

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

```
import React from 'react';
import Header, { HeaderProps } from './Header';
import ToolBar, { ToolBarProps } from './ToolBar';
import DataCard, { DataCardProps } from './DataCard/index';
import View, { ViewProps } from './View';
import Footer, { FooterProps } from './Footer';
import PisellGridProComponent, { PisellGridProps } from './PisellGridPro';

export type {
  HeaderProps,
  ToolBarProps,
  DataCardProps,
  ViewProps,
  FooterProps,
  PisellGridProps,
};

/**
 * PisellGridPro 复合组件类型定义
 */
interface PisellGridProType extends React.FC<PisellGridProps> {
  Header: typeof Header;
  ToolBar: typeof ToolBar;
  DataCard: typeof DataCard;
  View: typeof View;
  Footer: typeof Footer;
}

const PisellGridPro = PisellGridProComponent as PisellGridProType;

PisellGridPro.Header = Header;
PisellGridPro.ToolBar = ToolBar;
PisellGridPro.DataCard = DataCard;
PisellGridPro.View = View;
PisellGridPro.Footer = Footer;

export default PisellGridPro;
```

**实现要点**：

- **类型定义**: 定义复合组件的类型，包含所有子组件
- **类型断言**: 使用 as 将主组件断言为复合组件类型
- **挂载子组件**: 将所有子组件挂载到主组件上
- **类型导出**: 导出所有子组件的 Props 类型

## 开发新子组件的步骤

### 1. 创建组件目录

```bash
mkdir -p packages/materials/src/components/pisellGridPro/NewComponent
```

### 2. 创建组件文件

```
// packages/materials/src/components/pisellGridPro/NewComponent/index.tsx

import React from 'react';
import classNames from 'classnames';
import './index.less';

export interface NewComponentProps {
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
  // 自定义属性
}

const NewComponent: React.FC<NewComponentProps> = (props) => {
  const { className, style, children } = props;

  const componentClassName = classNames('pisell-grid-pro-new-component', className);

  return (
    <div className={componentClassName} style={style}>
      {children}
    </div>
  );
};

export default NewComponent;
```

### 3. 创建样式文件

```less
// packages/materials/src/components/pisellGridPro/NewComponent/index.less

.pisell-grid-pro-new-component {
  // 样式
}
```

### 4. 更新主入口

```
// packages/materials/src/components/pisellGridPro/index.tsx

import NewComponent, { NewComponentProps } from './NewComponent';

export type {
  // ... 其他类型
  NewComponentProps,
};

interface PisellGridProType extends React.FC<PisellGridProps> {
  // ... 其他子组件
  NewComponent: typeof NewComponent;
}

const PisellGridPro = PisellGridProComponent as PisellGridProType;

// ... 其他挂载
PisellGridPro.NewComponent = NewComponent;

export default PisellGridPro;
```

### 5. 使用新组件

```
<PisellGridPro>
  <PisellGridPro.NewComponent>
    新组件内容
  </PisellGridPro.NewComponent>
</PisellGridPro>
```

## 技术要点

### 1. Flex 布局的高度问题

**问题**：Flex 子元素的高度计算可能不符合预期

**解决**：

```css
.pisell-grid-pro-view {
  flex: 1;
  overflow: hidden;
  min-height: 0; /* 关键：解决 Flex 子元素高度问题 */
}
```

**原理**：

- Flex 子元素默认 `min-height: auto`
- 当内容超出时，会撑开容器
- 设置 `min-height: 0` 允许子元素缩小到 0

### 2. Grid 响应式布局

**实现**：

```css
.pisell-grid-pro-datacard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}
```

**原理**：

- `auto-fit`: 自动填充可用空间
- `minmax(240px, 1fr)`: 最小 240px，最大 1fr（等分）
- `gap: 16px`: 卡片之间的间距

**效果**：

- 大屏幕：4-5 个卡片
- 中屏幕：2-3 个卡片
- 小屏幕：1 个卡片

### 3. 条件渲染优化

**推荐**：

```
// Good: 只在有内容时渲染容器
{(topLeft || topRight) && (
  <div className="toolbar-row">
    <div className="toolbar-row-left">{topLeft}</div>
    <div className="toolbar-row-right">{topRight}</div>
  </div>
)}

// Bad: 总是渲染容器，即使没有内容
<div className="toolbar-row">
  <div className="toolbar-row-left">{topLeft}</div>
  <div className="toolbar-row-right">{topRight}</div>
</div>
```

**好处**：

- 减少 DOM 节点
- 避免空容器占用空间
- 更好的性能

### 4. classNames 使用

```
const cardClassName = classNames(
  'stat-card',                      // 基础类名
  {
    'stat-card-clickable': !!onClick,  // 条件类名
  },
  className                         // 外部传入类名
);
```

**要点**：

- 基础类名始终存在
- 条件类名根据条件动态添加
- 外部类名最后合并，可以覆盖内部样式

### 5. TypeScript 类型设计

#### 复合组件类型

```
interface PisellGridProType extends React.FC<PisellGridProps> {
  Header: typeof Header;
  ToolBar: typeof ToolBar;
  DataCard: typeof DataCard;
  View: typeof View;
  Footer: typeof Footer;
}
```

**要点**：

- 继承 `React.FC` 保留主组件的类型
- 添加子组件的类型定义
- 提供完整的类型提示

#### Props 类型设计

```
export interface HeaderProps {
  title?: React.ReactNode;        // 使用 ReactNode 而非 string
  badge?: React.ReactNode;
  subTitle?: React.ReactNode;
  actionsList?: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
}
```

**要点**：

- 使用 `ReactNode` 提供最大灵活性
- 所有自定义 props 都是可选的
- 始终支持 className 和 style

## 调试技巧

### 1. 布局调试

```
// 临时添加边框查看布局
<PisellGridPro style={{ border: '2px solid red' }}>
  <PisellGridPro.Header style={{ border: '2px solid blue' }} title="标题" />
  <PisellGridPro.View style={{ border: '2px solid green' }}>
    内容
  </PisellGridPro.View>
</PisellGridPro>
```

### 2. 高度调试

```
// 检查各组件的高度
useEffect(() => {
  console.log('Header height:', document.querySelector('.pisell-grid-pro-header')?.clientHeight);
  console.log('View height:', document.querySelector('.pisell-grid-pro-view')?.clientHeight);
}, []);
```

### 3. Flex 布局调试

使用 Chrome DevTools 的 Flex 布局调试工具：

1. 选中 `.pisell-grid-pro` 元素
2. 查看 Flex 布局信息
3. 检查子元素的 flex 属性

### 4. Grid 布局调试

使用 Chrome DevTools 的 Grid 布局调试工具：

1. 选中 `.pisell-grid-pro-datacard` 元素
2. 启用 Grid 覆盖层
3. 查看网格线和单元格

## 常见问题

### 1. View 内容超出怎么办？

**问题**：View 内容超出容器高度

**原因**：可能是 View 没有设置 flex: 1 或 overflow: hidden

**解决**：

```css
.pisell-grid-pro-view {
  flex: 1;
  overflow: hidden;
  min-height: 0;
}
```

### 2. DataCard 卡片不换行怎么办？

**问题**：所有卡片挤在一行

**原因**：grid-template-columns 设置有问题

**解决**：

```css
.pisell-grid-pro-datacard {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
```

### 3. 如何让某个子组件占据更多空间？

**问题**：想让 Header 占据更多空间

**解决**：

```css
.pisell-grid-pro-header {
  flex-shrink: 0; /* 不收缩 */
  flex-grow: 0; /* 不放大 */
  /* 或者设置固定高度 */
  height: 80px;
}
```

### 4. 如何隐藏某个子组件？

**问题**：想在某些情况下隐藏 DataCard

**解决**：

```
// 方式 1：条件渲染
{showDataCard && (
  <PisellGridPro.DataCard dataItems={items} />
)}

// 方式 2：传入空数组（DataCard 会自动返回 null）
<PisellGridPro.DataCard dataItems={[]} />
```

### 5. 如何自定义子组件的顺序？

**问题**：想把 Footer 放在 Header 后面

**解决**：

```
<PisellGridPro>
  <PisellGridPro.Header title="标题" />
  <PisellGridPro.Footer>页脚</PisellGridPro.Footer>
  <PisellGridPro.View>内容</PisellGridPro.View>
</PisellGridPro>
```

**注意**：虽然可以自定义顺序，但建议遵循标准顺序以保持一致性。

## 最佳实践

### 1. 保持组件职责单一

```
// Good: Header 只负责展示标题和操作
<PisellGridPro.Header
  title="用户列表"
  actionsList={<Button>创建用户</Button>}
/>

// Bad: Header 包含过多业务逻辑
<PisellGridPro.Header
  title="用户列表"
  actionsList={
    <ComplexBusinessLogic />  // 业务逻辑应该独立组件
  }
/>
```

### 2. 合理使用 ToolBar 的插槽

```
// Good: 清晰的功能区域划分
<PisellGridPro.ToolBar
  tabs={<Tabs items={tabItems} />}        // 标签页
  topLeft={<SearchBar />}                 // 搜索
  topRight={<FilterButton />}             // 筛选
  bottomLeft={<BatchActions />}           // 批量操作
  bottomRight={<ViewSwitch />}            // 视图切换
/>

// Bad: 功能混乱
<PisellGridPro.ToolBar
  topLeft={
    <>
      <SearchBar />
      <FilterButton />
      <BatchActions />
      <ViewSwitch />
    </>
  }
/>
```

### 3. DataCard 数据设计

```
// Good: 清晰的数据结构
const statsData = [
  {
    key: 'revenue',
    title: '总收入',
    value: '$45,231',
    compareValue: '+20%',
    trend: 'up' as const,
  },
];

// Bad: 不明确的数据
const statsData = [
  {
    key: '1',  // 不语义化
    title: '收入',  // 不具体
    value: 45231,  // 没有格式化
    compareValue: 0.2,  // 没有格式化
  },
];
```

### 4. 样式定制

```
// Good: 使用 CSS 变量
<PisellGridPro
  style={{
    '--header-padding': '24px 0',
    '--view-background': '#f5f5f5',
  } as React.CSSProperties}
>

// Good: 使用 className
<PisellGridPro className="custom-grid-pro">
  <PisellGridPro.Header className="custom-header" />
</PisellGridPro>

// Bad: 内联样式过多
<PisellGridPro
  style={{
    background: '#fff',
    padding: '0 32px',
    height: '100vh',
    display: 'flex',
    flexDirection: 'column',
    // ... 太多样式
  }}
>
```

### 5. 性能优化

```
// Good: 使用 React.memo 优化不变的组件
const Header = React.memo<HeaderProps>((props) => {
  // ...
});

// Good: 合理使用 useMemo
const statsData = useMemo(() => {
  return computeStats(rawData);
}, [rawData]);

<PisellGridPro.DataCard dataItems={statsData} />
```
