736 lines
15 KiB
Markdown
736 lines
15 KiB
Markdown
# 组件设计文档
|
||
|
||
## 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
|