Files
ocean_project_manager/frontend/docs/component-design.md
T
2026-01-25 15:05:03 +08:00

15 KiB
Raw Blame History

组件设计文档

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 布尔类型

  • 使用 ishas 前缀
  • 示例: isLoading, hasPermission, isVisible, isRequired

2.1.2 事件处理

  • 使用 on 前缀
  • 示例: onClick, onChange, onSubmit, onCancel

2.1.3 渲染内容

  • 使用 render 前缀或作为 children
  • 示例: renderHeader, renderFooter, children

2.2 组件Props定义模板

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(按钮组件)

/**
 * 按钮组件
 */
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(表格组件)

/**
 * 表格组件
 */
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(表单组件)

/**
 * 表单组件
 */
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(弹窗组件)

/**
 * 弹窗组件
 */
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(卡片组件)

/**
 * 卡片组件
 */
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(标签组件)

/**
 * 标签组件
 */
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(项目卡片)

/**
 * 项目卡片组件
 */
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(统计卡片)

/**
 * 统计卡片组件
 */
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(状态标签)

/**
 * 状态标签组件
 */
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(金额显示)

/**
 * 金额显示组件
 */
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(进度条)

/**
 * 进度条组件
 */
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(日期范围选择器)

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

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

5. 高阶组件

5.1 withAuth(认证高阶组件)

/**
 * 认证高阶组件
 */
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(加载状态高阶组件)

/**
 * 加载状态高阶组件
 */
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

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

7.2 useMemo

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

7.3 useCallback

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

7.4 虚拟滚动

对于大数据量的列表,使用虚拟滚动:

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 单元测试示例

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进行组件文档和展示:

// 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 错误边界

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