# 海洋项目管理系统 - 产品交互文档（前端开发版）

## 文档说明

本文档面向前端开发人员，详细描述系统各功能模块的交互流程、状态管理、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`

加油！！！
