# 组件设计文档

## 1. 组件分类

### 1.1 按功能分类

#### 布局组件 (components/Layout/)
- **Layout.jsx**: 主布局容器
- **Header.jsx**: 顶部导航栏
- **Sidebar.jsx**: 侧边栏菜单

#### 通用组件 (components/Common/)
- **Button.jsx**: 按钮组件
- **Input.jsx**: 输入框组件
- **Select.jsx**: 下拉选择框组件
- **DatePicker.jsx**: 日期选择器组件
- **Table.jsx**: 表格组件
- **Modal.jsx**: 弹窗组件
- **Form.jsx**: 表单组件
- **Card.jsx**: 卡片组件
- **Tag.jsx**: 标签组件
- **Badge.jsx**: 徽章组件
- **Tooltip.jsx**: 提示框组件

#### 业务组件 (components/Business/)
- **ProjectCard.jsx**: 项目卡片
- **StatCard.jsx**: 统计卡片
- **UserAvatar.jsx**: 用户头像
- **ProgressBar.jsx**: 进度条
- **StatusTag.jsx**: 状态标签
- **AmountDisplay.jsx**: 金额显示
- **DateRangePicker.jsx**: 日期范围选择器

## 2. 组件设计规范

### 2.1 Props命名规范

#### 2.1.1 布尔类型
- 使用 `is` 或 `has` 前缀
- 示例: `isLoading`, `hasPermission`, `isVisible`, `isRequired`

#### 2.1.2 事件处理
- 使用 `on` 前缀
- 示例: `onClick`, `onChange`, `onSubmit`, `onCancel`

#### 2.1.3 渲染内容
- 使用 `render` 前缀或作为 `children`
- 示例: `renderHeader`, `renderFooter`, `children`

### 2.2 组件Props定义模板

```jsx
import React from 'react';
import PropTypes from 'prop-types';

/**
 * 按钮组件
 * @param {string} type - 按钮类型：primary/default/danger
 * @param {boolean} loading - 是否加载中
 * @param {boolean} disabled - 是否禁用
 * @param {function} onClick - 点击事件
 * @param {node} children - 子元素
 */
const Button = ({
  type = 'default',
  loading = false,
  disabled = false,
  onClick,
  children,
  className,
  ...rest
}) => {
  return (
    <button
      className={`btn btn-${type} ${className}`}
      disabled={disabled || loading}
      onClick={onClick}
      {...rest}
    >
      {loading && <span className="spinner" />}
      {children}
    </button>
  );
};

Button.propTypes = {
  type: PropTypes.oneOf(['primary', 'default', 'danger']),
  loading: PropTypes.bool,
  disabled: PropTypes.bool,
  onClick: PropTypes.func,
  children: PropTypes.node,
  className: PropTypes.string,
};

export default Button;
```

## 3. 核心组件设计

### 3.1 Button（按钮组件）

```jsx
/**
 * 按钮组件
 */
const Button = ({
  type = 'default',
  size = 'medium',
  icon,
  loading = false,
  disabled = false,
  block = false,
  ghost = false,
  danger = false,
  onClick,
  children,
  ...props
}) => {
  const handleClick = (e) => {
    if (!loading && !disabled && onClick) {
      onClick(e);
    }
  };

  return (
    <button
      className={classNames(
        'btn',
        `btn-${type}`,
        `btn-${size}`,
        {
          'btn-loading': loading,
          'btn-block': block,
          'btn-ghost': ghost,
          'btn-danger': danger,
        }
      )}
      disabled={disabled || loading}
      onClick={handleClick}
      {...props}
    >
      {loading && <Spin size="small" className="btn-loading-icon" />}
      {icon && !loading && <span className="btn-icon">{icon}</span>}
      {children}
    </button>
  );
};
```

**Props说明:**
- `type`: 按钮类型 (default/primary/danger)
- `size`: 按钮大小 (small/medium/large)
- `icon`: 图标
- `loading`: 加载状态
- `disabled`: 禁用状态
- `block`: 块级按钮
- `ghost`: 幽灵按钮
- `danger`: 危险按钮

### 3.2 Table（表格组件）

```jsx
/**
 * 表格组件
 */
const Table = ({
  columns,
  dataSource,
  loading = false,
  pagination,
  rowKey = 'id',
  rowSelection,
  onRow,
  scroll,
  ...props
}) => {
  return (
    <AntTable
      columns={columns}
      dataSource={dataSource}
      loading={loading}
      pagination={pagination}
      rowKey={rowKey}
      rowSelection={rowSelection}
      onRow={onRow}
      scroll={scroll}
      {...props}
    />
  );
};
```

**Props说明:**
- `columns`: 列配置
- `dataSource`: 数据源
- `loading`: 加载状态
- `pagination`: 分页配置
- `rowKey`: 行key
- `rowSelection`: 行选择配置
- `scroll`: 滚动配置

### 3.3 Form（表单组件）

