save code

This commit is contained in:
xsl
2026-01-25 15:05:03 +08:00
parent 29565e9dfe
commit 0895398138
23 changed files with 11859 additions and 966 deletions
File diff suppressed because it is too large Load Diff
+735
View File
@@ -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
+882
View File
@@ -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
HookscamelCase
- 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
+687
View File
@@ -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
+879
View File
@@ -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
+958
View File
@@ -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