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

736 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 组件设计文档
## 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