```jsx
/**
 * 表单组件
 */
const Form = ({
  form,
  initialValues,
  onFinish,
  onFinishFailed,
  layout = 'vertical',
  children,
  ...props
}) => {
  const [formInstance] = Form.useForm();

  const currentForm = form || formInstance;

  return (
    <AntForm
      form={currentForm}
      initialValues={initialValues}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
      layout={layout}
      {...props}
    >
      {children}
    </AntForm>
  );
};
```

### 3.4 Modal（弹窗组件）

```jsx
/**
 * 弹窗组件
 */
const Modal = ({
  visible,
  title,
  onOk,
  onCancel,
  okText = '确定',
  cancelText = '取消',
  confirmLoading = false,
  width = 520,
  children,
  ...props
}) => {
  return (
    <AntModal
      visible={visible}
      title={title}
      onOk={onOk}
      onCancel={onCancel}
      okText={okText}
      cancelText={cancelText}
      confirmLoading={confirmLoading}
      width={width}
      {...props}
    >
      {children}
    </AntModal>
  );
};
```

### 3.5 Card（卡片组件）

```jsx
/**
 * 卡片组件
 */
const Card = ({
  title,
  extra,
  bordered = true,
  hoverable = false,
  loading = false,
  children,
  className,
  ...props
}) => {
  return (
    <AntCard
      title={title}
      extra={extra}
      bordered={bordered}
      hoverable={hoverable}
      loading={loading}
      className={className}
      {...props}
    >
      {children}
    </AntCard>
  );
};
```

### 3.6 Tag（标签组件）

```jsx
/**
 * 标签组件
 */
const Tag = ({ type = 'default', color, children, ...props }) => {
  const colorMap = {
    new: 'blue',
    inProgress: 'green',
    completed: 'default',
    paused: 'orange',
    cancelled: 'red',
    admin: 'red',
    market: 'blue',
    other: 'cyan',
  };

  return (
    <AntTag color={color || colorMap[type]} {...props}>
      {children}
    </AntTag>
  );
};
```

## 4. 业务组件设计

### 4.1 ProjectCard（项目卡片）

```jsx
/**
 * 项目卡片组件
 */
const ProjectCard = ({
  project,
  onView,
  onEdit,
  onDelete,
  showActions = true,
}) => {
  const { hasRole } = usePermission();

  return (
    <Card
      hoverable
      title={
        <div className="project-card-header">
          <span>{project.name}</span>
          <Tag type={project.engineering_type}>
            {project.engineering_type}
          </Tag>
        </div>
      }
      extra={
        <span className="project-card-no">{project.project_no}</span>
      }
      className="project-card"
    >
      <div className="project-card-content">
        <div className="project-card-item">
          <span className="label">合同金额：</span>
          <AmountDisplay value={project.contract_amount} />
        </div>
        <div className="project-card-item">
          <span className="label">项目负责人：</span>
          <span>{project.project_leader}</span>
        </div>
        <div className="project-card-item">
          <span className="label">签订日期：</span>
          <span>{project.signing_date}</span>
        </div>
        <ProgressBar
          percent={project.cumulative_progress}
          status={project.cumulative_progress === 100 ? 'success' : 'active'}
        />
      </div>

      {showActions && (
        <div className="project-card-actions">
          <Button type="link" onClick={() => onView(project.id)}>
            查看
          </Button>
          {hasRole(['admin']) || project.created_by === getCurrentUserId() ? (
            <>
              <Button type="link" onClick={() => onEdit(project.id)}>
                编辑
              </Button>
              <Button
                type="link"
                danger
                onClick={() => onDelete(project.id)}
              >
                删除
              </Button>
            </>
          ) : null}
        </div>
      )}
    </Card>
  );
};
```

### 4.2 StatCard（统计卡片）

```jsx
/**
 * 统计卡片组件
 */
const StatCard = ({ title, value, icon, color, prefix, suffix }) => {
  return (
    <Card className="stat-card">
      <div className="stat-card-content">
        <div className="stat-card-left">
          <div className="stat-card-title">{title}</div>
          <div className="stat-card-value">
            {prefix && <span className="prefix">{prefix}</span>}
            {typeof value === 'number' ? value.toLocaleString() : value}
            {suffix && <span className="suffix">{suffix}</span>}
          </div>
        </div>
        {icon && (
          <div className="stat-card-right">
            <div className={`stat-card-icon stat-card-icon-${color}`}>
              {icon}
            </div>
          </div>
        )}
      </div>
    </Card>
  );
};
```

**Props说明:**
- `title`: 标题
- `value`: 数值
- `icon`: 图标
- `color`: 颜色 (blue/green/orange/red/cyan)
- `prefix`: 前缀（如¥）
- `suffix`: 后缀（如%）

### 4.3 StatusTag（状态标签）

