save code
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,735 @@
|
||||
# 组件设计文档
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,882 @@
|
||||
# 前端开发指南
|
||||
|
||||
## 1. 开发环境搭建
|
||||
|
||||
### 1.1 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装Node.js
|
||||
# 推荐版本:18.x 或更高
|
||||
|
||||
# 进入frontend目录
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
```
|
||||
|
||||
### 1.2 环境变量配置
|
||||
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑.env文件
|
||||
VITE_API_BASE_URL=http://localhost:5000/api/v1
|
||||
VITE_APP_TITLE=海洋项目管理系统
|
||||
```
|
||||
|
||||
### 1.3 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 访问 http://localhost:3000
|
||||
```
|
||||
|
||||
### 1.4 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
npm run dev
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 预览构建
|
||||
npm run preview
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 代码格式化
|
||||
npm run format
|
||||
|
||||
# 运行测试
|
||||
npm run test
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 2. 代码规范
|
||||
|
||||
### 2.1 ESLint配置
|
||||
|
||||
```javascript
|
||||
// .eslintrc.js
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react', 'react-hooks'],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 Prettier配置
|
||||
|
||||
```javascript
|
||||
// .prettierrc
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Git提交规范
|
||||
|
||||
```
|
||||
格式:[类型] 简短描述
|
||||
|
||||
类型:
|
||||
- feat: 新功能
|
||||
- fix: 修复bug
|
||||
- style: 代码格式调整
|
||||
- refactor: 重构
|
||||
- test: 测试
|
||||
- docs: 文档
|
||||
- chore: 构建/工具链
|
||||
|
||||
示例:
|
||||
[frontend] feat: 添加项目列表页面
|
||||
[frontend] fix: 修复登录后Token未保存的问题
|
||||
[frontend] style: 统一代码格式
|
||||
[frontend] refactor: 重构API服务模块
|
||||
[frontend] test: 添加组件单元测试
|
||||
[frontend] docs: 更新开发文档
|
||||
[frontend] chore: 更新依赖版本
|
||||
```
|
||||
|
||||
## 3. 目录结构说明
|
||||
|
||||
### 3.1 src目录
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/ # 静态资源
|
||||
│ ├── images/ # 图片
|
||||
│ ├── fonts/ # 字体
|
||||
│ └── styles/ # 样式文件
|
||||
├── components/ # 组件
|
||||
│ ├── Layout/ # 布局组件
|
||||
│ ├── Common/ # 通用组件
|
||||
│ └── Business/ # 业务组件
|
||||
├── pages/ # 页面组件
|
||||
├── contexts/ # Context上下文
|
||||
├── hooks/ # 自定义Hooks
|
||||
├── services/ # API服务
|
||||
├── utils/ # 工具函数
|
||||
├── config/ # 配置文件
|
||||
└── router/ # 路由配置
|
||||
```
|
||||
|
||||
### 3.2 文件命名规范
|
||||
|
||||
```
|
||||
组件:PascalCase
|
||||
- Button.jsx
|
||||
- ProjectList.jsx
|
||||
- UserForm.jsx
|
||||
|
||||
Hooks:camelCase
|
||||
- useAuth.js
|
||||
- useTable.js
|
||||
- useApi.js
|
||||
|
||||
工具函数:camelCase
|
||||
- formatDate.js
|
||||
- formatMoney.js
|
||||
- validate.js
|
||||
|
||||
样式文件:kebab-case
|
||||
- button.css
|
||||
- project-list.css
|
||||
- user-form.css
|
||||
```
|
||||
|
||||
## 4. 组件开发规范
|
||||
|
||||
### 4.1 函数式组件
|
||||
|
||||
```jsx
|
||||
// 推荐:使用函数式组件 + Hooks
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const MyComponent = ({ title, data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 副作用逻辑
|
||||
}, []);
|
||||
|
||||
return <div>{title}</div>;
|
||||
};
|
||||
|
||||
export default MyComponent;
|
||||
```
|
||||
|
||||
### 4.2 Props类型检查
|
||||
|
||||
```jsx
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MyComponent = ({ title, count, onAdd }) => {
|
||||
return <div>{title}: {count}</div>;
|
||||
};
|
||||
|
||||
MyComponent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
count: PropTypes.number,
|
||||
onAdd: PropTypes.func,
|
||||
};
|
||||
|
||||
MyComponent.defaultProps = {
|
||||
count: 0,
|
||||
onAdd: () => {},
|
||||
};
|
||||
|
||||
export default MyComponent;
|
||||
```
|
||||
|
||||
### 4.3 事件处理
|
||||
|
||||
```jsx
|
||||
// 推荐:使用箭头函数或bind绑定
|
||||
const MyComponent = () => {
|
||||
const handleClick = () => {
|
||||
console.log('Clicked');
|
||||
};
|
||||
|
||||
return <button onClick={handleClick}>Click me</button>;
|
||||
};
|
||||
|
||||
// 不推荐:在JSX中直接定义函数
|
||||
const MyComponent = () => {
|
||||
return <button onClick={() => console.log('Clicked')}>Click me</button>;
|
||||
};
|
||||
```
|
||||
|
||||
## 5. Hooks使用规范
|
||||
|
||||
### 5.1 useState
|
||||
|
||||
```jsx
|
||||
// 基本用法
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// 函数式更新
|
||||
const [count, setCount] = useState(0);
|
||||
const increment = () => setCount(prev => prev + 1);
|
||||
|
||||
// 对象更新
|
||||
const [user, setUser] = useState({ name: '', age: 0 });
|
||||
const updateName = (name) => setUser(prev => ({ ...prev, name }));
|
||||
```
|
||||
|
||||
### 5.2 useEffect
|
||||
|
||||
```jsx
|
||||
// 副作用
|
||||
useEffect(() => {
|
||||
// 执行副作用
|
||||
return () => {
|
||||
// 清理函数
|
||||
};
|
||||
}, [dependencies]);
|
||||
|
||||
// 空依赖数组:只在挂载时执行一次
|
||||
useEffect(() => {
|
||||
console.log('Component mounted');
|
||||
}, []);
|
||||
|
||||
// 依赖数组为空时,不要在依赖数组中省略
|
||||
// 错误示例
|
||||
useEffect(() => {
|
||||
fetchUser(userId);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 正确示例
|
||||
useEffect(() => {
|
||||
fetchUser(userId);
|
||||
}, [userId]);
|
||||
```
|
||||
|
||||
### 5.3 useMemo和useCallback
|
||||
|
||||
```jsx
|
||||
// useMemo:缓存计算结果
|
||||
const expensiveValue = useMemo(() => {
|
||||
return computeExpensiveValue(a, b);
|
||||
}, [a, b]);
|
||||
|
||||
// useCallback:缓存函数
|
||||
const handleClick = useCallback(() => {
|
||||
doSomething(a, b);
|
||||
}, [a, b]);
|
||||
```
|
||||
|
||||
### 5.4 自定义Hooks
|
||||
|
||||
```jsx
|
||||
// 自定义Hook以use开头
|
||||
const useFetch = (url) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
setData(await response.json());
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [url]);
|
||||
|
||||
return { data, loading, error };
|
||||
};
|
||||
```
|
||||
|
||||
## 6. 样式开发规范
|
||||
|
||||
### 6.1 CSS模块
|
||||
|
||||
```jsx
|
||||
// MyComponent.jsx
|
||||
import styles from './MyComponent.module.css';
|
||||
|
||||
const MyComponent = () => {
|
||||
return <div className={styles.container}>Content</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 CSS-in-JS(可选)
|
||||
|
||||
```jsx
|
||||
// 使用styled-components或emotion
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
`;
|
||||
|
||||
const MyComponent = () => {
|
||||
return <StyledDiv>Content</StyledDiv>;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.3 Tailwind CSS(可选)
|
||||
|
||||
```jsx
|
||||
// 使用Tailwind CSS工具类
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<div className="p-5 bg-gray-100 hover:bg-gray-200">
|
||||
Content
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 状态管理规范
|
||||
|
||||
### 7.1 使用Context API
|
||||
|
||||
```jsx
|
||||
// Context定义
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const MyContext = createContext();
|
||||
|
||||
export const MyProvider = ({ children }) => {
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
return (
|
||||
<MyContext.Provider value={{ state, setState }}>
|
||||
{children}
|
||||
</MyContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMyContext = () => {
|
||||
const context = useContext(MyContext);
|
||||
if (!context) {
|
||||
throw new Error('useMyContext must be used within MyProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 状态分类
|
||||
|
||||
```jsx
|
||||
// 1. 本地状态:使用useState
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// 2. 上下文状态:使用Context
|
||||
const { user, setUser } = useAuth();
|
||||
|
||||
// 3. 表单状态:使用Ant Design Form
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 4. URL状态:使用useSearchParams
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
```
|
||||
|
||||
## 8. 路由开发规范
|
||||
|
||||
### 8.1 使用React Router
|
||||
|
||||
```jsx
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/projects" element={<ProjectList />} />
|
||||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 路由参数
|
||||
|
||||
```jsx
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
const ProjectDetail = () => {
|
||||
const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
|
||||
return <div>Project ID: {id}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 8.3 路由守卫
|
||||
|
||||
```jsx
|
||||
const PrivateRoute = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
```
|
||||
|
||||
## 9. API调用规范
|
||||
|
||||
### 9.1 使用Axios
|
||||
|
||||
```jsx
|
||||
import { useEffect } from 'react';
|
||||
import { projectAPI } from '../services/project';
|
||||
|
||||
const ProjectList = () => {
|
||||
const [list, setList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await projectAPI.getList();
|
||||
setList(data.items);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchList();
|
||||
}, []);
|
||||
|
||||
return <div>{/* 渲染列表 */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 9.2 使用自定义Hooks
|
||||
|
||||
```jsx
|
||||
import { useList } from '../hooks/useList';
|
||||
import { projectAPI } from '../services/project';
|
||||
|
||||
const ProjectList = () => {
|
||||
const { list, loading, pagination, handleTableChange } = useList(
|
||||
projectAPI.getList
|
||||
);
|
||||
|
||||
return <Table {...} />;
|
||||
};
|
||||
```
|
||||
|
||||
## 10. 错误处理规范
|
||||
|
||||
### 10.1 错误边界
|
||||
|
||||
```jsx
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = { hasError: false, error: null };
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error('Error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <div>Something went wrong.</div>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### 10.2 错误处理组件
|
||||
|
||||
```jsx
|
||||
const ErrorFallback = ({ error, resetErrorBoundary }) => {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:</p>
|
||||
<pre>{error.message}</pre>
|
||||
<button onClick={resetErrorBoundary}>Try again</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 11. 性能优化
|
||||
|
||||
### 11.1 代码分割
|
||||
|
||||
```jsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const ProjectList = lazy(() => import('./pages/Projects/ProjectList'));
|
||||
const ProjectDetail = lazy(() => import('./pages/Projects/ProjectDetail'));
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Suspense fallback={<Spin />}>
|
||||
<Routes>
|
||||
<Route path="/projects" element={<ProjectList />} />
|
||||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 11.2 React.memo
|
||||
|
||||
```jsx
|
||||
const ExpensiveComponent = React.memo(({ data }) => {
|
||||
// 渲染逻辑
|
||||
});
|
||||
|
||||
// 自定义比较函数
|
||||
const MyComponent = React.memo(({ a, b }) => {
|
||||
// 渲染逻辑
|
||||
}, (prevProps, nextProps) => {
|
||||
return prevProps.a === nextProps.a && prevProps.b === nextProps.b;
|
||||
});
|
||||
```
|
||||
|
||||
### 11.3 useMemo和useCallback
|
||||
|
||||
```jsx
|
||||
const MyComponent = ({ data, onUpdate }) => {
|
||||
// 缓存计算结果
|
||||
const sortedData = useMemo(() => {
|
||||
return data.sort((a, b) => a.id - b.id);
|
||||
}, [data]);
|
||||
|
||||
// 缓存函数
|
||||
const handleClick = useCallback(() => {
|
||||
onUpdate(sortedData);
|
||||
}, [onUpdate, sortedData]);
|
||||
|
||||
return <div onClick={handleClick}>Content</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## 12. 测试规范
|
||||
|
||||
### 12.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);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 12.2 Hooks测试
|
||||
|
||||
```jsx
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useCounter } from './useCounter';
|
||||
|
||||
describe('useCounter', () => {
|
||||
test('should increment counter', () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 12.3 集成测试
|
||||
|
||||
```jsx
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
|
||||
const renderWithRouter = (component) => {
|
||||
return render(<BrowserRouter>{component}</BrowserRouter>);
|
||||
};
|
||||
|
||||
test('user can login', async () => {
|
||||
renderWithRouter(<App />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Username'), {
|
||||
target: { value: 'admin' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Password'), {
|
||||
target: { value: 'password' },
|
||||
});
|
||||
fireEvent.click(screen.getByText('Login'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 13. 调试技巧
|
||||
|
||||
### 13.1 React DevTools
|
||||
|
||||
```jsx
|
||||
// 安装React DevTools浏览器扩展
|
||||
// 可以查看组件树、props、state、Hooks等
|
||||
```
|
||||
|
||||
### 13.2 Console日志
|
||||
|
||||
```jsx
|
||||
// 开发环境下的调试日志
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Debug:', { data, loading });
|
||||
}
|
||||
|
||||
// 生产环境下自动移除
|
||||
```
|
||||
|
||||
### 13.3 性能分析
|
||||
|
||||
```jsx
|
||||
// 使用React Profiler分析性能
|
||||
import { Profiler } from 'react';
|
||||
|
||||
const onRenderCallback = (id, phase, actualDuration) => {
|
||||
console.log(`${id} ${phase} took ${actualDuration}ms`);
|
||||
};
|
||||
|
||||
<Profiler id="MyComponent" onRender={onRenderCallback}>
|
||||
<MyComponent />
|
||||
</Profiler>
|
||||
```
|
||||
|
||||
## 14. 部署规范
|
||||
|
||||
### 14.1 构建配置
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/app/', // 基础路径
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false, // 生产环境关闭sourcemap
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 14.2 环境变量
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
VITE_API_BASE_URL=http://localhost:5000/api/v1
|
||||
|
||||
# .env.production
|
||||
VITE_API_BASE_URL=https://api.example.com/api/v1
|
||||
```
|
||||
|
||||
### 14.3 Docker部署
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile
|
||||
FROM node:18-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
## 15. 最佳实践
|
||||
|
||||
### 15.1 组件设计
|
||||
|
||||
- 单一职责原则:每个组件只做一件事
|
||||
- 组件大小控制在300行以内
|
||||
- 合理拆分组件,提高复用性
|
||||
- 使用组合而非继承
|
||||
|
||||
### 15.2 性能优化
|
||||
|
||||
- 使用React.memo避免不必要的渲染
|
||||
- 使用useMemo缓存计算结果
|
||||
- 使用useCallback缓存函数
|
||||
- 使用懒加载减少初始加载时间
|
||||
|
||||
### 15.3 代码质量
|
||||
|
||||
- 遵循ESLint和Prettier规范
|
||||
- 编写单元测试
|
||||
- 添加必要的注释
|
||||
- 保持代码简洁清晰
|
||||
|
||||
### 15.4 安全性
|
||||
|
||||
- 避免直接渲染用户输入
|
||||
- 使用HTTPS
|
||||
- Token存储安全
|
||||
- 设置适当的CORS策略
|
||||
|
||||
## 16. 常见问题
|
||||
|
||||
### 16.1 如何处理跨域?
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 16.2 如何优化首屏加载?
|
||||
|
||||
```jsx
|
||||
// 使用路由懒加载
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
|
||||
// 使用Suspense包裹
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Dashboard />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### 16.3 如何处理表单验证?
|
||||
|
||||
```jsx
|
||||
import { Form, Input, Button } from 'antd';
|
||||
|
||||
const MyForm = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = (values) => {
|
||||
console.log('Form values:', values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={onFinish} layout="vertical">
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">提交</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端程序员
|
||||
**文档类型**: 前端开发指南
|
||||
**最后更新**: 2026-01-25
|
||||
@@ -0,0 +1,687 @@
|
||||
# 海洋项目管理系统 - 前端技术架构文档
|
||||
|
||||
## 1. 前端技术栈
|
||||
|
||||
### 1.1 核心框架
|
||||
- **React 18**: 用于构建用户界面的JavaScript库
|
||||
- **React Router 6**: 客户端路由管理
|
||||
- **Vite**: 现代化的前端构建工具(替代Create React App)
|
||||
|
||||
### 1.2 UI组件库
|
||||
- **Ant Design 5**: 企业级UI组件库
|
||||
- **@ant-design/icons**: Ant Design图标库
|
||||
|
||||
### 1.3 HTTP客户端
|
||||
- **Axios**: HTTP请求库
|
||||
|
||||
### 1.4 状态管理
|
||||
- **React Context API**: 轻量级状态管理
|
||||
- **useReducer Hook**: 复杂状态逻辑
|
||||
|
||||
### 1.5 表单处理
|
||||
- **Ant Design Form**: 表单组件和验证
|
||||
- **react-hook-form**: 表单状态管理(可选)
|
||||
|
||||
### 1.6 工具库
|
||||
- **dayjs**: 日期处理(轻量级替代moment.js)
|
||||
- **lodash**: 工具函数库
|
||||
- **classnames**: 条件类名处理
|
||||
|
||||
### 1.7 开发工具
|
||||
- **ESLint**: 代码质量检查
|
||||
- **Prettier**: 代码格式化
|
||||
- **Vitest**: 单元测试框架
|
||||
- **React Testing Library**: 组件测试
|
||||
- **TypeScript**: 类型检查(可选,当前使用JavaScript)
|
||||
|
||||
### 1.8 构建和部署
|
||||
- **Vite**: 开发服务器和构建工具
|
||||
- **Docker**: 容器化部署(生产环境)
|
||||
|
||||
## 2. 前端目录结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── images/ # 图片
|
||||
│ │ ├── fonts/ # 字体文件
|
||||
│ │ └── styles/ # 全局样式
|
||||
│ │ ├── global.css # 全局CSS
|
||||
│ │ ├── variables.css # CSS变量
|
||||
│ │ └── mixins.css # CSS mixins
|
||||
│ ├── components/ # 可复用组件
|
||||
│ │ ├── Layout/ # 布局组件
|
||||
│ │ │ ├── Layout.jsx # 主布局
|
||||
│ │ │ ├── Header.jsx # 顶部导航
|
||||
│ │ │ └── Sidebar.jsx # 侧边栏
|
||||
│ │ ├── Common/ # 通用组件
|
||||
│ │ │ ├── Button.jsx # 按钮组件
|
||||
│ │ │ ├── Input.jsx # 输入框组件
|
||||
│ │ │ ├── Select.jsx # 选择框组件
|
||||
│ │ │ ├── Table.jsx # 表格组件
|
||||
│ │ │ ├── Modal.jsx # 弹窗组件
|
||||
│ │ │ ├── Form.jsx # 表单组件
|
||||
│ │ │ ├── Card.jsx # 卡片组件
|
||||
│ │ │ └── Tag.jsx # 标签组件
|
||||
│ │ ├── Business/ # 业务组件
|
||||
│ │ │ ├── ProjectCard.jsx # 项目卡片
|
||||
│ │ │ ├── StatCard.jsx # 统计卡片
|
||||
│ │ │ └── UserAvatar.jsx # 用户头像
|
||||
│ │ └── index.js # 组件导出
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Login/ # 登录页
|
||||
│ │ │ └── Login.jsx
|
||||
│ │ ├── Dashboard/ # 仪表盘
|
||||
│ │ │ └── Dashboard.jsx
|
||||
│ │ ├── Projects/ # 项目管理
|
||||
│ │ │ ├── ProjectList.jsx # 项目列表
|
||||
│ │ │ ├── ProjectForm.jsx # 项目表单
|
||||
│ │ │ ├── ProjectDetail.jsx # 项目详情
|
||||
│ │ │ └── ProjectStatistics.jsx # 项目统计
|
||||
│ │ ├── Users/ # 用户管理(仅管理员)
|
||||
│ │ │ ├── UserList.jsx # 用户列表
|
||||
│ │ │ ├── UserForm.jsx # 用户表单
|
||||
│ │ │ └── UserResetPassword.jsx # 重置密码
|
||||
│ │ └── Statistics/ # 统计分析
|
||||
│ │ └── Statistics.jsx
|
||||
│ ├── contexts/ # Context上下文
|
||||
│ │ ├── AuthContext.jsx # 认证上下文
|
||||
│ │ └── ThemeContext.jsx # 主题上下文(可选)
|
||||
│ ├── hooks/ # 自定义Hooks
|
||||
│ │ ├── useAuth.js # 认证Hook
|
||||
│ │ ├── useApi.js # API调用Hook
|
||||
│ │ ├── usePermission.js # 权限检查Hook
|
||||
│ │ ├── useTable.js # 表格Hook
|
||||
│ │ ├── useForm.js # 表单Hook
|
||||
│ │ └── useDebounce.js # 防抖Hook
|
||||
│ ├── services/ # API服务
|
||||
│ │ ├── api.js # Axios配置
|
||||
│ │ ├── auth.js # 认证API
|
||||
│ │ ├── user.js # 用户API
|
||||
│ │ ├── project.js # 项目API
|
||||
│ │ └── statistics.js # 统计API
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── request.js # 请求封装
|
||||
│ │ ├── storage.js # 本地存储
|
||||
│ │ ├── auth.js # 认证工具
|
||||
│ │ ├── format.js # 格式化函数
|
||||
│ │ ├── validation.js # 验证函数
|
||||
│ │ └── constants.js # 常量定义
|
||||
│ ├── config/ # 配置文件
|
||||
│ │ ├── routes.js # 路由配置
|
||||
│ │ ├── menu.js # 菜单配置
|
||||
│ │ ├── colors.js # 颜色配置
|
||||
│ │ └── app.config.js # 应用配置
|
||||
│ ├── router/ # 路由相关
|
||||
│ │ ├── index.jsx # 路由配置
|
||||
│ │ ├── PrivateRoute.jsx # 路由守卫
|
||||
│ │ └── routes.js # 路由定义
|
||||
│ ├── styles/ # 样式文件
|
||||
│ │ └── index.css # 主样式文件
|
||||
│ ├── App.jsx # 根组件
|
||||
│ └── main.jsx # 应用入口
|
||||
├── tests/ # 测试目录
|
||||
│ ├── components/ # 组件测试
|
||||
│ ├── pages/ # 页面测试
|
||||
│ ├── hooks/ # Hooks测试
|
||||
│ └── utils/ # 工具测试
|
||||
├── public/ # 公共资源
|
||||
│ ├── favicon.ico # 网站图标
|
||||
│ ├── logo.png # Logo
|
||||
│ └── robots.txt # 爬虫配置
|
||||
├── docs/ # 前端文档
|
||||
│ ├── frontend-architecture.md # 前端技术架构(本文档)
|
||||
│ ├── component-design.md # 组件设计文档
|
||||
│ ├── api-service.md # API服务封装
|
||||
│ ├── state-management.md # 状态管理设计
|
||||
│ ├── routing-design.md # 路由设计文档
|
||||
│ └── development-guide.md # 开发指南
|
||||
├── .env # 环境变量
|
||||
├── .env.development # 开发环境变量
|
||||
├── .env.production # 生产环境变量
|
||||
├── .eslintrc.js # ESLint配置
|
||||
├── .prettierrc # Prettier配置
|
||||
├── package.json # 项目依赖
|
||||
├── vite.config.js # Vite配置
|
||||
├── index.html # HTML模板
|
||||
├── WORKSTANDARDS.md # 前端工作规范
|
||||
└── README.md # 前端说明文档
|
||||
```
|
||||
|
||||
## 3. 组件架构
|
||||
|
||||
### 3.1 组件层级
|
||||
|
||||
```
|
||||
App
|
||||
├── AuthProvider (认证上下文)
|
||||
├── Router (路由)
|
||||
│ ├── PrivateRoute (路由守卫)
|
||||
│ ├── Login (登录页)
|
||||
│ └── Layout (主布局)
|
||||
│ ├── Header (顶部导航)
|
||||
│ ├── Sidebar (侧边栏)
|
||||
│ └── Content (内容区)
|
||||
│ ├── Dashboard (仪表盘)
|
||||
│ ├── ProjectList (项目列表)
|
||||
│ ├── ProjectForm (项目表单)
|
||||
│ ├── ProjectDetail (项目详情)
|
||||
│ ├── UserList (用户列表)
|
||||
│ ├── UserForm (用户表单)
|
||||
│ └── Statistics (统计页)
|
||||
```
|
||||
|
||||
### 3.2 组件设计原则
|
||||
|
||||
#### 3.2.1 单一职责原则
|
||||
- 每个组件只负责一个功能
|
||||
- 避免组件过大,合理拆分
|
||||
|
||||
#### 3.2.2 组件复用性
|
||||
- 提取可复用的通用组件到 `components/Common/`
|
||||
- 提取业务相关组件到 `components/Business/`
|
||||
|
||||
#### 3.2.3 组件分类
|
||||
- **布局组件**: Layout, Header, Sidebar
|
||||
- **通用组件**: Button, Input, Form, Modal, Table
|
||||
- **业务组件**: ProjectCard, StatCard, UserAvatar
|
||||
- **页面组件**: Login, Dashboard, ProjectList等
|
||||
|
||||
#### 3.2.4 组件Props规范
|
||||
```jsx
|
||||
// 推荐的Props定义方式
|
||||
const MyComponent = ({
|
||||
title, // 必填props
|
||||
value, // 可选props
|
||||
onChange, // 事件回调
|
||||
className, // 样式类名
|
||||
style, // 内联样式
|
||||
children, // 子元素
|
||||
}) => {
|
||||
return <div>{title}</div>;
|
||||
};
|
||||
|
||||
MyComponent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
MyComponent.defaultProps = {
|
||||
value: '',
|
||||
onChange: () => {},
|
||||
className: '',
|
||||
style: {},
|
||||
};
|
||||
```
|
||||
|
||||
## 4. 状态管理
|
||||
|
||||
### 4.1 Context API方案
|
||||
|
||||
#### 4.1.1 AuthContext(认证上下文)
|
||||
```javascript
|
||||
// contexts/AuthContext.jsx
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [token, setToken] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 登录
|
||||
const login = async (username, password) => {
|
||||
// 登录逻辑
|
||||
};
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
// 登出逻辑
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
const checkAuth = async () => {
|
||||
// 检查逻辑
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
isAuthenticated: !!user,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 4.1.2 ThemeContext(主题上下文,可选)
|
||||
```javascript
|
||||
// contexts/ThemeContext.jsx
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 本地状态管理
|
||||
- 使用 `useState` 管理组件内部状态
|
||||
- 使用 `useReducer` 管理复杂的状态逻辑
|
||||
|
||||
### 4.3 表单状态管理
|
||||
- 使用 Ant Design Form 组件
|
||||
- 使用 `react-hook-form`(可选)
|
||||
|
||||
## 5. 路由设计
|
||||
|
||||
### 5.1 路由配置
|
||||
|
||||
```javascript
|
||||
// router/routes.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
meta: { title: '登录', requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
meta: { title: '主页', requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard',
|
||||
element: <Dashboard />,
|
||||
meta: { title: '仪表盘', icon: 'DashboardOutlined' },
|
||||
},
|
||||
{
|
||||
path: '/projects',
|
||||
element: <ProjectList />,
|
||||
meta: { title: '项目管理', icon: 'FileTextOutlined' },
|
||||
},
|
||||
{
|
||||
path: '/projects/create',
|
||||
element: <ProjectForm />,
|
||||
meta: { title: '创建项目' },
|
||||
},
|
||||
{
|
||||
path: '/projects/:id',
|
||||
element: <ProjectDetail />,
|
||||
meta: { title: '项目详情' },
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/edit',
|
||||
element: <ProjectForm />,
|
||||
meta: { title: '编辑项目' },
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
element: <UserList />,
|
||||
meta: { title: '用户管理', icon: 'UserOutlined', requiresRole: ['admin'] },
|
||||
},
|
||||
{
|
||||
path: '/users/create',
|
||||
element: <UserForm />,
|
||||
meta: { title: '创建用户', requiresRole: ['admin'] },
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
element: <Statistics />,
|
||||
meta: { title: '统计分析', icon: 'BarChartOutlined' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 5.2 路由守卫
|
||||
|
||||
```javascript
|
||||
// router/PrivateRoute.jsx
|
||||
const PrivateRoute = ({ children, requiresAuth, requiresRole }) => {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (requiresRole && !requiresRole.includes(user?.role)) {
|
||||
return <Navigate to="/403" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
```
|
||||
|
||||
## 6. API服务封装
|
||||
|
||||
### 6.1 Axios配置
|
||||
|
||||
```javascript
|
||||
// services/api.js
|
||||
import axios from 'axios';
|
||||
import { message } from 'antd';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const { status, data } = error.response;
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
message.error('登录已过期,请重新登录');
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
break;
|
||||
case 403:
|
||||
message.error('权限不足');
|
||||
break;
|
||||
case 404:
|
||||
message.error('请求的资源不存在');
|
||||
break;
|
||||
case 500:
|
||||
message.error('服务器错误');
|
||||
break;
|
||||
default:
|
||||
message.error(data.message || '请求失败');
|
||||
}
|
||||
} else {
|
||||
message.error('网络错误,请检查网络连接');
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
```
|
||||
|
||||
### 6.2 API模块化
|
||||
|
||||
```javascript
|
||||
// services/project.js
|
||||
import api from './api';
|
||||
|
||||
export const projectAPI = {
|
||||
// 获取项目列表
|
||||
getList: (params) => api.get('/projects', { params }),
|
||||
|
||||
// 获取项目详情
|
||||
getDetail: (id) => api.get(`/projects/${id}`),
|
||||
|
||||
// 创建项目
|
||||
create: (data) => api.post('/projects', data),
|
||||
|
||||
// 更新项目
|
||||
update: (id, data) => api.put(`/projects/${id}`, data),
|
||||
|
||||
// 删除项目
|
||||
delete: (id) => api.delete(`/projects/${id}`),
|
||||
|
||||
// 获取统计信息
|
||||
getStatistics: (params) => api.get('/projects/statistics', { params }),
|
||||
|
||||
// 获取分组统计
|
||||
getGroupStatistics: (params) => api.get('/projects/statistics/group', { params }),
|
||||
|
||||
// 获取时间维度统计
|
||||
getTimelineStatistics: (params) => api.get('/projects/statistics/timeline', { params }),
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 权限控制
|
||||
|
||||
### 7.1 路由级权限
|
||||
- 使用 `PrivateRoute` 组件检查认证状态
|
||||
- 使用 `requiresRole` 属性检查角色权限
|
||||
|
||||
### 7.2 组件级权限
|
||||
```javascript
|
||||
// hooks/usePermission.js
|
||||
export const usePermission = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const hasRole = (roles) => {
|
||||
if (!user) return false;
|
||||
return roles.includes(user.role);
|
||||
};
|
||||
|
||||
const canEdit = (resource, createdBy) => {
|
||||
if (!user) return false;
|
||||
if (user.role === 'admin') return true;
|
||||
if (user.role === 'market' && createdBy === user.id) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return { hasRole, canEdit };
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 按钮级权限
|
||||
```javascript
|
||||
// 组件中使用
|
||||
const { hasRole } = usePermission();
|
||||
|
||||
{hasRole(['admin', 'market']) && (
|
||||
<Button type="primary">创建项目</Button>
|
||||
)}
|
||||
```
|
||||
|
||||
## 8. 性能优化
|
||||
|
||||
### 8.1 代码分割
|
||||
```javascript
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const ProjectList = lazy(() => import('./pages/Projects/ProjectList'));
|
||||
const ProjectDetail = lazy(() => import('./pages/Projects/ProjectDetail'));
|
||||
|
||||
<Suspense fallback={<Spin />}>
|
||||
<ProjectList />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### 8.2 图片优化
|
||||
- 使用懒加载
|
||||
- 压缩图片文件
|
||||
- 使用WebP格式
|
||||
|
||||
### 8.3 请求优化
|
||||
- 防抖处理搜索请求
|
||||
- 取消重复请求
|
||||
- 使用缓存策略
|
||||
|
||||
### 8.4 渲染优化
|
||||
- 使用 `React.memo` 避免不必要的重渲染
|
||||
- 使用 `useMemo` 缓存计算结果
|
||||
- 使用 `useCallback` 缓存函数引用
|
||||
|
||||
## 9. 前端安全
|
||||
|
||||
### 9.1 XSS防护
|
||||
- React自动转义HTML
|
||||
- 避免使用 `dangerouslySetInnerHTML`
|
||||
- 输入验证和过滤
|
||||
|
||||
### 9.2 CSRF防护
|
||||
- 使用Cookie SameSite属性
|
||||
- 添加CSRF Token(可选)
|
||||
|
||||
### 9.3 敏感信息保护
|
||||
- Token存储在LocalStorage(可改为HttpOnly Cookie)
|
||||
- 不在URL中传递敏感信息
|
||||
- 使用HTTPS(生产环境)
|
||||
|
||||
### 9.4 内容安全策略(CSP)
|
||||
- 设置CSP头部
|
||||
- 限制外部资源加载
|
||||
|
||||
## 10. 开发规范
|
||||
|
||||
### 10.1 代码风格
|
||||
- 使用ESLint进行代码检查
|
||||
- 使用Prettier进行代码格式化
|
||||
- 遵循Airbnb JavaScript风格指南
|
||||
|
||||
### 10.2 命名规范
|
||||
- 组件名使用PascalCase(如 `ProjectList`)
|
||||
- 函数和变量使用camelCase(如 `getUserById`)
|
||||
- 常量使用UPPER_SNAKE_CASE(如 `API_BASE_URL`)
|
||||
- CSS类名使用kebab-case(如 `project-card`)
|
||||
|
||||
### 10.3 注释规范
|
||||
```javascript
|
||||
/**
|
||||
* 获取项目列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
* @returns {Promise} 项目列表数据
|
||||
*/
|
||||
const getProjectList = async (params) => {
|
||||
// 实现代码
|
||||
};
|
||||
```
|
||||
|
||||
### 10.4 Git提交规范
|
||||
```
|
||||
[frontend] feat: 添加项目列表页面
|
||||
[frontend] fix: 修复登录bug
|
||||
[frontend] style: 优化代码格式
|
||||
[frontend] refactor: 重构API服务
|
||||
[frontend] test: 添加组件测试
|
||||
[frontend] docs: 更新文档
|
||||
[frontend] chore: 更新依赖
|
||||
```
|
||||
|
||||
## 11. 测试策略
|
||||
|
||||
### 11.1 单元测试
|
||||
- 测试工具函数和自定义Hooks
|
||||
- 使用Vitest测试框架
|
||||
|
||||
### 11.2 组件测试
|
||||
- 测试组件渲染和交互
|
||||
- 使用React Testing Library
|
||||
|
||||
### 11.3 集成测试
|
||||
- 测试页面和路由
|
||||
- 测试API调用
|
||||
|
||||
### 11.4 E2E测试(可选)
|
||||
- 测试完整用户流程
|
||||
- 使用Playwright
|
||||
|
||||
## 12. 部署方案
|
||||
|
||||
### 12.1 开发环境
|
||||
```bash
|
||||
npm run dev
|
||||
# 访问 http://localhost:3000
|
||||
```
|
||||
|
||||
### 12.2 生产构建
|
||||
```bash
|
||||
npm run build
|
||||
# 生成 dist 目录
|
||||
```
|
||||
|
||||
### 12.3 Docker部署
|
||||
```dockerfile
|
||||
# Dockerfile
|
||||
FROM node:18-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
### 12.4 Nginx配置
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 13. 相关文档
|
||||
|
||||
- [组件设计文档](component-design.md) - 详细的组件设计规范
|
||||
- [API服务封装](api-service.md) - API服务封装详解
|
||||
- [状态管理设计](state-management.md) - 状态管理方案详解
|
||||
- [路由设计文档](routing-design.md) - 路由设计详解
|
||||
- [开发指南](development-guide.md) - 前端开发指南
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端程序员
|
||||
**文档类型**: 前端技术架构文档
|
||||
**最后更新**: 2026-01-25
|
||||
@@ -0,0 +1,879 @@
|
||||
# 路由设计文档
|
||||
|
||||
## 1. 路由架构
|
||||
|
||||
### 1.1 路由结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ App Root │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Router (React Router v6) │
|
||||
│ ├─ /login (登录页) │
|
||||
│ ├─ /403 (无权限页) │
|
||||
│ ├─ /404 (404页) │
|
||||
│ ├─ / (主布局) │
|
||||
│ │ ├─ /dashboard (仪表盘) │
|
||||
│ │ ├─ /projects (项目列表) │
|
||||
│ │ ├─ /projects/create (创建项目) │
|
||||
│ │ ├─ /projects/:id (项目详情) │
|
||||
│ │ ├─ /projects/:id/edit (编辑项目) │
|
||||
│ │ ├─ /users (用户列表) [admin] │
|
||||
│ │ ├─ /users/create (创建用户) [admin] │
|
||||
│ │ ├─ /users/:id (用户详情) [admin] │
|
||||
│ │ └─ /statistics (统计分析) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 路由配置
|
||||
|
||||
```javascript
|
||||
// router/routes.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
meta: {
|
||||
title: '登录',
|
||||
requiresAuth: false,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
element: <Forbidden />,
|
||||
meta: {
|
||||
title: '无权限',
|
||||
requiresAuth: false,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
element: <NotFound />,
|
||||
meta: {
|
||||
title: '页面不存在',
|
||||
requiresAuth: false,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
meta: {
|
||||
title: '主页',
|
||||
requiresAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard',
|
||||
element: <Dashboard />,
|
||||
meta: {
|
||||
title: '仪表盘',
|
||||
icon: 'DashboardOutlined',
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects',
|
||||
element: <ProjectList />,
|
||||
meta: {
|
||||
title: '项目管理',
|
||||
icon: 'FileTextOutlined',
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/create',
|
||||
element: <ProjectForm />,
|
||||
meta: {
|
||||
title: '创建项目',
|
||||
parent: '/projects',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id',
|
||||
element: <ProjectDetail />,
|
||||
meta: {
|
||||
title: '项目详情',
|
||||
parent: '/projects',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/edit',
|
||||
element: <ProjectForm />,
|
||||
meta: {
|
||||
title: '编辑项目',
|
||||
parent: '/projects',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
element: <UserList />,
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'UserOutlined',
|
||||
order: 3,
|
||||
requiresRole: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/create',
|
||||
element: <UserForm />,
|
||||
meta: {
|
||||
title: '创建用户',
|
||||
parent: '/users',
|
||||
requiresRole: ['admin'],
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/:id',
|
||||
element: <UserDetail />,
|
||||
meta: {
|
||||
title: '用户详情',
|
||||
parent: '/users',
|
||||
requiresRole: ['admin'],
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/:id/edit',
|
||||
element: <UserForm />,
|
||||
meta: {
|
||||
title: '编辑用户',
|
||||
parent: '/users',
|
||||
requiresRole: ['admin'],
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
element: <Statistics />,
|
||||
meta: {
|
||||
title: '统计分析',
|
||||
icon: 'BarChartOutlined',
|
||||
order: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
```
|
||||
|
||||
## 2. 路由守卫
|
||||
|
||||
### 2.1 PrivateRoute组件
|
||||
|
||||
```javascript
|
||||
// router/PrivateRoute.jsx
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
/**
|
||||
* 路由守卫组件
|
||||
*/
|
||||
const PrivateRoute = ({ children, meta = {} }) => {
|
||||
const { isAuthenticated, user, loading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
const { requiresAuth = true, requiresRole = [] } = meta;
|
||||
|
||||
// 检查加载状态
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (requiresRole.length > 0 && !requiresRole.includes(user?.role)) {
|
||||
return <Navigate to="/403" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
```
|
||||
|
||||
### 2.2 使用路由守卫
|
||||
|
||||
```javascript
|
||||
// router/index.jsx
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import PrivateRoute from './PrivateRoute';
|
||||
import routes from './routes';
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{routes.map((route) => {
|
||||
const { path, element, meta, children } = route;
|
||||
|
||||
if (children) {
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={
|
||||
<PrivateRoute meta={meta}>
|
||||
{element}
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
{children.map((child) => (
|
||||
<Route
|
||||
key={child.path}
|
||||
path={child.path}
|
||||
element={
|
||||
<PrivateRoute meta={child.meta}>
|
||||
{child.element}
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={
|
||||
<PrivateRoute meta={meta}>
|
||||
{element}
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 默认重定向 */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
{/* 404页面 */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
```
|
||||
|
||||
## 3. 路由参数处理
|
||||
|
||||
### 3.1 useParams Hook
|
||||
|
||||
```jsx
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ProjectDetail = () => {
|
||||
const { id } = useParams();
|
||||
const { data, loading } = useApi(() => projectAPI.getDetail(id));
|
||||
|
||||
if (loading) return <Spin />;
|
||||
return <div>{/* 项目详情 */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 useSearchParams Hook
|
||||
|
||||
```jsx
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
|
||||
const ProjectList = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
const debouncedKeyword = useDebounce(keyword, 300);
|
||||
|
||||
// 搜索逻辑
|
||||
useEffect(() => {
|
||||
fetchList({ keyword: debouncedKeyword });
|
||||
}, [debouncedKeyword]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearchParams({ keyword: value });
|
||||
};
|
||||
|
||||
return <Input.Search value={keyword} onChange={(e) => handleSearch(e.target.value)} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 useLocation Hook
|
||||
|
||||
```jsx
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const ProjectForm = () => {
|
||||
const location = useLocation();
|
||||
const isEditMode = location.pathname.includes('/edit');
|
||||
|
||||
return <div>{isEditMode ? '编辑模式' : '创建模式'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.4 useNavigate Hook
|
||||
|
||||
```jsx
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const ProjectList = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCreate = () => {
|
||||
navigate('/projects/create');
|
||||
};
|
||||
|
||||
const handleEdit = (id) => {
|
||||
navigate(`/projects/${id}/edit`);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1); // 返回上一页
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleCreate}>创建项目</Button>
|
||||
<Button onClick={() => handleEdit(1)}>编辑项目</Button>
|
||||
<Button onClick={handleBack}>返回</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 4. 路由元信息
|
||||
|
||||
### 4.1 路由元信息定义
|
||||
|
||||
```javascript
|
||||
// router/routes.js
|
||||
|
||||
const routeMeta = {
|
||||
// 页面标题
|
||||
title: '项目管理',
|
||||
|
||||
// 菜单图标
|
||||
icon: 'FileTextOutlined',
|
||||
|
||||
// 菜单排序
|
||||
order: 1,
|
||||
|
||||
// 是否需要认证
|
||||
requiresAuth: true,
|
||||
|
||||
// 允许的角色
|
||||
requiresRole: ['admin', 'market'],
|
||||
|
||||
// 父路由
|
||||
parent: '/projects',
|
||||
|
||||
// 是否在菜单中隐藏
|
||||
hideInMenu: false,
|
||||
|
||||
// 是否在面包屑中隐藏
|
||||
hideInBreadcrumb: false,
|
||||
|
||||
// 缓存配置
|
||||
keepAlive: false,
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 使用路由元信息
|
||||
|
||||
```javascript
|
||||
// hooks/useRouteMeta.js
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import routes from '../router/routes';
|
||||
|
||||
/**
|
||||
* 路由元信息Hook
|
||||
*/
|
||||
export const useRouteMeta = () => {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
|
||||
// 查找当前路由配置
|
||||
const findRouteMeta = (path, routeList) => {
|
||||
for (const route of routeList) {
|
||||
// 精确匹配
|
||||
if (route.path === path) {
|
||||
return route.meta;
|
||||
}
|
||||
|
||||
// 参数匹配
|
||||
const routePattern = route.path.replace(/:[^/]+/g, '[^/]+');
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
if (regex.test(path)) {
|
||||
return route.meta;
|
||||
}
|
||||
|
||||
// 递归查找子路由
|
||||
if (route.children) {
|
||||
const childMeta = findRouteMeta(path, route.children);
|
||||
if (childMeta) return childMeta;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const meta = findRouteMeta(location.pathname, routes);
|
||||
|
||||
return {
|
||||
meta,
|
||||
pathname: location.pathname,
|
||||
search: location.search,
|
||||
params,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 动态设置页面标题
|
||||
|
||||
```javascript
|
||||
// App.jsx
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRouteMeta } from './hooks/useRouteMeta';
|
||||
|
||||
const App = () => {
|
||||
const { meta } = useRouteMeta();
|
||||
|
||||
useEffect(() => {
|
||||
if (meta.title) {
|
||||
document.title = `${meta.title} - 海洋项目管理系统`;
|
||||
}
|
||||
}, [meta.title]);
|
||||
|
||||
return <Router />;
|
||||
};
|
||||
```
|
||||
|
||||
## 5. 菜单生成
|
||||
|
||||
### 5.1 根据路由生成菜单
|
||||
|
||||
```javascript
|
||||
// hooks/useMenu.js
|
||||
import { useMemo } from 'react';
|
||||
import { useAuth } from './useAuth';
|
||||
import { useRouteMeta } from './useRouteMeta';
|
||||
import routes from '../router/routes';
|
||||
|
||||
/**
|
||||
* 生成菜单
|
||||
*/
|
||||
export const useMenu = () => {
|
||||
const { hasRole } = useAuth();
|
||||
const { pathname } = useRouteMeta();
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const generateMenu = (routeList) => {
|
||||
return routeList
|
||||
.filter((route) => {
|
||||
// 过滤隐藏的菜单项
|
||||
if (route.meta?.hideInMenu) return false;
|
||||
|
||||
// 过滤需要权限的菜单项
|
||||
if (route.meta?.requiresRole?.length > 0) {
|
||||
return hasRole(route.meta.requiresRole);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((route) => {
|
||||
if (route.children) {
|
||||
return {
|
||||
key: route.path,
|
||||
icon: route.meta?.icon,
|
||||
label: route.meta?.title,
|
||||
children: generateMenu(route.children),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key: route.path,
|
||||
icon: route.meta?.icon,
|
||||
label: route.meta?.title,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return generateMenu(routes);
|
||||
}, [hasRole]);
|
||||
|
||||
const selectedKeys = useMemo(() => {
|
||||
return [pathname];
|
||||
}, [pathname]);
|
||||
|
||||
const openKeys = useMemo(() => {
|
||||
// 根据当前路径展开子菜单
|
||||
const paths = pathname.split('/').filter(Boolean);
|
||||
const keys = paths.map((_, index) => `/${paths.slice(0, index + 1).join('/')}`);
|
||||
return keys;
|
||||
}, [pathname]);
|
||||
|
||||
return {
|
||||
menuItems,
|
||||
selectedKeys,
|
||||
openKeys,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 侧边栏菜单
|
||||
|
||||
```jsx
|
||||
// components/Layout/Sidebar.jsx
|
||||
import { Menu } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useMenu } from '../../hooks/useMenu';
|
||||
|
||||
const Sidebar = () => {
|
||||
const navigate = useNavigate();
|
||||
const { menuItems, selectedKeys, openKeys } = useMenu();
|
||||
|
||||
const handleMenuClick = ({ key }) => {
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-logo">
|
||||
<img src="/logo.png" alt="Logo" />
|
||||
<span>海洋项目管理系统</span>
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
items={menuItems}
|
||||
selectedKeys={selectedKeys}
|
||||
defaultOpenKeys={openKeys}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 6. 面包屑导航
|
||||
|
||||
### 6.1 面包屑生成
|
||||
|
||||
```javascript
|
||||
// hooks/useBreadcrumb.js
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation, useMatches } from 'react-router-dom';
|
||||
import routes from '../router/routes';
|
||||
|
||||
/**
|
||||
* 生成面包屑
|
||||
*/
|
||||
export const useBreadcrumb = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
const items = [];
|
||||
|
||||
// 查找当前路径的所有路由
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
let currentPath = '';
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
currentPath += `/${segment}`;
|
||||
|
||||
// 查找路由配置
|
||||
const findRoute = (routeList, path) => {
|
||||
for (const route of routeList) {
|
||||
if (route.path === path) {
|
||||
return route;
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const found = findRoute(route.children, path);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const route = findRoute(routes, currentPath);
|
||||
|
||||
if (route && !route.meta?.hideInBreadcrumb) {
|
||||
items.push({
|
||||
path: currentPath,
|
||||
title: route.meta?.title || segment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [location.pathname]);
|
||||
|
||||
return { breadcrumbItems };
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 面包屑组件
|
||||
|
||||
```jsx
|
||||
// components/Common/Breadcrumb.jsx
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../hooks/useBreadcrumb';
|
||||
|
||||
const BreadcrumbNav = () => {
|
||||
const navigate = useNavigate();
|
||||
const { breadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const handleBreadcrumbClick = (path) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item onClick={() => navigate('/home')}>首页</Breadcrumb.Item>
|
||||
{breadcrumbItems.map((item, index) => (
|
||||
<Breadcrumb.Item
|
||||
key={item.path}
|
||||
onClick={() => handleBreadcrumbClick(item.path)}
|
||||
>
|
||||
{item.title}
|
||||
</Breadcrumb.Item>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 路由过渡动画
|
||||
|
||||
### 7.1 路由过渡组件
|
||||
|
||||
```jsx
|
||||
// components/RouterTransition.jsx
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
||||
import './RouterTransition.css';
|
||||
|
||||
const RouterTransition = ({ children }) => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
key={location.pathname}
|
||||
timeout={300}
|
||||
classNames="fade"
|
||||
unmountOnExit
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 过渡动画样式
|
||||
|
||||
```css
|
||||
/* RouterTransition.css */
|
||||
.fade-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 路由缓存
|
||||
|
||||
### 8.1 KeepAlive组件
|
||||
|
||||
```jsx
|
||||
// hooks/useKeepAlive.js
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
const keepAliveCache = new Map();
|
||||
|
||||
export const useKeepAlive = (cacheKey, children) => {
|
||||
const [isAlive, setIsAlive] = useState(false);
|
||||
|
||||
const activate = () => setIsAlive(true);
|
||||
const deactivate = () => setIsAlive(false);
|
||||
|
||||
if (!keepAliveCache.has(cacheKey)) {
|
||||
keepAliveCache.set(cacheKey, { children, isActive: false });
|
||||
}
|
||||
|
||||
const cache = keepAliveCache.get(cacheKey);
|
||||
|
||||
return {
|
||||
isAlive,
|
||||
activate,
|
||||
deactivate,
|
||||
cachedChildren: cache.children,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 使用KeepAlive
|
||||
|
||||
```jsx
|
||||
const CachedComponent = () => {
|
||||
const { cachedChildren } = useKeepAlive('dashboard', <Dashboard />);
|
||||
return cachedChildren;
|
||||
};
|
||||
```
|
||||
|
||||
## 9. 路由权限控制
|
||||
|
||||
### 9.1 权限指令
|
||||
|
||||
```jsx
|
||||
// components/Permission.jsx
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
/**
|
||||
* 权限控制组件
|
||||
*/
|
||||
const Permission = ({ role, permission, children, fallback = null }) => {
|
||||
const { user, hasRole, hasPermission } = useAuth();
|
||||
|
||||
// 检查角色权限
|
||||
if (role && !hasRole(role)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 检查功能权限
|
||||
if (permission && !hasPermission(permission)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Permission;
|
||||
```
|
||||
|
||||
### 9.2 使用权限组件
|
||||
|
||||
```jsx
|
||||
import Permission from './Permission';
|
||||
|
||||
const ProjectList = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* 只有管理员可见 */}
|
||||
<Permission role="admin">
|
||||
<Button>删除项目</Button>
|
||||
</Permission>
|
||||
|
||||
{/* 只有市场部用户可见 */}
|
||||
<Permission role="market">
|
||||
<Button>创建项目</Button>
|
||||
</Permission>
|
||||
|
||||
{/* 没有权限时显示备用内容 */}
|
||||
<Permission permission="edit_project" fallback={<span>无权限</span>}>
|
||||
<Button>编辑项目</Button>
|
||||
</Permission>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 10. 路由最佳实践
|
||||
|
||||
### 10.1 路由命名规范
|
||||
|
||||
```javascript
|
||||
// 好的路由命名
|
||||
'/projects' // 项目列表
|
||||
'/projects/create' // 创建项目
|
||||
'/projects/:id' // 项目详情
|
||||
'/projects/:id/edit' // 编辑项目
|
||||
|
||||
// 不好的路由命名
|
||||
'/p' // 太短,不清晰
|
||||
'/project-list-page' // 太长,冗余
|
||||
'/projects/1/edit' // 使用硬编码ID
|
||||
```
|
||||
|
||||
### 10.2 路由嵌套
|
||||
|
||||
```javascript
|
||||
// 合理的路由嵌套
|
||||
<Route path="/projects" element={<Layout />}>
|
||||
<Route index element={<ProjectList />} />
|
||||
<Route path="create" element={<ProjectForm />} />
|
||||
<Route path=":id" element={<ProjectDetail />} />
|
||||
<Route path=":id/edit" element={<ProjectForm />} />
|
||||
</Route>
|
||||
|
||||
// 不合理的路由嵌套(过深)
|
||||
<Route path="/projects" element={<A />}>
|
||||
<Route path="list" element={<B />}>
|
||||
<Route path=":id" element={<C />}>
|
||||
<Route path="detail" element={<D />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
```
|
||||
|
||||
### 10.3 路由懒加载
|
||||
|
||||
```javascript
|
||||
// 懒加载路由组件
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const ProjectList = lazy(() => import('./pages/Projects/ProjectList'));
|
||||
const ProjectDetail = lazy(() => import('./pages/Projects/ProjectDetail'));
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
<Suspense fallback={<Spin />}>
|
||||
<Routes>
|
||||
<Route path="/projects" element={<ProjectList />} />
|
||||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端程序员
|
||||
**文档类型**: 路由设计文档
|
||||
**最后更新**: 2026-01-25
|
||||
@@ -0,0 +1,958 @@
|
||||
# 状态管理设计文档
|
||||
|
||||
## 1. 状态管理方案选择
|
||||
|
||||
### 1.1 为什么选择Context API
|
||||
|
||||
对于本项目的需求,React Context API是最佳选择:
|
||||
|
||||
**优势:**
|
||||
- 轻量级,无需引入额外的库
|
||||
- React官方内置,学习成本低
|
||||
- 适合中小型应用的状态管理
|
||||
- 与React生态系统完美集成
|
||||
|
||||
**适用场景:**
|
||||
- 认证状态(用户信息、Token)
|
||||
- 全局配置(主题、语言)
|
||||
- 简单的共享状态
|
||||
|
||||
**不适用场景:**
|
||||
- 复杂的跨组件通信
|
||||
- 高频率的状态更新
|
||||
- 需要时间旅行调试
|
||||
- 如果将来需要,可以迁移到Redux或Zustand
|
||||
|
||||
### 1.2 状态管理架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ App Root │
|
||||
├─────────────────────────────────────────┤
|
||||
│ AuthProvider (认证状态) │
|
||||
│ ├─ user (用户信息) │
|
||||
│ ├─ token (认证令牌) │
|
||||
│ └─ authState (认证状态) │
|
||||
│ │
|
||||
│ ThemeProvider (主题状态,可选) │
|
||||
│ └─ theme (主题配置) │
|
||||
│ │
|
||||
│ Router (路由状态) │
|
||||
│ └─ currentPath (当前路径) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Context实现
|
||||
|
||||
### 2.1 AuthContext(认证上下文)
|
||||
|
||||
```javascript
|
||||
// contexts/AuthContext.jsx
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { authAPI } from '../services/auth';
|
||||
import { message } from 'antd';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 认证上下文Provider
|
||||
*/
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [token, setToken] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authState, setAuthState] = useState({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化认证状态
|
||||
*/
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const savedToken = localStorage.getItem('token');
|
||||
const savedUser = localStorage.getItem('user');
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
// 验证Token有效性
|
||||
const currentUser = await authAPI.getCurrentUser();
|
||||
|
||||
setToken(savedToken);
|
||||
setUser(currentUser);
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
clearAuth();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
clearAuth();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
const login = async (username, password) => {
|
||||
try {
|
||||
const response = await authAPI.login(username, password);
|
||||
|
||||
const { token: newToken, user: userData } = response;
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', newToken);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
// 更新状态
|
||||
setToken(newToken);
|
||||
setUser(userData);
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await authAPI.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
} finally {
|
||||
clearAuth();
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 清除认证信息
|
||||
*/
|
||||
const clearAuth = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
const updateUser = useCallback((userData) => {
|
||||
setUser((prev) => ({ ...prev, ...userData }));
|
||||
localStorage.setItem('user', JSON.stringify({ ...user, ...userData }));
|
||||
}, [user]);
|
||||
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
const hasRole = useCallback(
|
||||
(roles) => {
|
||||
if (!user) return false;
|
||||
if (Array.isArray(roles)) {
|
||||
return roles.includes(user.role);
|
||||
}
|
||||
return user.role === roles;
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
/**
|
||||
* 检查是否可以编辑
|
||||
*/
|
||||
const canEdit = useCallback(
|
||||
(createdBy) => {
|
||||
if (!user) return false;
|
||||
if (user.role === 'admin') return true;
|
||||
if (user.role === 'market' && createdBy === user.id) return true;
|
||||
return false;
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const value = {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
authState,
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
updateUser,
|
||||
hasRole,
|
||||
canEdit,
|
||||
isAuthenticated: authState.isAuthenticated,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用AuthContext的Hook
|
||||
*/
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default AuthContext;
|
||||
```
|
||||
|
||||
### 2.2 ThemeContext(主题上下文,可选)
|
||||
|
||||
```javascript
|
||||
// contexts/ThemeContext.jsx
|
||||
import { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
|
||||
const ThemeContext = createContext(null);
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
||||
// 从localStorage加载主题
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme((prev) => {
|
||||
const newTheme = prev === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
return newTheme;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
*/
|
||||
const setThemeMode = useCallback((mode) => {
|
||||
setTheme(mode);
|
||||
localStorage.setItem('theme', mode);
|
||||
document.documentElement.setAttribute('data-theme', mode);
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
toggleTheme,
|
||||
setThemeMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
```
|
||||
|
||||
## 3. 自定义Hooks
|
||||
|
||||
### 3.1 useAuth Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useAuth.js
|
||||
import { useAuth as useAuthContext } from '../contexts/AuthContext';
|
||||
|
||||
export const useAuth = () => {
|
||||
return useAuthContext();
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 usePermission Hook
|
||||
|
||||
```javascript
|
||||
// hooks/usePermission.js
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
/**
|
||||
* 权限检查Hook
|
||||
*/
|
||||
export const usePermission = () => {
|
||||
const { user, hasRole, canEdit } = useAuth();
|
||||
|
||||
/**
|
||||
* 检查是否是管理员
|
||||
*/
|
||||
const isAdmin = () => {
|
||||
return user?.role === 'admin';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否是市场部
|
||||
*/
|
||||
const isMarket = () => {
|
||||
return user?.role === 'market';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否是其他部门
|
||||
*/
|
||||
const isOther = () => {
|
||||
return user?.role === 'other';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否有指定权限
|
||||
*/
|
||||
const hasPermission = (permission) => {
|
||||
// 可以扩展更复杂的权限系统
|
||||
const permissions = {
|
||||
admin: ['create_user', 'edit_user', 'delete_user', 'create_project', 'edit_project', 'delete_project'],
|
||||
market: ['create_project', 'edit_own_project', 'delete_own_project'],
|
||||
other: ['edit_project_cost', 'edit_project_finance', 'edit_project_progress'],
|
||||
};
|
||||
|
||||
return permissions[user?.role]?.includes(permission) || false;
|
||||
};
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
isMarket,
|
||||
isOther,
|
||||
hasRole,
|
||||
canEdit,
|
||||
hasPermission,
|
||||
user,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 useModal Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useModal.js
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 弹窗管理Hook
|
||||
*/
|
||||
export const useModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const open = useCallback((modalData) => {
|
||||
setData(modalData);
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setVisible((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const setLoadingState = useCallback((state) => {
|
||||
setLoading(state);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
visible,
|
||||
loading,
|
||||
data,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
setLoading: setLoadingState,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 3.4 useTable Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useTable.js
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 表格管理Hook
|
||||
*/
|
||||
export const useTable = (initialState = {}) => {
|
||||
const {
|
||||
pagination: initialPagination = { current: 1, pageSize: 10 },
|
||||
filters: initialFilters = {},
|
||||
sorter: initialSorter = {},
|
||||
selectedRowKeys: initialSelectedRowKeys = [],
|
||||
} = initialState;
|
||||
|
||||
const [pagination, setPagination] = useState(initialPagination);
|
||||
const [filters, setFilters] = useState(initialFilters);
|
||||
const [sorter, setSorter] = useState(initialSorter);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState(initialSelectedRowKeys);
|
||||
|
||||
const handleTableChange = useCallback((newPagination, newFilters, newSorter) => {
|
||||
setPagination(newPagination);
|
||||
setFilters(newFilters);
|
||||
setSorter({
|
||||
field: newSorter.field,
|
||||
order: newSorter.order,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetTable = useCallback(() => {
|
||||
setPagination(initialPagination);
|
||||
setFilters(initialFilters);
|
||||
setSorter(initialSorter);
|
||||
setSelectedRowKeys(initialSelectedRowKeys);
|
||||
}, [initialPagination, initialFilters, initialSorter, initialSelectedRowKeys]);
|
||||
|
||||
return {
|
||||
pagination,
|
||||
filters,
|
||||
sorter,
|
||||
selectedRowKeys,
|
||||
setSelectedRowKeys,
|
||||
handleTableChange,
|
||||
resetTable,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 3.5 useForm Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useForm.js
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 表单管理Hook
|
||||
*/
|
||||
export const useForm = (initialValues = {}) => {
|
||||
const [values, setValues] = useState(initialValues);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [touched, setTouched] = useState({});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* 更新表单值
|
||||
*/
|
||||
const handleChange = useCallback((field, value) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
// 清除该字段的错误
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[field]: null,
|
||||
}));
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
/**
|
||||
* 批量更新表单值
|
||||
*/
|
||||
const setFieldsValue = useCallback((newValues) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
...newValues,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 标记字段为已触摸
|
||||
*/
|
||||
const handleBlur = useCallback((field) => {
|
||||
setTouched((prev) => ({
|
||||
...prev,
|
||||
[field]: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 设置错误
|
||||
*/
|
||||
const setFieldError = useCallback((field, error) => {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[field]: error,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 批量设置错误
|
||||
*/
|
||||
const setErrorsAll = useCallback((newErrors) => {
|
||||
setErrors(newErrors);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 验证表单
|
||||
*/
|
||||
const validate = useCallback((validationRules) => {
|
||||
const newErrors = {};
|
||||
let isValid = true;
|
||||
|
||||
Object.keys(validationRules).forEach((field) => {
|
||||
const rules = validationRules[field];
|
||||
const value = values[field];
|
||||
|
||||
for (const rule of rules) {
|
||||
let error = null;
|
||||
|
||||
if (rule.required && !value) {
|
||||
error = rule.message || `${field} is required`;
|
||||
} else if (rule.pattern && !rule.pattern.test(value)) {
|
||||
error = rule.message || `${field} is invalid`;
|
||||
} else if (rule.validator && !rule.validator(value)) {
|
||||
error = rule.message || `${field} is invalid`;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
newErrors[field] = error;
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
return isValid;
|
||||
}, [values]);
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const resetForm = useCallback(() => {
|
||||
setValues(initialValues);
|
||||
setErrors({});
|
||||
setTouched({});
|
||||
setSubmitting(false);
|
||||
}, [initialValues]);
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = useCallback(async (onSubmit, validationRules) => {
|
||||
if (validationRules && !validate(validationRules)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit(values);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Form submit error:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [values, validate]);
|
||||
|
||||
return {
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
submitting,
|
||||
handleChange,
|
||||
setFieldsValue,
|
||||
handleBlur,
|
||||
setFieldError,
|
||||
setErrorsAll,
|
||||
validate,
|
||||
resetForm,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 3.6 useDebounce Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useDebounce.js
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* 防抖Hook
|
||||
*/
|
||||
export const useDebounce = (value, delay = 300) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.7 useLocalStorage Hook
|
||||
|
||||
```javascript
|
||||
// hooks/useLocalStorage.js
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* LocalStorage Hook
|
||||
*/
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.error(`Error reading localStorage key "${key}":`, error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = useCallback(
|
||||
(value) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.error(`Error setting localStorage key "${key}":`, error);
|
||||
}
|
||||
},
|
||||
[key, storedValue]
|
||||
);
|
||||
|
||||
const removeValue = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(initialValue);
|
||||
} catch (error) {
|
||||
console.error(`Error removing localStorage key "${key}":`, error);
|
||||
}
|
||||
}, [key, initialValue]);
|
||||
|
||||
return [storedValue, setValue, removeValue];
|
||||
};
|
||||
```
|
||||
|
||||
## 4. 状态管理最佳实践
|
||||
|
||||
### 4.1 状态分类
|
||||
|
||||
```javascript
|
||||
// 1. 本地状态 - 使用useState
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// 2. 上下文状态 - 使用Context
|
||||
const { user, login, logout } = useAuth();
|
||||
|
||||
// 3. URL状态 - 使用useSearchParams
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// 4. 表单状态 - 使用Form Hook
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 5. 列表状态 - 使用自定义Hook
|
||||
const { list, loading, pagination } = useList(projectAPI.getList);
|
||||
```
|
||||
|
||||
### 4.2 状态提升
|
||||
|
||||
**错误示例:**
|
||||
```jsx
|
||||
// 父组件
|
||||
const Parent = () => {
|
||||
const [value, setValue] = useState('');
|
||||
return <Child value={value} onChange={setValue} />;
|
||||
};
|
||||
|
||||
// 子组件
|
||||
const Child = ({ value, onChange }) => {
|
||||
return <Input value={value} onChange={(e) => onChange(e.target.value)} />;
|
||||
};
|
||||
```
|
||||
|
||||
**正确示例:**
|
||||
```jsx
|
||||
// 状态留在子组件
|
||||
const Child = () => {
|
||||
const [value, setValue] = useState('');
|
||||
return <Input value={value} onChange={(e) => setValue(e.target.value)} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 避免不必要的重渲染
|
||||
|
||||
```jsx
|
||||
// 使用React.memo
|
||||
const ExpensiveComponent = React.memo(({ data }) => {
|
||||
return <div>{/* 渲染逻辑 */}</div>;
|
||||
});
|
||||
|
||||
// 使用useMemo缓存计算结果
|
||||
const expensiveValue = useMemo(() => {
|
||||
return computeExpensiveValue(data);
|
||||
}, [data]);
|
||||
|
||||
// 使用useCallback缓存函数
|
||||
const handleClick = useCallback(() => {
|
||||
doSomething(a, b);
|
||||
}, [a, b]);
|
||||
```
|
||||
|
||||
## 5. 状态持久化
|
||||
|
||||
### 5.1 本地存储策略
|
||||
|
||||
```javascript
|
||||
// 需要持久化的状态
|
||||
const persistentState = {
|
||||
user: 'localStorage', // 用户信息
|
||||
token: 'localStorage', // 认证令牌
|
||||
theme: 'localStorage', // 主题设置
|
||||
language: 'localStorage', // 语言设置
|
||||
};
|
||||
|
||||
// 不需要持久化的状态
|
||||
const transientState = {
|
||||
loading: false, // 加载状态
|
||||
error: null, // 错误信息
|
||||
modalVisible: false, // 弹窗状态
|
||||
formData: {}, // 表单数据
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 状态恢复
|
||||
|
||||
```javascript
|
||||
// 在应用启动时恢复状态
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
const savedUser = localStorage.getItem('user');
|
||||
if (savedUser) {
|
||||
setUser(JSON.parse(savedUser));
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
## 6. 调试工具
|
||||
|
||||
### 6.1 React DevTools
|
||||
|
||||
```javascript
|
||||
// 安装React DevTools浏览器扩展
|
||||
// 在开发环境中,可以使用React DevTools查看组件树和状态
|
||||
```
|
||||
|
||||
### 6.2 自定义状态日志
|
||||
|
||||
```javascript
|
||||
// 开发环境下的状态日志
|
||||
if (import.meta.env.DEV) {
|
||||
useEffect(() => {
|
||||
console.log('Auth state changed:', { user, token, authState });
|
||||
}, [user, token, authState]);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 状态变化监听
|
||||
|
||||
```javascript
|
||||
// 监听特定状态的变化
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
console.log('User logged in:', user);
|
||||
} else {
|
||||
console.log('User logged out');
|
||||
}
|
||||
}, [user]);
|
||||
```
|
||||
|
||||
## 7. 性能优化
|
||||
|
||||
### 7.1 减少Context更新
|
||||
|
||||
```javascript
|
||||
// 错误示例:频繁更新Context
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [timestamp, setTimestamp] = useState(Date.now());
|
||||
|
||||
// 这个timestamp变化会导致所有使用AuthContext的组件重渲染
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setTimestamp(Date.now());
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return <AuthContext.Provider value={{ user, timestamp }}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
// 正确示例:分离频繁更新的状态
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [timestamp, setTimestamp] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setTimestamp(Date.now());
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
|
||||
<TimestampContext.Provider value={{ timestamp}}>{/* 不渲染 */}</TimestampContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 使用useReducer
|
||||
|
||||
```javascript
|
||||
// 对于复杂的状态逻辑,使用useReducer
|
||||
const initialState = {
|
||||
user: null,
|
||||
token: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function authReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'LOGIN_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
user: action.payload.user,
|
||||
token: action.payload.token,
|
||||
loading: false,
|
||||
};
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 8. 未来扩展
|
||||
|
||||
### 8.1 迁移到Redux
|
||||
|
||||
如果项目变得复杂,可以考虑迁移到Redux Toolkit:
|
||||
|
||||
```javascript
|
||||
// slices/authSlice.js
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: {
|
||||
user: null,
|
||||
token: null,
|
||||
loading: true,
|
||||
},
|
||||
reducers: {
|
||||
setCredentials: (state, action) => {
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
state.loading = false;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setCredentials, logout } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
```
|
||||
|
||||
### 8.2 迁移到Zustand
|
||||
|
||||
Zustand是一个更轻量级的替代方案:
|
||||
|
||||
```javascript
|
||||
// store/authStore.js
|
||||
import { create } from 'zustand';
|
||||
|
||||
export const useAuthStore = create((set) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
loading: true,
|
||||
login: (user, token) => set({ user, token, loading: false }),
|
||||
logout: () => set({ user: null, token: null }),
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端程序员
|
||||
**文档类型**: 状态管理设计文档
|
||||
**最后更新**: 2026-01-25
|
||||
Reference in New Issue
Block a user