2074 lines
48 KiB
Markdown
2074 lines
48 KiB
Markdown
# 海洋项目管理系统 - 产品交互文档(前端开发版)
|
|
|
|
## 文档说明
|
|
|
|
本文档面向前端开发人员,详细描述系统各功能模块的交互流程、状态管理、API调用时机和实现细节。
|
|
|
|
**版本**: v1.0
|
|
**更新日期**: 2026-01-25
|
|
**适用系统版本**: v1.0
|
|
|
|
---
|
|
|
|
## 目录
|
|
|
|
1. [系统整体架构](#1-系统整体架构)
|
|
2. [认证与授权](#2-认证与授权)
|
|
3. [页面导航与路由](#3-页面导航与路由)
|
|
4. [登录模块](#4-登录模块)
|
|
5. [仪表盘模块](#5-仪表盘模块)
|
|
6. [项目管理模块](#6-项目管理模块)
|
|
7. [用户管理模块](#7-用户管理模块)
|
|
8. [项目统计模块](#8-项目统计模块)
|
|
9. [通用组件与工具](#9-通用组件与工具)
|
|
10. [状态管理](#10-状态管理)
|
|
11. [错误处理](#11-错误处理)
|
|
12. [性能优化](#12-性能优化)
|
|
|
|
---
|
|
|
|
## 1. 系统整体架构
|
|
|
|
### 1.1 技术栈
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ 前端架构 │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ 框架: React 18 + TypeScript │
|
|
│ 状态管理: Redux Toolkit + React Query │
|
|
│ 路由: React Router v6 │
|
|
│ UI组件库: Ant Design 5.x │
|
|
│ HTTP客户端: Axios │
|
|
│ 图表库: Apache ECharts │
|
|
│ 构建工具: Vite │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 1.2 目录结构
|
|
|
|
```
|
|
src/
|
|
├── api/ # API接口定义
|
|
│ ├── auth.ts # 认证相关API
|
|
│ ├── projects.ts # 项目相关API
|
|
│ ├── users.ts # 用户相关API
|
|
│ └── statistics.ts # 统计相关API
|
|
├── components/ # 通用组件
|
|
│ ├── Layout/ # 布局组件
|
|
│ ├── Table/ # 表格组件
|
|
│ ├── Form/ # 表单组件
|
|
│ ├── Card/ # 卡片组件
|
|
│ ├── Tag/ # 标签组件
|
|
│ └── Modal/ # 模态框组件
|
|
├── pages/ # 页面组件
|
|
│ ├── Login/ # 登录页
|
|
│ ├── Dashboard/ # 仪表盘
|
|
│ ├── Projects/ # 项目管理
|
|
│ ├── Users/ # 用户管理
|
|
│ └── Statistics/ # 项目统计
|
|
├── store/ # 状态管理
|
|
│ ├── authSlice.ts # 认证状态
|
|
│ ├── projectSlice.ts # 项目状态
|
|
│ └── userSlice.ts # 用户状态
|
|
├── hooks/ # 自定义Hooks
|
|
│ ├── useAuth.ts # 认证Hook
|
|
│ ├── useProjects.ts # 项目Hook
|
|
│ └── useStatistics.ts # 统计Hook
|
|
├── utils/ # 工具函数
|
|
│ ├── request.ts # Axios封装
|
|
│ ├── format.ts # 格式化函数
|
|
│ └── validate.ts # 验证函数
|
|
├── types/ # TypeScript类型定义
|
|
│ ├── auth.ts # 认证类型
|
|
│ ├── project.ts # 项目类型
|
|
│ └── user.ts # 用户类型
|
|
└── App.tsx # 应用入口
|
|
```
|
|
|
|
### 1.3 API基础配置
|
|
|
|
**Base URL**: `http://localhost:5000/api/v1`
|
|
|
|
**请求拦截器**:
|
|
```typescript
|
|
// utils/request.ts
|
|
axios.interceptors.request.use((config) => {
|
|
// 添加Token
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
```
|
|
|
|
**响应拦截器**:
|
|
```typescript
|
|
axios.interceptors.response.use(
|
|
(response) => {
|
|
// 统一处理成功响应
|
|
return response.data.data;
|
|
},
|
|
(error) => {
|
|
// 统一处理错误响应
|
|
handleApiError(error);
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 认证与授权
|
|
|
|
### 2.1 认证流程
|
|
|
|
```
|
|
用户登录 → 输入用户名密码 → 调用登录API → 获取Token → 存储Token → 跳转首页
|
|
```
|
|
|
|
### 2.2 Token管理
|
|
|
|
**Token存储位置**: `localStorage`
|
|
|
|
**Token结构**:
|
|
```typescript
|
|
interface AuthToken {
|
|
token: string;
|
|
token_type: string;
|
|
user: UserInfo;
|
|
}
|
|
```
|
|
|
|
**Token过期处理**:
|
|
- Token有效期:24小时
|
|
- 每次API调用检查Token是否过期
|
|
- 过期时自动跳转登录页
|
|
|
|
### 2.3 权限控制
|
|
|
|
**权限类型**:
|
|
- `admin`: 管理员
|
|
- `market`: 市场部
|
|
- `other`: 其他部门
|
|
|
|
**权限检查Hook**:
|
|
```typescript
|
|
// hooks/useAuth.ts
|
|
export const useAuth = () => {
|
|
const user = useSelector((state: RootState) => state.auth.user);
|
|
const hasPermission = (permission: string) => {
|
|
return checkPermission(user.role, permission);
|
|
};
|
|
return { user, hasPermission };
|
|
};
|
|
```
|
|
|
|
**页面级权限**:
|
|
```typescript
|
|
// 路由守卫
|
|
const ProtectedRoute = ({ children, requiredRole }) => {
|
|
const { user } = useAuth();
|
|
if (!user) return <Navigate to="/login" />;
|
|
if (requiredRole && !hasPermission(user.role, requiredRole)) {
|
|
return <div>您没有访问该页面的权限</div>;
|
|
}
|
|
return children;
|
|
};
|
|
```
|
|
|
|
**组件级权限**:
|
|
```typescript
|
|
const { hasPermission } = useAuth();
|
|
{hasPermission('admin') && <Button>删除用户</Button>}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 页面导航与路由
|
|
|
|
### 3.1 路由配置
|
|
|
|
```typescript
|
|
// router.tsx
|
|
const routes = [
|
|
{ path: '/login', element: <LoginPage /> },
|
|
{
|
|
path: '/',
|
|
element: <ProtectedRoute><Layout /></ProtectedRoute>,
|
|
children: [
|
|
{ path: '', element: <Navigate to="/dashboard" /> },
|
|
{ path: 'dashboard', element: <DashboardPage /> },
|
|
{ path: 'projects', element: <ProjectListPage /> },
|
|
{ path: 'projects/:id', element: <ProjectDetailPage /> },
|
|
{ path: 'users', element: <UserListPage />, meta: { requiresRole: 'admin' } },
|
|
{ path: 'statistics', element: <StatisticsPage /> },
|
|
],
|
|
},
|
|
];
|
|
```
|
|
|
|
### 3.2 面包屑导航
|
|
|
|
**实现方式**:
|
|
```typescript
|
|
// components/Breadcrumb.tsx
|
|
const Breadcrumb = () => {
|
|
const location = useLocation();
|
|
const breadcrumbItems = generateBreadcrumb(location.pathname);
|
|
return <AntdBreadcrumb items={breadcrumbItems} />;
|
|
};
|
|
```
|
|
|
|
**面包屑数据结构**:
|
|
```typescript
|
|
interface BreadcrumbItem {
|
|
label: string;
|
|
path?: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 登录模块
|
|
|
|
### 4.1 页面交互流程
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ 1. 用户输入用户名和密码 │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 2. 前端验证:检查必填字段 │
|
|
│ - 用户名不能为空 │
|
|
│ - 密码不能为空 │
|
|
└────────────┬──────────────────────────────┘
|
|
│ 验证通过
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 3. 调用登录API: POST /auth/login │
|
|
│ 请求体: { username, password } │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 4. API响应处理 │
|
|
│ - 成功: 存储Token,跳转首页 │
|
|
│ - 失败: 显示错误提示 │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 4.2 表单验证规则
|
|
|
|
```typescript
|
|
// utils/validate.ts
|
|
export const loginValidationSchema = {
|
|
username: {
|
|
required: '请输入用户名',
|
|
minLength: { value: 3, message: '用户名至少3个字符' },
|
|
},
|
|
password: {
|
|
required: '请输入密码',
|
|
minLength: { value: 6, message: '密码至少6个字符' },
|
|
},
|
|
};
|
|
```
|
|
|
|
### 4.3 记住密码功能
|
|
|
|
**实现逻辑**:
|
|
```typescript
|
|
// 本地存储加密的密码(可选)
|
|
const savePassword = (username: string, password: string) => {
|
|
const encrypted = btoa(`${username}:${password}`);
|
|
localStorage.setItem('savedCredentials', encrypted);
|
|
};
|
|
|
|
const loadSavedPassword = () => {
|
|
const saved = localStorage.getItem('savedCredentials');
|
|
if (saved) {
|
|
const decoded = atob(saved);
|
|
const [username, password] = decoded.split(':');
|
|
return { username, password };
|
|
}
|
|
return null;
|
|
};
|
|
```
|
|
|
|
### 4.4 登录状态
|
|
|
|
**Loading状态**:
|
|
- 点击登录按钮后,按钮显示loading状态
|
|
- 禁用登录按钮,防止重复提交
|
|
|
|
**错误提示**:
|
|
- 用户名或密码错误:显示红色提示
|
|
- 网络错误:显示网络错误提示
|
|
|
|
### 4.5 API调用
|
|
|
|
```typescript
|
|
// api/auth.ts
|
|
export const login = (data: LoginRequest) => {
|
|
return axios.post('/auth/login', data);
|
|
};
|
|
```
|
|
|
|
**请求参数**:
|
|
```typescript
|
|
interface LoginRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
```
|
|
|
|
**响应数据**:
|
|
```typescript
|
|
interface LoginResponse {
|
|
token: string;
|
|
token_type: string;
|
|
user: UserInfo;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 仪表盘模块
|
|
|
|
### 5.1 页面加载流程
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ 1. 页面初始化 │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 2. 并行调用API: │
|
|
│ - GET /projects/statistics │
|
|
│ - GET /projects?page=1&page_size=5 │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 3. 数据加载完成,渲染统计卡片和图表 │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 5.2 统计数据获取
|
|
|
|
**API调用**:
|
|
```typescript
|
|
// api/statistics.ts
|
|
export const getBasicStatistics = (params?: StatisticsFilter) => {
|
|
return axios.get('/projects/statistics', { params });
|
|
};
|
|
```
|
|
|
|
**响应数据**:
|
|
```typescript
|
|
interface StatisticsResponse {
|
|
total_count: number;
|
|
total_investment: number;
|
|
total_contract_amount: number;
|
|
total_settlement_amount: number;
|
|
total_receipt_amount: number;
|
|
total_payment_amount: number;
|
|
avg_receipt_completion_rate: number;
|
|
avg_payment_completion_rate: number;
|
|
avg_cumulative_progress: number;
|
|
}
|
|
```
|
|
|
|
### 5.3 统计卡片渲染
|
|
|
|
**卡片数据映射**:
|
|
```typescript
|
|
const statCards = [
|
|
{
|
|
label: '项目总数',
|
|
value: statistics.total_count,
|
|
icon: '📊',
|
|
},
|
|
{
|
|
label: '进行中',
|
|
value: calculateInProgressCount(statistics),
|
|
icon: '🔄',
|
|
},
|
|
{
|
|
label: '已完成',
|
|
value: calculateCompletedCount(statistics),
|
|
icon: '✅',
|
|
},
|
|
{
|
|
label: '总合同金额(万元)',
|
|
value: formatNumber(statistics.total_contract_amount),
|
|
icon: '💰',
|
|
},
|
|
];
|
|
```
|
|
|
|
### 5.4 项目趋势图
|
|
|
|
**数据获取**:
|
|
```typescript
|
|
export const getTimelineStatistics = (params: TimelineParams) => {
|
|
return axios.get('/projects/statistics/timeline', { params });
|
|
};
|
|
```
|
|
|
|
**图表配置**:
|
|
```typescript
|
|
const chartOption = {
|
|
xAxis: {
|
|
type: 'category',
|
|
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
},
|
|
series: [
|
|
{
|
|
name: '项目数量',
|
|
type: 'bar',
|
|
data: [20, 30, 40, 35, 50, 45],
|
|
},
|
|
{
|
|
name: '合同金额',
|
|
type: 'line',
|
|
yAxisIndex: 1,
|
|
data: [1000, 1500, 2000, 1800, 2500, 2200],
|
|
},
|
|
],
|
|
};
|
|
```
|
|
|
|
### 5.5 最近项目列表
|
|
|
|
**数据获取**:
|
|
```typescript
|
|
export const getRecentProjects = () => {
|
|
return axios.get('/projects', {
|
|
params: {
|
|
page: 1,
|
|
page_size: 5,
|
|
sort_by: 'created_at',
|
|
sort_order: 'desc',
|
|
},
|
|
});
|
|
};
|
|
```
|
|
|
|
**点击交互**:
|
|
```typescript
|
|
const handleProjectClick = (projectId: number) => {
|
|
navigate(`/projects/${projectId}`);
|
|
};
|
|
```
|
|
|
|
### 5.6 刷新机制
|
|
|
|
**自动刷新**:
|
|
- 每5分钟自动刷新统计数据
|
|
- 用户手动刷新:点击刷新按钮
|
|
|
|
**实现**:
|
|
```typescript
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
refetchStatistics();
|
|
}, 5 * 60 * 1000); // 5分钟
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 项目管理模块
|
|
|
|
### 6.1 项目列表页
|
|
|
|
#### 6.1.1 页面加载流程
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ 1. 页面初始化,加载项目列表 │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 2. 调用API: GET /projects │
|
|
│ 查询参数: page, page_size, 筛选条件 │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 3. 渲染表格,显示分页 │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 6.1.2 搜索功能
|
|
|
|
**实时搜索**:
|
|
```typescript
|
|
// 防抖处理,避免频繁请求
|
|
const debouncedSearch = useDebounce(keyword, 500);
|
|
|
|
useEffect(() => {
|
|
if (debouncedSearch) {
|
|
fetchProjects({ keyword: debouncedSearch });
|
|
}
|
|
}, [debouncedSearch]);
|
|
```
|
|
|
|
**搜索字段**:
|
|
- 项目名称
|
|
- 合同编号
|
|
- 业主单位
|
|
|
|
#### 6.1.3 筛选功能
|
|
|
|
**筛选条件**:
|
|
```typescript
|
|
interface ProjectFilter {
|
|
project_no?: string;
|
|
engineering_type?: string;
|
|
project_department?: string;
|
|
signing_date_start?: string;
|
|
signing_date_end?: string;
|
|
contract_amount_min?: number;
|
|
contract_amount_max?: number;
|
|
keyword?: string;
|
|
}
|
|
```
|
|
|
|
**筛选交互**:
|
|
1. 选择筛选条件
|
|
2. 点击"筛选"按钮或按Enter键
|
|
3. 调用API获取筛选后的数据
|
|
4. 更新表格显示
|
|
|
|
**清空筛选**:
|
|
```typescript
|
|
const handleClearFilters = () => {
|
|
setFilters({});
|
|
fetchProjects({ page: 1 });
|
|
};
|
|
```
|
|
|
|
#### 6.1.4 排序功能
|
|
|
|
**点击表头排序**:
|
|
```typescript
|
|
const handleSort = (field: string) => {
|
|
const newOrder = currentSort.field === field
|
|
? currentSort.order === 'asc' ? 'desc' : 'asc'
|
|
: 'asc';
|
|
setSort({ field, order: newOrder });
|
|
fetchProjects({
|
|
sort_by: field,
|
|
sort_order: newOrder,
|
|
});
|
|
};
|
|
```
|
|
|
|
**排序字段**:
|
|
- 签订日期 (signing_date)
|
|
- 合同金额 (contract_amount)
|
|
- 创建时间 (created_at)
|
|
|
|
#### 6.1.5 分页功能
|
|
|
|
**分页参数**:
|
|
```typescript
|
|
interface PaginationParams {
|
|
page: number;
|
|
page_size: number;
|
|
}
|
|
```
|
|
|
|
**分页交互**:
|
|
```typescript
|
|
const handlePageChange = (page: number) => {
|
|
fetchProjects({ page });
|
|
// 滚动到表格顶部
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
```
|
|
|
|
#### 6.1.6 新建项目
|
|
|
|
**按钮点击**:
|
|
```typescript
|
|
const handleCreateProject = () => {
|
|
// 打开新建项目模态框
|
|
setModalVisible(true);
|
|
setModalMode('create');
|
|
};
|
|
```
|
|
|
|
**模态框表单**:
|
|
```typescript
|
|
const [form] = Form.useForm();
|
|
|
|
const handleSubmit = async (values: ProjectFormData) => {
|
|
try {
|
|
// 表单验证
|
|
await form.validateFields();
|
|
|
|
// 调用API创建项目
|
|
await createProject(values);
|
|
|
|
// 关闭模态框
|
|
setModalVisible(false);
|
|
|
|
// 刷新列表
|
|
refetchProjects();
|
|
|
|
// 显示成功提示
|
|
message.success('项目创建成功');
|
|
} catch (error) {
|
|
// 显示错误提示
|
|
message.error('创建失败,请重试');
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 6.1.7 查看项目详情
|
|
|
|
**点击"查看"按钮**:
|
|
```typescript
|
|
const handleViewProject = (projectId: number) => {
|
|
navigate(`/projects/${projectId}`);
|
|
};
|
|
```
|
|
|
|
#### 6.1.8 编辑项目
|
|
|
|
**点击"编辑"按钮**:
|
|
```typescript
|
|
const handleEditProject = async (projectId: number) => {
|
|
try {
|
|
// 获取项目详情
|
|
const project = await getProjectDetail(projectId);
|
|
|
|
// 填充表单
|
|
form.setFieldsValue(project);
|
|
|
|
// 打开编辑模态框
|
|
setModalVisible(true);
|
|
setModalMode('edit');
|
|
setEditingId(projectId);
|
|
} catch (error) {
|
|
message.error('获取项目信息失败');
|
|
}
|
|
};
|
|
```
|
|
|
|
**权限控制**:
|
|
- 市场部用户:只能编辑自己创建的项目
|
|
- 其他部门用户:只能编辑财务、成本等字段
|
|
|
|
#### 6.1.9 删除项目
|
|
|
|
**点击"删除"按钮**:
|
|
```typescript
|
|
const handleDeleteProject = (projectId: number) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '确定要删除该项目吗?此操作不可恢复。',
|
|
onOk: async () => {
|
|
try {
|
|
await deleteProject(projectId);
|
|
message.success('项目删除成功');
|
|
refetchProjects();
|
|
} catch (error) {
|
|
message.error('删除失败,请重试');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
```
|
|
|
|
**权限控制**:
|
|
- 市场部用户:只能删除自己创建的项目
|
|
- 管理员:可以删除所有项目
|
|
|
|
### 6.2 项目详情页
|
|
|
|
#### 6.2.1 页面加载流程
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ 1. 页面初始化,从URL获取项目ID │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 2. 调用API: GET /projects/:id │
|
|
└────────────┬──────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────┐
|
|
│ 3. 渲染项目详情,按分组显示 │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 6.2.2 数据获取
|
|
|
|
**API调用**:
|
|
```typescript
|
|
export const getProjectDetail = (id: number) => {
|
|
return axios.get(`/projects/${id}`);
|
|
};
|
|
```
|
|
|
|
#### 6.2.3 信息分组显示
|
|
|
|
**分组配置**:
|
|
```typescript
|
|
const infoGroups = [
|
|
{
|
|
title: '基本信息',
|
|
fields: [
|
|
{ label: '合同编号', key: 'project_no' },
|
|
{ label: '项目名称', key: 'name' },
|
|
{ label: '工程类别', key: 'engineering_type', render: renderTag },
|
|
{ label: '合同金额', key: 'contract_amount', render: formatMoney },
|
|
],
|
|
},
|
|
{
|
|
title: '项目时间',
|
|
fields: [
|
|
{ label: '签订日期', key: 'signing_date', render: formatDate },
|
|
{ label: '开工日期', key: 'start_date', render: formatDate },
|
|
{ label: '计划竣工日期', key: 'planned_end_date', render: formatDate },
|
|
{ label: '实际竣工日期', key: 'actual_end_date', render: formatDate },
|
|
],
|
|
},
|
|
// ... 其他分组
|
|
];
|
|
```
|
|
|
|
#### 6.2.4 编辑项目
|
|
|
|
**权限控制**:
|
|
```typescript
|
|
const canEdit = () => {
|
|
if (user.role === 'admin') return true;
|
|
if (user.role === 'market' && project.created_by === user.id) return true;
|
|
if (user.role === 'other') return 'partial'; // 部分字段可编辑
|
|
return false;
|
|
};
|
|
```
|
|
|
|
**表单字段权限**:
|
|
- 市场部用户:所有字段可编辑
|
|
- 其他部门用户:只能编辑成本、财务、进度相关字段
|
|
- 基础信息字段(合同编号、项目名称等)设置为只读
|
|
|
|
#### 6.2.5 删除项目
|
|
|
|
**二次确认**:
|
|
```typescript
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: (
|
|
<div>
|
|
<p>确定要删除该项目吗?</p>
|
|
<p>项目名称:{project.name}</p>
|
|
<p>合同编号:{project.project_no}</p>
|
|
<p style={{ color: '#ff4d4f' }}>此操作不可恢复!</p>
|
|
</div>
|
|
),
|
|
okText: '确认删除',
|
|
okType: 'danger',
|
|
onOk: handleDelete,
|
|
});
|
|
```
|
|
|
|
#### 6.2.6 返回列表
|
|
|
|
```typescript
|
|
const handleBack = () => {
|
|
navigate('/projects');
|
|
};
|
|
```
|
|
|
|
### 6.3 项目表单
|
|
|
|
#### 6.3.1 表单字段验证
|
|
|
|
**必填字段**:
|
|
```typescript
|
|
const requiredFields = {
|
|
project_no: '合同编号',
|
|
name: '项目名称',
|
|
engineering_type: '工程类别',
|
|
contract_amount: '合同金额',
|
|
};
|
|
```
|
|
|
|
**验证规则**:
|
|
```typescript
|
|
export const projectValidationRules = {
|
|
project_no: [
|
|
{ required: true, message: '请输入合同编号' },
|
|
{ pattern: /^[A-Z0-9]+$/, message: '合同编号格式不正确' },
|
|
],
|
|
name: [
|
|
{ required: true, message: '请输入项目名称' },
|
|
{ min: 2, message: '项目名称至少2个字符' },
|
|
],
|
|
engineering_type: [
|
|
{ required: true, message: '请选择工程类别' },
|
|
],
|
|
contract_amount: [
|
|
{ required: true, message: '请输入合同金额' },
|
|
{ type: 'number', min: 0, message: '合同金额必须大于0' },
|
|
],
|
|
signing_date: [
|
|
{ required: true, message: '请选择签订日期' },
|
|
],
|
|
// ... 其他字段验证
|
|
};
|
|
```
|
|
|
|
#### 6.3.2 表单提交
|
|
|
|
**提交前验证**:
|
|
```typescript
|
|
const handleSubmit = async () => {
|
|
try {
|
|
// 验证所有字段
|
|
const values = await form.validateFields();
|
|
|
|
// 格式化数据
|
|
const formattedData = formatProjectData(values);
|
|
|
|
// 提交API
|
|
if (mode === 'create') {
|
|
await createProject(formattedData);
|
|
} else {
|
|
await updateProject(editingId, formattedData);
|
|
}
|
|
|
|
// 成功处理
|
|
message.success(mode === 'create' ? '项目创建成功' : '项目更新成功');
|
|
handleClose();
|
|
} catch (error) {
|
|
// 错误处理
|
|
if (error.errorFields) {
|
|
// 表单验证错误
|
|
message.error('请检查表单填写是否正确');
|
|
} else {
|
|
// API错误
|
|
message.error('操作失败,请重试');
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 6.3.3 表单字段分组
|
|
|
|
**分组配置**:
|
|
```typescript
|
|
const formGroups = [
|
|
{
|
|
title: '基本信息',
|
|
fields: ['project_no', 'name', 'engineering_type', 'contract_amount'],
|
|
},
|
|
{
|
|
title: '项目时间',
|
|
fields: ['signing_date', 'start_date', 'planned_end_date', 'actual_end_date'],
|
|
},
|
|
// ... 其他分组
|
|
];
|
|
```
|
|
|
|
#### 6.3.4 动态字段显示
|
|
|
|
**根据角色显示/隐藏字段**:
|
|
```typescript
|
|
const getVisibleFields = (role: string, mode: 'create' | 'edit') => {
|
|
const fields = allFields;
|
|
|
|
// 其他部门用户编辑时,隐藏基础信息字段
|
|
if (role === 'other' && mode === 'edit') {
|
|
return fields.filter(field => !isBaseField(field.key));
|
|
}
|
|
|
|
return fields;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 用户管理模块
|
|
|
|
### 7.1 权限控制
|
|
|
|
**页面级权限**:
|
|
```typescript
|
|
// 只有管理员可以访问用户管理页面
|
|
const UserManagementPage = () => {
|
|
const { user, hasPermission } = useAuth();
|
|
|
|
if (!hasPermission('admin')) {
|
|
return <div>您没有访问该页面的权限</div>;
|
|
}
|
|
|
|
return <UserList />;
|
|
};
|
|
```
|
|
|
|
### 7.2 用户列表
|
|
|
|
#### 7.2.1 数据获取
|
|
|
|
**API调用**:
|
|
```typescript
|
|
export const getUsers = (params: UserListParams) => {
|
|
return axios.get('/users', { params });
|
|
};
|
|
```
|
|
|
|
**查询参数**:
|
|
```typescript
|
|
interface UserListParams {
|
|
page: number;
|
|
page_size: number;
|
|
department?: string;
|
|
role?: string;
|
|
keyword?: string;
|
|
}
|
|
```
|
|
|
|
#### 7.2.2 搜索用户
|
|
|
|
**搜索字段**:
|
|
- 用户名
|
|
- 真实姓名
|
|
- 邮箱
|
|
|
|
#### 7.2.3 筛选用户
|
|
|
|
**筛选条件**:
|
|
- 部门
|
|
- 角色
|
|
|
|
### 7.3 新建用户
|
|
|
|
#### 7.3.1 表单字段
|
|
|
|
```typescript
|
|
interface CreateUserForm {
|
|
username: string; // 必填,唯一
|
|
password: string; // 必填,最少6位
|
|
real_name: string; // 必填
|
|
department: string; // 必填
|
|
role: string; // 必填:admin/market/other
|
|
email?: string; // 可选,唯一
|
|
phone?: string; // 可选
|
|
}
|
|
```
|
|
|
|
#### 7.3.2 表单验证
|
|
|
|
```typescript
|
|
export const userValidationRules = {
|
|
username: [
|
|
{ required: true, message: '请输入用户名' },
|
|
{ min: 3, message: '用户名至少3个字符' },
|
|
{ max: 20, message: '用户名最多20个字符' },
|
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
|
|
],
|
|
password: [
|
|
{ required: true, message: '请输入密码' },
|
|
{ min: 6, message: '密码至少6个字符' },
|
|
{ max: 20, message: '密码最多20个字符' },
|
|
],
|
|
real_name: [
|
|
{ required: true, message: '请输入真实姓名' },
|
|
{ min: 2, message: '真实姓名至少2个字符' },
|
|
],
|
|
email: [
|
|
{ type: 'email', message: '邮箱格式不正确' },
|
|
],
|
|
phone: [
|
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
|
|
],
|
|
};
|
|
```
|
|
|
|
#### 7.3.3 提交处理
|
|
|
|
```typescript
|
|
const handleCreateUser = async (values: CreateUserForm) => {
|
|
try {
|
|
await createUser(values);
|
|
message.success('用户创建成功');
|
|
refetchUsers();
|
|
handleClose();
|
|
} catch (error) {
|
|
if (error.response?.data?.error_code === '2002') {
|
|
message.error('用户名已存在');
|
|
} else if (error.response?.data?.error_code === '1001') {
|
|
message.error('邮箱已被使用');
|
|
} else {
|
|
message.error('创建失败,请重试');
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.4 编辑用户
|
|
|
|
#### 7.4.1 加载用户信息
|
|
|
|
```typescript
|
|
const handleEditUser = async (userId: number) => {
|
|
try {
|
|
const user = await getUserDetail(userId);
|
|
form.setFieldsValue(user);
|
|
setModalVisible(true);
|
|
setEditingId(userId);
|
|
} catch (error) {
|
|
message.error('获取用户信息失败');
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 7.4.2 用户名只读
|
|
|
|
```typescript
|
|
<Form.Item
|
|
name="username"
|
|
label="用户名"
|
|
rules={userValidationRules.username}
|
|
>
|
|
<Input disabled={mode === 'edit'} />
|
|
</Form.Item>
|
|
```
|
|
|
|
### 7.5 删除用户
|
|
|
|
```typescript
|
|
const handleDeleteUser = (userId: number) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '确定要删除该用户吗?',
|
|
onOk: async () => {
|
|
try {
|
|
await deleteUser(userId);
|
|
message.success('用户删除成功');
|
|
refetchUsers();
|
|
} catch (error) {
|
|
message.error('删除失败,请重试');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
```
|
|
|
|
### 7.6 重置密码
|
|
|
|
```typescript
|
|
const handleResetPassword = (userId: number) => {
|
|
Modal.confirm({
|
|
title: '重置密码',
|
|
content: (
|
|
<>
|
|
<p>确定要重置该用户的密码吗?</p>
|
|
<Form.Item label="新密码" required>
|
|
<Input.Password />
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
onOk: async (values) => {
|
|
try {
|
|
await resetUserPassword(userId, values.newPassword);
|
|
message.success('密码重置成功');
|
|
} catch (error) {
|
|
message.error('密码重置失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 项目统计模块
|
|
|
|
### 8.1 基础统计
|
|
|
|
#### 8.1.1 数据获取
|
|
|
|
```typescript
|
|
export const getBasicStatistics = (filters?: StatisticsFilter) => {
|
|
return axios.get('/projects/statistics', { params: filters });
|
|
};
|
|
```
|
|
|
|
#### 8.1.2 统计卡片渲染
|
|
|
|
```typescript
|
|
const statCards = [
|
|
{
|
|
title: '项目总数',
|
|
value: statistics.total_count,
|
|
icon: '📊',
|
|
color: '#1890ff',
|
|
},
|
|
{
|
|
title: '总投资金额(万元)',
|
|
value: formatNumber(statistics.total_investment),
|
|
icon: '💰',
|
|
color: '#52c41a',
|
|
},
|
|
{
|
|
title: '总合同金额(万元)',
|
|
value: formatNumber(statistics.total_contract_amount),
|
|
icon: '💵',
|
|
color: '#1890ff',
|
|
},
|
|
{
|
|
title: '总收款金额(万元)',
|
|
value: formatNumber(statistics.total_receipt_amount),
|
|
icon: '📥',
|
|
color: '#52c41a',
|
|
},
|
|
{
|
|
title: '总付款金额(万元)',
|
|
value: formatNumber(statistics.total_payment_amount),
|
|
icon: '📤',
|
|
color: '#fa8c16',
|
|
},
|
|
{
|
|
title: '平均收款完成率',
|
|
value: formatPercentage(statistics.avg_receipt_completion_rate),
|
|
icon: '📈',
|
|
color: '#1890ff',
|
|
},
|
|
{
|
|
title: '平均付款完成率',
|
|
value: formatPercentage(statistics.avg_payment_completion_rate),
|
|
icon: '📉',
|
|
color: '#52c41a',
|
|
},
|
|
{
|
|
title: '平均项目进度',
|
|
value: formatPercentage(statistics.avg_cumulative_progress),
|
|
icon: '⚡',
|
|
color: '#1890ff',
|
|
},
|
|
];
|
|
```
|
|
|
|
### 8.2 分组统计
|
|
|
|
#### 8.2.1 数据获取
|
|
|
|
```typescript
|
|
export const getGroupStatistics = (params: GroupStatisticsParams) => {
|
|
return axios.get('/projects/statistics/group', { params });
|
|
};
|
|
```
|
|
|
|
**参数**:
|
|
```typescript
|
|
interface GroupStatisticsParams {
|
|
group_by: 'engineering_type' | 'project_department';
|
|
signing_date_start?: string;
|
|
signing_date_end?: string;
|
|
}
|
|
```
|
|
|
|
#### 8.2.2 饼图渲染
|
|
|
|
```typescript
|
|
const pieChartOption = {
|
|
tooltip: {
|
|
trigger: 'item',
|
|
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
|
},
|
|
series: [
|
|
{
|
|
name: '工程类别',
|
|
type: 'pie',
|
|
radius: '50%',
|
|
data: groupStatistics.map(item => ({
|
|
value: item.count,
|
|
name: item.engineering_type,
|
|
})),
|
|
},
|
|
],
|
|
};
|
|
```
|
|
|
|
#### 8.2.3 柱状图渲染
|
|
|
|
```typescript
|
|
const barChartOption = {
|
|
xAxis: {
|
|
type: 'category',
|
|
data: groupStatistics.map(item => item.engineering_type),
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
},
|
|
series: [
|
|
{
|
|
name: '合同金额',
|
|
type: 'bar',
|
|
data: groupStatistics.map(item => item.total_contract_amount),
|
|
},
|
|
],
|
|
};
|
|
```
|
|
|
|
### 8.3 时间维度统计
|
|
|
|
#### 8.3.1 数据获取
|
|
|
|
```typescript
|
|
export const getTimelineStatistics = (params: TimelineParams) => {
|
|
return axios.get('/projects/statistics/timeline', { params });
|
|
};
|
|
```
|
|
|
|
**参数**:
|
|
```typescript
|
|
interface TimelineParams {
|
|
time_field: 'signing_date' | 'start_date' | 'planned_end_date';
|
|
group_by: 'day' | 'month' | 'year';
|
|
start_date?: string;
|
|
end_date?: string;
|
|
engineering_type?: string;
|
|
}
|
|
```
|
|
|
|
#### 8.3.2 折线图渲染
|
|
|
|
```typescript
|
|
const lineChartOption = {
|
|
xAxis: {
|
|
type: 'category',
|
|
data: timelineStatistics.map(item => item.month),
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
},
|
|
series: [
|
|
{
|
|
name: '项目数量',
|
|
type: 'line',
|
|
data: timelineStatistics.map(item => item.count),
|
|
},
|
|
{
|
|
name: '合同金额',
|
|
type: 'line',
|
|
yAxisIndex: 1,
|
|
data: timelineStatistics.map(item => item.total_contract_amount),
|
|
},
|
|
],
|
|
};
|
|
```
|
|
|
|
### 8.4 筛选功能
|
|
|
|
#### 8.4.1 筛选条件
|
|
|
|
```typescript
|
|
interface StatisticsFilter {
|
|
engineering_type?: string;
|
|
signing_date_start?: string;
|
|
signing_date_end?: string;
|
|
contract_amount_min?: number;
|
|
contract_amount_max?: number;
|
|
}
|
|
```
|
|
|
|
#### 8.4.2 筛选交互
|
|
|
|
```typescript
|
|
const handleFilterChange = (filters: StatisticsFilter) => {
|
|
setFilters(filters);
|
|
// 重新获取统计数据
|
|
refetchStatistics(filters);
|
|
};
|
|
```
|
|
|
|
### 8.5 导出报表
|
|
|
|
```typescript
|
|
const handleExport = async () => {
|
|
try {
|
|
// 调用导出API
|
|
const blob = await exportStatistics(filters);
|
|
|
|
// 创建下载链接
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `项目统计_${formatDate(new Date(), 'YYYY-MM-DD')}.xlsx`;
|
|
a.click();
|
|
|
|
// 释放资源
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
message.success('报表导出成功');
|
|
} catch (error) {
|
|
message.error('导出失败,请重试');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 通用组件与工具
|
|
|
|
### 9.1 布局组件
|
|
|
|
#### 9.1.1 顶部导航栏
|
|
|
|
```typescript
|
|
// components/Layout/Header.tsx
|
|
const Header = () => {
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const handleLogout = () => {
|
|
Modal.confirm({
|
|
title: '确认退出',
|
|
content: '确定要退出登录吗?',
|
|
onOk: async () => {
|
|
await logout();
|
|
navigate('/login');
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="layout-header">
|
|
<div className="logo">海洋项目管理系统</div>
|
|
<div className="user-info">
|
|
<Avatar>{user.real_name?.[0]}</Avatar>
|
|
<span>{user.real_name} ({user.department})</span>
|
|
<Button onClick={handleLogout}>登出</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
#### 9.1.2 侧边栏
|
|
|
|
```typescript
|
|
// components/Layout/Sidebar.tsx
|
|
const Sidebar = () => {
|
|
const location = useLocation();
|
|
const { user } = useAuth();
|
|
|
|
const menuItems = [
|
|
{ key: '/dashboard', label: '仪表盘', icon: <DashboardOutlined /> },
|
|
{ key: '/projects', label: '项目管理', icon: <FileTextOutlined /> },
|
|
...(user.role === 'admin' ? [
|
|
{ key: '/users', label: '用户管理', icon: <UserOutlined /> },
|
|
] : []),
|
|
{ key: '/statistics', label: '项目统计', icon: <BarChartOutlined /> },
|
|
];
|
|
|
|
return (
|
|
<div className="layout-sider">
|
|
<Menu
|
|
selectedKeys={[location.pathname]}
|
|
mode="inline"
|
|
items={menuItems}
|
|
onClick={handleMenuClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 9.2 表格组件
|
|
|
|
#### 9.2.1 可配置表格
|
|
|
|
```typescript
|
|
// components/Table/ProTable.tsx
|
|
interface ProTableProps<T> {
|
|
columns: ColumnType<T>[];
|
|
data: T[];
|
|
loading?: boolean;
|
|
pagination?: PaginationProps;
|
|
onRow?: (record: T) => { onClick?: () => void };
|
|
}
|
|
|
|
const ProTable = <T extends Record<string, any>>({
|
|
columns,
|
|
data,
|
|
loading,
|
|
pagination,
|
|
onRow,
|
|
}: ProTableProps<T>) => {
|
|
return (
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
pagination={pagination}
|
|
onRow={onRow}
|
|
rowKey="id"
|
|
/>
|
|
);
|
|
};
|
|
```
|
|
|
|
#### 9.2.2 自定义列渲染
|
|
|
|
```typescript
|
|
const projectColumns = [
|
|
{
|
|
title: '合同编号',
|
|
dataIndex: 'project_no',
|
|
key: 'project_no',
|
|
sorter: true,
|
|
},
|
|
{
|
|
title: '工程类别',
|
|
dataIndex: 'engineering_type',
|
|
key: 'engineering_type',
|
|
render: (type: string) => <Tag color="blue">{type}</Tag>,
|
|
},
|
|
{
|
|
title: '合同金额',
|
|
dataIndex: 'contract_amount',
|
|
key: 'contract_amount',
|
|
render: (amount: number) => `${amount.toFixed(2)} 万元`,
|
|
align: 'right',
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button type="link" onClick={() => handleView(record)}>查看</Button>
|
|
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
|
<Button type="link" danger onClick={() => handleDelete(record)}>删除</Button>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
```
|
|
|
|
### 9.3 表单组件
|
|
|
|
#### 9.3.1 日期选择器
|
|
|
|
```typescript
|
|
<Form.Item
|
|
name="signing_date"
|
|
label="签订日期"
|
|
rules={[{ required: true, message: '请选择签订日期' }]}
|
|
>
|
|
<DatePicker
|
|
style={{ width: '100%' }}
|
|
format="YYYY-MM-DD"
|
|
placeholder="请选择日期"
|
|
/>
|
|
</Form.Item>
|
|
```
|
|
|
|
#### 9.3.2 数字输入框
|
|
|
|
```typescript
|
|
<Form.Item
|
|
name="contract_amount"
|
|
label="合同金额(万元)"
|
|
rules={[
|
|
{ required: true, message: '请输入合同金额' },
|
|
{ type: 'number', min: 0, message: '金额必须大于0' },
|
|
]}
|
|
>
|
|
<InputNumber
|
|
style={{ width: '100%' }}
|
|
min={0}
|
|
precision={2}
|
|
placeholder="请输入金额"
|
|
/>
|
|
</Form.Item>
|
|
```
|
|
|
|
### 9.4 标签组件
|
|
|
|
```typescript
|
|
// components/Tag/index.tsx
|
|
interface TagProps {
|
|
type?: 'blue' | 'green' | 'orange' | 'red' | 'gray';
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const Tag = ({ type = 'blue', children }: TagProps) => {
|
|
const colorMap = {
|
|
blue: { bg: '#e6f7ff', color: '#1890ff', border: '#91d5ff' },
|
|
green: { bg: '#f6ffed', color: '#52c41a', border: '#b7eb8f' },
|
|
orange: { bg: '#fff7e6', color: '#fa8c16', border: '#ffd591' },
|
|
red: { bg: '#fff1f0', color: '#ff4d4f', border: '#ffa39e' },
|
|
gray: { bg: '#f5f5f5', color: '#999999', border: '#d9d9d9' },
|
|
};
|
|
|
|
const style = colorMap[type];
|
|
|
|
return (
|
|
<span
|
|
style={{
|
|
display: 'inline-block',
|
|
padding: '2px 8px',
|
|
borderRadius: '2px',
|
|
fontSize: '12px',
|
|
lineHeight: 1.5,
|
|
background: style.bg,
|
|
color: style.color,
|
|
border: `1px solid ${style.border}`,
|
|
}}
|
|
>
|
|
{children}
|
|
</span>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 10. 状态管理
|
|
|
|
### 10.1 Redux Store配置
|
|
|
|
```typescript
|
|
// store/index.ts
|
|
import { configureStore } from '@reduxjs/toolkit';
|
|
import authReducer from './authSlice';
|
|
import projectReducer from './projectSlice';
|
|
import userReducer from './userSlice';
|
|
|
|
export const store = configureStore({
|
|
reducer: {
|
|
auth: authReducer,
|
|
projects: projectReducer,
|
|
users: userReducer,
|
|
},
|
|
});
|
|
|
|
export type RootState = ReturnType<typeof store.getState>;
|
|
export type AppDispatch = typeof store.dispatch;
|
|
```
|
|
|
|
### 10.2 认证状态管理
|
|
|
|
```typescript
|
|
// store/authSlice.ts
|
|
interface AuthState {
|
|
user: UserInfo | null;
|
|
token: string | null;
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
const authSlice = createSlice({
|
|
name: 'auth',
|
|
initialState: {
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false,
|
|
} as AuthState,
|
|
reducers: {
|
|
setCredentials: (state, action) => {
|
|
state.user = action.payload.user;
|
|
state.token = action.payload.token;
|
|
state.isAuthenticated = true;
|
|
},
|
|
clearCredentials: (state) => {
|
|
state.user = null;
|
|
state.token = null;
|
|
state.isAuthenticated = false;
|
|
},
|
|
},
|
|
});
|
|
|
|
export const { setCredentials, clearCredentials } = authSlice.actions;
|
|
```
|
|
|
|
### 10.3 项目状态管理
|
|
|
|
```typescript
|
|
// store/projectSlice.ts
|
|
interface ProjectState {
|
|
list: Project[];
|
|
detail: Project | null;
|
|
total: number;
|
|
loading: boolean;
|
|
}
|
|
|
|
const projectSlice = createSlice({
|
|
name: 'projects',
|
|
initialState: {
|
|
list: [],
|
|
detail: null,
|
|
total: 0,
|
|
loading: false,
|
|
} as ProjectState,
|
|
reducers: {
|
|
setProjects: (state, action) => {
|
|
state.list = action.payload.items;
|
|
state.total = action.payload.total;
|
|
},
|
|
setProjectDetail: (state, action) => {
|
|
state.detail = action.payload;
|
|
},
|
|
setLoading: (state, action) => {
|
|
state.loading = action.payload;
|
|
},
|
|
},
|
|
});
|
|
|
|
export const { setProjects, setProjectDetail, setLoading } = projectSlice.actions;
|
|
```
|
|
|
|
### 10.4 React Query集成
|
|
|
|
```typescript
|
|
// hooks/useProjects.ts
|
|
export const useProjects = (params: ProjectListParams) => {
|
|
return useQuery(
|
|
['projects', params],
|
|
() => getProjects(params),
|
|
{
|
|
keepPreviousData: true,
|
|
staleTime: 5 * 60 * 1000, // 5分钟
|
|
}
|
|
);
|
|
};
|
|
|
|
export const useProjectDetail = (id: number) => {
|
|
return useQuery(['project', id], () => getProjectDetail(id), {
|
|
enabled: !!id,
|
|
});
|
|
};
|
|
|
|
export const useCreateProject = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation(createProject, {
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries(['projects']);
|
|
message.success('项目创建成功');
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useUpdateProject = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation(updateProject, {
|
|
onSuccess: (data, variables) => {
|
|
queryClient.invalidateQueries(['projects']);
|
|
queryClient.invalidateQueries(['project', variables.id]);
|
|
message.success('项目更新成功');
|
|
},
|
|
});
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 11. 错误处理
|
|
|
|
### 11.1 API错误处理
|
|
|
|
```typescript
|
|
// utils/request.ts
|
|
axios.interceptors.response.use(
|
|
(response) => {
|
|
return response.data;
|
|
},
|
|
(error) => {
|
|
if (error.response) {
|
|
const { status, data } = error.response;
|
|
|
|
switch (status) {
|
|
case 400:
|
|
message.error(data.message || '参数错误');
|
|
break;
|
|
case 401:
|
|
message.error('登录已过期,请重新登录');
|
|
// 清除Token,跳转登录页
|
|
store.dispatch(clearCredentials());
|
|
window.location.href = '/login';
|
|
break;
|
|
case 403:
|
|
message.error('您没有权限执行此操作');
|
|
break;
|
|
case 404:
|
|
message.error('请求的资源不存在');
|
|
break;
|
|
case 500:
|
|
message.error('服务器错误,请稍后重试');
|
|
break;
|
|
default:
|
|
message.error('操作失败,请重试');
|
|
}
|
|
} else if (error.request) {
|
|
message.error('网络错误,请检查网络连接');
|
|
} else {
|
|
message.error('操作失败,请重试');
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
```
|
|
|
|
### 11.2 表单验证错误
|
|
|
|
```typescript
|
|
const handleSubmit = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
// 处理表单提交
|
|
} catch (error) {
|
|
if (error.errorFields) {
|
|
// 显示第一个错误字段的提示
|
|
const firstError = error.errorFields[0];
|
|
message.error(firstError.errors[0].message);
|
|
// 聚焦到错误字段
|
|
form.scrollToField(firstError.name);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 11.3 加载错误处理
|
|
|
|
```typescript
|
|
const { data, isLoading, error } = useProjects(params);
|
|
|
|
if (isLoading) {
|
|
return <Spin />;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="error-state">
|
|
<p>数据加载失败</p>
|
|
<Button onClick={() => refetch()}>重试</Button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. 性能优化
|
|
|
|
### 12.1 虚拟滚动
|
|
|
|
**适用于**:大数据量列表(1000+条)
|
|
|
|
```typescript
|
|
import { FixedSizeList } from 'react-window';
|
|
|
|
const ProjectList = ({ projects }) => {
|
|
return (
|
|
<FixedSizeList
|
|
height={600}
|
|
itemCount={projects.length}
|
|
itemSize={48}
|
|
width="100%"
|
|
>
|
|
{({ index, style }) => (
|
|
<div style={style}>
|
|
<ProjectItem project={projects[index]} />
|
|
</div>
|
|
)}
|
|
</FixedSizeList>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 12.2 防抖和节流
|
|
|
|
**防抖**:搜索输入
|
|
```typescript
|
|
import { useDebouncedValue } from 'usehooks-ts';
|
|
|
|
const ProjectSearch = () => {
|
|
const [keyword, setKeyword] = useState('');
|
|
const debouncedKeyword = useDebouncedValue(keyword, 500);
|
|
|
|
useEffect(() => {
|
|
if (debouncedKeyword) {
|
|
fetchProjects({ keyword: debouncedKeyword });
|
|
}
|
|
}, [debouncedKeyword]);
|
|
|
|
return <Input onChange={(e) => setKeyword(e.target.value)} />;
|
|
};
|
|
```
|
|
|
|
**节流**:滚动加载
|
|
```typescript
|
|
import { useThrottle } from 'usehooks-ts';
|
|
|
|
const InfiniteScroll = () => {
|
|
const handleScroll = useThrottle(() => {
|
|
if (isNearBottom()) {
|
|
loadMore();
|
|
}
|
|
}, 200);
|
|
|
|
return <div onScroll={handleScroll}>...</div>;
|
|
};
|
|
```
|
|
|
|
### 12.3 懒加载
|
|
|
|
**路由懒加载**:
|
|
```typescript
|
|
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
|
const ProjectList = lazy(() => import('./pages/ProjectList'));
|
|
|
|
const App = () => (
|
|
<Suspense fallback={<Spin />}>
|
|
<Routes>
|
|
<Route path="/dashboard" element={<Dashboard />} />
|
|
<Route path="/projects" element={<ProjectList />} />
|
|
</Routes>
|
|
</Suspense>
|
|
);
|
|
```
|
|
|
|
### 12.4 图片懒加载
|
|
|
|
```typescript
|
|
const LazyImage = ({ src, alt }) => {
|
|
const [loaded, setLoaded] = useState(false);
|
|
const imgRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setLoaded(true);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.1 }
|
|
);
|
|
|
|
if (imgRef.current) {
|
|
observer.observe(imgRef.current);
|
|
}
|
|
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={imgRef}>
|
|
{loaded ? <img src={src} alt={alt} /> : <Skeleton.Image />}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 12.5 缓存策略
|
|
|
|
**React Query缓存**:
|
|
```typescript
|
|
// 默认缓存时间:5分钟
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 5 * 60 * 1000,
|
|
cacheTime: 10 * 60 * 1000,
|
|
retry: 1,
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 附录
|
|
|
|
### A. API错误码对照表
|
|
|
|
| 错误码 | 说明 | HTTP状态码 | 处理方式 |
|
|
|--------|------|-----------|---------|
|
|
| 1001 | 参数验证失败 | 400 | 显示字段级错误提示 |
|
|
| 1002 | 用户名或密码错误 | 401 | 提示"用户名或密码错误" |
|
|
| 1003 | Token无效或过期 | 401 | 跳转登录页 |
|
|
| 2001 | 资源不存在 | 404 | 显示"资源不存在" |
|
|
| 2002 | 资源已存在 | 409 | 显示"已存在"提示 |
|
|
| 3001 | 权限不足 | 403 | 显示"无权限"提示 |
|
|
| 5000 | 服务器内部错误 | 500 | 显示"服务器错误" |
|
|
|
|
### B. 常用格式化函数
|
|
|
|
```typescript
|
|
// utils/format.ts
|
|
|
|
// 格式化金额
|
|
export const formatMoney = (amount: number): string => {
|
|
return amount.toLocaleString('zh-CN', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
};
|
|
|
|
// 格式化百分比
|
|
export const formatPercentage = (value: number): string => {
|
|
return `${(value * 100).toFixed(2)}%`;
|
|
};
|
|
|
|
// 格式化日期
|
|
export const formatDate = (date: string | Date): string => {
|
|
const d = new Date(date);
|
|
return d.toLocaleDateString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
});
|
|
};
|
|
|
|
// 格式化日期时间
|
|
export const formatDateTime = (date: string | Date): string => {
|
|
const d = new Date(date);
|
|
return d.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
```
|
|
|
|
### C. TypeScript类型定义
|
|
|
|
```typescript
|
|
// types/project.ts
|
|
export interface Project {
|
|
id: number;
|
|
project_no: string;
|
|
power_contract_no?: string;
|
|
name: string;
|
|
engineering_type: '基建' | '业扩' | '客户' | '营销' | '检修';
|
|
owner_unit?: string;
|
|
owner_contact?: string;
|
|
contract_amount: number;
|
|
signing_date?: string;
|
|
start_date?: string;
|
|
planned_end_date?: string;
|
|
actual_end_date?: string;
|
|
project_department?: string;
|
|
project_leader?: string;
|
|
payment_method?: string;
|
|
// ... 其他字段
|
|
}
|
|
|
|
export interface ProjectListParams {
|
|
page: number;
|
|
page_size: number;
|
|
project_no?: string;
|
|
engineering_type?: string;
|
|
project_department?: string;
|
|
signing_date_start?: string;
|
|
signing_date_end?: string;
|
|
contract_amount_min?: number;
|
|
contract_amount_max?: number;
|
|
keyword?: string;
|
|
sort_by?: string;
|
|
sort_order?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface ProjectFormData {
|
|
project_no: string;
|
|
name: string;
|
|
engineering_type: string;
|
|
contract_amount: number;
|
|
// ... 其他字段
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// types/user.ts
|
|
export interface UserInfo {
|
|
id: number;
|
|
username: string;
|
|
real_name: string;
|
|
department: string;
|
|
role: 'admin' | 'market' | 'other';
|
|
email?: string;
|
|
phone?: string;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export interface CreateUserForm {
|
|
username: string;
|
|
password: string;
|
|
real_name: string;
|
|
department: string;
|
|
role: string;
|
|
email?: string;
|
|
phone?: string;
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// types/statistics.ts
|
|
export interface StatisticsResponse {
|
|
total_count: number;
|
|
total_investment: number;
|
|
total_contract_amount: number;
|
|
total_settlement_amount: number;
|
|
total_receipt_amount: number;
|
|
total_payment_amount: number;
|
|
avg_receipt_completion_rate: number;
|
|
avg_payment_completion_rate: number;
|
|
avg_cumulative_progress: number;
|
|
}
|
|
|
|
export interface GroupStatisticsItem {
|
|
engineering_type: string;
|
|
count: number;
|
|
total_contract_amount: number;
|
|
total_receipt_amount: number;
|
|
total_payment_amount: number;
|
|
}
|
|
|
|
export interface TimelineStatisticsItem {
|
|
month: string;
|
|
count: number;
|
|
total_contract_amount: number;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**文档结束**
|
|
|
|
如有任何疑问,请参考:
|
|
- API文档:`docs/desigen/api.md`
|
|
- UI设计规范:`docs/desigen/ui-design-spec.md`
|
|
- 设计参考预览:`docs/desigen/design-preview.html`
|
|
|
|
加油!!!
|