```jsx
/**
 * 状态标签组件
 */
const StatusTag = ({ status, type = 'project' }) => {
  const statusMap = {
    project: {
      new: { text: '新建', color: 'blue' },
      inProgress: { text: '进行中', color: 'green' },
      completed: { text: '已完成', color: 'default' },
      paused: { text: '已暂停', color: 'orange' },
      cancelled: { text: '已取消', color: 'red' },
    },
    user: {
      active: { text: '正常', color: 'green' },
      inactive: { text: '禁用', color: 'default' },
    },
  };

  const statusInfo = statusMap[type]?.[status] || { text: status, color: 'default' };

  return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
};
```

### 4.4 AmountDisplay（金额显示）

```jsx
/**
 * 金额显示组件
 */
const AmountDisplay = ({ value, unit = '万元', precision = 2 }) => {
  const formatAmount = (val) => {
    if (val === null || val === undefined) return '-';
    return val.toFixed(precision);
  };

  return (
    <span className="amount-display">
      {formatAmount(value)} <span className="unit">{unit}</span>
    </span>
  );
};
```

### 4.5 ProgressBar（进度条）

```jsx
/**
 * 进度条组件
 */
const ProgressBar = ({ percent, status = 'active', showText = true }) => {
  return (
    <div className="progress-bar-wrapper">
      <Progress
        percent={Math.round(percent)}
        status={status}
        showInfo={showText}
        strokeColor={{
          '0%': '#108ee9',
          '100%': '#87d068',
        }}
      />
    </div>
  );
};
```

### 4.6 DateRangePicker（日期范围选择器）

```jsx
/**
 * 日期范围选择器组件
 */
const DateRangePicker = ({ value, onChange, placeholder }) => {
  const { RangePicker } = DatePicker;

  return (
    <RangePicker
      value={value}
      onChange={onChange}
      placeholder={placeholder || ['开始日期', '结束日期']}
      style={{ width: '100%' }}
    />
  );
};
```

## 5. 高阶组件

### 5.1 withAuth（认证高阶组件）

```jsx
/**
 * 认证高阶组件
 */
const withAuth = (WrappedComponent, requiredRoles = []) => {
  return (props) => {
    const { user, isAuthenticated } = useAuth();

    if (!isAuthenticated) {
      return <Navigate to="/login" replace />;
    }

    if (requiredRoles.length > 0 && !requiredRoles.includes(user?.role)) {
      return <Navigate to="/403" replace />;
    }

    return <WrappedComponent {...props} />;
  };
};
```

### 5.2 withLoading（加载状态高阶组件）

```jsx
/**
 * 加载状态高阶组件
 */
const withLoading = (WrappedComponent) => {
  return ({ loading, ...props }) => {
    if (loading) {
      return <Spin />;
    }

    return <WrappedComponent {...props} />;
  };
};
```

## 6. 组件复用策略

### 6.1 通过Props自定义
- 提供灵活的Props接口
- 支持自定义样式和类名
- 支持自定义渲染内容

### 6.2 通过插槽模式
- 使用 `children` 传递内容
- 使用 `render` 属性传递渲染函数

### 6.3 通过组合模式
- 将复杂组件拆分为多个小组件
- 通过组合实现复杂功能

## 7. 组件性能优化

### 7.1 React.memo
```jsx
const MemoComponent = React.memo(({ data }) => {
  return <div>{data}</div>;
});
```

### 7.2 useMemo
```jsx
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);
```

### 7.3 useCallback
```jsx
const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
```

### 7.4 虚拟滚动
对于大数据量的列表，使用虚拟滚动：
```jsx
import { List } from 'react-virtualized';

<List
  width={600}
  height={400}
  rowCount={data.length}
  rowHeight={50}
  rowRenderer={({ index, key, style }) => (
    <div key={key} style={style}>
      {data[index]}
    </div>
  )}
/>
```

## 8. 组件测试

### 8.1 单元测试示例
```jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button Component', () => {
  test('renders button with children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    fireEvent.click(screen.getByText('Click'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('shows loading state', () => {
    render(<Button loading>Click</Button>);
    expect(screen.getByText('Click')).toBeDisabled();
  });
});
```

## 9. 组件文档

### 9.1 Storybook（可选）
使用Storybook进行组件文档和展示：

```jsx
// stories/Button.stories.jsx
import Button from '../components/Common/Button';

export default {
  title: 'Components/Button',
  component: Button,
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  type: 'primary',
  children: 'Primary Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  type: 'default',
  children: 'Default Button',
};
```

## 10. 组件最佳实践

### 10.1 避免过度抽象
- 不要为了复用而过度抽象
- 保持组件的简单和直观

### 10.2 合理拆分组件
- 单一职责原则
- 组件不应过大（建议不超过300行）

### 10.3 Props类型检查
- 使用PropTypes进行类型检查
- 提供默认值

### 10.4 错误边界
```jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong.</div>;
    }

    return this.props.children;
  }
}
```

---

**文档维护**: 前端程序员
**文档类型**: 组件设计文档
**最后更新**: 2026-01-25
